@fudge-me/design-system 0.1.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.
Files changed (57) hide show
  1. package/README.md +140 -0
  2. package/dist/components/.gitkeep +0 -0
  3. package/dist/components/Button.svelte +118 -0
  4. package/dist/components/Button.svelte.d.ts +12 -0
  5. package/dist/components/CommandPalette.svelte +167 -0
  6. package/dist/components/CommandPalette.svelte.d.ts +16 -0
  7. package/dist/components/Input.svelte +70 -0
  8. package/dist/components/Input.svelte.d.ts +9 -0
  9. package/dist/components/Modal.svelte +83 -0
  10. package/dist/components/Modal.svelte.d.ts +12 -0
  11. package/dist/components/Tooltip.svelte +92 -0
  12. package/dist/components/Tooltip.svelte.d.ts +12 -0
  13. package/dist/components/index.d.ts +8 -0
  14. package/dist/components/index.js +5 -0
  15. package/dist/index.d.ts +3 -0
  16. package/dist/index.js +3 -0
  17. package/dist/layout/.gitkeep +0 -0
  18. package/dist/layout/AIPanel.svelte +34 -0
  19. package/dist/layout/AIPanel.svelte.d.ts +11 -0
  20. package/dist/layout/AppShell.svelte +78 -0
  21. package/dist/layout/AppShell.svelte.d.ts +11 -0
  22. package/dist/layout/Canvas.svelte +22 -0
  23. package/dist/layout/Canvas.svelte.d.ts +7 -0
  24. package/dist/layout/CommandBar.svelte +25 -0
  25. package/dist/layout/CommandBar.svelte.d.ts +7 -0
  26. package/dist/layout/Panel.svelte +157 -0
  27. package/dist/layout/Panel.svelte.d.ts +13 -0
  28. package/dist/layout/Sidebar.svelte +34 -0
  29. package/dist/layout/Sidebar.svelte.d.ts +11 -0
  30. package/dist/layout/StatusBar.svelte +26 -0
  31. package/dist/layout/StatusBar.svelte.d.ts +7 -0
  32. package/dist/layout/__tests__/helpers.js +53 -0
  33. package/dist/layout/index.d.ts +8 -0
  34. package/dist/layout/index.js +7 -0
  35. package/dist/themes/ThemeProvider.svelte +44 -0
  36. package/dist/themes/ThemeProvider.svelte.d.ts +10 -0
  37. package/dist/themes/dark.css +49 -0
  38. package/dist/themes/index.d.ts +2 -0
  39. package/dist/themes/index.js +1 -0
  40. package/dist/themes/light.css +79 -0
  41. package/dist/tokens/.gitkeep +0 -0
  42. package/dist/tokens/__tests__/helpers.js +117 -0
  43. package/dist/tokens/color.css +112 -0
  44. package/dist/tokens/components/appshell.css +22 -0
  45. package/dist/tokens/components/button.css +16 -0
  46. package/dist/tokens/components/command-palette.css +15 -0
  47. package/dist/tokens/components/input.css +16 -0
  48. package/dist/tokens/components/modal.css +9 -0
  49. package/dist/tokens/components/panel.css +16 -0
  50. package/dist/tokens/components/tooltip.css +10 -0
  51. package/dist/tokens/index.css +13 -0
  52. package/dist/tokens/radii.css +10 -0
  53. package/dist/tokens/semantic.css +79 -0
  54. package/dist/tokens/shadows.css +11 -0
  55. package/dist/tokens/spacing.css +32 -0
  56. package/dist/tokens/typography.css +39 -0
  57. package/package.json +70 -0
package/README.md ADDED
@@ -0,0 +1,140 @@
1
+ # fudge-design-system
2
+
3
+ Visual language and UI primitives for the fudge ecosystem. Product-agnostic — no application logic, no backend assumptions, no domain semantics.
4
+
5
+ **Status:** v0.1.0-rc.1 — foundation complete.
6
+
7
+ ## Consumers
8
+
9
+ - **fudge-client** (Tauri v2 + Svelte 5, desktop)
10
+ - Future web clients
11
+ - Future mobile clients
12
+
13
+ ## Stack
14
+
15
+ Svelte 5 component library. Product repos depend on this; never the reverse.
16
+
17
+ ## Shell Model
18
+
19
+ The target UI is canvas-first:
20
+
21
+ | Slot | Position |
22
+ |---|---|
23
+ | Command bar | Top |
24
+ | Sidebar | Left |
25
+ | Canvas | Center |
26
+ | AI panel | Right |
27
+ | Status bar | Bottom |
28
+
29
+ Layout primitives in this repo map directly to these slots.
30
+
31
+ ## What It Provides
32
+
33
+ - **Tokens** — three-tier system (raw → semantic → component), exported as `--fds-*` CSS custom properties. Covers color, spacing, typography, radii, shadows
34
+ - **Layout primitives** — AppShell, CommandBar, Sidebar, Canvas, AIPanel, StatusBar, Panel
35
+ - **Core components** — Button, Input, Tooltip, Modal, Command Palette
36
+ - **Themes** — light/dark switching via ThemeProvider
37
+
38
+ See [ROADMAP.md](ROADMAP.md) for future plans (icons, motion, custom themes, platform variants).
39
+
40
+ ## What It Excludes
41
+
42
+ - Product logic
43
+ - API calls or backend assumptions
44
+ - Workspace, comms, or any domain semantics
45
+
46
+ ## Local Development
47
+
48
+ Run `pnpm run package:watch` in this repo while developing with fudge-client. See [`docs/ai/local-dev-fast-path.md`](docs/ai/local-dev-fast-path.md) for the full workflow and failure modes.
49
+
50
+ ## Consumer Import Pattern
51
+
52
+ Root layout in fudge-client:
53
+
54
+ ```svelte
55
+ <!-- 1. Tokens — import once at app root, establishes all --fds-* custom properties -->
56
+ <script>
57
+ import '@fudge-me/design-system/tokens';
58
+
59
+ // 2. Theme CSS — both files needed so switching works
60
+ import '@fudge-me/design-system/themes/light.css';
61
+ import '@fudge-me/design-system/themes/dark.css';
62
+
63
+ // 3. Components — ThemeProvider, layout, UI primitives
64
+ import { ThemeProvider } from '@fudge-me/design-system/themes';
65
+ import { AppShell, Sidebar, Canvas, AIPanel, CommandBar, StatusBar } from '@fudge-me/design-system/layout';
66
+ import { Button } from '@fudge-me/design-system/components';
67
+ </script>
68
+
69
+ <!-- 4. ThemeProvider sets data-theme on root; consumer handles persistence -->
70
+ <ThemeProvider theme="system">
71
+ <AppShell>
72
+ {#snippet commandbar()}<CommandBar>...</CommandBar>{/snippet}
73
+ {#snippet sidebar()}<Sidebar>...</Sidebar>{/snippet}
74
+ {#snippet canvas()}<Canvas>...</Canvas>{/snippet}
75
+ {#snippet aipanel()}<AIPanel>...</AIPanel>{/snippet}
76
+ {#snippet statusbar()}<StatusBar>...</StatusBar>{/snippet}
77
+ </AppShell>
78
+ </ThemeProvider>
79
+ ```
80
+
81
+ ### Per-layer examples
82
+
83
+ **Token cherry-pick** (individual CSS file):
84
+
85
+ ```svelte
86
+ <script>
87
+ import '@fudge-me/design-system/tokens/color.css';
88
+ </script>
89
+ ```
90
+
91
+ **Component import**:
92
+
93
+ ```svelte
94
+ <script>
95
+ import { Button } from '@fudge-me/design-system/components';
96
+ import { Modal } from '@fudge-me/design-system/components';
97
+ </script>
98
+
99
+ <Button variant="primary" size="md" onclick={save}>Save</Button>
100
+ <Modal open={showDialog} onclose={() => showDialog = false}>
101
+ <p>Content here</p>
102
+ </Modal>
103
+ ```
104
+
105
+ **Theme usage**:
106
+
107
+ ```svelte
108
+ <script>
109
+ import { ThemeProvider } from '@fudge-me/design-system/themes';
110
+ </script>
111
+
112
+ <!-- "system" follows prefers-color-scheme; "light"/"dark" force a theme -->
113
+ <ThemeProvider theme="system">
114
+ <slot />
115
+ </ThemeProvider>
116
+ ```
117
+
118
+ ## Consumer Responsibilities
119
+
120
+ 1. Import tokens once at the app root
121
+ 2. Import both theme CSS files so switching works
122
+ 3. Wrap app in `ThemeProvider` — consumer handles persistence
123
+ 4. Do not override component tokens with raw tokens
124
+ 5. Use subpath exports only — do not import internal DS paths
125
+ 6. Use layered entrypoints when CSS is needed (root re-export excludes tokens)
126
+ 7. Component token overrides use direct child combinators (`>`) to prevent leaking
127
+
128
+ ## Docs
129
+
130
+ | File | Purpose |
131
+ |---|---|
132
+ | `docs/ai/contracts.md` | Repo boundaries and responsibilities |
133
+ | `docs/ai/prd.json` | Task definitions |
134
+ | `docs/ai/decisions.md` | Architectural decisions |
135
+ | `ARCHITECTURE.md` | Technical architecture and design decisions |
136
+ | `ROADMAP.md` | Versioned delivery plan |
137
+ | `docs/ai/css-variable-scoping.md` | CSS selector-tier scoping rules and per-zone token table |
138
+ | `docs/ai/component-theming-boundaries.md` | Component theming contract with allowed/forbidden patterns |
139
+ | `docs/ai/local-dev-fast-path.md` | Dev workflow: package:watch, Vite config, failure modes |
140
+ | [Ecosystem contract](../docs/ecosystem/contracts/fudge-design-system.md) | Cross-repo consumption contract and public API surface |
File without changes
@@ -0,0 +1,118 @@
1
+ <script lang="ts" module>
2
+ export type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'destructive';
3
+ export type ButtonSize = 'sm' | 'md' | 'lg';
4
+ </script>
5
+
6
+ <script lang="ts">
7
+ import type { Snippet } from 'svelte';
8
+ import type { HTMLButtonAttributes } from 'svelte/elements';
9
+
10
+ interface Props extends HTMLButtonAttributes {
11
+ variant?: ButtonVariant;
12
+ size?: ButtonSize;
13
+ children?: Snippet;
14
+ }
15
+
16
+ let {
17
+ variant = 'primary',
18
+ size = 'md',
19
+ type = 'button',
20
+ disabled = false,
21
+ children,
22
+ ...rest
23
+ }: Props = $props();
24
+ </script>
25
+
26
+ <button
27
+ class="fds-button fds-button--{variant} fds-button--{size}"
28
+ {type}
29
+ {disabled}
30
+ {...rest}
31
+ >
32
+ {#if children}{@render children()}{/if}
33
+ </button>
34
+
35
+ <style>
36
+ .fds-button {
37
+ font-family: var(--fds-button-font-family);
38
+ font-size: var(--fds-button-font-size);
39
+ font-weight: var(--fds-button-font-weight);
40
+ line-height: 1;
41
+ padding: var(--fds-button-padding-y) var(--fds-button-padding-x);
42
+ background: var(--fds-button-bg);
43
+ color: var(--fds-button-text);
44
+ border: 1px solid var(--fds-button-border-color);
45
+ border-radius: var(--fds-button-radius);
46
+ cursor: pointer;
47
+ display: inline-flex;
48
+ align-items: center;
49
+ justify-content: center;
50
+ transition: background 0.15s ease, border-color 0.15s ease, opacity 0.15s ease;
51
+ }
52
+
53
+ .fds-button:hover:not(:disabled) {
54
+ background: var(--fds-button-bg-hover);
55
+ }
56
+
57
+ .fds-button:active:not(:disabled) {
58
+ background: var(--fds-button-bg-active);
59
+ }
60
+
61
+ .fds-button:focus-visible {
62
+ outline: 2px solid var(--fds-button-border-color-focus);
63
+ outline-offset: 2px;
64
+ }
65
+
66
+ .fds-button:disabled {
67
+ background: var(--fds-button-bg-disabled);
68
+ color: var(--fds-button-text-disabled);
69
+ border-color: var(--fds-button-bg-disabled);
70
+ cursor: not-allowed;
71
+ }
72
+
73
+ /* Variant: secondary */
74
+ .fds-button--secondary {
75
+ --fds-button-bg: var(--fds-color-surface);
76
+ --fds-button-text: var(--fds-color-text);
77
+ --fds-button-border-color: var(--fds-color-border);
78
+ --fds-button-bg-hover: var(--fds-color-interactive-hover);
79
+ --fds-button-bg-active: var(--fds-color-interactive-active);
80
+ }
81
+
82
+ /* Variant: ghost */
83
+ .fds-button--ghost {
84
+ --fds-button-bg: transparent;
85
+ --fds-button-border-color: transparent;
86
+ --fds-button-text: var(--fds-color-text);
87
+ --fds-button-bg-hover: var(--fds-color-interactive-hover);
88
+ --fds-button-bg-active: var(--fds-color-interactive-active);
89
+ }
90
+
91
+ /* Variant: destructive */
92
+ .fds-button--destructive {
93
+ --fds-button-bg: var(--fds-color-error);
94
+ --fds-button-border-color: var(--fds-color-error);
95
+ --fds-button-text: var(--fds-color-text-on-primary);
96
+ }
97
+
98
+ .fds-button--destructive:hover:not(:disabled) {
99
+ opacity: 0.9;
100
+ }
101
+
102
+ .fds-button--destructive:active:not(:disabled) {
103
+ opacity: 0.8;
104
+ }
105
+
106
+ /* Size: sm */
107
+ .fds-button--sm {
108
+ --fds-button-padding-x: var(--fds-spacing-element);
109
+ --fds-button-padding-y: var(--fds-spacing-element-xs);
110
+ --fds-button-font-size: var(--fds-font-label-size);
111
+ }
112
+
113
+ /* Size: lg */
114
+ .fds-button--lg {
115
+ --fds-button-padding-x: var(--fds-spacing-section);
116
+ --fds-button-padding-y: var(--fds-spacing-element);
117
+ }
118
+ </style>
@@ -0,0 +1,12 @@
1
+ export type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'destructive';
2
+ export type ButtonSize = 'sm' | 'md' | 'lg';
3
+ import type { Snippet } from 'svelte';
4
+ import type { HTMLButtonAttributes } from 'svelte/elements';
5
+ interface Props extends HTMLButtonAttributes {
6
+ variant?: ButtonVariant;
7
+ size?: ButtonSize;
8
+ children?: Snippet;
9
+ }
10
+ declare const Button: import("svelte").Component<Props, {}, "">;
11
+ type Button = ReturnType<typeof Button>;
12
+ export default Button;
@@ -0,0 +1,167 @@
1
+ <script lang="ts" module>
2
+ export interface CommandPaletteItem {
3
+ id: string;
4
+ label: string;
5
+ description?: string;
6
+ }
7
+ </script>
8
+
9
+ <script lang="ts">
10
+ import Input from './Input.svelte';
11
+
12
+ interface Props {
13
+ items: CommandPaletteItem[];
14
+ placeholder?: string;
15
+ onselect?: (item: CommandPaletteItem) => void;
16
+ onclose?: () => void;
17
+ filter?: (item: CommandPaletteItem, query: string) => boolean;
18
+ emptyMessage?: string;
19
+ }
20
+
21
+ let {
22
+ items,
23
+ placeholder = 'Search...',
24
+ onselect,
25
+ onclose,
26
+ filter,
27
+ emptyMessage = 'No results',
28
+ }: Props = $props();
29
+
30
+ const instanceId = `fds-cp-${Math.random().toString(36).slice(2, 9)}`;
31
+ const listboxId = `${instanceId}-listbox`;
32
+
33
+ let query = $state('');
34
+ let activeIndex = $state(0);
35
+
36
+ function defaultFilter(item: CommandPaletteItem, q: string): boolean {
37
+ return item.label.toLowerCase().includes(q.toLowerCase());
38
+ }
39
+
40
+ let filteredItems = $derived(
41
+ query ? items.filter((item) => (filter ?? defaultFilter)(item, query)) : items
42
+ );
43
+
44
+ $effect(() => {
45
+ // Reset active index when filtered items change
46
+ filteredItems;
47
+ activeIndex = 0;
48
+ });
49
+
50
+ $effect(() => {
51
+ const el = document.getElementById(`${instanceId}-item-${activeIndex}`);
52
+ el?.scrollIntoView({ block: 'nearest' });
53
+ });
54
+
55
+ function handleKeydown(e: KeyboardEvent) {
56
+ switch (e.key) {
57
+ case 'ArrowDown':
58
+ e.preventDefault();
59
+ if (filteredItems.length > 0) {
60
+ activeIndex = (activeIndex + 1) % filteredItems.length;
61
+ }
62
+ break;
63
+ case 'ArrowUp':
64
+ e.preventDefault();
65
+ if (filteredItems.length > 0) {
66
+ activeIndex = (activeIndex - 1 + filteredItems.length) % filteredItems.length;
67
+ }
68
+ break;
69
+ case 'Enter':
70
+ e.preventDefault();
71
+ if (filteredItems.length > 0 && activeIndex < filteredItems.length) {
72
+ onselect?.(filteredItems[activeIndex]);
73
+ }
74
+ break;
75
+ case 'Escape':
76
+ e.preventDefault();
77
+ onclose?.();
78
+ break;
79
+ }
80
+ }
81
+ </script>
82
+
83
+ <div class="fds-command-palette">
84
+ <div class="fds-command-palette__input">
85
+ <Input
86
+ type="search"
87
+ {placeholder}
88
+ bind:value={query}
89
+ role="combobox"
90
+ aria-expanded={true}
91
+ aria-controls={listboxId}
92
+ aria-activedescendant={activeIndex >= 0 && filteredItems.length > 0
93
+ ? `${instanceId}-item-${activeIndex}`
94
+ : undefined}
95
+ aria-autocomplete="list"
96
+ onkeydown={handleKeydown}
97
+ />
98
+ </div>
99
+ <ul class="fds-command-palette__list" role="listbox" id={listboxId}>
100
+ {#each filteredItems as item, i}
101
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
102
+ <li
103
+ role="option"
104
+ id="{instanceId}-item-{i}"
105
+ aria-selected={i === activeIndex}
106
+ class="fds-command-palette__item"
107
+ class:fds-command-palette__item--active={i === activeIndex}
108
+ onclick={() => onselect?.(item)}
109
+ >
110
+ <span class="fds-command-palette__item-label">{item.label}</span>
111
+ {#if item.description}
112
+ <span class="fds-command-palette__item-description">{item.description}</span>
113
+ {/if}
114
+ </li>
115
+ {/each}
116
+ {#if filteredItems.length === 0}
117
+ <li class="fds-command-palette__empty">{emptyMessage}</li>
118
+ {/if}
119
+ </ul>
120
+ </div>
121
+
122
+ <style>
123
+ .fds-command-palette {
124
+ background: var(--fds-command-palette-bg);
125
+ border: 1px solid var(--fds-command-palette-border-color);
126
+ border-radius: var(--fds-command-palette-radius);
127
+ box-shadow: var(--fds-command-palette-shadow);
128
+ padding: var(--fds-command-palette-padding);
129
+ display: flex;
130
+ flex-direction: column;
131
+ }
132
+
133
+ .fds-command-palette__input {
134
+ padding-bottom: var(--fds-command-palette-padding);
135
+ }
136
+
137
+ .fds-command-palette__list {
138
+ list-style: none;
139
+ margin: 0;
140
+ padding: 0;
141
+ max-height: 16rem;
142
+ overflow-y: auto;
143
+ }
144
+
145
+ .fds-command-palette__item {
146
+ display: flex;
147
+ flex-direction: column;
148
+ padding: var(--fds-command-palette-item-padding-y) var(--fds-command-palette-item-padding-x);
149
+ border-radius: var(--fds-command-palette-item-radius);
150
+ color: var(--fds-command-palette-item-text);
151
+ cursor: pointer;
152
+ }
153
+
154
+ .fds-command-palette__item--active {
155
+ background: var(--fds-command-palette-item-bg-active);
156
+ }
157
+
158
+ .fds-command-palette__item-description {
159
+ font-size: var(--fds-command-palette-item-font-size-secondary);
160
+ color: var(--fds-command-palette-item-text-secondary);
161
+ }
162
+
163
+ .fds-command-palette__empty {
164
+ padding: var(--fds-command-palette-item-padding-y) var(--fds-command-palette-item-padding-x);
165
+ color: var(--fds-command-palette-empty-text);
166
+ }
167
+ </style>
@@ -0,0 +1,16 @@
1
+ export interface CommandPaletteItem {
2
+ id: string;
3
+ label: string;
4
+ description?: string;
5
+ }
6
+ interface Props {
7
+ items: CommandPaletteItem[];
8
+ placeholder?: string;
9
+ onselect?: (item: CommandPaletteItem) => void;
10
+ onclose?: () => void;
11
+ filter?: (item: CommandPaletteItem, query: string) => boolean;
12
+ emptyMessage?: string;
13
+ }
14
+ declare const CommandPalette: import("svelte").Component<Props, {}, "">;
15
+ type CommandPalette = ReturnType<typeof CommandPalette>;
16
+ export default CommandPalette;
@@ -0,0 +1,70 @@
1
+ <script lang="ts">
2
+ import type { HTMLInputAttributes } from 'svelte/elements';
3
+
4
+ interface Props extends HTMLInputAttributes {
5
+ type?: 'text' | 'search';
6
+ error?: boolean;
7
+ value?: string;
8
+ }
9
+
10
+ let {
11
+ type = 'text',
12
+ error = false,
13
+ disabled = false,
14
+ value = $bindable(''),
15
+ ...rest
16
+ }: Props = $props();
17
+ </script>
18
+
19
+ <input
20
+ class="fds-input"
21
+ class:fds-input--error={error}
22
+ {type}
23
+ {disabled}
24
+ bind:value
25
+ {...rest}
26
+ />
27
+
28
+ <style>
29
+ .fds-input {
30
+ width: 100%;
31
+ font-family: var(--fds-input-font-family);
32
+ font-size: var(--fds-input-font-size);
33
+ padding: var(--fds-input-padding-y) var(--fds-input-padding-x);
34
+ background: var(--fds-input-bg);
35
+ color: var(--fds-input-text);
36
+ border: 1px solid var(--fds-input-border-color);
37
+ border-radius: var(--fds-input-radius);
38
+ outline: none;
39
+ transition: border-color 0.15s ease;
40
+ }
41
+
42
+ .fds-input::placeholder {
43
+ color: var(--fds-input-placeholder);
44
+ }
45
+
46
+ .fds-input:hover:not(:disabled):not(.fds-input--error) {
47
+ border-color: var(--fds-input-border-color-hover);
48
+ }
49
+
50
+ .fds-input:focus-visible {
51
+ border-color: var(--fds-input-border-color-focus);
52
+ outline: 2px solid var(--fds-input-border-color-focus);
53
+ outline-offset: -1px;
54
+ }
55
+
56
+ .fds-input--error {
57
+ border-color: var(--fds-input-border-color-error);
58
+ }
59
+
60
+ .fds-input--error:focus-visible {
61
+ border-color: var(--fds-input-border-color-error);
62
+ outline-color: var(--fds-input-border-color-error);
63
+ }
64
+
65
+ .fds-input:disabled {
66
+ background: var(--fds-input-bg-disabled);
67
+ color: var(--fds-input-text-disabled);
68
+ cursor: not-allowed;
69
+ }
70
+ </style>
@@ -0,0 +1,9 @@
1
+ import type { HTMLInputAttributes } from 'svelte/elements';
2
+ interface Props extends HTMLInputAttributes {
3
+ type?: 'text' | 'search';
4
+ error?: boolean;
5
+ value?: string;
6
+ }
7
+ declare const Input: import("svelte").Component<Props, {}, "value">;
8
+ type Input = ReturnType<typeof Input>;
9
+ export default Input;
@@ -0,0 +1,83 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLDialogAttributes } from 'svelte/elements';
4
+
5
+ interface Props extends HTMLDialogAttributes {
6
+ open?: boolean;
7
+ closeOnEscape?: boolean;
8
+ closeOnBackdrop?: boolean;
9
+ children?: Snippet;
10
+ onclose?: () => void;
11
+ }
12
+
13
+ let {
14
+ open = $bindable(false),
15
+ closeOnEscape = true,
16
+ closeOnBackdrop = true,
17
+ children,
18
+ onclose: onclosecb,
19
+ ...rest
20
+ }: Props = $props();
21
+
22
+ let dialogEl: HTMLDialogElement | undefined = $state();
23
+
24
+ $effect(() => {
25
+ if (!dialogEl) return;
26
+ if (open && !dialogEl.open) {
27
+ dialogEl.showModal();
28
+ } else if (!open && dialogEl.open) {
29
+ dialogEl.close();
30
+ }
31
+ });
32
+
33
+ function handleCancel(e: Event) {
34
+ if (!closeOnEscape) {
35
+ e.preventDefault();
36
+ }
37
+ }
38
+
39
+ function handleClose() {
40
+ open = false;
41
+ onclosecb?.();
42
+ }
43
+
44
+ function handleClick(e: MouseEvent) {
45
+ if (closeOnBackdrop && e.target === dialogEl) {
46
+ open = false;
47
+ }
48
+ }
49
+ </script>
50
+
51
+ <dialog
52
+ bind:this={dialogEl}
53
+ class="fds-modal"
54
+ oncancel={handleCancel}
55
+ onclose={handleClose}
56
+ onclick={handleClick}
57
+ {...rest}
58
+ >
59
+ <div class="fds-modal__content">
60
+ {#if children}{@render children()}{/if}
61
+ </div>
62
+ </dialog>
63
+
64
+ <style>
65
+ .fds-modal {
66
+ border: 1px solid var(--fds-modal-border-color);
67
+ padding: 0;
68
+ margin: auto;
69
+ max-width: min(90vw, 32rem);
70
+ background: var(--fds-modal-bg);
71
+ color: var(--fds-modal-text);
72
+ border-radius: var(--fds-modal-radius);
73
+ box-shadow: var(--fds-modal-shadow);
74
+ }
75
+
76
+ .fds-modal::backdrop {
77
+ background: var(--fds-modal-backdrop);
78
+ }
79
+
80
+ .fds-modal__content {
81
+ padding: var(--fds-modal-padding);
82
+ }
83
+ </style>
@@ -0,0 +1,12 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLDialogAttributes } from 'svelte/elements';
3
+ interface Props extends HTMLDialogAttributes {
4
+ open?: boolean;
5
+ closeOnEscape?: boolean;
6
+ closeOnBackdrop?: boolean;
7
+ children?: Snippet;
8
+ onclose?: () => void;
9
+ }
10
+ declare const Modal: import("svelte").Component<Props, {}, "open">;
11
+ type Modal = ReturnType<typeof Modal>;
12
+ export default Modal;