refactor: solidify the FE codebase and improve UI consistency (#45)

* chore: add storybook

* chore: unify FE codeabse

* chore: update eslint rules

* chore: harmonize the use of "subtle" color

* chore: remove an extra sidebar rail

* refactor: make panel items more consistent

* chore: cleanups after merging new code from base

* refactor: refine composables

* fix: add lost import

* chore: make icon style consistent

* fix: don't show empty "supported" methods

* refactor: solidify select items
This commit is contained in:
Mazen Touati
2026-01-25 14:30:07 +01:00
committed by GitHub
parent 2895a0ddc6
commit 35b96042f0
368 changed files with 13230 additions and 6838 deletions

1
.gitignore vendored
View File

@@ -17,6 +17,7 @@ _ide_helper_models.php
**/*/.DS_Store
tools/phpstan/build
resources/dist/hot
storybook-static
# Playwright
node_modules/

29
.storybook/main.ts Normal file
View File

@@ -0,0 +1,29 @@
import type { StorybookConfig } from '@storybook/vue3-vite';
import path from 'node:path';
const config: StorybookConfig = {
stories: [
'../resources/js/components/**/*.stories.@(js|jsx|ts|tsx|mdx)',
],
addons: [
'@storybook/addon-essentials',
'@storybook/addon-interactions',
'@storybook/addon-a11y',
],
framework: {
name: '@storybook/vue3-vite',
options: {},
},
viteFinal: async (config) => {
config.resolve = config.resolve ?? {};
config.resolve.alias = {
...config.resolve.alias,
'@': path.resolve(__dirname, '../resources/js'),
'~': path.resolve(__dirname, '../resources/css'),
};
return config;
},
};
export default config;

22
.storybook/preview.ts Normal file
View File

@@ -0,0 +1,22 @@
import type { Preview } from '@storybook/vue3';
import '../resources/css/app.css';
const preview: Preview = {
parameters: {
backgrounds: {
default: 'light',
values: [
{ name: 'light', value: '#ffffff' },
{ name: 'dark', value: '#09090b' },
],
},
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
};
export default preview;

1756
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -17,13 +17,20 @@
"test:watch": "npm run test -- --watch",
"test:snapshot": "npm run test:run --reporter=verbose --update",
"test:e2e": "npx playwright test -c ./tests/E2E/playwright.config.ts",
"test:e2e:ui": "npm run test:e2e -- --ui"
"test:e2e:ui": "npm run test:e2e -- --ui",
"storybook": "storybook dev -p 6006",
"storybook:build": "storybook build"
},
"devDependencies": {
"@playwright/test": "^1.56.1",
"@rushstack/eslint-patch": "^1.8.0",
"@storybook/addon-a11y": "^8.6.14",
"@storybook/addon-essentials": "^8.6.14",
"@storybook/addon-interactions": "^8.6.14",
"@storybook/vue3-vite": "^8.6.14",
"@tailwindcss/postcss": "^4.0.6",
"@tailwindcss/vite": "^4.1.13",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.8.0",
"@testing-library/user-event": "^14.6.1",
"@testing-library/vue": "^8.1.0",
@@ -53,6 +60,7 @@
"prettier-plugin-organize-imports": "^4.0.0",
"prettier-plugin-tailwindcss": "^0.6.11",
"radix-vue": "^1.9.14",
"storybook": "^8.6.14",
"tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.13",
"tailwindcss-animate": "^1.0.7",

View File

@@ -51,16 +51,17 @@
}
.cm-gutters {
background-color: var(--color-subtle-background) !important;
background-color: var(--color-subtle) !important;
border-color: var(--color-border) !important;
}
.cm-activeLineGutter, .cm-activeLine {
.cm-activeLineGutter,
.cm-activeLine {
background-color: var(--color-accent) !important;
}
.cm-line span {
color: var(--color-muted-foreground) !important;
color: var(--color-subtle-foreground) !important;
}
}
@@ -84,22 +85,18 @@
*/
.p-panel {
padding: var(--panel-padding-x);
padding: var(--panel-padding);
}
.px-panel {
padding-inline: var(--panel-padding-x);
padding-inline: var(--panel-padding);
}
.pl-panel {
padding-left: var(--panel-padding-x);
padding-left: var(--panel-padding);
}
.pr-panel {
padding-right: var(--panel-padding-x);
}
.text-subtle {
color: var(--color-subtle-foreground);
padding-right: var(--panel-padding);
}
}

View File

@@ -10,7 +10,7 @@
--sub-toolbar-height: 34px;
/* Panel Content Padding */
--panel-padding-x: calc(var(--spacing) * 2);
--panel-padding: calc(var(--spacing) * 2);
/* Additional Layout Variables */
--header-height: 60px;

View File

@@ -19,10 +19,8 @@
/* Base Colors */
--color-background: oklch(1 0 0);
--color-foreground: oklch(0.279 0.02 262.86);
--color-subtle-foreground: var(--color-zinc-600);
--color-subtle-background: var(--color-zinc-50);
--color-muted: oklch(0.96 0.015 240.01);
--color-muted-foreground: oklch(0.48 0.024 262.37);
--color-subtle: hsl(0 0% 98%);
--color-subtle-foreground: oklch(0.48 0.024 262.37);
/* Surface Colors */
--color-card: oklch(1 0 0);
@@ -42,19 +40,19 @@
--color-secondary-foreground: oklch(21.03% 0.0318 264.65);
--color-accent: oklch(96.71% 0.0029 264.54);
--color-accent-foreground: oklch(21.03% 0.0318 264.65);
--color-destructive: oklch(63.68% 0.2078 25.33);
--color-destructive: var(--color-rose-500);
--color-destructive-foreground: oklch(98.43% 0.0017 247.84);
/* Status Colors */
--color-success: oklch(60% 0.15 140);
--color-success: var(--color-emerald-600);
--color-success-foreground: oklch(98% 0.01 140);
--color-warning: oklch(70% 0.15 60);
--color-warning: var(--color-amber-600);
--color-warning-foreground: oklch(20% 0.01 60);
--color-info: oklch(60% 0.15 240);
--color-info-foreground: oklch(98% 0.01 240);
/* Sidebar Colors */
--sidebar-background: hsl(0 0% 98%);
--sidebar-background: var(--color-subtle);
--sidebar-foreground: hsl(240 5.3% 26.1%);
--sidebar-primary: hsl(240 5.9% 10%);
--sidebar-primary-foreground: hsl(0 0% 98%);
@@ -72,10 +70,8 @@
/* Base Colors */
--color-background: var(--color-zinc-950);
--color-foreground: oklch(1 0 0);
--color-subtle-background: oklch(0.203 0.004 266);
--color-subtle: var(--color-zinc-900);
--color-subtle-foreground: var(--color-zinc-300);
--color-muted: var(--color-gray-800);
--color-muted-foreground: var(--color-zinc-300);
/* Surface Colors */
--color-card: oklch(12.94% 0.0273 261.67);
@@ -107,12 +103,12 @@
--color-info-foreground: oklch(15% 0.01 240);
/* Sidebar Colors */
--sidebar-background: var(--color-subtle-background);
--sidebar-background: var(--color-subtle);
--sidebar-foreground: hsl(240 4.8% 95.9%);
--sidebar-primary: hsl(224.3 76.3% 48%);
--sidebar-primary-foreground: hsl(0 0% 100%);
--sidebar-accent: var(--color-accent);
--sidebar-accent-foreground: hsl( 240 4.8% 95.9%);
--sidebar-accent-foreground: hsl(240 4.8% 95.9%);
--sidebar-border: hsl(240 3.7% 15.9%);
--sidebar-ring: hsl(217.2 91.2% 59.8%);
}

View File

@@ -6,7 +6,8 @@
*/
import { httpClientConfig } from '@/config';
import axios, { AxiosResponse, InternalAxiosRequestConfig } from 'axios';
import type { AxiosResponse, InternalAxiosRequestConfig } from 'axios';
import axios from 'axios';
// Make Axios globally available (legacy compatibility and convenience)
window.axios = axios;

View File

@@ -0,0 +1,83 @@
<script setup lang="ts">
/**
* @component ExampleComponent
* @description Brief description of the component's purpose.
*
* USAGE:
* Copy this template when creating new components to ensure consistent
* Props/Emits contracts and TypeScript interfaces.
*/
import { computed } from 'vue';
/*
* Types & Interfaces.
*/
/**
* Props interface - defines all accepted properties.
* Export this interface if consumers need to type-check component usage.
*/
export interface ExampleComponentProps {
/** Primary data to display (required) */
modelValue: string;
/** Visual variant of the component */
variant?: 'default' | 'primary' | 'destructive';
/** Disabled state - prevents user interaction */
disabled?: boolean;
}
/**
* Emits interface - defines all events with payload types.
* Using type-based declaration for ESLint compliance.
*/
export interface ExampleComponentEmits {
(event: 'update:modelValue', value: string): void;
(event: 'submit'): void;
(event: 'error', error: Error): void;
}
/*
* Component Setup.
*/
const props = withDefaults(defineProps<ExampleComponentProps>(), {
variant: 'default',
disabled: false,
});
const emit = defineEmits<ExampleComponentEmits>();
/*
* Computed & Methods.
*/
const computedClasses = computed(() => ({
'is-disabled': props.disabled,
[`variant-${props.variant}`]: true,
}));
function handleSubmit(): void {
if (props.disabled) {
return;
}
emit('submit');
}
function _handleError(error: Error): void {
emit('error', error);
}
</script>
<template>
<div :class="computedClasses">
<!-- Default slot for content -->
<slot />
<!-- Named slot example -->
<slot name="actions">
<button type="button" :disabled="disabled" @click="handleSubmit">
Submit
</button>
</slot>
</div>
</template>

View File

@@ -1,5 +1,15 @@
<script setup lang="ts">
/**
* @component AppPanelRipple
* @description A decorative ripple effect container used in panel backgrounds.
*/
import AppRipple from '@/components/base/ripple/AppRipple.vue';
/*
* Types & Interfaces.
*/
export interface AppPanelRippleProps {}
</script>
<template>

View File

@@ -1,13 +1,25 @@
<script setup lang="ts">
/**
* @component AppPanelStateContainer
* @description A layout container for panel content with an integrated ripple background effect.
*/
import AppPanelRipple from '@/components/base/AppPanelRipple.vue';
import { cn } from '@/utils/ui';
import type { HTMLAttributes } from 'vue';
interface Props {
/*
* Types & Interfaces.
*/
export interface AppPanelStateContainerProps {
class?: HTMLAttributes['class'];
}
const props = defineProps<Props>();
/*
* Component Setup.
*/
const props = defineProps<AppPanelStateContainerProps>();
</script>
<template>

View File

@@ -0,0 +1,40 @@
import type { Meta, StoryObj } from '@storybook/vue3';
import { AppBadge } from './index';
const meta: Meta<typeof AppBadge> = {
title: 'Base/Badge',
component: AppBadge,
tags: ['autodocs'],
argTypes: {
variant: {
control: 'select',
options: ['default', 'secondary', 'destructive', 'outline'],
},
},
};
export default meta;
type Story = StoryObj<typeof AppBadge>;
export const Default: Story = {
args: { variant: 'default' },
render: args => ({
components: { AppBadge },
setup: () => ({ args }),
template: '<AppBadge v-bind="args">Badge</AppBadge>',
}),
};
export const AllVariants: Story = {
render: () => ({
components: { AppBadge },
template: `
<div class="flex flex-wrap gap-4">
<AppBadge variant="default">Default</AppBadge>
<AppBadge variant="secondary">Secondary</AppBadge>
<AppBadge variant="destructive">Destructive</AppBadge>
<AppBadge variant="outline">Outline</AppBadge>
</div>
`,
}),
};

View File

@@ -1,14 +1,26 @@
<script setup lang="ts">
/**
* @component AppBadge
* @description A small visual indicator for status, categories, or labels.
*/
import { cn } from '@/utils/ui';
import type { HTMLAttributes } from 'vue';
import { type BadgeVariants, badgeVariants } from './index';
type BadgeInterface = {
/*
* Types & Interfaces.
*/
export interface AppBadgeProps {
variant?: BadgeVariants['variant'];
class?: HTMLAttributes['class'];
};
}
const props = defineProps<BadgeInterface>();
/*
* Component Setup.
*/
const props = defineProps<AppBadgeProps>();
</script>
<template>

View File

@@ -3,17 +3,17 @@ import { cva, type VariantProps } from 'class-variance-authority';
export { default as AppBadge } from './AppBadge.vue';
export const badgeVariants = cva(
'inline-flex items-center rounded-sm border border-zinc-200 px-1.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-zinc-950 focus-visible:ring-offset-2 dark:border-zinc-800 dark:focus-visible:ring-zinc-300',
'inline-flex items-center rounded-sm border border-border px-1.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-2',
{
variants: {
variant: {
default:
'border-transparent bg-zinc-900 text-zinc-50 shadow-sm hover:bg-zinc-900/80 dark:bg-zinc-50 dark:text-zinc-900 dark:hover:bg-zinc-50/80',
'border-transparent bg-primary text-primary-foreground shadow-sm hover:bg-primary/80',
secondary:
'border-transparent bg-zinc-100 text-zinc-900 hover:bg-zinc-100/80 dark:bg-zinc-800 dark:text-zinc-50 dark:hover:bg-zinc-800/80',
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
destructive:
'border-transparent bg-red-500 text-zinc-50 shadow-sm hover:bg-red-500/80 dark:bg-red-900 dark:text-zinc-50 dark:hover:bg-red-900/80',
outline: 'text-zinc-950 dark:text-zinc-50',
'border-transparent bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/80',
outline: 'text-foreground',
},
},
defaultVariants: {

View File

@@ -0,0 +1,51 @@
import type { Meta, StoryObj } from '@storybook/vue3';
import { AppButton } from './index';
const meta: Meta<typeof AppButton> = {
title: 'Base/Button',
component: AppButton,
tags: ['autodocs'],
argTypes: {
variant: {
control: 'select',
options: ['default', 'destructive', 'outline', 'secondary', 'ghost', 'link'],
},
size: {
control: 'select',
options: ['default', 'xs', 'sm', 'lg', 'icon'],
},
},
};
export default meta;
type Story = StoryObj<typeof AppButton>;
export const Default: Story = {
args: {
variant: 'default',
size: 'default',
},
render: args => ({
components: { AppButton },
setup() {
return { args };
},
template: '<AppButton v-bind="args">Button</AppButton>',
}),
};
export const Variants: Story = {
render: () => ({
components: { AppButton },
template: `
<div class="flex flex-wrap gap-4">
<AppButton variant="default">Default</AppButton>
<AppButton variant="destructive">Destructive</AppButton>
<AppButton variant="outline">Outline</AppButton>
<AppButton variant="secondary">Secondary</AppButton>
<AppButton variant="ghost">Ghost</AppButton>
<AppButton variant="link">Link</AppButton>
</div>
`,
}),
};

View File

@@ -1,16 +1,28 @@
<script setup lang="ts">
/**
* @component AppButton
* @description Standard button component with multiple variants and sizes.
*/
import { cn } from '@/utils/ui';
import { Primitive, type PrimitiveProps } from 'reka-ui';
import type { HTMLAttributes } from 'vue';
import { type ButtonVariants, buttonVariants } from './index';
export interface Props extends PrimitiveProps {
/*
* Types & Interfaces.
*/
export interface AppButtonProps extends PrimitiveProps {
variant?: ButtonVariants['variant'];
size?: ButtonVariants['size'];
class?: HTMLAttributes['class'];
}
const props = withDefaults(defineProps<Props>(), {
/*
* Component Setup.
*/
const props = withDefaults(defineProps<AppButtonProps>(), {
as: 'button',
variant: 'default',
size: 'default',

View File

@@ -1,14 +1,26 @@
<script setup lang="ts">
import { Props } from '@/components/base/button/AppButton.vue';
/**
* @component AppGlowingButton
* @description A button with a glowing animated border effect.
*/
import { type AppButtonProps } from '@/components/base/button/AppButton.vue';
import { AppButton } from '@/components/base/button/index';
import AppBorderBeam from '@/components/base/glow-border/AppBorderBeam.vue';
import { cn } from '@/utils/ui';
interface AppGlowingButtonProps extends Props {
/*
* Types & Interfaces.
*/
export interface AppGlowingButtonProps extends AppButtonProps {
duration?: number;
beamSize?: number;
}
/*
* Component Setup.
*/
const props = withDefaults(defineProps<AppGlowingButtonProps>(), {
size: 'xs',
variant: 'secondary',

View File

@@ -8,15 +8,15 @@ export const buttonVariants = cva(
variants: {
variant: {
default:
'bg-zinc-900 text-zinc-50 shadow-sm hover:bg-zinc-900/90 dark:bg-zinc-50 dark:text-zinc-900 dark:hover:bg-zinc-50/90',
'bg-primary text-primary-foreground shadow-sm hover:bg-primary/90',
destructive:
'bg-red-500 text-zinc-50 shadow-sm hover:bg-red-500/90 dark:bg-red-900 dark:text-zinc-50 dark:hover:bg-red-900/90',
'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
outline:
'border border-zinc-200 bg-white shadow-sm hover:bg-zinc-100 hover:text-zinc-900 dark:border-zinc-800 dark:bg-zinc-950 dark:hover:bg-zinc-800 dark:hover:text-zinc-50',
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
secondary:
'bg-zinc-100 text-zinc-900 shadow-sm hover:bg-zinc-100/80 dark:bg-zinc-800 dark:text-zinc-50 dark:hover:bg-zinc-800/80',
ghost: 'hover:bg-zinc-100 hover:text-zinc-900 dark:hover:bg-zinc-800 dark:hover:text-zinc-50',
link: 'text-zinc-900 underline-offset-4 hover:underline dark:text-zinc-50',
'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-9 px-4 py-2',

View File

@@ -1,4 +1,8 @@
<script setup lang="ts">
/**
* @component AppCallout
* @description A callout component for displaying important information, warnings, or success messages.
*/
import { cn } from '@/utils/ui';
import {
AlertTriangleIcon,
@@ -8,18 +12,30 @@ import {
} from 'lucide-vue-next';
import type { HTMLAttributes } from 'vue';
interface AppCalloutProps {
/*
* Types & Interfaces.
*/
export interface AppCalloutProps {
class?: HTMLAttributes['class'];
variant?: 'default' | 'info' | 'success' | 'warning' | 'destructive';
title?: string;
}
/*
* Component Setup.
*/
const props = withDefaults(defineProps<AppCalloutProps>(), {
variant: 'default',
class: '',
title: '',
});
/*
* Computed & Methods.
*/
const iconMap = {
default: InfoIcon,
info: InfoIcon,

View File

@@ -0,0 +1,46 @@
import type { Meta, StoryObj } from '@storybook/vue3';
import {
AppCard,
AppCardContent,
AppCardDescription,
AppCardFooter,
AppCardHeader,
AppCardTitle,
} from './index';
const meta: Meta<typeof AppCard> = {
title: 'Base/Card',
component: AppCard,
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<typeof AppCard>;
export const Default: Story = {
render: () => ({
components: {
AppCard,
AppCardContent,
AppCardDescription,
AppCardFooter,
AppCardHeader,
AppCardTitle,
},
template: `
<AppCard class="w-[350px]">
<AppCardHeader>
<AppCardTitle>Card Title</AppCardTitle>
<AppCardDescription>Card Description providing more context.</AppCardDescription>
</AppCardHeader>
<AppCardContent>
<p>This is the main content of the card. It can contain anything.</p>
</AppCardContent>
<AppCardFooter class="flex justify-between">
<button class="px-4 py-2 text-sm font-medium text-zinc-900 bg-zinc-100 rounded-md">Cancel</button>
<button class="px-4 py-2 text-sm font-medium text-zinc-50 bg-zinc-900 rounded-md">Deploy</button>
</AppCardFooter>
</AppCard>
`,
}),
};

View File

@@ -1,10 +1,24 @@
<script setup lang="ts">
/**
* @component AppCard
* @description Root container for card layouts.
*/
import { cn } from '@/utils/ui';
import type { HTMLAttributes } from 'vue';
const props = defineProps<{
/*
* Types & Interfaces.
*/
export interface AppCardProps {
class?: HTMLAttributes['class'];
}>();
}
/*
* Component Setup.
*/
const props = defineProps<AppCardProps>();
</script>
<template>

View File

@@ -1,10 +1,24 @@
<script setup lang="ts">
/**
* @component AppCardContent
* @description Main content area of a card.
*/
import { cn } from '@/utils/ui';
import type { HTMLAttributes } from 'vue';
const props = defineProps<{
/*
* Types & Interfaces.
*/
export interface AppCardContentProps {
class?: HTMLAttributes['class'];
}>();
}
/*
* Component Setup.
*/
const props = defineProps<AppCardContentProps>();
</script>
<template>

View File

@@ -1,14 +1,28 @@
<script setup lang="ts">
/**
* @component AppCardDescription
* @description Supporting text for a card title.
*/
import { cn } from '@/utils/ui';
import type { HTMLAttributes } from 'vue';
const props = defineProps<{
/*
* Types & Interfaces.
*/
export interface AppCardDescriptionProps {
class?: HTMLAttributes['class'];
}>();
}
/*
* Component Setup.
*/
const props = defineProps<AppCardDescriptionProps>();
</script>
<template>
<p :class="cn('text-subtle text-sm', props.class)">
<p :class="cn('text-subtle-foreground text-sm', props.class)">
<slot />
</p>
</template>

View File

@@ -1,10 +1,24 @@
<script setup lang="ts">
/**
* @component AppCardFooter
* @description Footer container for card actions or metadata.
*/
import { cn } from '@/utils/ui';
import type { HTMLAttributes } from 'vue';
const props = defineProps<{
/*
* Types & Interfaces.
*/
export interface AppCardFooterProps {
class?: HTMLAttributes['class'];
}>();
}
/*
* Component Setup.
*/
const props = defineProps<AppCardFooterProps>();
</script>
<template>

View File

@@ -1,10 +1,24 @@
<script setup lang="ts">
/**
* @component AppCardHeader
* @description Header container for card title and description.
*/
import { cn } from '@/utils/ui';
import type { HTMLAttributes } from 'vue';
const props = defineProps<{
/*
* Types & Interfaces.
*/
export interface AppCardHeaderProps {
class?: HTMLAttributes['class'];
}>();
}
/*
* Component Setup.
*/
const props = defineProps<AppCardHeaderProps>();
</script>
<template>

View File

@@ -1,10 +1,24 @@
<script setup lang="ts">
/**
* @component AppCardTitle
* @description Main heading for a card.
*/
import { cn } from '@/utils/ui';
import type { HTMLAttributes } from 'vue';
const props = defineProps<{
/*
* Types & Interfaces.
*/
export interface AppCardTitleProps {
class?: HTMLAttributes['class'];
}>();
}
/*
* Component Setup.
*/
const props = defineProps<AppCardTitleProps>();
</script>
<template>

View File

@@ -0,0 +1,36 @@
import type { Meta, StoryObj } from '@storybook/vue3';
import { AppLabel } from '../label';
import { AppCheckbox } from './index';
const meta: Meta<typeof AppCheckbox> = {
title: 'Base/Checkbox',
component: AppCheckbox,
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<typeof AppCheckbox>;
export const Default: Story = {
render: () => ({
components: { AppCheckbox, AppLabel },
template: `
<div class="flex items-center space-x-2">
<AppCheckbox id="terms" />
<AppLabel for="terms">Accept terms and conditions</AppLabel>
</div>
`,
}),
};
export const Disabled: Story = {
render: () => ({
components: { AppCheckbox, AppLabel },
template: `
<div class="flex items-center space-x-2">
<AppCheckbox id="terms2" disabled />
<AppLabel for="terms2" class="opacity-50">Disabled checkbox</AppLabel>
</div>
`,
}),
};

View File

@@ -1,11 +1,27 @@
<script setup lang="ts">
/**
* @component AppCheckbox
* @description A boolean input component for toggling states.
*/
import { cn } from '@/utils/ui';
import { Check } from 'lucide-vue-next';
import type { CheckboxRootEmits, CheckboxRootProps } from 'reka-ui';
import { CheckboxIndicator, CheckboxRoot, useForwardPropsEmits } from 'reka-ui';
import { computed, type HTMLAttributes } from 'vue';
const props = defineProps<CheckboxRootProps & { class?: HTMLAttributes['class'] }>();
/*
* Types & Interfaces.
*/
export interface AppCheckboxProps extends CheckboxRootProps {
class?: HTMLAttributes['class'];
}
/*
* Component Setup.
*/
const props = defineProps<AppCheckboxProps>();
const emits = defineEmits<CheckboxRootEmits>();
const delegatedProps = computed(() => {
@@ -22,7 +38,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
v-bind="forwarded"
:class="
cn(
'peer h-4 w-4 shrink-0 rounded-sm border border-zinc-200 border-zinc-900 shadow focus-visible:ring-1 focus-visible:ring-zinc-950 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-zinc-900 data-[state=checked]:text-zinc-50 dark:border-zinc-50 dark:border-zinc-800 dark:focus-visible:ring-zinc-300 dark:data-[state=checked]:bg-zinc-50 dark:data-[state=checked]:text-zinc-900',
'peer border-input focus-visible:ring-ring data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground h-4 w-4 shrink-0 rounded-sm border shadow focus-visible:ring-1 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50',
props.class,
)
"

View File

@@ -1,8 +1,22 @@
<script setup lang="ts">
/**
* @component AppCollapsible
* @description An interactive component that expands/collapses content.
*/
import type { CollapsibleRootEmits, CollapsibleRootProps } from 'reka-ui';
import { CollapsibleRoot, useForwardPropsEmits } from 'reka-ui';
const props = defineProps<CollapsibleRootProps>();
/*
* Types & Interfaces.
*/
export interface AppCollapsibleProps extends CollapsibleRootProps {}
/*
* Component Setup.
*/
const props = defineProps<AppCollapsibleProps>();
const emits = defineEmits<CollapsibleRootEmits>();
const forwarded = useForwardPropsEmits(props, emits);

View File

@@ -1,7 +1,21 @@
<script setup lang="ts">
/**
* @component AppCollapsibleContent
* @description The content area that expands/collapses within a collapsible.
*/
import { CollapsibleContent, type CollapsibleContentProps } from 'reka-ui';
const props = defineProps<CollapsibleContentProps>();
/*
* Types & Interfaces.
*/
export interface AppCollapsibleContentProps extends CollapsibleContentProps {}
/*
* Component Setup.
*/
const props = defineProps<AppCollapsibleContentProps>();
</script>
<template>

View File

@@ -1,7 +1,21 @@
<script setup lang="ts">
/**
* @component AppCollapsibleTrigger
* @description The interactive element that toggles the collapsible state.
*/
import { CollapsibleTrigger, type CollapsibleTriggerProps } from 'reka-ui';
const props = defineProps<CollapsibleTriggerProps>();
/*
* Types & Interfaces.
*/
export interface AppCollapsibleTriggerProps extends CollapsibleTriggerProps {}
/*
* Component Setup.
*/
const props = defineProps<AppCollapsibleTriggerProps>();
</script>
<template>

View File

@@ -1,4 +1,8 @@
<script setup lang="ts">
/**
* @component AppCommand
* @description The root container for a command palette or search menu.
*/
import { cn } from '@/utils/ui';
import { reactiveOmit } from '@vueuse/core';
import type { ListboxRootEmits, ListboxRootProps } from 'reka-ui';
@@ -7,13 +11,22 @@ import type { HTMLAttributes } from 'vue';
import { reactive, ref, watch } from 'vue';
import { provideCommandContext } from '.';
const props = withDefaults(
defineProps<ListboxRootProps & { class?: HTMLAttributes['class'] }>(),
{
modelValue: '',
class: undefined,
},
);
/*
* Types & Interfaces.
*/
export interface AppCommandProps extends ListboxRootProps {
class?: HTMLAttributes['class'];
}
/*
* Component Setup.
*/
const props = withDefaults(defineProps<AppCommandProps>(), {
modelValue: '',
class: undefined,
});
const emits = defineEmits<ListboxRootEmits>();
@@ -21,6 +34,10 @@ const delegatedProps = reactiveOmit(props, 'class');
const forwarded = useForwardPropsEmits(delegatedProps, emits);
/*
* Computed & Methods.
*/
const allItems = ref<Map<string, string>>(new Map());
const allGroups = ref<Map<string, Set<string>>>(new Map());

View File

@@ -1,4 +1,8 @@
<script setup lang="ts">
/**
* @component AppCommandDialog
* @description A command palette wrapped in a modal dialog.
*/
import {
AppDialog,
AppDialogContent,
@@ -10,18 +14,23 @@ import type { DialogRootEmits, DialogRootProps } from 'reka-ui';
import { useForwardPropsEmits } from 'reka-ui';
import AppCommand from './AppCommand.vue';
const props = withDefaults(
defineProps<
DialogRootProps & {
title?: string;
description?: string;
}
>(),
{
title: 'Command Palette',
description: 'Search for a command to run...',
},
);
/*
* Types & Interfaces.
*/
export interface AppCommandDialogProps extends DialogRootProps {
title?: string;
description?: string;
}
/*
* Component Setup.
*/
const props = withDefaults(defineProps<AppCommandDialogProps>(), {
title: 'Command Palette',
description: 'Search for a command to run...',
});
const emits = defineEmits<DialogRootEmits>();
const forwarded = useForwardPropsEmits(props, emits);

View File

@@ -1,4 +1,8 @@
<script setup lang="ts">
/**
* @component AppCommandEmpty
* @description Renders content when no command items match the filter.
*/
import { cn } from '@/utils/ui';
import { reactiveOmit } from '@vueuse/core';
import type { PrimitiveProps } from 'reka-ui';
@@ -7,10 +11,26 @@ import type { HTMLAttributes } from 'vue';
import { computed } from 'vue';
import { useCommand } from '.';
const props = defineProps<PrimitiveProps & { class?: HTMLAttributes['class'] }>();
/*
* Types & Interfaces.
*/
export interface AppCommandEmptyProps extends PrimitiveProps {
class?: HTMLAttributes['class'];
}
/*
* Component Setup.
*/
const props = defineProps<AppCommandEmptyProps>();
const delegatedProps = reactiveOmit(props, 'class');
/*
* Computed & Methods.
*/
const { filterState } = useCommand();
const isRender = computed(() => !!filterState.search && filterState.filtered.count === 0);
</script>

View File

@@ -1,4 +1,8 @@
<script setup lang="ts">
/**
* @component AppCommandGroup
* @description Groups related command items together with an optional heading.
*/
import { cn } from '@/utils/ui';
import { reactiveOmit } from '@vueuse/core';
import type { ListboxGroupProps } from 'reka-ui';
@@ -7,15 +11,27 @@ import type { HTMLAttributes } from 'vue';
import { computed, onMounted, onUnmounted } from 'vue';
import { provideCommandGroupContext, useCommand } from '.';
const props = defineProps<
ListboxGroupProps & {
class?: HTMLAttributes['class'];
heading?: string;
}
>();
/*
* Types & Interfaces.
*/
export interface AppCommandGroupProps extends ListboxGroupProps {
class?: HTMLAttributes['class'];
heading?: string;
}
/*
* Component Setup.
*/
const props = defineProps<AppCommandGroupProps>();
const delegatedProps = reactiveOmit(props, 'class');
/*
* Computed & Methods.
*/
const { allGroups, filterState } = useCommand();
const id = useId();
@@ -44,7 +60,7 @@ onUnmounted(() => {
>
<ListboxGroupLabel
v-if="heading"
class="text-muted-foreground px-2 py-1.5 text-xs font-medium"
class="text-subtle-foreground px-2 py-1.5 text-xs font-medium"
>
{{ heading }}
</ListboxGroupLabel>

View File

@@ -1,4 +1,8 @@
<script setup lang="ts">
/**
* @component AppCommandInput
* @description The search input field for filtering command items.
*/
import { cn } from '@/utils/ui';
import { reactiveOmit } from '@vueuse/core';
import { Search } from 'lucide-vue-next';
@@ -11,16 +15,28 @@ defineOptions({
inheritAttrs: false,
});
const props = defineProps<
ListboxFilterProps & {
class?: HTMLAttributes['class'];
}
>();
/*
* Types & Interfaces.
*/
export interface AppCommandInputProps extends ListboxFilterProps {
class?: HTMLAttributes['class'];
}
/*
* Component Setup.
*/
const props = defineProps<AppCommandInputProps>();
const delegatedProps = reactiveOmit(props, 'class');
const forwardedProps = useForwardProps(delegatedProps);
/*
* Computed & Methods.
*/
const { filterState } = useCommand();
</script>
@@ -37,7 +53,7 @@ const { filterState } = useCommand();
auto-focus
:class="
cn(
'placeholder:text-muted-foreground flex h-12 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50',
'placeholder:text-subtle-foreground flex h-12 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50',
props.class,
)
"

View File

@@ -1,4 +1,8 @@
<script setup lang="ts">
/**
* @component AppCommandItem
* @description An individual selectable item within a command group or list.
*/
import { cn } from '@/utils/ui';
import { reactiveOmit, useCurrentElement } from '@vueuse/core';
import type { ListboxItemEmits, ListboxItemProps } from 'reka-ui';
@@ -7,13 +11,29 @@ import type { HTMLAttributes } from 'vue';
import { computed, onMounted, onUnmounted, ref } from 'vue';
import { useCommand, useCommandGroup } from '.';
const props = defineProps<ListboxItemProps & { class?: HTMLAttributes['class'] }>();
/*
* Types & Interfaces.
*/
export interface AppCommandItemProps extends ListboxItemProps {
class?: HTMLAttributes['class'];
}
/*
* Component Setup.
*/
const props = defineProps<AppCommandItemProps>();
const emits = defineEmits<ListboxItemEmits>();
const delegatedProps = reactiveOmit(props, 'class');
const forwarded = useForwardPropsEmits(delegatedProps, emits);
/*
* Computed & Methods.
*/
const id = useId();
const { filterState, allItems, allGroups } = useCommand();
const groupContext = useCommandGroup();
@@ -70,7 +90,7 @@ onUnmounted(() => {
data-slot="command-item"
:class="
cn(
`data-[highlighted]:bg-subtle-background data-[highlighted]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4`,
`data-[highlighted]:bg-subtle data-[highlighted]:text-accent-foreground [&_svg:not([class*='text-'])]:text-subtle-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4`,
props.class,
)
"

View File

@@ -1,11 +1,27 @@
<script setup lang="ts">
/**
* @component AppCommandList
* @description Scrollable container for command items.
*/
import { cn } from '@/utils/ui';
import { reactiveOmit } from '@vueuse/core';
import type { ListboxContentProps } from 'reka-ui';
import { ListboxContent, useForwardProps } from 'reka-ui';
import type { HTMLAttributes } from 'vue';
const props = defineProps<ListboxContentProps & { class?: HTMLAttributes['class'] }>();
/*
* Types & Interfaces.
*/
export interface AppCommandListProps extends ListboxContentProps {
class?: HTMLAttributes['class'];
}
/*
* Component Setup.
*/
const props = defineProps<AppCommandListProps>();
const delegatedProps = reactiveOmit(props, 'class');

View File

@@ -1,11 +1,27 @@
<script setup lang="ts">
/**
* @component AppCommandSeparator
* @description A visual divider for separating command groups or items.
*/
import { cn } from '@/utils/ui';
import { reactiveOmit } from '@vueuse/core';
import type { SeparatorProps } from 'reka-ui';
import { Separator } from 'reka-ui';
import type { HTMLAttributes } from 'vue';
const props = defineProps<SeparatorProps & { class?: HTMLAttributes['class'] }>();
/*
* Types & Interfaces.
*/
export interface AppCommandSeparatorProps extends SeparatorProps {
class?: HTMLAttributes['class'];
}
/*
* Component Setup.
*/
const props = defineProps<AppCommandSeparatorProps>();
const delegatedProps = reactiveOmit(props, 'class');
</script>

View File

@@ -1,16 +1,30 @@
<script setup lang="ts">
/**
* @component AppCommandShortcut
* @description Displays keyboard shortcuts for a command item.
*/
import { cn } from '@/utils/ui';
import type { HTMLAttributes } from 'vue';
const props = defineProps<{
/*
* Types & Interfaces.
*/
export interface AppCommandShortcutProps {
class?: HTMLAttributes['class'];
}>();
}
/*
* Component Setup.
*/
const props = defineProps<AppCommandShortcutProps>();
</script>
<template>
<span
data-slot="command-shortcut"
:class="cn('text-muted-foreground ml-auto text-xs tracking-widest', props.class)"
:class="cn('text-subtle-foreground ml-auto text-xs tracking-widest', props.class)"
>
<slot />
</span>

View File

@@ -0,0 +1,59 @@
import type { Meta, StoryObj } from '@storybook/vue3';
import { AppButton } from '../button';
import {
AppDialog,
AppDialogContent,
AppDialogDescription,
AppDialogFooter,
AppDialogHeader,
AppDialogTitle,
AppDialogTrigger,
} from './index';
const meta: Meta<typeof AppDialog> = {
title: 'Base/Dialog',
component: AppDialog,
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<typeof AppDialog>;
export const Default: Story = {
render: () => ({
components: {
AppDialog,
AppDialogTrigger,
AppDialogContent,
AppDialogHeader,
AppDialogTitle,
AppDialogDescription,
AppDialogFooter,
AppButton,
},
template: `
<AppDialog>
<AppDialogTrigger asChild>
<AppButton variant="outline">Open Dialog</AppButton>
</AppDialogTrigger>
<AppDialogContent class="sm:max-w-[425px]">
<AppDialogHeader>
<AppDialogTitle>Edit profile</AppDialogTitle>
<AppDialogDescription>
Make changes to your profile here. Click save when you're done.
</AppDialogDescription>
</AppDialogHeader>
<div class="grid gap-4 py-4">
<div class="grid grid-cols-4 items-center gap-4">
<label class="text-right text-sm">Name</label>
<input class="col-span-3 h-9 px-3 border rounded-md" value="Pedro Duarte" />
</div>
</div>
<AppDialogFooter>
<AppButton type="submit">Save changes</AppButton>
</AppDialogFooter>
</AppDialogContent>
</AppDialog>
`,
}),
};

View File

@@ -1,8 +1,22 @@
<script setup lang="ts">
/**
* @component AppDialog
* @description Root container for a modal dialog.
*/
import type { DialogRootEmits, DialogRootProps } from 'reka-ui';
import { DialogRoot, useForwardPropsEmits } from 'reka-ui';
const props = defineProps<DialogRootProps>();
/*
* Types & Interfaces.
*/
export interface AppDialogProps extends DialogRootProps {}
/*
* Component Setup.
*/
const props = defineProps<AppDialogProps>();
const emits = defineEmits<DialogRootEmits>();
const forwarded = useForwardPropsEmits(props, emits);

View File

@@ -1,8 +1,22 @@
<script setup lang="ts">
/**
* @component AppDialogClose
* @description An interactive element that closes the dialog.
*/
import type { DialogCloseProps } from 'reka-ui';
import { DialogClose } from 'reka-ui';
const props = defineProps<DialogCloseProps>();
/*
* Types & Interfaces.
*/
export interface AppDialogCloseProps extends DialogCloseProps {}
/*
* Component Setup.
*/
const props = defineProps<AppDialogCloseProps>();
</script>
<template>

View File

@@ -1,4 +1,8 @@
<script setup lang="ts">
/**
* @component AppDialogContent
* @description The main content area of a dialog, including its overlay and portal.
*/
import { cn } from '@/utils/ui';
import { reactiveOmit } from '@vueuse/core';
import { X } from 'lucide-vue-next';
@@ -7,7 +11,19 @@ import { DialogClose, DialogContent, DialogPortal, useForwardPropsEmits } from '
import type { HTMLAttributes } from 'vue';
import AppDialogOverlay from './AppDialogOverlay.vue';
const props = defineProps<DialogContentProps & { class?: HTMLAttributes['class'] }>();
/*
* Types & Interfaces.
*/
export interface AppDialogContentProps extends DialogContentProps {
class?: HTMLAttributes['class'];
}
/*
* Component Setup.
*/
const props = defineProps<AppDialogContentProps>();
const emits = defineEmits<DialogContentEmits>();
const delegatedProps = reactiveOmit(props, 'class');
@@ -23,7 +39,8 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
v-bind="forwarded"
:class="
cn(
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-sm border border-zinc-200 bg-white p-4 shadow-sm duration-200 sm:max-w-lg dark:border-zinc-800 dark:bg-zinc-950',
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-2.5 rounded-sm border border-zinc-200 bg-white p-3 shadow-sm duration-200 sm:max-w-lg dark:border-zinc-800 dark:bg-zinc-950',
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-2.5 rounded-sm border border-zinc-200 bg-white p-3 shadow-sm duration-200 sm:max-w-lg dark:border-zinc-800 dark:bg-zinc-950',
props.class,
)
"

View File

@@ -1,11 +1,27 @@
<script setup lang="ts">
/**
* @component AppDialogDescription
* @description Supporting text for a dialog title.
*/
import { cn } from '@/utils/ui';
import { reactiveOmit } from '@vueuse/core';
import type { DialogDescriptionProps } from 'reka-ui';
import { DialogDescription, useForwardProps } from 'reka-ui';
import type { HTMLAttributes } from 'vue';
const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes['class'] }>();
/*
* Types & Interfaces.
*/
export interface AppDialogDescriptionProps extends DialogDescriptionProps {
class?: HTMLAttributes['class'];
}
/*
* Component Setup.
*/
const props = defineProps<AppDialogDescriptionProps>();
const delegatedProps = reactiveOmit(props, 'class');
@@ -16,7 +32,7 @@ const forwardedProps = useForwardProps(delegatedProps);
<DialogDescription
data-slot="dialog-description"
v-bind="forwardedProps"
:class="cn('text-subtle text-sm', props.class)"
:class="cn('text-subtle-foreground text-sm', props.class)"
>
<slot />
</DialogDescription>

View File

@@ -1,8 +1,24 @@
<script setup lang="ts">
/**
* @component AppDialogFooter
* @description Footer container for dialog actions.
*/
import { cn } from '@/utils/ui';
import type { HTMLAttributes } from 'vue';
const props = defineProps<{ class?: HTMLAttributes['class'] }>();
/*
* Types & Interfaces.
*/
export interface AppDialogFooterProps {
class?: HTMLAttributes['class'];
}
/*
* Component Setup.
*/
const props = defineProps<AppDialogFooterProps>();
</script>
<template>

View File

@@ -1,10 +1,24 @@
<script setup lang="ts">
/**
* @component AppDialogHeader
* @description Header container for dialog title and description.
*/
import { cn } from '@/utils/ui';
import type { HTMLAttributes } from 'vue';
const props = defineProps<{
/*
* Types & Interfaces.
*/
export interface AppDialogHeaderProps {
class?: HTMLAttributes['class'];
}>();
}
/*
* Component Setup.
*/
const props = defineProps<AppDialogHeaderProps>();
</script>
<template>

View File

@@ -1,11 +1,27 @@
<script setup lang="ts">
/**
* @component AppDialogOverlay
* @description The semi-transparent backdrop for a modal dialog.
*/
import { cn } from '@/utils/ui';
import { reactiveOmit } from '@vueuse/core';
import type { DialogOverlayProps } from 'reka-ui';
import { DialogOverlay } from 'reka-ui';
import type { HTMLAttributes } from 'vue';
const props = defineProps<DialogOverlayProps & { class?: HTMLAttributes['class'] }>();
/*
* Types & Interfaces.
*/
export interface AppDialogOverlayProps extends DialogOverlayProps {
class?: HTMLAttributes['class'];
}
/*
* Component Setup.
*/
const props = defineProps<AppDialogOverlayProps>();
const delegatedProps = reactiveOmit(props, 'class');
</script>

View File

@@ -1,4 +1,8 @@
<script setup lang="ts">
/**
* @component AppDialogScrollContent
* @description A dialog content variant that allows internal scrolling.
*/
import { cn } from '@/utils/ui';
import { reactiveOmit } from '@vueuse/core';
import { X } from 'lucide-vue-next';
@@ -12,7 +16,19 @@ import {
} from 'reka-ui';
import type { HTMLAttributes } from 'vue';
const props = defineProps<DialogContentProps & { class?: HTMLAttributes['class'] }>();
/*
* Types & Interfaces.
*/
export interface AppDialogScrollContentProps extends DialogContentProps {
class?: HTMLAttributes['class'];
}
/*
* Component Setup.
*/
const props = defineProps<AppDialogScrollContentProps>();
const emits = defineEmits<DialogContentEmits>();
const delegatedProps = reactiveOmit(props, 'class');

View File

@@ -1,11 +1,27 @@
<script setup lang="ts">
/**
* @component AppDialogTitle
* @description Main heading for a dialog.
*/
import { cn } from '@/utils/ui';
import { reactiveOmit } from '@vueuse/core';
import type { DialogTitleProps } from 'reka-ui';
import { DialogTitle, useForwardProps } from 'reka-ui';
import type { HTMLAttributes } from 'vue';
const props = defineProps<DialogTitleProps & { class?: HTMLAttributes['class'] }>();
/*
* Types & Interfaces.
*/
export interface AppDialogTitleProps extends DialogTitleProps {
class?: HTMLAttributes['class'];
}
/*
* Component Setup.
*/
const props = defineProps<AppDialogTitleProps>();
const delegatedProps = reactiveOmit(props, 'class');

View File

@@ -1,8 +1,22 @@
<script setup lang="ts">
/**
* @component AppDialogTrigger
* @description The interactive element that opens the dialog.
*/
import type { DialogTriggerProps } from 'reka-ui';
import { DialogTrigger } from 'reka-ui';
const props = defineProps<DialogTriggerProps>();
/*
* Types & Interfaces.
*/
export interface AppDialogTriggerProps extends DialogTriggerProps {}
/*
* Component Setup.
*/
const props = defineProps<AppDialogTriggerProps>();
</script>
<template>

View File

@@ -0,0 +1,48 @@
import type { Meta, StoryObj } from '@storybook/vue3';
import { AppButton } from '../button';
import {
AppDropdownMenu,
AppDropdownMenuContent,
AppDropdownMenuItem,
AppDropdownMenuLabel,
AppDropdownMenuSeparator,
AppDropdownMenuTrigger,
} from './index';
const meta: Meta<typeof AppDropdownMenu> = {
title: 'Base/DropdownMenu',
component: AppDropdownMenu,
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<typeof AppDropdownMenu>;
export const Default: Story = {
render: () => ({
components: {
AppDropdownMenu,
AppDropdownMenuTrigger,
AppDropdownMenuContent,
AppDropdownMenuItem,
AppDropdownMenuLabel,
AppDropdownMenuSeparator,
AppButton,
},
template: `
<AppDropdownMenu>
<AppDropdownMenuTrigger asChild>
<AppButton variant="outline">Open Menu</AppButton>
</AppDropdownMenuTrigger>
<AppDropdownMenuContent class="w-56">
<AppDropdownMenuLabel>My Account</AppDropdownMenuLabel>
<AppDropdownMenuSeparator />
<AppDropdownMenuItem>Profile</AppDropdownMenuItem>
<AppDropdownMenuItem>Billing</AppDropdownMenuItem>
<AppDropdownMenuItem>Team</AppDropdownMenuItem>
<AppDropdownMenuItem>Subscription</AppDropdownMenuItem>
</AppDropdownMenuContent>
</AppDropdownMenu>
`,
}),
};

View File

@@ -1,4 +1,8 @@
<script setup lang="ts">
/**
* @component AppDropdownMenu
* @description Root container for a dropdown menu.
*/
import {
DropdownMenuRoot,
type DropdownMenuRootEmits,
@@ -6,7 +10,17 @@ import {
useForwardPropsEmits,
} from 'reka-ui';
const props = defineProps<DropdownMenuRootProps>();
/*
* Types & Interfaces.
*/
export interface AppDropdownMenuProps extends DropdownMenuRootProps {}
/*
* Component Setup.
*/
const props = defineProps<AppDropdownMenuProps>();
const emits = defineEmits<DropdownMenuRootEmits>();
const forwarded = useForwardPropsEmits(props, emits);

View File

@@ -1,4 +1,8 @@
<script setup lang="ts">
/**
* @component AppDropdownMenuCheckboxItem
* @description A dropdown menu item that can be toggled on/off.
*/
import { cn } from '@/utils/ui';
import { Check } from 'lucide-vue-next';
import {
@@ -10,9 +14,19 @@ import {
} from 'reka-ui';
import { computed, type HTMLAttributes } from 'vue';
const props = defineProps<
DropdownMenuCheckboxItemProps & { class?: HTMLAttributes['class'] }
>();
/*
* Types & Interfaces.
*/
export interface AppDropdownMenuCheckboxItemProps extends DropdownMenuCheckboxItemProps {
class?: HTMLAttributes['class'];
}
/*
* Component Setup.
*/
const props = defineProps<AppDropdownMenuCheckboxItemProps>();
const emits = defineEmits<DropdownMenuCheckboxItemEmits>();
const delegatedProps = computed(() => {

View File

@@ -1,4 +1,8 @@
<script setup lang="ts">
/**
* @component AppDropdownMenuContent
* @description The container for dropdown menu items, including portal and animations.
*/
import { cn } from '@/utils/ui';
import {
DropdownMenuContent,
@@ -9,13 +13,22 @@ import {
} from 'reka-ui';
import { computed, type HTMLAttributes } from 'vue';
const props = withDefaults(
defineProps<DropdownMenuContentProps & { class?: HTMLAttributes['class'] }>(),
{
sideOffset: 4,
class: '',
},
);
/*
* Types & Interfaces.
*/
export interface AppDropdownMenuContentProps extends DropdownMenuContentProps {
class?: HTMLAttributes['class'];
}
/*
* Component Setup.
*/
const props = withDefaults(defineProps<AppDropdownMenuContentProps>(), {
sideOffset: 4,
class: '',
});
const emits = defineEmits<DropdownMenuContentEmits>();
const delegatedProps = computed(() => {

View File

@@ -1,7 +1,21 @@
<script setup lang="ts">
/**
* @component AppDropdownMenuGroup
* @description A logical grouping for dropdown menu items.
*/
import { DropdownMenuGroup, type DropdownMenuGroupProps } from 'reka-ui';
const props = defineProps<DropdownMenuGroupProps>();
/*
* Types & Interfaces.
*/
export interface AppDropdownMenuGroupProps extends DropdownMenuGroupProps {}
/*
* Component Setup.
*/
const props = defineProps<AppDropdownMenuGroupProps>();
</script>
<template>

View File

@@ -1,11 +1,26 @@
<script setup lang="ts">
/**
* @component AppDropdownMenuItem
* @description An individual selectable item within a dropdown menu.
*/
import { cn } from '@/utils/ui';
import { DropdownMenuItem, type DropdownMenuItemProps, useForwardProps } from 'reka-ui';
import { computed, type HTMLAttributes } from 'vue';
const props = defineProps<
DropdownMenuItemProps & { class?: HTMLAttributes['class']; inset?: boolean }
>();
/*
* Types & Interfaces.
*/
export interface AppDropdownMenuItemProps extends DropdownMenuItemProps {
class?: HTMLAttributes['class'];
inset?: boolean;
}
/*
* Component Setup.
*/
const props = defineProps<AppDropdownMenuItemProps>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;

View File

@@ -1,14 +1,26 @@
<script setup lang="ts">
/**
* @component AppDropdownMenuLabel
* @description A label for a group of dropdown menu items.
*/
import { cn } from '@/utils/ui';
import { DropdownMenuLabel, type DropdownMenuLabelProps, useForwardProps } from 'reka-ui';
import { computed, type HTMLAttributes } from 'vue';
const props = defineProps<
DropdownMenuLabelProps & {
class?: HTMLAttributes['class'];
inset?: boolean;
}
>();
/*
* Types & Interfaces.
*/
export interface AppDropdownMenuLabelProps extends DropdownMenuLabelProps {
class?: HTMLAttributes['class'];
inset?: boolean;
}
/*
* Component Setup.
*/
const props = defineProps<AppDropdownMenuLabelProps>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;

View File

@@ -1,4 +1,8 @@
<script setup lang="ts">
/**
* @component AppDropdownMenuRadioGroup
* @description A group for managing mutually exclusive dropdown menu items.
*/
import {
DropdownMenuRadioGroup,
type DropdownMenuRadioGroupEmits,
@@ -6,7 +10,17 @@ import {
useForwardPropsEmits,
} from 'reka-ui';
const props = defineProps<DropdownMenuRadioGroupProps>();
/*
* Types & Interfaces.
*/
export interface AppDropdownMenuRadioGroupProps extends DropdownMenuRadioGroupProps {}
/*
* Component Setup.
*/
const props = defineProps<AppDropdownMenuRadioGroupProps>();
const emits = defineEmits<DropdownMenuRadioGroupEmits>();
const forwarded = useForwardPropsEmits(props, emits);

View File

@@ -1,4 +1,8 @@
<script setup lang="ts">
/**
* @component AppDropdownMenuRadioItem
* @description A dropdown menu item that acts as a radio button within a group.
*/
import { cn } from '@/utils/ui';
import { Circle } from 'lucide-vue-next';
import {
@@ -10,9 +14,19 @@ import {
} from 'reka-ui';
import { computed, type HTMLAttributes } from 'vue';
const props = defineProps<
DropdownMenuRadioItemProps & { class?: HTMLAttributes['class'] }
>();
/*
* Types & Interfaces.
*/
export interface AppDropdownMenuRadioItemProps extends DropdownMenuRadioItemProps {
class?: HTMLAttributes['class'];
}
/*
* Component Setup.
*/
const props = defineProps<AppDropdownMenuRadioItemProps>();
const emits = defineEmits<DropdownMenuRadioItemEmits>();

View File

@@ -1,13 +1,25 @@
<script setup lang="ts">
/**
* @component AppDropdownMenuSeparator
* @description A visual divider for dropdown menu items.
*/
import { cn } from '@/utils/ui';
import { DropdownMenuSeparator, type DropdownMenuSeparatorProps } from 'reka-ui';
import { computed, type HTMLAttributes } from 'vue';
const props = defineProps<
DropdownMenuSeparatorProps & {
class?: HTMLAttributes['class'];
}
>();
/*
* Types & Interfaces.
*/
export interface AppDropdownMenuSeparatorProps extends DropdownMenuSeparatorProps {
class?: HTMLAttributes['class'];
}
/*
* Component Setup.
*/
const props = defineProps<AppDropdownMenuSeparatorProps>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;

View File

@@ -1,10 +1,24 @@
<script setup lang="ts">
/**
* @component AppDropdownMenuShortcut
* @description Displays keyboard shortcuts for a dropdown menu item.
*/
import { cn } from '@/utils/ui';
import type { HTMLAttributes } from 'vue';
const props = defineProps<{
/*
* Types & Interfaces.
*/
export interface AppDropdownMenuShortcutProps {
class?: HTMLAttributes['class'];
}>();
}
/*
* Component Setup.
*/
const props = defineProps<AppDropdownMenuShortcutProps>();
</script>
<template>

View File

@@ -1,4 +1,8 @@
<script setup lang="ts">
/**
* @component AppDropdownMenuSub
* @description Root container for a sub-dropdown menu.
*/
import {
DropdownMenuSub,
type DropdownMenuSubEmits,
@@ -6,7 +10,17 @@ import {
useForwardPropsEmits,
} from 'reka-ui';
const props = defineProps<DropdownMenuSubProps>();
/*
* Types & Interfaces.
*/
export interface AppDropdownMenuSubProps extends DropdownMenuSubProps {}
/*
* Component Setup.
*/
const props = defineProps<AppDropdownMenuSubProps>();
const emits = defineEmits<DropdownMenuSubEmits>();
const forwarded = useForwardPropsEmits(props, emits);

View File

@@ -1,4 +1,8 @@
<script setup lang="ts">
/**
* @component AppDropdownMenuSubContent
* @description The container for sub-dropdown menu items.
*/
import { cn } from '@/utils/ui';
import {
DropdownMenuSubContent,
@@ -8,9 +12,19 @@ import {
} from 'reka-ui';
import { computed, type HTMLAttributes } from 'vue';
const props = defineProps<
DropdownMenuSubContentProps & { class?: HTMLAttributes['class'] }
>();
/*
* Types & Interfaces.
*/
export interface AppDropdownMenuSubContentProps extends DropdownMenuSubContentProps {
class?: HTMLAttributes['class'];
}
/*
* Component Setup.
*/
const props = defineProps<AppDropdownMenuSubContentProps>();
const emits = defineEmits<DropdownMenuSubContentEmits>();
const delegatedProps = computed(() => {

View File

@@ -1,4 +1,8 @@
<script setup lang="ts">
/**
* @component AppDropdownMenuSubTrigger
* @description The interactive element that opens a sub-dropdown menu.
*/
import { cn } from '@/utils/ui';
import { ChevronRight } from 'lucide-vue-next';
import {
@@ -8,9 +12,19 @@ import {
} from 'reka-ui';
import { computed, type HTMLAttributes } from 'vue';
const props = defineProps<
DropdownMenuSubTriggerProps & { class?: HTMLAttributes['class'] }
>();
/*
* Types & Interfaces.
*/
export interface AppDropdownMenuSubTriggerProps extends DropdownMenuSubTriggerProps {
class?: HTMLAttributes['class'];
}
/*
* Component Setup.
*/
const props = defineProps<AppDropdownMenuSubTriggerProps>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;

View File

@@ -1,11 +1,25 @@
<script setup lang="ts">
/**
* @component AppDropdownMenuTrigger
* @description The interactive element that opens the dropdown menu.
*/
import {
DropdownMenuTrigger,
type DropdownMenuTriggerProps,
useForwardProps,
} from 'reka-ui';
const props = defineProps<DropdownMenuTriggerProps>();
/*
* Types & Interfaces.
*/
export interface AppDropdownMenuTriggerProps extends DropdownMenuTriggerProps {}
/*
* Component Setup.
*/
const props = defineProps<AppDropdownMenuTriggerProps>();
const forwardedProps = useForwardProps(props);
</script>

View File

@@ -1,7 +1,21 @@
<script lang="ts" setup>
<script setup lang="ts">
/**
* @component AppFormControl
* @description Provides accessibility attributes to form control elements.
*/
import { Slot } from 'reka-ui';
import { useFormField } from './useFormField';
/*
* Types & Interfaces.
*/
export interface AppFormControlProps {}
/*
* Component Setup.
*/
const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
</script>

View File

@@ -1,17 +1,31 @@
<script lang="ts" setup>
<script setup lang="ts">
/**
* @component AppFormDescription
* @description Supporting text for a form field.
*/
import { cn } from '@/utils/ui';
import type { HTMLAttributes } from 'vue';
import { useFormField } from './useFormField';
const props = defineProps<{
/*
* Types & Interfaces.
*/
export interface AppFormDescriptionProps {
class?: HTMLAttributes['class'];
}>();
}
/*
* Component Setup.
*/
const props = defineProps<AppFormDescriptionProps>();
const { formDescriptionId } = useFormField();
</script>
<template>
<p :id="formDescriptionId" :class="cn('text-subtle text-sm', props.class)">
<p :id="formDescriptionId" :class="cn('text-subtle-foreground text-sm', props.class)">
<slot />
</p>
</template>

View File

@@ -1,12 +1,26 @@
<script lang="ts" setup>
<script setup lang="ts">
/**
* @component AppFormItem
* @description Container for a single form field, providing context to its sub-components.
*/
import { cn } from '@/utils/ui';
import { useId } from 'reka-ui';
import { type HTMLAttributes, provide } from 'vue';
import { FORM_ITEM_INJECTION_KEY } from './injectionKeys';
const props = defineProps<{
/*
* Types & Interfaces.
*/
export interface AppFormItemProps {
class?: HTMLAttributes['class'];
}>();
}
/*
* Component Setup.
*/
const props = defineProps<AppFormItemProps>();
const id = useId();
provide(FORM_ITEM_INJECTION_KEY, id);

View File

@@ -1,11 +1,27 @@
<script lang="ts" setup>
<script setup lang="ts">
/**
* @component AppFormLabel
* @description A label for a form field that automatically handles error states.
*/
import { AppLabel } from '@/components/base/label';
import { cn } from '@/utils/ui';
import type { LabelProps } from 'reka-ui';
import type { HTMLAttributes } from 'vue';
import { useFormField } from './useFormField';
const props = defineProps<LabelProps & { class?: HTMLAttributes['class'] }>();
/*
* Types & Interfaces.
*/
export interface AppFormLabelProps extends LabelProps {
class?: HTMLAttributes['class'];
}
/*
* Component Setup.
*/
const props = defineProps<AppFormLabelProps>();
const { error, formItemId } = useFormField();
</script>

View File

@@ -1,8 +1,22 @@
<script lang="ts" setup>
<script setup lang="ts">
/**
* @component AppFormMessage
* @description Displays validation error messages for a form field.
*/
import { ErrorMessage } from 'vee-validate';
import { toValue } from 'vue';
import { useFormField } from './useFormField';
/*
* Types & Interfaces.
*/
export interface AppFormMessageProps {}
/*
* Component Setup.
*/
const { name, formMessageId } = useFormField();
</script>

View File

@@ -5,10 +5,20 @@ import {
useIsFieldTouched,
useIsFieldValid,
} from 'vee-validate';
import { inject } from 'vue';
import { type ComputedRef, inject, unref } from 'vue';
import { FORM_ITEM_INJECTION_KEY } from './injectionKeys';
export function useFormField() {
export function useFormField(): {
id: string | undefined;
name: string;
formItemId: string;
formDescriptionId: string;
formMessageId: string;
valid: ComputedRef<boolean>;
isDirty: ComputedRef<boolean>;
isTouched: ComputedRef<boolean>;
error: ComputedRef<string | undefined>;
} {
const fieldContext = inject(FieldContextKey);
const fieldItemContext = inject(FORM_ITEM_INJECTION_KEY);
@@ -16,7 +26,7 @@ export function useFormField() {
throw new Error('useFormField should be used within <FormField>');
}
const { name } = fieldContext;
const name = unref(fieldContext.name);
const id = fieldItemContext;
const fieldState = {

View File

@@ -1,3 +1,49 @@
<script setup lang="ts">
/**
* @component AppBorderBeam
* @description An animated beam of light that travels around a container's border.
*/
import { cn } from '@/utils/ui';
import { computed } from 'vue';
/*
* Types & Interfaces.
*/
export interface AppBorderBeamProps {
class?: string;
size?: number;
duration?: number;
borderWidth?: number;
anchor?: number;
colorFrom?: string;
colorTo?: string;
delay?: number;
}
/*
* Component Setup.
*/
const props = withDefaults(defineProps<AppBorderBeamProps>(), {
class: '',
size: 200,
duration: 15000,
anchor: 10,
borderWidth: 1.5,
colorFrom: '#ffaa40',
colorTo: '#9c40ff',
delay: 0,
});
/*
* Computed & Methods.
*/
const durationInSeconds = computed(() => `${props.duration}s`);
const delayInSeconds = computed(() => `${props.delay}s`);
</script>
<template>
<div
:class="
@@ -12,36 +58,6 @@
></div>
</template>
<script setup lang="ts">
import { cn } from '@/utils/ui';
import { computed } from 'vue';
interface BorderBeamProps {
class?: string;
size?: number;
duration?: number;
borderWidth?: number;
anchor?: number;
colorFrom?: string;
colorTo?: string;
delay?: number;
}
const props = withDefaults(defineProps<BorderBeamProps>(), {
class: '',
size: 200,
duration: 15000,
anchor: 10,
borderWidth: 1.5,
colorFrom: '#ffaa40',
colorTo: '#9c40ff',
delay: 0,
});
const durationInSeconds = computed(() => `${props.duration}s`);
const delayInSeconds = computed(() => `${props.delay}s`);
</script>
<style scoped>
.border-beam {
--size: v-bind(size);

View File

@@ -1,8 +1,24 @@
<script setup lang="ts">
import { cn } from '@/utils';
import { HTMLAttributes } from 'vue';
/**
* @component AppBrandIcon
* @description The main SVG brand icon for the application.
*/
import { cn } from '@/utils/ui';
import { type HTMLAttributes } from 'vue';
const props = defineProps<{ class?: HTMLAttributes['class'] }>();
/*
* Types & Interfaces.
*/
export interface AppBrandIconProps {
class?: HTMLAttributes['class'];
}
/*
* Component Setup.
*/
const props = defineProps<AppBrandIconProps>();
</script>
<template>
<svg

View File

@@ -1,12 +1,24 @@
<script setup lang="ts">
/**
* @component AppSpinner
* @description A simple animated SVG spinner for indicating loading states.
*/
import { cn } from '@/utils/ui';
import { HTMLAttributes } from 'vue';
import { type HTMLAttributes } from 'vue';
interface SpinnerProps {
/*
* Types & Interfaces.
*/
export interface AppSpinnerProps {
class?: HTMLAttributes['class'];
}
const props = defineProps<SpinnerProps>();
/*
* Component Setup.
*/
const props = defineProps<AppSpinnerProps>();
</script>
<template>

View File

@@ -1,10 +1,24 @@
<script setup lang="ts">
/**
* @component AppInputGroup
* @description A container for grouping inputs with addons and buttons.
*/
import { cn } from '@/utils';
import type { HTMLAttributes } from 'vue';
const props = defineProps<{
/*
* Types & Interfaces.
*/
export interface AppInputGroupProps {
class?: HTMLAttributes['class'];
}>();
}
/*
* Component Setup.
*/
const props = defineProps<AppInputGroupProps>();
</script>
<template>

View File

@@ -1,19 +1,34 @@
<script setup lang="ts">
/**
* @component AppInputGroupAddon
* @description An addon element (text or icon) for an input group.
*/
import { cn } from '@/utils';
import type { HTMLAttributes } from 'vue';
import type { InputGroupVariants } from '.';
import { inputGroupAddonVariants } from '.';
const props = withDefaults(
defineProps<{
align?: InputGroupVariants['align'];
class?: HTMLAttributes['class'];
}>(),
{
align: 'inline-start',
class: undefined,
},
);
/*
* Types & Interfaces.
*/
export interface AppInputGroupAddonProps {
align?: InputGroupVariants['align'];
class?: HTMLAttributes['class'];
}
/*
* Component Setup.
*/
const props = withDefaults(defineProps<AppInputGroupAddonProps>(), {
align: 'inline-start',
class: undefined,
});
/*
* Computed & Methods.
*/
function handleInputGroupAddonClick(e: MouseEvent) {
const currentTarget = e.currentTarget as HTMLElement | null;

View File

@@ -1,10 +1,24 @@
<script setup lang="ts">
/**
* @component AppInputGroupButton
* @description A button intended for use within an input group.
*/
import { AppButton } from '@/components/base/button';
import { cn } from '@/utils';
import type { InputGroupButtonProps } from '.';
import { inputGroupButtonVariants } from '.';
const props = withDefaults(defineProps<InputGroupButtonProps>(), {
/*
* Types & Interfaces.
*/
export interface AppInputGroupButtonComponentProps extends InputGroupButtonProps {}
/*
* Component Setup.
*/
const props = withDefaults(defineProps<AppInputGroupButtonComponentProps>(), {
size: 'xs',
variant: 'ghost',
});

View File

@@ -1,12 +1,30 @@
<script setup lang="ts">
/**
* @component AppInputGroupInput
* @description A specialized input for use inside an input group, removing default borders/shadows.
*/
import { AppInput } from '@/components/base/input';
import { cn } from '@/utils';
import type { HTMLAttributes } from 'vue';
import { ref } from 'vue';
const props = defineProps<{
/*
* Types & Interfaces.
*/
export interface AppInputGroupInputProps {
class?: HTMLAttributes['class'];
}>();
}
/*
* Component Setup.
*/
const props = defineProps<AppInputGroupInputProps>();
/*
* Computed & Methods.
*/
const inputRef = ref<InstanceType<typeof AppInput> | null>(null);

View File

@@ -1,10 +1,24 @@
<script setup lang="ts">
/**
* @component AppInputGroupText
* @description Plain text addon for an input group.
*/
import { cn } from '@/utils';
import type { HTMLAttributes } from 'vue';
const props = defineProps<{
/*
* Types & Interfaces.
*/
export interface AppInputGroupTextProps {
class?: HTMLAttributes['class'];
}>();
}
/*
* Component Setup.
*/
const props = defineProps<AppInputGroupTextProps>();
</script>
<template>

View File

@@ -1,11 +1,25 @@
<script setup lang="ts">
/**
* @component AppInputGroupTextarea
* @description A specialized textarea for use inside an input group.
*/
import { AppTextarea } from '@/components/base/textarea';
import { cn } from '@/utils';
import type { HTMLAttributes } from 'vue';
const props = defineProps<{
/*
* Types & Interfaces.
*/
export interface AppInputGroupTextareaProps {
class?: HTMLAttributes['class'];
}>();
}
/*
* Component Setup.
*/
const props = defineProps<AppInputGroupTextareaProps>();
</script>
<template>

View File

@@ -0,0 +1,51 @@
import type { Meta, StoryObj } from '@storybook/vue3';
import AppInput from './AppInput.vue';
const meta: Meta<typeof AppInput> = {
title: 'Base/Input',
component: AppInput,
tags: ['autodocs'],
argTypes: {
type: {
control: 'select',
options: ['text', 'password', 'email', 'number', 'tel', 'url'],
},
},
};
export default meta;
type Story = StoryObj<typeof AppInput>;
export const Default: Story = {
args: {
modelValue: '',
placeholder: 'Type something...',
},
render: args => ({
components: { AppInput },
setup: () => ({ args }),
template: '<AppInput v-bind="args" />',
}),
};
export const Password: Story = {
args: {
type: 'password',
modelValue: 'secret',
},
};
export const Disabled: Story = {
args: {
disabled: true,
modelValue: 'Cannot edit this',
},
};
export const Toolbar: Story = {
args: {
variant: 'toolbar',
placeholder: 'Toolbar input...',
class: 'border-b',
},
};

View File

@@ -1,17 +1,35 @@
<script setup lang="ts">
/**
* @component AppInput
* @description A standard text input component with double-shift value generation support.
*/
import { ValueGeneratorCommandOpenMethod } from '@/interfaces/ui';
import { useValueGeneratorStore } from '@/stores';
import { cn } from '@/utils/ui';
import { useVModel } from '@vueuse/core';
import type { HTMLAttributes } from 'vue';
import { ref } from 'vue';
import { inputVariants, type InputVariants } from './index';
const props = defineProps<{
/*
* Types & Interfaces.
*/
export interface AppInputProps {
defaultValue?: string | number;
modelValue?: string | number;
class?: HTMLAttributes['class'];
type?: HTMLAttributes['inputmode'];
}>();
type?: string;
placeholder?: string;
disabled?: boolean;
variant?: InputVariants['variant'];
}
/*
* Component Setup.
*/
const props = defineProps<AppInputProps>();
const emits = defineEmits<{
(e: 'update:modelValue', payload: string | number): void;
@@ -22,6 +40,10 @@ const modelValue = useVModel(props, 'modelValue', emits, {
defaultValue: props.defaultValue,
});
/*
* Computed & Methods.
*/
const { openCommand } = useValueGeneratorStore();
const inputRef = ref<HTMLInputElement>();
@@ -49,12 +71,9 @@ const handleKeydown = (event: KeyboardEvent) => {
ref="inputRef"
v-model="modelValue"
:type="type ?? 'text'"
:class="
cn(
'flex h-9 w-full rounded-md border border-zinc-200 bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-zinc-500 focus-visible:ring-1 focus-visible:ring-zinc-950 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 dark:border-zinc-800 dark:placeholder:text-zinc-400 dark:focus-visible:ring-zinc-300',
props.class,
)
"
:class="cn(inputVariants({ variant }), props.class)"
:placeholder="placeholder"
:disabled="disabled"
@keydown="handleKeydown"
/>
</template>

View File

@@ -1 +1,21 @@
import { cva, type VariantProps } from 'class-variance-authority';
export { default as AppInput } from './AppInput.vue';
export const inputVariants = cva(
'flex h-9 w-full bg-transparent px-3 py-1 text-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-zinc-500 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 dark:placeholder:text-zinc-400',
{
variants: {
variant: {
default:
'rounded-md border border-zinc-200 shadow-sm focus-visible:ring-1 focus-visible:ring-zinc-950 dark:border-zinc-800 dark:focus-visible:ring-zinc-300',
toolbar: 'rounded-none border-0 shadow-none focus-visible:ring-0',
},
},
defaultVariants: {
variant: 'default',
},
},
);
export type InputVariants = VariantProps<typeof inputVariants>;

View File

@@ -0,0 +1,18 @@
import type { Meta, StoryObj } from '@storybook/vue3';
import { AppLabel } from './index';
const meta: Meta<typeof AppLabel> = {
title: 'Base/Label',
component: AppLabel,
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<typeof AppLabel>;
export const Default: Story = {
render: () => ({
components: { AppLabel },
template: '<AppLabel>Email Address</AppLabel>',
}),
};

View File

@@ -1,9 +1,25 @@
<script setup lang="ts">
/**
* @component AppLabel
* @description Primitive label component with consistency across themes.
*/
import { cn } from '@/utils/ui';
import { Label, type LabelProps } from 'reka-ui';
import { computed, type HTMLAttributes } from 'vue';
const props = defineProps<LabelProps & { class?: HTMLAttributes['class'] }>();
/*
* Types & Interfaces.
*/
export interface AppLabelProps extends LabelProps {
class?: HTMLAttributes['class'];
}
/*
* Component Setup.
*/
const props = defineProps<AppLabelProps>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;

View File

@@ -0,0 +1,45 @@
import type { Meta, StoryObj } from '@storybook/vue3';
import {
AppPanel,
AppPanelContent,
AppPanelDescription,
AppPanelHeader,
AppPanelTitle,
} from './index';
const meta: Meta<typeof AppPanel> = {
title: 'Base/Panel',
component: AppPanel,
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<typeof AppPanel>;
export const Default: Story = {
render: () => ({
components: {
AppPanel,
AppPanelContent,
AppPanelDescription,
AppPanelHeader,
AppPanelTitle,
},
template: `
<AppPanel class="w-[400px] border">
<AppPanelHeader>
<div class="flex flex-col">
<AppPanelTitle>Panel Title</AppPanelTitle>
<AppPanelDescription>Dense panel description for context.</AppPanelDescription>
</div>
</AppPanelHeader>
<AppPanelContent class="border-t">
<p class="text-xs">This is the main content of the panel. It uses px-panel for padding and is denser than a card.</p>
</AppPanelContent>
<AppPanelContent class="border-t">
<p class="text-xs">Multiple content sections can be used with borders.</p>
</AppPanelContent>
</AppPanel>
`,
}),
};

View File

@@ -0,0 +1,35 @@
<script setup lang="ts">
/**
* @component AppPanel
* @description A dense container component for panel-based layouts.
*/
import { cn } from '@/utils/ui';
import type { HTMLAttributes } from 'vue';
/*
* Types & Interfaces.
*/
export interface AppPanelProps {
class?: HTMLAttributes['class'];
}
/*
* Component Setup.
*/
const props = defineProps<AppPanelProps>();
</script>
<template>
<div
:class="
cn(
'flex flex-col bg-white text-zinc-950 dark:bg-zinc-950 dark:text-zinc-50',
props.class,
)
"
>
<slot />
</div>
</template>

View File

@@ -0,0 +1,28 @@
<script setup lang="ts">
/**
* @component AppPanelContent
* @description Main content area for AppPanel.
*/
import { cn } from '@/utils/ui';
import type { HTMLAttributes } from 'vue';
/*
* Types & Interfaces.
*/
export interface AppPanelContentProps {
class?: HTMLAttributes['class'];
}
/*
* Component Setup.
*/
const props = defineProps<AppPanelContentProps>();
</script>
<template>
<div :class="cn('px-panel py-2', props.class)">
<slot />
</div>
</template>

View File

@@ -0,0 +1,28 @@
<script setup lang="ts">
/**
* @component AppPanelDescription
* @description Description text for an AppPanel.
*/
import { cn } from '@/utils/ui';
import type { HTMLAttributes } from 'vue';
/*
* Types & Interfaces.
*/
export interface AppPanelDescriptionProps {
class?: HTMLAttributes['class'];
}
/*
* Component Setup.
*/
const props = defineProps<AppPanelDescriptionProps>();
</script>
<template>
<p :class="cn('text-subtle-foreground text-sm', props.class)">
<slot />
</p>
</template>

View File

@@ -0,0 +1,28 @@
<script setup lang="ts">
/**
* @component AppPanelHeader
* @description Header container for AppPanel.
*/
import { cn } from '@/utils/ui';
import type { HTMLAttributes } from 'vue';
/*
* Types & Interfaces.
*/
export interface AppPanelHeaderProps {
class?: HTMLAttributes['class'];
}
/*
* Component Setup.
*/
const props = defineProps<AppPanelHeaderProps>();
</script>
<template>
<div :class="cn('px-panel flex items-center gap-2 py-2.5', props.class)">
<slot />
</div>
</template>

View File

@@ -0,0 +1,28 @@
<script setup lang="ts">
/**
* @component AppPanelTitle
* @description Heading for an AppPanel.
*/
import { cn } from '@/utils/ui';
import type { HTMLAttributes } from 'vue';
/*
* Types & Interfaces.
*/
export interface AppPanelTitleProps {
class?: HTMLAttributes['class'];
}
/*
* Component Setup.
*/
const props = defineProps<AppPanelTitleProps>();
</script>
<template>
<h3 :class="cn('text-sm leading-none font-semibold tracking-tight', props.class)">
<slot />
</h3>
</template>

View File

@@ -0,0 +1,5 @@
export { default as AppPanel } from './AppPanel.vue';
export { default as AppPanelContent } from './AppPanelContent.vue';
export { default as AppPanelDescription } from './AppPanelDescription.vue';
export { default as AppPanelHeader } from './AppPanelHeader.vue';
export { default as AppPanelTitle } from './AppPanelTitle.vue';

View File

@@ -0,0 +1,33 @@
import type { Meta, StoryObj } from '@storybook/vue3';
import { AppButton } from '../button';
import { AppPopover, AppPopoverContent, AppPopoverTrigger } from './index';
const meta: Meta<typeof AppPopover> = {
title: 'Base/Popover',
component: AppPopover,
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<typeof AppPopover>;
export const Default: Story = {
render: () => ({
components: { AppPopover, AppPopoverTrigger, AppPopoverContent, AppButton },
template: `
<AppPopover>
<AppPopoverTrigger asChild>
<AppButton variant="outline">Open Popover</AppButton>
</AppPopoverTrigger>
<AppPopoverContent class="w-80">
<div class="grid gap-4">
<div class="space-y-2">
<h4 class="font-medium leading-none">Dimensions</h4>
<p class="text-sm text-zinc-500">Set the dimensions for the layer.</p>
</div>
</div>
</AppPopoverContent>
</AppPopover>
`,
}),
};

View File

@@ -1,8 +1,22 @@
<script setup lang="ts">
/**
* @component AppPopover
* @description Root container for a popover menu.
*/
import type { PopoverRootEmits, PopoverRootProps } from 'reka-ui';
import { PopoverRoot, useForwardPropsEmits } from 'reka-ui';
const props = defineProps<PopoverRootProps>();
/*
* Types & Interfaces.
*/
export interface AppPopoverProps extends PopoverRootProps {}
/*
* Component Setup.
*/
const props = defineProps<AppPopoverProps>();
const emits = defineEmits<PopoverRootEmits>();
const forwarded = useForwardPropsEmits(props, emits);

View File

@@ -1,8 +1,22 @@
<script setup lang="ts">
/**
* @component AppPopoverAnchor
* @description An optional element used to anchor the popover.
*/
import type { PopoverAnchorProps } from 'reka-ui';
import { PopoverAnchor } from 'reka-ui';
const props = defineProps<PopoverAnchorProps>();
/*
* Types & Interfaces.
*/
export interface AppPopoverAnchorProps extends PopoverAnchorProps {}
/*
* Component Setup.
*/
const props = defineProps<AppPopoverAnchorProps>();
</script>
<template>

View File

@@ -1,4 +1,8 @@
<script setup lang="ts">
/**
* @component AppPopoverContent
* @description The main content area for a popover, including portal and animations.
*/
import { cn } from '@/utils/ui';
import { reactiveOmit } from '@vueuse/core';
import type { PopoverContentEmits, PopoverContentProps } from 'reka-ui';
@@ -9,14 +13,23 @@ defineOptions({
inheritAttrs: false,
});
const props = withDefaults(
defineProps<PopoverContentProps & { class?: HTMLAttributes['class'] }>(),
{
align: 'center',
sideOffset: 4,
class: undefined,
},
);
/*
* Types & Interfaces.
*/
export interface AppPopoverContentProps extends PopoverContentProps {
class?: HTMLAttributes['class'];
}
/*
* Component Setup.
*/
const props = withDefaults(defineProps<AppPopoverContentProps>(), {
align: 'center',
sideOffset: 4,
class: undefined,
});
const emits = defineEmits<PopoverContentEmits>();
const delegatedProps = reactiveOmit(props, 'class');

View File

@@ -1,8 +1,22 @@
<script setup lang="ts">
/**
* @component AppPopoverTrigger
* @description The interactive element that opens the popover.
*/
import type { PopoverTriggerProps } from 'reka-ui';
import { PopoverTrigger } from 'reka-ui';
const props = defineProps<PopoverTriggerProps>();
/*
* Types & Interfaces.
*/
export interface AppPopoverTriggerProps extends PopoverTriggerProps {}
/*
* Component Setup.
*/
const props = defineProps<AppPopoverTriggerProps>();
</script>
<template>

View File

@@ -1,4 +1,8 @@
<script setup lang="ts">
/**
* @component AppResizableHandle
* @description The interactive handle used to resize panels in a group.
*/
import { cn } from '@/utils/ui';
import { reactiveOmit } from '@vueuse/core';
import { GripVertical } from 'lucide-vue-next';
@@ -10,12 +14,20 @@ import {
} from 'reka-ui';
import type { HTMLAttributes } from 'vue';
const props = defineProps<
SplitterResizeHandleProps & {
class?: HTMLAttributes['class'];
withHandle?: boolean;
}
>();
/*
* Types & Interfaces.
*/
export interface AppResizableHandleProps extends SplitterResizeHandleProps {
class?: HTMLAttributes['class'];
withHandle?: boolean;
}
/*
* Component Setup.
*/
const props = defineProps<AppResizableHandleProps>();
const emits = defineEmits<SplitterResizeHandleEmits>();
const delegatedProps = reactiveOmit(props, 'class', 'withHandle');

Some files were not shown because too many files have changed in this diff Show More