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:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -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
29
.storybook/main.ts
Normal 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
22
.storybook/preview.ts
Normal 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
1756
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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%);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
40
resources/js/components/base/badge/AppBadge.stories.ts
Normal file
40
resources/js/components/base/badge/AppBadge.stories.ts
Normal 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>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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: {
|
||||
|
||||
51
resources/js/components/base/button/AppButton.stories.ts
Normal file
51
resources/js/components/base/button/AppButton.stories.ts
Normal 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>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
46
resources/js/components/base/card/AppCard.stories.ts
Normal file
46
resources/js/components/base/card/AppCard.stories.ts
Normal 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>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
36
resources/js/components/base/checkbox/AppCheckbox.stories.ts
Normal file
36
resources/js/components/base/checkbox/AppCheckbox.stories.ts
Normal 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>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
@@ -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,
|
||||
)
|
||||
"
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
"
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
"
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
59
resources/js/components/base/dialog/AppDialog.stories.ts
Normal file
59
resources/js/components/base/dialog/AppDialog.stories.ts
Normal 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>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>();
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
51
resources/js/components/base/input/AppInput.stories.ts
Normal file
51
resources/js/components/base/input/AppInput.stories.ts
Normal 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',
|
||||
},
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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>;
|
||||
|
||||
18
resources/js/components/base/label/AppLabel.stories.ts
Normal file
18
resources/js/components/base/label/AppLabel.stories.ts
Normal 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>',
|
||||
}),
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
45
resources/js/components/base/panel/AppPanel.stories.ts
Normal file
45
resources/js/components/base/panel/AppPanel.stories.ts
Normal 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>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
35
resources/js/components/base/panel/AppPanel.vue
Normal file
35
resources/js/components/base/panel/AppPanel.vue
Normal 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>
|
||||
28
resources/js/components/base/panel/AppPanelContent.vue
Normal file
28
resources/js/components/base/panel/AppPanelContent.vue
Normal 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>
|
||||
28
resources/js/components/base/panel/AppPanelDescription.vue
Normal file
28
resources/js/components/base/panel/AppPanelDescription.vue
Normal 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>
|
||||
28
resources/js/components/base/panel/AppPanelHeader.vue
Normal file
28
resources/js/components/base/panel/AppPanelHeader.vue
Normal 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>
|
||||
28
resources/js/components/base/panel/AppPanelTitle.vue
Normal file
28
resources/js/components/base/panel/AppPanelTitle.vue
Normal 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>
|
||||
5
resources/js/components/base/panel/index.ts
Normal file
5
resources/js/components/base/panel/index.ts
Normal 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';
|
||||
33
resources/js/components/base/popover/AppPopover.stories.ts
Normal file
33
resources/js/components/base/popover/AppPopover.stories.ts
Normal 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>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user