@luxfi/ui 7.4.9 → 7.4.11

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 (82) hide show
  1. package/dist/accordion.cjs +107 -166
  2. package/dist/accordion.d.cts +18 -51
  3. package/dist/accordion.d.ts +18 -51
  4. package/dist/accordion.js +109 -166
  5. package/dist/chrome.cjs +193 -379
  6. package/dist/chrome.js +195 -379
  7. package/dist/collapsible.cjs +35 -26
  8. package/dist/collapsible.d.cts +2 -1
  9. package/dist/collapsible.d.ts +2 -1
  10. package/dist/collapsible.js +34 -24
  11. package/dist/dialog.d.cts +1 -1
  12. package/dist/dialog.d.ts +1 -1
  13. package/dist/drawer.cjs +92 -115
  14. package/dist/drawer.d.cts +43 -10
  15. package/dist/drawer.d.ts +43 -10
  16. package/dist/drawer.js +94 -115
  17. package/dist/field.cjs +60 -109
  18. package/dist/field.d.cts +0 -1
  19. package/dist/field.d.ts +0 -1
  20. package/dist/field.js +61 -109
  21. package/dist/heading.cjs +10 -17
  22. package/dist/heading.d.cts +0 -2
  23. package/dist/heading.d.ts +0 -2
  24. package/dist/heading.js +10 -17
  25. package/dist/icon-button.cjs +82 -82
  26. package/dist/icon-button.d.cts +16 -12
  27. package/dist/icon-button.d.ts +16 -12
  28. package/dist/icon-button.js +82 -82
  29. package/dist/index.cjs +1073 -1419
  30. package/dist/index.d.cts +1 -5
  31. package/dist/index.d.ts +1 -5
  32. package/dist/index.js +1075 -1414
  33. package/dist/input-group.cjs +11 -12
  34. package/dist/input-group.js +11 -12
  35. package/dist/input.cjs +27 -27
  36. package/dist/input.d.cts +21 -1
  37. package/dist/input.d.ts +21 -1
  38. package/dist/input.js +25 -28
  39. package/dist/menu.cjs +160 -271
  40. package/dist/menu.d.cts +41 -57
  41. package/dist/menu.d.ts +41 -57
  42. package/dist/menu.js +161 -251
  43. package/dist/next.cjs +6 -10
  44. package/dist/next.js +5 -9
  45. package/dist/popover.cjs +1 -1
  46. package/dist/popover.js +1 -1
  47. package/dist/select.cjs +40 -39
  48. package/dist/select.d.cts +35 -4
  49. package/dist/select.d.ts +35 -4
  50. package/dist/select.js +40 -38
  51. package/dist/separator.cjs +8 -33
  52. package/dist/separator.js +8 -33
  53. package/dist/tabs.cjs +108 -197
  54. package/dist/tabs.d.cts +16 -60
  55. package/dist/tabs.d.ts +16 -60
  56. package/dist/tabs.js +109 -196
  57. package/dist/textarea.cjs +56 -27
  58. package/dist/textarea.js +57 -28
  59. package/dist/toaster.cjs +97 -79
  60. package/dist/toaster.d.cts +7 -8
  61. package/dist/toaster.d.ts +7 -8
  62. package/dist/toaster.js +78 -80
  63. package/package.json +2 -16
  64. package/src/accordion.tsx +173 -227
  65. package/src/collapsible.tsx +55 -52
  66. package/src/drawer.tsx +123 -115
  67. package/src/engine.ts +0 -25
  68. package/src/field.tsx +68 -124
  69. package/src/heading.tsx +22 -26
  70. package/src/icon-button.tsx +134 -141
  71. package/src/ink.tsx +37 -0
  72. package/src/input-group.tsx +21 -12
  73. package/src/input.tsx +41 -32
  74. package/src/menu.tsx +224 -397
  75. package/src/next.ts +32 -1
  76. package/src/popover.tsx +1 -1
  77. package/src/select.tsx +80 -59
  78. package/src/separator.tsx +13 -34
  79. package/src/tabs.tsx +166 -296
  80. package/src/textarea.tsx +19 -29
  81. package/src/toaster.tsx +118 -86
  82. package/tokens.css +61 -0
package/src/toaster.tsx CHANGED
@@ -1,89 +1,73 @@
1
1
  'use client';
2
2
 
3
- import type React from 'react';
4
- import { Toaster as SonnerToaster, toast } from 'sonner';
3
+ import { Text, Toast, ToastProvider, ToastViewport, XStack, YStack } from '@hanzo/gui';
4
+ import * as React from 'react';
5
+
6
+ // Toasts, on the gui engine.
7
+ //
8
+ // The published surface is imperative — `toaster.error({ title })` from
9
+ // anywhere, including code that is not a component — while gui's toast is a
10
+ // hook (`useToastController`) that only a descendant of a provider can call.
11
+ // The bridge is a module-level store: callers push onto it, `<Toaster/>`
12
+ // subscribes with `useSyncExternalStore` and renders one gui `Toast` per entry.
13
+ // That is the whole adaptation, and it is why `sonner` is gone rather than
14
+ // wrapped — a second toast implementation would have been a second way to do
15
+ // the same thing.
5
16
 
6
17
  const SECOND = 1_000;
7
18
  const DURATION = 10 * SECOND;
8
19
 
20
+ type ToastType = 'info' | 'warning' | 'success' | 'error' | 'loading';
21
+
9
22
  interface ToastOptions {
10
23
  readonly id?: string | number;
11
24
  readonly title?: string;
12
25
  readonly description?: string;
13
- readonly type?: 'info' | 'warning' | 'success' | 'error' | 'loading';
26
+ readonly type?: ToastType;
14
27
  readonly duration?: number;
15
28
  readonly action?: { readonly label: string; readonly onClick: () => void };
16
29
  readonly onStatusChange?: (details: { readonly status: string }) => void;
17
30
  }
18
31
 
19
- function mapOptions(opts: ToastOptions): Parameters<typeof toast>[1] {
20
- const external: Parameters<typeof toast>[1] = {
21
- id: opts.id,
22
- description: opts.description,
23
- duration: opts.duration,
24
- };
25
-
26
- if (opts.action) {
27
- external.action = {
28
- label: opts.action.label,
29
- onClick: opts.action.onClick,
30
- };
31
- }
32
-
33
- if (opts.onStatusChange) {
34
- const cb = opts.onStatusChange;
35
- external.onDismiss = () => cb({ status: 'unmounted' });
36
- external.onAutoClose = () => cb({ status: 'unmounted' });
37
- }
38
-
39
- return external;
32
+ interface Entry extends ToastOptions {
33
+ readonly id: string | number;
40
34
  }
41
35
 
42
- function create(opts: ToastOptions): string | number {
43
- const external = mapOptions(opts);
44
- const title = opts.title ?? '';
45
-
46
- switch (opts.type) {
47
- case 'success':
48
- return toast.success(title, external);
49
- case 'error':
50
- return toast.error(title, external);
51
- case 'warning':
52
- return toast.warning(title, external);
53
- case 'loading':
54
- return toast.loading(title, external);
55
- case 'info':
56
- default:
57
- return toast.info(title, external);
58
- }
59
- }
36
+ // ── the store ──────────────────────────────────────────────────────────────
60
37
 
61
- function update(id: string | number, opts: ToastOptions): void {
62
- const title = opts.title ?? '';
63
-
64
- // Sonner does not have a dedicated update method.
65
- // Re-calling a typed toast with the same id replaces it in-place.
66
- const external = mapOptions({ ...opts, id });
67
-
68
- switch (opts.type) {
69
- case 'success':
70
- toast.success(title, external);
71
- break;
72
- case 'error':
73
- toast.error(title, external);
74
- break;
75
- case 'warning':
76
- toast.warning(title, external);
77
- break;
78
- case 'loading':
79
- toast.loading(title, external);
80
- break;
81
- case 'info':
82
- default:
83
- toast.info(title, external);
84
- break;
85
- }
86
- }
38
+ let entries: ReadonlyArray<Entry> = [];
39
+ const listeners = new Set<() => void>();
40
+ let counter = 0;
41
+
42
+ const emit = () => listeners.forEach((l) => l());
43
+
44
+ const subscribe = (l: () => void) => {
45
+ listeners.add(l);
46
+ return () => { listeners.delete(l); };
47
+ };
48
+
49
+ /** `useSyncExternalStore` compares by identity, so the array is replaced, never mutated. */
50
+ const snapshot = () => entries;
51
+
52
+ const remove = (id: string | number): void => {
53
+ const found = entries.find((e) => e.id === id);
54
+ entries = entries.filter((e) => e.id !== id);
55
+ emit();
56
+ found?.onStatusChange?.({ status: 'unmounted' });
57
+ };
58
+
59
+ const create = (opts: ToastOptions): string | number => {
60
+ const id = opts.id ?? `toast-${ ++counter }`;
61
+ const entry: Entry = { ...opts, id };
62
+ entries = [ ...entries.filter((e) => e.id !== id), entry ];
63
+ emit();
64
+ return id;
65
+ };
66
+
67
+ const update = (id: string | number, opts: ToastOptions): void => {
68
+ entries = entries.map((e) => (e.id === id ? { ...e, ...opts, id } : e));
69
+ emit();
70
+ };
87
71
 
88
72
  export const toaster = {
89
73
  create,
@@ -91,27 +75,75 @@ export const toaster = {
91
75
  error: (opts: ToastOptions) => create({ ...opts, type: 'error' }),
92
76
  loading: (opts: ToastOptions) => create({ ...opts, type: 'loading' }),
93
77
  update,
94
- remove: (id: string | number) => toast.dismiss(id),
78
+ remove,
95
79
  } as const;
96
80
 
81
+ // ── presentation ───────────────────────────────────────────────────────────
82
+
83
+ /** Border colour carries the severity; the surface stays the popover surface. */
84
+ const EDGE: Record<ToastType, string> = {
85
+ success: 'var(--color-status-good)',
86
+ error: 'var(--color-status-bad)',
87
+ warning: 'var(--color-status-warn)',
88
+ loading: 'var(--color-border-divider)',
89
+ info: 'var(--color-border-divider)',
90
+ };
91
+
92
+ const One = ({ entry }: { entry: Entry }) => (
93
+ <Toast
94
+ key={ entry.id }
95
+ duration={ entry.duration ?? DURATION }
96
+ onOpenChange={ (open: boolean) => { if (!open) remove(entry.id); } }
97
+ unstyled
98
+ flexDirection="column"
99
+ gap={ 4 }
100
+ padding={ 12 }
101
+ borderRadius={ 8 }
102
+ borderWidth={ 1 }
103
+ borderColor={ EDGE[entry.type ?? 'info'] as never }
104
+ backgroundColor={ 'var(--color-popover-bg)' as never }
105
+ transition="quick"
106
+ enterStyle={{ opacity: 0, y: -8 }}
107
+ exitStyle={{ opacity: 0, y: -8 }}
108
+ >
109
+ { entry.title ? (
110
+ <Toast.Title unstyled fontSize={ 14 } fontWeight="600" color={ 'var(--color-text-primary)' as never }>
111
+ { entry.title }
112
+ </Toast.Title>
113
+ ) : null }
114
+ { entry.description ? (
115
+ <Toast.Description unstyled fontSize={ 13 } color={ 'var(--color-text-secondary)' as never }>
116
+ { entry.description }
117
+ </Toast.Description>
118
+ ) : null }
119
+ { /* Toast.Action is the accessible affordance and takes no styling of its
120
+ own; the look belongs to the Text it wraps. */ }
121
+ { entry.action ? (
122
+ <XStack marginTop={ 4 }>
123
+ <Toast.Action altText={ entry.action.label } onPress={ entry.action.onClick }>
124
+ <Text
125
+ cursor="pointer"
126
+ fontSize={ 13 }
127
+ fontWeight="500"
128
+ color={ 'var(--color-link-primary)' as never }
129
+ >
130
+ { entry.action.label }
131
+ </Text>
132
+ </Toast.Action>
133
+ </XStack>
134
+ ) : null }
135
+ </Toast>
136
+ );
137
+
97
138
  export const Toaster: React.FC = () => {
139
+ const list = React.useSyncExternalStore(subscribe, snapshot, snapshot);
140
+
98
141
  return (
99
- <SonnerToaster
100
- position="top-right"
101
- richColors
102
- closeButton
103
- duration={ DURATION }
104
- offset={{ top: 12, right: 12, bottom: 12, left: 12 }}
105
- mobileOffset={{ top: 16, right: 16, bottom: 16, left: 16 }}
106
- toastOptions={{
107
- className: 'font-sans',
108
- style: {
109
- fontFamily: 'var(--font-sans, "Geist", sans-serif)',
110
- color: 'var(--color-text-primary)',
111
- backgroundColor: 'var(--color-bg-primary)',
112
- borderColor: 'var(--color-border-divider)',
113
- },
114
- }}
115
- />
142
+ <ToastProvider>
143
+ <YStack>
144
+ { list.map((entry) => <One key={ entry.id } entry={ entry }/>) }
145
+ </YStack>
146
+ <ToastViewport position="absolute" top={ 12 } right={ 12 }/>
147
+ </ToastProvider>
116
148
  );
117
149
  };
package/tokens.css CHANGED
@@ -97,6 +97,7 @@
97
97
  /* --- Text --- */
98
98
  --color-text-primary: var(--lux-ink-80);
99
99
  --color-text-secondary: var(--lux-n-500);
100
+ --color-text-disabled: var(--lux-ink-24);
100
101
  --color-text-error: var(--lux-state-error);
101
102
  --color-text-success: var(--lux-state-success);
102
103
 
@@ -174,8 +175,17 @@
174
175
  --color-input-border: var(--lux-n-100);
175
176
  --color-input-border-hover: var(--lux-n-200);
176
177
  --color-input-border-focus: var(--lux-n-600);
178
+ /* Three states that were being ASKED for and never answered: input.tsx and
179
+ textarea.tsx reference these, and nothing declared them. Their written
180
+ fallback was `theme(colors.red.500)`-style, which is a Tailwind 3 function
181
+ that v4 does not resolve inside an arbitrary value — so the whole
182
+ declaration was discarded. An invalid field drew no error border and the
183
+ `filled` variant drew no fill, in both themes. */
184
+ --color-input-border-error: var(--lux-state-error);
185
+ --color-input-filled-bg: var(--lux-n-50);
177
186
  /* n-400 on white is 2.5:1 — under the 3:1 non-text minimum. n-500 clears it. */
178
187
  --color-input-placeholder: var(--lux-n-500);
188
+ --color-input-placeholder-focus: var(--lux-n-400);
179
189
 
180
190
  /* --- Tags --- */
181
191
  --color-tag-subtle-bg: var(--lux-ink-04);
@@ -277,6 +287,11 @@
277
287
  /* --- Controls --- */
278
288
  --color-switch-bg: var(--lux-n-300);
279
289
  --color-progress-track: var(--lux-n-100);
290
+ /* The FILL. Referenced by progress.tsx and never declared, so the bar drew
291
+ its track and nothing else. */
292
+ --color-progress-indicator: var(--lux-n-900);
293
+ /* The row highlight shared by every menu and popover list. */
294
+ --color-popover-item-hover: var(--lux-ink-06);
280
295
 
281
296
  /* --- Chrome / charts --- */
282
297
  --color-stats-bg: var(--lux-n-50);
@@ -488,6 +503,7 @@
488
503
  /* --- Text --- */
489
504
  --color-text-primary: var(--lux-paper-80);
490
505
  --color-text-secondary: var(--lux-n-400);
506
+ --color-text-disabled: var(--lux-paper-24);
491
507
  --color-text-error: var(--lux-state-error);
492
508
  --color-text-success: var(--lux-state-success);
493
509
 
@@ -565,7 +581,10 @@
565
581
  --color-input-border: var(--lux-n-800);
566
582
  --color-input-border-hover: var(--lux-n-700);
567
583
  --color-input-border-focus: var(--lux-n-400);
584
+ --color-input-border-error: var(--lux-state-error);
585
+ --color-input-filled-bg: var(--lux-n-800);
568
586
  --color-input-placeholder: var(--lux-n-500);
587
+ --color-input-placeholder-focus: var(--lux-n-600);
569
588
 
570
589
  /* --- Tags --- */
571
590
  --color-tag-subtle-bg: var(--lux-paper-06);
@@ -664,6 +683,8 @@
664
683
  /* --- Controls --- */
665
684
  --color-switch-bg: var(--lux-paper-24);
666
685
  --color-progress-track: var(--lux-paper-06);
686
+ --color-progress-indicator: var(--lux-n-0);
687
+ --color-popover-item-hover: var(--lux-paper-06);
667
688
 
668
689
  /* --- Chrome / charts --- */
669
690
  --color-stats-bg: var(--lux-paper-04);
@@ -838,6 +859,46 @@
838
859
  .lux-interactive:disabled,
839
860
  .lux-interactive[aria-disabled='true'] { transform: none }
840
861
 
862
+ /* The floating field label. This is CSS rather than a gui prop because it keys
863
+ off the state of a SIBLING (`:focus` / `:placeholder-shown` of the control),
864
+ and a sibling combinator is a selector — there is no prop that expresses it.
865
+ The label rests centred over the empty control and rises into the top of the
866
+ frame once the control is focused or holds a value. */
867
+ .lux-field-float {
868
+ transform: translateY(-50%);
869
+ transform-origin: top left;
870
+ transition: top var(--lux-dur-hover) var(--lux-ease-out),
871
+ transform var(--lux-dur-hover) var(--lux-ease-out);
872
+ }
873
+ .lux-field-control:focus ~ .lux-field-float,
874
+ .lux-field-control:not(:placeholder-shown) ~ .lux-field-float {
875
+ top: 4px;
876
+ transform: translateY(0) scale(0.75);
877
+ }
878
+
879
+ /* An accordion row's label brightens while the ROW is hovered. Also a
880
+ selector — the state belongs to the row, the colour to the label inside it —
881
+ so the row sets a colour and its contents inherit. */
882
+ .lux-accordion-trigger { color: var(--color-text-primary) }
883
+ .lux-accordion-trigger:hover { color: var(--color-hover) }
884
+
885
+ /* Text controls: the two states no prop reaches. `::placeholder` styles a
886
+ pseudo-element the input owns rather than the input, and `:read-only` is a
887
+ selector with no prop counterpart — gui offers disabled, hover and focus, not
888
+ this. Everything else about an Input or Textarea is a prop. */
889
+ /* A keyframe is not a prop — gui's transitions interpolate between two states,
890
+ they do not loop. The spinner rotates forever, so it is CSS. */
891
+ .lux-spin { animation: lux-spin 0.8s linear infinite }
892
+
893
+ /* Size the ICON inside a control. A descendant selector has no prop form, so
894
+ the control publishes its icon size and this rule applies it. */
895
+ .lux-icon svg { width: var(--icon-size, 20px); height: var(--icon-size, 20px) }
896
+
897
+ .lux-control::placeholder { color: var(--color-input-placeholder) }
898
+ .lux-control:focus::placeholder { color: var(--color-input-placeholder-focus) }
899
+ .lux-control:read-only { opacity: 0.7 }
900
+ .lux-control[data-invalid] { border-color: var(--color-input-border-error) }
901
+
841
902
  /* ================================================================
842
903
  6c — The loading state. Two primitives, one job each.
843
904