@aiaiai-pt/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/components/Alert.svelte +100 -0
- package/components/Badge.svelte +108 -0
- package/components/BottomNav.svelte +37 -0
- package/components/BottomNavItem.svelte +121 -0
- package/components/Button.svelte +269 -0
- package/components/Card.svelte +108 -0
- package/components/Checkbox.svelte +138 -0
- package/components/CodeBlock.svelte +187 -0
- package/components/CodeEditor.svelte +221 -0
- package/components/CollapsibleSection.svelte +160 -0
- package/components/Combobox.svelte +396 -0
- package/components/EmptyState.svelte +148 -0
- package/components/FileUpload.svelte +280 -0
- package/components/FileUploadItem.svelte +222 -0
- package/components/Input.svelte +222 -0
- package/components/KeyValue.svelte +79 -0
- package/components/Label.svelte +49 -0
- package/components/List.svelte +70 -0
- package/components/ListItem.svelte +125 -0
- package/components/Menu.svelte +161 -0
- package/components/MenuItem.svelte +120 -0
- package/components/MenuSeparator.svelte +34 -0
- package/components/Modal.svelte +260 -0
- package/components/OptionGrid.svelte +195 -0
- package/components/Panel.svelte +256 -0
- package/components/Popover.svelte +194 -0
- package/components/Progress.svelte +78 -0
- package/components/Select.svelte +182 -0
- package/components/Separator.svelte +47 -0
- package/components/Sidebar.svelte +106 -0
- package/components/SidebarItem.svelte +154 -0
- package/components/SidebarSection.svelte +43 -0
- package/components/Skeleton.svelte +79 -0
- package/components/Status.svelte +104 -0
- package/components/Stepper.svelte +142 -0
- package/components/Tab.svelte +94 -0
- package/components/TabList.svelte +36 -0
- package/components/TabPanel.svelte +45 -0
- package/components/Tabs.svelte +46 -0
- package/components/Tag.svelte +96 -0
- package/components/Textarea.svelte +143 -0
- package/components/Toast.svelte +114 -0
- package/components/Toggle.svelte +132 -0
- package/components/index.js +70 -0
- package/package.json +45 -0
- package/tokens/base.css +175 -0
- package/tokens/components.css +530 -0
- package/tokens/semantic.css +211 -0
- package/tokens/themes/aiaiai.css +53 -0
- package/tokens/themes/bespoke-example.css +148 -0
- package/tokens/themes/branded-example.css +55 -0
- package/tokens/utilities.css +1865 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component KeyValue
|
|
3
|
+
|
|
4
|
+
Structured data display. Key in mono label, value in mono data.
|
|
5
|
+
Consumes --kv-* tokens from components.css.
|
|
6
|
+
|
|
7
|
+
@example Stacked (default)
|
|
8
|
+
<KeyValue key="STATUS" value="Active" />
|
|
9
|
+
|
|
10
|
+
@example Inline
|
|
11
|
+
<KeyValue key="PLAN" value="Pro" layout="inline" />
|
|
12
|
+
|
|
13
|
+
@example Custom value via snippet
|
|
14
|
+
<KeyValue key="STATUS">
|
|
15
|
+
{#snippet value()}<Badge variant="success">ACTIVE</Badge>{/snippet}
|
|
16
|
+
</KeyValue>
|
|
17
|
+
-->
|
|
18
|
+
<script>
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {'stacked' | 'inline'} Layout
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
let {
|
|
24
|
+
/** @type {string} */
|
|
25
|
+
key,
|
|
26
|
+
/** @type {string | import('svelte').Snippet | undefined} */
|
|
27
|
+
value = undefined,
|
|
28
|
+
/** @type {Layout} */
|
|
29
|
+
layout = 'stacked',
|
|
30
|
+
/** @type {string} */
|
|
31
|
+
class: className = '',
|
|
32
|
+
...rest
|
|
33
|
+
} = $props();
|
|
34
|
+
|
|
35
|
+
// Determine if we got a snippet or a string for value
|
|
36
|
+
const hasSnippet = $derived(typeof value === 'function');
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<div
|
|
40
|
+
class="kv kv-{layout} {className}"
|
|
41
|
+
{...rest}
|
|
42
|
+
>
|
|
43
|
+
<span class="kv-key">{key}</span>
|
|
44
|
+
{#if hasSnippet}
|
|
45
|
+
<span class="kv-value">{@render /** @type {import('svelte').Snippet} */ (value)()}</span>
|
|
46
|
+
{:else if value != null}
|
|
47
|
+
<span class="kv-value">{value}</span>
|
|
48
|
+
{/if}
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<style>
|
|
52
|
+
.kv {
|
|
53
|
+
display: flex;
|
|
54
|
+
gap: var(--kv-gap);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.kv-stacked {
|
|
58
|
+
flex-direction: column;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.kv-inline {
|
|
62
|
+
flex-direction: row;
|
|
63
|
+
justify-content: space-between;
|
|
64
|
+
align-items: center;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.kv-key {
|
|
68
|
+
font-family: var(--kv-key-font);
|
|
69
|
+
font-size: var(--kv-key-size);
|
|
70
|
+
color: var(--kv-key-color);
|
|
71
|
+
letter-spacing: var(--type-label-tracking);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.kv-value {
|
|
75
|
+
font-family: var(--kv-value-font);
|
|
76
|
+
font-size: var(--kv-value-size);
|
|
77
|
+
color: var(--kv-value-color);
|
|
78
|
+
}
|
|
79
|
+
</style>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component Label
|
|
3
|
+
|
|
4
|
+
Standalone form label. Uses the same typography tokens as Input labels.
|
|
5
|
+
Use this in complex form layouts where the label isn't built into a form component.
|
|
6
|
+
|
|
7
|
+
@example
|
|
8
|
+
<Label for="my-input">EMAIL ADDRESS</Label>
|
|
9
|
+
|
|
10
|
+
@example Disabled
|
|
11
|
+
<Label for="my-input" disabled>LOCKED FIELD</Label>
|
|
12
|
+
-->
|
|
13
|
+
<script>
|
|
14
|
+
let {
|
|
15
|
+
/** @type {string | undefined} */
|
|
16
|
+
for: htmlFor = undefined,
|
|
17
|
+
/** @type {boolean} */
|
|
18
|
+
disabled = false,
|
|
19
|
+
/** @type {string} */
|
|
20
|
+
class: className = '',
|
|
21
|
+
/** @type {import('svelte').Snippet | undefined} */
|
|
22
|
+
children = undefined,
|
|
23
|
+
...rest
|
|
24
|
+
} = $props();
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<label
|
|
28
|
+
for={htmlFor}
|
|
29
|
+
class="label {className}"
|
|
30
|
+
class:label-disabled={disabled}
|
|
31
|
+
{...rest}
|
|
32
|
+
>
|
|
33
|
+
{#if children}{@render children()}{/if}
|
|
34
|
+
</label>
|
|
35
|
+
|
|
36
|
+
<style>
|
|
37
|
+
.label {
|
|
38
|
+
font-family: var(--input-label-font);
|
|
39
|
+
font-size: var(--input-label-size);
|
|
40
|
+
letter-spacing: var(--input-label-tracking);
|
|
41
|
+
color: var(--input-label-color);
|
|
42
|
+
cursor: default;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.label-disabled {
|
|
46
|
+
opacity: 0.5;
|
|
47
|
+
cursor: not-allowed;
|
|
48
|
+
}
|
|
49
|
+
</style>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component List
|
|
3
|
+
|
|
4
|
+
Container for sequences of items. Two variants:
|
|
5
|
+
- "gap" (default) — flex column with `var(--list-gap)` between children.
|
|
6
|
+
- "bordered" — outer border + border-radius + overflow hidden. Children use border-bottom dividers.
|
|
7
|
+
|
|
8
|
+
Consumes --list-* tokens from components.css.
|
|
9
|
+
|
|
10
|
+
@example Gap variant (default)
|
|
11
|
+
<List>
|
|
12
|
+
<ListItem>First item</ListItem>
|
|
13
|
+
<ListItem>Second item</ListItem>
|
|
14
|
+
</List>
|
|
15
|
+
|
|
16
|
+
@example Bordered variant
|
|
17
|
+
<List variant="bordered">
|
|
18
|
+
<ListItem>Row one</ListItem>
|
|
19
|
+
<ListItem>Row two</ListItem>
|
|
20
|
+
</List>
|
|
21
|
+
-->
|
|
22
|
+
<script>
|
|
23
|
+
/**
|
|
24
|
+
* @typedef {'gap' | 'bordered'} Variant
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
let {
|
|
28
|
+
/** @type {Variant} */
|
|
29
|
+
variant = 'gap',
|
|
30
|
+
/** @type {string} */
|
|
31
|
+
class: className = '',
|
|
32
|
+
/** @type {import('svelte').Snippet | undefined} */
|
|
33
|
+
children = undefined,
|
|
34
|
+
...rest
|
|
35
|
+
} = $props();
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<div class="list list-{variant} {className}" role="list" {...rest}>
|
|
39
|
+
{#if children}{@render children()}{/if}
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<style>
|
|
43
|
+
.list {
|
|
44
|
+
display: flex;
|
|
45
|
+
flex-direction: column;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.list-gap {
|
|
49
|
+
gap: var(--list-gap);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.list-bordered {
|
|
53
|
+
border: var(--list-border);
|
|
54
|
+
border-radius: var(--list-border-radius);
|
|
55
|
+
overflow: hidden;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* Bordered variant: dividers + hover on direct children */
|
|
59
|
+
.list-bordered > :global([role="listitem"]) {
|
|
60
|
+
border-bottom: var(--list-item-divider);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.list-bordered > :global([role="listitem"]:last-child) {
|
|
64
|
+
border-bottom: none;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.list-bordered > :global([role="listitem"]:hover) {
|
|
68
|
+
background: var(--list-item-bg-hover);
|
|
69
|
+
}
|
|
70
|
+
</style>
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component ListItem
|
|
3
|
+
|
|
4
|
+
A single row with leading content area (flex: 1, column layout) and trailing action area (flex-shrink: 0).
|
|
5
|
+
Consumes --list-item-* tokens from components.css.
|
|
6
|
+
|
|
7
|
+
@example Simple content
|
|
8
|
+
<ListItem>Item text here</ListItem>
|
|
9
|
+
|
|
10
|
+
@example With leading and trailing snippets
|
|
11
|
+
<ListItem>
|
|
12
|
+
{#snippet leading()}
|
|
13
|
+
<span class="name">Pipeline v2</span>
|
|
14
|
+
<span class="desc">Last run: 2 hours ago</span>
|
|
15
|
+
{/snippet}
|
|
16
|
+
{#snippet trailing()}
|
|
17
|
+
<Toggle checked />
|
|
18
|
+
{/snippet}
|
|
19
|
+
</ListItem>
|
|
20
|
+
|
|
21
|
+
@example Interactive (renders as button)
|
|
22
|
+
<ListItem interactive onclick={() => select(id)}>
|
|
23
|
+
{#snippet leading()}
|
|
24
|
+
<span>Clickable row</span>
|
|
25
|
+
{/snippet}
|
|
26
|
+
</ListItem>
|
|
27
|
+
-->
|
|
28
|
+
<script>
|
|
29
|
+
let {
|
|
30
|
+
/** @type {boolean} */
|
|
31
|
+
interactive = false,
|
|
32
|
+
/** @type {string} */
|
|
33
|
+
class: className = '',
|
|
34
|
+
/** @type {import('svelte').Snippet | undefined} */
|
|
35
|
+
leading = undefined,
|
|
36
|
+
/** @type {import('svelte').Snippet | undefined} */
|
|
37
|
+
trailing = undefined,
|
|
38
|
+
/** @type {import('svelte').Snippet | undefined} */
|
|
39
|
+
children = undefined,
|
|
40
|
+
...rest
|
|
41
|
+
} = $props();
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
{#snippet body()}
|
|
45
|
+
<div class="list-item-leading">
|
|
46
|
+
{#if leading}
|
|
47
|
+
{@render leading()}
|
|
48
|
+
{:else if children}
|
|
49
|
+
{@render children()}
|
|
50
|
+
{/if}
|
|
51
|
+
</div>
|
|
52
|
+
{#if trailing}
|
|
53
|
+
<div class="list-item-trailing">
|
|
54
|
+
{@render trailing()}
|
|
55
|
+
</div>
|
|
56
|
+
{/if}
|
|
57
|
+
{/snippet}
|
|
58
|
+
|
|
59
|
+
{#if interactive}
|
|
60
|
+
<button
|
|
61
|
+
class="list-item list-item-interactive {className}"
|
|
62
|
+
role="listitem"
|
|
63
|
+
{...rest}
|
|
64
|
+
>
|
|
65
|
+
{@render body()}
|
|
66
|
+
</button>
|
|
67
|
+
{:else}
|
|
68
|
+
<div
|
|
69
|
+
class="list-item {className}"
|
|
70
|
+
role="listitem"
|
|
71
|
+
{...rest}
|
|
72
|
+
>
|
|
73
|
+
{@render body()}
|
|
74
|
+
</div>
|
|
75
|
+
{/if}
|
|
76
|
+
|
|
77
|
+
<style>
|
|
78
|
+
.list-item {
|
|
79
|
+
display: flex;
|
|
80
|
+
align-items: center;
|
|
81
|
+
gap: var(--list-item-gap);
|
|
82
|
+
padding: var(--list-item-padding-y) var(--list-item-padding-x);
|
|
83
|
+
background: var(--list-item-bg);
|
|
84
|
+
transition: background var(--list-item-transition);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.list-item-interactive {
|
|
88
|
+
all: unset;
|
|
89
|
+
display: flex;
|
|
90
|
+
align-items: center;
|
|
91
|
+
gap: var(--list-item-gap);
|
|
92
|
+
padding: var(--list-item-padding-y) var(--list-item-padding-x);
|
|
93
|
+
background: var(--list-item-bg);
|
|
94
|
+
transition: background var(--list-item-transition);
|
|
95
|
+
width: 100%;
|
|
96
|
+
cursor: pointer;
|
|
97
|
+
font-family: inherit;
|
|
98
|
+
font-size: inherit;
|
|
99
|
+
text-align: left;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.list-item-interactive:hover {
|
|
103
|
+
background: var(--list-item-bg-hover);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.list-item-interactive:focus-visible {
|
|
107
|
+
outline: var(--focus-ring-width) solid var(--focus-ring-color);
|
|
108
|
+
outline-offset: calc(-1 * var(--focus-ring-offset));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.list-item-leading {
|
|
112
|
+
flex: 1;
|
|
113
|
+
min-width: 0;
|
|
114
|
+
display: flex;
|
|
115
|
+
flex-direction: column;
|
|
116
|
+
gap: var(--list-item-leading-gap);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.list-item-trailing {
|
|
120
|
+
flex-shrink: 0;
|
|
121
|
+
display: flex;
|
|
122
|
+
align-items: center;
|
|
123
|
+
gap: var(--list-item-trailing-gap);
|
|
124
|
+
}
|
|
125
|
+
</style>
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component Menu
|
|
3
|
+
|
|
4
|
+
Positioned floating menu anchored to a trigger element.
|
|
5
|
+
Composes Popover internally and adds WAI-ARIA Menu Button semantics:
|
|
6
|
+
arrow key navigation, typeahead, and Enter/Space activation.
|
|
7
|
+
|
|
8
|
+
Consumes --menu-* tokens from components.css.
|
|
9
|
+
|
|
10
|
+
@example
|
|
11
|
+
<button bind:this={anchor} onclick={() => open = !open}>Options</button>
|
|
12
|
+
<Menu bind:open {anchor} placement="bottom-start">
|
|
13
|
+
<MenuItem onclick={handleEdit}>
|
|
14
|
+
{#snippet leading()}<PencilIcon size={14} />{/snippet}
|
|
15
|
+
Edit name
|
|
16
|
+
</MenuItem>
|
|
17
|
+
<MenuSeparator />
|
|
18
|
+
<MenuItem variant="destructive" onclick={handleDelete}>
|
|
19
|
+
{#snippet leading()}<TrashIcon size={14} />{/snippet}
|
|
20
|
+
Delete
|
|
21
|
+
</MenuItem>
|
|
22
|
+
</Menu>
|
|
23
|
+
-->
|
|
24
|
+
<script>
|
|
25
|
+
import Popover from './Popover.svelte';
|
|
26
|
+
|
|
27
|
+
let {
|
|
28
|
+
/** @type {boolean} */
|
|
29
|
+
open = $bindable(false),
|
|
30
|
+
/** @type {HTMLElement | undefined} */
|
|
31
|
+
anchor = undefined,
|
|
32
|
+
/** @type {'bottom-start' | 'bottom-end' | 'top-start' | 'top-end'} */
|
|
33
|
+
placement = 'bottom-start',
|
|
34
|
+
/** @type {(() => void) | undefined} */
|
|
35
|
+
onclose = undefined,
|
|
36
|
+
/** @type {string} */
|
|
37
|
+
class: className = '',
|
|
38
|
+
/** @type {import('svelte').Snippet | undefined} */
|
|
39
|
+
children = undefined,
|
|
40
|
+
...rest
|
|
41
|
+
} = $props();
|
|
42
|
+
|
|
43
|
+
/** @type {HTMLElement | undefined} */
|
|
44
|
+
let menuEl;
|
|
45
|
+
|
|
46
|
+
let typeaheadBuffer = $state('');
|
|
47
|
+
let typeaheadTimer = $state(0);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get all enabled menu items inside the menu container.
|
|
51
|
+
* @returns {HTMLElement[]}
|
|
52
|
+
*/
|
|
53
|
+
function getItems() {
|
|
54
|
+
if (!menuEl) return [];
|
|
55
|
+
return /** @type {HTMLElement[]} */ (
|
|
56
|
+
Array.from(menuEl.querySelectorAll('[role="menuitem"]:not([aria-disabled="true"])'))
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Focus the menu item at the given index (clamped).
|
|
62
|
+
* @param {HTMLElement[]} items
|
|
63
|
+
* @param {number} index
|
|
64
|
+
*/
|
|
65
|
+
function focusItem(items, index) {
|
|
66
|
+
const clamped = Math.max(0, Math.min(items.length - 1, index));
|
|
67
|
+
items[clamped]?.focus();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** @param {KeyboardEvent} e */
|
|
71
|
+
function handleKeydown(e) {
|
|
72
|
+
const items = getItems();
|
|
73
|
+
if (items.length === 0) return;
|
|
74
|
+
|
|
75
|
+
const current = /** @type {HTMLElement} */ (document.activeElement);
|
|
76
|
+
const currentIndex = items.indexOf(current);
|
|
77
|
+
|
|
78
|
+
switch (e.key) {
|
|
79
|
+
case 'ArrowDown': {
|
|
80
|
+
e.preventDefault();
|
|
81
|
+
const next = currentIndex < items.length - 1 ? currentIndex + 1 : 0;
|
|
82
|
+
focusItem(items, next);
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
case 'ArrowUp': {
|
|
86
|
+
e.preventDefault();
|
|
87
|
+
const prev = currentIndex > 0 ? currentIndex - 1 : items.length - 1;
|
|
88
|
+
focusItem(items, prev);
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
case 'Home': {
|
|
92
|
+
e.preventDefault();
|
|
93
|
+
focusItem(items, 0);
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
case 'End': {
|
|
97
|
+
e.preventDefault();
|
|
98
|
+
focusItem(items, items.length - 1);
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
case 'Enter':
|
|
102
|
+
case ' ': {
|
|
103
|
+
e.preventDefault();
|
|
104
|
+
if (current && current.getAttribute('role') === 'menuitem') {
|
|
105
|
+
current.click();
|
|
106
|
+
}
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
default: {
|
|
110
|
+
// Typeahead: printable characters
|
|
111
|
+
if (e.key.length === 1 && !e.ctrlKey && !e.metaKey) {
|
|
112
|
+
e.preventDefault();
|
|
113
|
+
clearTimeout(typeaheadTimer);
|
|
114
|
+
typeaheadBuffer += e.key.toLowerCase();
|
|
115
|
+
typeaheadTimer = /** @type {number} */ (/** @type {unknown} */ (setTimeout(() => {
|
|
116
|
+
typeaheadBuffer = '';
|
|
117
|
+
}, 500)));
|
|
118
|
+
|
|
119
|
+
const match = items.find((item) =>
|
|
120
|
+
(item.textContent ?? '').trim().toLowerCase().startsWith(typeaheadBuffer)
|
|
121
|
+
);
|
|
122
|
+
if (match) match.focus();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
</script>
|
|
128
|
+
|
|
129
|
+
<Popover
|
|
130
|
+
bind:open
|
|
131
|
+
{anchor}
|
|
132
|
+
{placement}
|
|
133
|
+
{onclose}
|
|
134
|
+
class="menu-popover {className}"
|
|
135
|
+
role="menu"
|
|
136
|
+
aria-orientation="vertical"
|
|
137
|
+
{...rest}
|
|
138
|
+
>
|
|
139
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
140
|
+
<div
|
|
141
|
+
bind:this={menuEl}
|
|
142
|
+
class="menu"
|
|
143
|
+
onkeydown={handleKeydown}
|
|
144
|
+
>
|
|
145
|
+
{#if children}{@render children()}{/if}
|
|
146
|
+
</div>
|
|
147
|
+
</Popover>
|
|
148
|
+
|
|
149
|
+
<style>
|
|
150
|
+
:global(.menu-popover) {
|
|
151
|
+
padding: 0 !important;
|
|
152
|
+
min-width: var(--menu-min-width);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.menu {
|
|
156
|
+
display: flex;
|
|
157
|
+
flex-direction: column;
|
|
158
|
+
padding: var(--menu-padding);
|
|
159
|
+
outline: none;
|
|
160
|
+
}
|
|
161
|
+
</style>
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component MenuItem
|
|
3
|
+
|
|
4
|
+
A single actionable row inside a Menu.
|
|
5
|
+
Renders as <button role="menuitem"> with optional leading/trailing snippets.
|
|
6
|
+
|
|
7
|
+
Consumes --menu-item-* tokens from components.css.
|
|
8
|
+
|
|
9
|
+
@example
|
|
10
|
+
<MenuItem onclick={handleEdit}>
|
|
11
|
+
{#snippet leading()}<PencilIcon size={14} />{/snippet}
|
|
12
|
+
Edit name
|
|
13
|
+
</MenuItem>
|
|
14
|
+
|
|
15
|
+
@example Destructive
|
|
16
|
+
<MenuItem variant="destructive" onclick={handleDelete}>
|
|
17
|
+
{#snippet leading()}<TrashIcon size={14} />{/snippet}
|
|
18
|
+
Delete
|
|
19
|
+
</MenuItem>
|
|
20
|
+
|
|
21
|
+
@example With keyboard shortcut
|
|
22
|
+
<MenuItem onclick={handleCopy}>
|
|
23
|
+
Copy
|
|
24
|
+
{#snippet trailing()}<kbd>⌘C</kbd>{/snippet}
|
|
25
|
+
</MenuItem>
|
|
26
|
+
-->
|
|
27
|
+
<script>
|
|
28
|
+
let {
|
|
29
|
+
/** @type {'default' | 'destructive'} */
|
|
30
|
+
variant = 'default',
|
|
31
|
+
/** @type {boolean} */
|
|
32
|
+
disabled = false,
|
|
33
|
+
/** @type {import('svelte').Snippet | undefined} */
|
|
34
|
+
leading = undefined,
|
|
35
|
+
/** @type {import('svelte').Snippet | undefined} */
|
|
36
|
+
trailing = undefined,
|
|
37
|
+
/** @type {import('svelte').Snippet | undefined} */
|
|
38
|
+
children = undefined,
|
|
39
|
+
/** @type {string} */
|
|
40
|
+
class: className = '',
|
|
41
|
+
...rest
|
|
42
|
+
} = $props();
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<button
|
|
46
|
+
role="menuitem"
|
|
47
|
+
class="menu-item menu-item-{variant} {className}"
|
|
48
|
+
class:menu-item-disabled={disabled}
|
|
49
|
+
disabled={disabled || undefined}
|
|
50
|
+
aria-disabled={disabled || undefined}
|
|
51
|
+
tabindex={disabled ? -1 : -1}
|
|
52
|
+
{...rest}
|
|
53
|
+
>
|
|
54
|
+
{#if leading}
|
|
55
|
+
<span class="menu-item-leading" aria-hidden="true">{@render leading()}</span>
|
|
56
|
+
{/if}
|
|
57
|
+
<span class="menu-item-label">
|
|
58
|
+
{#if children}{@render children()}{/if}
|
|
59
|
+
</span>
|
|
60
|
+
{#if trailing}
|
|
61
|
+
<span class="menu-item-trailing">{@render trailing()}</span>
|
|
62
|
+
{/if}
|
|
63
|
+
</button>
|
|
64
|
+
|
|
65
|
+
<style>
|
|
66
|
+
.menu-item {
|
|
67
|
+
all: unset;
|
|
68
|
+
box-sizing: border-box;
|
|
69
|
+
display: flex;
|
|
70
|
+
align-items: center;
|
|
71
|
+
gap: var(--menu-item-gap);
|
|
72
|
+
min-height: var(--menu-item-height);
|
|
73
|
+
padding: var(--menu-item-padding-y) var(--menu-item-padding-x);
|
|
74
|
+
border-radius: var(--menu-item-radius);
|
|
75
|
+
font-size: var(--menu-item-font-size);
|
|
76
|
+
color: var(--menu-item-color);
|
|
77
|
+
cursor: pointer;
|
|
78
|
+
user-select: none;
|
|
79
|
+
text-align: start;
|
|
80
|
+
width: 100%;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.menu-item:hover:not(.menu-item-disabled),
|
|
84
|
+
.menu-item:focus-visible:not(.menu-item-disabled) {
|
|
85
|
+
background: var(--menu-item-bg-hover);
|
|
86
|
+
outline: none;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.menu-item-destructive {
|
|
90
|
+
color: var(--menu-item-color-destructive);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.menu-item-disabled {
|
|
94
|
+
color: var(--menu-item-color-disabled);
|
|
95
|
+
cursor: default;
|
|
96
|
+
pointer-events: none;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.menu-item-leading {
|
|
100
|
+
flex-shrink: 0;
|
|
101
|
+
display: flex;
|
|
102
|
+
align-items: center;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.menu-item-label {
|
|
106
|
+
flex: 1;
|
|
107
|
+
min-width: 0;
|
|
108
|
+
overflow: hidden;
|
|
109
|
+
text-overflow: ellipsis;
|
|
110
|
+
white-space: nowrap;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.menu-item-trailing {
|
|
114
|
+
flex-shrink: 0;
|
|
115
|
+
display: flex;
|
|
116
|
+
align-items: center;
|
|
117
|
+
color: var(--menu-item-color-disabled);
|
|
118
|
+
font-size: var(--type-caption-size);
|
|
119
|
+
}
|
|
120
|
+
</style>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component MenuSeparator
|
|
3
|
+
|
|
4
|
+
A thin visual divider between groups of MenuItems.
|
|
5
|
+
Consumes --menu-separator-* tokens from components.css.
|
|
6
|
+
|
|
7
|
+
@example
|
|
8
|
+
<Menu bind:open {anchor}>
|
|
9
|
+
<MenuItem>Edit</MenuItem>
|
|
10
|
+
<MenuSeparator />
|
|
11
|
+
<MenuItem variant="destructive">Delete</MenuItem>
|
|
12
|
+
</Menu>
|
|
13
|
+
-->
|
|
14
|
+
<script>
|
|
15
|
+
let {
|
|
16
|
+
/** @type {string} */
|
|
17
|
+
class: className = '',
|
|
18
|
+
...rest
|
|
19
|
+
} = $props();
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<div
|
|
23
|
+
role="separator"
|
|
24
|
+
class="menu-separator {className}"
|
|
25
|
+
{...rest}
|
|
26
|
+
></div>
|
|
27
|
+
|
|
28
|
+
<style>
|
|
29
|
+
.menu-separator {
|
|
30
|
+
height: 1px;
|
|
31
|
+
background: var(--menu-separator-color);
|
|
32
|
+
margin: var(--menu-separator-spacing) 0;
|
|
33
|
+
}
|
|
34
|
+
</style>
|