@ultimat3/ui 16.0.0 → 18.0.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/ui",
3
- "version": "16.0.0",
3
+ "version": "18.0.0",
4
4
  "description": "SolidJS design system: semantic design tokens, dark/RTL-ready SCSS modules, a11y primitives",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -35,7 +35,7 @@
35
35
  "LICENSE"
36
36
  ],
37
37
  "engines": {
38
- "bun": ">=1.3.0"
38
+ "bun": ">=1.4.0"
39
39
  },
40
40
  "scripts": {
41
41
  "typecheck": "tsc --noEmit -p tsconfig.json",
@@ -44,10 +44,10 @@
44
44
  "icons": "bun run src/icons/build-icons.ts"
45
45
  },
46
46
  "dependencies": {
47
- "@ultimat3/core": "16.0.0",
48
- "@ultimat3/i18n": "16.0.0",
49
- "@ultimat3/money": "16.0.0",
50
- "@ultimat3/time": "16.0.0"
47
+ "@ultimat3/core": "18.0.0",
48
+ "@ultimat3/i18n": "18.0.0",
49
+ "@ultimat3/money": "18.0.0",
50
+ "@ultimat3/time": "18.0.0"
51
51
  },
52
52
  "peerDependencies": {
53
53
  "solid-js": "^1.9.0"
@@ -2,6 +2,7 @@
2
2
  // real list always has (loading, error, empty, data). The error state renders an
3
3
  // UltimateError with the same code/cause/fix strings the terminal prints.
4
4
 
5
+ import { finiteCount } from '@ultimat3/core';
5
6
  import type { JSX } from 'solid-js';
6
7
  import { ariaBool } from '../a11y';
7
8
  import { cx } from '../cx';
@@ -61,7 +62,13 @@ export function DataTable<Row>(props: DataTableProps<Row>): JSX.Element {
61
62
 
62
63
  const body = (): JSX.Element => {
63
64
  if (props.loading === true) {
64
- return Array.from({ length: props.skeletonRows ?? 5 }, () => (
65
+ // `Array.from({ length: NaN })` is `[]`, so a busy tbody with no placeholders in it is the
66
+ // collapsed layout this state exists to prevent, reported as healthy; `Infinity` asks for
67
+ // 2^53 - 1 elements and dies with a bare, uncoded `RangeError` out of a render. `??` reaches
68
+ // neither, because `NaN` is not nullish. 0 stays legal: a loading table with no placeholders
69
+ // is a choice, and it is a visible one.
70
+ const placeholders = finiteCount('DataTable', 'skeletonRows', props.skeletonRows ?? 5, 0);
71
+ return Array.from({ length: placeholders }, () => (
65
72
  <tr>
66
73
  {props.columns.map(() => (
67
74
  <td>
@@ -1,6 +1,7 @@
1
1
  // Loading placeholder. Sized by the caller so the real content lands in the same
2
2
  // box — a skeleton that changes size on load is just a slower layout shift.
3
3
 
4
+ import { finiteCount } from '@ultimat3/core';
4
5
  import type { JSX } from 'solid-js';
5
6
  import { cx } from '../cx';
6
7
  import styles from './Skeleton.module.scss';
@@ -16,6 +17,12 @@ export interface SkeletonProps {
16
17
  }
17
18
 
18
19
  export function Skeleton(props: SkeletonProps): JSX.Element {
20
+ // `Math.max` IS NOT A VALIDATOR — it propagates `NaN`, so `Math.max(1, NaN)` is `NaN` and
21
+ // `Array.from({ length: NaN })` is `[]`: the placeholder that holds the box while content loads
22
+ // rendered nothing at all, which is the layout shift it exists to prevent. The clamp stays, and
23
+ // it is the screened value it clamps — `lines: 0` still means one line, exactly as before.
24
+ const lines = (): number => Math.max(1, finiteCount('Skeleton', 'lines', props.lines ?? 1, 0));
25
+
19
26
  const one = (index: number): JSX.Element => (
20
27
  <span
21
28
  class={cx(styles['skeleton'], styles[`shape-${props.shape ?? 'text'}`])}
@@ -28,7 +35,7 @@ export function Skeleton(props: SkeletonProps): JSX.Element {
28
35
 
29
36
  return (
30
37
  <span aria-hidden="true" class={cx(styles['group'], props.class)}>
31
- {Array.from({ length: Math.max(1, props.lines ?? 1) }, (_unused, index) => one(index))}
38
+ {Array.from({ length: lines() }, (_unused, index) => one(index))}
32
39
  </span>
33
40
  );
34
41
  }
@@ -2,6 +2,8 @@
2
2
  // every debounced keystroke, so a no-JS form round-trip and a live filter narrow the list the
3
3
  // same way — two matchers would eventually disagree, and the user would see the list flicker.
4
4
 
5
+ import { finiteCount } from '@ultimat3/core';
6
+
5
7
  export interface ComboboxOption {
6
8
  /** The text the field takes when the suggestion is picked — and what the form submits. */
7
9
  value: string;
@@ -36,8 +38,14 @@ export function filterOptions(
36
38
  query = '',
37
39
  limit: number = COMBOBOX_LIMIT,
38
40
  ): readonly ComboboxOption[] {
41
+ // A BARE PARAMETER DEFAULT, which `??` never guards and `scripts/finite-bounds.ts` cannot see:
42
+ // `slice(0, NaN)` is `[]`, so every suggestion vanishes and `<Combobox>` renders "no results" for
43
+ // a list that matched. `Infinity` is the mirror — every option rendered, out of the function
44
+ // whose own doc says long lists are a scroll, not an answer. 0 stays legal: it renders nothing,
45
+ // on purpose and visibly.
46
+ const cap = finiteCount('filterOptions', 'limit', limit, 0);
39
47
  const needle = normalizeQuery(query);
40
- if (needle === '') return options.slice(0, limit);
48
+ if (needle === '') return options.slice(0, cap);
41
49
 
42
50
  const prefix: ComboboxOption[] = [];
43
51
  const contains: ComboboxOption[] = [];
@@ -46,5 +54,5 @@ export function filterOptions(
46
54
  if (text.startsWith(needle)) prefix.push(option);
47
55
  else if (text.includes(needle)) contains.push(option);
48
56
  }
49
- return [...prefix, ...contains].slice(0, limit);
57
+ return [...prefix, ...contains].slice(0, cap);
50
58
  }