@lyeve-labs/ui-kit 0.18.0 → 0.20.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/README.md +11 -4
  2. package/dist/components/AccordionItem.svelte +1 -1
  3. package/dist/components/AppShell.svelte +3 -3
  4. package/dist/components/Autocomplete.svelte +4 -4
  5. package/dist/components/AvatarGroup.svelte +2 -2
  6. package/dist/components/Breadcrumb.svelte +1 -1
  7. package/dist/components/Button.svelte +34 -9
  8. package/dist/components/ButtonGroup.svelte +3 -3
  9. package/dist/components/Card.svelte +1 -1
  10. package/dist/components/Checkbox.svelte +1 -1
  11. package/dist/components/CheckboxGroup.svelte +1 -1
  12. package/dist/components/Collapsible.svelte +3 -3
  13. package/dist/components/CopyButton.svelte +1 -1
  14. package/dist/components/DatePicker.svelte +2 -2
  15. package/dist/components/DateTimePicker.svelte +1 -1
  16. package/dist/components/Drawer.svelte +1 -1
  17. package/dist/components/Dropdown.svelte +1 -1
  18. package/dist/components/Field.svelte +1 -1
  19. package/dist/components/Input.svelte +1 -1
  20. package/dist/components/Label.svelte +1 -1
  21. package/dist/components/MultiSelect.svelte +4 -4
  22. package/dist/components/NumberInput.svelte +3 -3
  23. package/dist/components/Pagination.svelte +194 -65
  24. package/dist/components/Pagination.svelte.d.ts +52 -2
  25. package/dist/components/PasswordInput.svelte +4 -4
  26. package/dist/components/Radio.svelte +1 -1
  27. package/dist/components/RadioGroup.svelte +1 -1
  28. package/dist/components/SearchInput.svelte +4 -4
  29. package/dist/components/SegmentedControl.svelte +3 -1
  30. package/dist/components/Select.svelte +6 -6
  31. package/dist/components/SidebarNav.svelte +20 -5
  32. package/dist/components/Table.svelte +114 -1
  33. package/dist/components/Table.svelte.d.ts +37 -0
  34. package/dist/components/Tabs.svelte +1 -1
  35. package/dist/components/Tag.svelte +1 -1
  36. package/dist/components/Textarea.svelte +1 -1
  37. package/dist/components/ThemeToggle.svelte +1 -1
  38. package/dist/components/TimePicker.svelte +2 -2
  39. package/dist/components/Toaster.svelte +2 -2
  40. package/dist/components/Toggle.svelte +10 -4
  41. package/dist/components/TreeView.svelte +4 -4
  42. package/dist/components/dialog/Dialog.svelte +1 -1
  43. package/dist/index.d.ts +2 -1
  44. package/dist/index.js +1 -1
  45. package/dist/internal/href.d.ts +13 -0
  46. package/dist/internal/href.js +27 -0
  47. package/dist/internal/number.d.ts +26 -0
  48. package/dist/internal/number.js +35 -0
  49. package/dist/internal/panel.js +1 -1
  50. package/dist/styles/theme.css +53 -0
  51. package/package.json +3 -4
  52. package/src/lib/styles/theme.css +53 -0
@@ -1,3 +1,14 @@
1
+ <script lang="ts" module>
2
+ /**
3
+ * How a body cell treats content wider than its column.
4
+ *
5
+ * `wrap` breaks anywhere it has to. `truncate` keeps the cell to one line and
6
+ * ends it with an ellipsis. `auto` is the browser's own table algorithm, with
7
+ * no cap of any kind.
8
+ */
9
+ export type CellFit = 'auto' | 'wrap' | 'truncate';
10
+ </script>
11
+
1
12
  <script lang="ts">
2
13
  import type { Snippet } from 'svelte';
3
14
 
@@ -12,6 +23,35 @@
12
23
  * else, which is a landmark the reader has to enter to identify.
13
24
  */
14
25
  label?: string;
26
+ /**
27
+ * What a body cell does with content too wide for its column.
28
+ *
29
+ * `wrap` by default, and the default is not `auto`. A table constrained
30
+ * nothing: one cell holding an unbroken token - an API key, a signed URL,
31
+ * a base64 payload - has no break opportunity in it, so the auto table
32
+ * algorithm sizes the column to the whole string and the table grows to
33
+ * whatever that comes to. One measured page took its container from no
34
+ * overflow at all to 3,086px of it, and the audit log overran its own
35
+ * container by 419px at 390px wide.
36
+ *
37
+ * `wrap` fixes exactly that and nothing else. It is `overflow-wrap:
38
+ * anywhere`, which is the one value that shrinks the column's min-content
39
+ * width, so a paragraph still breaks at its spaces and only a token with
40
+ * no spaces in it is broken mid-string. Prose in a cell renders as it
41
+ * always did.
42
+ *
43
+ * The default wraps rather than truncates because wrapping is the choice
44
+ * that cannot lose anything. Truncation hides, and a table that hides is
45
+ * worse than one that scrolls, so it is opted into.
46
+ */
47
+ cell?: CellFit;
48
+ /**
49
+ * The width a truncating cell stops at. Any CSS length.
50
+ *
51
+ * Read through a custom property, so a single column can disagree with the
52
+ * table by setting `--cell-truncate` in its own style attribute.
53
+ */
54
+ truncateAt?: string;
15
55
  class?: string;
16
56
  children: Snippet;
17
57
  }
@@ -21,6 +61,8 @@
21
61
  hoverable = true,
22
62
  fixed = false,
23
63
  label = undefined,
64
+ cell = 'wrap',
65
+ truncateAt = '20rem',
24
66
  class: cls = '',
25
67
  children,
26
68
  }: Props = $props();
@@ -99,6 +141,73 @@
99
141
  * for the same reason and it reads in both themes.
100
142
  */
101
143
  const EDGE = 'pointer-events-none absolute inset-y-px w-6';
144
+
145
+ /*
146
+ * The table-wide treatment, applied only to cells that did not ask for their
147
+ * own.
148
+ *
149
+ * `:not([data-cell])` is what keeps the two apart. Without it a column that
150
+ * opted out still matched the table's rule for the same property, and which
151
+ * of the two won came down to the order Tailwind happened to emit them in.
152
+ */
153
+ const CELL_FIT: Record<CellFit, string> = {
154
+ auto: '',
155
+ wrap: '[&_tbody_td:not([data-cell])]:[overflow-wrap:anywhere]',
156
+ truncate:
157
+ '[&_tbody_td:not([data-cell])]:max-w-[var(--cell-truncate)] [&_tbody_td:not([data-cell])]:truncate',
158
+ };
159
+
160
+ /*
161
+ * The per-cell opt-in, always present so a caller can mix treatments down one
162
+ * row: `data-cell="nowrap"` on a timestamp, `data-cell="truncate"` on a
163
+ * description, nothing at all on the rest.
164
+ */
165
+ const CELL_NAMED =
166
+ '[&_tbody_td[data-cell=wrap]]:[overflow-wrap:anywhere] ' +
167
+ '[&_tbody_td[data-cell=nowrap]]:whitespace-nowrap ' +
168
+ '[&_tbody_td[data-cell=truncate]]:max-w-[var(--cell-truncate)] ' +
169
+ '[&_tbody_td[data-cell=truncate]]:truncate';
170
+
171
+ /** Marks a title this component put there, so it can take it back again. */
172
+ const OWNED = 'data-cell-title';
173
+
174
+ /**
175
+ * Gives a clipped cell its full value back.
176
+ *
177
+ * The text is never removed from the document, so a screen reader already
178
+ * reads the whole of it and it is the sighted pointer user who loses the tail
179
+ * to the ellipsis. The title that returns it is attached when a pointer or
180
+ * the keyboard actually reaches the cell, from one listener on the scroll
181
+ * box, rather than measured for every cell as the table renders: a tenant
182
+ * with 10,000 rows would pay for 10,000 measurements and 10,000 title
183
+ * attributes to answer a question asked of about three of them.
184
+ */
185
+ function reveal(event: Event) {
186
+ const from = event.target;
187
+ if (!(from instanceof Element)) return;
188
+ const td = from.closest('td');
189
+ if (!(td instanceof HTMLElement)) return;
190
+
191
+ const named = td.dataset.cell;
192
+ const clipping = named === 'truncate' || (named === undefined && cell === 'truncate');
193
+ if (!clipping) return;
194
+
195
+ const ours = td.hasAttribute(OWNED);
196
+ // A title the caller wrote is theirs. It may well say more than the cell
197
+ // holds, and replacing it with the visible text would be a downgrade.
198
+ if (td.hasAttribute('title') && !ours) return;
199
+
200
+ const full = (td.textContent ?? '').trim();
201
+ if (full && td.scrollWidth - td.clientWidth > 1) {
202
+ td.setAttribute('title', full);
203
+ td.setAttribute(OWNED, '');
204
+ } else if (ours) {
205
+ // The column widened, or the row now holds something shorter. A tooltip
206
+ // repeating text the reader can already see is noise.
207
+ td.removeAttribute('title');
208
+ td.removeAttribute(OWNED);
209
+ }
210
+ }
102
211
  </script>
103
212
 
104
213
  <div data-testid="table-frame" class="relative w-full rounded-xl border border-line {cls}">
@@ -109,6 +218,8 @@
109
218
  <div
110
219
  bind:this={scroller}
111
220
  onscroll={measure}
221
+ onpointerover={reveal}
222
+ onfocusin={reveal}
112
223
  data-print="unclip"
113
224
  data-testid="table-scroll"
114
225
  class="w-full overflow-x-auto rounded-xl"
@@ -126,8 +237,10 @@
126
237
  same place.
127
238
  -->
128
239
  <table
129
- class="w-full border-separate border-spacing-0 text-sm text-left
240
+ style="--cell-truncate: {truncateAt}"
241
+ class="w-full border-separate border-spacing-0 text-sm text-start
130
242
  {fixed ? 'table-fixed' : ''}
243
+ {CELL_FIT[cell]} {CELL_NAMED}
131
244
  [&_thead_th]:border-b [&_thead_th]:border-line [&_thead]:bg-surface-2/40
132
245
  [&_thead_th]:px-4 [&_thead_th]:py-3 [&_thead_th]:text-xs
133
246
  [&_thead_th]:font-medium [&_thead_th]:text-faint
@@ -1,3 +1,11 @@
1
+ /**
2
+ * How a body cell treats content wider than its column.
3
+ *
4
+ * `wrap` breaks anywhere it has to. `truncate` keeps the cell to one line and
5
+ * ends it with an ellipsis. `auto` is the browser's own table algorithm, with
6
+ * no cap of any kind.
7
+ */
8
+ export type CellFit = 'auto' | 'wrap' | 'truncate';
1
9
  import type { Snippet } from 'svelte';
2
10
  interface Props {
3
11
  striped?: boolean;
@@ -10,6 +18,35 @@ interface Props {
10
18
  * else, which is a landmark the reader has to enter to identify.
11
19
  */
12
20
  label?: string;
21
+ /**
22
+ * What a body cell does with content too wide for its column.
23
+ *
24
+ * `wrap` by default, and the default is not `auto`. A table constrained
25
+ * nothing: one cell holding an unbroken token - an API key, a signed URL,
26
+ * a base64 payload - has no break opportunity in it, so the auto table
27
+ * algorithm sizes the column to the whole string and the table grows to
28
+ * whatever that comes to. One measured page took its container from no
29
+ * overflow at all to 3,086px of it, and the audit log overran its own
30
+ * container by 419px at 390px wide.
31
+ *
32
+ * `wrap` fixes exactly that and nothing else. It is `overflow-wrap:
33
+ * anywhere`, which is the one value that shrinks the column's min-content
34
+ * width, so a paragraph still breaks at its spaces and only a token with
35
+ * no spaces in it is broken mid-string. Prose in a cell renders as it
36
+ * always did.
37
+ *
38
+ * The default wraps rather than truncates because wrapping is the choice
39
+ * that cannot lose anything. Truncation hides, and a table that hides is
40
+ * worse than one that scrolls, so it is opted into.
41
+ */
42
+ cell?: CellFit;
43
+ /**
44
+ * The width a truncating cell stops at. Any CSS length.
45
+ *
46
+ * Read through a custom property, so a single column can disagree with the
47
+ * table by setting `--cell-truncate` in its own style attribute.
48
+ */
49
+ truncateAt?: string;
13
50
  class?: string;
14
51
  children: Snippet;
15
52
  }
@@ -27,7 +27,7 @@
27
27
  {tab.label}
28
28
  {#if tab.count !== undefined}
29
29
  <span
30
- class="ml-1.5 inline-flex items-center justify-center min-w-4.5 h-4.5 px-1
30
+ class="ms-1.5 inline-flex items-center justify-center min-w-4.5 h-4.5 px-1
31
31
  rounded-full text-[10px] font-semibold
32
32
  {active === tab.id ? 'bg-brand/40 text-brand' : 'bg-surface-2 text-faint'}"
33
33
  >
@@ -37,7 +37,7 @@
37
37
  type="button"
38
38
  onclick={onremove}
39
39
  aria-label="Remove {label}"
40
- class="ml-0.5 rounded transition-opacity hover:opacity-60 leading-none"
40
+ class="ms-0.5 rounded transition-opacity hover:opacity-60 leading-none"
41
41
  >
42
42
  <svg
43
43
  width="10"
@@ -54,7 +54,7 @@
54
54
  <div class="{FIELD_WRAP} {cls}">
55
55
  {#if label}
56
56
  <label for={fieldId} class={FIELD_LABEL}>
57
- {label}{#if required}<span class="text-danger ml-0.5" aria-hidden="true">*</span>{/if}
57
+ {label}{#if required}<span class="text-danger ms-0.5" aria-hidden="true">*</span>{/if}
58
58
  </label>
59
59
  {/if}
60
60
 
@@ -81,7 +81,7 @@
81
81
  data-print="hide"
82
82
  data-theme-preference={preference}
83
83
  aria-label="Theme: {LABEL[preference]}. Switch to {LABEL[upcoming]}."
84
- class="rounded-lg p-2 text-faint transition-colors duration-150 hover:bg-surface-2 hover:text-fg {klass}"
84
+ class="rounded-lg p-2 text-faint transition-colors duration-150 hover:bg-surface-2 hover:text-fg active:bg-line active:text-fg {klass}"
85
85
  >
86
86
  <!-- The icon shows the state the control is in, not the one it moves to. It
87
87
  showed the destination while the label named the same destination, which
@@ -401,7 +401,7 @@
401
401
  takes its name from this same element. The hour keeps its own aria-label,
402
402
  which is what a screen reader reads for it. -->
403
403
  <label id="{fieldId}-label" for="{fieldId}-hour" class={FIELD_LABEL}>
404
- {label}{#if required}<span class="ml-0.5 text-danger" aria-hidden="true">*</span>{/if}
404
+ {label}{#if required}<span class="ms-0.5 text-danger" aria-hidden="true">*</span>{/if}
405
405
  </label>
406
406
  {/if}
407
407
 
@@ -459,7 +459,7 @@
459
459
  aria-required={required ? 'true' : undefined}
460
460
  onchange={onMeridiemChange}
461
461
  onkeydown={onMeridiemKeydown}
462
- class="{CONTROL_SEGMENT} ml-1"
462
+ class="{CONTROL_SEGMENT} ms-1"
463
463
  >
464
464
  <option value="AM">AM</option>
465
465
  <option value="PM">PM</option>
@@ -18,7 +18,7 @@
18
18
  change. -->
19
19
  <div
20
20
  data-print="hide"
21
- class="pointer-events-none fixed bottom-4 right-4 z-toast flex w-80 max-w-[calc(100vw-2rem)] flex-col gap-2"
21
+ class="pointer-events-none fixed bottom-4 end-4 z-toast flex w-80 max-w-[calc(100vw-2rem)] flex-col gap-2"
22
22
  role="status"
23
23
  aria-live="polite"
24
24
  aria-atomic="false"
@@ -26,7 +26,7 @@
26
26
  {#each toast.items as t (t.id)}
27
27
  <div
28
28
  class="pointer-events-auto flex items-start gap-3 overflow-hidden rounded-lg border border-line
29
- bg-surface pl-0 pr-3 py-3 shadow-xl animate-[toast-in_var(--duration-toast-in)_ease-out]"
29
+ bg-surface ps-0 pe-3 py-3 shadow-xl animate-[toast-in_var(--duration-toast-in)_ease-out]"
30
30
  >
31
31
  <span class="w-1 self-stretch shrink-0 {styles[statusTone(t.tone)].bar}"></span>
32
32
  <span
@@ -27,13 +27,19 @@
27
27
  };
28
28
 
29
29
  const thumb: Record<'sm' | 'md', string> = {
30
- sm: 'w-3 h-3 top-0.5 left-0.5',
31
- md: 'w-4 h-4 top-0.5 left-0.5',
30
+ sm: 'w-3 h-3 top-0.5 start-0.5',
31
+ md: 'w-4 h-4 top-0.5 start-0.5',
32
32
  };
33
33
 
34
+ /*
35
+ * The travel is mirrored by hand. `start-0.5` moves the resting knob to the
36
+ * right-hand end of the track in a right-to-left page, and a positive
37
+ * translate from there would carry it straight off the track; there is no
38
+ * logical translate utility, so the sign is flipped instead.
39
+ */
34
40
  const thumbOn: Record<'sm' | 'md', string> = {
35
- sm: 'translate-x-4',
36
- md: 'translate-x-5',
41
+ sm: 'translate-x-4 rtl:-translate-x-4',
42
+ md: 'translate-x-5 rtl:-translate-x-5',
37
43
  };
38
44
 
39
45
  function handleClick() {
@@ -270,11 +270,11 @@
270
270
  scan, so no rule is generated and every row draws flush left.
271
271
  -->
272
272
  <div
273
- class="flex w-full items-center gap-2 rounded-md py-1.5 pr-2 text-sm transition-colors duration-150 {rowPaint(
273
+ class="flex w-full items-center gap-2 rounded-md py-1.5 pe-2 text-sm transition-colors duration-150 {rowPaint(
274
274
  selected === node.id,
275
275
  !!node.disabled,
276
276
  )}"
277
- style="padding-left: calc(var(--spacing-inline) + {depth} * var(--spacing-stack))"
277
+ style="padding-inline-start: calc(var(--spacing-inline) + {depth} * var(--spacing-stack))"
278
278
  >
279
279
  {#if isBranch(node)}
280
280
  <span
@@ -283,7 +283,7 @@
283
283
  >
284
284
  <ChevronRight
285
285
  size={14}
286
- class="transition-transform duration-150 {open ? 'rotate-90' : ''}"
286
+ class="transition-transform duration-150 {open ? 'rotate-90' : 'rtl:rotate-180'}"
287
287
  />
288
288
  </span>
289
289
  {:else}
@@ -321,7 +321,7 @@
321
321
  <span class="truncate">{node.label}</span>
322
322
 
323
323
  {#if node.badge !== undefined}
324
- <span class="ml-auto shrink-0 pl-2 text-xs text-faint">{node.badge}</span>
324
+ <span class="ms-auto shrink-0 ps-2 text-xs text-faint">{node.badge}</span>
325
325
  {/if}
326
326
  </div>
327
327
 
@@ -153,7 +153,7 @@
153
153
 
154
154
  {#if !entry.options.persistent}
155
155
  <button
156
- class="inline-flex items-center justify-center w-8 h-8 -mr-2 rounded-lg
156
+ class="inline-flex items-center justify-center w-8 h-8 -me-2 rounded-lg
157
157
  text-muted hover:text-fg hover:bg-surface-2 transition-colors duration-150 shrink-0"
158
158
  onclick={() => handleClose()}
159
159
  aria-label="Close"
package/dist/index.d.ts CHANGED
@@ -20,6 +20,7 @@ export { default as Table } from './components/Table.svelte';
20
20
  export { default as DescriptionList } from './components/DescriptionList.svelte';
21
21
  export { default as Toolbar } from './components/Toolbar.svelte';
22
22
  export { default as TreeView } from './components/TreeView.svelte';
23
+ export type { CellFit } from './components/Table.svelte';
23
24
  export type { PageWidth, SectionVariant } from './internal/layout.js';
24
25
  export type { TreeNode, TriState } from './internal/tree.js';
25
26
  export { default as Button } from './components/Button.svelte';
@@ -88,4 +89,4 @@ export type { ConfirmOptions } from './components/dialog/dialog-manager.svelte.j
88
89
  export type { DialogOptions, DialogEntry, DialogSize } from './components/dialog/types.js';
89
90
  export { cn, type ClassValue } from './utils/cn.js';
90
91
  export { getTheme, getThemePreference, nextThemePreference, resolveTheme, setTheme, setThemePreference, systemTheme, themeBootScript, toggleTheme, watchSystemTheme, THEME_PREFERENCES, type Theme, type ThemePreference, } from './utils/theme.js';
91
- export declare const VERSION = "0.18.0";
92
+ export declare const VERSION = "0.20.0";
package/dist/index.js CHANGED
@@ -96,4 +96,4 @@ export { getTheme, getThemePreference, nextThemePreference, resolveTheme, setThe
96
96
  // ── Version ────────────────────────────────────────────────────────────────
97
97
  // Generated from package.json by `pnpm version:sync`. Bump package.json, never
98
98
  // this line; the build and the test suite fail when the two disagree.
99
- export const VERSION = '0.18.0';
99
+ export const VERSION = '0.20.0';
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The one rule the kit applies to an href it did not write itself.
3
+ *
4
+ * A component takes a URL from its caller, and a caller takes it from data, so
5
+ * `javascript:` and `data:` arrive as markup and leave as a script the page
6
+ * never wrote. Button carried this rule inline and Pagination now builds hrefs
7
+ * from a caller supplied function, and a rule kept in two places is a rule that
8
+ * gets fixed in one of them.
9
+ *
10
+ * Not exported from the package entry point - this is an implementation detail.
11
+ */
12
+ /** The href as given, or nothing when its scheme is one no component will emit. */
13
+ export declare function safeHref(href: string | undefined | null): string | undefined;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The one rule the kit applies to an href it did not write itself.
3
+ *
4
+ * A component takes a URL from its caller, and a caller takes it from data, so
5
+ * `javascript:` and `data:` arrive as markup and leave as a script the page
6
+ * never wrote. Button carried this rule inline and Pagination now builds hrefs
7
+ * from a caller supplied function, and a rule kept in two places is a rule that
8
+ * gets fixed in one of them.
9
+ *
10
+ * Not exported from the package entry point - this is an implementation detail.
11
+ */
12
+ /**
13
+ * The characters a browser drops before it reads the scheme.
14
+ *
15
+ * The URL parser trims leading and trailing ASCII whitespace and strips tab,
16
+ * line feed and carriage return from anywhere in the value, so a tab written
17
+ * inside the word `javascript` still navigates while an anchored scheme test on
18
+ * the raw string sees an unknown scheme and allows it through.
19
+ */
20
+ const IGNORED = /[\u0000-\u0020]/g;
21
+ const DENIED = /^(?:javascript|data|vbscript):/i;
22
+ /** The href as given, or nothing when its scheme is one no component will emit. */
23
+ export function safeHref(href) {
24
+ if (!href)
25
+ return undefined;
26
+ return DENIED.test(href.replace(IGNORED, '')) ? undefined : href;
27
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Digit grouping for a figure a reader is meant to take in at a glance.
3
+ *
4
+ * The locale is fixed rather than the runtime's. `toLocaleString()` with no
5
+ * argument formats by whatever locale the environment reports, and the server
6
+ * that renders a page and the browser that hydrates it do not have to report
7
+ * the same one: the same figure then arrives as `4,210` in the HTML and as
8
+ * `4.210` or `4210` after hydration, which is a mismatch the framework warns
9
+ * about and a number the reader watches change under them. A component library
10
+ * has no way to know which locale a host has configured either, so the
11
+ * separator has to be a decision the kit makes once.
12
+ *
13
+ * `en-US` because that is the language every string in the kit is written in,
14
+ * and an English sentence carrying a German separator is the worse of the two
15
+ * inconsistencies.
16
+ *
17
+ * Not exported from the package entry point - this is an implementation detail.
18
+ */
19
+ /**
20
+ * A count, grouped in threes.
21
+ *
22
+ * A value that is not a finite number reads as zero rather than reaching the
23
+ * formatter, which spells infinity as a glyph no summary line should carry and
24
+ * NaN as a word no reader can act on.
25
+ */
26
+ export declare function formatCount(n: number): string;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Digit grouping for a figure a reader is meant to take in at a glance.
3
+ *
4
+ * The locale is fixed rather than the runtime's. `toLocaleString()` with no
5
+ * argument formats by whatever locale the environment reports, and the server
6
+ * that renders a page and the browser that hydrates it do not have to report
7
+ * the same one: the same figure then arrives as `4,210` in the HTML and as
8
+ * `4.210` or `4210` after hydration, which is a mismatch the framework warns
9
+ * about and a number the reader watches change under them. A component library
10
+ * has no way to know which locale a host has configured either, so the
11
+ * separator has to be a decision the kit makes once.
12
+ *
13
+ * `en-US` because that is the language every string in the kit is written in,
14
+ * and an English sentence carrying a German separator is the worse of the two
15
+ * inconsistencies.
16
+ *
17
+ * Not exported from the package entry point - this is an implementation detail.
18
+ */
19
+ /**
20
+ * Built at module load, not per call.
21
+ *
22
+ * Constructing an `Intl` formatter is the expensive half of using one, and a
23
+ * pager re-renders on every keystroke that filters the list behind it.
24
+ */
25
+ const GROUPED = new Intl.NumberFormat('en-US');
26
+ /**
27
+ * A count, grouped in threes.
28
+ *
29
+ * A value that is not a finite number reads as zero rather than reaching the
30
+ * formatter, which spells infinity as a glyph no summary line should carry and
31
+ * NaN as a word no reader can act on.
32
+ */
33
+ export function formatCount(n) {
34
+ return Number.isFinite(n) ? GROUPED.format(n) : '0';
35
+ }
@@ -55,7 +55,7 @@ export const PANEL_LIST = 'max-h-panel-max overflow-y-auto overscroll-contain py
55
55
  * No background and no text colour of its own: it inherits both from the
56
56
  * surface, which leaves each state below a single utility to override.
57
57
  */
58
- export const PANEL_OPTION = 'flex w-full items-center gap-2.5 px-3 py-2 text-left text-sm ' +
58
+ export const PANEL_OPTION = 'flex w-full items-center gap-2.5 px-3 py-2 text-start text-sm ' +
59
59
  'transition-colors duration-150 outline-none hover:bg-surface-2 ' +
60
60
  'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-brand';
61
61
  /**
@@ -151,6 +151,8 @@
151
151
  * Body 16px 1.55 0 400 Default body copy *
152
152
  * Caption 13px 1.50 0 400 Labels, metadata *
153
153
  * Mono 18px 1.50 0 500 Code, eyebrows *
154
+ * The ramp the guideline states, for marketing surfaces. *
155
+ * The scale the console renders is declared below it. *
154
156
  * */
155
157
  --text-display: 4rem; /* 64px */
156
158
  --text-h1: 2.75rem; /* 44px */
@@ -173,6 +175,57 @@
173
175
  --tracking-h2: -0.02em;
174
176
  --tracking-h3: -0.015em;
175
177
 
178
+ /* -- Typography - console type scale --------------------------- *
179
+ * Token Size Line Where it renders *
180
+ * text-xs 12px 16px labels, table heads, metadata, hints*
181
+ * text-sm 14px 20px body copy and every control *
182
+ * text-base 16px 24px a card title, the large button *
183
+ * text-lg 18px 28px a dialog title, a small stat value *
184
+ * text-xl 20px 28px the large wordmark *
185
+ * text-2xl 24px 32px a stat value, an empty-state glyph *
186
+ *
187
+ * Two scales, on purpose, and this is the one the components
188
+ * are drawn in.
189
+ *
190
+ * The ramp above is the brand guideline's, and it is a marketing
191
+ * ramp: it runs 64 / 44 / 32 / 22 / 16 / 13, holds nothing
192
+ * between 16px and 22px, and has no step at 14px at all. A
193
+ * console's body copy is 14px and its labels are 12px, both
194
+ * below the ramp's floor, so adopting the ramp wholesale would
195
+ * have meant restyling every screen to fit a scale drawn for a
196
+ * landing page. The ramp keeps its steps for the surfaces it was
197
+ * drawn for, and the two overlap at exactly the two steps a
198
+ * console does reach: PageHeader takes H2 for the page title and
199
+ * SectionHeading takes H3 for a section head.
200
+ *
201
+ * These are Tailwind's own steps at Tailwind's own values,
202
+ * stated here rather than inherited silently. Nothing renders
203
+ * one pixel differently for it. What changes is that the scale
204
+ * the components are built from is now declared beside the one
205
+ * they are not, so a designer reading this file sees both, and
206
+ * moving console body copy to 15px is one line here instead of
207
+ * 49 class attributes. Restated rather than renamed: an app
208
+ * writing `text-sm` in its own markup and a component writing it
209
+ * in the kit's must mean the same size, and a private spelling
210
+ * of the same 14px would be a second name for one thing.
211
+ *
212
+ * A step is declared with its leading, because a --text-* token
213
+ * sets font-size and nothing else. Only the six steps the kit
214
+ * actually renders are declared; the larger ones a marketing
215
+ * page reaches for stay as Tailwind ships them. */
216
+ --text-xs: 0.75rem; /* 12px */
217
+ --text-xs--line-height: calc(1 / 0.75); /* 16px */
218
+ --text-sm: 0.875rem; /* 14px */
219
+ --text-sm--line-height: calc(1.25 / 0.875); /* 20px */
220
+ --text-base: 1rem; /* 16px */
221
+ --text-base--line-height: calc(1.5 / 1); /* 24px */
222
+ --text-lg: 1.125rem; /* 18px */
223
+ --text-lg--line-height: calc(1.75 / 1.125); /* 28px */
224
+ --text-xl: 1.25rem; /* 20px */
225
+ --text-xl--line-height: calc(1.75 / 1.25); /* 28px */
226
+ --text-2xl: 1.5rem; /* 24px */
227
+ --text-2xl--line-height: calc(2 / 1.5); /* 32px */
228
+
176
229
  /* ── Spacing grid · 4px base (semantic shortcuts) ────────────── *
177
230
  * Tailwind's default 0.25rem (4px) grid is unchanged. *
178
231
  * These tokens add named slots for common component spacing. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lyeve-labs/ui-kit",
3
- "version": "0.18.0",
3
+ "version": "0.20.0",
4
4
  "description": "A clean, accessible, palette-aware Svelte 5 component library. The design system behind LyEve.",
5
5
  "license": "MIT",
6
6
  "author": "LyEve Labs <hello@lyeve.com>",
@@ -44,16 +44,15 @@
44
44
  "./package.json": "./package.json"
45
45
  },
46
46
  "peerDependencies": {
47
+ "@lucide/svelte": ">=0.511.0 <2",
47
48
  "svelte": "^5.0.0",
48
49
  "tailwindcss": "^4"
49
50
  },
50
51
  "publishConfig": {
51
52
  "access": "public"
52
53
  },
53
- "dependencies": {
54
- "@lucide/svelte": "^0.511.0"
55
- },
56
54
  "devDependencies": {
55
+ "@lucide/svelte": "^0.511.0",
57
56
  "@sveltejs/package": "^2.5.7",
58
57
  "@sveltejs/vite-plugin-svelte": "^4.0.4",
59
58
  "@testing-library/svelte": "^5.3.1",
@@ -151,6 +151,8 @@
151
151
  * Body 16px 1.55 0 400 Default body copy *
152
152
  * Caption 13px 1.50 0 400 Labels, metadata *
153
153
  * Mono 18px 1.50 0 500 Code, eyebrows *
154
+ * The ramp the guideline states, for marketing surfaces. *
155
+ * The scale the console renders is declared below it. *
154
156
  * */
155
157
  --text-display: 4rem; /* 64px */
156
158
  --text-h1: 2.75rem; /* 44px */
@@ -173,6 +175,57 @@
173
175
  --tracking-h2: -0.02em;
174
176
  --tracking-h3: -0.015em;
175
177
 
178
+ /* -- Typography - console type scale --------------------------- *
179
+ * Token Size Line Where it renders *
180
+ * text-xs 12px 16px labels, table heads, metadata, hints*
181
+ * text-sm 14px 20px body copy and every control *
182
+ * text-base 16px 24px a card title, the large button *
183
+ * text-lg 18px 28px a dialog title, a small stat value *
184
+ * text-xl 20px 28px the large wordmark *
185
+ * text-2xl 24px 32px a stat value, an empty-state glyph *
186
+ *
187
+ * Two scales, on purpose, and this is the one the components
188
+ * are drawn in.
189
+ *
190
+ * The ramp above is the brand guideline's, and it is a marketing
191
+ * ramp: it runs 64 / 44 / 32 / 22 / 16 / 13, holds nothing
192
+ * between 16px and 22px, and has no step at 14px at all. A
193
+ * console's body copy is 14px and its labels are 12px, both
194
+ * below the ramp's floor, so adopting the ramp wholesale would
195
+ * have meant restyling every screen to fit a scale drawn for a
196
+ * landing page. The ramp keeps its steps for the surfaces it was
197
+ * drawn for, and the two overlap at exactly the two steps a
198
+ * console does reach: PageHeader takes H2 for the page title and
199
+ * SectionHeading takes H3 for a section head.
200
+ *
201
+ * These are Tailwind's own steps at Tailwind's own values,
202
+ * stated here rather than inherited silently. Nothing renders
203
+ * one pixel differently for it. What changes is that the scale
204
+ * the components are built from is now declared beside the one
205
+ * they are not, so a designer reading this file sees both, and
206
+ * moving console body copy to 15px is one line here instead of
207
+ * 49 class attributes. Restated rather than renamed: an app
208
+ * writing `text-sm` in its own markup and a component writing it
209
+ * in the kit's must mean the same size, and a private spelling
210
+ * of the same 14px would be a second name for one thing.
211
+ *
212
+ * A step is declared with its leading, because a --text-* token
213
+ * sets font-size and nothing else. Only the six steps the kit
214
+ * actually renders are declared; the larger ones a marketing
215
+ * page reaches for stay as Tailwind ships them. */
216
+ --text-xs: 0.75rem; /* 12px */
217
+ --text-xs--line-height: calc(1 / 0.75); /* 16px */
218
+ --text-sm: 0.875rem; /* 14px */
219
+ --text-sm--line-height: calc(1.25 / 0.875); /* 20px */
220
+ --text-base: 1rem; /* 16px */
221
+ --text-base--line-height: calc(1.5 / 1); /* 24px */
222
+ --text-lg: 1.125rem; /* 18px */
223
+ --text-lg--line-height: calc(1.75 / 1.125); /* 28px */
224
+ --text-xl: 1.25rem; /* 20px */
225
+ --text-xl--line-height: calc(1.75 / 1.25); /* 28px */
226
+ --text-2xl: 1.5rem; /* 24px */
227
+ --text-2xl--line-height: calc(2 / 1.5); /* 32px */
228
+
176
229
  /* ── Spacing grid · 4px base (semantic shortcuts) ────────────── *
177
230
  * Tailwind's default 0.25rem (4px) grid is unchanged. *
178
231
  * These tokens add named slots for common component spacing. */