@sveltia/ui 0.72.2 → 0.73.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 (89) hide show
  1. package/dist/components/alert/alert.svelte +29 -5
  2. package/dist/components/alert/alert.svelte.d.ts +8 -4
  3. package/dist/components/alert/infobar.svelte +21 -5
  4. package/dist/components/alert/infobar.svelte.d.ts +10 -4
  5. package/dist/components/button/button.svelte +5 -1
  6. package/dist/components/button/select-button-group.svelte +1 -1
  7. package/dist/components/calendar/calendar.d.ts +1 -4
  8. package/dist/components/calendar/calendar.js +17 -0
  9. package/dist/components/calendar/calendar.svelte +67 -11
  10. package/dist/components/checkbox/checkbox-group.svelte +3 -1
  11. package/dist/components/checkbox/checkbox.svelte +3 -1
  12. package/dist/components/dialog/dialog.svelte +14 -2
  13. package/dist/components/dialog/prompt-dialog.svelte +9 -2
  14. package/dist/components/dialog/prompt-dialog.svelte.d.ts +4 -2
  15. package/dist/components/disclosure/disclosure.svelte +2 -1
  16. package/dist/components/drawer/drawer.svelte +28 -3
  17. package/dist/components/drawer/drawer.svelte.d.ts +16 -2
  18. package/dist/components/grid/grid-body.svelte +3 -1
  19. package/dist/components/grid/grid-foot.svelte +3 -1
  20. package/dist/components/grid/grid-head.svelte +3 -1
  21. package/dist/components/listbox/option-group.svelte +2 -1
  22. package/dist/components/listbox/option.svelte +4 -0
  23. package/dist/components/menu/menu-item-group.svelte +3 -1
  24. package/dist/components/radio/radio.svelte +21 -3
  25. package/dist/components/radio/radio.svelte.d.ts +12 -2
  26. package/dist/components/resizable-pane/resizable-handle.svelte +12 -0
  27. package/dist/components/select/combobox.svelte +69 -1
  28. package/dist/components/select/select-tags.svelte +34 -23
  29. package/dist/components/slider/slider.d.ts +6 -0
  30. package/dist/components/slider/slider.js +40 -0
  31. package/dist/components/slider/slider.svelte +45 -11
  32. package/dist/components/slider/slider.svelte.d.ts +14 -2
  33. package/dist/components/switch/switch.svelte +1 -1
  34. package/dist/components/table/table-body.svelte +3 -1
  35. package/dist/components/table/table-foot.svelte +3 -1
  36. package/dist/components/table/table-head.svelte +3 -1
  37. package/dist/components/text-editor/code-editor.svelte +22 -1
  38. package/dist/components/text-editor/code-editor.svelte.d.ts +28 -0
  39. package/dist/components/text-editor/shiki/version.d.ts +1 -1
  40. package/dist/components/text-editor/shiki/version.js +1 -1
  41. package/dist/components/text-editor/text-editor.svelte +23 -0
  42. package/dist/components/text-editor/text-editor.svelte.d.ts +28 -0
  43. package/dist/components/text-field/number-input.svelte +7 -17
  44. package/dist/components/text-field/search-bar.svelte +1 -0
  45. package/dist/components/text-field/text-input.svelte +1 -1
  46. package/dist/components/toast/toast.svelte +58 -4
  47. package/dist/components/toast/toast.svelte.d.ts +4 -2
  48. package/dist/components/util/app-shell.svelte +289 -5
  49. package/dist/components/util/popup.svelte +1 -1
  50. package/dist/locales/ar.yaml +20 -0
  51. package/dist/locales/bg.yaml +20 -0
  52. package/dist/locales/ca.yaml +20 -0
  53. package/dist/locales/cs.yaml +20 -0
  54. package/dist/locales/da.yaml +20 -0
  55. package/dist/locales/de.yaml +20 -0
  56. package/dist/locales/el.yaml +20 -0
  57. package/dist/locales/en-CA.yaml +20 -0
  58. package/dist/locales/en-GB.yaml +20 -0
  59. package/dist/locales/en-US.yaml +20 -0
  60. package/dist/locales/es-CO.yaml +20 -0
  61. package/dist/locales/fa.yaml +20 -0
  62. package/dist/locales/fi.yaml +20 -0
  63. package/dist/locales/fr.yaml +20 -0
  64. package/dist/locales/hr.yaml +20 -0
  65. package/dist/locales/it.yaml +20 -0
  66. package/dist/locales/ja.yaml +20 -0
  67. package/dist/locales/ko.yaml +20 -0
  68. package/dist/locales/nl.yaml +20 -0
  69. package/dist/locales/pl.yaml +20 -0
  70. package/dist/locales/pt-BR.yaml +20 -0
  71. package/dist/locales/pt-PT.yaml +20 -0
  72. package/dist/locales/ru.yaml +20 -0
  73. package/dist/locales/sv.yaml +20 -0
  74. package/dist/locales/tr.yaml +20 -0
  75. package/dist/locales/uk.yaml +20 -0
  76. package/dist/locales/vi.yaml +20 -0
  77. package/dist/locales/zh-CN.yaml +20 -0
  78. package/dist/locales/zh-TW.yaml +20 -0
  79. package/dist/services/group.svelte.js +60 -0
  80. package/dist/services/popup.svelte.js +24 -1
  81. package/dist/services/select.svelte.d.ts +5 -0
  82. package/dist/services/select.svelte.js +8 -0
  83. package/dist/services/type-ahead.d.ts +26 -0
  84. package/dist/services/type-ahead.js +81 -0
  85. package/dist/styles/core.scss +14 -3
  86. package/dist/styles/variables.scss +118 -1
  87. package/dist/typedefs.d.ts +7 -1
  88. package/dist/typedefs.js +4 -1
  89. package/package.json +3 -3
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Type-ahead support shared by the composite widgets: the collapsed select-only combobox, and the
3
+ * listbox and menu groups.
4
+ */
5
+
6
+ /**
7
+ * How long, in milliseconds, successive keystrokes keep adding up to one type-ahead prefix.
8
+ */
9
+ export const TYPE_AHEAD_TIMEOUT = 500;
10
+
11
+ /**
12
+ * Keystroke buffer behind type-ahead: printable keys typed in quick succession make up a prefix,
13
+ * which is dropped again after a short pause.
14
+ */
15
+ export class TypeAhead {
16
+ /** @type {string} */
17
+ #prefix = '';
18
+
19
+ /** @type {number} */
20
+ #timer = 0;
21
+
22
+ /**
23
+ * Add a keystroke to the prefix.
24
+ * @param {string} key Printable key.
25
+ * @returns {string} Current prefix.
26
+ */
27
+ push(key) {
28
+ globalThis.clearTimeout(this.#timer);
29
+ this.#prefix += key.toLocaleLowerCase();
30
+ this.#timer = /** @type {number} */ (
31
+ /** @type {unknown} */ (
32
+ globalThis.setTimeout(() => {
33
+ this.#prefix = '';
34
+ }, TYPE_AHEAD_TIMEOUT)
35
+ )
36
+ );
37
+
38
+ return this.#prefix;
39
+ }
40
+
41
+ /**
42
+ * Drop the prefix.
43
+ */
44
+ reset() {
45
+ globalThis.clearTimeout(this.#timer);
46
+ this.#prefix = '';
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Find the item that a type-ahead prefix lands on, the way a native `<select>` does. A single
52
+ * character moves to the next item after the current one whose label starts with it, wrapping
53
+ * around, and a repeated character (“aa”, …) keeps cycling through those. A longer prefix refines
54
+ * the match from the current item, which is where the first character landed.
55
+ * @param {string[]} labels Normalized item labels, in order.
56
+ * @param {string} prefix Normalized prefix typed so far.
57
+ * @param {number} currentIndex Index of the current item, or `-1` if there is none.
58
+ * @returns {number} Index of the matching item, or `-1` if nothing matches.
59
+ */
60
+ export const findTypeAheadMatch = (labels, prefix, currentIndex) => {
61
+ if (!prefix) {
62
+ return -1;
63
+ }
64
+
65
+ const cycling = prefix.length > 1 && [...prefix].every((char) => char === prefix[0]);
66
+ const needle = cycling ? prefix[0] : prefix;
67
+ // A single or repeated character is a request to move on, so the search starts after the current
68
+ // item; a longer prefix is refining what the first character found, so it starts there
69
+ const start = prefix.length === 1 || cycling ? currentIndex + 1 : Math.max(currentIndex, 0);
70
+ const { length } = labels;
71
+
72
+ for (let i = 0; i < length; i += 1) {
73
+ const index = (start + i) % length;
74
+
75
+ if (labels[index].startsWith(needle)) {
76
+ return index;
77
+ }
78
+ }
79
+
80
+ return -1;
81
+ };
@@ -46,6 +46,7 @@
46
46
 
47
47
  :focus-visible {
48
48
  outline-color: var(--sui-focus-ring-color);
49
+ outline-offset: var(--sui-focus-ring-offset, 0);
49
50
  z-index: 2;
50
51
  }
51
52
 
@@ -158,19 +159,19 @@
158
159
  padding-inline-start: 12px;
159
160
  }
160
161
 
161
- :is(.disabled, .readonly, [aria-disabled="true"], [aria-readonly="true"], [inert]):not(body) {
162
+ :is(.disabled, [aria-disabled="true"], [inert]):not(body) {
162
163
  cursor: default;
163
164
  pointer-events: none;
164
165
  -webkit-user-select: none;
165
166
  user-select: none;
166
- filter: grayscale(1) opacity(0.35);
167
+ filter: grayscale(1) opacity(var(--sui-disabled-opacity, 0.35));
167
168
 
168
169
  * {
169
170
  filter: grayscale(0) opacity(1); // Maintain the opacity on child nodes
170
171
  }
171
172
  }
172
173
 
173
- :is(.disabled, .readonly, [aria-disabled="true"], [aria-readonly="true"], [inert]) {
174
+ :is(.disabled, [aria-disabled="true"], [inert]) {
174
175
  * {
175
176
  cursor: default;
176
177
  pointer-events: none;
@@ -178,4 +179,14 @@
178
179
  user-select: none;
179
180
  }
180
181
  }
182
+
183
+ // A read-only control is not inactive: its value is content the user reads and copies, so it
184
+ // keeps full contrast and stays selectable (WCAG 1.4.3). Only the cursor changes.
185
+ :is(.readonly, [aria-readonly="true"]):not(body) {
186
+ cursor: default;
187
+
188
+ * {
189
+ cursor: default;
190
+ }
191
+ }
181
192
  }
@@ -72,6 +72,102 @@
72
72
  --sui-alert-border-color-lightness: 38%;
73
73
  }
74
74
 
75
+ // High-contrast variants, layered over the light and dark themes for `prefers-contrast: more` (or
76
+ // `data-contrast="more"`). Every text colour reaches 7:1 and every control boundary 3:1 or better;
77
+ // the translucent and faded treatments the default themes use for hierarchy are made opaque.
78
+ @mixin high-contrast-light {
79
+ --sui-foreground-color-2-hsl: var(--sui-base-hue) 5% 10%; // primary, 17.5:1
80
+ --sui-foreground-color-3-hsl: var(--sui-base-hue) 5% 18%; // secondary, 13.7:1
81
+ --sui-foreground-color-4-hsl: var(--sui-base-hue) 5% 28%; // tertiary, 9.3:1
82
+ --sui-foreground-color-5-hsl: var(--sui-base-hue) 5% 45%; // disabled, 4.8:1
83
+ // The surface levels are spread further apart, so panels read as panels without relying on
84
+ // the faint default steps
85
+ --sui-background-color-3-hsl: var(--sui-base-hue) 5% 93%; // secondary
86
+ --sui-background-color-4-hsl: var(--sui-base-hue) 5% 88%; // tertiary/disabled
87
+ --sui-background-color-5-hsl: var(--sui-base-hue) 5% 70%; // highlight
88
+ --sui-border-color-1-hsl: var(--sui-base-hue) 5% 40%; // control, 5.8:1
89
+ --sui-border-color-2-hsl: var(--sui-base-hue) 5% 55%; // primary, 3.4:1
90
+ --sui-border-color-3-hsl: var(--sui-base-hue) 5% 65%; // secondary
91
+ // Accents
92
+ --sui-primary-accent-color-text: hsl(var(--sui-base-hue) 80% 32%); // 8:1
93
+ --sui-primary-accent-color-light: hsl(var(--sui-base-hue) 80% 37%);
94
+ --sui-primary-accent-color: hsl(var(--sui-base-hue) 80% 32%);
95
+ --sui-primary-accent-color-dark: hsl(var(--sui-base-hue) 80% 26%);
96
+ // Alerts
97
+ --sui-alert-foreground-color-lightness: 18%;
98
+ --sui-alert-border-color-saturation: 55%;
99
+ --sui-alert-border-color-lightness: 40%;
100
+ @include high-contrast-common;
101
+ }
102
+
103
+ @mixin high-contrast-dark {
104
+ --sui-foreground-color-2-hsl: var(--sui-base-hue) 10% 92%; // primary, 15.3:1
105
+ --sui-foreground-color-3-hsl: var(--sui-base-hue) 10% 85%; // secondary, 12.9:1
106
+ --sui-foreground-color-4-hsl: var(--sui-base-hue) 10% 75%; // tertiary, 9.9:1
107
+ --sui-foreground-color-5-hsl: var(--sui-base-hue) 10% 55%; // disabled, 5.4:1
108
+ // Near-black content and input surfaces with distinctly lighter panels, the way macOS treats
109
+ // its own windows with Increase Contrast on
110
+ --sui-background-color-1-hsl: var(--sui-base-hue) 10% 4%; // content/input
111
+ --sui-background-color-2-hsl: var(--sui-base-hue) 10% 7%; // primary
112
+ --sui-background-color-3-hsl: var(--sui-base-hue) 10% 12%; // secondary
113
+ --sui-background-color-4-hsl: var(--sui-base-hue) 10% 18%; // tertiary/disabled
114
+ --sui-background-color-5-hsl: var(--sui-base-hue) 10% 40%; // highlight
115
+ --sui-border-color-1-hsl: var(--sui-base-hue) 10% 60%; // control, 6.4:1
116
+ --sui-border-color-2-hsl: var(--sui-base-hue) 10% 50%; // primary, 4.5:1
117
+ --sui-border-color-3-hsl: var(--sui-base-hue) 10% 40%; // secondary, 3.1:1
118
+ // Accents. The fill stays put: lighter would lose the white label on a primary button, darker
119
+ // would lose the fill against the page. The text accent and the focus ring go lighter instead.
120
+ --sui-primary-accent-color-text: hsl(var(--sui-base-hue) 100% 70%); // 8.3:1
121
+ --sui-focus-ring-color: var(--sui-primary-accent-color-text);
122
+ // Alerts
123
+ --sui-alert-foreground-color-lightness: 85%;
124
+ --sui-alert-border-color-saturation: 55%;
125
+ --sui-alert-border-color-lightness: 55%;
126
+ @include high-contrast-common;
127
+ }
128
+
129
+ // Apply the high-contrast variant matching the active theme, however the theme was chosen
130
+ @mixin high-contrast-themes {
131
+ &[data-theme="light"] {
132
+ @include high-contrast-light;
133
+ }
134
+
135
+ &[data-theme="dark"] {
136
+ @include high-contrast-dark;
137
+ }
138
+
139
+ &:not([data-theme]) {
140
+ @media (prefers-color-scheme: light) {
141
+ @include high-contrast-light;
142
+ }
143
+
144
+ @media (prefers-color-scheme: dark) {
145
+ @include high-contrast-dark;
146
+ }
147
+ }
148
+ }
149
+
150
+ // Shared by both high-contrast variants: solid state backgrounds and boundaries in place of the
151
+ // translucent ones, and placeholder text at full strength
152
+ @mixin high-contrast-common {
153
+ --sui-hover-background-color: hsl(var(--sui-background-color-5-hsl) / 60%);
154
+ --sui-selected-background-color: hsl(var(--sui-background-color-5-hsl));
155
+ --sui-active-background-color: hsl(var(--sui-background-color-5-hsl));
156
+ --sui-primary-background-color-translucent: hsl(var(--sui-background-color-2-hsl));
157
+ --sui-secondary-background-color-translucent: hsl(var(--sui-background-color-3-hsl));
158
+ --sui-tertiary-background-color-translucent: hsl(var(--sui-background-color-4-hsl));
159
+ --sui-popup-shadow-color: hsl(var(--sui-shadow-color) / 70%);
160
+ --sui-popup-backdrop-color: hsl(var(--sui-shadow-color) / 60%);
161
+ // A text box shares the page background, so its border is what marks the field
162
+ --sui-textbox-border-color: hsl(var(--sui-border-color-1-hsl));
163
+ --sui-textbox-placeholder-foreground-color: var(--sui-tertiary-foreground-color);
164
+ --sui-textbox-placeholder-opacity: 1;
165
+ // Disabled controls are dimmed less; the loss of colour still marks them
166
+ --sui-disabled-opacity: 0.6;
167
+ // Dialogs and drawers get an outline, as the OS draws around its own windows
168
+ --sui-modal-border-width: 1px;
169
+ }
170
+
75
171
  :global {
76
172
  :root,
77
173
  :host {
@@ -128,9 +224,16 @@
128
224
  var(--sui-success-color-hue) var(--sui-alert-background-color-saturation)
129
225
  var(--sui-alert-background-color-lightness)
130
226
  );
227
+ // Disabled controls: how far they fade (they also lose their colour)
228
+ --sui-disabled-opacity: 0.35;
229
+ // Dialog & drawer outline; none by default, drawn by the high-contrast theme
230
+ --sui-modal-border-width: 0;
131
231
  // Outline
132
232
  --sui-focus-ring-width: 2px;
133
- --sui-focus-ring-color: var(--sui-primary-accent-color-translucent);
233
+ // Opaque, so the ring reaches 3:1 against the page (WCAG 1.4.11); the offset keeps a sliver of
234
+ // page background between the ring and a control filled with the same accent colour
235
+ --sui-focus-ring-color: var(--sui-primary-accent-color);
236
+ --sui-focus-ring-offset: 1px;
134
237
  // Borders
135
238
  --sui-primary-border-color: hsl(var(--sui-border-color-2-hsl));
136
239
  --sui-secondary-border-color: hsl(var(--sui-border-color-3-hsl));
@@ -290,5 +393,19 @@
290
393
  @include dark-theme;
291
394
  }
292
395
  }
396
+
397
+ // High contrast is a second dimension on top of the light/dark theme, and is resolved the same
398
+ // way: `data-contrast="more"` turns it on, any other value keeps the standard palette, and
399
+ // without the attribute the system setting decides. The selectors here are more specific than
400
+ // the theme blocks above, so these overrides win whichever way the theme was picked.
401
+ &[data-contrast="more"] {
402
+ @include high-contrast-themes;
403
+ }
404
+
405
+ &:not([data-contrast]) {
406
+ @media (prefers-contrast: more) {
407
+ @include high-contrast-themes;
408
+ }
409
+ }
293
410
  }
294
411
  }
@@ -205,9 +205,15 @@ export type ModalProps = {
205
205
  };
206
206
  export type DialogProps = {
207
207
  /**
208
- * Text label displayed on the header. Required.
208
+ * Text label displayed on the header. Required. With a custom `header`
209
+ * snippet, it’s used as the dialog’s `aria-label` instead.
209
210
  */
210
211
  title?: string | undefined;
212
+ /**
213
+ * ID of the element that labels the dialog, typically a heading
214
+ * inside a custom `header` snippet. Takes precedence over `title` for the accessible name.
215
+ */
216
+ ariaLabelledby?: string | undefined;
211
217
  /**
212
218
  * The `role` attribute on the `<dialog>` element.
213
219
  */
package/dist/typedefs.js CHANGED
@@ -77,7 +77,10 @@
77
77
 
78
78
  /**
79
79
  * @typedef {object} DialogProps
80
- * @property {string} [title] Text label displayed on the header. Required.
80
+ * @property {string} [title] Text label displayed on the header. Required. With a custom `header`
81
+ * snippet, it’s used as the dialog’s `aria-label` instead.
82
+ * @property {string} [ariaLabelledby] ID of the element that labels the dialog, typically a heading
83
+ * inside a custom `header` snippet. Takes precedence over `title` for the accessible name.
81
84
  * @property {'dialog' | 'alertdialog'} [role] The `role` attribute on the `<dialog>` element.
82
85
  * @property {'small' | 'medium' | 'large' | 'x-large'} [size] Width of the dialog.
83
86
  * @property {string} [class] The `class` attribute on the `<dialog>` element.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sveltia/ui",
3
- "version": "0.72.2",
3
+ "version": "0.73.0",
4
4
  "description": "A collection of Svelte components and utilities for building user interfaces.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -64,12 +64,12 @@
64
64
  "eslint-config-airbnb-extended": "^3.2.0",
65
65
  "eslint-config-prettier": "^10.1.8",
66
66
  "eslint-plugin-import": "^2.32.0",
67
- "eslint-plugin-jsdoc": "^64.3.10",
67
+ "eslint-plugin-jsdoc": "^64.4.0",
68
68
  "eslint-plugin-package-json": "^1.8.1",
69
69
  "eslint-plugin-svelte": "^3.23.0",
70
70
  "globals": "^17.12.0",
71
71
  "happy-dom": "^20.14.5",
72
- "oxlint": "^1.82.0",
72
+ "oxlint": "^1.83.0",
73
73
  "playwright": "^1.63.0",
74
74
  "postcss": "^8.5.28",
75
75
  "postcss-html": "^2.0.0",