@luxfi/ui 7.4.10 → 7.4.12

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 (137) 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/alert.cjs +68 -46
  6. package/dist/alert.js +69 -46
  7. package/dist/avatar.cjs +49 -46
  8. package/dist/avatar.js +52 -48
  9. package/dist/badge.cjs +82 -49
  10. package/dist/badge.js +83 -50
  11. package/dist/checkbox.cjs +42 -75
  12. package/dist/checkbox.js +43 -76
  13. package/dist/chrome.cjs +196 -386
  14. package/dist/chrome.js +198 -386
  15. package/dist/close-button.cjs +3 -7
  16. package/dist/close-button.js +3 -7
  17. package/dist/collapsible.cjs +35 -26
  18. package/dist/collapsible.d.cts +2 -1
  19. package/dist/collapsible.d.ts +2 -1
  20. package/dist/collapsible.js +34 -24
  21. package/dist/dialog.cjs +3 -7
  22. package/dist/dialog.d.cts +1 -1
  23. package/dist/dialog.d.ts +1 -1
  24. package/dist/dialog.js +3 -7
  25. package/dist/drawer.cjs +95 -122
  26. package/dist/drawer.d.cts +43 -10
  27. package/dist/drawer.d.ts +43 -10
  28. package/dist/drawer.js +97 -122
  29. package/dist/empty-state.cjs +13 -13
  30. package/dist/empty-state.js +13 -13
  31. package/dist/expiration-selector.cjs +58 -63
  32. package/dist/expiration-selector.js +58 -63
  33. package/dist/field.cjs +60 -109
  34. package/dist/field.d.cts +0 -1
  35. package/dist/field.d.ts +0 -1
  36. package/dist/field.js +61 -109
  37. package/dist/greeks-display.cjs +48 -39
  38. package/dist/greeks-display.js +48 -39
  39. package/dist/heading.cjs +10 -17
  40. package/dist/heading.d.cts +0 -2
  41. package/dist/heading.d.ts +0 -2
  42. package/dist/heading.js +10 -17
  43. package/dist/icon-button.cjs +82 -82
  44. package/dist/icon-button.d.cts +16 -12
  45. package/dist/icon-button.d.ts +16 -12
  46. package/dist/icon-button.js +82 -82
  47. package/dist/index.cjs +1812 -2105
  48. package/dist/index.d.cts +1 -5
  49. package/dist/index.d.ts +1 -5
  50. package/dist/index.js +1814 -2100
  51. package/dist/input-group.cjs +11 -12
  52. package/dist/input-group.js +11 -12
  53. package/dist/input.cjs +27 -27
  54. package/dist/input.d.cts +21 -1
  55. package/dist/input.d.ts +21 -1
  56. package/dist/input.js +25 -28
  57. package/dist/menu.cjs +160 -271
  58. package/dist/menu.d.cts +41 -57
  59. package/dist/menu.d.ts +41 -57
  60. package/dist/menu.js +161 -251
  61. package/dist/option-chain.cjs +89 -67
  62. package/dist/option-chain.js +89 -67
  63. package/dist/option-position.cjs +88 -85
  64. package/dist/option-position.js +88 -85
  65. package/dist/pnl-diagram.cjs +20 -20
  66. package/dist/pnl-diagram.js +20 -20
  67. package/dist/popover.cjs +4 -8
  68. package/dist/popover.js +4 -8
  69. package/dist/progress.cjs +15 -22
  70. package/dist/progress.d.cts +6 -6
  71. package/dist/progress.d.ts +6 -6
  72. package/dist/progress.js +15 -22
  73. package/dist/radio.cjs +35 -67
  74. package/dist/radio.d.cts +18 -18
  75. package/dist/radio.d.ts +18 -18
  76. package/dist/radio.js +36 -68
  77. package/dist/select.cjs +40 -39
  78. package/dist/select.d.cts +35 -4
  79. package/dist/select.d.ts +35 -4
  80. package/dist/select.js +40 -38
  81. package/dist/separator.cjs +8 -33
  82. package/dist/separator.js +8 -33
  83. package/dist/slider.cjs +33 -49
  84. package/dist/slider.js +34 -50
  85. package/dist/switch.cjs +48 -55
  86. package/dist/switch.js +49 -56
  87. package/dist/tabs.cjs +108 -197
  88. package/dist/tabs.d.cts +16 -60
  89. package/dist/tabs.d.ts +16 -60
  90. package/dist/tabs.js +109 -196
  91. package/dist/tag.cjs +112 -69
  92. package/dist/tag.js +113 -69
  93. package/dist/textarea.cjs +56 -27
  94. package/dist/textarea.js +57 -28
  95. package/dist/toaster.cjs +97 -79
  96. package/dist/toaster.d.cts +7 -8
  97. package/dist/toaster.d.ts +7 -8
  98. package/dist/toaster.js +78 -80
  99. package/dist/tooltip.cjs +28 -17
  100. package/dist/tooltip.js +30 -18
  101. package/package.json +2 -16
  102. package/src/accordion.tsx +173 -227
  103. package/src/alert.tsx +78 -45
  104. package/src/avatar.tsx +54 -37
  105. package/src/badge.tsx +65 -44
  106. package/src/checkbox.tsx +70 -89
  107. package/src/close-button.tsx +3 -8
  108. package/src/collapsible.tsx +55 -52
  109. package/src/drawer.tsx +123 -115
  110. package/src/empty-state.tsx +21 -17
  111. package/src/expiration-selector.tsx +84 -66
  112. package/src/field.tsx +68 -124
  113. package/src/greeks-display.tsx +59 -51
  114. package/src/heading.tsx +22 -26
  115. package/src/icon-button.tsx +134 -141
  116. package/src/ink.tsx +37 -0
  117. package/src/input-group.tsx +59 -36
  118. package/src/input.tsx +41 -32
  119. package/src/menu.tsx +224 -397
  120. package/src/option-chain.tsx +154 -104
  121. package/src/option-position.tsx +136 -114
  122. package/src/pin-input.tsx +37 -29
  123. package/src/pnl-diagram.tsx +36 -28
  124. package/src/popover.tsx +1 -1
  125. package/src/progress.tsx +17 -21
  126. package/src/radio.tsx +56 -75
  127. package/src/select.tsx +80 -59
  128. package/src/separator.tsx +13 -34
  129. package/src/slider.tsx +43 -44
  130. package/src/strategy-builder.tsx +245 -156
  131. package/src/switch.tsx +63 -64
  132. package/src/tabs.tsx +166 -296
  133. package/src/tag.tsx +89 -51
  134. package/src/textarea.tsx +19 -29
  135. package/src/toaster.tsx +118 -86
  136. package/src/tooltip.tsx +21 -13
  137. package/tokens.css +79 -0
package/src/accordion.tsx CHANGED
@@ -1,285 +1,231 @@
1
- import * as AccordionPrimitive from '@radix-ui/react-accordion';
1
+ import { Accordion, View, XStack } from '@hanzo/gui';
2
2
  import * as React from 'react';
3
3
 
4
- import { cn } from './utils';
5
-
6
- // Inline chevron icon (east-mini arrow)
7
- const IndicatorIcon = ({ className }: { readonly className?: string }) => (
8
- <svg className={ className } viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
4
+ import { ink } from './ink';
5
+
6
+ // The Lux accordion, on gui's Accordion.
7
+ //
8
+ // Everything behavioural belongs to the primitive and none of it is
9
+ // re-implemented here: roving Arrow/Home/End focus on the root, `aria-expanded`
10
+ // + `aria-controls` on the trigger, `role="region"` + `aria-labelledby` on the
11
+ // content. This file names the four parts and dresses them.
12
+ //
13
+ // Three corrections the primitive needs, each of which is a real bug if skipped:
14
+ //
15
+ // 1. gui's `Accordion.Header` renders an `<h1>` at the display type scale. An
16
+ // accordion is a list of sections inside a page that already has a title, so
17
+ // unqualified every row publishes a competing document heading and a screen
18
+ // reader's heading list comes out as N level-1s. `headingLevel` re-hosts it
19
+ // and sets the matching `aria-level`, which the inherited `role="heading"`
20
+ // requires to be valid.
21
+ // 2. `unstyled` means gui applies nothing — which on the web leaves the USER
22
+ // AGENT's chrome, and a bare `<button>` is #efefef with an outset border.
23
+ // Unstyled has to mean "no chrome", so the reset is explicit.
24
+ // 3. That trigger is a bare `<button>`, and a bare button inside a form
25
+ // SUBMITS it. `type="button"` or opening a section posts the form.
26
+ //
27
+ // The indicator's open state comes from the trigger's function child — gui
28
+ // exports no per-item context, and that is the only seam. It replaces a
29
+ // `group-data-[state=open]/trigger:` Tailwind chain that could not have worked
30
+ // here anyway: gui compiles style props to atomic classes and has no
31
+ // parent-state selector.
32
+
33
+ const IndicatorIcon = () => (
34
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
9
35
  <path d="M7.5 15L12.5 10L7.5 5" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
10
36
  </svg>
11
37
  );
12
38
 
39
+ const BAR = 'var(--color-text-primary)' as never;
40
+
41
+ /**
42
+ * The open/closed mark. The FAQ variant is a plus that becomes a minus: two
43
+ * bars, and the upright one turns a quarter turn onto the flat one.
44
+ */
45
+ const Indicator = ({ open, variant }: { open: boolean; variant?: 'outline' | 'faq' }) => (
46
+ variant === 'faq' ? (
47
+ <XStack width={ 12 } height={ 12 } flexShrink={ 0 } alignItems="center" justifyContent="center" position="relative">
48
+ <View position="absolute" width="100%" height={ 2 } borderRadius={ 2 } backgroundColor={ BAR }/>
49
+ <View
50
+ position="absolute"
51
+ width={ 2 }
52
+ height="100%"
53
+ borderRadius={ 2 }
54
+ backgroundColor={ BAR }
55
+ rotate={ open ? '90deg' : '0deg' }
56
+ />
57
+ </XStack>
58
+ ) : (
59
+ <XStack flexShrink={ 0 } alignItems="center" rotate={ open ? '270deg' : '180deg' }>
60
+ <IndicatorIcon/>
61
+ </XStack>
62
+ )
63
+ );
64
+
13
65
  // ---------------------------------------------------------------------------
14
- // AccordionRoot
66
+ // Root
15
67
  // ---------------------------------------------------------------------------
16
68
 
17
- interface AccordionRootProps extends Omit<React.ComponentPropsWithoutRef<'div'>, 'defaultValue' | 'dir'> {
69
+ export interface AccordionRootProps
70
+ extends Omit<React.ComponentProps<typeof Accordion>, 'type' | 'value' | 'defaultValue' | 'onValueChange' | 'size'> {
18
71
 
19
- /** Open multiple items simultaneously. Defaults to true. */
20
- readonly multiple?: boolean;
21
-
22
- /** Controlled open items (array of `value` strings). */
72
+ /** Controlled open items. */
23
73
  readonly value?: Array<string>;
24
74
 
25
75
  /** Uncontrolled initial open items. */
26
76
  readonly defaultValue?: Array<string>;
27
77
 
28
- /**
29
- * Called when open items change.
30
- * Wraps the value in `{ value }` to stay compatible with the Chakra callback shape
31
- * that consumers already rely on.
32
- */
78
+ /** Fires with the full set of open items. */
33
79
  readonly onValueChange?: (details: { value: Array<string> }) => void;
80
+
34
81
  readonly variant?: 'outline' | 'faq';
35
82
  readonly size?: 'sm' | 'md';
36
-
37
- /** Accepted for compatibility; not applied visually. */
38
- readonly noAnimation?: boolean;
39
-
40
- /** Accepted for compatibility; Radix defers rendering internally. */
41
- readonly lazyMount?: boolean;
42
- readonly children?: React.ReactNode;
43
- // Legacy Chakra style-prop shims
44
- readonly position?: string;
45
- readonly w?: string;
46
- readonly bgColor?: string | Record<string, string>;
47
- readonly borderRadius?: string;
48
83
  }
49
84
 
50
- export const AccordionRoot = React.forwardRef<HTMLDivElement, AccordionRootProps>(
51
- function AccordionRoot(props, ref) {
52
- const {
53
- multiple: _multiple,
54
- value,
55
- defaultValue,
56
- onValueChange,
57
- variant,
58
- size,
59
- noAnimation: _noAnimation,
60
- lazyMount: _lazyMount,
61
- className,
62
- children,
63
- position: _position,
64
- w: _w,
65
- bgColor: _bgColor,
66
- borderRadius: _borderRadius,
67
- ...rest
68
- } = props;
69
-
70
- // Radix requires `type` discriminant. We default to "multiple" to match
71
- // the original Chakra wrapper behavior.
72
- const handleValueChange = React.useCallback(
73
- (next: Array<string>) => {
74
- onValueChange?.({ value: next });
75
- },
76
- [ onValueChange ],
77
- );
78
-
79
- return (
80
- <AccordionPrimitive.Root
81
- ref={ ref }
82
- type="multiple"
83
- value={ value }
84
- defaultValue={ defaultValue }
85
- onValueChange={ handleValueChange }
86
- className={ cn(
87
- 'w-full',
88
- variant === 'faq' && 'accordion-faq',
89
- size === 'sm' && 'accordion-sm',
90
- className,
91
- ) }
92
- data-variant={ variant }
93
- data-size={ size }
94
- { ...rest }
95
- >
96
- { children }
97
- </AccordionPrimitive.Root>
98
- );
99
- },
85
+ const Variant = React.createContext<'outline' | 'faq' | undefined>(undefined);
86
+
87
+ export const AccordionRoot = ({
88
+ value,
89
+ defaultValue,
90
+ onValueChange,
91
+ variant,
92
+ size,
93
+ children,
94
+ ...rest
95
+ }: AccordionRootProps): React.ReactElement => (
96
+ <Variant.Provider value={ variant }>
97
+ <Accordion
98
+ unstyled
99
+ type="multiple"
100
+ width="100%"
101
+ value={ value }
102
+ defaultValue={ defaultValue }
103
+ onValueChange={ onValueChange ? (next: Array<string>) => onValueChange({ value: next }) : undefined }
104
+ data-variant={ variant }
105
+ data-size={ size }
106
+ { ...rest }
107
+ >
108
+ { children }
109
+ </Accordion>
110
+ </Variant.Provider>
100
111
  );
101
112
 
102
113
  // ---------------------------------------------------------------------------
103
- // AccordionItem
114
+ // Item
104
115
  // ---------------------------------------------------------------------------
105
116
 
106
- interface AccordionItemProps extends React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item> {
107
- // Legacy Chakra style-prop shims
108
- as?: string;
109
- _first?: Record<string, unknown>;
110
- _last?: Record<string, unknown>;
111
- display?: string;
112
- }
117
+ export type AccordionItemProps = React.ComponentProps<typeof Accordion.Item>;
113
118
 
114
- export const AccordionItem = React.forwardRef<HTMLDivElement, AccordionItemProps>(
115
- function AccordionItem(props, ref) {
116
- const { className, as: _as, _first, _last, display: _display, ...rest } = props;
117
- return (
118
- <AccordionPrimitive.Item
119
- ref={ ref }
120
- className={ cn(
121
- 'border-b border-[var(--color-border-divider)]',
122
- className,
123
- ) }
124
- { ...rest }
125
- />
126
- );
127
- },
119
+ export const AccordionItem = (props: AccordionItemProps): React.ReactElement => (
120
+ <Accordion.Item
121
+ unstyled
122
+ borderBottomWidth={ 1 }
123
+ borderColor={ 'var(--color-border-divider)' as never }
124
+ { ...props }
125
+ />
128
126
  );
129
127
 
130
128
  // ---------------------------------------------------------------------------
131
- // AccordionItemTrigger
129
+ // Trigger
132
130
  // ---------------------------------------------------------------------------
133
131
 
134
- interface AccordionItemTriggerProps extends Omit<React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>, 'dir'> {
132
+ export type AccordionItemTriggerProps = React.ComponentProps<typeof Accordion.Trigger> & {
135
133
  readonly indicatorPlacement?: 'start' | 'end';
136
134
  readonly noIndicator?: boolean;
137
135
  readonly variant?: 'outline' | 'faq';
138
- // Legacy Chakra style-prop shims
139
- px?: number | string;
140
- py?: number | string;
141
- _hover?: Record<string, string>;
142
- wordBreak?: string;
143
- textAlign?: string;
144
- cursor?: string;
145
- display?: string;
146
- alignItems?: string;
147
- columnGap?: number | string;
148
- }
149
-
150
- export const AccordionItemTrigger = React.forwardRef<
151
- HTMLButtonElement,
152
- AccordionItemTriggerProps
153
- >(function AccordionItemTrigger(props, ref) {
154
- const {
155
- children, indicatorPlacement: indicatorPlacementProp, variant, noIndicator, className,
156
- // Strip Chakra style props
157
- px: _px, py: _py, _hover, wordBreak: _wordBreak, textAlign: _textAlign,
158
- cursor: _cursor, display: _display, alignItems: _alignItems, columnGap: _columnGap,
159
- ...rest
160
- } = props;
161
-
162
- const indicatorPlacement = variant === 'faq' ? 'start' : (indicatorPlacementProp ?? 'end');
163
-
164
- const indicator = variant === 'faq' ? (
165
- <span
166
- className={ cn(
167
- 'relative inline-block h-3 w-3 shrink-0',
168
- // horizontal bar (always visible)
169
- 'before:absolute before:left-0 before:top-1/2 before:block before:h-[2px] before:w-full before:-translate-y-1/2 before:rounded-[2px] before:bg-current',
170
- // vertical bar (rotates to 0 when open)
171
- 'after:absolute after:left-1/2 after:top-0 after:block after:h-full after:w-[2px]',
172
- 'after:-translate-x-1/2 after:rounded-[2px] after:bg-current',
173
- 'after:transition-transform after:duration-200 after:ease-in-out',
174
- // When parent trigger has data-state="open", rotate vertical bar
175
- 'group-data-[state=open]/trigger:after:rotate-90',
176
- ) }
177
- />
178
- ) : (
179
- <span
180
- className={ cn(
181
- 'inline-flex shrink-0 transition-transform duration-200',
182
- 'rotate-180',
183
- 'group-data-[state=open]/trigger:rotate-[270deg]',
184
- ) }
185
- >
186
- <IndicatorIcon className="h-5 w-5"/>
187
- </span>
188
- );
136
+ readonly headingLevel?: 1 | 2 | 3 | 4 | 5 | 6;
137
+
138
+ /** The DOM button type — see (3) above. Defaults to `button`, not submit. */
139
+ readonly type?: 'button' | 'submit' | 'reset';
140
+ };
141
+
142
+ export const AccordionItemTrigger = ({
143
+ children,
144
+ indicatorPlacement: placementProp,
145
+ variant: variantProp,
146
+ noIndicator,
147
+ headingLevel = 3,
148
+ ...rest
149
+ }: AccordionItemTriggerProps): React.ReactElement => {
150
+ const variant = variantProp ?? React.useContext(Variant);
151
+ const placement = variant === 'faq' ? 'start' : (placementProp ?? 'end');
189
152
 
190
153
  return (
191
- <AccordionPrimitive.Header asChild>
192
- <div>
193
- <AccordionPrimitive.Trigger
194
- ref={ ref }
195
- className={ cn(
196
- 'group/trigger flex w-full cursor-pointer items-center gap-2 py-3 text-left text-[var(--color-text-primary)]',
197
- 'hover:text-[var(--color-hover)]',
198
- 'focus-visible:outline-none',
199
- className,
200
- ) }
201
- { ...rest }
202
- >
203
- { indicatorPlacement === 'start' && !noIndicator && indicator }
204
- { children }
205
- { indicatorPlacement === 'end' && !noIndicator && indicator }
206
- </AccordionPrimitive.Trigger>
207
- </div>
208
- </AccordionPrimitive.Header>
154
+ <Accordion.Header unstyled render={ `h${ headingLevel }` } aria-level={ headingLevel } display="flex" width="100%">
155
+ <Accordion.Trigger
156
+ unstyled
157
+ type="button"
158
+ width="100%"
159
+ flexDirection="row"
160
+ alignItems="center"
161
+ gap={ 8 }
162
+ paddingVertical={ 12 }
163
+ paddingHorizontal={ 0 }
164
+ borderWidth={ 0 }
165
+ backgroundColor="transparent"
166
+ cursor="pointer"
167
+ // Ink colour and its hover live in one CSS rule rather than on this
168
+ // frame: `color` is a Text prop, not a Stack prop, and "the label
169
+ // brightens while the ROW is hovered" is a pseudo-class on an ancestor
170
+ // — a selector, which no prop expresses. The children inherit it.
171
+ className="lux-accordion-trigger"
172
+ { ...rest }
173
+ >
174
+ { (({ open }: { open: boolean }) => (
175
+ <>
176
+ { placement === 'start' && !noIndicator && <Indicator open={ open } variant={ variant }/> }
177
+ { ink(children, undefined, { color: 'inherit', textAlign: 'left' }) }
178
+ { placement === 'end' && !noIndicator && <Indicator open={ open } variant={ variant }/> }
179
+ </>
180
+ )) as unknown as React.ReactNode }
181
+ </Accordion.Trigger>
182
+ </Accordion.Header>
209
183
  );
210
- });
184
+ };
211
185
 
212
186
  // ---------------------------------------------------------------------------
213
- // AccordionItemContent
187
+ // Content
214
188
  // ---------------------------------------------------------------------------
215
189
 
216
- export interface AccordionItemContentProps extends React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content> {
217
- // Legacy Chakra style-prop shims
218
- pb?: number | string;
219
- pr?: number | string;
220
- pl?: string;
221
- w?: string;
222
- display?: string;
223
- flexDir?: string;
224
- rowGap?: number | string;
225
- }
226
-
227
- export const AccordionItemContent = React.forwardRef<
228
- HTMLDivElement,
229
- AccordionItemContentProps
230
- >(function AccordionItemContent(props, ref) {
231
- const { className, children, pb: _pb, pr: _pr, pl: _pl, w: _w, display: _display, flexDir: _flexDir, rowGap: _rowGap, ...rest } = props;
232
- return (
233
- <AccordionPrimitive.Content
234
- ref={ ref }
235
- className={ cn(
236
- 'overflow-hidden text-[var(--color-text-primary)]',
237
- 'data-[state=open]:animate-accordion-down',
238
- 'data-[state=closed]:animate-accordion-up',
239
- className,
240
- ) }
241
- { ...rest }
242
- >
243
- <div className="pb-3">
244
- { children }
245
- </div>
246
- </AccordionPrimitive.Content>
247
- );
248
- });
190
+ export type AccordionItemContentProps = React.ComponentProps<typeof Accordion.Content>;
191
+
192
+ export const AccordionItemContent = ({ children, ...rest }: AccordionItemContentProps): React.ReactElement => (
193
+ <Accordion.Content
194
+ unstyled
195
+ overflow="hidden"
196
+ paddingBottom={ 12 }
197
+ backgroundColor="transparent"
198
+ { ...rest }
199
+ >
200
+ { ink(children, undefined, { color: 'var(--color-text-primary)' }) }
201
+ </Accordion.Content>
202
+ );
249
203
 
250
204
  // ---------------------------------------------------------------------------
251
205
  // useAccordion
252
206
  // ---------------------------------------------------------------------------
253
207
 
208
+ /** Open state for an accordion whose items are addressable by URL fragment. */
254
209
  export function useAccordion(items: Array<{ id: string }>) {
255
210
  const [ value, setValue ] = React.useState<Array<string>>([]);
256
211
 
257
- const onValueChange = React.useCallback(({ value }: { value: Array<string> }) => {
258
- setValue(value);
212
+ const onValueChange = React.useCallback(({ value: next }: { value: Array<string> }) => {
213
+ setValue(next);
259
214
  }, []);
260
215
 
261
216
  const scrollToItemFromUrl = React.useCallback(() => {
262
217
  const hash = window.location.hash.replace('#', '');
263
-
264
- if (!hash) {
265
- return;
266
- }
218
+ if (!hash) return;
267
219
 
268
220
  const itemToScroll = items.find((item) => item.id === hash);
269
- if (itemToScroll) {
270
- const el = document.getElementById(itemToScroll.id);
271
- if (el) {
272
- el.scrollIntoView({ behavior: 'smooth', block: 'start' });
273
- }
274
- setValue([ itemToScroll.id ]);
275
- }
221
+ if (!itemToScroll) return;
222
+
223
+ document.getElementById(itemToScroll.id)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
224
+ setValue([ itemToScroll.id ]);
276
225
  }, [ items ]);
277
226
 
278
- return React.useMemo(() => {
279
- return {
280
- value,
281
- onValueChange,
282
- scrollToItemFromUrl,
283
- };
284
- }, [ value, onValueChange, scrollToItemFromUrl ]);
227
+ return React.useMemo(
228
+ () => ({ value, onValueChange, scrollToItemFromUrl }),
229
+ [ value, onValueChange, scrollToItemFromUrl ],
230
+ );
285
231
  }
package/src/alert.tsx CHANGED
@@ -1,15 +1,15 @@
1
+ import { Text, View, XStack, YStack } from '@hanzo/gui';
1
2
  import * as React from 'react';
2
3
 
3
- import { cn } from './utils';
4
-
5
4
  // Inline info icon (filled circle with i)
6
- const IndicatorIcon = ({ className }: { readonly className?: string }) => (
7
- <svg className={ className } viewBox="0 0 20 20" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
5
+ const IndicatorIcon = () => (
6
+ <svg width={ 20 } height={ 20 } viewBox="0 0 20 20" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
8
7
  <path fillRule="evenodd" clipRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm.75-11.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zM9.25 10a.75.75 0 011.5 0v3a.75.75 0 01-1.5 0v-3z"/>
9
8
  </svg>
10
9
  );
11
10
 
12
11
  import { CloseButton } from './close-button';
12
+ import { ink } from './ink';
13
13
  import { Skeleton } from './skeleton';
14
14
 
15
15
  /* ------------------------------------------------------------------ */
@@ -19,27 +19,38 @@ import { Skeleton } from './skeleton';
19
19
  type AlertStatus = 'info' | 'warning' | 'warning_table' | 'success' | 'error';
20
20
 
21
21
  /* ------------------------------------------------------------------ */
22
- /* Class maps (replaced CVA) */
22
+ /* Style maps (replaced CVA) */
23
23
  /* ------------------------------------------------------------------ */
24
24
 
25
- const ALERT_BASE = 'w-full flex items-start relative rounded text-[var(--color-alert-fg)]';
26
-
27
- const STATUS_CLASSES: Record<AlertStatus, string> = {
28
- info: 'bg-[var(--color-alert-bg-info)]',
29
- warning: 'bg-[var(--color-alert-bg-warning)]',
30
- warning_table: 'bg-[var(--color-alert-bg-warning-table)]',
31
- success: 'bg-[var(--color-alert-bg-success)]',
32
- error: 'bg-[var(--color-alert-bg-error)]',
25
+ /** The frame every alert shares, regardless of status or size. */
26
+ const ALERT_BASE = {
27
+ width: '100%',
28
+ position: 'relative',
29
+ alignItems: 'flex-start',
30
+ borderRadius: 4,
31
+ gap: 8,
32
+ paddingVertical: 8,
33
+ } as const;
34
+
35
+ /** Foreground is constant across every status — only the background moves. */
36
+ const ALERT_FG = 'var(--color-alert-fg)' as never;
37
+
38
+ const STATUS_BG: Record<AlertStatus, string> = {
39
+ info: 'var(--color-alert-bg-info)',
40
+ warning: 'var(--color-alert-bg-warning)',
41
+ warning_table: 'var(--color-alert-bg-warning-table)',
42
+ success: 'var(--color-alert-bg-success)',
43
+ error: 'var(--color-alert-bg-error)',
33
44
  };
34
45
 
35
- const SIZE_CLASSES: Record<string, string> = {
36
- sm: 'gap-2 px-2 py-2 text-xs',
37
- md: 'gap-2 px-3 py-2 text-base',
46
+ const SIZE: Record<'sm' | 'md', { paddingHorizontal: number; fontSize: number }> = {
47
+ sm: { paddingHorizontal: 8, fontSize: 12 },
48
+ md: { paddingHorizontal: 12, fontSize: 16 },
38
49
  };
39
50
 
40
- const INDICATOR_SIZE_CLASSES: Record<string, string> = {
41
- sm: 'w-5 h-5 my-0',
42
- md: 'w-5 h-5 my-[2px]',
51
+ const INDICATOR_SIZE: Record<'sm' | 'md', { marginVertical: number }> = {
52
+ sm: { marginVertical: 0 },
53
+ md: { marginVertical: 2 },
43
54
  };
44
55
 
45
56
  /* ------------------------------------------------------------------ */
@@ -107,10 +118,9 @@ export const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
107
118
  const [ isOpen, setIsOpen ] = React.useState(true);
108
119
 
109
120
  const resolvedSize = size ?? 'md';
121
+ const fontSize = SIZE[resolvedSize].fontSize;
110
122
 
111
- const defaultIcon = (
112
- <IndicatorIcon className="w-5 h-5"/>
113
- );
123
+ const defaultIcon = <IndicatorIcon/>;
114
124
 
115
125
  const iconElement = (() => {
116
126
  if (startElement !== undefined) {
@@ -122,12 +132,23 @@ export const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
122
132
  }
123
133
 
124
134
  return (
125
- <span className={ cn(
126
- 'inline-flex items-center justify-center shrink-0 text-[var(--color-alert-fg)] [&>svg]:w-full [&>svg]:h-full',
127
- INDICATOR_SIZE_CLASSES[resolvedSize],
128
- ) }>
135
+ <XStack
136
+ alignItems="center"
137
+ justifyContent="center"
138
+ flexShrink={ 0 }
139
+ width={ 20 }
140
+ height={ 20 }
141
+ { ...INDICATOR_SIZE[resolvedSize] }
142
+ // The default icon and a caller's custom `icon` both use
143
+ // `fill="currentColor"`; `.lux-icon` (tokens.css) is the existing
144
+ // descendant-selector rule that sizes an arbitrary child svg to the
145
+ // box (20px default) instead of its own intrinsic size — no prop
146
+ // reaches a child element, so this is shared, not reinvented here.
147
+ className="lux-icon"
148
+ style={{ color: ALERT_FG }}
149
+ >
129
150
  { icon || defaultIcon }
130
- </span>
151
+ </XStack>
131
152
  );
132
153
  })();
133
154
 
@@ -156,37 +177,49 @@ export const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
156
177
  ...(descColumnGap !== undefined ? { columnGap: typeof descColumnGap === 'number' ? `${ descColumnGap * 4 }px` : descColumnGap } : {}),
157
178
  };
158
179
 
180
+ // Row when inline (text sits beside the title), column otherwise.
181
+ const ContentFrame = inline ? XStack : YStack;
182
+
159
183
  return (
160
184
  <Skeleton loading={ loading }>
161
- <div
162
- ref={ ref }
163
- className={ cn(ALERT_BASE, STATUS_CLASSES[status], SIZE_CLASSES[resolvedSize], className) }
185
+ <XStack
186
+ ref={ ref as never }
187
+ { ...ALERT_BASE }
188
+ paddingHorizontal={ SIZE[resolvedSize].paddingHorizontal }
189
+ backgroundColor={ STATUS_BG[status] as never }
190
+ className={ className }
164
191
  style={ alertStyle }
165
- { ...alertRest }
192
+ { ...(alertRest as object) }
166
193
  >
167
194
  { iconElement }
168
195
  { children ? (
169
- <div className={ cn('flex flex-1', inline ? 'inline-flex flex-row items-center' : 'flex flex-col') }>
170
- { title && <div className="font-semibold">{ title }</div> }
171
- <div
172
- className={ cn('inline-flex flex-wrap', descClassName) }
196
+ <ContentFrame flex={ 1 } alignItems={ inline ? 'center' : undefined }>
197
+ { title && (
198
+ <Text fontWeight="600" fontSize={ fontSize } color={ ALERT_FG }>
199
+ { title }
200
+ </Text>
201
+ ) }
202
+ <XStack
203
+ flexWrap="wrap"
204
+ className={ descClassName }
173
205
  style={ Object.keys(descStyle).length > 0 ? descStyle : undefined }
174
- { ...descRest }
206
+ { ...(descRest as object) }
175
207
  >
176
- { children }
177
- </div>
178
- </div>
208
+ { ink(children, undefined, { fontSize, color: ALERT_FG }) }
209
+ </XStack>
210
+ </ContentFrame>
179
211
  ) : (
180
- <div className="font-semibold flex-1">{ title }</div>
212
+ <Text flex={ 1 } fontWeight="600" fontSize={ fontSize } color={ ALERT_FG }>
213
+ { title }
214
+ </Text>
181
215
  ) }
182
216
  { endElement }
183
217
  { closable && (
184
- <CloseButton
185
- className="relative self-start"
186
- onClick={ handleClose }
187
- />
218
+ <View position="relative" alignSelf="flex-start">
219
+ <CloseButton onClick={ handleClose }/>
220
+ </View>
188
221
  ) }
189
- </div>
222
+ </XStack>
190
223
  </Skeleton>
191
224
  );
192
225
  },