@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.
- package/README.md +140 -0
- package/dist/components/.gitkeep +0 -0
- package/dist/components/Button.svelte +118 -0
- package/dist/components/Button.svelte.d.ts +12 -0
- package/dist/components/CommandPalette.svelte +167 -0
- package/dist/components/CommandPalette.svelte.d.ts +16 -0
- package/dist/components/Input.svelte +70 -0
- package/dist/components/Input.svelte.d.ts +9 -0
- package/dist/components/Modal.svelte +83 -0
- package/dist/components/Modal.svelte.d.ts +12 -0
- package/dist/components/Tooltip.svelte +92 -0
- package/dist/components/Tooltip.svelte.d.ts +12 -0
- package/dist/components/index.d.ts +8 -0
- package/dist/components/index.js +5 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/layout/.gitkeep +0 -0
- package/dist/layout/AIPanel.svelte +34 -0
- package/dist/layout/AIPanel.svelte.d.ts +11 -0
- package/dist/layout/AppShell.svelte +78 -0
- package/dist/layout/AppShell.svelte.d.ts +11 -0
- package/dist/layout/Canvas.svelte +22 -0
- package/dist/layout/Canvas.svelte.d.ts +7 -0
- package/dist/layout/CommandBar.svelte +25 -0
- package/dist/layout/CommandBar.svelte.d.ts +7 -0
- package/dist/layout/Panel.svelte +157 -0
- package/dist/layout/Panel.svelte.d.ts +13 -0
- package/dist/layout/Sidebar.svelte +34 -0
- package/dist/layout/Sidebar.svelte.d.ts +11 -0
- package/dist/layout/StatusBar.svelte +26 -0
- package/dist/layout/StatusBar.svelte.d.ts +7 -0
- package/dist/layout/__tests__/helpers.js +53 -0
- package/dist/layout/index.d.ts +8 -0
- package/dist/layout/index.js +7 -0
- package/dist/themes/ThemeProvider.svelte +44 -0
- package/dist/themes/ThemeProvider.svelte.d.ts +10 -0
- package/dist/themes/dark.css +49 -0
- package/dist/themes/index.d.ts +2 -0
- package/dist/themes/index.js +1 -0
- package/dist/themes/light.css +79 -0
- package/dist/tokens/.gitkeep +0 -0
- package/dist/tokens/__tests__/helpers.js +117 -0
- package/dist/tokens/color.css +112 -0
- package/dist/tokens/components/appshell.css +22 -0
- package/dist/tokens/components/button.css +16 -0
- package/dist/tokens/components/command-palette.css +15 -0
- package/dist/tokens/components/input.css +16 -0
- package/dist/tokens/components/modal.css +9 -0
- package/dist/tokens/components/panel.css +16 -0
- package/dist/tokens/components/tooltip.css +10 -0
- package/dist/tokens/index.css +13 -0
- package/dist/tokens/radii.css +10 -0
- package/dist/tokens/semantic.css +79 -0
- package/dist/tokens/shadows.css +11 -0
- package/dist/tokens/spacing.css +32 -0
- package/dist/tokens/typography.css +39 -0
- 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;
|