Initial commit from monorepo

This commit is contained in:
Ruslan Bakiev
2026-01-07 09:10:35 +07:00
commit 3db50d9637
371 changed files with 43223 additions and 0 deletions

View 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: {}
}

View 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>

View 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>

View 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
}
}

View 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>

View 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: {}
}

View 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>

View 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: {}
}

View 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>

View 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>

View 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: {}
}

View 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>

View 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: {}
}

View 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>

View 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: {}
}

View 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>

View 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: {}
}

View 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>

View 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: {}
}

View 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>

View 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: {}
}

View 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>

View 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>

View 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>

View 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: {}
}

View 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>

View 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: {}
}

View 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>

View 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>

View 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: {}
}

View File

@@ -0,0 +1,5 @@
<template>
<select v-bind="$attrs" class="select select-bordered w-full">
<slot />
</select>
</template>

View 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: {}
}

View 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>

View 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: {}
}

View 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>

View 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: {}
}

View 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>

View 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: {}
}

View 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>