@luxfi/ui 7.4.10 → 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.
- package/dist/accordion.cjs +107 -166
- package/dist/accordion.d.cts +18 -51
- package/dist/accordion.d.ts +18 -51
- package/dist/accordion.js +109 -166
- package/dist/chrome.cjs +193 -379
- package/dist/chrome.js +195 -379
- package/dist/collapsible.cjs +35 -26
- package/dist/collapsible.d.cts +2 -1
- package/dist/collapsible.d.ts +2 -1
- package/dist/collapsible.js +34 -24
- package/dist/dialog.d.cts +1 -1
- package/dist/dialog.d.ts +1 -1
- package/dist/drawer.cjs +92 -115
- package/dist/drawer.d.cts +43 -10
- package/dist/drawer.d.ts +43 -10
- package/dist/drawer.js +94 -115
- package/dist/field.cjs +60 -109
- package/dist/field.d.cts +0 -1
- package/dist/field.d.ts +0 -1
- package/dist/field.js +61 -109
- package/dist/heading.cjs +10 -17
- package/dist/heading.d.cts +0 -2
- package/dist/heading.d.ts +0 -2
- package/dist/heading.js +10 -17
- package/dist/icon-button.cjs +82 -82
- package/dist/icon-button.d.cts +16 -12
- package/dist/icon-button.d.ts +16 -12
- package/dist/icon-button.js +82 -82
- package/dist/index.cjs +1073 -1419
- package/dist/index.d.cts +1 -5
- package/dist/index.d.ts +1 -5
- package/dist/index.js +1075 -1414
- package/dist/input-group.cjs +11 -12
- package/dist/input-group.js +11 -12
- package/dist/input.cjs +27 -27
- package/dist/input.d.cts +21 -1
- package/dist/input.d.ts +21 -1
- package/dist/input.js +25 -28
- package/dist/menu.cjs +160 -271
- package/dist/menu.d.cts +41 -57
- package/dist/menu.d.ts +41 -57
- package/dist/menu.js +161 -251
- package/dist/popover.cjs +1 -1
- package/dist/popover.js +1 -1
- package/dist/select.cjs +40 -39
- package/dist/select.d.cts +35 -4
- package/dist/select.d.ts +35 -4
- package/dist/select.js +40 -38
- package/dist/separator.cjs +8 -33
- package/dist/separator.js +8 -33
- package/dist/tabs.cjs +108 -197
- package/dist/tabs.d.cts +16 -60
- package/dist/tabs.d.ts +16 -60
- package/dist/tabs.js +109 -196
- package/dist/textarea.cjs +56 -27
- package/dist/textarea.js +57 -28
- package/dist/toaster.cjs +97 -79
- package/dist/toaster.d.cts +7 -8
- package/dist/toaster.d.ts +7 -8
- package/dist/toaster.js +78 -80
- package/package.json +2 -16
- package/src/accordion.tsx +173 -227
- package/src/collapsible.tsx +55 -52
- package/src/drawer.tsx +123 -115
- package/src/field.tsx +68 -124
- package/src/heading.tsx +22 -26
- package/src/icon-button.tsx +134 -141
- package/src/ink.tsx +37 -0
- package/src/input-group.tsx +21 -12
- package/src/input.tsx +41 -32
- package/src/menu.tsx +224 -397
- package/src/popover.tsx +1 -1
- package/src/select.tsx +80 -59
- package/src/separator.tsx +13 -34
- package/src/tabs.tsx +166 -296
- package/src/textarea.tsx +19 -29
- package/src/toaster.tsx +118 -86
- package/tokens.css +61 -0
package/src/toaster.tsx
CHANGED
|
@@ -1,89 +1,73 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import
|
|
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?:
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
|
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
|
-
<
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
|