@dorsk/tsumikit 0.74.0 → 0.76.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 (132) hide show
  1. package/README.md +38 -1
  2. package/dist/autoresize.d.ts +2 -0
  3. package/dist/autoresize.js +8 -1
  4. package/dist/components/atoms/Artwork.svelte +12 -5
  5. package/dist/components/atoms/Avatar.svelte +1 -1
  6. package/dist/components/atoms/Badge.svelte +59 -23
  7. package/dist/components/atoms/Button.svelte +130 -96
  8. package/dist/components/atoms/Button.svelte.d.ts +2 -0
  9. package/dist/components/atoms/Card.svelte +10 -4
  10. package/dist/components/atoms/Checkbox.svelte +36 -9
  11. package/dist/components/atoms/Checkbox.svelte.d.ts +2 -0
  12. package/dist/components/atoms/Gauge.svelte +20 -14
  13. package/dist/components/atoms/Heading.svelte +1 -1
  14. package/dist/components/atoms/Icon.svelte +1 -0
  15. package/dist/components/atoms/Input.svelte +67 -13
  16. package/dist/components/atoms/Kbd.svelte +3 -3
  17. package/dist/components/atoms/Link.svelte +7 -2
  18. package/dist/components/atoms/Progress.svelte +4 -2
  19. package/dist/components/atoms/Scrim.svelte +3 -8
  20. package/dist/components/atoms/SegmentedProgress.svelte +5 -4
  21. package/dist/components/atoms/Select.svelte +58 -27
  22. package/dist/components/atoms/Slider.svelte +79 -22
  23. package/dist/components/atoms/Slider.svelte.d.ts +2 -0
  24. package/dist/components/atoms/Swatch.svelte +19 -8
  25. package/dist/components/atoms/Switch.svelte +34 -21
  26. package/dist/components/atoms/Switch.svelte.d.ts +2 -0
  27. package/dist/components/atoms/Text.svelte +1 -1
  28. package/dist/components/atoms/Textarea.svelte +34 -12
  29. package/dist/components/atoms/TimeDisplay.svelte +4 -2
  30. package/dist/components/layouts/AppShell.svelte +26 -15
  31. package/dist/components/layouts/MasterDetail.svelte +23 -21
  32. package/dist/components/layouts/NavBar.svelte +11 -8
  33. package/dist/components/layouts/NavItem.svelte +15 -7
  34. package/dist/components/layouts/NavSection.svelte +1 -1
  35. package/dist/components/layouts/ResizablePanel.svelte +37 -16
  36. package/dist/components/layouts/TabBar.svelte +24 -14
  37. package/dist/components/layouts/TabBar.svelte.d.ts +2 -0
  38. package/dist/components/molecules/Accordion.svelte +4 -0
  39. package/dist/components/molecules/Accordion.svelte.d.ts +2 -0
  40. package/dist/components/molecules/AttachmentList.svelte +14 -11
  41. package/dist/components/molecules/Breadcrumb.svelte +13 -9
  42. package/dist/components/molecules/Callout.svelte +3 -3
  43. package/dist/components/molecules/CapBar.svelte +23 -22
  44. package/dist/components/molecules/Carousel.svelte +27 -17
  45. package/dist/components/molecules/ChatBubble.svelte +2 -2
  46. package/dist/components/molecules/CodeBlock.svelte +2 -2
  47. package/dist/components/molecules/Combobox.svelte +30 -1
  48. package/dist/components/molecules/Combobox.svelte.d.ts +4 -0
  49. package/dist/components/molecules/Composer.svelte +2 -2
  50. package/dist/components/molecules/ConfirmModal.svelte +4 -0
  51. package/dist/components/molecules/ConfirmModal.svelte.d.ts +2 -0
  52. package/dist/components/molecules/CopyButton.svelte +46 -2
  53. package/dist/components/molecules/CopyButton.svelte.d.ts +2 -0
  54. package/dist/components/molecules/Disclosure.svelte +23 -12
  55. package/dist/components/molecules/Disclosure.svelte.d.ts +2 -0
  56. package/dist/components/molecules/Drawer.svelte +43 -23
  57. package/dist/components/molecules/Drawer.svelte.d.ts +2 -0
  58. package/dist/components/molecules/Dropzone.svelte +24 -8
  59. package/dist/components/molecules/EmojiPicker.svelte +27 -8
  60. package/dist/components/molecules/EmojiPicker.svelte.d.ts +2 -0
  61. package/dist/components/molecules/EmptyState.svelte +5 -5
  62. package/dist/components/molecules/Fieldset.svelte +8 -13
  63. package/dist/components/molecules/FileButton.svelte +57 -31
  64. package/dist/components/molecules/FileButton.svelte.d.ts +2 -0
  65. package/dist/components/molecules/FilterInput.svelte +51 -11
  66. package/dist/components/molecules/FontScalePicker.svelte +20 -9
  67. package/dist/components/molecules/GitRef.svelte +9 -7
  68. package/dist/components/molecules/GlyphStack.svelte +46 -14
  69. package/dist/components/molecules/IconButton.svelte +6 -3
  70. package/dist/components/molecules/IconButton.svelte.d.ts +2 -0
  71. package/dist/components/molecules/InputGroup.svelte +13 -12
  72. package/dist/components/molecules/LoadMore.svelte +6 -1
  73. package/dist/components/molecules/LoadMore.svelte.d.ts +2 -0
  74. package/dist/components/molecules/Menu.svelte +33 -7
  75. package/dist/components/molecules/Menu.svelte.d.ts +2 -0
  76. package/dist/components/molecules/Metric.svelte +11 -9
  77. package/dist/components/molecules/Modal.svelte +76 -26
  78. package/dist/components/molecules/Modal.svelte.d.ts +2 -0
  79. package/dist/components/molecules/OptionButton.svelte +28 -10
  80. package/dist/components/molecules/OptionButton.svelte.d.ts +2 -0
  81. package/dist/components/molecules/Pagination.svelte +26 -9
  82. package/dist/components/molecules/Pagination.svelte.d.ts +2 -0
  83. package/dist/components/molecules/Popover.svelte +107 -51
  84. package/dist/components/molecules/Popover.svelte.d.ts +2 -0
  85. package/dist/components/molecules/Prose.svelte +1 -1
  86. package/dist/components/molecules/PullToRefresh.svelte +14 -10
  87. package/dist/components/molecules/PullToRefresh.svelte.d.ts +2 -0
  88. package/dist/components/molecules/RadialMenu.svelte +52 -19
  89. package/dist/components/molecules/RadialMenu.svelte.d.ts +2 -0
  90. package/dist/components/molecules/RadioGroup.svelte +38 -23
  91. package/dist/components/molecules/RadioGroup.svelte.d.ts +2 -0
  92. package/dist/components/molecules/RichSelect.svelte +583 -0
  93. package/dist/components/molecules/RichSelect.svelte.d.ts +68 -0
  94. package/dist/components/molecules/SectionHeader.svelte +13 -8
  95. package/dist/components/molecules/SeekSlider.svelte +38 -27
  96. package/dist/components/molecules/SeekSlider.svelte.d.ts +2 -0
  97. package/dist/components/molecules/SegmentedControl.svelte +139 -86
  98. package/dist/components/molecules/SegmentedControl.svelte.d.ts +2 -0
  99. package/dist/components/molecules/SelectButton.svelte +21 -6
  100. package/dist/components/molecules/SelectButton.svelte.d.ts +2 -0
  101. package/dist/components/molecules/SortableList.svelte +10 -11
  102. package/dist/components/molecules/SoundSettings.svelte +161 -0
  103. package/dist/components/molecules/SoundSettings.svelte.d.ts +17 -0
  104. package/dist/components/molecules/SplitButton.svelte +30 -3
  105. package/dist/components/molecules/SplitButton.svelte.d.ts +2 -0
  106. package/dist/components/molecules/SwipeRow.svelte +18 -15
  107. package/dist/components/molecules/SwipeRow.svelte.d.ts +2 -0
  108. package/dist/components/molecules/Tabs.svelte +92 -10
  109. package/dist/components/molecules/Tabs.svelte.d.ts +2 -0
  110. package/dist/components/molecules/ThemePicker.svelte +25 -10
  111. package/dist/components/molecules/Timestamp.svelte +12 -7
  112. package/dist/components/molecules/Toaster.svelte +65 -14
  113. package/dist/components/molecules/Toaster.svelte.d.ts +2 -0
  114. package/dist/components/molecules/Toggle.svelte +36 -13
  115. package/dist/components/molecules/Toggle.svelte.d.ts +2 -0
  116. package/dist/components/molecules/Tooltip.svelte +41 -10
  117. package/dist/components/molecules/Tooltip.svelte.d.ts +1 -1
  118. package/dist/components/molecules/VolumeSlider.svelte +34 -14
  119. package/dist/components/molecules/VolumeSlider.svelte.d.ts +2 -0
  120. package/dist/components/molecules/WorkingDir.svelte +1 -0
  121. package/dist/components/molecules/rich-select-keyboard.d.ts +82 -0
  122. package/dist/components/molecules/rich-select-keyboard.js +114 -0
  123. package/dist/components/organisms/ConversationFeed.svelte +24 -1
  124. package/dist/components/organisms/DataTable.svelte +37 -14
  125. package/dist/floating.js +26 -0
  126. package/dist/index.d.ts +2 -0
  127. package/dist/index.js +2 -0
  128. package/dist/sound-synth.js +17 -12
  129. package/dist/stores/toast.svelte.d.ts +9 -0
  130. package/dist/stores/toast.svelte.js +45 -9
  131. package/dist/styles/reset.css +3 -3
  132. package/package.json +1 -1
@@ -7,6 +7,7 @@
7
7
  import Button from '../atoms/Button.svelte';
8
8
  import Icon from '../atoms/Icon.svelte';
9
9
  import { copyToClipboard } from '../../clipboard';
10
+ import { playSound } from '../../sound';
10
11
 
11
12
  let {
12
13
  text,
@@ -17,6 +18,7 @@
17
18
  box,
18
19
  hitArea = 'auto',
19
20
  resetMs = 1500,
21
+ sound = true,
20
22
  class: klass = ''
21
23
  }: {
22
24
  /** The string to copy. */
@@ -30,6 +32,8 @@
30
32
  box?: 'xs' | 'sm' | 'md' | 'lg';
31
33
  hitArea?: 'auto' | 'compact';
32
34
  resetMs?: number;
35
+ /** Play UI sounds when enabled globally; false silences this instance. */
36
+ sound?: boolean;
33
37
  class?: string;
34
38
  } = $props();
35
39
 
@@ -40,6 +44,7 @@
40
44
 
41
45
  async function copy() {
42
46
  const ok = await copyToClipboard(text);
47
+ playSound(ok ? 'success' : 'error', sound);
43
48
  copied = ok;
44
49
  status = ok ? copiedLabel : 'Copy failed';
45
50
  clearTimeout(timer);
@@ -55,12 +60,51 @@
55
60
  {variant}
56
61
  {box}
57
62
  {hitArea}
58
- class={klass}
63
+ class="copy-btn {klass}"
64
+ sound={false}
59
65
  onclick={copy}
60
66
  aria-label={copied ? copiedLabel : label}
61
67
  title={label}
62
68
  >
63
- <Icon name={copied ? 'check' : 'copy'} />
69
+ <span class="glyphs" class:copied aria-hidden="true">
70
+ <span class="glyph copy"><Icon name="copy" /></span>
71
+ <span class="glyph check"><Icon name="check" /></span>
72
+ </span>
64
73
  {#if labelShown}<span>{copied ? copiedLabel : label}</span>{/if}
65
74
  </Button>
66
75
  <span class="sr-only" role="status" aria-live="polite">{status}</span>
76
+
77
+ <style>
78
+ /* Both glyphs share one grid cell so the swap never changes the width. The
79
+ check springs in (the kit's one bounce: a rare confirmation); the copy
80
+ glyph fades out fast and back in over the base duration on revert. */
81
+ .glyphs {
82
+ display: inline-grid;
83
+ place-items: center;
84
+ }
85
+ .glyph {
86
+ grid-area: 1 / 1;
87
+ display: inline-flex;
88
+ align-items: center;
89
+ justify-content: center;
90
+ transition: opacity var(--dur-fast) ease;
91
+ }
92
+ .check {
93
+ opacity: 0;
94
+ scale: 0.6;
95
+ transition:
96
+ opacity var(--dur-fast) ease,
97
+ scale var(--dur-fast) var(--ease-out);
98
+ }
99
+ .copied .copy {
100
+ opacity: 0;
101
+ transition-duration: var(--dur-exit-fast);
102
+ }
103
+ .copied .check {
104
+ opacity: 1;
105
+ scale: 1;
106
+ transition:
107
+ opacity var(--dur-spring-bounce) ease,
108
+ scale var(--dur-spring-bounce) var(--ease-spring-bounce);
109
+ }
110
+ </style>
@@ -10,6 +10,8 @@ type $$ComponentProps = {
10
10
  box?: 'xs' | 'sm' | 'md' | 'lg';
11
11
  hitArea?: 'auto' | 'compact';
12
12
  resetMs?: number;
13
+ /** Play UI sounds when enabled globally; false silences this instance. */
14
+ sound?: boolean;
13
15
  class?: string;
14
16
  };
15
17
  declare const CopyButton: import("svelte").Component<$$ComponentProps, {}, "">;
@@ -13,6 +13,7 @@
13
13
  import type { Snippet } from 'svelte';
14
14
  import type { HTMLAttributes } from 'svelte/elements';
15
15
  import Icon from '../atoms/Icon.svelte';
16
+ import { playSound } from '../../sound';
16
17
 
17
18
  let {
18
19
  open = $bindable(false),
@@ -23,6 +24,7 @@
23
24
  id,
24
25
  onchange,
25
26
  disabled = false,
27
+ sound = true,
26
28
  class: klass = '',
27
29
  style: styleProp = '',
28
30
  buttonClass = '',
@@ -45,6 +47,8 @@
45
47
  id?: string;
46
48
  onchange?: (open: boolean) => void;
47
49
  disabled?: boolean;
50
+ /** Play UI sounds when enabled globally; false silences this instance. */
51
+ sound?: boolean;
48
52
  class?: string;
49
53
  style?: string;
50
54
  buttonClass?: string;
@@ -58,6 +62,7 @@
58
62
 
59
63
  function toggle() {
60
64
  open = !open;
65
+ playSound(open ? 'open' : 'close', sound);
61
66
  onchange?.(open);
62
67
  }
63
68
  </script>
@@ -119,16 +124,18 @@
119
124
  cursor: pointer;
120
125
  user-select: none;
121
126
  }
122
- .disclosure__button:hover:not(:disabled) {
123
- background: var(--bg-elevated-2);
127
+ @media (hover: hover) and (pointer: fine) {
128
+ .disclosure__button:hover:not(:disabled) {
129
+ background: var(--bg-elevated-2);
130
+ }
124
131
  }
125
132
  .disclosure__button:focus-visible {
126
- outline: 2px solid var(--accent);
127
- outline-offset: -2px;
133
+ outline: var(--focus-ring);
134
+ outline-offset: var(--focus-ring-offset-inset);
128
135
  }
129
136
  .disclosure__button:disabled {
130
137
  cursor: not-allowed;
131
- opacity: 0.6;
138
+ opacity: var(--disabled-opacity);
132
139
  }
133
140
  .disclosure__header {
134
141
  display: flex;
@@ -141,20 +148,24 @@
141
148
  display: inline-flex;
142
149
  flex: none;
143
150
  color: var(--text-muted);
144
- transition: transform 0.15s var(--ease);
151
+ transition: rotate var(--dur-base) var(--ease-out);
145
152
  }
146
153
  .disclosure--open .disclosure__chevron {
147
- transform: rotate(180deg);
148
- }
149
- @media (prefers-reduced-motion: reduce) {
150
- .disclosure__chevron {
151
- transition: none;
152
- }
154
+ rotate: 180deg;
153
155
  }
154
156
  .disclosure__panel {
155
157
  padding: var(--disclosure-panel-pad, 0 var(--sp-4) var(--sp-4));
156
158
  font-size: var(--disclosure-fs, var(--fs-sm));
157
159
  color: var(--text-muted);
160
+ transition:
161
+ opacity var(--dur-base) var(--ease-out),
162
+ translate var(--dur-base) var(--ease-out);
163
+ }
164
+ @starting-style {
165
+ .disclosure__panel:not([hidden]) {
166
+ opacity: 0;
167
+ translate: 0 calc(-1 * var(--shift-xs));
168
+ }
158
169
  }
159
170
  .disclosure--compact {
160
171
  --disclosure-pad: var(--sp-1) var(--sp-2);
@@ -20,6 +20,8 @@ type Own = {
20
20
  id?: string;
21
21
  onchange?: (open: boolean) => void;
22
22
  disabled?: boolean;
23
+ /** Play UI sounds when enabled globally; false silences this instance. */
24
+ sound?: boolean;
23
25
  class?: string;
24
26
  style?: string;
25
27
  buttonClass?: string;
@@ -6,6 +6,7 @@
6
6
  import { browser } from '../../env';
7
7
  import Icon, { type IconName } from '../atoms/Icon.svelte';
8
8
  import IconButton from './IconButton.svelte';
9
+ import { playSound } from '../../sound';
9
10
 
10
11
  let {
11
12
  open = $bindable(),
@@ -23,7 +24,8 @@
23
24
  children,
24
25
  footer,
25
26
  class: className,
26
- style
27
+ style,
28
+ sound = true
27
29
  }: {
28
30
  /** Controlled visibility. When provided the `<dialog>` stays mounted and
29
31
  * `showModal()`/`close()` follow the value; closing sets it back to false.
@@ -53,6 +55,8 @@
53
55
  footer?: Snippet;
54
56
  class?: string;
55
57
  style?: string;
58
+ /** Play UI sounds when enabled globally; false silences this instance. */
59
+ sound?: boolean;
56
60
  } = $props();
57
61
 
58
62
  const titleId = `drawer-title-${Math.random().toString(36).slice(2, 8)}`;
@@ -61,14 +65,20 @@
61
65
  const controlled = $derived(open !== undefined);
62
66
  const shown = $derived(!controlled || open === true);
63
67
 
68
+ // Mounting (or mounting with `open` already true) is a sync, not a
69
+ // transition, so it stays silent.
70
+ let synced = false;
64
71
  $effect(() => {
65
72
  if (!dialogEl) return;
66
73
  if (!controlled) {
67
74
  dialogEl.showModal();
68
75
  return;
69
76
  }
70
- if (open && !dialogEl.open) dialogEl.showModal();
71
- else if (!open && dialogEl.open) dialogEl.close();
77
+ if (open && !dialogEl.open) {
78
+ dialogEl.showModal();
79
+ if (synced) playSound('open', sound);
80
+ } else if (!open && dialogEl.open) dialogEl.close();
81
+ synced = true;
72
82
  });
73
83
 
74
84
  $effect(() => {
@@ -82,6 +92,7 @@
82
92
 
83
93
  function requestClose() {
84
94
  if (controlled) open = false;
95
+ else playSound('close', sound);
85
96
  onclose?.();
86
97
  }
87
98
 
@@ -111,6 +122,7 @@
111
122
  }}
112
123
  onclose={() => {
113
124
  if (controlled) open = false;
125
+ playSound('close', sound);
114
126
  }}
115
127
  onpointerdown={onDialogPointerDown}
116
128
  onclick={onDialogClick}
@@ -126,7 +138,7 @@
126
138
  {@render header()}
127
139
  {:else}
128
140
  <div class="panel-head">
129
- {#if icon}<span class="panel-icon"><Icon name={icon} size={18} /></span>{/if}
141
+ {#if icon}<span class="panel-icon"><Icon name={icon} /></span>{/if}
130
142
  <span id={titleId} class="panel-title truncate">{title}</span>
131
143
  {#if page}<span class="panel-page truncate">{page}</span>{/if}
132
144
  <div class="spacer"></div>
@@ -163,6 +175,10 @@
163
175
  color: var(--text);
164
176
  display: flex;
165
177
  justify-content: flex-end;
178
+ /* Exits run natively on close(): the box leaves the top layer once the panel is gone. */
179
+ transition:
180
+ display var(--dur-exit-slow) allow-discrete,
181
+ overlay var(--dur-exit-slow) allow-discrete;
166
182
  }
167
183
  .drawer:not([open]) {
168
184
  display: none;
@@ -172,10 +188,18 @@
172
188
  }
173
189
  .drawer::backdrop {
174
190
  background: rgba(0, 0, 0, 0.55);
175
- animation: drawer-backdrop-in 0.18s var(--ease);
191
+ opacity: 0;
192
+ transition:
193
+ opacity var(--dur-exit) var(--ease-out),
194
+ display var(--dur-exit) allow-discrete,
195
+ overlay var(--dur-exit) allow-discrete;
196
+ }
197
+ .drawer[open]::backdrop {
198
+ opacity: 1;
199
+ transition-duration: var(--dur-base);
176
200
  }
177
- @keyframes drawer-backdrop-in {
178
- from {
201
+ @starting-style {
202
+ .drawer[open]::backdrop {
179
203
  opacity: 0;
180
204
  }
181
205
  }
@@ -192,28 +216,23 @@
192
216
  width: 100%;
193
217
  height: 100%;
194
218
  background: var(--bg-elevated);
195
- will-change: transform;
219
+ will-change: translate;
196
220
  padding-top: var(--safe-top);
197
221
  padding-bottom: var(--safe-bottom);
198
- animation: drawer-slide-right 0.2s var(--ease);
222
+ --drawer-from: 100% 0;
223
+ translate: var(--drawer-from);
224
+ transition: translate var(--dur-exit-slow) var(--ease-drawer);
199
225
  }
200
226
  .drawer[data-side='left'] .panel {
201
- animation-name: drawer-slide-left;
202
- }
203
- @keyframes drawer-slide-right {
204
- from {
205
- transform: translateX(100%);
206
- }
227
+ --drawer-from: -100% 0;
207
228
  }
208
- @keyframes drawer-slide-left {
209
- from {
210
- transform: translateX(-100%);
211
- }
229
+ .drawer[open] .panel {
230
+ translate: 0;
231
+ transition-duration: var(--dur-sheet);
212
232
  }
213
- @media (prefers-reduced-motion: reduce) {
214
- .drawer::backdrop,
215
- .panel {
216
- animation: none;
233
+ @starting-style {
234
+ .drawer[open] .panel {
235
+ translate: var(--drawer-from);
217
236
  }
218
237
  }
219
238
 
@@ -228,6 +247,7 @@
228
247
  .panel-icon {
229
248
  display: inline-flex;
230
249
  flex: none;
250
+ font-size: var(--icon-lg);
231
251
  color: var(--text-muted);
232
252
  }
233
253
  .panel-title {
@@ -29,6 +29,8 @@ type $$ComponentProps = {
29
29
  footer?: Snippet;
30
30
  class?: string;
31
31
  style?: string;
32
+ /** Play UI sounds when enabled globally; false silences this instance. */
33
+ sound?: boolean;
32
34
  };
33
35
  declare const Drawer: import("svelte").Component<$$ComponentProps, {}, "open">;
34
36
  type Drawer = ReturnType<typeof Drawer>;
@@ -122,7 +122,7 @@
122
122
  {@render children?.()}
123
123
  {#if over}
124
124
  <div class="dz-overlay" aria-hidden="true">
125
- <Icon name="upload" size={28} />
125
+ <Icon name="upload" />
126
126
  <span class="dz-label">{label}</span>
127
127
  </div>
128
128
  {/if}
@@ -163,7 +163,7 @@
163
163
  {#if children}
164
164
  {@render children()}
165
165
  {:else}
166
- <Icon name="upload" size={28} />
166
+ <Icon name="upload" />
167
167
  <span class="dz-label">{label}</span>
168
168
  <span class="dz-hint">{hint}</span>
169
169
  {/if}
@@ -185,15 +185,25 @@
185
185
  border-radius: var(--r-lg);
186
186
  cursor: pointer;
187
187
  transition:
188
- border-color 0.12s var(--ease),
189
- background 0.12s var(--ease),
190
- color 0.12s var(--ease);
188
+ border-color var(--dur-fast) ease,
189
+ background-color var(--dur-fast) ease,
190
+ color var(--dur-fast) ease;
191
+ }
192
+ .dz > :global(svg),
193
+ .dz-overlay > :global(svg) {
194
+ font-size: var(--icon-xl);
191
195
  }
192
- .dz:hover:not(.disabled),
193
196
  .dz:focus-visible {
194
197
  border-color: var(--accent);
195
198
  color: var(--text);
196
- outline: none;
199
+ outline: var(--focus-ring);
200
+ outline-offset: var(--focus-ring-offset);
201
+ }
202
+ @media (hover: hover) and (pointer: fine) {
203
+ .dz:hover:not(.disabled) {
204
+ border-color: var(--accent);
205
+ color: var(--text);
206
+ }
197
207
  }
198
208
  .dz.over {
199
209
  border-color: var(--accent);
@@ -201,7 +211,7 @@
201
211
  color: var(--text);
202
212
  }
203
213
  .dz.disabled {
204
- opacity: 0.45;
214
+ opacity: var(--disabled-opacity);
205
215
  cursor: not-allowed;
206
216
  }
207
217
 
@@ -233,6 +243,12 @@
233
243
  border-radius: var(--r-lg);
234
244
  pointer-events: none;
235
245
  backdrop-filter: blur(1px);
246
+ transition: opacity var(--dur-fast) ease;
247
+ }
248
+ @starting-style {
249
+ .dz-overlay {
250
+ opacity: 0;
251
+ }
236
252
  }
237
253
 
238
254
  .dz-label {
@@ -5,6 +5,7 @@
5
5
  import Input from '../atoms/Input.svelte';
6
6
  import Popover from './Popover.svelte';
7
7
  import { type EmojiGroup, EMOJI_GROUPS, searchEmoji } from '../../emoji';
8
+ import { playSound } from '../../sound';
8
9
 
9
10
  let {
10
11
  value = '',
@@ -17,6 +18,7 @@
17
18
  placement = 'bottom-start',
18
19
  triggerClass = '',
19
20
  disabled = false,
21
+ sound = true,
20
22
  class: klass = '',
21
23
  style: styleProp = '',
22
24
  }: {
@@ -33,6 +35,8 @@
33
35
  placement?: 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end';
34
36
  triggerClass?: string;
35
37
  disabled?: boolean;
38
+ /** Play UI sounds when enabled globally; false silences this instance. */
39
+ sound?: boolean;
36
40
  class?: string;
37
41
  style?: string;
38
42
  } = $props();
@@ -110,10 +114,13 @@
110
114
  style={styleProp}
111
115
  control
112
116
  role="dialog"
117
+ {sound}
113
118
  onopen={() => {
114
119
  query = '';
115
120
  cursor = 0;
121
+ playSound('open', sound);
116
122
  }}
123
+ onclose={() => playSound('close', sound)}
117
124
  >
118
125
  {#snippet trigger()}
119
126
  <span class="emoji-trigger" aria-hidden="true">{picked || '🙂'}</span>
@@ -198,7 +205,7 @@
198
205
  <style>
199
206
  .emoji-trigger {
200
207
  font-size: var(--fs-md);
201
- line-height: 1;
208
+ line-height: var(--lh-none);
202
209
  }
203
210
  .picker {
204
211
  display: flex;
@@ -221,24 +228,36 @@
221
228
  background: none;
222
229
  color: inherit;
223
230
  font-size: var(--fs-lg);
224
- line-height: 1;
231
+ line-height: var(--lh-none);
225
232
  cursor: pointer;
233
+ --cell-scale-dur: var(--dur-fast);
234
+ transition:
235
+ background-color var(--dur-fast) ease,
236
+ border-color var(--dur-fast) ease,
237
+ scale var(--cell-scale-dur) var(--ease-out);
226
238
  }
227
239
  .tab.on,
228
240
  .cell.on {
229
241
  border-color: var(--accent);
230
242
  background: var(--bg-elevated-2);
231
243
  }
232
- .tab:hover,
233
- .cell:hover,
234
244
  .tab:focus-visible,
235
245
  .cell:focus-visible {
236
246
  background: var(--bg-elevated-2);
237
- }
238
- .tab:focus-visible,
239
- .cell:focus-visible {
240
247
  outline: var(--focus-ring);
241
- outline-offset: var(--focus-ring-offset);
248
+ outline-offset: var(--focus-ring-offset-inset);
249
+ }
250
+ .tab:active,
251
+ .cell:active {
252
+ --cell-scale-dur: var(--dur-press);
253
+ scale: var(--press-scale-sm);
254
+ }
255
+ @media (hover: hover) and (pointer: fine) {
256
+ .tab:hover,
257
+ .cell:hover {
258
+ background: var(--bg-elevated-2);
259
+ transition-duration: var(--dur-instant);
260
+ }
242
261
  }
243
262
  .grid {
244
263
  display: grid;
@@ -13,6 +13,8 @@ type $$ComponentProps = {
13
13
  placement?: 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end';
14
14
  triggerClass?: string;
15
15
  disabled?: boolean;
16
+ /** Play UI sounds when enabled globally; false silences this instance. */
17
+ sound?: boolean;
16
18
  class?: string;
17
19
  style?: string;
18
20
  };
@@ -132,7 +132,7 @@
132
132
  width: 3rem;
133
133
  height: 3rem;
134
134
  border-radius: var(--r-lg);
135
- font-size: 1.5rem;
135
+ font-size: var(--fs-xl);
136
136
  color: var(--empty-tone);
137
137
  background: color-mix(in srgb, var(--empty-tone) 14%, transparent);
138
138
  border: 1px solid color-mix(in srgb, var(--empty-tone) 30%, transparent);
@@ -140,7 +140,7 @@
140
140
  .empty-compact .empty-chip {
141
141
  width: 2.25rem;
142
142
  height: 2.25rem;
143
- font-size: 1.125rem;
143
+ font-size: var(--fs-lg);
144
144
  }
145
145
  .empty-inline {
146
146
  flex-direction: row;
@@ -151,7 +151,7 @@
151
151
  .empty-inline .empty-chip {
152
152
  width: auto;
153
153
  height: auto;
154
- font-size: 1em;
154
+ font-size: var(--icon-md);
155
155
  background: none;
156
156
  border: none;
157
157
  }
@@ -171,14 +171,14 @@
171
171
  font-size: var(--fs-md);
172
172
  font-weight: var(--fw-semibold);
173
173
  color: var(--text);
174
- line-height: 1.3;
174
+ line-height: var(--lh-snug);
175
175
  }
176
176
  .empty-desc {
177
177
  margin: 0;
178
178
  max-width: 32ch;
179
179
  font-size: var(--fs-sm);
180
180
  color: var(--text-muted);
181
- line-height: 1.5;
181
+ line-height: var(--lh-normal);
182
182
  }
183
183
  .empty-action {
184
184
  margin-top: var(--sp-1);
@@ -129,8 +129,8 @@
129
129
  border-radius: var(--r-lg);
130
130
  background: var(--bg);
131
131
  transition:
132
- border-color 0.12s var(--ease),
133
- background 0.12s var(--ease);
132
+ border-color var(--dur-fast) ease,
133
+ background-color var(--dur-fast) ease;
134
134
  }
135
135
  .fieldset.dashed {
136
136
  border-style: dashed;
@@ -151,16 +151,16 @@
151
151
  .fieldset-legend {
152
152
  position: absolute;
153
153
  top: -13px;
154
- left: 16px;
154
+ left: var(--sp-4);
155
155
  display: inline-flex;
156
156
  align-items: center;
157
157
  gap: var(--sp-2);
158
- max-width: calc(100% - 32px);
159
- padding: 0 8px;
158
+ max-width: calc(100% - var(--sp-8));
159
+ padding: 0 var(--sp-2);
160
160
  background: var(--bg);
161
161
  font-size: var(--fs-sm);
162
162
  font-weight: var(--fw-semibold);
163
- line-height: 1.5;
163
+ line-height: var(--lh-normal);
164
164
  color: var(--text);
165
165
  }
166
166
 
@@ -182,12 +182,7 @@
182
182
  pointer-events: none;
183
183
  }
184
184
  .fieldset.disabled {
185
- opacity: 0.6;
186
- }
187
-
188
- @media (prefers-reduced-motion: reduce) {
189
- .fieldset {
190
- transition: none;
191
- }
185
+ opacity: var(--disabled-opacity);
186
+ cursor: not-allowed;
192
187
  }
193
188
  </style>