@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.
Files changed (52) hide show
  1. package/components/Alert.svelte +100 -0
  2. package/components/Badge.svelte +108 -0
  3. package/components/BottomNav.svelte +37 -0
  4. package/components/BottomNavItem.svelte +121 -0
  5. package/components/Button.svelte +269 -0
  6. package/components/Card.svelte +108 -0
  7. package/components/Checkbox.svelte +138 -0
  8. package/components/CodeBlock.svelte +187 -0
  9. package/components/CodeEditor.svelte +221 -0
  10. package/components/CollapsibleSection.svelte +160 -0
  11. package/components/Combobox.svelte +396 -0
  12. package/components/EmptyState.svelte +148 -0
  13. package/components/FileUpload.svelte +280 -0
  14. package/components/FileUploadItem.svelte +222 -0
  15. package/components/Input.svelte +222 -0
  16. package/components/KeyValue.svelte +79 -0
  17. package/components/Label.svelte +49 -0
  18. package/components/List.svelte +70 -0
  19. package/components/ListItem.svelte +125 -0
  20. package/components/Menu.svelte +161 -0
  21. package/components/MenuItem.svelte +120 -0
  22. package/components/MenuSeparator.svelte +34 -0
  23. package/components/Modal.svelte +260 -0
  24. package/components/OptionGrid.svelte +195 -0
  25. package/components/Panel.svelte +256 -0
  26. package/components/Popover.svelte +194 -0
  27. package/components/Progress.svelte +78 -0
  28. package/components/Select.svelte +182 -0
  29. package/components/Separator.svelte +47 -0
  30. package/components/Sidebar.svelte +106 -0
  31. package/components/SidebarItem.svelte +154 -0
  32. package/components/SidebarSection.svelte +43 -0
  33. package/components/Skeleton.svelte +79 -0
  34. package/components/Status.svelte +104 -0
  35. package/components/Stepper.svelte +142 -0
  36. package/components/Tab.svelte +94 -0
  37. package/components/TabList.svelte +36 -0
  38. package/components/TabPanel.svelte +45 -0
  39. package/components/Tabs.svelte +46 -0
  40. package/components/Tag.svelte +96 -0
  41. package/components/Textarea.svelte +143 -0
  42. package/components/Toast.svelte +114 -0
  43. package/components/Toggle.svelte +132 -0
  44. package/components/index.js +70 -0
  45. package/package.json +45 -0
  46. package/tokens/base.css +175 -0
  47. package/tokens/components.css +530 -0
  48. package/tokens/semantic.css +211 -0
  49. package/tokens/themes/aiaiai.css +53 -0
  50. package/tokens/themes/bespoke-example.css +148 -0
  51. package/tokens/themes/branded-example.css +55 -0
  52. 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>