@ultimat3/ui 31.17.0 → 31.18.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": "31.17.0",
3
+ "version": "31.18.0",
4
4
  "description": "SolidJS design system: semantic design tokens, dark/RTL-ready SCSS modules, a11y primitives",
5
5
  "keywords": [
6
6
  "ultimate",
@@ -58,11 +58,11 @@
58
58
  "icons": "bun run src/icons/build-icons.ts"
59
59
  },
60
60
  "dependencies": {
61
- "@ultimat3/core": "31.17.0",
62
- "@ultimat3/i18n": "31.17.0",
63
- "@ultimat3/money": "31.17.0",
64
- "@ultimat3/schema": "31.17.0",
65
- "@ultimat3/time": "31.17.0"
61
+ "@ultimat3/core": "31.18.0",
62
+ "@ultimat3/i18n": "31.18.0",
63
+ "@ultimat3/money": "31.18.0",
64
+ "@ultimat3/schema": "31.18.0",
65
+ "@ultimat3/time": "31.18.0"
66
66
  },
67
67
  "peerDependencies": {
68
68
  "solid-js": "^1.9.0"
@@ -59,8 +59,11 @@ export function Combobox(props: ComboboxProps): JSX.Element {
59
59
  rt.onCleanup(() => notify.cancel());
60
60
  });
61
61
 
62
- const matches = (): readonly ComboboxOption[] =>
63
- filterOptions(props.options, props.value ?? '', props.limit);
62
+ // One filter pass per change of query or options: the datalist, the status class and its count
63
+ // all read it, and a plain function ran the O(n) pass once per read.
64
+ const matches = rt.createMemo((): readonly ComboboxOption[] =>
65
+ filterOptions(props.options, props.value ?? '', props.limit),
66
+ );
64
67
 
65
68
  const describedBy = (): string =>
66
69
  [props['aria-describedby'], statusId].filter((id) => id !== undefined).join(' ');
@@ -64,7 +64,11 @@ export function Drawer(props: DrawerProps): JSX.Element {
64
64
  rt.createEffect(() => {
65
65
  const dialog = element;
66
66
  if (dialog === undefined) return;
67
- if (props.open && !dialog.open) dialog.showModal();
67
+ if (props.open && !dialog.open) {
68
+ // `snap` is where a sheet OPENS: last time's drag to `full` does not decide this time.
69
+ setSnap(props.snap ?? 'half');
70
+ dialog.showModal();
71
+ }
68
72
  if (!props.open && dialog.open) dialog.close();
69
73
  });
70
74
 
@@ -92,14 +92,16 @@ export function SearchableSelect(props: SearchableSelectProps): JSX.Element {
92
92
  finiteOption('SearchableSelect', 'debounceMs', props.debounceMs ?? DEBOUNCE_DEFAULT_MS),
93
93
  );
94
94
 
95
- const view = (): SearchableView => {
95
+ // One pass per keystroke: the rows, the status text and its class, and every key handler read
96
+ // the view, and a plain function re-ran the O(n) check and ranking once per read.
97
+ const view = rt.createMemo((): SearchableView => {
96
98
  checkOptions(props.options, chosen());
97
99
  const limit = finiteCount('SearchableSelect', 'limit', props.limit ?? COMBOBOX_LIMIT, 0);
98
100
  // Under `onSearch` the caller already matched: ranking again could drop what it found.
99
101
  return props.onSearch === undefined
100
102
  ? searchableView(props.options, query() ?? '', limit)
101
103
  : searchableView(props.options, '', limit);
102
- };
104
+ });
103
105
  const picked = (): readonly SearchableOption[] => chosenOptions(props.options, chosen());
104
106
  const text = (): string => query() ?? (many() ? '' : (picked()[0]?.label ?? ''));
105
107
 
@@ -8,11 +8,14 @@
8
8
 
9
9
  import type { JSX } from 'solid-js';
10
10
  import type { Politeness } from '../a11y';
11
+ import { solid } from '../theme/solid-adapter';
11
12
  import { type ToastItem, visibleToasts } from '../toast/toast-state';
12
13
  import type { ToastStore } from '../toast/toast-store';
13
14
  import { useToasts } from '../toast/use-toasts';
14
15
  import { Button } from './Button';
16
+ import { keyedRows } from './data-table-rows';
15
17
  import { Toast, type ToastPlacement, ToastRegion } from './Toast';
18
+ import { thunk } from './thunk';
16
19
 
17
20
  export interface ToasterProps {
18
21
  /** The queue. One per app — `toastStore()` in the island that mounts this. */
@@ -27,6 +30,7 @@ export interface ToasterProps {
27
30
  }
28
31
 
29
32
  export function Toaster(props: ToasterProps): JSX.Element {
33
+ const rt = solid();
30
34
  const queue = useToasts(props.store);
31
35
 
32
36
  /**
@@ -34,7 +38,7 @@ export function Toaster(props: ToasterProps): JSX.Element {
34
38
  * it disappears on a timer, and a keyboard user who was typing when it arrived never reached it.
35
39
  * Taking the undo also dismisses — the offer has been answered.
36
40
  */
37
- const actionSlot = (item: ToastItem): JSX.Element => {
41
+ const actionSlot = (item: ToastItem, id: string): JSX.Element => {
38
42
  const action = item.action;
39
43
  if (action === undefined) return null;
40
44
  return (
@@ -44,7 +48,7 @@ export function Toaster(props: ToasterProps): JSX.Element {
44
48
  tone={item.tone}
45
49
  onClick={() => {
46
50
  action.onAction();
47
- props.store.dismiss(item.id);
51
+ props.store.dismiss(id);
48
52
  }}
49
53
  >
50
54
  {action.label}
@@ -52,6 +56,25 @@ export function Toaster(props: ToasterProps): JSX.Element {
52
56
  );
53
57
  };
54
58
 
59
+ // One node per toast id, built once in the body: a publish that adds toast B leaves toast A's
60
+ // <li> where it was, so the live region reads only B (`aria-atomic="false"`) and a keyboard user
61
+ // on A's Undo keeps their focus. A plain `.map` rebuilt every visible <li> on every arrival.
62
+ const toastNodes = keyedRows<ToastItem, JSX.Element>(
63
+ rt,
64
+ (item) => item.id,
65
+ (item, id) => (
66
+ <Toast
67
+ title={item().title}
68
+ tone={item().tone}
69
+ dismissLabel={props.dismissLabel}
70
+ action={actionSlot(item(), id)}
71
+ onDismiss={() => props.store.dismiss(id)}
72
+ >
73
+ {item().message}
74
+ </Toast>
75
+ ),
76
+ );
77
+
55
78
  return (
56
79
  <ToastRegion
57
80
  label={props.label}
@@ -61,17 +84,7 @@ export function Toaster(props: ToasterProps): JSX.Element {
61
84
  onHold={(reason) => props.store.hold(reason)}
62
85
  onRelease={(reason) => props.store.release(reason)}
63
86
  >
64
- {visibleToasts(queue()).map((item) => (
65
- <Toast
66
- title={item.title}
67
- tone={item.tone}
68
- dismissLabel={props.dismissLabel}
69
- action={actionSlot(item)}
70
- onDismiss={() => props.store.dismiss(item.id)}
71
- >
72
- {item.message}
73
- </Toast>
74
- ))}
87
+ {thunk(() => toastNodes(visibleToasts(queue())))}
75
88
  </ToastRegion>
76
89
  );
77
90
  }