feature/project-redesign #28

Merged
ilia merged 88 commits from feature/project-redesign into main 2026-03-02 19:46:39 +00:00
12 changed files with 0 additions and 1110 deletions
Showing only changes of commit 0c3dcc243a - Show all commits

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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