Initial commit from monorepo
This commit is contained in:
21
app/components/ui/Alert.stories.ts
Normal file
21
app/components/ui/Alert.stories.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3'
|
||||
import StoryComponent from './Alert.vue'
|
||||
|
||||
const meta: Meta<typeof StoryComponent> = {
|
||||
title: 'Ui/Alert',
|
||||
component: StoryComponent,
|
||||
render: (args) => ({
|
||||
components: { StoryComponent },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<StoryComponent v-bind="args" />'
|
||||
})
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Primary: Story = {
|
||||
args: {}
|
||||
}
|
||||
23
app/components/ui/Alert.vue
Normal file
23
app/components/ui/Alert.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<div :class="alertClass">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'info', // info | error | success | warning
|
||||
},
|
||||
})
|
||||
|
||||
const variantMap: Record<string, string> = {
|
||||
info: 'alert alert-info',
|
||||
error: 'alert alert-error',
|
||||
success: 'alert alert-success',
|
||||
warning: 'alert alert-warning',
|
||||
}
|
||||
|
||||
const alertClass = computed(() => variantMap[props.variant] || variantMap.info)
|
||||
</script>
|
||||
40
app/components/ui/Badge.vue
Normal file
40
app/components/ui/Badge.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<span :class="badgeClass">
|
||||
<slot />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'default', // default | success | warning | error | muted | primary
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'sm', // xs | sm | md
|
||||
},
|
||||
})
|
||||
|
||||
const variantMap: Record<string, string> = {
|
||||
default: 'badge-neutral',
|
||||
success: 'badge-success',
|
||||
warning: 'badge-warning',
|
||||
error: 'badge-error',
|
||||
muted: 'badge-ghost',
|
||||
primary: 'badge-primary',
|
||||
}
|
||||
|
||||
const sizeMap: Record<string, string> = {
|
||||
xs: 'badge-xs',
|
||||
sm: 'badge-sm',
|
||||
md: 'badge-md',
|
||||
}
|
||||
|
||||
const badgeClass = computed(() => {
|
||||
const base = 'badge'
|
||||
const variantClass = variantMap[props.variant] || variantMap.default
|
||||
const sizeClass = sizeMap[props.size] || sizeMap.sm
|
||||
return [base, variantClass, sizeClass].join(' ')
|
||||
})
|
||||
</script>
|
||||
46
app/components/ui/Button.stories.ts
Normal file
46
app/components/ui/Button.stories.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3'
|
||||
import Button from './Button.vue'
|
||||
|
||||
const meta: Meta<typeof Button> = {
|
||||
title: 'UI/Button',
|
||||
component: Button,
|
||||
render: (args) => ({
|
||||
components: { Button },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<Button v-bind="args">{{ args.label }}</Button>'
|
||||
}),
|
||||
argTypes: {
|
||||
variant: {
|
||||
control: { type: 'select' },
|
||||
options: ['primary', 'outline']
|
||||
}
|
||||
},
|
||||
tags: ['autodocs']
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Primary: Story = {
|
||||
args: {
|
||||
label: 'Primary button',
|
||||
variant: 'primary'
|
||||
}
|
||||
}
|
||||
|
||||
export const Outline: Story = {
|
||||
args: {
|
||||
label: 'Outline button',
|
||||
variant: 'outline'
|
||||
}
|
||||
}
|
||||
|
||||
export const FullWidth: Story = {
|
||||
args: {
|
||||
label: 'Full width',
|
||||
variant: 'primary',
|
||||
fullWidth: true
|
||||
}
|
||||
}
|
||||
45
app/components/ui/Button.vue
Normal file
45
app/components/ui/Button.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<component
|
||||
:is="componentTag"
|
||||
:type="componentType"
|
||||
:class="['btn', variantClass, fullWidth ? 'w-full' : '']"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<slot />
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'primary',
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'button',
|
||||
},
|
||||
fullWidth: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
as: {
|
||||
type: [String, Object],
|
||||
default: 'button', // button | NuxtLink | a | etc
|
||||
},
|
||||
})
|
||||
|
||||
const componentTag = computed(() => {
|
||||
if (props.as === 'NuxtLink') {
|
||||
return resolveComponent('NuxtLink')
|
||||
}
|
||||
return props.as || 'button'
|
||||
})
|
||||
const componentType = computed(() => (props.as === 'button' ? props.type : undefined))
|
||||
|
||||
const variantClass = computed(() => {
|
||||
if (props.variant === 'outline') return 'btn-outline btn-primary'
|
||||
if (props.variant === 'ghost') return 'btn-ghost'
|
||||
return 'btn-primary'
|
||||
})
|
||||
</script>
|
||||
21
app/components/ui/Card.stories.ts
Normal file
21
app/components/ui/Card.stories.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3'
|
||||
import StoryComponent from './Card.vue'
|
||||
|
||||
const meta: Meta<typeof StoryComponent> = {
|
||||
title: 'Ui/Card',
|
||||
component: StoryComponent,
|
||||
render: (args) => ({
|
||||
components: { StoryComponent },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<StoryComponent v-bind="args" />'
|
||||
})
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Primary: Story = {
|
||||
args: {}
|
||||
}
|
||||
42
app/components/ui/Card.vue
Normal file
42
app/components/ui/Card.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<div :class="cardClass">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
padding: {
|
||||
type: String,
|
||||
default: 'medium', // none | small | medium
|
||||
},
|
||||
tone: {
|
||||
type: String,
|
||||
default: 'default', // default | muted | primary
|
||||
},
|
||||
interactive: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const paddingMap: Record<string, string> = {
|
||||
none: '',
|
||||
small: 'p-4',
|
||||
medium: 'p-6',
|
||||
}
|
||||
|
||||
const toneMap: Record<string, string> = {
|
||||
default: 'bg-base-100',
|
||||
muted: 'bg-base-200',
|
||||
primary: 'bg-primary/10',
|
||||
}
|
||||
|
||||
const cardClass = computed(() => {
|
||||
const paddingClass = paddingMap[props.padding] || paddingMap.medium
|
||||
const toneClass = toneMap[props.tone] || toneMap.default
|
||||
const interactiveClass = props.interactive ? 'cursor-pointer hover:shadow-lg' : ''
|
||||
const baseClass = 'card transition-all duration-200'
|
||||
return [baseClass, paddingClass, toneClass, interactiveClass].filter(Boolean).join(' ')
|
||||
})
|
||||
</script>
|
||||
21
app/components/ui/Container.stories.ts
Normal file
21
app/components/ui/Container.stories.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3'
|
||||
import StoryComponent from './Container.vue'
|
||||
|
||||
const meta: Meta<typeof StoryComponent> = {
|
||||
title: 'Ui/Container',
|
||||
component: StoryComponent,
|
||||
render: (args) => ({
|
||||
components: { StoryComponent },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<StoryComponent v-bind="args" />'
|
||||
})
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Primary: Story = {
|
||||
args: {}
|
||||
}
|
||||
29
app/components/ui/Container.vue
Normal file
29
app/components/ui/Container.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<div :class="containerClass">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
size: {
|
||||
type: String,
|
||||
default: 'content', // content (1400px) | narrow (800px)
|
||||
},
|
||||
padding: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
|
||||
const sizeMap: Record<string, string> = {
|
||||
content: 'max-w-7xl',
|
||||
narrow: 'max-w-3xl',
|
||||
}
|
||||
|
||||
const containerClass = computed(() => {
|
||||
const sizeClass = sizeMap[props.size] || sizeMap.content
|
||||
const paddingClass = props.padding ? 'px-4 sm:px-6 lg:px-8' : ''
|
||||
return ['mx-auto w-full', sizeClass, paddingClass].filter(Boolean).join(' ')
|
||||
})
|
||||
</script>
|
||||
50
app/components/ui/EmptyState.vue
Normal file
50
app/components/ui/EmptyState.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div class="card bg-base-200 p-8 sm:p-12">
|
||||
<div class="flex flex-col items-center text-center gap-4">
|
||||
<div class="w-16 h-16 rounded-full bg-base-300 flex items-center justify-center">
|
||||
<slot name="icon">
|
||||
<svg class="w-8 h-8 text-base-content/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"/>
|
||||
</svg>
|
||||
</slot>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<h3 class="text-lg font-semibold text-base-content">{{ title }}</h3>
|
||||
<p v-if="description" class="text-base-content/70 max-w-md">{{ description }}</p>
|
||||
</div>
|
||||
<div v-if="actionLabel" class="mt-2">
|
||||
<NuxtLink v-if="actionTo" :to="actionTo">
|
||||
<button class="btn btn-primary gap-2">
|
||||
<Icon v-if="actionIcon" :name="actionIcon" size="18" />
|
||||
<svg v-else class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
|
||||
</svg>
|
||||
{{ actionLabel }}
|
||||
</button>
|
||||
</NuxtLink>
|
||||
<button v-else class="btn btn-primary gap-2" @click="$emit('action')">
|
||||
<Icon v-if="actionIcon" :name="actionIcon" size="18" />
|
||||
<svg v-else class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
|
||||
</svg>
|
||||
{{ actionLabel }}
|
||||
</button>
|
||||
</div>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
title: string
|
||||
description?: string
|
||||
actionLabel?: string
|
||||
actionTo?: string
|
||||
actionIcon?: string
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
(e: 'action'): void
|
||||
}>()
|
||||
</script>
|
||||
21
app/components/ui/FieldButton.stories.ts
Normal file
21
app/components/ui/FieldButton.stories.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3'
|
||||
import StoryComponent from './FieldButton.vue'
|
||||
|
||||
const meta: Meta<typeof StoryComponent> = {
|
||||
title: 'Ui/FieldButton',
|
||||
component: StoryComponent,
|
||||
render: (args) => ({
|
||||
components: { StoryComponent },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<StoryComponent v-bind="args" />'
|
||||
})
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Primary: Story = {
|
||||
args: {}
|
||||
}
|
||||
31
app/components/ui/FieldButton.vue
Normal file
31
app/components/ui/FieldButton.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<button :type="type" class="input input-bordered w-full flex items-center justify-between text-left" v-bind="$attrs">
|
||||
<span :class="value ? 'text-base-content' : 'text-base-content/60'">
|
||||
{{ value || placeholder }}
|
||||
</span>
|
||||
<svg v-if="chevron" class="w-4 h-4 text-base-content/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'button',
|
||||
},
|
||||
chevron: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
21
app/components/ui/Grid.stories.ts
Normal file
21
app/components/ui/Grid.stories.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3'
|
||||
import StoryComponent from './Grid.vue'
|
||||
|
||||
const meta: Meta<typeof StoryComponent> = {
|
||||
title: 'Ui/Grid',
|
||||
component: StoryComponent,
|
||||
render: (args) => ({
|
||||
components: { StoryComponent },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<StoryComponent v-bind="args" />'
|
||||
})
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Primary: Story = {
|
||||
args: {}
|
||||
}
|
||||
52
app/components/ui/Grid.vue
Normal file
52
app/components/ui/Grid.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div :class="gridClass">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
cols: {
|
||||
type: [String, Number],
|
||||
default: 1,
|
||||
},
|
||||
md: {
|
||||
type: [String, Number],
|
||||
default: null,
|
||||
},
|
||||
lg: {
|
||||
type: [String, Number],
|
||||
default: null,
|
||||
},
|
||||
gap: {
|
||||
type: [String, Number],
|
||||
default: 6,
|
||||
},
|
||||
})
|
||||
|
||||
const colMap: Record<string, string> = {
|
||||
'1': 'grid-cols-1',
|
||||
'2': 'grid-cols-2',
|
||||
'3': 'grid-cols-3',
|
||||
'4': 'grid-cols-4',
|
||||
}
|
||||
|
||||
const gapMap: Record<string, string> = {
|
||||
'4': 'gap-4',
|
||||
'6': 'gap-6',
|
||||
'8': 'gap-8',
|
||||
}
|
||||
|
||||
const gridClass = computed(() => {
|
||||
const baseCols = colMap[String(props.cols)] || colMap['1']
|
||||
const mdClass = props.md ? colMap[String(props.md)] : ''
|
||||
const lgClass = props.lg ? colMap[String(props.lg)] : ''
|
||||
const gapClass = gapMap[String(props.gap)] || gapMap['6']
|
||||
|
||||
const classes = ['grid', baseCols, gapClass]
|
||||
if (mdClass) classes.push(`md:${mdClass}`)
|
||||
if (lgClass) classes.push(`lg:${lgClass}`)
|
||||
|
||||
return classes.join(' ')
|
||||
})
|
||||
</script>
|
||||
21
app/components/ui/GridItem.stories.ts
Normal file
21
app/components/ui/GridItem.stories.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3'
|
||||
import StoryComponent from './GridItem.vue'
|
||||
|
||||
const meta: Meta<typeof StoryComponent> = {
|
||||
title: 'Ui/GridItem',
|
||||
component: StoryComponent,
|
||||
render: (args) => ({
|
||||
components: { StoryComponent },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<StoryComponent v-bind="args" />'
|
||||
})
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Primary: Story = {
|
||||
args: {}
|
||||
}
|
||||
32
app/components/ui/GridItem.vue
Normal file
32
app/components/ui/GridItem.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<div :class="itemClass">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
md: {
|
||||
type: [String, Number],
|
||||
default: null,
|
||||
},
|
||||
lg: {
|
||||
type: [String, Number],
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const spanMap: Record<string, string> = {
|
||||
'1': 'col-span-1',
|
||||
'2': 'col-span-2',
|
||||
'3': 'col-span-3',
|
||||
'4': 'col-span-4',
|
||||
}
|
||||
|
||||
const itemClass = computed(() => {
|
||||
const classes = [] as string[]
|
||||
if (props.md && spanMap[String(props.md)]) classes.push(`md:${spanMap[String(props.md)]}`)
|
||||
if (props.lg && spanMap[String(props.lg)]) classes.push(`lg:${spanMap[String(props.lg)]}`)
|
||||
return classes.join(' ')
|
||||
})
|
||||
</script>
|
||||
21
app/components/ui/Heading.stories.ts
Normal file
21
app/components/ui/Heading.stories.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3'
|
||||
import StoryComponent from './Heading.vue'
|
||||
|
||||
const meta: Meta<typeof StoryComponent> = {
|
||||
title: 'Ui/Heading',
|
||||
component: StoryComponent,
|
||||
render: (args) => ({
|
||||
components: { StoryComponent },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<StoryComponent v-bind="args" />'
|
||||
})
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Primary: Story = {
|
||||
args: {}
|
||||
}
|
||||
45
app/components/ui/Heading.vue
Normal file
45
app/components/ui/Heading.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<component :is="tag" :class="headingClass">
|
||||
<slot />
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
level: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
weight: {
|
||||
type: String,
|
||||
default: 'bold',
|
||||
},
|
||||
tone: {
|
||||
type: String,
|
||||
default: 'default', // default | inverse | muted
|
||||
},
|
||||
})
|
||||
|
||||
const tag = computed(() => {
|
||||
const n = Math.min(Math.max(props.level, 1), 4)
|
||||
return `h${n}`
|
||||
})
|
||||
|
||||
const headingClass = computed(() => {
|
||||
const sizeMap: Record<number, string> = {
|
||||
1: 'text-3xl lg:text-4xl',
|
||||
2: 'text-2xl lg:text-3xl',
|
||||
3: 'text-xl lg:text-2xl',
|
||||
4: 'text-lg',
|
||||
}
|
||||
const sizeClass = sizeMap[Math.min(Math.max(props.level, 1), 4)]
|
||||
const weightClass = props.weight === 'semibold' ? 'font-semibold' : 'font-bold'
|
||||
const toneMap: Record<string, string> = {
|
||||
default: 'text-base-content',
|
||||
inverse: 'text-primary-content',
|
||||
muted: 'text-base-content/80',
|
||||
}
|
||||
const toneClass = toneMap[props.tone] || toneMap.default
|
||||
return [sizeClass, weightClass, toneClass].join(' ')
|
||||
})
|
||||
</script>
|
||||
21
app/components/ui/IconCircle.stories.ts
Normal file
21
app/components/ui/IconCircle.stories.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3'
|
||||
import StoryComponent from './IconCircle.vue'
|
||||
|
||||
const meta: Meta<typeof StoryComponent> = {
|
||||
title: 'Ui/IconCircle',
|
||||
component: StoryComponent,
|
||||
render: (args) => ({
|
||||
components: { StoryComponent },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<StoryComponent v-bind="args" />'
|
||||
})
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Primary: Story = {
|
||||
args: {}
|
||||
}
|
||||
34
app/components/ui/IconCircle.vue
Normal file
34
app/components/ui/IconCircle.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div :class="circleClass">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
size: {
|
||||
type: String,
|
||||
default: 'md', // md | lg
|
||||
},
|
||||
tone: {
|
||||
type: String,
|
||||
default: 'primary', // primary | neutral
|
||||
},
|
||||
})
|
||||
|
||||
const sizeMap: Record<string, string> = {
|
||||
md: 'w-16 h-16 text-2xl',
|
||||
lg: 'w-20 h-20 text-3xl',
|
||||
}
|
||||
|
||||
const toneMap: Record<string, string> = {
|
||||
primary: 'bg-primary/10 text-primary',
|
||||
neutral: 'bg-base-200 text-base-content',
|
||||
}
|
||||
|
||||
const circleClass = computed(() => {
|
||||
const sizeClass = sizeMap[props.size] || sizeMap.md
|
||||
const toneClass = toneMap[props.tone] || toneMap.primary
|
||||
return ['rounded-full flex items-center justify-center', sizeClass, toneClass].join(' ')
|
||||
})
|
||||
</script>
|
||||
21
app/components/ui/Input.stories.ts
Normal file
21
app/components/ui/Input.stories.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3'
|
||||
import StoryComponent from './Input.vue'
|
||||
|
||||
const meta: Meta<typeof StoryComponent> = {
|
||||
title: 'Ui/Input',
|
||||
component: StoryComponent,
|
||||
render: (args) => ({
|
||||
components: { StoryComponent },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<StoryComponent v-bind="args" />'
|
||||
})
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Primary: Story = {
|
||||
args: {}
|
||||
}
|
||||
36
app/components/ui/Input.vue
Normal file
36
app/components/ui/Input.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<label v-if="label" class="w-full space-y-1">
|
||||
<span class="text-base font-semibold text-base-content">{{ label }}</span>
|
||||
<input
|
||||
v-bind="$attrs"
|
||||
class="input input-bordered w-full bg-base-100 text-base-content placeholder-base-content/60"
|
||||
:value="modelValue"
|
||||
@input="onInput"
|
||||
/>
|
||||
</label>
|
||||
<input
|
||||
v-else
|
||||
v-bind="$attrs"
|
||||
class="input input-bordered w-full bg-base-100 text-base-content placeholder-base-content/60"
|
||||
:value="modelValue"
|
||||
@input="onInput"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const onInput = (event: Event) => {
|
||||
emit('update:modelValue', (event.target as HTMLInputElement).value)
|
||||
}
|
||||
</script>
|
||||
36
app/components/ui/PageHeader.vue
Normal file
36
app/components/ui/PageHeader.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-2xl lg:text-3xl font-bold text-base-content">{{ title }}</h1>
|
||||
<p v-if="description" class="text-base-content/70">{{ description }}</p>
|
||||
</div>
|
||||
<div v-if="$slots.actions || actions?.length" class="flex items-center gap-2 flex-shrink-0">
|
||||
<slot name="actions">
|
||||
<PageHeaderAction
|
||||
v-for="(action, i) in actions"
|
||||
:key="i"
|
||||
:to="action.to"
|
||||
:icon="action.icon"
|
||||
@click="action.onClick"
|
||||
>
|
||||
{{ action.label }}
|
||||
</PageHeaderAction>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Action {
|
||||
label: string
|
||||
icon?: string
|
||||
to?: string
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
title: string
|
||||
description?: string
|
||||
actions?: Action[]
|
||||
}>()
|
||||
</script>
|
||||
24
app/components/ui/PageHeaderAction.vue
Normal file
24
app/components/ui/PageHeaderAction.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<component
|
||||
:is="to ? NuxtLink : 'button'"
|
||||
:to="to"
|
||||
class="btn btn-sm btn-ghost gap-2"
|
||||
@click="!to && $emit('click')"
|
||||
>
|
||||
<Icon v-if="icon" :name="icon" size="16" />
|
||||
<slot />
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { NuxtLink } from '#components'
|
||||
|
||||
defineProps<{
|
||||
to?: string
|
||||
icon?: string
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
(e: 'click'): void
|
||||
}>()
|
||||
</script>
|
||||
21
app/components/ui/Pill.stories.ts
Normal file
21
app/components/ui/Pill.stories.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3'
|
||||
import StoryComponent from './Pill.vue'
|
||||
|
||||
const meta: Meta<typeof StoryComponent> = {
|
||||
title: 'Ui/Pill',
|
||||
component: StoryComponent,
|
||||
render: (args) => ({
|
||||
components: { StoryComponent },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<StoryComponent v-bind="args" />'
|
||||
})
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Primary: Story = {
|
||||
args: {}
|
||||
}
|
||||
42
app/components/ui/Pill.vue
Normal file
42
app/components/ui/Pill.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<span :class="pillClass">
|
||||
<slot />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'neutral', // neutral | primary | outline | inverse
|
||||
},
|
||||
tone: {
|
||||
type: String,
|
||||
default: 'default', // default | success | warning
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'md', // sm | md
|
||||
},
|
||||
})
|
||||
|
||||
const variantMap: Record<string, string> = {
|
||||
neutral: 'badge-neutral',
|
||||
primary: 'badge-primary',
|
||||
outline: 'badge-outline',
|
||||
inverse: 'badge-ghost bg-white/10 text-white border border-white/40',
|
||||
}
|
||||
|
||||
const toneMap: Record<string, string> = {
|
||||
default: '',
|
||||
success: 'badge-success',
|
||||
warning: 'badge-warning',
|
||||
}
|
||||
|
||||
const pillClass = computed(() => {
|
||||
const base = ['badge', props.size === 'sm' ? 'badge-sm' : 'badge-md']
|
||||
const variantClass = variantMap[props.variant] || variantMap.neutral
|
||||
const toneClass = toneMap[props.tone] || ''
|
||||
return [base, variantClass, toneClass].flat().filter(Boolean).join(' ')
|
||||
})
|
||||
</script>
|
||||
21
app/components/ui/Section.stories.ts
Normal file
21
app/components/ui/Section.stories.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3'
|
||||
import StoryComponent from './Section.vue'
|
||||
|
||||
const meta: Meta<typeof StoryComponent> = {
|
||||
title: 'Ui/Section',
|
||||
component: StoryComponent,
|
||||
render: (args) => ({
|
||||
components: { StoryComponent },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<StoryComponent v-bind="args" />'
|
||||
})
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Primary: Story = {
|
||||
args: {}
|
||||
}
|
||||
35
app/components/ui/Section.vue
Normal file
35
app/components/ui/Section.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<section :class="sectionClass">
|
||||
<slot />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'default',
|
||||
},
|
||||
paddingY: {
|
||||
type: String,
|
||||
default: 'lg',
|
||||
},
|
||||
})
|
||||
|
||||
const variantMap: Record<string, string> = {
|
||||
default: 'bg-base-200 text-base-content',
|
||||
hero: 'bg-primary text-primary-content rounded-box overflow-hidden px-6',
|
||||
plain: '',
|
||||
}
|
||||
|
||||
const paddingMap: Record<string, string> = {
|
||||
md: 'py-10',
|
||||
lg: 'py-12 lg:py-16',
|
||||
}
|
||||
|
||||
const sectionClass = computed(() => {
|
||||
const variantClass = variantMap[props.variant] ?? variantMap.default
|
||||
const pyClass = paddingMap[props.paddingY] ?? paddingMap.lg
|
||||
return `${variantClass} ${pyClass}`.trim()
|
||||
})
|
||||
</script>
|
||||
31
app/components/ui/SegmentedControl.vue
Normal file
31
app/components/ui/SegmentedControl.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<div role="tablist" class="tabs tabs-boxed">
|
||||
<button
|
||||
v-for="option in options"
|
||||
:key="option.value"
|
||||
type="button"
|
||||
role="tab"
|
||||
class="tab"
|
||||
:class="{ 'tab-active': modelValue === option.value }"
|
||||
@click="$emit('update:modelValue', option.value)"
|
||||
>
|
||||
{{ option.label }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Option {
|
||||
value: string
|
||||
label: string
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
modelValue: string
|
||||
options: Option[]
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
(e: 'update:modelValue', value: string): void
|
||||
}>()
|
||||
</script>
|
||||
21
app/components/ui/Select.stories.ts
Normal file
21
app/components/ui/Select.stories.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3'
|
||||
import StoryComponent from './Select.vue'
|
||||
|
||||
const meta: Meta<typeof StoryComponent> = {
|
||||
title: 'Ui/Select',
|
||||
component: StoryComponent,
|
||||
render: (args) => ({
|
||||
components: { StoryComponent },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<StoryComponent v-bind="args" />'
|
||||
})
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Primary: Story = {
|
||||
args: {}
|
||||
}
|
||||
5
app/components/ui/Select.vue
Normal file
5
app/components/ui/Select.vue
Normal file
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<select v-bind="$attrs" class="select select-bordered w-full">
|
||||
<slot />
|
||||
</select>
|
||||
</template>
|
||||
21
app/components/ui/Spinner.stories.ts
Normal file
21
app/components/ui/Spinner.stories.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3'
|
||||
import StoryComponent from './Spinner.vue'
|
||||
|
||||
const meta: Meta<typeof StoryComponent> = {
|
||||
title: 'Ui/Spinner',
|
||||
component: StoryComponent,
|
||||
render: (args) => ({
|
||||
components: { StoryComponent },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<StoryComponent v-bind="args" />'
|
||||
})
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Primary: Story = {
|
||||
args: {}
|
||||
}
|
||||
23
app/components/ui/Spinner.vue
Normal file
23
app/components/ui/Spinner.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<span :class="spinnerClass" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
size: {
|
||||
type: String,
|
||||
default: 'md', // sm | md | lg
|
||||
},
|
||||
})
|
||||
|
||||
const sizeMap: Record<string, string> = {
|
||||
sm: 'loading-sm',
|
||||
md: 'loading-md',
|
||||
lg: 'loading-lg',
|
||||
}
|
||||
|
||||
const spinnerClass = computed(() => {
|
||||
const sizeClass = sizeMap[props.size] || sizeMap.md
|
||||
return ['loading loading-spinner', sizeClass].join(' ')
|
||||
})
|
||||
</script>
|
||||
21
app/components/ui/Stack.stories.ts
Normal file
21
app/components/ui/Stack.stories.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3'
|
||||
import StoryComponent from './Stack.vue'
|
||||
|
||||
const meta: Meta<typeof StoryComponent> = {
|
||||
title: 'Ui/Stack',
|
||||
component: StoryComponent,
|
||||
render: (args) => ({
|
||||
components: { StoryComponent },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<StoryComponent v-bind="args" />'
|
||||
})
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Primary: Story = {
|
||||
args: {}
|
||||
}
|
||||
74
app/components/ui/Stack.vue
Normal file
74
app/components/ui/Stack.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<component
|
||||
:is="tag"
|
||||
:class="['flex', directionClass, gapClass, alignClass, justifyClass, wrapClass, fullHeightClass]"
|
||||
>
|
||||
<slot />
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
tag: {
|
||||
type: String,
|
||||
default: 'div',
|
||||
},
|
||||
gap: {
|
||||
type: [String, Number],
|
||||
default: 6,
|
||||
},
|
||||
direction: {
|
||||
type: String,
|
||||
default: 'col', // col | row
|
||||
},
|
||||
align: {
|
||||
type: String,
|
||||
default: 'stretch', // start | center | end | stretch
|
||||
},
|
||||
justify: {
|
||||
type: String,
|
||||
default: 'start', // start | center | end | between
|
||||
},
|
||||
wrap: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
fullHeight: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const gapMap: Record<string, string> = {
|
||||
'1': 'gap-1',
|
||||
'2': 'gap-2',
|
||||
'3': 'gap-3',
|
||||
'4': 'gap-4',
|
||||
'5': 'gap-5',
|
||||
'6': 'gap-6',
|
||||
'8': 'gap-8',
|
||||
'10': 'gap-10',
|
||||
'12': 'gap-12',
|
||||
}
|
||||
|
||||
const alignMap: Record<string, string> = {
|
||||
start: 'items-start',
|
||||
center: 'items-center',
|
||||
end: 'items-end',
|
||||
stretch: 'items-stretch',
|
||||
}
|
||||
|
||||
const justifyMap: Record<string, string> = {
|
||||
start: 'justify-start',
|
||||
center: 'justify-center',
|
||||
end: 'justify-end',
|
||||
between: 'justify-between',
|
||||
}
|
||||
|
||||
const gapClass = computed(() => gapMap[String(props.gap)] || gapMap['6'])
|
||||
const alignClass = computed(() => alignMap[props.align] || alignMap.start)
|
||||
const justifyClass = computed(() => justifyMap[props.justify] || justifyMap.start)
|
||||
const directionClass = computed(() => (props.direction === 'row' ? 'flex-row' : 'flex-col'))
|
||||
const wrapClass = computed(() => (props.wrap ? 'flex-wrap' : ''))
|
||||
const fullHeightClass = computed(() => (props.fullHeight ? 'min-h-screen' : ''))
|
||||
</script>
|
||||
21
app/components/ui/Text.stories.ts
Normal file
21
app/components/ui/Text.stories.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3'
|
||||
import StoryComponent from './Text.vue'
|
||||
|
||||
const meta: Meta<typeof StoryComponent> = {
|
||||
title: 'Ui/Text',
|
||||
component: StoryComponent,
|
||||
render: (args) => ({
|
||||
components: { StoryComponent },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<StoryComponent v-bind="args" />'
|
||||
})
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Primary: Story = {
|
||||
args: {}
|
||||
}
|
||||
79
app/components/ui/Text.vue
Normal file
79
app/components/ui/Text.vue
Normal file
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<component :is="tag" :class="textClass">
|
||||
<slot />
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
tag: {
|
||||
type: String,
|
||||
default: 'p',
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'body', // body | base
|
||||
},
|
||||
tone: {
|
||||
type: String,
|
||||
default: 'default', // default | muted | inverse
|
||||
},
|
||||
weight: {
|
||||
type: String,
|
||||
default: 'normal', // normal | semibold
|
||||
},
|
||||
align: {
|
||||
type: String,
|
||||
default: 'start', // start | center | end
|
||||
},
|
||||
transform: {
|
||||
type: String,
|
||||
default: 'none', // none | uppercase
|
||||
},
|
||||
mono: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const sizeMap: Record<string, string> = {
|
||||
body: 'text-base',
|
||||
base: 'text-base',
|
||||
sm: 'text-sm',
|
||||
caption: 'text-sm',
|
||||
}
|
||||
|
||||
const toneMap: Record<string, string> = {
|
||||
default: 'text-base-content',
|
||||
muted: 'text-base-content/70',
|
||||
inverse: 'text-primary-content/90',
|
||||
}
|
||||
|
||||
const weightMap: Record<string, string> = {
|
||||
normal: 'font-normal',
|
||||
semibold: 'font-semibold',
|
||||
}
|
||||
|
||||
const alignMap: Record<string, string> = {
|
||||
start: 'text-left',
|
||||
center: 'text-center',
|
||||
end: 'text-right',
|
||||
}
|
||||
|
||||
const transformMap: Record<string, string> = {
|
||||
none: '',
|
||||
uppercase: 'uppercase tracking-[0.08em]',
|
||||
}
|
||||
|
||||
const textClass = computed(() => {
|
||||
const sizeClass = sizeMap[props.size] || sizeMap.body
|
||||
const toneClass = toneMap[props.tone] || toneMap.default
|
||||
const weightClass = weightMap[props.weight] || weightMap.normal
|
||||
const alignClass = alignMap[props.align] || alignMap.start
|
||||
const transformClass = transformMap[props.transform] || ''
|
||||
const familyClass = props.mono ? 'font-mono' : ''
|
||||
return [sizeClass, toneClass, weightClass, alignClass, transformClass, familyClass]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
})
|
||||
</script>
|
||||
21
app/components/ui/Textarea.stories.ts
Normal file
21
app/components/ui/Textarea.stories.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3'
|
||||
import StoryComponent from './Textarea.vue'
|
||||
|
||||
const meta: Meta<typeof StoryComponent> = {
|
||||
title: 'Ui/Textarea',
|
||||
component: StoryComponent,
|
||||
render: (args) => ({
|
||||
components: { StoryComponent },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<StoryComponent v-bind="args" />'
|
||||
})
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Primary: Story = {
|
||||
args: {}
|
||||
}
|
||||
44
app/components/ui/Textarea.vue
Normal file
44
app/components/ui/Textarea.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<label v-if="label" class="w-full space-y-1">
|
||||
<span class="text-base font-semibold text-base-content">{{ label }}</span>
|
||||
<textarea
|
||||
v-bind="$attrs"
|
||||
:class="fieldClass"
|
||||
:value="modelValue"
|
||||
@input="onInput"
|
||||
/>
|
||||
</label>
|
||||
<textarea
|
||||
v-else
|
||||
v-bind="$attrs"
|
||||
:class="fieldClass"
|
||||
:value="modelValue"
|
||||
@input="onInput"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
mono: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const onInput = (event: Event) => {
|
||||
emit('update:modelValue', (event.target as HTMLTextAreaElement).value)
|
||||
}
|
||||
|
||||
const fieldClass = computed(() =>
|
||||
['textarea textarea-bordered w-full min-h-[120px]', props.mono ? 'font-mono' : ''].join(' ')
|
||||
)
|
||||
</script>
|
||||
Reference in New Issue
Block a user