@tempoxyz/ds 0.3.0 → 0.3.1
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/web/components/Accordion.js.zyzz.json +1 -1
- package/dist/web/components/AgentLogo.js.zyzz.json +1 -1
- package/dist/web/components/Alert.css +1 -1
- package/dist/web/components/Alert.d.ts +7 -2
- package/dist/web/components/Alert.d.ts.map +1 -1
- package/dist/web/components/Alert.js +12 -7
- package/dist/web/components/Alert.js.map +1 -1
- package/dist/web/components/Alert.js.zyzz.json +1 -1
- package/dist/web/components/AlertDialog.js.zyzz.json +1 -1
- package/dist/web/components/Amount.js.zyzz.json +1 -1
- package/dist/web/components/AmountInput.js.zyzz.json +1 -1
- package/dist/web/components/AppShell.css +1 -1
- package/dist/web/components/AppShell.d.ts +108 -0
- package/dist/web/components/AppShell.d.ts.map +1 -1
- package/dist/web/components/AppShell.js +72 -24
- package/dist/web/components/AppShell.js.map +1 -1
- package/dist/web/components/AppShell.js.zyzz.json +1 -1
- package/dist/web/components/AssetChip.js.zyzz.json +1 -1
- package/dist/web/components/Autocomplete.js.zyzz.json +1 -1
- package/dist/web/components/Avatar.js.zyzz.json +1 -1
- package/dist/web/components/Badge.css +1 -1
- package/dist/web/components/Badge.d.ts +1 -1
- package/dist/web/components/Badge.d.ts.map +1 -1
- package/dist/web/components/Badge.js.map +1 -1
- package/dist/web/components/Badge.js.zyzz.json +1 -1
- package/dist/web/components/Banner.css +1 -1
- package/dist/web/components/Banner.js.zyzz.json +1 -1
- package/dist/web/components/BarChart.css +1 -1
- package/dist/web/components/BarChart.js.zyzz.json +1 -1
- package/dist/web/components/Breadcrumbs.js.zyzz.json +1 -1
- package/dist/web/components/Button.css +1 -1
- package/dist/web/components/Button.d.ts +10 -6
- package/dist/web/components/Button.d.ts.map +1 -1
- package/dist/web/components/Button.js +4 -3
- package/dist/web/components/Button.js.map +1 -1
- package/dist/web/components/Button.js.zyzz.json +1 -1
- package/dist/web/components/ButtonGroup.js.zyzz.json +1 -1
- package/dist/web/components/Calendar.css +1 -1
- package/dist/web/components/Calendar.js.zyzz.json +1 -1
- package/dist/web/components/Checkbox.js.zyzz.json +1 -1
- package/dist/web/components/CheckboxGroup.js.zyzz.json +1 -1
- package/dist/web/components/ChoiceGroup.css +1 -1
- package/dist/web/components/ChoiceGroup.js.zyzz.json +1 -1
- package/dist/web/components/Choicebox.js.zyzz.json +1 -1
- package/dist/web/components/Combobox.js.zyzz.json +1 -1
- package/dist/web/components/Container.js.zyzz.json +1 -1
- package/dist/web/components/CopyButton.js.zyzz.json +1 -1
- package/dist/web/components/DatePicker.css +1 -1
- package/dist/web/components/DatePicker.js.zyzz.json +1 -1
- package/dist/web/components/Dialog.d.ts.map +1 -1
- package/dist/web/components/Dialog.js +1 -1
- package/dist/web/components/Dialog.js.map +1 -1
- package/dist/web/components/Dialog.js.zyzz.json +1 -1
- package/dist/web/components/Drawer.js.zyzz.json +1 -1
- package/dist/web/components/Empty.css +1 -1
- package/dist/web/components/Empty.js.zyzz.json +1 -1
- package/dist/web/components/Field.js.zyzz.json +1 -1
- package/dist/web/components/Fieldset.js.zyzz.json +1 -1
- package/dist/web/components/Frame.css +1 -1
- package/dist/web/components/Frame.js.zyzz.json +1 -1
- package/dist/web/components/Gauge.js.zyzz.json +1 -1
- package/dist/web/components/Grid.js.zyzz.json +1 -1
- package/dist/web/components/Hex.css +1 -1
- package/dist/web/components/Hex.js.zyzz.json +1 -1
- package/dist/web/components/IconTile.css +1 -1
- package/dist/web/components/IconTile.js.zyzz.json +1 -1
- package/dist/web/components/Identicon.js.zyzz.json +1 -1
- package/dist/web/components/Inline.js.zyzz.json +1 -1
- package/dist/web/components/Inset.js.zyzz.json +1 -1
- package/dist/web/components/Kbd.css +1 -1
- package/dist/web/components/Kbd.js.zyzz.json +1 -1
- package/dist/web/components/ListGroup.css +1 -1
- package/dist/web/components/ListGroup.d.ts +55 -8
- package/dist/web/components/ListGroup.d.ts.map +1 -1
- package/dist/web/components/ListGroup.js +23 -19
- package/dist/web/components/ListGroup.js.map +1 -1
- package/dist/web/components/ListGroup.js.zyzz.json +1 -1
- package/dist/web/components/LoadingDots.js.zyzz.json +1 -1
- package/dist/web/components/Menu.css +1 -1
- package/dist/web/components/Menu.js.zyzz.json +1 -1
- package/dist/web/components/NetworkLogo.js.zyzz.json +1 -1
- package/dist/web/components/NumberField.js.zyzz.json +1 -1
- package/dist/web/components/OtpField.css +1 -1
- package/dist/web/components/OtpField.js.zyzz.json +1 -1
- package/dist/web/components/Pagination.css +1 -1
- package/dist/web/components/Pagination.js.zyzz.json +1 -1
- package/dist/web/components/Popover.js.zyzz.json +1 -1
- package/dist/web/components/Progress.js.zyzz.json +1 -1
- package/dist/web/components/Qr.js.zyzz.json +1 -1
- package/dist/web/components/RadioGroup.js.zyzz.json +1 -1
- package/dist/web/components/SecretValue.css +1 -1
- package/dist/web/components/SecretValue.js.zyzz.json +1 -1
- package/dist/web/components/Select.css +1 -1
- package/dist/web/components/Select.d.ts.map +1 -1
- package/dist/web/components/Select.js +2 -2
- package/dist/web/components/Select.js.map +1 -1
- package/dist/web/components/Select.js.zyzz.json +1 -1
- package/dist/web/components/Separator.js.zyzz.json +1 -1
- package/dist/web/components/SidebarMenu.css +1 -1
- package/dist/web/components/SidebarMenu.d.ts +52 -0
- package/dist/web/components/SidebarMenu.d.ts.map +1 -1
- package/dist/web/components/SidebarMenu.js +47 -16
- package/dist/web/components/SidebarMenu.js.map +1 -1
- package/dist/web/components/SidebarMenu.js.zyzz.json +1 -1
- package/dist/web/components/Skeleton.css +1 -1
- package/dist/web/components/Skeleton.js.zyzz.json +1 -1
- package/dist/web/components/Slider.js.zyzz.json +1 -1
- package/dist/web/components/Snippet.js.zyzz.json +1 -1
- package/dist/web/components/Spinner.js.zyzz.json +1 -1
- package/dist/web/components/SplitBar.js.zyzz.json +1 -1
- package/dist/web/components/SplitButton.js.zyzz.json +1 -1
- package/dist/web/components/Stack.js.zyzz.json +1 -1
- package/dist/web/components/StatusChip.css +1 -1
- package/dist/web/components/StatusChip.js.zyzz.json +1 -1
- package/dist/web/components/StatusRow.js.zyzz.json +1 -1
- package/dist/web/components/Stepper.js.zyzz.json +1 -1
- package/dist/web/components/Surface.css +1 -1
- package/dist/web/components/Surface.d.ts +85 -0
- package/dist/web/components/Surface.d.ts.map +1 -1
- package/dist/web/components/Surface.js +6 -3
- package/dist/web/components/Surface.js.map +1 -1
- package/dist/web/components/Surface.js.zyzz.json +1 -1
- package/dist/web/components/Switch.js.zyzz.json +1 -1
- package/dist/web/components/Table.css +1 -1
- package/dist/web/components/Table.js.zyzz.json +1 -1
- package/dist/web/components/Tabs.js.zyzz.json +1 -1
- package/dist/web/components/TempoLogo.js.zyzz.json +1 -1
- package/dist/web/components/TextInput.js.zyzz.json +1 -1
- package/dist/web/components/Textarea.css +1 -1
- package/dist/web/components/Textarea.js.zyzz.json +1 -1
- package/dist/web/components/Toast.css +1 -1
- package/dist/web/components/Toast.js.zyzz.json +1 -1
- package/dist/web/components/Token.js.zyzz.json +1 -1
- package/dist/web/components/TokenLogo.js.zyzz.json +1 -1
- package/dist/web/components/Tooltip.js.zyzz.json +1 -1
- package/dist/web/components/VerificationCode.css +1 -1
- package/dist/web/components/VerificationCode.js.zyzz.json +1 -1
- package/dist/web/components/Visual.js.zyzz.json +1 -1
- package/dist/web/components/internal/Field.css +1 -1
- package/dist/web/components/internal/Field.js.zyzz.json +1 -1
- package/dist/web/components/internal/InputGroup.css +1 -1
- package/dist/web/components/internal/InputGroup.js.zyzz.json +1 -1
- package/dist/web/components/internal/Listbox.css +1 -1
- package/dist/web/components/internal/Listbox.js.zyzz.json +1 -1
- package/dist/web/components/internal/TabBar.d.ts +33 -0
- package/dist/web/components/internal/TabBar.d.ts.map +1 -0
- package/dist/web/components/internal/TabBar.js +30 -0
- package/dist/web/components/internal/TabBar.js.map +1 -0
- package/dist/web/components/internal/Text.d.ts +9 -0
- package/dist/web/components/internal/Text.d.ts.map +1 -0
- package/dist/web/components/internal/Text.js +35 -0
- package/dist/web/components/internal/Text.js.map +1 -0
- package/dist/web/index.js.zyzz.json +1 -1
- package/dist/web/vars.d.ts +26 -3
- package/dist/web/vars.d.ts.map +1 -1
- package/dist/web/vars.js.map +1 -1
- package/dist/web/vars.js.zyzz.json +1 -1
- package/dist/web/zyzz.config.d.ts +130 -15
- package/dist/web/zyzz.config.d.ts.map +1 -1
- package/dist/web/zyzz.config.js.zyzz.json +1 -1
- package/package.json +1 -1
- package/src/web/components/Alert.tsx +25 -5
- package/src/web/components/AppShell.tsx +273 -8
- package/src/web/components/Badge.tsx +4 -2
- package/src/web/components/Button.tsx +14 -10
- package/src/web/components/Dialog.tsx +7 -1
- package/src/web/components/ListGroup.tsx +95 -14
- package/src/web/components/Select.tsx +2 -0
- package/src/web/components/SidebarMenu.tsx +97 -3
- package/src/web/components/Surface.tsx +105 -1
- package/src/web/components/internal/TabBar.ts +52 -0
- package/src/web/components/internal/Text.ts +39 -0
- package/src/web/vars.ts +20 -7
|
@@ -5,7 +5,7 @@ import { useRender } from '@base-ui/react/use-render'
|
|
|
5
5
|
import type { ComponentPropsWithRef, MouseEvent, ReactNode } from 'react'
|
|
6
6
|
import type * as zyzz from 'zyzz'
|
|
7
7
|
import { ChevronRight } from '../icons/ChevronRight.js'
|
|
8
|
-
import { style, variants } from '../zyzz.config.js'
|
|
8
|
+
import { style, variants, vars } from '../zyzz.config.js'
|
|
9
9
|
import * as RowDisabled from './internal/RowDisabled.js'
|
|
10
10
|
import * as RowsSize from './internal/RowsSize.js'
|
|
11
11
|
|
|
@@ -262,11 +262,13 @@ export namespace ListGroup {
|
|
|
262
262
|
|
|
263
263
|
/**
|
|
264
264
|
* `variant` sets the container. `plain` rows have none and line up with the content around them.
|
|
265
|
-
* `boxed` rows sit in a bordered block with 16px sides and 8px above and below
|
|
265
|
+
* `boxed` rows sit in a bordered block with 16px sides and 8px above and below, and a row's
|
|
266
|
+
* hover fill reaches the border. `spaced` rows
|
|
266
267
|
* sit in the same block with 16px all round, 12px apart, with 14px text and no row padding.
|
|
267
268
|
*
|
|
268
269
|
* Rows are 36px with 12px text, which fits a 32px control. `dividers={false}` drops the line
|
|
269
270
|
* between rows, which `spaced` never draws. Variants default to `dividers`, `variant="plain"`.
|
|
271
|
+
* Inside a `Surface`, `plain` rows reach its edges and pad by its padding.
|
|
270
272
|
*/
|
|
271
273
|
export function Root(props: Root.Props) {
|
|
272
274
|
const { className, dividers, variant, vars, ...attributes } = props
|
|
@@ -275,6 +277,8 @@ export namespace ListGroup {
|
|
|
275
277
|
<RowsSize.Context value>
|
|
276
278
|
<div
|
|
277
279
|
{...attributes}
|
|
280
|
+
data-list-group=""
|
|
281
|
+
data-variant={variant ?? 'plain'}
|
|
278
282
|
{...styles.root({ className, dividers, style: props.style, variant, vars })}
|
|
279
283
|
/>
|
|
280
284
|
</RowsSize.Context>
|
|
@@ -291,6 +295,8 @@ export namespace ListGroup {
|
|
|
291
295
|
* apart, such as an `Avatar`, a `ListGroup.Info` and a trailing `Button`. A pressable row's hover
|
|
292
296
|
* fill covers the row, flush with its content. A static row that holds a control, such as a Copy
|
|
293
297
|
* `Button`, takes the same fill on hover and stays static, so the control is the only target.
|
|
298
|
+
* A `Select` in a row hugs its value at the row's end, in the row's type, 32px tall and without a
|
|
299
|
+
* fill of its own.
|
|
294
300
|
*
|
|
295
301
|
* A row with a `value` is a radio of the `ListGroup.RadioGroup` around it, in a 12px block with
|
|
296
302
|
* 16px sides. A press anywhere on the row selects it, and a 16px ring at its end shows the
|
|
@@ -443,6 +449,30 @@ namespace styles {
|
|
|
443
449
|
minWidth: '0',
|
|
444
450
|
padding: '0',
|
|
445
451
|
textAlign: 'start',
|
|
452
|
+
selectors: {
|
|
453
|
+
// A select in a row reads as the row's value: it hugs its value at the end in the row's
|
|
454
|
+
// type, fits the row's 32px control height, and draws no fill, since the row draws the
|
|
455
|
+
// hover. A disabled or invalid trigger keeps its own fill. Its box ends on the content
|
|
456
|
+
// edge, so it never overflows the container, and its chevron sits 8px inside its focus
|
|
457
|
+
// ring. `:is(*, :focus-visible)` outweighs the trigger's focus padding, so focus never moves
|
|
458
|
+
// the value. `Select.Trigger` marks itself, so text-input comboboxes keep their field and a
|
|
459
|
+
// trigger rendered as another element still matches.
|
|
460
|
+
'& [data-select-trigger][role="combobox"]:is(*, :focus-visible)': {
|
|
461
|
+
flex: '0 1 auto !custom',
|
|
462
|
+
font: 'inherit !custom',
|
|
463
|
+
height: '32',
|
|
464
|
+
letterSpacing: 'inherit !custom',
|
|
465
|
+
marginInlineStart: 'auto !custom',
|
|
466
|
+
minWidth: '0',
|
|
467
|
+
paddingInline: '8',
|
|
468
|
+
width: 'auto !custom',
|
|
469
|
+
},
|
|
470
|
+
'& [data-select-trigger]:not(:disabled, [data-disabled], [data-invalid], [aria-invalid="true"])':
|
|
471
|
+
{
|
|
472
|
+
backgroundColor: 'transparent !custom',
|
|
473
|
+
},
|
|
474
|
+
'& [data-select-trigger] > [data-select-value]': { flex: '0 1 auto !custom' },
|
|
475
|
+
},
|
|
446
476
|
},
|
|
447
477
|
{ id: 'tds/web/list-group/content' },
|
|
448
478
|
)
|
|
@@ -626,9 +656,9 @@ namespace styles {
|
|
|
626
656
|
// A row with a meter pads 10px, which centers a 24px agent logo in 44px.
|
|
627
657
|
'& [data-divider]:has([role="meter"])': { paddingBlock: '10' },
|
|
628
658
|
// A hovered row's fill would otherwise sit between two visible separators.
|
|
629
|
-
'& [data-divider]:is(a[href], button, [role="button"], :has(a[href], button, input, select, textarea)):not(:disabled, [data-disabled]):hover::before':
|
|
659
|
+
'& [data-divider]:is(a[href], button, [role="button"], :has(:where(a[href], :is(button, input, select, textarea, [data-select-trigger]):not(:disabled, [data-disabled])))):not(:disabled, [data-disabled]):hover::before':
|
|
630
660
|
{ display: 'none' },
|
|
631
|
-
'& [data-divider]:is(a[href], button, [role="button"], :has(a[href], button, input, select, textarea)):not(:disabled, [data-disabled]):hover + [data-divider]::before':
|
|
661
|
+
'& [data-divider]:is(a[href], button, [role="button"], :has(:where(a[href], :is(button, input, select, textarea, [data-select-trigger]):not(:disabled, [data-disabled])))):not(:disabled, [data-disabled]):hover + [data-divider]::before':
|
|
632
662
|
{ display: 'none' },
|
|
633
663
|
},
|
|
634
664
|
},
|
|
@@ -646,6 +676,7 @@ namespace styles {
|
|
|
646
676
|
borderRadius: 'sm',
|
|
647
677
|
borderStyle: 'solid',
|
|
648
678
|
borderWidth: 'regular',
|
|
679
|
+
isolation: 'isolate !custom',
|
|
649
680
|
paddingBlock: '8',
|
|
650
681
|
paddingInline: '16',
|
|
651
682
|
selectors: {
|
|
@@ -653,6 +684,39 @@ namespace styles {
|
|
|
653
684
|
'& > [data-divider]:first-child:has([data-info][data-size="large"])': {
|
|
654
685
|
paddingBlockStart: '0',
|
|
655
686
|
},
|
|
687
|
+
// A row's fill is a layer behind the row that reaches the border across the block's
|
|
688
|
+
// 16px sides, and its 8px above the first row and below the last. The row keeps its
|
|
689
|
+
// place, and the fill rounds inside the border at the block's corners.
|
|
690
|
+
'& > [data-divider]::after': {
|
|
691
|
+
backgroundColor: 'transparent !custom',
|
|
692
|
+
content: '"" !custom',
|
|
693
|
+
insetBlock: '0',
|
|
694
|
+
insetInline: '-16',
|
|
695
|
+
position: 'absolute',
|
|
696
|
+
transitionDuration: '160',
|
|
697
|
+
transitionProperty: 'background-color',
|
|
698
|
+
transitionTimingFunction: 'snappy',
|
|
699
|
+
zIndex: '-1 !custom',
|
|
700
|
+
},
|
|
701
|
+
'& > [data-divider]:first-child::after': {
|
|
702
|
+
borderStartEndRadius: `calc(${vars.radius.sm} - 1px) !custom`,
|
|
703
|
+
borderStartStartRadius: `calc(${vars.radius.sm} - 1px) !custom`,
|
|
704
|
+
insetBlockStart: '-8',
|
|
705
|
+
},
|
|
706
|
+
'& > [data-divider]:last-child::after': {
|
|
707
|
+
borderEndEndRadius: `calc(${vars.radius.sm} - 1px) !custom`,
|
|
708
|
+
borderEndStartRadius: `calc(${vars.radius.sm} - 1px) !custom`,
|
|
709
|
+
insetBlockEnd: '-8',
|
|
710
|
+
},
|
|
711
|
+
'& > [data-divider]:is(a[href], button, [role="button"], :has(:where(a[href], :is(button, input, select, textarea, [data-select-trigger]):not(:disabled, [data-disabled])))):not(:disabled, [data-disabled], [data-radio]):hover::after':
|
|
712
|
+
{ backgroundColor: 'container.regular' },
|
|
713
|
+
'& > [data-divider]:is(a[href], button, [role="button"]):not(:disabled, [data-disabled]):active::after':
|
|
714
|
+
{
|
|
715
|
+
backgroundColor: 'container.strong',
|
|
716
|
+
},
|
|
717
|
+
},
|
|
718
|
+
'@media (prefers-reduced-motion: reduce)': {
|
|
719
|
+
selectors: { '& > [data-divider]::after': { transitionProperty: 'none' } },
|
|
656
720
|
},
|
|
657
721
|
},
|
|
658
722
|
plain: {},
|
|
@@ -703,22 +767,39 @@ namespace styles {
|
|
|
703
767
|
textAlign: 'start',
|
|
704
768
|
textDecoration: 'none !custom',
|
|
705
769
|
transitionDuration: '160',
|
|
706
|
-
transitionProperty: 'background-color, opacity, outline-color, outline-offset',
|
|
770
|
+
transitionProperty: 'background-color, box-shadow, opacity, outline-color, outline-offset',
|
|
707
771
|
transitionTimingFunction: 'snappy',
|
|
708
772
|
width: '100% !custom',
|
|
709
773
|
selectors: {
|
|
710
|
-
// The
|
|
711
|
-
//
|
|
712
|
-
|
|
774
|
+
// The row keeps Figma's List Row geometry, flush with its content, and its fill bleeds
|
|
775
|
+
// 12px past the content at each side. A shadow on each side draws the bleed: an outer
|
|
776
|
+
// shadow never paints under the row, so a translucent fill never doubles. A dialog body
|
|
777
|
+
// scrolls and clips at its 4px padding, so the bleed there is 4px, except in a surface,
|
|
778
|
+
// whose rows draw none. A boxed group draws the fill behind its rows. A static row that holds an enabled control takes the same fill
|
|
779
|
+
// on hover, without becoming pressable.
|
|
780
|
+
'&:is(a[href], button, [role="button"], :has(:where(a[href], :is(button, input, select, textarea, [data-select-trigger]):not(:disabled, [data-disabled])))):not([data-radio])':
|
|
713
781
|
{ borderRadius: 'xs' },
|
|
714
|
-
'&:is(a[href], button, [role="button"], :has(a[href], button, input, select, textarea)):not(:disabled, [data-disabled], [data-radio]):hover':
|
|
715
|
-
{
|
|
782
|
+
'&:is(a[href], button, [role="button"], :has(:where(a[href], :is(button, input, select, textarea, [data-select-trigger]):not(:disabled, [data-disabled])))):not(:disabled, [data-disabled], [data-radio], [data-variant="boxed"] > *):hover':
|
|
783
|
+
{
|
|
784
|
+
backgroundColor: 'container.regular',
|
|
785
|
+
boxShadow: `-12px 0 0 0 ${vars.color.container.regular}, 12px 0 0 0 ${vars.color.container.regular} !custom`,
|
|
786
|
+
},
|
|
716
787
|
'&:is(a[href], button, [role="button"]):not(:disabled, [data-disabled])': {
|
|
717
788
|
cursor: 'pointer',
|
|
718
789
|
},
|
|
719
|
-
'&:is(a[href], button, [role="button"]):not(:disabled, [data-disabled]):active':
|
|
720
|
-
|
|
721
|
-
|
|
790
|
+
'&:is(a[href], button, [role="button"]):not(:disabled, [data-disabled], [data-variant="boxed"] > *):active':
|
|
791
|
+
{
|
|
792
|
+
backgroundColor: 'container.strong',
|
|
793
|
+
boxShadow: `-12px 0 0 0 ${vars.color.container.strong}, 12px 0 0 0 ${vars.color.container.strong} !custom`,
|
|
794
|
+
},
|
|
795
|
+
':is([data-dialog-body], [data-dialog-body] :not([data-surface-size])) > [data-list-group]:not([data-variant="boxed"]) > &:is(a[href], button, [role="button"], :has(:where(a[href], :is(button, input, select, textarea, [data-select-trigger]):not(:disabled, [data-disabled])))):not(:disabled, [data-disabled], [data-radio]):hover':
|
|
796
|
+
{
|
|
797
|
+
boxShadow: `-4px 0 0 0 ${vars.color.container.regular}, 4px 0 0 0 ${vars.color.container.regular} !custom`,
|
|
798
|
+
},
|
|
799
|
+
':is([data-dialog-body], [data-dialog-body] :not([data-surface-size])) > [data-list-group]:not([data-variant="boxed"]) > &:is(a[href], button, [role="button"]):not(:disabled, [data-disabled]):active':
|
|
800
|
+
{
|
|
801
|
+
boxShadow: `-4px 0 0 0 ${vars.color.container.strong}, 4px 0 0 0 ${vars.color.container.strong} !custom`,
|
|
802
|
+
},
|
|
722
803
|
'&:is(a[href], button, [role="button"]):focus-visible': {
|
|
723
804
|
outlineColor: 'border.focus',
|
|
724
805
|
outlineOffset: '-2',
|
|
@@ -758,7 +839,7 @@ namespace styles {
|
|
|
758
839
|
},
|
|
759
840
|
},
|
|
760
841
|
'@media (prefers-reduced-motion: reduce)': {
|
|
761
|
-
transitionProperty: 'background-color, opacity, outline-color',
|
|
842
|
+
transitionProperty: 'background-color, box-shadow, opacity, outline-color',
|
|
762
843
|
selectors: {
|
|
763
844
|
'&[data-radio]': { transitionProperty: 'background-color, border-color, opacity' },
|
|
764
845
|
'&[data-radio]:active': { scale: 'none !custom' },
|
|
@@ -273,6 +273,7 @@ export namespace Select {
|
|
|
273
273
|
})}
|
|
274
274
|
aria-invalid={invalid || attributes['aria-invalid']}
|
|
275
275
|
data-invalid={invalid || undefined}
|
|
276
|
+
data-select-trigger=""
|
|
276
277
|
/>
|
|
277
278
|
)
|
|
278
279
|
}
|
|
@@ -296,6 +297,7 @@ export namespace Select {
|
|
|
296
297
|
return (
|
|
297
298
|
<BaseUi.Select.Value
|
|
298
299
|
{...attributes}
|
|
300
|
+
data-select-value=""
|
|
299
301
|
{...styles.value({ className, style: props.style, vars })}
|
|
300
302
|
/>
|
|
301
303
|
)
|
|
@@ -13,6 +13,8 @@ import type * as zyzz from 'zyzz'
|
|
|
13
13
|
import { ChevronDown } from '../icons/ChevronDown.js'
|
|
14
14
|
import { ChevronRight } from '../icons/ChevronRight.js'
|
|
15
15
|
import { style, variants } from '../zyzz.config.js'
|
|
16
|
+
import * as TabBar from './internal/TabBar.js'
|
|
17
|
+
import * as Text from './internal/Text.js'
|
|
16
18
|
|
|
17
19
|
// A group names itself after the ID of the label inside it, which a caller can set.
|
|
18
20
|
type GroupContext = { id: string; setLabel: (id: string | undefined) => void }
|
|
@@ -180,15 +182,44 @@ export namespace SidebarMenu {
|
|
|
180
182
|
* Renders a link by default. Pass `render` to use a router link. `active` marks the current page
|
|
181
183
|
* with `aria-current="page"`, and sets its label in the strong weight. Keyboard focus draws a
|
|
182
184
|
* 2px ring inside the row and moves its content 2px inward.
|
|
185
|
+
*
|
|
186
|
+
* Inside an `AppShell`, `tabBar` repeats the item in the bottom bar below 768px, with the same
|
|
187
|
+
* `href`, `render` and handlers. In an `AppShell` with `collapse`, the sidebar shows only the
|
|
188
|
+
* first element of each item from 768px to 1023px, such as its icon, and keeps its text as the
|
|
189
|
+
* accessible name.
|
|
190
|
+
*
|
|
191
|
+
* @example
|
|
192
|
+
* <SidebarMenu.Item active href="/" tabBar="tab">
|
|
193
|
+
* <Home />
|
|
194
|
+
* Home
|
|
195
|
+
* </SidebarMenu.Item>
|
|
183
196
|
*/
|
|
184
197
|
export function Item(props: Item.Props) {
|
|
185
|
-
const { active = false, className, ref, render, vars, ...attributes } = props
|
|
198
|
+
const { active = false, className, ref, render, tabBar, vars, ...attributes } = props
|
|
199
|
+
const store = use(TabBar.Context)
|
|
200
|
+
const id = useId()
|
|
201
|
+
|
|
202
|
+
// Registers on every render, so the bottom bar repeats the current props of the item.
|
|
203
|
+
useLayoutEffect(() => {
|
|
204
|
+
if (!store) return
|
|
205
|
+
if (!tabBar) return store.delete(id)
|
|
206
|
+
|
|
207
|
+
const { children, style: _, ...rest } = attributes
|
|
208
|
+
store.set(id, { active, attributes: rest, children, render, variant: tabBar })
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
useLayoutEffect(() => {
|
|
212
|
+
if (!store) return
|
|
213
|
+
return () => store.delete(id)
|
|
214
|
+
}, [id, store])
|
|
186
215
|
|
|
187
216
|
return useRender({
|
|
188
217
|
defaultTagName: 'a',
|
|
189
218
|
props: {
|
|
190
219
|
...attributes,
|
|
191
220
|
'aria-current': active ? 'page' : undefined,
|
|
221
|
+
// Text renders in a span, so the collapsed sidebar can hide it as an element.
|
|
222
|
+
children: Text.wrap(attributes.children),
|
|
192
223
|
...styles.item({ active, className, style: props.style, vars }),
|
|
193
224
|
},
|
|
194
225
|
ref,
|
|
@@ -200,6 +231,12 @@ export namespace SidebarMenu {
|
|
|
200
231
|
export type Props = Omit<useRender.ComponentProps<'a'>, 'color'> &
|
|
201
232
|
Omit<zyzz.Props.Variants<typeof styles.item>, 'active'> & {
|
|
202
233
|
active?: boolean | undefined
|
|
234
|
+
/**
|
|
235
|
+
* Repeats the item in the bottom bar of an `AppShell` below 768px: `tab` as an icon in the
|
|
236
|
+
* tab bar, or `action` as the floating action button beside it. Give the tab bar up to
|
|
237
|
+
* three items and one action. Absent, the item stays in the sidebar drawer only.
|
|
238
|
+
*/
|
|
239
|
+
tabBar?: TabBar.Variant | undefined
|
|
203
240
|
}
|
|
204
241
|
}
|
|
205
242
|
|
|
@@ -293,7 +330,8 @@ export namespace SidebarMenu {
|
|
|
293
330
|
{...attributes}
|
|
294
331
|
{...styles.item({ className, style: props.style, vars })}
|
|
295
332
|
>
|
|
296
|
-
{
|
|
333
|
+
{/* Text renders in a span, so the collapsed sidebar can hide it as an element. */}
|
|
334
|
+
{Text.wrap(children)}
|
|
297
335
|
<ChevronRight {...styles.chevron()} />
|
|
298
336
|
</BaseUi.Collapsible.Trigger>
|
|
299
337
|
)
|
|
@@ -330,6 +368,9 @@ namespace styles {
|
|
|
330
368
|
selectors: {
|
|
331
369
|
':hover > &, [data-popup-open] > &': { color: 'content.primary' },
|
|
332
370
|
},
|
|
371
|
+
'@media (min-width: 768px) and (max-width: 1023px)': {
|
|
372
|
+
selectors: { '[data-app-shell-rail] &': { display: 'none !custom' } },
|
|
373
|
+
},
|
|
333
374
|
},
|
|
334
375
|
{ id: 'tds/web/sidebar-menu/account-chevron' },
|
|
335
376
|
)
|
|
@@ -365,6 +406,15 @@ namespace styles {
|
|
|
365
406
|
export const accountRow = variants(
|
|
366
407
|
{
|
|
367
408
|
base: {
|
|
409
|
+
'@media (min-width: 768px) and (max-width: 1023px)': {
|
|
410
|
+
selectors: {
|
|
411
|
+
'[data-app-shell-rail] &': {
|
|
412
|
+
columnGap: '0',
|
|
413
|
+
justifyContent: 'center',
|
|
414
|
+
paddingInline: '0',
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
},
|
|
368
418
|
alignItems: 'center',
|
|
369
419
|
appearance: 'none !custom',
|
|
370
420
|
backgroundColor: 'transparent !custom',
|
|
@@ -413,7 +463,24 @@ namespace styles {
|
|
|
413
463
|
)
|
|
414
464
|
|
|
415
465
|
export const accountText = style(
|
|
416
|
-
{
|
|
466
|
+
{
|
|
467
|
+
display: 'flex',
|
|
468
|
+
flex: '1 !custom',
|
|
469
|
+
flexDirection: 'column',
|
|
470
|
+
minWidth: '0',
|
|
471
|
+
'@media (min-width: 768px) and (max-width: 1023px)': {
|
|
472
|
+
selectors: {
|
|
473
|
+
'[data-app-shell-rail] &': {
|
|
474
|
+
clipPath: 'inset(50%) !custom',
|
|
475
|
+
height: '1px !custom',
|
|
476
|
+
overflow: 'hidden',
|
|
477
|
+
position: 'absolute',
|
|
478
|
+
whiteSpace: 'nowrap',
|
|
479
|
+
width: '1px !custom',
|
|
480
|
+
},
|
|
481
|
+
},
|
|
482
|
+
},
|
|
483
|
+
},
|
|
417
484
|
{ id: 'tds/web/sidebar-menu/account-text' },
|
|
418
485
|
)
|
|
419
486
|
|
|
@@ -457,6 +524,10 @@ namespace styles {
|
|
|
457
524
|
paddingInline: '8',
|
|
458
525
|
selectors: { '[data-sidebar-menu-size="large"] &': { paddingInline: '12' } },
|
|
459
526
|
typography: 'body.b3',
|
|
527
|
+
// The collapsed sidebar keeps the label for the name of its group without painting it.
|
|
528
|
+
'@media (min-width: 768px) and (max-width: 1023px)': {
|
|
529
|
+
selectors: { '[data-app-shell-rail] &': { display: 'none !custom' } },
|
|
530
|
+
},
|
|
460
531
|
},
|
|
461
532
|
},
|
|
462
533
|
{ id: 'tds/web/sidebar-menu/group-label' },
|
|
@@ -523,6 +594,26 @@ namespace styles {
|
|
|
523
594
|
transitionProperty: 'background-color, color, outline-color !custom',
|
|
524
595
|
selectors: { '&:active': { scale: 'none !custom' } },
|
|
525
596
|
},
|
|
597
|
+
// The collapsed sidebar centers the first element, such as the icon, in a 40px row. Its
|
|
598
|
+
// other elements and its text stay in the accessible name.
|
|
599
|
+
'@media (min-width: 768px) and (max-width: 1023px)': {
|
|
600
|
+
selectors: {
|
|
601
|
+
'[data-app-shell-rail] &': {
|
|
602
|
+
columnGap: '0',
|
|
603
|
+
justifyContent: 'center',
|
|
604
|
+
paddingInline: '0',
|
|
605
|
+
position: 'relative',
|
|
606
|
+
},
|
|
607
|
+
'[data-app-shell-rail] & > :not(:first-child)': {
|
|
608
|
+
clipPath: 'inset(50%) !custom',
|
|
609
|
+
height: '1px !custom',
|
|
610
|
+
overflow: 'hidden',
|
|
611
|
+
position: 'absolute',
|
|
612
|
+
whiteSpace: 'nowrap',
|
|
613
|
+
width: '1px !custom',
|
|
614
|
+
},
|
|
615
|
+
},
|
|
616
|
+
},
|
|
526
617
|
},
|
|
527
618
|
defaultVariants: { active: false },
|
|
528
619
|
variants: {
|
|
@@ -579,6 +670,9 @@ namespace styles {
|
|
|
579
670
|
'[data-sidebar-menu-size="large"] &': { paddingTop: '4', rowGap: '4' },
|
|
580
671
|
'[data-sidebar-menu-size="small"] &': { paddingInlineStart: '24' },
|
|
581
672
|
},
|
|
673
|
+
'@media (min-width: 768px) and (max-width: 1023px)': {
|
|
674
|
+
selectors: { '[data-app-shell-rail] &': { paddingInlineStart: '0' } },
|
|
675
|
+
},
|
|
582
676
|
},
|
|
583
677
|
{ id: 'tds/web/sidebar-menu/submenu-content' },
|
|
584
678
|
)
|
|
@@ -13,6 +13,9 @@ import { variants, vars } from '../zyzz.config.js'
|
|
|
13
13
|
* - A pressable surface should hold no other interactive controls, since nested targets conflict.
|
|
14
14
|
* - A direct child `table` bleeds 12px into the padding at the sides and the bottom, as rows pad
|
|
15
15
|
* their cells, so its text stays on the content edge. The surface draws no border around it.
|
|
16
|
+
* - A direct child `plain` `ListGroup` bleeds into the padding, so a hover fill reaches the edges.
|
|
17
|
+
* Each row is 44px, pads its sides by the surface's padding, and shares the height of the
|
|
18
|
+
* tallest row.
|
|
16
19
|
*
|
|
17
20
|
* @example
|
|
18
21
|
* <Surface>
|
|
@@ -62,7 +65,13 @@ export namespace Surface {
|
|
|
62
65
|
export function Header(props: Header.Props) {
|
|
63
66
|
const { className, vars, ...attributes } = props
|
|
64
67
|
|
|
65
|
-
return
|
|
68
|
+
return (
|
|
69
|
+
<div
|
|
70
|
+
{...attributes}
|
|
71
|
+
data-surface-header=""
|
|
72
|
+
{...styles.header({ className, style: props.style, vars })}
|
|
73
|
+
/>
|
|
74
|
+
)
|
|
66
75
|
}
|
|
67
76
|
|
|
68
77
|
export namespace Header {
|
|
@@ -226,6 +235,38 @@ namespace styles {
|
|
|
226
235
|
// table clips rather than hides its overflow, so a sticky header still sticks to the
|
|
227
236
|
// scrolling ancestor. The surface draws no border around the rows.
|
|
228
237
|
'&:has(> table)': { borderStyle: 'none', overflow: 'clip !custom' },
|
|
238
|
+
// Every row takes the height of the tallest, such as a row that holds a 48px field. The
|
|
239
|
+
// column can shrink below a row's content, so a long value truncates inside the surface.
|
|
240
|
+
'& > [data-list-group][data-variant="plain"]': {
|
|
241
|
+
display: 'grid',
|
|
242
|
+
gridAutoRows: '1fr !custom',
|
|
243
|
+
gridTemplateColumns: 'minmax(0, 1fr) !custom',
|
|
244
|
+
},
|
|
245
|
+
// Rows are 44px, with 4px above and below a 32px control. Their fills stay square, except
|
|
246
|
+
// at the surface's corners, where each size rounds them inside the border.
|
|
247
|
+
'& > [data-list-group][data-variant="plain"] > [data-divider]': {
|
|
248
|
+
borderRadius: 'none',
|
|
249
|
+
minHeight: '44',
|
|
250
|
+
paddingBlock: '4',
|
|
251
|
+
},
|
|
252
|
+
// A row of a title over a description, or of a meter, keeps its own block padding.
|
|
253
|
+
'& > [data-list-group][data-variant="plain"] > [data-divider]:has([data-info])': {
|
|
254
|
+
paddingBlock: '12',
|
|
255
|
+
},
|
|
256
|
+
'& > [data-list-group][data-variant="plain"] > [data-divider]:has([role="meter"])': {
|
|
257
|
+
paddingBlock: '10',
|
|
258
|
+
},
|
|
259
|
+
// The rows already reach the surface's edges, so their fills draw no bleed past them.
|
|
260
|
+
'& > [data-list-group][data-variant="plain"] > [data-divider]:is(:hover, :active)': {
|
|
261
|
+
boxShadow: 'none !custom',
|
|
262
|
+
},
|
|
263
|
+
// After a header, the list rises by the 14px above the text of a one-line row, so the
|
|
264
|
+
// text keeps the header's 20px gap. After other content, the list keeps its place, so its
|
|
265
|
+
// fills never cover that content.
|
|
266
|
+
'& > [data-surface-header] + [data-list-group][data-variant="plain"]:has(> [data-divider])':
|
|
267
|
+
{
|
|
268
|
+
marginBlockStart: 'calc((1rem - 2.75rem) / 2) !custom',
|
|
269
|
+
},
|
|
229
270
|
// A table bleeds by the 12px that its rows pad their edge cells, so the rows sit 12px
|
|
230
271
|
// inside the padding and their text on the content edge.
|
|
231
272
|
'& > table': {
|
|
@@ -267,12 +308,35 @@ namespace styles {
|
|
|
267
308
|
variants: {
|
|
268
309
|
// Each size pads by its own radius, and they scale together. A direct child table with a
|
|
269
310
|
// header bleeds into the top padding, and its header cells take back exactly the padding.
|
|
311
|
+
// A direct child list bleeds into the padding at the sides, and above and below it at the
|
|
312
|
+
// surface's edges once it holds a row, so an empty list keeps the padding. Its rows take
|
|
313
|
+
// back the padding at the sides.
|
|
270
314
|
size: {
|
|
271
315
|
large: {
|
|
272
316
|
borderRadius: 'md',
|
|
273
317
|
padding: '24',
|
|
274
318
|
selectors: {
|
|
275
319
|
'& > table:first-child:has(> thead)': { marginBlockStart: '-24' },
|
|
320
|
+
'& > [data-list-group][data-variant="plain"]': { marginInline: '-24' },
|
|
321
|
+
'& > [data-list-group][data-variant="plain"]:first-child:has(> [data-divider])': {
|
|
322
|
+
marginBlockStart: '-24',
|
|
323
|
+
},
|
|
324
|
+
'& > [data-list-group][data-variant="plain"]:last-child:has(> [data-divider])': {
|
|
325
|
+
marginBlockEnd: '-24',
|
|
326
|
+
},
|
|
327
|
+
'& > [data-list-group][data-variant="plain"]:first-child > [data-divider]:first-child':
|
|
328
|
+
{
|
|
329
|
+
borderStartEndRadius: `calc(${vars.radius.md} - 1px) !custom`,
|
|
330
|
+
borderStartStartRadius: `calc(${vars.radius.md} - 1px) !custom`,
|
|
331
|
+
},
|
|
332
|
+
'& > [data-list-group][data-variant="plain"]:last-child > [data-divider]:last-child':
|
|
333
|
+
{
|
|
334
|
+
borderEndEndRadius: `calc(${vars.radius.md} - 1px) !custom`,
|
|
335
|
+
borderEndStartRadius: `calc(${vars.radius.md} - 1px) !custom`,
|
|
336
|
+
},
|
|
337
|
+
'& > [data-list-group][data-variant="plain"] > [data-divider]': {
|
|
338
|
+
paddingInline: '24',
|
|
339
|
+
},
|
|
276
340
|
'& > table:first-child > thead > tr:first-child > th': {
|
|
277
341
|
paddingBlockStart: '24',
|
|
278
342
|
},
|
|
@@ -283,6 +347,26 @@ namespace styles {
|
|
|
283
347
|
padding: '16',
|
|
284
348
|
selectors: {
|
|
285
349
|
'& > table:first-child:has(> thead)': { marginBlockStart: '-16' },
|
|
350
|
+
'& > [data-list-group][data-variant="plain"]': { marginInline: '-16' },
|
|
351
|
+
'& > [data-list-group][data-variant="plain"]:first-child:has(> [data-divider])': {
|
|
352
|
+
marginBlockStart: '-16',
|
|
353
|
+
},
|
|
354
|
+
'& > [data-list-group][data-variant="plain"]:last-child:has(> [data-divider])': {
|
|
355
|
+
marginBlockEnd: '-16',
|
|
356
|
+
},
|
|
357
|
+
'& > [data-list-group][data-variant="plain"]:first-child > [data-divider]:first-child':
|
|
358
|
+
{
|
|
359
|
+
borderStartEndRadius: `calc(${vars.radius.sm} - 1px) !custom`,
|
|
360
|
+
borderStartStartRadius: `calc(${vars.radius.sm} - 1px) !custom`,
|
|
361
|
+
},
|
|
362
|
+
'& > [data-list-group][data-variant="plain"]:last-child > [data-divider]:last-child':
|
|
363
|
+
{
|
|
364
|
+
borderEndEndRadius: `calc(${vars.radius.sm} - 1px) !custom`,
|
|
365
|
+
borderEndStartRadius: `calc(${vars.radius.sm} - 1px) !custom`,
|
|
366
|
+
},
|
|
367
|
+
'& > [data-list-group][data-variant="plain"] > [data-divider]': {
|
|
368
|
+
paddingInline: '16',
|
|
369
|
+
},
|
|
286
370
|
'& > table:first-child > thead > tr:first-child > th': {
|
|
287
371
|
paddingBlockStart: '16',
|
|
288
372
|
},
|
|
@@ -293,6 +377,26 @@ namespace styles {
|
|
|
293
377
|
padding: '12',
|
|
294
378
|
selectors: {
|
|
295
379
|
'& > table:first-child:has(> thead)': { marginBlockStart: '-12' },
|
|
380
|
+
'& > [data-list-group][data-variant="plain"]': { marginInline: '-12' },
|
|
381
|
+
'& > [data-list-group][data-variant="plain"]:first-child:has(> [data-divider])': {
|
|
382
|
+
marginBlockStart: '-12',
|
|
383
|
+
},
|
|
384
|
+
'& > [data-list-group][data-variant="plain"]:last-child:has(> [data-divider])': {
|
|
385
|
+
marginBlockEnd: '-12',
|
|
386
|
+
},
|
|
387
|
+
'& > [data-list-group][data-variant="plain"]:first-child > [data-divider]:first-child':
|
|
388
|
+
{
|
|
389
|
+
borderStartEndRadius: `calc(${vars.radius.xs} - 1px) !custom`,
|
|
390
|
+
borderStartStartRadius: `calc(${vars.radius.xs} - 1px) !custom`,
|
|
391
|
+
},
|
|
392
|
+
'& > [data-list-group][data-variant="plain"]:last-child > [data-divider]:last-child':
|
|
393
|
+
{
|
|
394
|
+
borderEndEndRadius: `calc(${vars.radius.xs} - 1px) !custom`,
|
|
395
|
+
borderEndStartRadius: `calc(${vars.radius.xs} - 1px) !custom`,
|
|
396
|
+
},
|
|
397
|
+
'& > [data-list-group][data-variant="plain"] > [data-divider]': {
|
|
398
|
+
paddingInline: '12',
|
|
399
|
+
},
|
|
296
400
|
'& > table:first-child > thead > tr:first-child > th': {
|
|
297
401
|
paddingBlockStart: '12',
|
|
298
402
|
},
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Holds the `SidebarMenu.Item` entries that `AppShell` repeats in its bottom bar below 768px. The
|
|
3
|
+
* store lives outside React state, so an item that registers on every render updates the bar
|
|
4
|
+
* without rendering the shell again.
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
import type { useRender } from '@base-ui/react/use-render'
|
|
8
|
+
import { createContext, type ReactNode } from 'react'
|
|
9
|
+
|
|
10
|
+
/** The props of a `SidebarMenu.Item` that the bottom bar repeats, without its sidebar styles. */
|
|
11
|
+
export type Entry = {
|
|
12
|
+
active: boolean
|
|
13
|
+
attributes: Readonly<Record<string, unknown>>
|
|
14
|
+
children: ReactNode
|
|
15
|
+
render: useRender.ComponentProps<'a'>['render']
|
|
16
|
+
variant: Variant
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** `tab` renders an icon in the tab bar, and `action` the floating action button beside it. */
|
|
20
|
+
export type Variant = 'action' | 'tab'
|
|
21
|
+
|
|
22
|
+
export type Store = ReturnType<typeof create>
|
|
23
|
+
|
|
24
|
+
export const Context = createContext<Store | undefined>(undefined)
|
|
25
|
+
|
|
26
|
+
/** Creates the store of one `AppShell.Root`. Entries keep the order in which they first register. */
|
|
27
|
+
export function create() {
|
|
28
|
+
const entries = new Map<string, Entry>()
|
|
29
|
+
const listeners = new Set<() => void>()
|
|
30
|
+
let snapshot: readonly (readonly [string, Entry])[] = []
|
|
31
|
+
|
|
32
|
+
const notify = () => {
|
|
33
|
+
snapshot = [...entries]
|
|
34
|
+
for (const listener of listeners) listener()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
delete(id: string) {
|
|
39
|
+
if (!entries.delete(id)) return
|
|
40
|
+
notify()
|
|
41
|
+
},
|
|
42
|
+
set(id: string, entry: Entry) {
|
|
43
|
+
entries.set(id, entry)
|
|
44
|
+
notify()
|
|
45
|
+
},
|
|
46
|
+
snapshot: () => snapshot,
|
|
47
|
+
subscribe(listener: () => void) {
|
|
48
|
+
listeners.add(listener)
|
|
49
|
+
return () => listeners.delete(listener)
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wraps each run of adjacent string and number children in one span, so a rule can hide a label
|
|
3
|
+
* as an element while the label still reads as one run, such as `Inbox ({count})`.
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import { Children, createElement, type HTMLAttributes, type ReactNode } from 'react'
|
|
7
|
+
|
|
8
|
+
/** Returns `children` with each run of adjacent text wrapped in a span with `props`. */
|
|
9
|
+
export function wrap(children: ReactNode, props: HTMLAttributes<HTMLSpanElement> = {}) {
|
|
10
|
+
const result: ReactNode[] = []
|
|
11
|
+
let run: (number | string)[] = []
|
|
12
|
+
|
|
13
|
+
// A run of only whitespace, such as the space between two elements on one JSX line, renders no
|
|
14
|
+
// flex item in a flex row, so it passes through unwrapped and adds no gap.
|
|
15
|
+
const flush = () => {
|
|
16
|
+
if (run.length === 0) return
|
|
17
|
+
if (run.every((part) => typeof part === 'string' && part.trim() === '')) {
|
|
18
|
+
result.push(...run)
|
|
19
|
+
run = []
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
result.push(createElement('span', { ...props, key: result.length }, ...run))
|
|
24
|
+
run = []
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
for (const child of Children.toArray(children)) {
|
|
28
|
+
if (typeof child === 'string' || typeof child === 'number') {
|
|
29
|
+
run.push(child)
|
|
30
|
+
continue
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
flush()
|
|
34
|
+
result.push(child)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
flush()
|
|
38
|
+
return result
|
|
39
|
+
}
|