@luxfi/ui 7.4.11 → 7.4.13

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 (84) hide show
  1. package/dist/alert.cjs +68 -46
  2. package/dist/alert.js +69 -46
  3. package/dist/avatar.cjs +49 -47
  4. package/dist/avatar.js +52 -49
  5. package/dist/badge.cjs +82 -49
  6. package/dist/badge.js +83 -50
  7. package/dist/checkbox.cjs +42 -75
  8. package/dist/checkbox.js +43 -76
  9. package/dist/chrome.cjs +3 -7
  10. package/dist/chrome.js +3 -7
  11. package/dist/close-button.cjs +3 -7
  12. package/dist/close-button.js +3 -7
  13. package/dist/dialog.cjs +3 -7
  14. package/dist/dialog.js +3 -7
  15. package/dist/drawer.cjs +3 -7
  16. package/dist/drawer.js +3 -7
  17. package/dist/empty-state.cjs +13 -13
  18. package/dist/empty-state.js +13 -13
  19. package/dist/expiration-selector.cjs +58 -63
  20. package/dist/expiration-selector.js +58 -63
  21. package/dist/greeks-display.cjs +48 -39
  22. package/dist/greeks-display.js +48 -39
  23. package/dist/index.cjs +1053 -888
  24. package/dist/index.js +1054 -889
  25. package/dist/input-group.cjs +36 -19
  26. package/dist/input-group.js +37 -19
  27. package/dist/option-chain.cjs +89 -67
  28. package/dist/option-chain.js +89 -67
  29. package/dist/option-position.cjs +88 -85
  30. package/dist/option-position.js +88 -85
  31. package/dist/pin-input.cjs +30 -29
  32. package/dist/pin-input.js +30 -29
  33. package/dist/pnl-diagram.cjs +20 -20
  34. package/dist/pnl-diagram.js +20 -20
  35. package/dist/popover.cjs +3 -7
  36. package/dist/popover.js +3 -7
  37. package/dist/progress-circle.cjs +40 -22
  38. package/dist/progress-circle.d.cts +5 -5
  39. package/dist/progress-circle.d.ts +5 -5
  40. package/dist/progress-circle.js +41 -22
  41. package/dist/progress.cjs +15 -22
  42. package/dist/progress.d.cts +6 -6
  43. package/dist/progress.d.ts +6 -6
  44. package/dist/progress.js +15 -22
  45. package/dist/radio.cjs +35 -68
  46. package/dist/radio.d.cts +18 -18
  47. package/dist/radio.d.ts +18 -18
  48. package/dist/radio.js +36 -69
  49. package/dist/rating.cjs +15 -17
  50. package/dist/rating.js +15 -17
  51. package/dist/slider.cjs +33 -50
  52. package/dist/slider.js +34 -51
  53. package/dist/strategy-builder.cjs +194 -115
  54. package/dist/strategy-builder.js +194 -115
  55. package/dist/switch.cjs +48 -55
  56. package/dist/switch.js +49 -56
  57. package/dist/tag.cjs +115 -69
  58. package/dist/tag.js +116 -69
  59. package/dist/tooltip.cjs +28 -17
  60. package/dist/tooltip.js +30 -18
  61. package/package.json +1 -1
  62. package/src/alert.tsx +78 -45
  63. package/src/avatar.tsx +54 -38
  64. package/src/badge.tsx +65 -44
  65. package/src/checkbox.tsx +70 -89
  66. package/src/close-button.tsx +3 -8
  67. package/src/empty-state.tsx +21 -17
  68. package/src/expiration-selector.tsx +84 -66
  69. package/src/greeks-display.tsx +59 -51
  70. package/src/input-group.tsx +38 -24
  71. package/src/option-chain.tsx +154 -104
  72. package/src/option-position.tsx +143 -114
  73. package/src/pin-input.tsx +37 -29
  74. package/src/pnl-diagram.tsx +36 -28
  75. package/src/progress-circle.tsx +52 -28
  76. package/src/progress.tsx +17 -21
  77. package/src/radio.tsx +56 -76
  78. package/src/rating.tsx +22 -20
  79. package/src/slider.tsx +43 -45
  80. package/src/strategy-builder.tsx +260 -162
  81. package/src/switch.tsx +63 -64
  82. package/src/tag.tsx +89 -51
  83. package/src/tooltip.tsx +21 -13
  84. package/tokens.css +18 -0
package/src/switch.tsx CHANGED
@@ -1,32 +1,23 @@
1
- import { Switch as GuiSwitch } from '@hanzo/gui';
1
+ import { Label, Switch as GuiSwitch, Text, VisuallyHidden } from '@hanzo/gui';
2
2
  import * as React from 'react';
3
3
 
4
- import { cn } from './utils';
5
-
6
4
  const NOOP = () => { /* noop */ };
7
5
 
8
6
  // ─── Size mappings ────────────────────────────────────────────────────
9
7
  // Paint and box size only. Thumb travel is NOT a hardcoded translate any more:
10
8
  // gui measures the track and the knob and moves the knob by the difference, so
11
9
  // the switch stays correct at any size and on native.
12
- const SIZE_CLASSES = {
13
- sm: {
14
- root: 'h-4 w-7',
15
- thumb: 'h-3 w-3',
16
- label: 'text-xs',
17
- },
18
- md: {
19
- root: 'h-5 w-9',
20
- thumb: 'h-4 w-4',
21
- label: 'text-sm',
22
- },
23
- lg: {
24
- root: 'h-6 w-11',
25
- thumb: 'h-5 w-5',
26
- label: 'text-base',
27
- },
10
+ const SIZE = {
11
+ sm: { width: 28, height: 16, thumb: 12, label: 12 },
12
+ md: { width: 36, height: 20, thumb: 16, label: 14 },
13
+ lg: { width: 44, height: 24, thumb: 20, label: 16 },
28
14
  } as const;
29
15
 
16
+ const TRACK_ON = 'var(--color-text-primary)' as never;
17
+ const TRACK_OFF = 'var(--color-switch-bg)' as never;
18
+ const OUTLINE = 'var(--color-gray-500)' as never;
19
+ const WHITE = 'var(--color-white)' as never;
20
+
30
21
  // ─── Props ────────────────────────────────────────────────────────────
31
22
  export interface SwitchProps extends Omit<React.ComponentPropsWithoutRef<'label'>, 'onChange' | 'dir'> {
32
23
  inputProps?: React.InputHTMLAttributes<HTMLInputElement>;
@@ -59,7 +50,6 @@ const SwitchBase = React.forwardRef<HTMLInputElement, SwitchProps>(
59
50
  disabled = false,
60
51
  size = 'md',
61
52
  direction,
62
- className,
63
53
  ...rest
64
54
  } = props;
65
55
 
@@ -80,78 +70,87 @@ const SwitchBase = React.forwardRef<HTMLInputElement, SwitchProps>(
80
70
  [ isControlled, onCheckedChange ],
81
71
  );
82
72
 
83
- const sizeClasses = SIZE_CLASSES[size];
73
+ const s = SIZE[size];
84
74
  const isRtl = direction === 'rtl';
85
75
 
86
76
  return (
87
- <label
88
- ref={ rootRef }
89
- className={ cn(
90
- 'inline-flex items-center cursor-pointer select-none gap-2',
91
- isRtl && 'flex-row-reverse',
92
- disabled && 'opacity-50 cursor-not-allowed',
93
- className,
94
- ) }
95
- onChange={ onChange }
77
+ <Label
78
+ ref={ rootRef as never }
79
+ unstyled
80
+ flexDirection={ isRtl ? 'row-reverse' : 'row' }
81
+ alignItems="center"
82
+ gap={ 8 }
83
+ cursor={ disabled ? 'not-allowed' : 'pointer' }
84
+ userSelect="none"
85
+ opacity={ disabled ? 0.5 : 1 }
86
+ onChange={ onChange as never }
96
87
  data-disabled={ disabled || undefined }
97
- { ...rest }
88
+ { ...(rest as object) }
98
89
  >
99
90
  <GuiSwitch
100
91
  unstyled
101
92
  checked={ checkedState }
102
93
  onCheckedChange={ handleCheckedChange }
103
94
  disabled={ disabled }
104
- className={ cn(
105
- 'relative inline-flex shrink-0 items-center rounded-full',
106
- 'bg-gray-300 dark:bg-gray-600',
107
- 'data-[state=checked]:bg-gray-800 dark:bg-white',
108
- 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500',
109
- 'transition-colors duration-200',
110
- sizeClasses.root,
111
- ) }
95
+ position="relative"
96
+ flexShrink={ 0 }
97
+ alignItems="center"
98
+ width={ s.width }
99
+ height={ s.height }
100
+ borderRadius={ 9999 }
101
+ backgroundColor={ checkedState ? TRACK_ON : TRACK_OFF }
102
+ transition="quick"
103
+ focusStyle={{ outlineWidth: 2, outlineStyle: 'solid', outlineColor: OUTLINE, outlineOffset: 2 }}
112
104
  >
113
105
  { trackLabel && (
114
- <span className="absolute inset-0 flex items-center justify-center text-[8px] font-bold text-white pointer-events-none">
106
+ <Text
107
+ position="absolute"
108
+ width="100%"
109
+ textAlign="center"
110
+ fontSize={ 8 }
111
+ fontWeight="700"
112
+ color={ WHITE }
113
+ pointerEvents="none"
114
+ >
115
115
  { checkedState ? trackLabel.on : trackLabel.off }
116
- </span>
116
+ </Text>
117
117
  ) }
118
118
  <GuiSwitch.Thumb
119
119
  unstyled
120
120
  data-thumb
121
121
  transition="quicker"
122
- className={ cn(
123
- 'block rounded-full bg-white shadow-sm',
124
- sizeClasses.thumb,
125
- ) }
122
+ width={ s.thumb }
123
+ height={ s.thumb }
124
+ borderRadius={ 9999 }
125
+ backgroundColor={ WHITE }
126
+ boxShadow="0 1px 2px 0 rgba(0,0,0,0.05)"
126
127
  >
127
128
  { thumbLabel && (
128
- <span className="flex items-center justify-center h-full w-full text-[8px]">
129
+ <Text width="100%" height="100%" textAlign="center" fontSize={ 8 }>
129
130
  { checkedState ? thumbLabel.on : thumbLabel.off }
130
- </span>
131
+ </Text>
131
132
  ) }
132
133
  </GuiSwitch.Thumb>
133
134
  </GuiSwitch>
134
135
  { /* Hidden native input for form compat and ref forwarding */ }
135
- <input
136
- ref={ ref }
137
- type="checkbox"
138
- className="sr-only"
139
- checked={ checkedState }
140
- disabled={ disabled }
141
- tabIndex={ -1 }
142
- aria-hidden
143
- onChange={ NOOP }
144
- { ...inputProps }
145
- />
136
+ <VisuallyHidden>
137
+ <input
138
+ ref={ ref }
139
+ type="checkbox"
140
+ checked={ checkedState }
141
+ disabled={ disabled }
142
+ tabIndex={ -1 }
143
+ aria-hidden
144
+ onChange={ NOOP }
145
+ { ...inputProps }
146
+ />
147
+ </VisuallyHidden>
146
148
  { children != null && (
147
- <span
148
- { ...labelProps }
149
- className={ cn(sizeClasses.label, labelProps?.className) }
150
- >
149
+ <Text { ...(labelProps as object) } fontSize={ s.label }>
151
150
  { children }
152
- </span>
151
+ </Text>
153
152
  ) }
154
- </label>
153
+ </Label>
155
154
  );
156
155
  },
157
156
  );
package/src/tag.tsx CHANGED
@@ -1,7 +1,6 @@
1
+ import { Text, XStack } from '@hanzo/gui';
1
2
  import * as React from 'react';
2
3
 
3
- import { cn } from './utils';
4
-
5
4
  import { CloseButton } from './close-button';
6
5
  import { Skeleton } from './skeleton';
7
6
  import { Tooltip } from './tooltip';
@@ -21,29 +20,29 @@ function TruncatedTextTooltip({ label, children }: { readonly label: React.React
21
20
  type TagVariant = 'subtle' | 'clickable' | 'filter' | 'select';
22
21
  type TagSize = 'sm' | 'md' | 'lg';
23
22
 
24
- const TAG_BASE = 'inline-flex items-center align-top max-w-full select-text rounded-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500';
25
-
26
- const TAG_VARIANT_CLASSES: Record<TagVariant, string> = {
27
- subtle: 'bg-[var(--color-tag-subtle-bg)] text-[var(--color-tag-subtle-fg)]',
28
- clickable: 'cursor-pointer bg-[var(--color-tag-clickable-bg)] text-[var(--color-tag-clickable-fg)] hover:opacity-76',
29
- filter: 'bg-[var(--color-tag-filter-bg)]',
30
- select: 'cursor-pointer bg-[var(--color-tag-select-bg)] text-[var(--color-tag-select-fg)] hover:text-[var(--color-hover)] hover:opacity-76',
23
+ /** The chip background, cursor, and the hover opacity `clickable`/`select` get. */
24
+ const TAG_VARIANT: Record<TagVariant, { backgroundColor: string; cursor?: string; hoverStyle?: Record<string, unknown> }> = {
25
+ subtle: { backgroundColor: 'var(--color-tag-subtle-bg)' },
26
+ clickable: { backgroundColor: 'var(--color-tag-clickable-bg)', cursor: 'pointer', hoverStyle: { opacity: 0.76 } },
27
+ filter: { backgroundColor: 'var(--color-tag-filter-bg)' },
28
+ select: { backgroundColor: 'var(--color-tag-select-bg)', cursor: 'pointer', hoverStyle: { opacity: 0.76 } },
31
29
  };
32
30
 
33
- const TAG_SIZE_CLASSES: Record<TagSize, string> = {
34
- sm: 'px-1 py-0.5 min-h-5 gap-1 text-xs',
35
- md: 'px-1 py-0.5 min-h-6 gap-1 text-sm',
36
- lg: 'px-1.5 py-1.5 min-h-8 min-w-8 gap-1 text-sm',
31
+ /** The label inside it — same four variants, the colour half (`filter` has none). */
32
+ const TAG_VARIANT_FG: Record<TagVariant, string | undefined> = {
33
+ subtle: 'var(--color-tag-subtle-fg)',
34
+ clickable: 'var(--color-tag-clickable-fg)',
35
+ filter: undefined,
36
+ select: 'var(--color-tag-select-fg)',
37
37
  };
38
38
 
39
- const TAG_SELECTED_CLASSES = [
40
- 'bg-[var(--color-tag-select-selected-bg)]',
41
- 'text-[var(--color-tag-select-selected-fg)]',
42
- 'hover:text-[var(--color-tag-select-selected-fg)]',
43
- 'hover:opacity-76',
44
- ].join(' ');
39
+ const TAG_SIZE: Record<TagSize, Record<string, number>> = {
40
+ sm: { paddingHorizontal: 4, paddingVertical: 2, minHeight: 20, gap: 4 },
41
+ md: { paddingHorizontal: 4, paddingVertical: 2, minHeight: 24, gap: 4 },
42
+ lg: { paddingHorizontal: 6, paddingVertical: 6, minHeight: 32, minWidth: 32, gap: 4 },
43
+ };
45
44
 
46
- const TAG_DISABLED_CLASSES = 'opacity-40 pointer-events-none cursor-not-allowed';
45
+ const TAG_SIZE_FONT: Record<TagSize, number> = { sm: 12, md: 14, lg: 14 };
47
46
 
48
47
  export interface TagProps extends Omit<React.ComponentPropsWithRef<'span'>, 'size'> {
49
48
  readonly variant?: TagVariant;
@@ -80,14 +79,39 @@ export const Tag = React.forwardRef<HTMLSpanElement, TagProps>(
80
79
  ...rest
81
80
  } = props;
82
81
 
82
+ const resolvedVariant = variant ?? 'subtle';
83
+ const resolvedSize = size ?? 'md';
84
+ const isSelected = Boolean(selected) && !loading;
85
+
86
+ // `selected` overrides the variant's own colours; `disabled` only dims —
87
+ // neither is known until render, so resolve the final tokens here rather
88
+ // than lean on a class-merge order that no longer exists.
89
+ const backgroundColor = isSelected ? 'var(--color-tag-select-selected-bg)' : TAG_VARIANT[resolvedVariant].backgroundColor;
90
+ const color = isSelected ? 'var(--color-tag-select-selected-fg)' : TAG_VARIANT_FG[resolvedVariant];
91
+ const hoverStyle = isSelected ? { opacity: 0.76 } : TAG_VARIANT[resolvedVariant].hoverStyle;
92
+ // Unselected `select` rows preview the selected look on hover; once
93
+ // selected the hover colour equals the resting colour, i.e. no change.
94
+ const textHoverColor = !isSelected && resolvedVariant === 'select' ? 'var(--color-hover)' : undefined;
95
+
83
96
  const labelElement = label ? (
84
- <span className="text-[var(--color-text-secondary)]">{ label }:{ nbsp }</span>
97
+ <Text fontSize={ TAG_SIZE_FONT[resolvedSize] } fontWeight="500" color={ 'var(--color-text-secondary)' as never }>
98
+ { label }:{ nbsp }
99
+ </Text>
85
100
  ) : null;
86
101
 
87
102
  const labelSpan = (
88
- <span className="line-clamp-1 whitespace-nowrap text-ellipsis font-medium inline">
103
+ // `ellipsis` is Text's own built-in single-line-truncation variant —
104
+ // the same rule Tailwind's `line-clamp-1`/`text-ellipsis` drew, through
105
+ // the component's variants rather than three separate style props.
106
+ <Text
107
+ ellipsis
108
+ fontWeight="500"
109
+ fontSize={ TAG_SIZE_FONT[resolvedSize] }
110
+ color={ color as never }
111
+ hoverStyle={ textHoverColor ? { color: textHoverColor as never } : undefined }
112
+ >
89
113
  { labelElement }{ children }
90
- </span>
114
+ </Text>
91
115
  );
92
116
 
93
117
  const contentElement = truncated ? (
@@ -98,43 +122,57 @@ export const Tag = React.forwardRef<HTMLSpanElement, TagProps>(
98
122
 
99
123
  return (
100
124
  <Skeleton loading={ loading } asChild>
101
- <span
102
- ref={ ref }
103
- className={ cn(
104
- TAG_BASE,
105
- TAG_VARIANT_CLASSES[variant ?? 'subtle'],
106
- TAG_SIZE_CLASSES[size ?? 'md'],
107
- selected && !loading && TAG_SELECTED_CLASSES,
108
- disabled && TAG_DISABLED_CLASSES,
109
- className,
110
- ) }
111
- { ...(selected && !loading && { 'data-selected': true }) }
125
+ <XStack
126
+ ref={ ref as never }
127
+ render={ <span/> }
128
+ display="inline-flex"
129
+ alignItems="center"
130
+ verticalAlign="top"
131
+ maxWidth="100%"
132
+ userSelect="text"
133
+ { ...TAG_SIZE[resolvedSize] }
134
+ backgroundColor={ backgroundColor as never }
135
+ hoverStyle={ hoverStyle }
136
+ focusVisibleStyle={{
137
+ outlineWidth: 2,
138
+ outlineOffset: 2,
139
+ outlineColor: 'var(--color-gray-500)' as never,
140
+ outlineStyle: 'solid',
141
+ }}
142
+ // One cursor, decided once: disabled outranks the variant's.
143
+ cursor={ disabled ? 'not-allowed' : TAG_VARIANT[resolvedVariant].cursor }
144
+ { ...(disabled ? { opacity: 0.4, pointerEvents: 'none' } : {}) }
145
+ { ...(isSelected && { 'data-selected': true }) }
112
146
  { ...(disabled && { 'data-disabled': true }) }
113
- { ...rest }
147
+ className={ className }
148
+ { ...(rest as object) }
114
149
  >
115
150
  { startElement && (
116
- <span className="shrink-0 inline-flex items-center justify-center empty:hidden">
151
+ <XStack flexShrink={ 0 } alignItems="center" justifyContent="center">
117
152
  { startElement }
118
- </span>
153
+ </XStack>
119
154
  ) }
120
155
  { contentElement }
121
- { endElement && (
122
- <span
123
- { ...endElementProps }
124
- className={ cn(
125
- 'shrink-0 inline-flex items-center justify-center',
126
- endElementProps?.className,
127
- ) }
128
- >
129
- { endElement }
130
- </span>
131
- ) }
156
+ { endElement && (() => {
157
+ const { className: endClassName, ...endElementRest } = endElementProps ?? {};
158
+ return (
159
+ <XStack
160
+ { ...(endElementRest as object) }
161
+ flexShrink={ 0 }
162
+ alignItems="center"
163
+ justifyContent="center"
164
+ className={ endClassName }
165
+ >
166
+ { endElement }
167
+ </XStack>
168
+ );
169
+ })() }
132
170
  { closable && (
133
- <span className="shrink-0 inline-flex items-center justify-center">
171
+ <XStack flexShrink={ 0 } alignItems="center" justifyContent="center">
134
172
  <CloseButton onClick={ onClose }/>
135
- </span>
173
+ </XStack>
136
174
  ) }
137
- </span>
175
+ </XStack>
138
176
  </Skeleton>
139
177
  );
140
178
  },
package/src/tooltip.tsx CHANGED
@@ -5,8 +5,8 @@ import {
5
5
  import { useClickAway } from '@uidotdev/usehooks';
6
6
  import * as React from 'react';
7
7
 
8
+ import { ink } from './ink';
8
9
  import { useIsTouch } from './use-narrow';
9
- import { cn } from './utils';
10
10
 
11
11
  /** Map placement string to @hanzogui Popper placement prop. */
12
12
  function mapPlacement(p: string | undefined): string | undefined {
@@ -161,14 +161,21 @@ export const Tooltip = React.forwardRef<HTMLDivElement, TooltipProps>(
161
161
  <GuiTooltip.Content
162
162
  ref={ ref }
163
163
  unstyled
164
- className={ cn(
165
- 'z-[9999] overflow-hidden rounded-lg px-3 py-2 text-sm',
166
- 'animate-in fade-in-0 zoom-in-95',
167
- isPopover && 'bg-[var(--color-popover-bg)] text-[var(--color-text-primary)]',
168
- isPopover && 'shadow-[var(--shadow-popover)] border border-[var(--color-border-divider)] max-w-sm',
169
- !isPopover && 'bg-[var(--color-tooltip-bg)] text-[var(--color-tooltip-fg)] max-w-xs',
170
- contentProps?.className,
171
- ) }
164
+ zIndex={ 9999 }
165
+ overflow="hidden"
166
+ borderRadius={ 8 }
167
+ paddingHorizontal={ 12 }
168
+ paddingVertical={ 8 }
169
+ maxWidth={ isPopover ? 384 : 320 }
170
+ backgroundColor={ (isPopover ? 'var(--color-popover-bg)' : 'var(--color-tooltip-bg)') as never }
171
+ transition="quick"
172
+ enterStyle={{ opacity: 0, scale: 0.95 }}
173
+ exitStyle={{ opacity: 0, scale: 0.95 }}
174
+ { ...(isPopover ? {
175
+ borderWidth: 1,
176
+ borderColor: 'var(--color-border-divider)' as never,
177
+ shadowColor: 'var(--shadow-popover)' as never,
178
+ } : {}) }
172
179
  onClick={ interactive ? handleContentClick : undefined }
173
180
  { ...(selected ? { 'data-selected': true } : {}) }
174
181
  { ...contentProps as any }
@@ -176,12 +183,13 @@ export const Tooltip = React.forwardRef<HTMLDivElement, TooltipProps>(
176
183
  { showArrow && (
177
184
  <GuiTooltip.Arrow
178
185
  unstyled
179
- className={ cn(
180
- isPopover ? 'fill-[var(--color-popover-bg)]' : 'fill-[var(--color-tooltip-bg)]',
181
- ) }
186
+ backgroundColor={ (isPopover ? 'var(--color-popover-bg)' : 'var(--color-tooltip-bg)') as never }
182
187
  />
183
188
  ) }
184
- { content }
189
+ { ink(content, undefined, {
190
+ fontSize: 14,
191
+ color: (isPopover ? 'var(--color-text-primary)' : 'var(--color-tooltip-fg)') as never,
192
+ }) }
185
193
  </GuiTooltip.Content>
186
194
  </GuiTooltip>
187
195
  </GuiTooltipGroup>
package/tokens.css CHANGED
@@ -890,6 +890,15 @@
890
890
  they do not loop. The spinner rotates forever, so it is CSS. */
891
891
  .lux-spin { animation: lux-spin 0.8s linear infinite }
892
892
 
893
+ /* A scrolling strip with no visible scrollbar. The scrollbar is a
894
+ pseudo-element on the box, so this is the only place it can be said. */
895
+ /* One-line truncation. `text-overflow` and `white-space` are not in gui's Text
896
+ style base, and the three declarations only mean anything together. */
897
+ .lux-truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
898
+
899
+ .lux-no-scrollbar { scrollbar-width: none; -ms-overflow-style: none }
900
+ .lux-no-scrollbar::-webkit-scrollbar { display: none }
901
+
893
902
  /* Size the ICON inside a control. A descendant selector has no prop form, so
894
903
  the control publishes its icon size and this rule applies it. */
895
904
  .lux-icon svg { width: var(--icon-size, 20px); height: var(--icon-size, 20px) }
@@ -943,6 +952,15 @@
943
952
  user-select: none;
944
953
  }
945
954
 
955
+ /* A skeleton wrapping real content (asChild, or loading with children already
956
+ mounted) must mask it — an <img> or an icon doesn't pick up `color:
957
+ transparent` above. `visibility` rather than `display:none` so the masked
958
+ subtree keeps its layout instead of collapsing it, which is the zero-CLS
959
+ promise this component makes. A descendant selector has no prop form. */
960
+ .lux-skeleton * {
961
+ visibility: hidden;
962
+ }
963
+
946
964
  /* The one moving part. Fixed, 2px, above everything, out of flow — so it
947
965
  cannot shift layout and cannot tint content. Mount it while any request
948
966
  is in flight; unmount it when they land. One per page. */