@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,194 @@
1
+ <!--
2
+ @component Popover
3
+
4
+ Positioned floating content anchored to a trigger element.
5
+ Uses position: fixed + JS positioning (no CSS Anchor).
6
+ Consumes --popover-* tokens from components.css.
7
+
8
+ @example
9
+ <button bind:this={anchor} onclick={() => open = !open}>Options</button>
10
+ <Popover bind:open {anchor} placement="bottom-start">
11
+ <p>Popover content</p>
12
+ </Popover>
13
+
14
+ @example Match trigger width
15
+ <Popover bind:open {anchor} matchWidth>
16
+ <ul>...</ul>
17
+ </Popover>
18
+ -->
19
+ <script module>
20
+ let _popoverUid = 0;
21
+ </script>
22
+
23
+ <script>
24
+ /**
25
+ * @typedef {'bottom-start' | 'bottom-end' | 'top-start' | 'top-end'} Placement
26
+ */
27
+
28
+ let {
29
+ /** @type {boolean} */
30
+ open = $bindable(false),
31
+ /** @type {HTMLElement | undefined} */
32
+ anchor = undefined,
33
+ /** @type {Placement} */
34
+ placement = 'bottom-start',
35
+ /** @type {number} */
36
+ offset = 4,
37
+ /** @type {boolean} */
38
+ matchWidth = false,
39
+ /** @type {(() => void) | undefined} */
40
+ onclose = undefined,
41
+ /** @type {string} */
42
+ class: className = '',
43
+ /** @type {import('svelte').Snippet | undefined} */
44
+ children = undefined,
45
+ ...rest
46
+ } = $props();
47
+
48
+ const popoverId = `popover-${_popoverUid++}`;
49
+
50
+ /** @type {HTMLElement | undefined} */
51
+ let popoverEl = $state();
52
+
53
+ let posX = $state(0);
54
+ let posY = $state(0);
55
+ let width = $state(0);
56
+
57
+ function reposition() {
58
+ if (!anchor || !popoverEl) return;
59
+
60
+ const rect = anchor.getBoundingClientRect();
61
+ const popRect = popoverEl.getBoundingClientRect();
62
+ const viewportH = window.innerHeight;
63
+ const viewportW = window.innerWidth;
64
+
65
+ if (matchWidth) {
66
+ width = rect.width;
67
+ }
68
+
69
+ const isTop = placement.startsWith('top');
70
+ const isEnd = placement.endsWith('end');
71
+
72
+ let x = isEnd ? rect.right - (matchWidth ? rect.width : popRect.width) : rect.left;
73
+ let y = isTop ? rect.top - popRect.height - offset : rect.bottom + offset;
74
+
75
+ // Flip vertical if out of viewport
76
+ if (!isTop && y + popRect.height > viewportH) {
77
+ y = rect.top - popRect.height - offset;
78
+ } else if (isTop && y < 0) {
79
+ y = rect.bottom + offset;
80
+ }
81
+
82
+ // Clamp horizontal
83
+ if (x + popRect.width > viewportW) {
84
+ x = viewportW - popRect.width - offset;
85
+ }
86
+ if (x < 0) x = offset;
87
+
88
+ posX = x;
89
+ posY = y;
90
+ }
91
+
92
+ // Position + close listeners
93
+ $effect(() => {
94
+ if (!open || !popoverEl) return;
95
+
96
+ requestAnimationFrame(() => reposition());
97
+
98
+ /** @type {HTMLElement | null} */
99
+ const previouslyFocused = /** @type {HTMLElement | null} */ (document.activeElement);
100
+
101
+ // Focus first focusable child
102
+ requestAnimationFrame(() => {
103
+ const focusable = popoverEl?.querySelector(
104
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
105
+ );
106
+ if (focusable) /** @type {HTMLElement} */ (focusable).focus();
107
+ });
108
+
109
+ /** @param {KeyboardEvent} e */
110
+ function handleKeydown(e) {
111
+ if (e.key === 'Escape') {
112
+ e.stopPropagation();
113
+ open = false;
114
+ onclose?.();
115
+ }
116
+ }
117
+
118
+ /** @param {MouseEvent} e */
119
+ function handleClickOutside(e) {
120
+ const target = /** @type {Node} */ (e.target);
121
+ if (
122
+ popoverEl &&
123
+ !popoverEl.contains(target) &&
124
+ anchor &&
125
+ !anchor.contains(target)
126
+ ) {
127
+ open = false;
128
+ onclose?.();
129
+ }
130
+ }
131
+
132
+ function handleScroll() {
133
+ reposition();
134
+ }
135
+
136
+ document.addEventListener('keydown', handleKeydown);
137
+ document.addEventListener('mousedown', handleClickOutside);
138
+ window.addEventListener('scroll', handleScroll, true);
139
+ window.addEventListener('resize', reposition);
140
+
141
+ return () => {
142
+ document.removeEventListener('keydown', handleKeydown);
143
+ document.removeEventListener('mousedown', handleClickOutside);
144
+ window.removeEventListener('scroll', handleScroll, true);
145
+ window.removeEventListener('resize', reposition);
146
+ previouslyFocused?.focus();
147
+ };
148
+ });
149
+ </script>
150
+
151
+ {#if open}
152
+ <div
153
+ bind:this={popoverEl}
154
+ id={popoverId}
155
+ class="popover {className}"
156
+ role="dialog"
157
+ style:left="{posX}px"
158
+ style:top="{posY}px"
159
+ style:width={matchWidth ? `${width}px` : undefined}
160
+ {...rest}
161
+ >
162
+ {#if children}{@render children()}{/if}
163
+ </div>
164
+ {/if}
165
+
166
+ <style>
167
+ .popover {
168
+ position: fixed;
169
+ z-index: var(--popover-z);
170
+ background: var(--popover-bg);
171
+ border: var(--popover-border);
172
+ border-radius: var(--popover-radius);
173
+ box-shadow: var(--popover-shadow);
174
+ padding: var(--popover-padding);
175
+ animation: popover-enter var(--duration-fast) var(--easing-enter);
176
+ }
177
+
178
+ @keyframes popover-enter {
179
+ from {
180
+ opacity: 0;
181
+ transform: translateY(var(--popover-enter-offset));
182
+ }
183
+ to {
184
+ opacity: 1;
185
+ transform: translateY(0);
186
+ }
187
+ }
188
+
189
+ @media (prefers-reduced-motion: reduce) {
190
+ .popover {
191
+ animation: none;
192
+ }
193
+ }
194
+ </style>
@@ -0,0 +1,78 @@
1
+ <!--
2
+ @component Progress
3
+
4
+ Determinate or indeterminate progress bar.
5
+ Consumes --progress-* tokens from components.css.
6
+
7
+ @example Determinate
8
+ <Progress value={65} />
9
+
10
+ @example Indeterminate
11
+ <Progress indeterminate />
12
+ -->
13
+ <script>
14
+ let {
15
+ /** @type {number} */
16
+ value = 0,
17
+ /** @type {number} */
18
+ max = 100,
19
+ /** @type {boolean} */
20
+ indeterminate = false,
21
+ /** @type {string} */
22
+ class: className = '',
23
+ ...rest
24
+ } = $props();
25
+
26
+ const percentage = $derived(Math.min(Math.max((value / max) * 100, 0), 100));
27
+ </script>
28
+
29
+ <div
30
+ class="progress {className}"
31
+ class:progress-indeterminate={indeterminate}
32
+ role="progressbar"
33
+ aria-valuenow={indeterminate ? undefined : value}
34
+ aria-valuemin={0}
35
+ aria-valuemax={indeterminate ? undefined : max}
36
+ {...rest}
37
+ >
38
+ {#if indeterminate}
39
+ <div class="progress-fill progress-fill-indeterminate"></div>
40
+ {:else}
41
+ <div class="progress-fill" style:width="{percentage}%"></div>
42
+ {/if}
43
+ </div>
44
+
45
+ <style>
46
+ .progress {
47
+ height: var(--progress-height);
48
+ background: var(--progress-bg);
49
+ border-radius: var(--progress-radius);
50
+ overflow: hidden;
51
+ width: 100%;
52
+ }
53
+
54
+ .progress-fill {
55
+ height: 100%;
56
+ background: var(--progress-fill);
57
+ border-radius: var(--progress-radius);
58
+ transition: width var(--progress-transition);
59
+ }
60
+
61
+ .progress-fill-indeterminate {
62
+ width: 40%;
63
+ animation: progress-slide var(--progress-indeterminate-duration) var(--easing-linear) infinite;
64
+ }
65
+
66
+ @keyframes progress-slide {
67
+ 0% { transform: translateX(-100%); }
68
+ 100% { transform: translateX(350%); }
69
+ }
70
+
71
+ @media (prefers-reduced-motion: reduce) {
72
+ .progress-fill-indeterminate {
73
+ animation: none;
74
+ width: 100%;
75
+ opacity: 0.5;
76
+ }
77
+ }
78
+ </style>
@@ -0,0 +1,182 @@
1
+ <!--
2
+ @component Select
3
+
4
+ Native select with label, help text, and error state.
5
+ Consumes --input-* tokens from components.css.
6
+
7
+ @example
8
+ <Select label="COUNTRY" placeholder="Select a country" options={[
9
+ { value: 'pt', label: 'Portugal' },
10
+ { value: 'br', label: 'Brazil' },
11
+ ]} />
12
+ -->
13
+ <script module>
14
+ let _selectUid = 0;
15
+ </script>
16
+
17
+ <script>
18
+ /**
19
+ * @typedef {{ value: string, label: string, disabled?: boolean }} Option
20
+ * @typedef {'sm' | 'md' | 'lg'} Size
21
+ */
22
+
23
+ let {
24
+ /** @type {string | undefined} */
25
+ label = undefined,
26
+ /** @type {string | undefined} */
27
+ placeholder = undefined,
28
+ /** @type {string} */
29
+ value = $bindable(''),
30
+ /** @type {Option[]} */
31
+ options = [],
32
+ /** @type {string | undefined} */
33
+ help = undefined,
34
+ /** @type {string | undefined} */
35
+ error = undefined,
36
+ /** @type {Size} */
37
+ size = 'md',
38
+ /** @type {boolean} */
39
+ disabled = false,
40
+ /** @type {string | undefined} */
41
+ id = undefined,
42
+ /** @type {string} */
43
+ class: className = '',
44
+ ...rest
45
+ } = $props();
46
+
47
+ const fallbackId = `select-${_selectUid++}`;
48
+ const selectId = $derived(id ?? fallbackId);
49
+ const hintId = $derived(`${selectId}-hint`);
50
+ const hasHint = $derived(!!error || !!help);
51
+ </script>
52
+
53
+ <div class="input-group {className}">
54
+ {#if label}
55
+ <label class="input-label" for={selectId}>{label}</label>
56
+ {/if}
57
+
58
+ <div class="select-wrapper">
59
+ <select
60
+ id={selectId}
61
+ class="input select input-{size}"
62
+ class:input-error={!!error}
63
+ aria-invalid={error ? true : undefined}
64
+ aria-describedby={hasHint ? hintId : undefined}
65
+ {disabled}
66
+ bind:value
67
+ {...rest}
68
+ >
69
+ {#if placeholder}
70
+ <option value="" disabled>{placeholder}</option>
71
+ {/if}
72
+ {#each options as opt}
73
+ <option value={opt.value} disabled={opt.disabled}>{opt.label}</option>
74
+ {/each}
75
+ </select>
76
+ <span class="select-chevron" aria-hidden="true">
77
+ <svg width="10" height="6" viewBox="0 0 10 6" fill="none">
78
+ <path d="M1 1L5 5L9 1" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
79
+ </svg>
80
+ </span>
81
+ </div>
82
+
83
+ {#if error}
84
+ <span id={hintId} class="input-error-text" role="alert">{error}</span>
85
+ {:else if help}
86
+ <span id={hintId} class="input-help">{help}</span>
87
+ {/if}
88
+ </div>
89
+
90
+ <style>
91
+ .input-group {
92
+ display: flex;
93
+ flex-direction: column;
94
+ gap: var(--input-label-gap);
95
+ width: 100%;
96
+ }
97
+
98
+ .input-label {
99
+ font-family: var(--input-label-font);
100
+ font-size: var(--input-label-size);
101
+ letter-spacing: var(--input-label-tracking);
102
+ color: var(--input-label-color);
103
+ }
104
+
105
+ .input {
106
+ font-family: var(--input-font);
107
+ font-size: var(--input-font-size);
108
+ border: var(--input-border);
109
+ border-radius: var(--input-radius);
110
+ background: var(--input-bg);
111
+ color: var(--input-text);
112
+ transition: border var(--input-transition);
113
+ width: 100%;
114
+ }
115
+
116
+ .input-sm {
117
+ height: var(--input-sm-height);
118
+ padding: 0 var(--input-sm-padding-x);
119
+ }
120
+
121
+ .input-md {
122
+ height: var(--input-md-height);
123
+ padding: 0 var(--input-md-padding-x);
124
+ }
125
+
126
+ .input-lg {
127
+ height: var(--input-lg-height);
128
+ padding: 0 var(--input-lg-padding-x);
129
+ }
130
+
131
+ .input:focus {
132
+ outline: none;
133
+ border: var(--input-border-focus);
134
+ }
135
+
136
+ .input:disabled {
137
+ opacity: 0.5;
138
+ cursor: not-allowed;
139
+ }
140
+
141
+ .input-error {
142
+ border-color: var(--input-error-border-color);
143
+ }
144
+
145
+ .input-help {
146
+ font-family: var(--input-help-font);
147
+ font-size: var(--input-help-size);
148
+ color: var(--input-help-color);
149
+ }
150
+
151
+ .input-error-text {
152
+ font-family: var(--input-help-font);
153
+ font-size: var(--input-help-size);
154
+ color: var(--input-error-text);
155
+ }
156
+
157
+ .select {
158
+ appearance: none;
159
+ cursor: pointer;
160
+ padding-right: var(--space-xl);
161
+ }
162
+
163
+ .select-wrapper {
164
+ position: relative;
165
+ width: 100%;
166
+ }
167
+
168
+ .select-chevron {
169
+ position: absolute;
170
+ right: var(--input-md-padding-x);
171
+ top: 50%;
172
+ transform: translateY(-50%);
173
+ pointer-events: none;
174
+ display: flex;
175
+ color: var(--color-text-secondary);
176
+ }
177
+
178
+ .select-chevron svg {
179
+ width: 10px;
180
+ height: 6px;
181
+ }
182
+ </style>
@@ -0,0 +1,47 @@
1
+ <!--
2
+ @component Separator
3
+
4
+ Visual divider between content sections.
5
+ Consumes --separator-* tokens from components.css.
6
+
7
+ @example Horizontal
8
+ <Separator />
9
+
10
+ @example Vertical (inside a flex row)
11
+ <Separator orientation="vertical" />
12
+ -->
13
+ <script>
14
+ let {
15
+ /** @type {'horizontal' | 'vertical'} */
16
+ orientation = 'horizontal',
17
+ /** @type {boolean} */
18
+ decorative = true,
19
+ /** @type {string} */
20
+ class: className = '',
21
+ ...rest
22
+ } = $props();
23
+ </script>
24
+
25
+ <div
26
+ role={decorative ? 'none' : 'separator'}
27
+ aria-orientation={!decorative ? /** @type {'horizontal' | 'vertical'} */ (orientation) : undefined}
28
+ class="separator separator-{orientation} {className}"
29
+ {...rest}
30
+ ></div>
31
+
32
+ <style>
33
+ .separator {
34
+ flex-shrink: 0;
35
+ background: var(--separator-color);
36
+ }
37
+
38
+ .separator-horizontal {
39
+ height: var(--separator-thickness);
40
+ width: 100%;
41
+ }
42
+
43
+ .separator-vertical {
44
+ width: var(--separator-thickness);
45
+ height: 100%;
46
+ }
47
+ </style>
@@ -0,0 +1,106 @@
1
+ <!--
2
+ @component Sidebar
3
+
4
+ Vertical navigation container with collapsible support.
5
+ Children (SidebarItem, SidebarSection) read collapsed state via context.
6
+ Consumes --nav-sidebar-* tokens from components.css.
7
+
8
+ @example
9
+ <Sidebar bind:collapsed>
10
+ {#snippet header()}
11
+ <span class="type-label">AIAIAI</span>
12
+ {/snippet}
13
+ <SidebarSection title="WORKSPACE" />
14
+ <SidebarItem href="/dashboard" active>
15
+ {#snippet icon()}<DashboardIcon />{/snippet}
16
+ DASHBOARD
17
+ </SidebarItem>
18
+ </Sidebar>
19
+ -->
20
+ <script>
21
+ import { setContext } from 'svelte';
22
+
23
+ let {
24
+ /** @type {boolean} */
25
+ collapsed = $bindable(false),
26
+ /** @type {string} */
27
+ class: className = '',
28
+ /** @type {import('svelte').Snippet | undefined} */
29
+ header = undefined,
30
+ /** @type {import('svelte').Snippet | undefined} */
31
+ footer = undefined,
32
+ /** @type {import('svelte').Snippet | undefined} */
33
+ children = undefined,
34
+ ...rest
35
+ } = $props();
36
+
37
+ setContext('aiaiai-sidebar', {
38
+ get collapsed() { return collapsed; }
39
+ });
40
+ </script>
41
+
42
+ <aside
43
+ class="sidebar {className}"
44
+ class:sidebar-collapsed={collapsed}
45
+ {...rest}
46
+ >
47
+ {#if header}
48
+ <div class="sidebar-header">
49
+ {@render header()}
50
+ </div>
51
+ {/if}
52
+
53
+ <nav class="sidebar-nav">
54
+ {#if children}{@render children()}{/if}
55
+ </nav>
56
+
57
+ {#if footer}
58
+ <div class="sidebar-footer">
59
+ {@render footer()}
60
+ </div>
61
+ {/if}
62
+ </aside>
63
+
64
+ <style>
65
+ .sidebar {
66
+ width: var(--nav-sidebar-width);
67
+ min-width: var(--nav-sidebar-width);
68
+ background: var(--nav-sidebar-bg);
69
+ border-right: var(--nav-sidebar-border);
70
+ padding: var(--nav-sidebar-padding);
71
+ display: flex;
72
+ flex-direction: column;
73
+ gap: var(--space-sm);
74
+ transition: width var(--duration-slow) var(--easing-default),
75
+ min-width var(--duration-slow) var(--easing-default);
76
+ }
77
+
78
+ .sidebar-collapsed {
79
+ width: var(--nav-sidebar-width-collapsed);
80
+ min-width: var(--nav-sidebar-width-collapsed);
81
+ }
82
+
83
+ .sidebar-header {
84
+ display: flex;
85
+ align-items: baseline;
86
+ gap: var(--space-sm);
87
+ padding: var(--space-sm);
88
+ border-bottom: var(--elevation-border);
89
+ padding-bottom: var(--space-md);
90
+ flex-shrink: 0;
91
+ }
92
+
93
+ .sidebar-nav {
94
+ display: flex;
95
+ flex-direction: column;
96
+ gap: var(--border-width);
97
+ flex: 1;
98
+ overflow-y: auto;
99
+ }
100
+
101
+ .sidebar-footer {
102
+ flex-shrink: 0;
103
+ border-top: var(--elevation-border);
104
+ padding-top: var(--space-sm);
105
+ }
106
+ </style>