@djangocfg/ui-core 2.1.381 → 2.1.383
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +85 -21
- package/package.json +5 -12
- package/src/components/boundary/Boundary.tsx +204 -33
- package/src/components/boundary/README.md +249 -0
- package/src/components/boundary/index.ts +9 -2
- package/src/components/index.ts +9 -2
- package/src/components/select/combobox.tsx +47 -19
- package/src/hooks/audio/createSoundBus.ts +172 -0
- package/src/hooks/audio/index.ts +21 -0
- package/src/hooks/audio/useAudioPrefs.ts +91 -0
- package/src/hooks/audio/useNotificationSounds.ts +271 -0
- package/src/hooks/audio/useSoundEffect.ts +78 -0
- package/src/hooks/hotkey/formatHotkey.ts +96 -0
- package/src/hooks/hotkey/index.ts +10 -0
- package/src/hooks/hotkey/useHotkey.ts +106 -34
- package/src/hooks/hotkey/useHotkeyChord.ts +96 -0
- package/src/hooks/hotkey/useHotkeyHelp.ts +68 -0
- package/src/hooks/index.ts +1 -0
- package/src/components/boundary/boundary.story.tsx +0 -109
- package/src/components/data/avatar/avatar.story.tsx +0 -115
- package/src/components/data/badge/badge.story.tsx +0 -56
- package/src/components/data/calendar/calendar.story.tsx +0 -127
- package/src/components/data/carousel/carousel.story.tsx +0 -122
- package/src/components/data/progress/progress.story.tsx +0 -97
- package/src/components/data/table/table.story.tsx +0 -148
- package/src/components/data/toggle/toggle.story.tsx +0 -104
- package/src/components/data/toggle-group/toggle-group.story.tsx +0 -118
- package/src/components/feedback/alert/alert.story.tsx +0 -77
- package/src/components/feedback/empty/empty.story.tsx +0 -115
- package/src/components/feedback/preloader/preloader.story.tsx +0 -86
- package/src/components/feedback/spinner/spinner.story.tsx +0 -66
- package/src/components/forms/button/button.story.tsx +0 -116
- package/src/components/forms/button-download/button-download.story.tsx +0 -112
- package/src/components/forms/button-group/button-group.story.tsx +0 -79
- package/src/components/forms/checkbox/checkbox.story.tsx +0 -89
- package/src/components/forms/input/input.story.tsx +0 -77
- package/src/components/forms/input-group/input-group.story.tsx +0 -119
- package/src/components/forms/input-otp/input-otp.story.tsx +0 -105
- package/src/components/forms/label/label.story.tsx +0 -52
- package/src/components/forms/radio-group/radio-group.story.tsx +0 -113
- package/src/components/forms/slider/slider.story.tsx +0 -134
- package/src/components/forms/switch/switch.story.tsx +0 -98
- package/src/components/forms/textarea/textarea.story.tsx +0 -94
- package/src/components/layout/aspect-ratio/aspect-ratio.story.tsx +0 -94
- package/src/components/layout/card/card.story.tsx +0 -105
- package/src/components/layout/resizable/resizable.story.tsx +0 -119
- package/src/components/layout/scroll-area/scroll-area.story.tsx +0 -172
- package/src/components/layout/separator/separator.story.tsx +0 -69
- package/src/components/layout/skeleton/skeleton.story.tsx +0 -101
- package/src/components/navigation/accordion/accordion.story.tsx +0 -110
- package/src/components/navigation/collapsible/collapsible.story.tsx +0 -133
- package/src/components/navigation/command/command.story.tsx +0 -121
- package/src/components/navigation/context-menu/context-menu.story.tsx +0 -125
- package/src/components/navigation/dropdown-menu/dropdown-menu.story.tsx +0 -208
- package/src/components/navigation/menubar/menubar.story.tsx +0 -152
- package/src/components/navigation/navigation-menu/navigation-menu.story.tsx +0 -154
- package/src/components/navigation/tabs/tabs.story.tsx +0 -98
- package/src/components/overlay/alert-dialog/alert-dialog.story.tsx +0 -104
- package/src/components/overlay/dialog/dialog.story.tsx +0 -212
- package/src/components/overlay/drawer/drawer.story.tsx +0 -359
- package/src/components/overlay/hover-card/hover-card.story.tsx +0 -102
- package/src/components/overlay/popover/popover.story.tsx +0 -127
- package/src/components/overlay/responsive-sheet/responsive-sheet.story.tsx +0 -117
- package/src/components/overlay/sheet/sheet.story.tsx +0 -148
- package/src/components/overlay/tooltip/tooltip.story.tsx +0 -139
- package/src/components/select/combobox-async.story.tsx +0 -215
- package/src/components/select/combobox.story.tsx +0 -226
- package/src/components/select/country-select.story.tsx +0 -261
- package/src/components/select/language-select.story.tsx +0 -264
- package/src/components/select/multi-select.story.tsx +0 -122
- package/src/components/select/select.story.tsx +0 -112
- package/src/components/specialized/copy/copy.story.tsx +0 -77
- package/src/components/specialized/flag/flag.story.tsx +0 -82
- package/src/components/specialized/image-with-fallback/image-with-fallback.story.tsx +0 -105
- package/src/components/specialized/kbd/kbd.story.tsx +0 -113
- package/src/lib/dialog-service/dialog-service.story.tsx +0 -263
- package/src/stories/index.ts +0 -28
- package/src/styles/theme/theme-tokens.story.tsx +0 -157
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from 'react';
|
|
4
|
+
|
|
5
|
+
export interface UseHotkeyChordOptions {
|
|
6
|
+
/** Whether the chord is active. @default true */
|
|
7
|
+
enabled?: boolean;
|
|
8
|
+
/** Max delay between consecutive keys, in ms. @default 800 */
|
|
9
|
+
window?: number;
|
|
10
|
+
/**
|
|
11
|
+
* Fire even when focus is inside an input / textarea / contenteditable.
|
|
12
|
+
* @default false — chord sequences shouldn't hijack typing
|
|
13
|
+
*/
|
|
14
|
+
enableOnFormTags?: boolean;
|
|
15
|
+
/** Prevent default browser behaviour on each key. @default false */
|
|
16
|
+
preventDefault?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const FORM_TAGS = new Set(['INPUT', 'TEXTAREA', 'SELECT']);
|
|
20
|
+
|
|
21
|
+
function isEditableTarget(el: EventTarget | null): boolean {
|
|
22
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
23
|
+
if (FORM_TAGS.has(el.tagName)) return true;
|
|
24
|
+
if (el.isContentEditable) return true;
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Linear-style chord shortcuts — fire `callback` when the user presses
|
|
30
|
+
* a sequence of bare keys within a time window.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```tsx
|
|
34
|
+
* useHotkeyChord(['g', 't'], () => navigate('/tasks'));
|
|
35
|
+
* useHotkeyChord(['g', 'i'], () => navigate('/inbox'), { window: 1200 });
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* Each step is a single bare key (no modifiers). Pressing any
|
|
39
|
+
* non-sequence key resets progress, so partially-typed sequences don't
|
|
40
|
+
* fire accidentally.
|
|
41
|
+
*/
|
|
42
|
+
export function useHotkeyChord(
|
|
43
|
+
keys: readonly string[],
|
|
44
|
+
callback: (event: KeyboardEvent) => void,
|
|
45
|
+
options: UseHotkeyChordOptions = {},
|
|
46
|
+
): void {
|
|
47
|
+
const { enabled = true, window: chordWindow = 800, enableOnFormTags = false, preventDefault = false } = options;
|
|
48
|
+
const callbackRef = useRef(callback);
|
|
49
|
+
callbackRef.current = callback;
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (!enabled) return;
|
|
53
|
+
if (typeof window === 'undefined') return;
|
|
54
|
+
if (!keys.length) return;
|
|
55
|
+
|
|
56
|
+
const sequence = keys.map((k) => k.toLowerCase());
|
|
57
|
+
let progress = 0;
|
|
58
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
59
|
+
|
|
60
|
+
const reset = () => {
|
|
61
|
+
progress = 0;
|
|
62
|
+
if (timer) {
|
|
63
|
+
clearTimeout(timer);
|
|
64
|
+
timer = null;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const onKey = (e: KeyboardEvent) => {
|
|
69
|
+
if (!enableOnFormTags && isEditableTarget(e.target)) return;
|
|
70
|
+
// Chords don't carry modifiers.
|
|
71
|
+
if (e.metaKey || e.ctrlKey || e.altKey) return reset();
|
|
72
|
+
|
|
73
|
+
const expected = sequence[progress];
|
|
74
|
+
if (!expected) return;
|
|
75
|
+
const pressed = e.key.toLowerCase();
|
|
76
|
+
if (pressed !== expected) return reset();
|
|
77
|
+
|
|
78
|
+
if (preventDefault) e.preventDefault();
|
|
79
|
+
progress++;
|
|
80
|
+
if (progress >= sequence.length) {
|
|
81
|
+
callbackRef.current(e);
|
|
82
|
+
reset();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (timer) clearTimeout(timer);
|
|
87
|
+
timer = setTimeout(reset, chordWindow);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
window.addEventListener('keydown', onKey);
|
|
91
|
+
return () => {
|
|
92
|
+
window.removeEventListener('keydown', onKey);
|
|
93
|
+
if (timer) clearTimeout(timer);
|
|
94
|
+
};
|
|
95
|
+
}, [keys.join('|'), enabled, chordWindow, enableOnFormTags, preventDefault]);
|
|
96
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useSyncExternalStore } from 'react';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Module-level store of every hotkey that registered itself with a
|
|
7
|
+
* `description`. Lets you render a `?` cheat-sheet without prop-drilling.
|
|
8
|
+
*/
|
|
9
|
+
export interface RegisteredHotkey {
|
|
10
|
+
/** Key combo (raw string passed to `useHotkey` — format with `formatHotkey()` for display). */
|
|
11
|
+
combo: string;
|
|
12
|
+
/** Human-readable purpose. */
|
|
13
|
+
description: string;
|
|
14
|
+
/** Optional scope label for grouping (e.g. `'chat'`, `'global'`). */
|
|
15
|
+
scope?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const registry = new Map<symbol, RegisteredHotkey>();
|
|
19
|
+
const listeners = new Set<() => void>();
|
|
20
|
+
|
|
21
|
+
function notify() {
|
|
22
|
+
for (const cb of listeners) cb();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Register a hotkey entry while a component is mounted. Internal —
|
|
27
|
+
* called by `useHotkey` when a `description` is provided.
|
|
28
|
+
*/
|
|
29
|
+
export function registerHotkey(entry: RegisteredHotkey): () => void {
|
|
30
|
+
const key = Symbol(entry.combo);
|
|
31
|
+
registry.set(key, entry);
|
|
32
|
+
notify();
|
|
33
|
+
return () => {
|
|
34
|
+
registry.delete(key);
|
|
35
|
+
notify();
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Read the live list of registered hotkeys. SSR-safe. */
|
|
40
|
+
export function useHotkeyHelp(): RegisteredHotkey[] {
|
|
41
|
+
return useSyncExternalStore(
|
|
42
|
+
(cb) => {
|
|
43
|
+
listeners.add(cb);
|
|
44
|
+
return () => {
|
|
45
|
+
listeners.delete(cb);
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
() => Array.from(registry.values()),
|
|
49
|
+
() => [],
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Imperative read for non-React consumers (debug panels, etc.). */
|
|
54
|
+
export function getRegisteredHotkeys(): RegisteredHotkey[] {
|
|
55
|
+
return Array.from(registry.values());
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Hook variant — call alongside `useHotkey` when the binding lives
|
|
60
|
+
* outside the hook (e.g. raw `window.addEventListener` consumers that
|
|
61
|
+
* still want to appear in the cheat sheet).
|
|
62
|
+
*/
|
|
63
|
+
export function useRegisterHotkey(entry: RegisteredHotkey | null): void {
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (!entry) return;
|
|
66
|
+
return registerHotkey(entry);
|
|
67
|
+
}, [entry?.combo, entry?.description, entry?.scope]);
|
|
68
|
+
}
|
package/src/hooks/index.ts
CHANGED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import { defineStory, useSelect } from '@djangocfg/playground';
|
|
2
|
-
import { useState } from 'react';
|
|
3
|
-
|
|
4
|
-
import { Button } from '../forms/button';
|
|
5
|
-
import { Boundary } from '.';
|
|
6
|
-
import type { BoundaryVariant } from '.';
|
|
7
|
-
|
|
8
|
-
export default defineStory({
|
|
9
|
-
title: 'Core/Boundary',
|
|
10
|
-
component: Boundary,
|
|
11
|
-
description:
|
|
12
|
-
'React error boundary with multiple visual variants. Wrap untrusted subtrees (widgets, third-party iframes, dynamic renderers) so a single render error does not crash the whole page.',
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
function BoomButton({ label = 'Throw error' }: { label?: string }) {
|
|
16
|
-
const [boom, setBoom] = useState(false);
|
|
17
|
-
if (boom) throw new Error('Demo crash from BoomButton');
|
|
18
|
-
return (
|
|
19
|
-
<Button variant="outline" size="sm" onClick={() => setBoom(true)}>
|
|
20
|
-
{label}
|
|
21
|
-
</Button>
|
|
22
|
-
);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export const Interactive = () => {
|
|
26
|
-
const [variant] = useSelect('variant', {
|
|
27
|
-
options: ['silent', 'inline', 'card', 'fullscreen'] as const,
|
|
28
|
-
defaultValue: 'card',
|
|
29
|
-
label: 'Variant',
|
|
30
|
-
description: 'Fallback visual style',
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
return (
|
|
34
|
-
<div className="max-w-lg space-y-3">
|
|
35
|
-
<p className="text-sm text-muted-foreground">
|
|
36
|
-
Click the button to throw — the surrounding page stays alive, only this block swaps to the fallback.
|
|
37
|
-
</p>
|
|
38
|
-
<Boundary variant={variant as BoundaryVariant} name="story">
|
|
39
|
-
<div className="rounded-md border p-4">
|
|
40
|
-
<BoomButton />
|
|
41
|
-
</div>
|
|
42
|
-
</Boundary>
|
|
43
|
-
</div>
|
|
44
|
-
);
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
export const Silent = () => (
|
|
48
|
-
<div className="max-w-lg space-y-2">
|
|
49
|
-
<p className="text-sm text-muted-foreground">
|
|
50
|
-
variant="silent" renders nothing on error. Use for non-critical widgets (chat launcher, embeds).
|
|
51
|
-
</p>
|
|
52
|
-
<Boundary variant="silent" name="silent-demo">
|
|
53
|
-
<BoomButton label="Crash silently" />
|
|
54
|
-
</Boundary>
|
|
55
|
-
<p className="text-xs text-muted-foreground">↑ button disappears after click. Page is fine.</p>
|
|
56
|
-
</div>
|
|
57
|
-
);
|
|
58
|
-
|
|
59
|
-
export const Inline = () => (
|
|
60
|
-
<div className="max-w-lg">
|
|
61
|
-
<Boundary variant="inline" name="inline-demo">
|
|
62
|
-
<BoomButton />
|
|
63
|
-
</Boundary>
|
|
64
|
-
</div>
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
export const Card = () => (
|
|
68
|
-
<div className="max-w-lg">
|
|
69
|
-
<Boundary variant="card" name="card-demo">
|
|
70
|
-
<BoomButton />
|
|
71
|
-
</Boundary>
|
|
72
|
-
</div>
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
export const CustomFallback = () => (
|
|
76
|
-
<div className="max-w-lg">
|
|
77
|
-
<Boundary
|
|
78
|
-
fallback={({ error, reset }) => (
|
|
79
|
-
<div className="rounded-md border-2 border-dashed border-amber-500 bg-amber-500/5 p-4">
|
|
80
|
-
<p className="text-sm font-semibold text-amber-700">Custom fallback</p>
|
|
81
|
-
<p className="mt-1 text-xs text-amber-700/80">{error.message}</p>
|
|
82
|
-
<Button size="sm" variant="outline" className="mt-2" onClick={reset}>
|
|
83
|
-
Reset boundary
|
|
84
|
-
</Button>
|
|
85
|
-
</div>
|
|
86
|
-
)}
|
|
87
|
-
>
|
|
88
|
-
<BoomButton />
|
|
89
|
-
</Boundary>
|
|
90
|
-
</div>
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
export const ResetKeys = () => {
|
|
94
|
-
const [key, setKey] = useState(0);
|
|
95
|
-
return (
|
|
96
|
-
<div className="max-w-lg space-y-3">
|
|
97
|
-
<p className="text-sm text-muted-foreground">
|
|
98
|
-
Pass <code className="text-xs">resetKeys</code> — when any value in the array changes, the boundary auto-resets.
|
|
99
|
-
Good for clearing errors on route change.
|
|
100
|
-
</p>
|
|
101
|
-
<Button size="sm" onClick={() => setKey((k) => k + 1)}>
|
|
102
|
-
Bump resetKey ({key})
|
|
103
|
-
</Button>
|
|
104
|
-
<Boundary variant="card" resetKeys={[key]} name="resetkeys-demo">
|
|
105
|
-
<BoomButton />
|
|
106
|
-
</Boundary>
|
|
107
|
-
</div>
|
|
108
|
-
);
|
|
109
|
-
};
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import { defineStory, useSelect } from '@djangocfg/playground';
|
|
2
|
-
import { Avatar, AvatarImage, AvatarFallback } from '.';
|
|
3
|
-
|
|
4
|
-
export default defineStory({
|
|
5
|
-
title: 'Core/Avatar',
|
|
6
|
-
component: Avatar,
|
|
7
|
-
description: 'User avatar with image and fallback support.',
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
const AVATARS = [
|
|
11
|
-
{ src: 'https://github.com/shadcn.png', fallback: 'CN' },
|
|
12
|
-
{ src: 'https://github.com/vercel.png', fallback: 'VC' },
|
|
13
|
-
{ src: 'https://github.com/radix-ui.png', fallback: 'RX' },
|
|
14
|
-
];
|
|
15
|
-
|
|
16
|
-
export const Interactive = () => {
|
|
17
|
-
const [size] = useSelect('size', {
|
|
18
|
-
options: ['sm', 'md', 'lg'] as const,
|
|
19
|
-
defaultValue: 'md',
|
|
20
|
-
label: 'Size',
|
|
21
|
-
description: 'Avatar size',
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
const sizeClasses = {
|
|
25
|
-
sm: 'h-8 w-8',
|
|
26
|
-
md: 'h-10 w-10',
|
|
27
|
-
lg: 'h-14 w-14',
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
return (
|
|
31
|
-
<Avatar className={sizeClasses[size]}>
|
|
32
|
-
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
|
|
33
|
-
<AvatarFallback>CN</AvatarFallback>
|
|
34
|
-
</Avatar>
|
|
35
|
-
);
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
export const Default = () => (
|
|
39
|
-
<Avatar>
|
|
40
|
-
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
|
|
41
|
-
<AvatarFallback>CN</AvatarFallback>
|
|
42
|
-
</Avatar>
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
export const Fallback = () => (
|
|
46
|
-
<Avatar>
|
|
47
|
-
<AvatarImage src="/broken-image.jpg" alt="User" />
|
|
48
|
-
<AvatarFallback>JD</AvatarFallback>
|
|
49
|
-
</Avatar>
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
export const Sizes = () => (
|
|
53
|
-
<div className="flex items-center gap-4">
|
|
54
|
-
<Avatar className="h-6 w-6">
|
|
55
|
-
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
|
|
56
|
-
<AvatarFallback className="text-xs">CN</AvatarFallback>
|
|
57
|
-
</Avatar>
|
|
58
|
-
<Avatar className="h-8 w-8">
|
|
59
|
-
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
|
|
60
|
-
<AvatarFallback>CN</AvatarFallback>
|
|
61
|
-
</Avatar>
|
|
62
|
-
<Avatar className="h-10 w-10">
|
|
63
|
-
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
|
|
64
|
-
<AvatarFallback>CN</AvatarFallback>
|
|
65
|
-
</Avatar>
|
|
66
|
-
<Avatar className="h-14 w-14">
|
|
67
|
-
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
|
|
68
|
-
<AvatarFallback>CN</AvatarFallback>
|
|
69
|
-
</Avatar>
|
|
70
|
-
<Avatar className="h-20 w-20">
|
|
71
|
-
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
|
|
72
|
-
<AvatarFallback className="text-xl">CN</AvatarFallback>
|
|
73
|
-
</Avatar>
|
|
74
|
-
</div>
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
export const Group = () => (
|
|
78
|
-
<div className="flex -space-x-4">
|
|
79
|
-
{AVATARS.map((avatar, i) => (
|
|
80
|
-
<Avatar key={i} className="border-2 border-background">
|
|
81
|
-
<AvatarImage src={avatar.src} />
|
|
82
|
-
<AvatarFallback>{avatar.fallback}</AvatarFallback>
|
|
83
|
-
</Avatar>
|
|
84
|
-
))}
|
|
85
|
-
<Avatar className="border-2 border-background">
|
|
86
|
-
<AvatarFallback>+5</AvatarFallback>
|
|
87
|
-
</Avatar>
|
|
88
|
-
</div>
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
export const WithStatus = () => (
|
|
92
|
-
<div className="flex gap-4">
|
|
93
|
-
<div className="relative">
|
|
94
|
-
<Avatar>
|
|
95
|
-
<AvatarImage src="https://github.com/shadcn.png" />
|
|
96
|
-
<AvatarFallback>CN</AvatarFallback>
|
|
97
|
-
</Avatar>
|
|
98
|
-
<span className="absolute bottom-0 right-0 h-3 w-3 rounded-full bg-green-500 border-2 border-background" />
|
|
99
|
-
</div>
|
|
100
|
-
<div className="relative">
|
|
101
|
-
<Avatar>
|
|
102
|
-
<AvatarImage src="https://github.com/vercel.png" />
|
|
103
|
-
<AvatarFallback>VC</AvatarFallback>
|
|
104
|
-
</Avatar>
|
|
105
|
-
<span className="absolute bottom-0 right-0 h-3 w-3 rounded-full bg-yellow-500 border-2 border-background" />
|
|
106
|
-
</div>
|
|
107
|
-
<div className="relative">
|
|
108
|
-
<Avatar>
|
|
109
|
-
<AvatarImage src="https://github.com/radix-ui.png" />
|
|
110
|
-
<AvatarFallback>RX</AvatarFallback>
|
|
111
|
-
</Avatar>
|
|
112
|
-
<span className="absolute bottom-0 right-0 h-3 w-3 rounded-full bg-gray-400 border-2 border-background" />
|
|
113
|
-
</div>
|
|
114
|
-
</div>
|
|
115
|
-
);
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { defineStory, useSelect } from '@djangocfg/playground';
|
|
2
|
-
import { Badge } from '.';
|
|
3
|
-
|
|
4
|
-
export default defineStory({
|
|
5
|
-
title: 'Core/Badge',
|
|
6
|
-
component: Badge,
|
|
7
|
-
description: 'Small status indicators and labels.',
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
export const Interactive = () => {
|
|
11
|
-
const [variant] = useSelect('variant', {
|
|
12
|
-
options: ['default', 'secondary', 'destructive', 'outline'] as const,
|
|
13
|
-
defaultValue: 'default',
|
|
14
|
-
label: 'Variant',
|
|
15
|
-
description: 'Badge style variant',
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
return (
|
|
19
|
-
<div className="flex gap-4">
|
|
20
|
-
<Badge variant={variant}>Badge</Badge>
|
|
21
|
-
</div>
|
|
22
|
-
);
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
export const Variants = () => (
|
|
26
|
-
<div className="flex flex-wrap gap-4">
|
|
27
|
-
<Badge variant="default">Default</Badge>
|
|
28
|
-
<Badge variant="secondary">Secondary</Badge>
|
|
29
|
-
<Badge variant="destructive">Destructive</Badge>
|
|
30
|
-
<Badge variant="outline">Outline</Badge>
|
|
31
|
-
</div>
|
|
32
|
-
);
|
|
33
|
-
|
|
34
|
-
export const StatusBadges = () => (
|
|
35
|
-
<div className="flex flex-wrap gap-4">
|
|
36
|
-
<Badge variant="default">Active</Badge>
|
|
37
|
-
<Badge variant="secondary">Pending</Badge>
|
|
38
|
-
<Badge variant="destructive">Expired</Badge>
|
|
39
|
-
<Badge variant="outline">Draft</Badge>
|
|
40
|
-
</div>
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
export const InContext = () => (
|
|
44
|
-
<div className="space-y-4">
|
|
45
|
-
<div className="flex items-center gap-2">
|
|
46
|
-
<span className="font-medium">Status:</span>
|
|
47
|
-
<Badge variant="default">Published</Badge>
|
|
48
|
-
</div>
|
|
49
|
-
<div className="flex items-center gap-2">
|
|
50
|
-
<span className="font-medium">Tags:</span>
|
|
51
|
-
<Badge variant="secondary">React</Badge>
|
|
52
|
-
<Badge variant="secondary">TypeScript</Badge>
|
|
53
|
-
<Badge variant="secondary">UI</Badge>
|
|
54
|
-
</div>
|
|
55
|
-
</div>
|
|
56
|
-
);
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import { useState } from 'react';
|
|
2
|
-
import { defineStory } from '@djangocfg/playground';
|
|
3
|
-
import { Calendar } from '.';
|
|
4
|
-
import { DatePicker, DateRangePicker, type DateRange } from './date-picker';
|
|
5
|
-
|
|
6
|
-
export default defineStory({
|
|
7
|
-
title: 'Core/Calendar',
|
|
8
|
-
component: Calendar,
|
|
9
|
-
description: 'Calendar and date picker components.',
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
export const Default = () => {
|
|
13
|
-
const [date, setDate] = useState<Date | undefined>(new Date());
|
|
14
|
-
|
|
15
|
-
return (
|
|
16
|
-
<Calendar
|
|
17
|
-
mode="single"
|
|
18
|
-
selected={date}
|
|
19
|
-
onSelect={setDate}
|
|
20
|
-
className="rounded-md border"
|
|
21
|
-
/>
|
|
22
|
-
);
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
export const Range = () => {
|
|
26
|
-
const [range, setRange] = useState<DateRange | undefined>({
|
|
27
|
-
from: new Date(),
|
|
28
|
-
to: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
return (
|
|
32
|
-
<Calendar
|
|
33
|
-
mode="range"
|
|
34
|
-
selected={range}
|
|
35
|
-
onSelect={setRange}
|
|
36
|
-
className="rounded-md border"
|
|
37
|
-
numberOfMonths={2}
|
|
38
|
-
/>
|
|
39
|
-
);
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
export const Multiple = () => {
|
|
43
|
-
const [dates, setDates] = useState<Date[] | undefined>([]);
|
|
44
|
-
|
|
45
|
-
return (
|
|
46
|
-
<Calendar
|
|
47
|
-
mode="multiple"
|
|
48
|
-
selected={dates}
|
|
49
|
-
onSelect={setDates}
|
|
50
|
-
className="rounded-md border"
|
|
51
|
-
/>
|
|
52
|
-
);
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
export const Picker = () => {
|
|
56
|
-
const [date, setDate] = useState<Date>();
|
|
57
|
-
|
|
58
|
-
return (
|
|
59
|
-
<div className="max-w-xs">
|
|
60
|
-
<DatePicker
|
|
61
|
-
value={date}
|
|
62
|
-
onChange={setDate}
|
|
63
|
-
placeholder="Pick a date"
|
|
64
|
-
/>
|
|
65
|
-
</div>
|
|
66
|
-
);
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
export const PickerWithLabel = () => {
|
|
70
|
-
const [date, setDate] = useState<Date>();
|
|
71
|
-
|
|
72
|
-
return (
|
|
73
|
-
<div className="max-w-xs space-y-2">
|
|
74
|
-
<label className="text-sm font-medium">Date of birth</label>
|
|
75
|
-
<DatePicker
|
|
76
|
-
value={date}
|
|
77
|
-
onChange={setDate}
|
|
78
|
-
placeholder="Select your birth date"
|
|
79
|
-
/>
|
|
80
|
-
</div>
|
|
81
|
-
);
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
export const RangePicker = () => {
|
|
85
|
-
const [range, setRange] = useState<DateRange>();
|
|
86
|
-
|
|
87
|
-
return (
|
|
88
|
-
<div className="max-w-sm">
|
|
89
|
-
<DateRangePicker
|
|
90
|
-
value={range}
|
|
91
|
-
onChange={setRange}
|
|
92
|
-
placeholder="Select date range"
|
|
93
|
-
/>
|
|
94
|
-
</div>
|
|
95
|
-
);
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
export const RangePickerWithPresets = () => {
|
|
99
|
-
const [range, setRange] = useState<DateRange>();
|
|
100
|
-
|
|
101
|
-
return (
|
|
102
|
-
<div className="max-w-sm space-y-2">
|
|
103
|
-
<label className="text-sm font-medium">Booking dates</label>
|
|
104
|
-
<DateRangePicker
|
|
105
|
-
value={range}
|
|
106
|
-
onChange={setRange}
|
|
107
|
-
placeholder="Check-in — Check-out"
|
|
108
|
-
numberOfMonths={2}
|
|
109
|
-
/>
|
|
110
|
-
</div>
|
|
111
|
-
);
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
export const Disabled = () => {
|
|
115
|
-
const [date, setDate] = useState<Date>();
|
|
116
|
-
|
|
117
|
-
return (
|
|
118
|
-
<div className="max-w-xs">
|
|
119
|
-
<DatePicker
|
|
120
|
-
value={date}
|
|
121
|
-
onChange={setDate}
|
|
122
|
-
placeholder="Pick a date"
|
|
123
|
-
disabled
|
|
124
|
-
/>
|
|
125
|
-
</div>
|
|
126
|
-
);
|
|
127
|
-
};
|