feat(ui-components): more new components (#1613)

This commit is contained in:
Kristaps Fabians Geikins
2023-06-13 09:33:10 +03:00
committed by GitHub
parent 2dd79d52a7
commit cf65cfd57b
31 changed files with 791 additions and 101 deletions
@@ -1,5 +1,5 @@
<template>
<LayoutPanel fancy-glow class="max-w-lg mx-auto w-full">
<LayoutPanel fancy-glow no-shadow class="max-w-lg mx-auto w-full">
<div class="space-y-4">
<div class="flex flex-col items-center space-y-2">
<h1
@@ -1,5 +1,5 @@
<template>
<LayoutPanel fancy-glow class="max-w-lg mx-auto w-full">
<LayoutPanel fancy-glow no-shadow class="max-w-lg mx-auto w-full">
<div class="space-y-4">
<div class="flex flex-col items-center space-y-2">
<h1
@@ -1,14 +1 @@
export type InfiniteLoaderState = {
/**
* Informs the component that this loading has been successful
*/
loaded: () => void
/**
* Informs the component that all of the data has been loaded successfully
*/
complete: () => void
/**
* Inform the component that this loading failed, the content of the `error` slot will be displayed
*/
error: () => void
}
export type { InfiniteLoaderState } from '@speckle/ui-components'
-1
View File
@@ -64,7 +64,6 @@
"nanoid": "^3.0.0",
"portal-vue": "^3.0.0",
"subscriptions-transport-ws": "^0.11.0",
"v3-infinite-loading": "^1.2.2",
"vee-validate": "^4.7.0",
"vue-advanced-cropper": "^2.8.8",
"vue-tippy": "^6.0.0",
+2 -2
View File
@@ -31,7 +31,7 @@ export const lightThemeVariables = {
/* focused primary color */
'--primary-focus': '#2563eb',
/* muted primary color */
'--primary-muted': '#3b82f60d',
'--primary-muted': '#e8eff8',
/* outline variations */
'--outline-1': '#3b82f6',
@@ -89,7 +89,7 @@ export const darkThemeVariables = {
/* focused primary color */
'--primary-focus': '#60a5fa',
/* muted primary color */
'--primary-muted': '#71717a0d',
'--primary-muted': '#1d1d20',
/* outline variations */
'--outline-1': '#a1a1aa',
+32
View File
@@ -6,3 +6,35 @@ Nuxt v3 module that sets up @speckle/ui-components auto-importing like any other
1. Make sure you've got `@speckle/ui-components` installed and set up
1. Install `@speckle/ui-components-nuxt` and add it to your nuxt modules in `nuxt.config.ts`
1. Add the following to your `build.transpile` array in your nuxt config:
```js
// nuxt.config.js
export default {
build: {
transpile: [
'@headlessui/vue',
/^@heroicons\/vue/,
'@vueuse/core',
'@vueuse/shared',
'@speckle/ui-components'
]
}
}
```
1. Add the following to your `vite.resolve.dedupe` array in your nuxt config:
```js
// nuxt.config.js
export default {
vite: {
resolve: {
dedupe: ['vee-validate']
}
}
}
```
This will ensure that some dependencies are transpiled properly so that they work correctly both during SSR & CSR.
-19
View File
@@ -13,25 +13,6 @@ Speckle UI component library built with Vue 3 and relying on the Speckle Tailwin
It's suggested that you also install the `@speckle/ui-components-nuxt` Nuxt module. It will ensure that all of the Vue components can be auto-imported like components in nuxt's `./components` directory. No need to import them manually anymore and you'll also get proper TS typing in your Vue templates out of the box!
Additionally you should add the following to your `build.transpile` array in your nuxt config:
```js
// nuxt.config.js
export default {
build: {
transpile: [
'@headlessui/vue',
/^@heroicons\/vue/,
'@vueuse/core',
'@vueuse/shared',
'@speckle/ui-components'
]
}
}
```
This will ensure that some dependencies are transpiled properly so that they work correctly both during SSR & CSR.
### Troubleshooting
#### Form validation doesn't work
+1
View File
@@ -45,6 +45,7 @@
"lodash": "^4.0.0",
"lodash-es": "^4.0.0",
"nanoid": "^3.0.0",
"v3-infinite-loading": "^1.2.2",
"vee-validate": "^4.7.0",
"vue-tippy": "^6.0.0"
},
@@ -0,0 +1,85 @@
import { wait } from '@speckle/shared'
import { Meta, StoryObj } from '@storybook/vue3'
import { computed, ref } from 'vue'
import InfiniteLoading from '~~/src/components/InfiniteLoading.vue'
import { InfiniteLoaderState } from '~~/src/helpers/global/components'
export default {
component: InfiniteLoading,
parameters: {
docs: {
description: {
component: 'Infinite loader built on top of v3-infinite-loading'
}
}
}
} as Meta
type FakePaginationItem = {
id: string
title: string
}
const buildStory = (
params?: Partial<{ throwError: boolean; allowRetry: boolean }>
): StoryObj => ({
render: (args) => ({
components: { InfiniteLoading },
setup() {
const itemsLimit = ref(5)
const items = ref([] as FakePaginationItem[])
const moreToLoad = computed(() => items.value.length < itemsLimit.value)
const loadMore = async () => {
await wait(1000)
if (params?.throwError) {
throw new Error('Simulated loading failure')
}
const newNumber = items.value.length + 1
items.value.push({
id: `id-${newNumber}`,
title: `Item #${newNumber}`
})
}
const infiniteLoad = async (state: InfiniteLoaderState) => {
if (!moreToLoad.value) return state.complete()
try {
await loadMore()
} catch (e) {
console.error(e)
state.error()
return
}
state.loaded()
if (!moreToLoad.value) {
state.complete()
}
}
return { args, infiniteLoad, items, moreToLoad, itemsLimit }
},
template: `
<div>
<div v-for="item in items" :key="item.id">
{{ item }}
</div>
<InfiniteLoading @infinite="infiniteLoad" v-bind="args"/>
</div>
`
}),
args: {
allowRetry: params?.allowRetry || false,
settings: {}
}
})
export const Default: StoryObj = buildStory()
export const WithError: StoryObj = buildStory({ throwError: true })
export const WithRetry: StoryObj = buildStory({ throwError: true, allowRetry: true })
@@ -31,8 +31,12 @@
<script setup lang="ts">
import InternalInfiniteLoading from 'v3-infinite-loading'
import { ExclamationTriangleIcon, CheckIcon } from '@heroicons/vue/24/outline'
import { InfiniteLoaderState } from '~~/lib/global/helpers/components'
import { InfiniteLoaderState } from '~~/src/helpers/global/components'
import { Nullable } from '@speckle/shared'
import CommonLoadingBar from '~~/src/components/common/loading/Bar.vue'
import { onMounted, ref } from 'vue'
import { isClient } from '@vueuse/core'
import FormButton from '~~/src/components/form/Button.vue'
defineEmits<{
(e: 'infinite', $state: InfiniteLoaderState): void
@@ -50,6 +54,9 @@ defineProps<{
identifier?: any
firstload?: boolean
}
/**
* Whether to allow retry and show a retry button when loading fails
*/
allowRetry?: boolean
}>()
@@ -57,7 +64,7 @@ const wrapper = ref(null as Nullable<HTMLElement>)
const initializeLoader = ref(false)
// This hack is necessary cause sometimes v3-infinite-loading initializes too early and doesnt trigger
if (process.client) {
if (isClient) {
onMounted(() => {
const int = setInterval(() => {
if (wrapper.value?.isConnected) {
@@ -0,0 +1,114 @@
import { Meta, StoryObj } from '@storybook/vue3'
import CommonAlert from '~~/src/components/common/Alert.vue'
export default {
component: CommonAlert,
argTypes: {
color: {
options: ['success', 'danger', 'warning', 'info'],
control: { type: 'select' }
}
}
} as Meta
export const Default: StoryObj = {
render: (args) => ({
components: { CommonAlert },
setup() {
return { args }
},
template: `
<CommonAlert v-bind="args">
<template #title>
Some title
</template>
<template #description>
Some description
</template>
</CommonAlert>
`
}),
args: {
color: 'success',
withDismiss: false,
actions: [
{ title: 'View', url: 'https://google.com' },
{ title: 'Open', onClick: () => console.log('click') }
]
}
}
export const WithDismisser = {
...Default,
args: {
...Default.args,
withDismiss: true
}
}
export const WithoutDescription: StoryObj = {
render: (args) => ({
components: { CommonAlert },
setup() {
return { args }
},
template: `
<CommonAlert v-bind="args">
<template #title>
Some title
</template>
</CommonAlert>
`
}),
args: {
...Default.args
}
}
export const WithoutDescriptionAndWithDismisser = {
...WithoutDescription,
args: {
...WithoutDescription.args,
withDismiss: true
}
}
export const WithoutActions = {
...Default,
args: {
...Default.args,
actions: undefined
}
}
export const WithoutDescriptionAndActions = {
...WithoutDescription,
args: {
...WithoutDescription.args,
actions: undefined
}
}
export const Info = {
...Default,
args: {
...Default.args,
color: 'info'
}
}
export const Danger = {
...Default,
args: {
...Default.args,
color: 'danger'
}
}
export const Warning = {
...Default,
args: {
...Default.args,
color: 'warning'
}
}
@@ -0,0 +1,171 @@
<template>
<div class="rounded-md" :class="[containerClasses, textClasses]">
<div class="flex" :class="[hasDescription ? '' : 'items-center space-x-2']">
<div class="flex-shrink-0">
<CheckCircleIcon class="h-5 w-5" :class="iconClasses" aria-hidden="true" />
</div>
<div
class="ml-3 grow"
:class="[hasDescription ? '' : 'flex items-center space-x-2']"
>
<h3 class="text-sm" :class="[hasDescription ? 'font-medium' : '']">
<slot name="title">Title</slot>
</h3>
<div v-if="hasDescription" class="mt-2 text-sm">
<slot name="description">
Lorem ipsum dolor sit amet consectetur adipisicing elit. Aliquid pariatur,
ipsum similique veniam.
</slot>
</div>
<div :class="[hasDescription ? (actions?.length ? 'mt-4' : '') : 'grow flex']">
<div
class="flex"
:class="['space-x-2', hasDescription ? '' : 'grow justify-end']"
>
<FormButton
v-for="(action, i) in actions || []"
:key="i"
:color="color"
size="sm"
:to="action.url"
:external="action.externalUrl || false"
@click="action.onClick || noop"
>
{{ action.title }}
</FormButton>
</div>
</div>
</div>
<div
v-if="withDismiss"
class="flex"
:class="[hasDescription ? 'items-start' : 'items-center']"
>
<button
type="button"
class="inline-flex rounded-md focus:outline-none focus:ring-2"
:class="buttonClasses"
@click="$emit('dismiss')"
>
<span class="sr-only">Dismiss</span>
<XMarkIcon class="h-5 w-5" aria-hidden="true" />
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { CheckCircleIcon, XMarkIcon } from '@heroicons/vue/20/solid'
import { noop } from 'lodash'
import { computed, useSlots } from 'vue'
import FormButton from '~~/src/components/form/Button.vue'
type AlertColor = 'success' | 'danger' | 'warning' | 'info'
defineEmits<{ (e: 'dismiss'): void }>()
const props = withDefaults(
defineProps<{
color?: AlertColor
withDismiss?: boolean
actions?: Array<{
title: string
url?: string
onClick?: () => void
externalUrl?: boolean
}>
}>(),
{
color: 'success'
}
)
const slots = useSlots()
const hasDescription = computed(() => !!slots['description'])
const containerClasses = computed(() => {
const classParts: string[] = []
classParts.push(hasDescription.value ? 'p-4' : 'p-2')
switch (props.color) {
case 'success':
classParts.push('bg-success-lighter border-l-4 border-success')
break
case 'info':
classParts.push('bg-info-lighter border-l-4 border-info')
break
case 'danger':
classParts.push('bg-danger-lighter border-l-4 border-danger')
break
case 'warning':
classParts.push('bg-warning-lighter border-l-4 border-warning')
break
}
return classParts.join(' ')
})
const textClasses = computed(() => {
const classParts: string[] = []
switch (props.color) {
case 'success':
classParts.push('text-success-darker')
break
case 'info':
classParts.push('text-info-darker')
break
case 'danger':
classParts.push('text-danger-darker')
break
case 'warning':
classParts.push('text-warning-darker')
break
}
return classParts.join(' ')
})
const iconClasses = computed(() => {
const classParts: string[] = []
switch (props.color) {
case 'success':
classParts.push('text-success')
break
case 'info':
classParts.push('text-info')
break
case 'danger':
classParts.push('text-danger')
break
case 'warning':
classParts.push('text-warning')
break
}
return classParts.join(' ')
})
const buttonClasses = computed(() => {
const classParts: string[] = []
switch (props.color) {
case 'success':
classParts.push('bg-success-lighter ring-success')
break
case 'info':
classParts.push('bg-info-lighter ring-info')
break
case 'danger':
classParts.push('bg-danger-lighter ring-danger')
break
case 'warning':
classParts.push('bg-warning-lighter ring-warning')
break
}
return classParts.join(' ')
})
</script>
@@ -33,6 +33,10 @@ export default {
options: ['horizontal', 'vertical'],
control: { type: 'select' }
},
stepsPadding: {
options: ['base', 'xs', 'sm'],
control: { type: 'select' }
},
'update:modelValue': {
type: 'function',
action: 'v-model'
@@ -65,7 +69,8 @@ export const Default: StoryType = {
orientation: 'horizontal',
steps: testSteps,
modelValue: 1,
nonInteractive: false
nonInteractive: false,
stepsPadding: 'base'
}
}
@@ -81,6 +86,13 @@ export const VersionBasic: StoryType = mergeStories(Default, {
}
})
export const BasicVertical: StoryType = mergeStories(Default, {
args: {
basic: true,
orientation: 'vertical'
}
})
export const StartOnNegativeStep: StoryType = mergeStories(Default, {
args: {
modelValue: -1
@@ -1,6 +1,6 @@
<template>
<nav class="flex justify-center" :aria-label="ariaLabel || 'Progress steps'">
<ol :class="[listClasses, basic ? 'basic' : '']">
<ol :class="[listClasses, extraListClasses]">
<li v-for="(step, i) in steps" :key="step.name">
<a
v-if="isFinishedStep(i)"
@@ -67,7 +67,7 @@
<script setup lang="ts">
import { CheckCircleIcon } from '@heroicons/vue/20/solid'
import { computed, toRefs } from 'vue'
import { useStepsInternals } from '~~/src/composables/common/steps'
import { StepsPadding, useStepsInternals } from '~~/src/composables/common/steps'
import { BulletStepType, HorizontalOrVertical } from '~~/src/helpers/common/components'
import { TailwindBreakpoints } from '~~/src/helpers/tailwind'
@@ -83,6 +83,7 @@ const props = defineProps<{
modelValue?: number
goVerticalBelow?: TailwindBreakpoints
nonInteractive?: boolean
stepsPadding?: StepsPadding
}>()
const { isCurrentStep, isFinishedStep, switchStep, listClasses, linkClasses } =
@@ -92,7 +93,18 @@ const { isCurrentStep, isFinishedStep, switchStep, listClasses, linkClasses } =
})
const labelClasses = computed(() => {
const classParts: string[] = ['ml-3 h6 font-medium leading-7']
const classParts: string[] = ['h6 font-medium leading-7']
let leftMargin: string
if (props.stepsPadding === 'xs') {
leftMargin = 'ml-1'
} else if (props.stepsPadding === 'sm') {
leftMargin = 'ml-2'
} else {
leftMargin = 'ml-3'
}
classParts.push(leftMargin)
if (props.basic) {
classParts.push('sr-only')
@@ -100,9 +112,14 @@ const labelClasses = computed(() => {
return classParts.join(' ')
})
const extraListClasses = computed(() => {
const classParts: string[] = []
if (props.basic) {
classParts.push('basic')
}
return classParts.join(' ')
})
</script>
<style scoped>
.basic {
@apply space-x-4 !important;
}
</style>
@@ -54,6 +54,10 @@ export default {
'update:modelValue': {
type: 'function',
action: 'v-model'
},
stepsPadding: {
options: ['base', 'xs', 'sm'],
control: { type: 'select' }
}
},
parameters: {
@@ -81,7 +85,8 @@ export const Default: StoryType = {
ariaLabel: 'Steps ARIA title!',
orientation: 'horizontal',
steps: testStepsWithDescription,
modelValue: 1
modelValue: 1,
stepsPadding: 'base'
}
}
@@ -76,7 +76,7 @@
<script setup lang="ts">
import { CheckIcon } from '@heroicons/vue/20/solid'
import { toRefs } from 'vue'
import { useStepsInternals } from '~~/src/composables/common/steps'
import { StepsPadding, useStepsInternals } from '~~/src/composables/common/steps'
import { HorizontalOrVertical, NumberStepType } from '~~/src/helpers/common/components'
import { TailwindBreakpoints } from '~~/src/helpers/tailwind'
@@ -91,6 +91,7 @@ const props = defineProps<{
modelValue?: number
goVerticalBelow?: TailwindBreakpoints
nonInteractive?: boolean
stepsPadding?: StepsPadding
}>()
const {
@@ -9,7 +9,7 @@ export default {
component: FormButton,
argTypes: {
color: {
options: ['default', 'invert', 'danger', 'warning', 'secondary'],
options: ['default', 'invert', 'danger', 'warning', 'secondary', 'info'],
control: { type: 'select' }
},
outlined: {
@@ -123,6 +123,42 @@ export const WarningButton: StoryObj = mergeStories(Default, {
}
})
export const InfoButton: StoryObj = mergeStories(Default, {
args: {
color: 'info'
}
})
export const DangerButton: StoryObj = mergeStories(Default, {
args: {
color: 'danger'
}
})
export const SuccessButton: StoryObj = mergeStories(Default, {
args: {
color: 'success'
}
})
export const SecondaryButton: StoryObj = mergeStories(Default, {
args: {
color: 'secondary'
}
})
export const InvertButton: StoryObj = mergeStories(Default, {
args: {
color: 'invert'
}
})
export const CardButton: StoryObj = mergeStories(Default, {
args: {
color: 'card'
}
})
export const RoundedOutlined: StoryObj = mergeStories(Default, {
args: {
rounded: true,
@@ -42,6 +42,7 @@ type FormButtonColor =
| 'success'
| 'card'
| 'secondary'
| 'info'
const emit = defineEmits<{
/**
@@ -232,6 +233,9 @@ const bgAndBorderClasses = computed(() => {
case 'warning':
classParts.push(props.outlined ? 'border-warning' : 'bg-warning border-warning')
break
case 'info':
classParts.push(props.outlined ? 'border-info' : 'bg-info border-info')
break
case 'success':
classParts.push(props.outlined ? 'border-success' : 'bg-success border-success')
break
@@ -276,6 +280,11 @@ const foregroundClasses = computed(() => {
props.outlined ? 'text-warning' : 'text-foundation dark:text-foreground'
)
break
case 'info':
classParts.push(
props.outlined ? 'text-info' : 'text-foundation dark:text-foreground'
)
break
case 'success':
classParts.push(
props.outlined ? 'text-success' : 'text-foundation dark:text-foreground'
@@ -312,6 +321,8 @@ const foregroundClasses = computed(() => {
classParts.push('text-success')
} else if (props.color === 'warning') {
classParts.push('text-warning')
} else if (props.color === 'info') {
classParts.push('text-info')
} else if (props.color === 'danger') {
classParts.push('text-danger')
} else {
@@ -341,6 +352,9 @@ const ringClasses = computed(() => {
case 'warning':
classParts.push('hover:ring-4 ring-warning-lighter dark:ring-warning-darker')
break
case 'info':
classParts.push('hover:ring-4 ring-info-lighter dark:ring-info-darker')
break
case 'success':
classParts.push('hover:ring-4 ring-success-lighter dark:ring-success-darker')
break
@@ -86,7 +86,7 @@ import { ConcreteComponent, PropType, computed, ref, toRefs, useSlots } from 'vu
import { Nullable, Optional } from '@speckle/shared'
import { useTextInputCore } from '~~/src/composables/form/textInput'
type InputType = 'text' | 'email' | 'password' | 'url' | 'search'
type InputType = 'text' | 'email' | 'password' | 'url' | 'search' | 'number' | string
type InputSize = 'sm' | 'base' | 'lg' | 'xl'
type InputColor = 'page' | 'foundation'
@@ -125,6 +125,31 @@ export const Default: StoryType = {
}
}
export const WithLabel: StoryType = {
...Default,
args: {
...Default.args,
showLabel: true
}
}
export const WithLabelAndHelp: StoryType = {
...Default,
args: {
...Default.args,
showLabel: true,
help: 'Some help text'
}
}
export const Tinted: StoryType = {
...Default,
args: {
...Default.args,
buttonStyle: 'tinted'
}
}
export const LimitedWidth: StoryType = {
...Default,
render: (args, ctx) => ({
@@ -9,7 +9,7 @@
as="div"
>
<ListboxLabel
class="block label text-foreground"
class="block label text-foreground-2 mb-2"
:class="{ 'sr-only': !showLabel }"
>
{{ label }}
@@ -138,12 +138,7 @@
</Transition>
</div>
</Listbox>
<p
v-if="helpTipId"
:id="helpTipId"
class="mt-2 ml-3 text-sm"
:class="helpTipClasses"
>
<p v-if="helpTipId" :id="helpTipId" class="mt-2 text-sm" :class="helpTipClasses">
{{ helpTip }}
</p>
</div>
@@ -180,7 +175,7 @@ import CommonLoadingBar from '~~/src/components/common/loading/Bar.vue'
// @ts-ignore
import { directive as vTippy } from 'vue-tippy'
type ButtonStyle = 'base' | 'simple'
type ButtonStyle = 'base' | 'simple' | 'tinted'
type SingleItem = any
type ValueType = SingleItem | SingleItem[] | undefined
@@ -284,7 +279,7 @@ const props = defineProps({
* Validation stuff
*/
rules: {
type: [String, Object, Function, Array] as PropType<RuleExpression<string>>,
type: [String, Object, Function, Array] as PropType<RuleExpression<ValueType>>,
default: undefined
},
/**
@@ -354,7 +349,7 @@ const renderClearButton = computed(
)
const buttonsWrapperClasses = computed(() => {
const classParts: string[] = ['relative flex group', props.showLabel ? 'mt-1' : '']
const classParts: string[] = ['relative flex group']
if (props.buttonStyle !== 'simple') {
classParts.push('hover:shadow rounded-md')
@@ -389,13 +384,19 @@ const clearButtonClasses = computed(() => {
'relative z-[1]',
'flex items-center justify-center text-center shrink-0',
'rounded-r-md overflow-hidden transition-all',
'text-foreground',
hasValueSelected.value ? `w-6 ${commonButtonClasses.value}` : 'w-0'
]
if (!isDisabled.value) {
classParts.push(
'bg-primary-muted hover:bg-primary hover:text-foreground-on-primary'
'hover:bg-primary hover:text-foreground-on-primary dark:text-foreground-on-primary'
)
if (props.buttonStyle === 'tinted') {
classParts.push('bg-outline-3')
} else {
classParts.push('bg-primary-muted')
}
}
return classParts.join(' ')
@@ -413,7 +414,11 @@ const buttonClasses = computed(() => {
classParts.push('py-2 px-3')
if (!isDisabled.value) {
classParts.push('bg-foundation text-foreground')
if (props.buttonStyle === 'tinted') {
classParts.push('bg-foundation-page text-foreground')
} else {
classParts.push('bg-foundation text-foreground')
}
}
}
@@ -492,7 +497,6 @@ const itemKey = (v: SingleItem): string | number =>
props.by ? (v[props.by] as string) : v
const triggerSearch = async () => {
console.log('triggerSearch')
if (!isAsyncSearchMode.value || !props.getSearchResults) return
isAsyncLoading.value = true
@@ -42,6 +42,15 @@ export const Default: StoryObj = {
}),
args: {
maxWidth: 'sm',
hideCloser: false
hideCloser: false,
preventCloseOnClickOutside: false
}
}
export const ManualCloseOnly = {
...Default,
args: {
...Default.args,
preventCloseOnClickOutside: true
}
}
@@ -1,6 +1,6 @@
<template>
<TransitionRoot as="template" :show="open">
<Dialog as="div" class="relative z-40" @close="open = false">
<Dialog as="div" class="relative z-40" @close="onClose">
<TransitionChild
as="template"
enter="ease-out duration-300"
@@ -66,6 +66,10 @@ const props = defineProps<{
open: boolean
maxWidth?: MaxWidthValue
hideCloser?: boolean
/**
* Prevent modal from closing when the user clicks outside of the modal or presses Esc
*/
preventCloseOnClickOutside?: boolean
}>()
const open = computed({
@@ -112,4 +116,9 @@ const widthClasses = computed(() => {
return classParts.join(' ')
})
const onClose = () => {
if (props.preventCloseOnClickOutside) return
open.value = false
}
</script>
@@ -0,0 +1,101 @@
import { Meta, StoryObj } from '@storybook/vue3'
import LayoutPanel from '~~/src/components/layout/Panel.vue'
export default {
component: LayoutPanel,
parameters: {
docs: {
description: {
component: 'A basic panel that can be used as a basis for various cards/panels'
}
}
}
} as Meta
export const Default: StoryObj = {
render: (args) => ({
components: { LayoutPanel },
setup() {
return { args }
},
template: `
<div>
<LayoutPanel v-bind="args">
<template #default>
Default slot
</template>
<template #header>
Header slot
</template>
<template #footer>
Footer slot
</template>
</LayoutPanel>
</div>
`
}),
args: {
form: false,
ring: false,
customPadding: false,
fancyGlow: false,
noShadow: false
}
}
export const CustomPadding: StoryObj = {
render: (args) => ({
components: { LayoutPanel },
setup() {
return { args }
},
template: `
<div>
<LayoutPanel v-bind="args">
<template #default>
<div class="p-2">
Default slot
</div>
</template>
<template #header>
Header slot (no padding)
</template>
<template #footer>
<div class="p-8">
Footer slot (big padding)
</div>
</template>
</LayoutPanel>
</div>
`
}),
args: {
form: false,
ring: false,
customPadding: true
}
}
export const WithRingOutline = {
...Default,
args: {
...Default.args,
ring: true
}
}
export const WithFancyGlow = {
...Default,
args: {
...Default.args,
fancyGlow: true
}
}
export const NoShadow = {
...Default,
args: {
...Default.args,
noShadow: true
}
}
@@ -12,51 +12,68 @@
]"
@submit="emit('submit', $event)"
>
<div v-if="$slots.header" class="px-4 py-4 sm:px-6">
<div v-if="$slots.header" :class="secondarySlotPaddingClasses">
<slot name="header" />
</div>
<div class="grow px-4 py-4 sm:p-6">
<div :class="['grow', defaultSlotPaddingClasses]">
<slot />
</div>
<div v-if="$slots.footer" class="px-4 py-4 sm:px-6">
<div v-if="$slots.footer" :class="secondarySlotPaddingClasses">
<slot name="footer" />
</div>
</Component>
</div>
</template>
<script setup lang="ts">
import { PropType } from 'vue'
import { computed } from 'vue'
const emit = defineEmits<{ (e: 'submit', val: SubmitEvent): void }>()
type RoundedBorderSize = '2xl' | 'base'
const props = defineProps({
/**
* Use a `<form/>` element as a wrapper that will emit 'submit' events out from the component when they occur
*/
form: {
type: Boolean,
default: false
},
roundedBorderSize: {
type: String as PropType<RoundedBorderSize>,
default: '2xl'
/**
* Add a ring outline on hover
*/
ring: {
type: Boolean,
default: false
},
/**
* Add a primary-colored glow on hover
*/
fancyGlow: {
type: Boolean,
default: false
},
customPadding: {
type: Boolean,
default: false
},
noShadow: {
type: Boolean,
default: false
}
})
const secondarySlotPaddingClasses = computed(() =>
props.customPadding ? '' : 'px-4 py-4 sm:px-6'
)
const defaultSlotPaddingClasses = computed(() =>
props.customPadding ? '' : 'px-4 py-4 sm:p-6'
)
const computedClasses = computed(() => {
const classParts: string[] = []
if (!props.fancyGlow) classParts.push('shadow')
switch (props.roundedBorderSize) {
case 'base':
classParts.push('rounded-md')
break
case '2xl':
default:
classParts.push('rounded-md')
break
const classParts: string[] = ['rounded-lg']
if (!props.noShadow) classParts.push('shadow')
if (props.ring) {
classParts.push('ring-outline-2 hover:ring-2')
}
return classParts.join(' ')
@@ -1,7 +1,9 @@
import { ToRefs, computed } from 'vue'
import { HorizontalOrVertical, StepCoreType } from '~~/src/helpers/common/components'
import { clamp } from 'lodash'
import { TailwindBreakpoints } from '~~/src/helpers/tailwind'
import { TailwindBreakpoints, markClassesUsed } from '~~/src/helpers/tailwind'
export type StepsPadding = 'base' | 'xs' | 'sm'
export function useStepsInternals(params: {
props: ToRefs<{
@@ -10,13 +12,21 @@ export function useStepsInternals(params: {
modelValue?: number
goVerticalBelow?: TailwindBreakpoints
nonInteractive?: boolean
stepsPadding?: StepsPadding
}>
emit: {
(e: 'update:modelValue', val: number): void
}
}) {
const {
props: { modelValue, steps, orientation, goVerticalBelow, nonInteractive },
props: {
modelValue,
steps,
orientation,
goVerticalBelow,
nonInteractive,
stepsPadding
},
emit
} = params
@@ -51,29 +61,42 @@ export function useStepsInternals(params: {
const listClasses = computed(() => {
const classParts: string[] = ['flex']
let paddingHorizontal: string
let paddingVertical: string
if (stepsPadding?.value === 'xs') {
paddingHorizontal = 'space-x-2'
paddingVertical = 'space-y-1'
} else if (stepsPadding?.value === 'sm') {
paddingHorizontal = 'space-x-4'
paddingVertical = 'space-y-1'
} else {
paddingHorizontal = 'space-x-8'
paddingVertical = 'space-y-4'
}
classParts.push('flex')
if (finalOrientation.value === 'vertical' || goVerticalBelow?.value) {
classParts.push('flex-col space-y-4 justify-center')
classParts.push(`flex-col ${paddingVertical} justify-center`)
if (goVerticalBelow?.value === TailwindBreakpoints.sm) {
classParts.push(
'sm:flex-row sm:space-y-0 sm:justify-start sm:space-x-8 sm:items-center'
`sm:flex-row sm:space-y-0 sm:justify-start sm:${paddingHorizontal} sm:items-center`
)
} else if (goVerticalBelow?.value === TailwindBreakpoints.md) {
classParts.push(
'md:flex-row md:space-y-0 md:justify-start md:space-x-8 md:items-center'
`md:flex-row md:space-y-0 md:justify-start md:${paddingHorizontal} md:items-center`
)
} else if (goVerticalBelow?.value === TailwindBreakpoints.lg) {
classParts.push(
'lg:flex-row lg:space-y-0 lg:justify-start lg:space-x-8 lg:items-center'
`lg:flex-row lg:space-y-0 lg:justify-start lg:${paddingHorizontal} lg:items-center`
)
} else if (goVerticalBelow?.value === TailwindBreakpoints.xl) {
classParts.push(
'xl:flex-row xl:space-y-0 xl:justify-start xl:space-x-8 xl:items-center'
`xl:flex-row xl:space-y-0 xl:justify-start xl:${paddingHorizontal} xl:items-center`
)
}
} else {
classParts.push('flex-row space-x-8 items-center')
classParts.push(`flex-row ${paddingHorizontal} items-center`)
}
return classParts.join(' ')
@@ -100,3 +123,19 @@ export function useStepsInternals(params: {
orientation: finalOrientation
}
}
// to allow for dynamic class building above:
markClassesUsed([
'sm:space-x-8',
'md:space-x-8',
'lg:space-x-8',
'xl:space-x-8',
'sm:space-x-2',
'md:space-x-2',
'lg:space-x-2',
'xl:space-x-2',
'sm:space-x-4',
'md:space-x-4',
'lg:space-x-4',
'xl:space-x-4'
])
@@ -0,0 +1,14 @@
export type InfiniteLoaderState = {
/**
* Informs the component that this loading has been successful
*/
loaded: () => void
/**
* Informs the component that all of the data has been loaded successfully
*/
complete: () => void
/**
* Inform the component that this loading failed, the content of the `error` slot will be displayed
*/
error: () => void
}
@@ -1,3 +1,5 @@
let junkVariable: string[] = []
/**
* If you use concatenation or variables to build tailwind classes, PurgeCSS won't pick up on them
* during build and will not add them to the build. So you can use this function to just add string
@@ -7,9 +9,9 @@
* variable so it's better to use this instead.
*/
export function markClassesUsed(classes: string[]) {
// this doesn't do anything, we just need PurgeCSS to be able to read
// invocations of this function
false && classes
// this doesn't do anything, except trick the compiler into thinking this isn't a pure
// function so that the invocations aren't tree-shaken out
junkVariable = junkVariable ? classes : classes.slice()
}
/**
+10 -2
View File
@@ -43,6 +43,10 @@ import {
import LayoutMenu from '~~/src/components/layout/Menu.vue'
import { LayoutMenuItem, LayoutTabItem } from '~~/src/helpers/layout/components'
import LayoutTabs from '~~/src/components/layout/Tabs.vue'
import InfiniteLoading from '~~/src/components/InfiniteLoading.vue'
import { InfiniteLoaderState } from '~~/src/helpers/global/components'
import LayoutPanel from '~~/src/components/layout/Panel.vue'
import CommonAlert from '~~/src/components/common/Alert.vue'
export {
GlobalToastRenderer,
@@ -78,7 +82,10 @@ export {
useOnBeforeWindowUnload,
useResponsiveHorizontalDirectionCalculation,
LayoutMenu,
LayoutTabs
LayoutTabs,
InfiniteLoading,
LayoutPanel,
CommonAlert
}
export type {
ToastNotification,
@@ -86,5 +93,6 @@ export type {
NumberStepType,
HorizontalOrVertical,
LayoutMenuItem,
LayoutTabItem
LayoutTabItem,
InfiniteLoaderState
}
+1 -1
View File
@@ -11216,7 +11216,6 @@ __metadata:
tailwindcss: ^3.3.2
type-fest: ^3.5.1
typescript: ^4.8.3
v3-infinite-loading: ^1.2.2
vee-validate: ^4.7.0
vue-advanced-cropper: ^2.8.8
vue-tippy: ^6.0.0
@@ -11611,6 +11610,7 @@ __metadata:
type-fest: ^2.13.1
typescript: ^5.0.4
unplugin-vue-macros: ^2.1.4
v3-infinite-loading: ^1.2.2
vee-validate: ^4.7.0
vite: ^4.3.9
vite-plugin-dts: ^2.3.0