feature/project-redesign #28
@@ -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