zabi-components 6.0.0 → 7.0.0
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 +137 -1473
- package/dist/atoms/ActionPanel.svelte +116 -0
- package/dist/atoms/ActionPanel.svelte.d.ts +19 -0
- package/dist/atoms/Badge.svelte +1 -1
- package/dist/atoms/Badge.svelte.d.ts +1 -1
- package/dist/atoms/Button.svelte +35 -16
- package/dist/atoms/Button.svelte.d.ts +3 -1
- package/dist/atoms/Card.svelte +12 -3
- package/dist/atoms/Card.svelte.d.ts +2 -2
- package/dist/atoms/CardHeader.svelte +8 -4
- package/dist/atoms/CardHeader.svelte.d.ts +4 -2
- package/dist/atoms/Checkbox.svelte +50 -76
- package/dist/atoms/Checkbox.svelte.d.ts +7 -2
- package/dist/atoms/CodeBlock.svelte +20 -34
- package/dist/atoms/CodeBlock.svelte.d.ts +1 -0
- package/dist/atoms/ColorPicker.svelte +2 -6
- package/dist/atoms/Container.svelte +41 -0
- package/dist/atoms/Container.svelte.d.ts +12 -0
- package/dist/atoms/Divider.svelte +68 -0
- package/dist/atoms/Divider.svelte.d.ts +13 -0
- package/dist/atoms/FeatureCard.svelte +35 -50
- package/dist/atoms/FeatureCard.svelte.d.ts +9 -4
- package/dist/atoms/IconButton.svelte +33 -24
- package/dist/atoms/IconButton.svelte.d.ts +4 -8
- package/dist/atoms/Input.svelte +58 -26
- package/dist/atoms/Input.svelte.d.ts +6 -1
- package/dist/atoms/List.svelte +2 -7
- package/dist/atoms/List.svelte.d.ts +1 -6
- package/dist/atoms/ListItem.svelte +54 -65
- package/dist/atoms/ListItem.svelte.d.ts +7 -13
- package/dist/atoms/ListItemLeading.svelte +19 -0
- package/dist/atoms/ListItemLeading.svelte.d.ts +9 -0
- package/dist/atoms/Progress.svelte +1 -1
- package/dist/atoms/Radio.svelte +51 -0
- package/dist/atoms/Radio.svelte.d.ts +14 -0
- package/dist/atoms/Select.svelte +23 -30
- package/dist/atoms/Select.svelte.d.ts +0 -10
- package/dist/atoms/SelectionControl.svelte +98 -0
- package/dist/atoms/SelectionControl.svelte.d.ts +26 -0
- package/dist/atoms/Skeleton.svelte +39 -15
- package/dist/atoms/Skeleton.svelte.d.ts +7 -3
- package/dist/atoms/Table.svelte +27 -0
- package/dist/atoms/Table.svelte.d.ts +10 -0
- package/dist/atoms/Text.svelte +46 -0
- package/dist/atoms/Text.svelte.d.ts +13 -0
- package/dist/atoms/Textarea.svelte +74 -53
- package/dist/atoms/Textarea.svelte.d.ts +6 -2
- package/dist/atoms/ThemeToggle.svelte +35 -24
- package/dist/atoms/ThemeToggle.svelte.d.ts +1 -0
- package/dist/atoms/Toast.svelte +55 -31
- package/dist/atoms/Toast.svelte.d.ts +4 -1
- package/dist/atoms/Toggle.svelte +37 -22
- package/dist/atoms/Toggle.svelte.d.ts +2 -1
- package/dist/atoms/Tooltip.svelte +90 -11
- package/dist/atoms/index.d.ts +6 -0
- package/dist/atoms/index.js +6 -0
- package/dist/atoms/selection-control.styles.d.ts +25 -0
- package/dist/atoms/selection-control.styles.js +30 -0
- package/dist/components/atoms/index.d.ts +6 -0
- package/dist/components/atoms/index.d.ts.map +1 -1
- package/dist/components/atoms/selection-control.styles.d.ts +26 -0
- package/dist/components/atoms/selection-control.styles.d.ts.map +1 -0
- package/dist/components/index.d.ts +41 -5
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/molecules/index.d.ts +10 -0
- package/dist/components/molecules/index.d.ts.map +1 -1
- package/dist/components/molecules/navigation-menu-context.d.ts +11 -0
- package/dist/components/molecules/navigation-menu-context.d.ts.map +1 -1
- package/dist/components/molecules/toast-store.d.ts +39 -0
- package/dist/components/molecules/toast-store.d.ts.map +1 -0
- package/dist/components/organisms/index.d.ts +2 -1
- package/dist/components/organisms/index.d.ts.map +1 -1
- package/dist/components/types/page.types.d.ts +69 -0
- package/dist/components/types/page.types.d.ts.map +1 -0
- package/dist/components/types/page.types.js +1 -0
- package/dist/components/types/variants.d.ts +64 -0
- package/dist/components/types/variants.d.ts.map +1 -0
- package/dist/components/types/variants.js +44 -0
- package/dist/components/util/fixed-sidebar-flyout.d.ts +23 -0
- package/dist/components/util/fixed-sidebar-flyout.d.ts.map +1 -0
- package/dist/components/util/focus-utils.d.ts +12 -0
- package/dist/components/util/focus-utils.d.ts.map +1 -0
- package/dist/components/util/ssr-safe.d.ts +17 -0
- package/dist/components/util/ssr-safe.d.ts.map +1 -0
- package/dist/index.d.ts +41 -5
- package/dist/index.js +44 -8
- package/dist/lib/layout-width-tokens.d.ts +17 -0
- package/dist/lib/layout-width-tokens.d.ts.map +1 -0
- package/dist/lib/showcase/component-docs/Button.d.ts +3 -0
- package/dist/lib/showcase/component-docs/Button.d.ts.map +1 -0
- package/dist/lib/showcase/component-docs/Input.d.ts +3 -0
- package/dist/lib/showcase/component-docs/Input.d.ts.map +1 -0
- package/dist/lib/showcase/component-docs/List.d.ts +3 -0
- package/dist/lib/showcase/component-docs/List.d.ts.map +1 -0
- package/dist/lib/showcase/component-docs/Modal.d.ts +3 -0
- package/dist/lib/showcase/component-docs/Modal.d.ts.map +1 -0
- package/dist/lib/showcase/component-docs/Radio.d.ts +3 -0
- package/dist/lib/showcase/component-docs/Radio.d.ts.map +1 -0
- package/dist/lib/showcase/component-docs/SidebarNavigation.d.ts +3 -0
- package/dist/lib/showcase/component-docs/SidebarNavigation.d.ts.map +1 -0
- package/dist/lib/showcase/component-docs/Skeleton.d.ts +3 -0
- package/dist/lib/showcase/component-docs/Skeleton.d.ts.map +1 -0
- package/dist/lib/showcase/component-docs/_shared.d.ts +12 -0
- package/dist/lib/showcase/component-docs/_shared.d.ts.map +1 -0
- package/dist/lib/showcase/component-docs/index.d.ts +4 -0
- package/dist/lib/showcase/component-docs/index.d.ts.map +1 -0
- package/dist/lib/showcase/components-catalog.d.ts.map +1 -1
- package/dist/lib/showcase/components-showcase-constants.d.ts +26 -0
- package/dist/lib/showcase/components-showcase-constants.d.ts.map +1 -1
- package/dist/molecules/Alert.svelte +50 -23
- package/dist/molecules/Alert.svelte.d.ts +1 -1
- package/dist/molecules/ComponentDemo.svelte +2 -2
- package/dist/molecules/ContactForm.svelte +1 -1
- package/dist/molecules/Dropdown.svelte +99 -109
- package/dist/molecules/Dropdown.svelte.d.ts +12 -6
- package/dist/molecules/EmptyState.svelte +44 -0
- package/dist/molecules/EmptyState.svelte.d.ts +11 -0
- package/dist/molecules/FormField.svelte +89 -0
- package/dist/molecules/FormField.svelte.d.ts +23 -0
- package/dist/molecules/Header.svelte +41 -0
- package/dist/molecules/Header.svelte.d.ts +10 -0
- package/dist/molecules/Modal.svelte +37 -24
- package/dist/molecules/Modal.svelte.d.ts +4 -2
- package/dist/molecules/NavigationMenu.svelte +16 -0
- package/dist/molecules/NavigationMenu.svelte.d.ts +2 -0
- package/dist/molecules/NavigationMenuContent.svelte +9 -1
- package/dist/molecules/NavigationMenuLink.svelte +1 -1
- package/dist/molecules/NavigationMenuTrigger.svelte +9 -1
- package/dist/molecules/Page.svelte +16 -0
- package/dist/molecules/Page.svelte.d.ts +8 -0
- package/dist/molecules/PropsTable.svelte +61 -0
- package/dist/molecules/PropsTable.svelte.d.ts +8 -0
- package/dist/molecules/RadioGroup.svelte +197 -0
- package/dist/molecules/RadioGroup.svelte.d.ts +20 -0
- package/dist/molecules/Section.svelte +2 -11
- package/dist/molecules/Section.svelte.d.ts +2 -11
- package/dist/molecules/SidebarBrandHeader.svelte +68 -0
- package/dist/molecules/SidebarBrandHeader.svelte.d.ts +10 -0
- package/dist/molecules/SidebarFooter.svelte +119 -0
- package/dist/molecules/SidebarFooter.svelte.d.ts +23 -0
- package/dist/molecules/SidebarNavSection.svelte +49 -0
- package/dist/molecules/SidebarNavSection.svelte.d.ts +14 -0
- package/dist/molecules/SlideUp.svelte +19 -14
- package/dist/molecules/SlideUp.svelte.d.ts +1 -1
- package/dist/molecules/Tabs.svelte +4 -9
- package/dist/molecules/Toaster.svelte +21 -0
- package/dist/molecules/Toaster.svelte.d.ts +6 -0
- package/dist/molecules/ToasterToast.svelte +217 -0
- package/dist/molecules/ToasterToast.svelte.d.ts +7 -0
- package/dist/molecules/index.d.ts +10 -0
- package/dist/molecules/index.js +10 -0
- package/dist/molecules/navigation-menu-context.d.ts +11 -0
- package/dist/molecules/navigation-menu-context.js +18 -0
- package/dist/molecules/toast-store.d.ts +38 -0
- package/dist/molecules/toast-store.js +34 -0
- package/dist/organisms/SidebarAccountPanel.svelte +118 -0
- package/dist/organisms/SidebarAccountPanel.svelte.d.ts +20 -0
- package/dist/organisms/SidebarNavigation.svelte +267 -237
- package/dist/organisms/SidebarNavigation.svelte.d.ts +21 -5
- package/dist/organisms/SidebarPanel.svelte +54 -22
- package/dist/organisms/SidebarPanel.svelte.d.ts +3 -0
- package/dist/organisms/{Navbar.svelte → TopNavbar.svelte} +22 -17
- package/dist/organisms/TopNavbar.svelte.d.ts +35 -0
- package/dist/organisms/index.d.ts +2 -1
- package/dist/organisms/index.js +2 -1
- package/dist/routes/lib/focus-utils.d.ts +7 -18
- package/dist/routes/lib/focus-utils.ts +32 -34
- package/dist/routes/lib/ssr-safe.ts +5 -4
- package/dist/routes/lib/variant-utils.ts +48 -39
- package/dist/types/components.d.ts +187 -0
- package/dist/types/components.d.ts.map +1 -0
- package/dist/types/events.d.ts +200 -0
- package/dist/types/events.d.ts.map +1 -0
- package/dist/types/index.d.ts +171 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/page.types.d.ts +2 -31
- package/dist/types/page.types.d.ts.map +1 -1
- package/dist/types/page.types.js +1 -0
- package/dist/types/page.types.ts +79 -0
- package/dist/types/variants.d.ts +3 -0
- package/dist/types/variants.d.ts.map +1 -0
- package/dist/types/variants.js +1 -0
- package/dist/types/variants.ts +5 -6
- package/dist/util/fixed-sidebar-flyout.d.ts +22 -0
- package/dist/util/fixed-sidebar-flyout.js +148 -0
- package/dist/util/focus-utils.d.ts +11 -0
- package/dist/util/focus-utils.js +94 -0
- package/dist/util/ssr-safe.d.ts +16 -0
- package/dist/util/ssr-safe.js +48 -0
- package/dist/zabi-components-colors.css +14 -1
- package/dist/zabi-components-theme-dark-only.css +5 -0
- package/dist/zabi-components-theme-dark.css +5 -0
- package/dist/zabi-components-theme-only.css +9 -1
- package/dist/zabi-components-theme.css +9 -1
- package/dist/zabi-components.css +877 -192
- package/package.json +19 -7
- package/dist/organisms/Navbar.svelte.d.ts +0 -41
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
export type FormFieldControlProps = {
|
|
3
|
+
id: string;
|
|
4
|
+
'aria-describedby'?: string;
|
|
5
|
+
'aria-invalid'?: 'true';
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
required?: boolean;
|
|
8
|
+
};
|
|
9
|
+
interface Props {
|
|
10
|
+
id?: string;
|
|
11
|
+
label: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
error?: string;
|
|
14
|
+
required?: boolean;
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
class?: string;
|
|
17
|
+
labelClass?: string;
|
|
18
|
+
control: Snippet<[FormFieldControlProps]>;
|
|
19
|
+
meta?: Snippet;
|
|
20
|
+
}
|
|
21
|
+
declare const FormField: import("svelte").Component<Props, {}, "">;
|
|
22
|
+
type FormField = ReturnType<typeof FormField>;
|
|
23
|
+
export default FormField;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Badge from "../atoms/Badge.svelte";
|
|
3
|
+
import Heading from "../atoms/Heading.svelte";
|
|
4
|
+
import type { ComponentCategory } from "../types/page.types";
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
title: string;
|
|
8
|
+
description: string;
|
|
9
|
+
category: ComponentCategory;
|
|
10
|
+
variantsStates?: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let { title, description, category, variantsStates }: Props = $props();
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<header class="space-y-4">
|
|
17
|
+
<div class="space-y-2">
|
|
18
|
+
<Heading level={1} text={title} />
|
|
19
|
+
<p class="text-lg whitespace-pre-line text-description">{description}</p>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
23
|
+
<Badge variant="info" text={category} />
|
|
24
|
+
{#if variantsStates?.length}
|
|
25
|
+
<Badge variant="info" text="{variantsStates.length} variants" />
|
|
26
|
+
{/if}
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
{#if variantsStates?.length}
|
|
30
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
31
|
+
<span class="text-sm font-medium text-headline">
|
|
32
|
+
Variants / states:
|
|
33
|
+
</span>
|
|
34
|
+
{#each variantsStates as v (v)}
|
|
35
|
+
<span class="rounded-md bg-base-100 px-2 py-1 text-xs text-headline">
|
|
36
|
+
{v}
|
|
37
|
+
</span>
|
|
38
|
+
{/each}
|
|
39
|
+
</div>
|
|
40
|
+
{/if}
|
|
41
|
+
</header>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ComponentCategory } from "../types/page.types";
|
|
2
|
+
interface Props {
|
|
3
|
+
title: string;
|
|
4
|
+
description: string;
|
|
5
|
+
category: ComponentCategory;
|
|
6
|
+
variantsStates?: string[];
|
|
7
|
+
}
|
|
8
|
+
declare const Header: import("svelte").Component<Props, {}, "">;
|
|
9
|
+
type Header = ReturnType<typeof Header>;
|
|
10
|
+
export default Header;
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
import {
|
|
3
|
+
trapFocus,
|
|
4
|
+
saveFocus,
|
|
5
|
+
returnFocus,
|
|
6
|
+
focusFirstElement,
|
|
7
|
+
} from '../util/focus-utils.js';
|
|
8
|
+
import { generateId } from "../util/ssr-safe.js";
|
|
9
|
+
import Card from '../atoms/Card.svelte';
|
|
10
|
+
import CardHeader from '../atoms/CardHeader.svelte';
|
|
11
|
+
import CardContent from '../atoms/CardContent.svelte';
|
|
12
|
+
import CardFooter from '../atoms/CardFooter.svelte';
|
|
7
13
|
|
|
8
|
-
type Size =
|
|
14
|
+
type Size = 'sm' | 'md' | 'lg';
|
|
9
15
|
|
|
10
16
|
interface Props {
|
|
11
17
|
isOpen?: boolean;
|
|
@@ -14,29 +20,35 @@
|
|
|
14
20
|
size?: Size;
|
|
15
21
|
onclick?: (event: Event) => void;
|
|
16
22
|
onkeydown?: (event: Event) => void;
|
|
23
|
+
/** On the `role="dialog"` root (testing, analytics). */
|
|
24
|
+
"data-testid"?: string;
|
|
17
25
|
}
|
|
18
26
|
|
|
19
27
|
let {
|
|
20
|
-
isOpen = false,
|
|
21
|
-
title =
|
|
22
|
-
description =
|
|
23
|
-
size =
|
|
28
|
+
isOpen = $bindable(false),
|
|
29
|
+
title = '',
|
|
30
|
+
description = '',
|
|
31
|
+
size = 'md',
|
|
24
32
|
onclick,
|
|
25
33
|
onkeydown,
|
|
34
|
+
"data-testid": dataTestId = undefined,
|
|
26
35
|
children,
|
|
27
36
|
footer,
|
|
28
37
|
...restProps
|
|
29
38
|
}: Props & { children?: any; footer?: any } = $props();
|
|
30
39
|
|
|
40
|
+
const modalTitleId = generateId('modal-title');
|
|
41
|
+
const modalDescriptionId = generateId('modal-description');
|
|
42
|
+
|
|
31
43
|
let modalContainer = $state<HTMLDivElement>();
|
|
32
44
|
let cleanupFocusTrap: (() => void) | null = null;
|
|
33
45
|
|
|
34
46
|
const sizeClasses = $derived(
|
|
35
47
|
{
|
|
36
|
-
sm:
|
|
37
|
-
md:
|
|
38
|
-
lg:
|
|
39
|
-
}[size] ||
|
|
48
|
+
sm: 'w-full md:w-[24rem]',
|
|
49
|
+
md: 'w-full md:w-[28rem]',
|
|
50
|
+
lg: 'w-full md:w-[42rem]',
|
|
51
|
+
}[size] || 'w-full md:w-[28rem]',
|
|
40
52
|
);
|
|
41
53
|
|
|
42
54
|
function closeModal(event?: Event) {
|
|
@@ -51,19 +63,18 @@
|
|
|
51
63
|
}
|
|
52
64
|
}
|
|
53
65
|
|
|
54
|
-
// Handle focus trap when modal opens
|
|
55
66
|
$effect(() => {
|
|
56
67
|
const container = modalContainer;
|
|
57
68
|
if (isOpen && container) {
|
|
58
69
|
saveFocus();
|
|
59
70
|
cleanupFocusTrap = trapFocus(container);
|
|
60
|
-
// Small delay to ensure modal is rendered
|
|
61
71
|
setTimeout(() => {
|
|
62
72
|
focusFirstElement(container);
|
|
63
73
|
}, 0);
|
|
64
74
|
} else if (!isOpen && cleanupFocusTrap) {
|
|
65
75
|
cleanupFocusTrap();
|
|
66
76
|
cleanupFocusTrap = null;
|
|
77
|
+
returnFocus();
|
|
67
78
|
}
|
|
68
79
|
});
|
|
69
80
|
|
|
@@ -75,12 +86,10 @@
|
|
|
75
86
|
|
|
76
87
|
function handleKeydown(event: Event) {
|
|
77
88
|
const keyboardEvent = event as KeyboardEvent;
|
|
78
|
-
if (keyboardEvent.key ===
|
|
89
|
+
if (keyboardEvent.key === 'Escape') {
|
|
79
90
|
closeModal(event);
|
|
80
91
|
}
|
|
81
|
-
|
|
82
|
-
onkeydown(event);
|
|
83
|
-
}
|
|
92
|
+
onkeydown?.(event);
|
|
84
93
|
}
|
|
85
94
|
</script>
|
|
86
95
|
|
|
@@ -91,8 +100,11 @@
|
|
|
91
100
|
onkeydown={handleKeydown}
|
|
92
101
|
role="dialog"
|
|
93
102
|
aria-modal="true"
|
|
94
|
-
aria-labelledby={title ?
|
|
103
|
+
aria-labelledby={title ? modalTitleId : undefined}
|
|
104
|
+
aria-describedby={description ? modalDescriptionId : undefined}
|
|
95
105
|
tabindex="-1"
|
|
106
|
+
data-testid={dataTestId}
|
|
107
|
+
{...restProps}
|
|
96
108
|
>
|
|
97
109
|
<div
|
|
98
110
|
bind:this={modalContainer}
|
|
@@ -102,20 +114,21 @@
|
|
|
102
114
|
{#if title || description}
|
|
103
115
|
<CardHeader
|
|
104
116
|
{description}
|
|
117
|
+
descriptionId={description ? modalDescriptionId : undefined}
|
|
105
118
|
className="px-6 pt-6 pb-4"
|
|
106
119
|
>
|
|
107
120
|
{#if title}
|
|
108
121
|
<div class="flex items-center justify-between">
|
|
109
122
|
<h2
|
|
110
|
-
id=
|
|
111
|
-
class="text-2xl font-normal leading-8 text-headline
|
|
123
|
+
id={modalTitleId}
|
|
124
|
+
class="text-2xl font-normal leading-8 tracking-normal text-headline"
|
|
112
125
|
>
|
|
113
126
|
{title}
|
|
114
127
|
</h2>
|
|
115
128
|
<button
|
|
116
129
|
type="button"
|
|
117
130
|
onclick={closeModal}
|
|
118
|
-
class="
|
|
131
|
+
class="flex h-8 w-8 cursor-pointer items-center justify-center rounded-full text-2xl text-description transition-colors hover:bg-base-100 hover:text-headline"
|
|
119
132
|
aria-label="Close"
|
|
120
133
|
>
|
|
121
134
|
×
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
type Size =
|
|
1
|
+
type Size = 'sm' | 'md' | 'lg';
|
|
2
2
|
interface Props {
|
|
3
3
|
isOpen?: boolean;
|
|
4
4
|
title?: string;
|
|
@@ -6,11 +6,13 @@ interface Props {
|
|
|
6
6
|
size?: Size;
|
|
7
7
|
onclick?: (event: Event) => void;
|
|
8
8
|
onkeydown?: (event: Event) => void;
|
|
9
|
+
/** On the `role="dialog"` root (testing, analytics). */
|
|
10
|
+
"data-testid"?: string;
|
|
9
11
|
}
|
|
10
12
|
type $$ComponentProps = Props & {
|
|
11
13
|
children?: any;
|
|
12
14
|
footer?: any;
|
|
13
15
|
};
|
|
14
|
-
declare const Modal: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
16
|
+
declare const Modal: import("svelte").Component<$$ComponentProps, {}, "isOpen">;
|
|
15
17
|
type Modal = ReturnType<typeof Modal>;
|
|
16
18
|
export default Modal;
|
|
@@ -8,8 +8,10 @@
|
|
|
8
8
|
import NavigationMenuContent from "./NavigationMenuContent.svelte";
|
|
9
9
|
import NavigationMenuLink from "./NavigationMenuLink.svelte";
|
|
10
10
|
|
|
11
|
+
import { generateId } from "../util/ssr-safe.js";
|
|
11
12
|
import {
|
|
12
13
|
NAVIGATION_MENU_CONTEXT_KEY,
|
|
14
|
+
navigationMenuStableInstanceId,
|
|
13
15
|
type NavigationMenuContextValue,
|
|
14
16
|
} from "./navigation-menu-context.js";
|
|
15
17
|
|
|
@@ -35,6 +37,8 @@
|
|
|
35
37
|
items?: NavigationMenuItemData[];
|
|
36
38
|
children?: Snippet;
|
|
37
39
|
listClassName?: string;
|
|
40
|
+
/** Panel id / `aria-controls` prefix; pass for SSR or multiple menus. If omitted with non-empty `items`, a deterministic hash is used. */
|
|
41
|
+
menuId?: string;
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
let {
|
|
@@ -43,6 +47,7 @@
|
|
|
43
47
|
items = [],
|
|
44
48
|
children,
|
|
45
49
|
listClassName = "",
|
|
50
|
+
menuId = "",
|
|
46
51
|
...restProps
|
|
47
52
|
}: Props = $props();
|
|
48
53
|
|
|
@@ -50,6 +55,14 @@
|
|
|
50
55
|
let isMobile = $state(false);
|
|
51
56
|
let containerElement = $state<HTMLElement | null>(null);
|
|
52
57
|
|
|
58
|
+
const menuInstanceId = $derived(
|
|
59
|
+
menuId.trim() !== ""
|
|
60
|
+
? menuId.trim()
|
|
61
|
+
: items.length > 0
|
|
62
|
+
? navigationMenuStableInstanceId(items)
|
|
63
|
+
: generateId("nav-menu"),
|
|
64
|
+
);
|
|
65
|
+
|
|
53
66
|
function setActiveItem(item: string | null) {
|
|
54
67
|
activeItem = item;
|
|
55
68
|
}
|
|
@@ -81,6 +94,9 @@
|
|
|
81
94
|
get isMobile() {
|
|
82
95
|
return isMobile;
|
|
83
96
|
},
|
|
97
|
+
get menuInstanceId() {
|
|
98
|
+
return menuInstanceId;
|
|
99
|
+
},
|
|
84
100
|
};
|
|
85
101
|
|
|
86
102
|
setContext(NAVIGATION_MENU_CONTEXT_KEY, context);
|
|
@@ -19,6 +19,8 @@ interface Props {
|
|
|
19
19
|
items?: NavigationMenuItemData[];
|
|
20
20
|
children?: Snippet;
|
|
21
21
|
listClassName?: string;
|
|
22
|
+
/** Panel id / `aria-controls` prefix; pass for SSR or multiple menus. If omitted with non-empty `items`, a deterministic hash is used. */
|
|
23
|
+
menuId?: string;
|
|
22
24
|
}
|
|
23
25
|
declare const NavigationMenu: import("svelte").Component<Props, {}, "">;
|
|
24
26
|
type NavigationMenu = ReturnType<typeof NavigationMenu>;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { onMount } from "svelte";
|
|
5
5
|
import {
|
|
6
6
|
NAVIGATION_MENU_CONTEXT_KEY,
|
|
7
|
+
navigationMenuPanelId,
|
|
7
8
|
type NavigationMenuContextValue,
|
|
8
9
|
} from "./navigation-menu-context.js";
|
|
9
10
|
|
|
@@ -29,6 +30,12 @@
|
|
|
29
30
|
|
|
30
31
|
const isActive = $derived(context?.activeItem === value);
|
|
31
32
|
|
|
33
|
+
const panelId = $derived(
|
|
34
|
+
context
|
|
35
|
+
? navigationMenuPanelId(context.menuInstanceId, value)
|
|
36
|
+
: "",
|
|
37
|
+
);
|
|
38
|
+
|
|
32
39
|
function handleKeydown(event: KeyboardEvent) {
|
|
33
40
|
if (event.key === "Escape") {
|
|
34
41
|
event.preventDefault();
|
|
@@ -71,7 +78,8 @@
|
|
|
71
78
|
{#if isActive}
|
|
72
79
|
<div
|
|
73
80
|
bind:this={contentElement}
|
|
74
|
-
|
|
81
|
+
id={panelId || undefined}
|
|
82
|
+
class="absolute left-0 top-full mt-2 bg-card rounded-lg shadow-lg border border-border p-4 z-dropdown min-w-[200px] transition-all duration-200 ease-in-out {className}"
|
|
75
83
|
role="menu"
|
|
76
84
|
onkeydown={handleKeydown}
|
|
77
85
|
data-navigation-menu-content
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
const linkClasses =
|
|
33
|
-
"block cursor-pointer rounded-md px-4 py-2 text-sm text-nav-menu-item no-underline transition-colors duration-150 outline-none hover:bg-nav-menu-hover hover:text-nav-menu-item-hover
|
|
33
|
+
"focus-ring focus-ring--nav block cursor-pointer rounded-md px-4 py-2 text-sm text-nav-menu-item no-underline transition-colors duration-150 outline-none hover:bg-nav-menu-hover hover:text-nav-menu-item-hover {className}";
|
|
34
34
|
</script>
|
|
35
35
|
|
|
36
36
|
{#if asChild}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { getContext } from "svelte";
|
|
4
4
|
import {
|
|
5
5
|
NAVIGATION_MENU_CONTEXT_KEY,
|
|
6
|
+
navigationMenuPanelId,
|
|
6
7
|
type NavigationMenuContextValue,
|
|
7
8
|
} from "./navigation-menu-context.js";
|
|
8
9
|
|
|
@@ -25,6 +26,12 @@
|
|
|
25
26
|
|
|
26
27
|
const isActive = $derived(context?.activeItem === value);
|
|
27
28
|
|
|
29
|
+
const panelId = $derived(
|
|
30
|
+
context
|
|
31
|
+
? navigationMenuPanelId(context.menuInstanceId, value)
|
|
32
|
+
: "",
|
|
33
|
+
);
|
|
34
|
+
|
|
28
35
|
function handleClick() {
|
|
29
36
|
if (context) {
|
|
30
37
|
if (isActive) {
|
|
@@ -51,8 +58,9 @@
|
|
|
51
58
|
|
|
52
59
|
<button
|
|
53
60
|
type="button"
|
|
54
|
-
class="inline-flex cursor-pointer items-center justify-center gap-2 rounded-lg bg-transparent px-4 py-2 text-sm font-medium text-nav-menu-item transition-colors duration-150 outline-none hover:bg-nav-menu-hover hover:text-nav-menu-item-hover data-[active=true]:bg-nav-menu-active data-[active=true]:text-nav-menu-item-active
|
|
61
|
+
class="focus-ring focus-ring--nav inline-flex cursor-pointer items-center justify-center gap-2 rounded-lg bg-transparent px-4 py-2 text-sm font-medium text-nav-menu-item transition-colors duration-150 outline-none hover:bg-nav-menu-hover hover:text-nav-menu-item-hover data-[active=true]:bg-nav-menu-active data-[active=true]:text-nav-menu-item-active {className}"
|
|
55
62
|
aria-expanded={isActive ? 'true' : 'false'}
|
|
63
|
+
aria-controls={panelId || undefined}
|
|
56
64
|
aria-haspopup="true"
|
|
57
65
|
onclick={handleClick}
|
|
58
66
|
onkeydown={handleKeydown}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
children?: Snippet;
|
|
6
|
+
className?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
let { children, className = "" }: Props = $props();
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<div class="mx-auto w-full space-y-10 {className}">
|
|
13
|
+
{#if children}
|
|
14
|
+
{@render children()}
|
|
15
|
+
{/if}
|
|
16
|
+
</div>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ComponentProp } from "../types/page.types";
|
|
3
|
+
import Table from "../atoms/Table.svelte";
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
props: ComponentProp[];
|
|
7
|
+
caption?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let { props, caption = "Props / API" }: Props = $props();
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
{#if props.length === 0}
|
|
14
|
+
<div class="rounded-lg border border-border bg-base-50 px-4 py-4 text-sm text-description">
|
|
15
|
+
No documented props.
|
|
16
|
+
</div>
|
|
17
|
+
{:else}
|
|
18
|
+
<Table caption={caption}>
|
|
19
|
+
<thead>
|
|
20
|
+
<tr class="bg-base-50">
|
|
21
|
+
<th class="border-b border-border px-4 py-3 text-left font-semibold text-headline">
|
|
22
|
+
Name
|
|
23
|
+
</th>
|
|
24
|
+
<th class="border-b border-border px-4 py-3 text-left font-semibold text-headline">
|
|
25
|
+
Type
|
|
26
|
+
</th>
|
|
27
|
+
<th class="border-b border-border px-4 py-3 text-left font-semibold text-headline">
|
|
28
|
+
Required
|
|
29
|
+
</th>
|
|
30
|
+
<th class="border-b border-border px-4 py-3 text-left font-semibold text-headline">
|
|
31
|
+
Default
|
|
32
|
+
</th>
|
|
33
|
+
<th class="border-b border-border px-4 py-3 text-left font-semibold text-headline">
|
|
34
|
+
Description
|
|
35
|
+
</th>
|
|
36
|
+
</tr>
|
|
37
|
+
</thead>
|
|
38
|
+
<tbody>
|
|
39
|
+
{#each props as prop}
|
|
40
|
+
<tr class="hover:bg-base-50/70">
|
|
41
|
+
<td class="border-b border-border px-4 py-3 font-mono text-body">
|
|
42
|
+
{prop.name}
|
|
43
|
+
</td>
|
|
44
|
+
<td class="border-b border-border px-4 py-3 text-description">
|
|
45
|
+
{prop.type}
|
|
46
|
+
</td>
|
|
47
|
+
<td class="border-b border-border px-4 py-3 text-description">
|
|
48
|
+
{prop.required ? "Yes" : "No"}
|
|
49
|
+
</td>
|
|
50
|
+
<td class="border-b border-border px-4 py-3 text-description">
|
|
51
|
+
{prop.defaultValue || "—"}
|
|
52
|
+
</td>
|
|
53
|
+
<td class="border-b border-border px-4 py-3 text-description">
|
|
54
|
+
{prop.description}
|
|
55
|
+
</td>
|
|
56
|
+
</tr>
|
|
57
|
+
{/each}
|
|
58
|
+
</tbody>
|
|
59
|
+
</Table>
|
|
60
|
+
{/if}
|
|
61
|
+
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ComponentProp } from "../types/page.types";
|
|
2
|
+
interface Props {
|
|
3
|
+
props: ComponentProp[];
|
|
4
|
+
caption?: string;
|
|
5
|
+
}
|
|
6
|
+
declare const PropsTable: import("svelte").Component<Props, {}, "">;
|
|
7
|
+
type PropsTable = ReturnType<typeof PropsTable>;
|
|
8
|
+
export default PropsTable;
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
RADIO_CHECKED_DOT_CLASSES,
|
|
4
|
+
RADIO_GROUP_CONTROL_SHELL,
|
|
5
|
+
RADIO_GROUP_OPTION_LABEL_ROW,
|
|
6
|
+
RADIO_GROUP_RING_OVERLAY,
|
|
7
|
+
} from "../atoms/selection-control.styles";
|
|
8
|
+
import { generateId } from "../util/ssr-safe.js";
|
|
9
|
+
import { SvelteMap } from "svelte/reactivity";
|
|
10
|
+
|
|
11
|
+
export type RadioGroupOption = {
|
|
12
|
+
value: string;
|
|
13
|
+
label: string;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
description?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
interface Props {
|
|
19
|
+
options: RadioGroupOption[];
|
|
20
|
+
/** Selected value; `bind:value`. Use `undefined` for no selection. */
|
|
21
|
+
value?: string | undefined;
|
|
22
|
+
/** Uncontrolled initial value. */
|
|
23
|
+
defaultValue?: string | undefined;
|
|
24
|
+
name?: string;
|
|
25
|
+
disabled?: boolean;
|
|
26
|
+
legend?: string;
|
|
27
|
+
label?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let {
|
|
31
|
+
options,
|
|
32
|
+
defaultValue,
|
|
33
|
+
value = $bindable(defaultValue),
|
|
34
|
+
name: nameProp,
|
|
35
|
+
disabled = false,
|
|
36
|
+
legend = "",
|
|
37
|
+
label = "",
|
|
38
|
+
...restProps
|
|
39
|
+
}: Props = $props();
|
|
40
|
+
|
|
41
|
+
const fallbackName = generateId("radiogroup");
|
|
42
|
+
const groupName = $derived(nameProp || fallbackName);
|
|
43
|
+
const legendText = $derived(legend || label);
|
|
44
|
+
|
|
45
|
+
const enabledOptions = $derived.by(() =>
|
|
46
|
+
options.filter((o: RadioGroupOption) => !disabled && !o.disabled),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const fallbackValue = $derived.by(() => enabledOptions[0]?.value ?? "");
|
|
50
|
+
|
|
51
|
+
/** False if controlled `value` is set but missing from `options` (stale); roving tabindex then follows first enabled instead. */
|
|
52
|
+
const valueMatchesOption = $derived(
|
|
53
|
+
value === undefined ||
|
|
54
|
+
value === "" ||
|
|
55
|
+
options.some((o: RadioGroupOption) => o.value === value),
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
function isOptionDisabled(option: RadioGroupOption): boolean {
|
|
59
|
+
return disabled || !!option.disabled;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function tabIndexFor(option: RadioGroupOption): number {
|
|
63
|
+
if (isOptionDisabled(option)) return -1;
|
|
64
|
+
if (
|
|
65
|
+
value !== undefined &&
|
|
66
|
+
value !== "" &&
|
|
67
|
+
valueMatchesOption
|
|
68
|
+
) {
|
|
69
|
+
return option.value === value ? 0 : -1;
|
|
70
|
+
}
|
|
71
|
+
return option.value === fallbackValue ? 0 : -1;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function handleChange(event: Event) {
|
|
75
|
+
if (disabled) return;
|
|
76
|
+
const target = event.target as HTMLInputElement;
|
|
77
|
+
value = target.value;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const elementsByValue = new SvelteMap<string, HTMLInputElement>();
|
|
81
|
+
function registerRadio(valueKey: string) {
|
|
82
|
+
return (node: HTMLInputElement) => {
|
|
83
|
+
elementsByValue.set(valueKey, node);
|
|
84
|
+
return () => {
|
|
85
|
+
elementsByValue.delete(valueKey);
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function focusValue(nextValue: string) {
|
|
91
|
+
const el = elementsByValue.get(nextValue);
|
|
92
|
+
el?.focus();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function moveSelection(delta: number) {
|
|
96
|
+
const enabled = enabledOptions;
|
|
97
|
+
if (enabled.length === 0) return;
|
|
98
|
+
|
|
99
|
+
const rawIndex = enabled.findIndex((o) => o.value === value);
|
|
100
|
+
const currentIndex =
|
|
101
|
+
rawIndex === -1
|
|
102
|
+
? delta > 0
|
|
103
|
+
? -1
|
|
104
|
+
: enabled.length
|
|
105
|
+
: rawIndex;
|
|
106
|
+
const nextIndex =
|
|
107
|
+
(currentIndex + delta + enabled.length) % enabled.length;
|
|
108
|
+
const next = enabled[nextIndex];
|
|
109
|
+
if (!next) return;
|
|
110
|
+
value = next.value;
|
|
111
|
+
focusValue(next.value);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function handleKeydown(event: KeyboardEvent) {
|
|
115
|
+
const key = event.key;
|
|
116
|
+
if (
|
|
117
|
+
key !== "ArrowRight" &&
|
|
118
|
+
key !== "ArrowDown" &&
|
|
119
|
+
key !== "ArrowLeft" &&
|
|
120
|
+
key !== "ArrowUp" &&
|
|
121
|
+
key !== "Home" &&
|
|
122
|
+
key !== "End"
|
|
123
|
+
) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const enabled = enabledOptions;
|
|
128
|
+
if (enabled.length === 0) return;
|
|
129
|
+
|
|
130
|
+
event.preventDefault();
|
|
131
|
+
if (key === "ArrowRight" || key === "ArrowDown") {
|
|
132
|
+
moveSelection(1);
|
|
133
|
+
} else if (key === "ArrowLeft" || key === "ArrowUp") {
|
|
134
|
+
moveSelection(-1);
|
|
135
|
+
} else if (key === "Home") {
|
|
136
|
+
value = enabled[0].value;
|
|
137
|
+
focusValue(enabled[0].value);
|
|
138
|
+
} else if (key === "End") {
|
|
139
|
+
const last = enabled[enabled.length - 1];
|
|
140
|
+
value = last.value;
|
|
141
|
+
focusValue(last.value);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const optionLabelWrapperClasses = RADIO_GROUP_OPTION_LABEL_ROW;
|
|
146
|
+
|
|
147
|
+
const radioControlShellClasses = RADIO_GROUP_CONTROL_SHELL;
|
|
148
|
+
|
|
149
|
+
const radioRingOverlayClasses = RADIO_GROUP_RING_OVERLAY;
|
|
150
|
+
</script>
|
|
151
|
+
|
|
152
|
+
<fieldset
|
|
153
|
+
class="space-y-3"
|
|
154
|
+
aria-disabled={disabled ? "true" : undefined}
|
|
155
|
+
onkeydown={handleKeydown}
|
|
156
|
+
{...restProps}
|
|
157
|
+
>
|
|
158
|
+
{#if legendText}
|
|
159
|
+
<legend class="text-sm font-medium text-label mb-1">
|
|
160
|
+
{legendText}
|
|
161
|
+
</legend>
|
|
162
|
+
{/if}
|
|
163
|
+
|
|
164
|
+
<div class="space-y-3">
|
|
165
|
+
{#each options as option (option.value)}
|
|
166
|
+
{@const optionDisabled = isOptionDisabled(option)}
|
|
167
|
+
{@const isChecked = value === option.value}
|
|
168
|
+
<label class={optionLabelWrapperClasses}>
|
|
169
|
+
<span class={radioControlShellClasses}>
|
|
170
|
+
<input
|
|
171
|
+
type="radio"
|
|
172
|
+
name={groupName}
|
|
173
|
+
value={option.value}
|
|
174
|
+
checked={isChecked}
|
|
175
|
+
disabled={optionDisabled}
|
|
176
|
+
tabindex={tabIndexFor(option)}
|
|
177
|
+
class="sr-only"
|
|
178
|
+
onchange={handleChange}
|
|
179
|
+
{@attach registerRadio(option.value)}
|
|
180
|
+
/>
|
|
181
|
+
<span class={radioRingOverlayClasses}></span>
|
|
182
|
+
<span class={RADIO_CHECKED_DOT_CLASSES} aria-hidden="true"></span>
|
|
183
|
+
</span>
|
|
184
|
+
<span class="flex flex-col gap-1">
|
|
185
|
+
<span class="text-sm font-medium text-label">
|
|
186
|
+
{option.label}
|
|
187
|
+
</span>
|
|
188
|
+
{#if option.description}
|
|
189
|
+
<span class="text-sm text-description">
|
|
190
|
+
{option.description}
|
|
191
|
+
</span>
|
|
192
|
+
{/if}
|
|
193
|
+
</span>
|
|
194
|
+
</label>
|
|
195
|
+
{/each}
|
|
196
|
+
</div>
|
|
197
|
+
</fieldset>
|