chore(ui): remove obsolete UI components
This commit is contained in:
@@ -1,189 +0,0 @@
|
|||||||
<script module>
|
|
||||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
|
||||||
import DotIndicator from './DotIndicator.svelte';
|
|
||||||
|
|
||||||
const { Story } = defineMeta({
|
|
||||||
title: 'Shared/DotIndicator',
|
|
||||||
component: DotIndicator,
|
|
||||||
tags: ['autodocs'],
|
|
||||||
parameters: {
|
|
||||||
docs: {
|
|
||||||
description: {
|
|
||||||
component:
|
|
||||||
'Circular status indicator with size options and pulse animation. Use for showing online/offline status, loading states, or feedback indicators.',
|
|
||||||
},
|
|
||||||
story: { inline: false },
|
|
||||||
},
|
|
||||||
layout: 'centered',
|
|
||||||
},
|
|
||||||
argTypes: {
|
|
||||||
variant: {
|
|
||||||
control: 'select',
|
|
||||||
options: ['online', 'offline', 'busy', 'away', 'success', 'warning', 'error'],
|
|
||||||
description: 'Status variant of the indicator',
|
|
||||||
},
|
|
||||||
size: {
|
|
||||||
control: 'select',
|
|
||||||
options: ['sm', 'md', 'lg'],
|
|
||||||
description: 'Size of the indicator',
|
|
||||||
},
|
|
||||||
pulse: {
|
|
||||||
control: 'boolean',
|
|
||||||
description: 'Enable pulse animation',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import type { Snippet } from 'svelte';
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Story name="Default">
|
|
||||||
{#snippet template(args)}
|
|
||||||
<DotIndicator {...args} />
|
|
||||||
{/snippet}
|
|
||||||
</Story>
|
|
||||||
|
|
||||||
<Story name="Variants">
|
|
||||||
{#snippet template(args)}
|
|
||||||
<div class="flex gap-4 items-center">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<DotIndicator variant="online" {...args} />
|
|
||||||
<span class="text-sm">Online</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<DotIndicator variant="offline" {...args} />
|
|
||||||
<span class="text-sm">Offline</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<DotIndicator variant="busy" {...args} />
|
|
||||||
<span class="text-sm">Busy</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<DotIndicator variant="away" {...args} />
|
|
||||||
<span class="text-sm">Away</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<DotIndicator variant="success" {...args} />
|
|
||||||
<span class="text-sm">Success</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<DotIndicator variant="warning" {...args} />
|
|
||||||
<span class="text-sm">Warning</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<DotIndicator variant="error" {...args} />
|
|
||||||
<span class="text-sm">Error</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/snippet}
|
|
||||||
</Story>
|
|
||||||
|
|
||||||
<Story name="Sizes">
|
|
||||||
{#snippet template(args)}
|
|
||||||
<div class="flex gap-4 items-center">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<DotIndicator size="sm" {...args} />
|
|
||||||
<span class="text-sm text-xs">Small</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<DotIndicator size="md" {...args} />
|
|
||||||
<span class="text-sm">Medium</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<DotIndicator size="lg" {...args} />
|
|
||||||
<span class="text-sm">Large</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/snippet}
|
|
||||||
</Story>
|
|
||||||
|
|
||||||
<Story name="With Pulse">
|
|
||||||
{#snippet template(args)}
|
|
||||||
<div class="flex gap-4 items-center">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<DotIndicator variant="online" pulse {...args} />
|
|
||||||
<span class="text-sm">Online</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<DotIndicator variant="busy" pulse {...args} />
|
|
||||||
<span class="text-sm">Busy</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<DotIndicator variant="away" pulse {...args} />
|
|
||||||
<span class="text-sm">Away</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/snippet}
|
|
||||||
</Story>
|
|
||||||
|
|
||||||
<Story name="All Combinations">
|
|
||||||
{#snippet template(args)}
|
|
||||||
<div class="space-y-3">
|
|
||||||
<h3 class="text-sm font-semibold">Sizes</h3>
|
|
||||||
<div class="flex gap-4 items-center">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<DotIndicator variant="online" size="sm" {...args} />
|
|
||||||
<span class="text-sm text-xs">Small</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<DotIndicator variant="online" size="md" {...args} />
|
|
||||||
<span class="text-sm">Medium</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<DotIndicator variant="online" size="lg" {...args} />
|
|
||||||
<span class="text-sm">Large</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 class="text-sm font-semibold mt-4">Status Variants</h3>
|
|
||||||
<div class="flex gap-4 items-center">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<DotIndicator variant="online" {...args} />
|
|
||||||
<span class="text-sm">Online</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<DotIndicator variant="offline" {...args} />
|
|
||||||
<span class="text-sm">Offline</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<DotIndicator variant="busy" {...args} />
|
|
||||||
<span class="text-sm">Busy</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<DotIndicator variant="away" {...args} />
|
|
||||||
<span class="text-sm">Away</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 class="text-sm font-semibold mt-4">Feedback Variants</h3>
|
|
||||||
<div class="flex gap-4 items-center">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<DotIndicator variant="success" {...args} />
|
|
||||||
<span class="text-sm">Success</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<DotIndicator variant="warning" {...args} />
|
|
||||||
<span class="text-sm">Warning</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<DotIndicator variant="error" {...args} />
|
|
||||||
<span class="text-sm">Error</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 class="text-sm font-semibold mt-4">With Pulse Animation</h3>
|
|
||||||
<div class="flex gap-4 items-center">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<DotIndicator variant="online" pulse {...args} />
|
|
||||||
<span class="text-sm">Online</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<DotIndicator variant="busy" pulse {...args} />
|
|
||||||
<span class="text-sm">Busy</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/snippet}
|
|
||||||
</Story>
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
<!--
|
|
||||||
Component: DotIndicator
|
|
||||||
Circular status indicator with size options and pulse animation
|
|
||||||
-->
|
|
||||||
<script lang="ts">
|
|
||||||
interface Props {
|
|
||||||
/**
|
|
||||||
* Status variant of the indicator
|
|
||||||
* @default online
|
|
||||||
*/
|
|
||||||
variant?: 'online' | 'offline' | 'busy' | 'away' | 'success' | 'warning' | 'error';
|
|
||||||
/**
|
|
||||||
* Size of the indicator
|
|
||||||
* @default md
|
|
||||||
*/
|
|
||||||
size?: 'sm' | 'md' | 'lg';
|
|
||||||
/**
|
|
||||||
* Enable pulse animation
|
|
||||||
* @default false
|
|
||||||
*/
|
|
||||||
pulse?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
variant = 'online',
|
|
||||||
size = 'md',
|
|
||||||
pulse = false,
|
|
||||||
}: Props = $props();
|
|
||||||
|
|
||||||
const baseClasses = 'rounded-full';
|
|
||||||
|
|
||||||
const variantClasses = $derived(
|
|
||||||
variant === 'online' || variant === 'success'
|
|
||||||
? 'bg-green-500'
|
|
||||||
: variant === 'offline'
|
|
||||||
? 'bg-gray-400'
|
|
||||||
: variant === 'busy' || variant === 'error'
|
|
||||||
? 'bg-red-500'
|
|
||||||
: variant === 'away'
|
|
||||||
? 'bg-yellow-500'
|
|
||||||
: variant === 'warning'
|
|
||||||
? 'bg-yellow-500'
|
|
||||||
: 'bg-gray-400',
|
|
||||||
);
|
|
||||||
|
|
||||||
const sizeClasses = $derived(
|
|
||||||
size === 'sm'
|
|
||||||
? 'w-1.5 h-1.5'
|
|
||||||
: size === 'lg'
|
|
||||||
? 'w-3 h-3'
|
|
||||||
: 'w-2 h-2',
|
|
||||||
);
|
|
||||||
|
|
||||||
const pulseAnimation = $derived(pulse && 'animate-pulse');
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<span
|
|
||||||
class="{baseClasses} {variantClasses} {sizeClasses} {pulseAnimation}"
|
|
||||||
aria-label="Status indicator"
|
|
||||||
></span>
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
<!-- Component: Drawer -->
|
|
||||||
<script lang="ts">
|
|
||||||
import { Button } from '$shared/shadcn/ui/button';
|
|
||||||
import {
|
|
||||||
Content as DrawerContent,
|
|
||||||
Footer as DrawerFooter,
|
|
||||||
Header as DrawerHeader,
|
|
||||||
Root as DrawerRoot,
|
|
||||||
Trigger as DrawerTrigger,
|
|
||||||
} from '$shared/shadcn/ui/drawer';
|
|
||||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
|
||||||
import type { Snippet } from 'svelte';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
isOpen?: boolean;
|
|
||||||
trigger?: Snippet<[{ isOpen: boolean; onClick: () => void }]>;
|
|
||||||
content?: Snippet<[{ isOpen: boolean; className?: string }]>;
|
|
||||||
contentClassName?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { isOpen = $bindable(false), trigger, content, contentClassName }: Props = $props();
|
|
||||||
|
|
||||||
function handleClick() {
|
|
||||||
isOpen = !isOpen;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<DrawerRoot bind:open={isOpen}>
|
|
||||||
<DrawerTrigger>
|
|
||||||
{#if trigger}
|
|
||||||
{@render trigger({ isOpen, onClick: handleClick })}
|
|
||||||
{:else}
|
|
||||||
<Button onclick={handleClick}>
|
|
||||||
Open
|
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
</DrawerTrigger>
|
|
||||||
<DrawerContent>
|
|
||||||
{@render content?.({ isOpen, className: cn('min-h-60 px-2 pt-4 pb-8', contentClassName) })}
|
|
||||||
</DrawerContent>
|
|
||||||
</DrawerRoot>
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
<script module lang="ts">
|
|
||||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
|
||||||
import { createRawSnippet } from 'svelte';
|
|
||||||
import ExpandableWrapper from './ExpandableWrapper.svelte';
|
|
||||||
|
|
||||||
const visibleSnippet = createRawSnippet(() => ({
|
|
||||||
render: () =>
|
|
||||||
`<div class="w-48 p-2 font-bold text-indigo-600">
|
|
||||||
Always visible
|
|
||||||
</div>`,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const hiddenSnippet = createRawSnippet(() => ({
|
|
||||||
render: () =>
|
|
||||||
`<div class="p-4 space-y-2 border-t border-indigo-100 mt-2">
|
|
||||||
<div class="h-4 w-full bg-indigo-100 rounded animate-pulse"></div>
|
|
||||||
<div class="h-4 w-2/3 bg-indigo-50 rounded animate-pulse"></div>
|
|
||||||
</div>`,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const badgeSnippet = createRawSnippet(() => ({
|
|
||||||
render: () =>
|
|
||||||
`<div class="">
|
|
||||||
<span class="badge badge-primary">*</span>
|
|
||||||
</div>`,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const { Story } = defineMeta({
|
|
||||||
title: 'Shared/ExpandableWrapper',
|
|
||||||
component: ExpandableWrapper,
|
|
||||||
tags: ['autodocs'],
|
|
||||||
parameters: {
|
|
||||||
docs: {
|
|
||||||
description: {
|
|
||||||
component: 'Animated styled wrapper for content that can be expanded and collapsed.',
|
|
||||||
},
|
|
||||||
story: { inline: false }, // Render stories in iframe for state isolation
|
|
||||||
},
|
|
||||||
},
|
|
||||||
args: {
|
|
||||||
expanded: false,
|
|
||||||
disabled: false,
|
|
||||||
rotation: 'clockwise',
|
|
||||||
visibleContent: visibleSnippet,
|
|
||||||
hiddenContent: hiddenSnippet,
|
|
||||||
},
|
|
||||||
argTypes: {
|
|
||||||
expanded: { control: 'boolean' },
|
|
||||||
disabled: { control: 'boolean' },
|
|
||||||
rotation: {
|
|
||||||
control: 'select',
|
|
||||||
options: ['clockwise', 'counterclockwise'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
</script>
|
|
||||||
{/* @ts-ignore */ null}
|
|
||||||
<Story name="With hidden content">
|
|
||||||
{#snippet template(args)}
|
|
||||||
<div class="p-12 bg-slate-100 min-h-[300px] flex justify-center items-start">
|
|
||||||
<ExpandableWrapper
|
|
||||||
{...args}
|
|
||||||
bind:expanded={args.expanded}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/snippet}
|
|
||||||
</Story>
|
|
||||||
|
|
||||||
{/* @ts-ignore */ null}
|
|
||||||
<Story name="Disabled" args={{ disabled: true }}>
|
|
||||||
{#snippet template(args)}
|
|
||||||
<div class="p-12 bg-slate-100 min-h-[300px] flex justify-center items-start">
|
|
||||||
<ExpandableWrapper
|
|
||||||
{...args}
|
|
||||||
bind:expanded={args.expanded}
|
|
||||||
disabled={args.disabled}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/snippet}
|
|
||||||
</Story>
|
|
||||||
|
|
||||||
{/* @ts-ignore */ null}
|
|
||||||
<Story name="With badge" args={{ badge: badgeSnippet }}>
|
|
||||||
{#snippet template(args)}
|
|
||||||
<div class="p-12 bg-slate-100 min-h-[300px] flex justify-center items-start">
|
|
||||||
<ExpandableWrapper
|
|
||||||
{...args}
|
|
||||||
bind:expanded={args.expanded}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/snippet}
|
|
||||||
</Story>
|
|
||||||
@@ -1,219 +0,0 @@
|
|||||||
<!--
|
|
||||||
Component: ExpandableWrapper
|
|
||||||
Animated wrapper for content that can be expanded and collapsed.
|
|
||||||
-->
|
|
||||||
<script lang="ts">
|
|
||||||
import { debounce } from '$shared/lib/utils';
|
|
||||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
|
||||||
import type { Snippet } from 'svelte';
|
|
||||||
import { cubicOut } from 'svelte/easing';
|
|
||||||
import type { HTMLAttributes } from 'svelte/elements';
|
|
||||||
import { Spring } from 'svelte/motion';
|
|
||||||
import { slide } from 'svelte/transition';
|
|
||||||
|
|
||||||
interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
||||||
/**
|
|
||||||
* Bindable property to control the expanded state of the wrapper.
|
|
||||||
* @default false
|
|
||||||
*/
|
|
||||||
expanded?: boolean;
|
|
||||||
/**
|
|
||||||
* Disabled flag
|
|
||||||
* @default false
|
|
||||||
*/
|
|
||||||
disabled?: boolean;
|
|
||||||
/**
|
|
||||||
* Bindable property to bind:this
|
|
||||||
* @default null
|
|
||||||
*/
|
|
||||||
element?: HTMLElement | null;
|
|
||||||
/**
|
|
||||||
* Content that's always visible
|
|
||||||
*/
|
|
||||||
visibleContent?: Snippet<[{ expanded?: boolean; disabled?: boolean }]>;
|
|
||||||
/**
|
|
||||||
* Content that's hidden when the wrapper is collapsed
|
|
||||||
*/
|
|
||||||
hiddenContent?: Snippet<[{ expanded?: boolean; disabled?: boolean }]>;
|
|
||||||
/**
|
|
||||||
* Optional badge to render
|
|
||||||
*/
|
|
||||||
badge?: Snippet<[{ expanded?: boolean; disabled?: boolean }]>;
|
|
||||||
/**
|
|
||||||
* Callback for when the element's size changes
|
|
||||||
*/
|
|
||||||
onResize?: (rect: DOMRectReadOnly) => void;
|
|
||||||
/**
|
|
||||||
* Rotation animation direction
|
|
||||||
* @default 'clockwise'
|
|
||||||
*/
|
|
||||||
rotation?: 'clockwise' | 'counterclockwise';
|
|
||||||
/**
|
|
||||||
* Classes for intermnal container
|
|
||||||
*/
|
|
||||||
containerClassName?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
let {
|
|
||||||
expanded = $bindable(false),
|
|
||||||
disabled = false,
|
|
||||||
element = $bindable(null),
|
|
||||||
visibleContent,
|
|
||||||
hiddenContent,
|
|
||||||
badge,
|
|
||||||
onResize,
|
|
||||||
rotation = 'clockwise',
|
|
||||||
class: className = '',
|
|
||||||
containerClassName = '',
|
|
||||||
...props
|
|
||||||
}: Props = $props();
|
|
||||||
|
|
||||||
let timeoutId = $state<ReturnType<typeof setTimeout> | null>(null);
|
|
||||||
|
|
||||||
const xSpring = new Spring(0, {
|
|
||||||
stiffness: 0.14, // Lower is slower
|
|
||||||
damping: 0.5, // Settle
|
|
||||||
});
|
|
||||||
|
|
||||||
const ySpring = new Spring(0, {
|
|
||||||
stiffness: 0.32,
|
|
||||||
damping: 0.65,
|
|
||||||
});
|
|
||||||
|
|
||||||
const scaleSpring = new Spring(1, {
|
|
||||||
stiffness: 0.32,
|
|
||||||
damping: 0.65,
|
|
||||||
});
|
|
||||||
|
|
||||||
const rotateSpring = new Spring(0, {
|
|
||||||
stiffness: 0.12,
|
|
||||||
damping: 0.55,
|
|
||||||
});
|
|
||||||
|
|
||||||
function handleClickOutside(e: MouseEvent) {
|
|
||||||
if (element && !element.contains(e.target as Node)) {
|
|
||||||
expanded = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleWrapperClick() {
|
|
||||||
if (!disabled) {
|
|
||||||
expanded = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleKeyDown(e: KeyboardEvent) {
|
|
||||||
if (e.key === 'Enter' || (e.key === ' ' && !expanded)) {
|
|
||||||
e.preventDefault();
|
|
||||||
handleWrapperClick();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (expanded && e.key === 'Escape') {
|
|
||||||
expanded = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create debounced recize callback
|
|
||||||
const debouncedResize = debounce((entry: ResizeObserverEntry) => onResize?.(entry.contentRect), 50);
|
|
||||||
|
|
||||||
// Elevation and scale on activation
|
|
||||||
$effect(() => {
|
|
||||||
if (expanded && !disabled) {
|
|
||||||
// Lift up
|
|
||||||
ySpring.target = 8;
|
|
||||||
// Slightly bigger
|
|
||||||
scaleSpring.target = 1.1;
|
|
||||||
|
|
||||||
rotateSpring.target = rotation === 'clockwise' ? -0.5 : 0.5;
|
|
||||||
|
|
||||||
timeoutId = setTimeout(() => {
|
|
||||||
rotateSpring.target = 0;
|
|
||||||
scaleSpring.target = 1.05;
|
|
||||||
}, 300);
|
|
||||||
} else {
|
|
||||||
ySpring.target = 0;
|
|
||||||
scaleSpring.target = 1;
|
|
||||||
rotateSpring.target = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (timeoutId) {
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Click outside handling
|
|
||||||
$effect(() => {
|
|
||||||
if (typeof window === 'undefined') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('click', handleClickOutside);
|
|
||||||
return () => document.removeEventListener('click', handleClickOutside);
|
|
||||||
});
|
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
if (disabled) {
|
|
||||||
expanded = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Use an effect to watch the element's actual physical size
|
|
||||||
$effect(() => {
|
|
||||||
if (!element) return;
|
|
||||||
|
|
||||||
const observer = new ResizeObserver(entries => {
|
|
||||||
const entry = entries[0];
|
|
||||||
if (entry) {
|
|
||||||
debouncedResize(entry);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
observer.observe(element);
|
|
||||||
return () => observer.disconnect();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div
|
|
||||||
bind:this={element}
|
|
||||||
onclick={handleWrapperClick}
|
|
||||||
onkeydown={handleKeyDown}
|
|
||||||
role="button"
|
|
||||||
tabindex={0}
|
|
||||||
class={cn(
|
|
||||||
'will-change-[transform, width, height] duration-300',
|
|
||||||
disabled ? 'pointer-events-none' : 'pointer-events-auto',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
style:transform="
|
|
||||||
translate({xSpring.current}px, {ySpring.current}px)
|
|
||||||
scale({scaleSpring.current})
|
|
||||||
rotateZ({rotateSpring.current}deg)
|
|
||||||
"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{@render badge?.({ expanded, disabled })}
|
|
||||||
|
|
||||||
<div
|
|
||||||
class={cn(
|
|
||||||
'relative p-0.5 sm:p-2 rounded-lg sm:rounded-2xl border transition-all duration-250 ease-out flex flex-col gap-1.5 backdrop-blur-lg',
|
|
||||||
expanded
|
|
||||||
? 'bg-background-20 border-indigo-400/40 shadow-[0_30px_70px_-10px_rgba(99,102,241,0.25)]'
|
|
||||||
: 'bg-background-40 border-background-40 shadow-[0_12px_40px_-12px_rgba(0,0,0,0.12)]',
|
|
||||||
disabled && 'opacity-80 grayscale-[0.2]',
|
|
||||||
containerClassName,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{@render visibleContent?.({ expanded, disabled })}
|
|
||||||
|
|
||||||
{#if expanded}
|
|
||||||
<div
|
|
||||||
in:slide={{ duration: 250, easing: cubicOut }}
|
|
||||||
out:slide={{ duration: 250, easing: cubicOut }}
|
|
||||||
>
|
|
||||||
{@render hiddenContent?.({ expanded, disabled })}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
<script module>
|
|
||||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
|
||||||
import Metric from './Metric.svelte';
|
|
||||||
|
|
||||||
const { Story } = defineMeta({
|
|
||||||
title: 'Shared/Metric',
|
|
||||||
component: Metric,
|
|
||||||
tags: ['autodocs'],
|
|
||||||
parameters: {
|
|
||||||
docs: {
|
|
||||||
description: {
|
|
||||||
component: 'Large technical value display with label below and optional unit.',
|
|
||||||
},
|
|
||||||
story: { inline: false },
|
|
||||||
},
|
|
||||||
layout: 'centered',
|
|
||||||
},
|
|
||||||
argTypes: {
|
|
||||||
value: {
|
|
||||||
control: 'text',
|
|
||||||
description: 'Metric value',
|
|
||||||
defaultValue: '42',
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
control: 'text',
|
|
||||||
description: 'Metric label',
|
|
||||||
defaultValue: 'Label',
|
|
||||||
},
|
|
||||||
unit: {
|
|
||||||
control: 'text',
|
|
||||||
description: 'Optional unit',
|
|
||||||
},
|
|
||||||
variant: {
|
|
||||||
control: 'select',
|
|
||||||
options: ['default', 'accent', 'muted', 'success', 'warning', 'error'],
|
|
||||||
description: 'Color variant',
|
|
||||||
defaultValue: 'accent',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Story
|
|
||||||
name="With unit"
|
|
||||||
args={{ value: 128, label: 'Font Size', unit: 'px', variant: 'accent' }}
|
|
||||||
>
|
|
||||||
{#snippet template()}
|
|
||||||
<Metric value={128} label="Font Size" unit="px" variant="accent" />
|
|
||||||
{/snippet}
|
|
||||||
</Story>
|
|
||||||
|
|
||||||
<Story
|
|
||||||
name="Without unit"
|
|
||||||
args={{ value: 500, label: 'Weight', variant: 'accent' }}
|
|
||||||
>
|
|
||||||
{#snippet template()}
|
|
||||||
<Metric value={500} label="Weight" variant="accent" />
|
|
||||||
{/snippet}
|
|
||||||
</Story>
|
|
||||||
|
|
||||||
<Story
|
|
||||||
name="Default variant"
|
|
||||||
args={{ value: 42, label: 'Value', variant: 'default' }}
|
|
||||||
>
|
|
||||||
{#snippet template()}
|
|
||||||
<Metric value={42} label="Value" variant="default" />
|
|
||||||
{/snippet}
|
|
||||||
</Story>
|
|
||||||
|
|
||||||
<Story
|
|
||||||
name="Accent variant"
|
|
||||||
args={{ value: 128, label: 'Size', variant: 'accent' }}
|
|
||||||
>
|
|
||||||
{#snippet template()}
|
|
||||||
<Metric value={128} label="Size" variant="accent" />
|
|
||||||
{/snippet}
|
|
||||||
</Story>
|
|
||||||
|
|
||||||
<Story
|
|
||||||
name="Muted variant"
|
|
||||||
args={{ value: 0, label: 'Spacing', variant: 'muted' }}
|
|
||||||
>
|
|
||||||
{#snippet template()}
|
|
||||||
<Metric value={0} label="Spacing" variant="muted" />
|
|
||||||
{/snippet}
|
|
||||||
</Story>
|
|
||||||
|
|
||||||
<Story
|
|
||||||
name="Success variant"
|
|
||||||
args={{ value: 100, label: 'Score', variant: 'success' }}
|
|
||||||
>
|
|
||||||
{#snippet template()}
|
|
||||||
<Metric value={100} label="Score" variant="success" />
|
|
||||||
{/snippet}
|
|
||||||
</Story>
|
|
||||||
|
|
||||||
<Story
|
|
||||||
name="Warning variant"
|
|
||||||
args={{ value: 3, label: 'Issues', variant: 'warning' }}
|
|
||||||
>
|
|
||||||
{#snippet template()}
|
|
||||||
<Metric value={3} label="Issues" variant="warning" />
|
|
||||||
{/snippet}
|
|
||||||
</Story>
|
|
||||||
|
|
||||||
<Story
|
|
||||||
name="Error variant"
|
|
||||||
args={{ value: 1, label: 'Errors', variant: 'error' }}
|
|
||||||
>
|
|
||||||
{#snippet template()}
|
|
||||||
<Metric value={1} label="Errors" variant="error" />
|
|
||||||
{/snippet}
|
|
||||||
</Story>
|
|
||||||
|
|
||||||
<Story name="All variants with unit">
|
|
||||||
{#snippet template()}
|
|
||||||
<div class="flex gap-6">
|
|
||||||
<Metric value={42} label="Default" unit="px" variant="default" />
|
|
||||||
<Metric value={42} label="Accent" unit="px" variant="accent" />
|
|
||||||
<Metric value={42} label="Muted" unit="px" variant="muted" />
|
|
||||||
<Metric value={42} label="Success" unit="px" variant="success" />
|
|
||||||
<Metric value={42} label="Warning" unit="px" variant="warning" />
|
|
||||||
<Metric value={42} label="Error" unit="px" variant="error" />
|
|
||||||
</div>
|
|
||||||
{/snippet}
|
|
||||||
</Story>
|
|
||||||
|
|
||||||
<Story name="All variants without unit">
|
|
||||||
{#snippet template()}
|
|
||||||
<div class="flex gap-6">
|
|
||||||
<Metric value={42} label="Default" variant="default" />
|
|
||||||
<Metric value={42} label="Accent" variant="accent" />
|
|
||||||
<Metric value={42} label="Muted" variant="muted" />
|
|
||||||
<Metric value={42} label="Success" variant="success" />
|
|
||||||
<Metric value={42} label="Warning" variant="warning" />
|
|
||||||
<Metric value={42} label="Error" variant="error" />
|
|
||||||
</div>
|
|
||||||
{/snippet}
|
|
||||||
</Story>
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
<!--
|
|
||||||
Component: Metric
|
|
||||||
Large technical value display with label below and optional unit.
|
|
||||||
-->
|
|
||||||
<script lang="ts">
|
|
||||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
|
||||||
import { Label } from '$shared/ui';
|
|
||||||
import {
|
|
||||||
type LabelVariant,
|
|
||||||
labelVariantConfig,
|
|
||||||
} from '$shared/ui/Label/config';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
value: string | number;
|
|
||||||
label: string;
|
|
||||||
unit?: string;
|
|
||||||
variant?: LabelVariant;
|
|
||||||
class?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
let {
|
|
||||||
value,
|
|
||||||
label,
|
|
||||||
unit,
|
|
||||||
variant = 'accent',
|
|
||||||
class: className,
|
|
||||||
}: Props = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class={cn('flex flex-col gap-1', className)}>
|
|
||||||
<div class="flex items-baseline gap-1">
|
|
||||||
<span
|
|
||||||
class={cn(
|
|
||||||
'font-mono text-2xl font-bold tabular-nums',
|
|
||||||
labelVariantConfig[variant],
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{value}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{#if unit}
|
|
||||||
<Label variant="muted" size="xs">{unit}</Label>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Label variant="muted" size="xs">{label}</Label>
|
|
||||||
</div>
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
<script module>
|
|
||||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
|
||||||
import MicroLabel from './MicroLabel.svelte';
|
|
||||||
|
|
||||||
const { Story } = defineMeta({
|
|
||||||
title: 'Shared/MicroLabel',
|
|
||||||
component: MicroLabel,
|
|
||||||
tags: ['autodocs'],
|
|
||||||
parameters: {
|
|
||||||
docs: {
|
|
||||||
description: {
|
|
||||||
component:
|
|
||||||
'Tiny uppercase label for annotations and metadata. Ideal for field labels, status indicators, and category markers.',
|
|
||||||
},
|
|
||||||
story: { inline: false },
|
|
||||||
},
|
|
||||||
layout: 'centered',
|
|
||||||
},
|
|
||||||
argTypes: {
|
|
||||||
variant: {
|
|
||||||
control: 'select',
|
|
||||||
options: ['muted', 'accent', 'subtle'],
|
|
||||||
description: 'Color variant of the label',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Story name="Default">
|
|
||||||
{#snippet template(args)}
|
|
||||||
<MicroLabel {...args}>Label</MicroLabel>
|
|
||||||
{/snippet}
|
|
||||||
</Story>
|
|
||||||
|
|
||||||
<Story name="Variants">
|
|
||||||
{#snippet template(args)}
|
|
||||||
<div class="space-y-2">
|
|
||||||
<div>
|
|
||||||
<MicroLabel variant="muted" {...args}>Muted Label</MicroLabel>
|
|
||||||
<p class="text-gray-600 dark:text-gray-400 mt-1">Supporting text goes here</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<MicroLabel variant="accent" {...args}>Accent Label</MicroLabel>
|
|
||||||
<p class="text-gray-600 dark:text-gray-400 mt-1">Supporting text goes here</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<MicroLabel variant="subtle" {...args}>Subtle Label</MicroLabel>
|
|
||||||
<p class="text-gray-600 dark:text-gray-400 mt-1">Supporting text goes here</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/snippet}
|
|
||||||
</Story>
|
|
||||||
|
|
||||||
<Story name="Inline Usage">
|
|
||||||
{#snippet template(args)}
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<MicroLabel variant="accent" {...args}>Status</MicroLabel>
|
|
||||||
<span class="text-sm">Active</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<MicroLabel {...args}>Category</MicroLabel>
|
|
||||||
<span class="text-sm">Typography</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/snippet}
|
|
||||||
</Story>
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<!--
|
|
||||||
Component: MicroLabel
|
|
||||||
Tiny uppercase label for annotations and metadata
|
|
||||||
-->
|
|
||||||
<script lang="ts">
|
|
||||||
interface Props {
|
|
||||||
/**
|
|
||||||
* Color variant of the label
|
|
||||||
* @default muted
|
|
||||||
*/
|
|
||||||
variant?: 'muted' | 'accent' | 'subtle';
|
|
||||||
}
|
|
||||||
|
|
||||||
const { variant = 'muted' }: Props = $props();
|
|
||||||
|
|
||||||
const baseClasses = 'text-[10px] uppercase tracking-wider font-semibold';
|
|
||||||
|
|
||||||
const variantClasses = $derived(
|
|
||||||
variant === 'accent'
|
|
||||||
? 'text-indigo-600 dark:text-indigo-400'
|
|
||||||
: variant === 'subtle'
|
|
||||||
? 'text-gray-500 dark:text-gray-500'
|
|
||||||
: 'text-gray-400 dark:text-gray-500',
|
|
||||||
);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<span class="{baseClasses} {variantClasses}">
|
|
||||||
<slot />
|
|
||||||
</span>
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
<!--
|
|
||||||
Component: SidebarMenu
|
|
||||||
Slides out from the right, closes on click outside
|
|
||||||
-->
|
|
||||||
<script lang="ts">
|
|
||||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
|
||||||
import type { Snippet } from 'svelte';
|
|
||||||
import { cubicOut } from 'svelte/easing';
|
|
||||||
import {
|
|
||||||
fade,
|
|
||||||
slide,
|
|
||||||
} from 'svelte/transition';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
/**
|
|
||||||
* Children to render conditionally
|
|
||||||
*/
|
|
||||||
children?: Snippet;
|
|
||||||
/**
|
|
||||||
* Action (always visible) to render
|
|
||||||
*/
|
|
||||||
action?: Snippet;
|
|
||||||
/**
|
|
||||||
* Wrapper reference to bind
|
|
||||||
*/
|
|
||||||
wrapper?: HTMLElement | null;
|
|
||||||
/**
|
|
||||||
* Class to add to the wrapper
|
|
||||||
*/
|
|
||||||
class?: string;
|
|
||||||
/**
|
|
||||||
* Bindable visibility flag
|
|
||||||
*/
|
|
||||||
visible?: boolean;
|
|
||||||
/**
|
|
||||||
* Handler for click outside
|
|
||||||
*/
|
|
||||||
onClickOutside?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
let {
|
|
||||||
children,
|
|
||||||
action,
|
|
||||||
wrapper = $bindable<HTMLElement | null>(null),
|
|
||||||
class: className,
|
|
||||||
visible = $bindable(false),
|
|
||||||
onClickOutside,
|
|
||||||
}: Props = $props();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Closes menu on click outside
|
|
||||||
*/
|
|
||||||
function handleClick(event: MouseEvent) {
|
|
||||||
if (!wrapper || !visible) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!wrapper.contains(event.target as Node)) {
|
|
||||||
visible = false;
|
|
||||||
onClickOutside?.();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svelte:window on:click={handleClick} />
|
|
||||||
|
|
||||||
<div
|
|
||||||
class={cn(
|
|
||||||
'transition-all duration-300 delay-200 cubic-bezier-out',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
bind:this={wrapper}
|
|
||||||
>
|
|
||||||
{@render action?.()}
|
|
||||||
{#if visible}
|
|
||||||
<div
|
|
||||||
class="relative z-20 h-full w-auto flex flex-col"
|
|
||||||
in:fade={{ duration: 300, delay: 400, easing: cubicOut }}
|
|
||||||
out:fade={{ duration: 150, easing: cubicOut }}
|
|
||||||
>
|
|
||||||
{@render children?.()}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Background Gradient -->
|
|
||||||
<div
|
|
||||||
class="
|
|
||||||
absolute inset-0 z-10 h-full transition-all duration-700
|
|
||||||
bg-linear-to-r from-white/75 via-white/45 to-white/10
|
|
||||||
bg-[radial-gradient(ellipse_at_left,_rgba(255,252,245,0.4)_0%,_transparent_70%)]
|
|
||||||
shadow-[_inset_-1px_0_0_rgba(0,0,0,0.04)]
|
|
||||||
border-r border-white/90
|
|
||||||
after:absolute after:right-[-1px] after:top-0 after:h-full after:w-[1px] after:bg-black/[0.05]
|
|
||||||
backdrop-blur-md
|
|
||||||
"
|
|
||||||
in:slide={{ axis: 'x', duration: 250, delay: 100, easing: cubicOut }}
|
|
||||||
out:slide={{ axis: 'x', duration: 150, easing: cubicOut }}
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
<script module>
|
|
||||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
|
||||||
import StatusIndicator from './StatusIndicator.svelte';
|
|
||||||
|
|
||||||
const { Story } = defineMeta({
|
|
||||||
title: 'Shared/StatusIndicator',
|
|
||||||
component: StatusIndicator,
|
|
||||||
tags: ['autodocs'],
|
|
||||||
parameters: {
|
|
||||||
docs: {
|
|
||||||
description: {
|
|
||||||
component: 'Coloured dot with optional label. Dot can pulse for live states.',
|
|
||||||
},
|
|
||||||
story: { inline: false },
|
|
||||||
},
|
|
||||||
layout: 'centered',
|
|
||||||
},
|
|
||||||
argTypes: {
|
|
||||||
status: {
|
|
||||||
control: 'select',
|
|
||||||
options: ['active', 'inactive', 'warning', 'error'],
|
|
||||||
description: 'Status type',
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
control: 'text',
|
|
||||||
description: 'Optional label text',
|
|
||||||
},
|
|
||||||
pulse: {
|
|
||||||
control: 'boolean',
|
|
||||||
description: 'Enable pulsing animation',
|
|
||||||
defaultValue: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Story
|
|
||||||
name="Active status"
|
|
||||||
args={{ status: 'active', label: 'Active' }}
|
|
||||||
>
|
|
||||||
{#snippet template()}
|
|
||||||
<StatusIndicator status="active" label="Active" />
|
|
||||||
{/snippet}
|
|
||||||
</Story>
|
|
||||||
|
|
||||||
<Story
|
|
||||||
name="Inactive status"
|
|
||||||
args={{ status: 'inactive', label: 'Inactive' }}
|
|
||||||
>
|
|
||||||
{#snippet template()}
|
|
||||||
<StatusIndicator status="inactive" label="Inactive" />
|
|
||||||
{/snippet}
|
|
||||||
</Story>
|
|
||||||
|
|
||||||
<Story
|
|
||||||
name="Warning status"
|
|
||||||
args={{ status: 'warning', label: 'Warning' }}
|
|
||||||
>
|
|
||||||
{#snippet template()}
|
|
||||||
<StatusIndicator status="warning" label="Warning" />
|
|
||||||
{/snippet}
|
|
||||||
</Story>
|
|
||||||
|
|
||||||
<Story
|
|
||||||
name="Error status"
|
|
||||||
args={{ status: 'error', label: 'Error' }}
|
|
||||||
>
|
|
||||||
{#snippet template()}
|
|
||||||
<StatusIndicator status="error" label="Error" />
|
|
||||||
{/snippet}
|
|
||||||
</Story>
|
|
||||||
|
|
||||||
<Story
|
|
||||||
name="With pulse"
|
|
||||||
args={{ status: 'active', label: 'Connected', pulse: true }}
|
|
||||||
>
|
|
||||||
{#snippet template()}
|
|
||||||
<StatusIndicator status="active" label="Connected" pulse={true} />
|
|
||||||
{/snippet}
|
|
||||||
</Story>
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
<!--
|
|
||||||
Component: StatusIndicator
|
|
||||||
Coloured dot with optional label. Dot can pulse for live states.
|
|
||||||
-->
|
|
||||||
<script lang="ts">
|
|
||||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
|
||||||
import { Label } from '$shared/ui';
|
|
||||||
|
|
||||||
type Status = 'active' | 'inactive' | 'warning' | 'error';
|
|
||||||
|
|
||||||
const statusConfig: Record<Status, string> = {
|
|
||||||
active: 'bg-green-500',
|
|
||||||
inactive: 'bg-neutral-400',
|
|
||||||
warning: 'bg-yellow-500',
|
|
||||||
error: 'bg-brand',
|
|
||||||
};
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
status: Status;
|
|
||||||
label?: string;
|
|
||||||
pulse?: boolean;
|
|
||||||
class?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
let {
|
|
||||||
status,
|
|
||||||
label,
|
|
||||||
pulse = false,
|
|
||||||
class: className,
|
|
||||||
}: Props = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class={cn('flex items-center gap-2', className)}>
|
|
||||||
<span
|
|
||||||
class={cn(
|
|
||||||
'w-1.5 h-1.5 rounded-full',
|
|
||||||
statusConfig[status],
|
|
||||||
pulse && 'animate-pulse',
|
|
||||||
)}
|
|
||||||
></span>
|
|
||||||
|
|
||||||
{#if label}
|
|
||||||
<Label variant="muted" size="xs">{label}</Label>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
Reference in New Issue
Block a user