@duro-app/ui 5.12.0 → 5.13.0

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 (188) hide show
  1. package/dist/Table-Bms2ftt3.js +1694 -0
  2. package/dist/Table-Bms2ftt3.js.map +1 -0
  3. package/dist/components/AppShell/native.css.d.ts +53 -0
  4. package/dist/components/AppShell/native.css.d.ts.map +1 -0
  5. package/dist/components/Drawer/Drawer.d.ts +7 -35
  6. package/dist/components/Drawer/Drawer.d.ts.map +1 -1
  7. package/dist/components/Drawer/Drawer.meta.d.ts.map +1 -1
  8. package/dist/components/Drawer/DrawerScroll.stories.d.ts.map +1 -1
  9. package/dist/components/Drawer/DrawerSnap.stories.d.ts +20 -0
  10. package/dist/components/Drawer/DrawerSnap.stories.d.ts.map +1 -0
  11. package/dist/components/Drawer/native.css.d.ts +55 -0
  12. package/dist/components/Drawer/native.css.d.ts.map +1 -0
  13. package/dist/components/Drawer/parts.css.d.ts +96 -0
  14. package/dist/components/Drawer/parts.css.d.ts.map +1 -0
  15. package/dist/components/Drawer/parts.d.ts +56 -0
  16. package/dist/components/Drawer/parts.d.ts.map +1 -0
  17. package/dist/components/Drawer/sheetPhysics.d.ts +56 -0
  18. package/dist/components/Drawer/sheetPhysics.d.ts.map +1 -0
  19. package/dist/components/Drawer/styles.css.d.ts +13 -58
  20. package/dist/components/Drawer/styles.css.d.ts.map +1 -1
  21. package/dist/components/Drawer/touchInput.d.ts +35 -0
  22. package/dist/components/Drawer/touchInput.d.ts.map +1 -0
  23. package/dist/components/Drawer/useDrawerRoot.d.ts +33 -0
  24. package/dist/components/Drawer/useDrawerRoot.d.ts.map +1 -0
  25. package/dist/components/Drawer/useSheetHeights.d.ts +7 -0
  26. package/dist/components/Drawer/useSheetHeights.d.ts.map +1 -0
  27. package/dist/components/Drawer/useSwipeDismiss.d.ts +13 -2
  28. package/dist/components/Drawer/useSwipeDismiss.d.ts.map +1 -1
  29. package/dist/components/Menu/Menu.d.ts +7 -1
  30. package/dist/components/Menu/Menu.d.ts.map +1 -1
  31. package/dist/components/Menu/native.css.d.ts +14 -0
  32. package/dist/components/Menu/native.css.d.ts.map +1 -0
  33. package/dist/components/Menu/useMenuOpen.d.ts +16 -0
  34. package/dist/components/Menu/useMenuOpen.d.ts.map +1 -0
  35. package/dist/components/Menu/useMenuRoot.d.ts +3 -6
  36. package/dist/components/Menu/useMenuRoot.d.ts.map +1 -1
  37. package/dist/components/Popover/native.css.d.ts +11 -0
  38. package/dist/components/Popover/native.css.d.ts.map +1 -0
  39. package/dist/components/SideNav/Chevron.d.ts +4 -0
  40. package/dist/components/SideNav/Chevron.d.ts.map +1 -0
  41. package/dist/components/SideNav/SideNav.d.ts.map +1 -1
  42. package/dist/components/SideNav/styles.css.d.ts +6 -0
  43. package/dist/components/SideNav/styles.css.d.ts.map +1 -1
  44. package/dist/components/SideNav/types.d.ts +6 -0
  45. package/dist/components/SideNav/types.d.ts.map +1 -0
  46. package/dist/components/SplitPane/SplitPane.d.ts +38 -0
  47. package/dist/components/SplitPane/SplitPane.d.ts.map +1 -0
  48. package/dist/components/SplitPane/SplitPane.meta.d.ts +3 -0
  49. package/dist/components/SplitPane/SplitPane.meta.d.ts.map +1 -0
  50. package/dist/components/SplitPane/SplitPane.stories.d.ts +17 -0
  51. package/dist/components/SplitPane/SplitPane.stories.d.ts.map +1 -0
  52. package/dist/components/SplitPane/styles.css.d.ts +78 -0
  53. package/dist/components/SplitPane/styles.css.d.ts.map +1 -0
  54. package/dist/components/Tabs/TabLayout.d.ts +7 -0
  55. package/dist/components/Tabs/TabLayout.d.ts.map +1 -0
  56. package/dist/components/Tabs/Tabs.d.ts.map +1 -1
  57. package/dist/components/Tabs/indicator.d.ts +28 -0
  58. package/dist/components/Tabs/indicator.d.ts.map +1 -0
  59. package/dist/components/Tabs/styles.css.d.ts +4 -0
  60. package/dist/components/Tabs/styles.css.d.ts.map +1 -1
  61. package/dist/components/Tabs/useTabIndicator.d.ts +4 -0
  62. package/dist/components/Tabs/useTabIndicator.d.ts.map +1 -0
  63. package/dist/components/Tabs/useTabKeys.d.ts +13 -0
  64. package/dist/components/Tabs/useTabKeys.d.ts.map +1 -0
  65. package/dist/components/Tag/RemoveGlyph.d.ts +4 -0
  66. package/dist/components/Tag/RemoveGlyph.d.ts.map +1 -0
  67. package/dist/components/Tag/Tag.d.ts +2 -24
  68. package/dist/components/Tag/Tag.d.ts.map +1 -1
  69. package/dist/components/Tag/styles.css.d.ts +8 -0
  70. package/dist/components/Tag/styles.css.d.ts.map +1 -1
  71. package/dist/components/Tag/types.d.ts +39 -0
  72. package/dist/components/Tag/types.d.ts.map +1 -0
  73. package/dist/components/Toast/ToastProvider.d.ts +2 -21
  74. package/dist/components/Toast/ToastProvider.d.ts.map +1 -1
  75. package/dist/components/Toast/native.css.d.ts +81 -0
  76. package/dist/components/Toast/native.css.d.ts.map +1 -0
  77. package/dist/components/Toast/store.d.ts +47 -0
  78. package/dist/components/Toast/store.d.ts.map +1 -0
  79. package/dist/components/Tooltip/TooltipHint.d.ts +9 -0
  80. package/dist/components/Tooltip/TooltipHint.d.ts.map +1 -0
  81. package/dist/components/Tree/Tree.d.ts.map +1 -1
  82. package/dist/components/Tree/styles.css.d.ts +6 -0
  83. package/dist/components/Tree/styles.css.d.ts.map +1 -1
  84. package/dist/components/Tree/useTreeKeyboard.d.ts +19 -0
  85. package/dist/components/Tree/useTreeKeyboard.d.ts.map +1 -0
  86. package/dist/components/VirtualList/VirtualList.d.ts +33 -0
  87. package/dist/components/VirtualList/VirtualList.d.ts.map +1 -0
  88. package/dist/components/VirtualList/VirtualList.meta.d.ts +3 -0
  89. package/dist/components/VirtualList/VirtualList.meta.d.ts.map +1 -0
  90. package/dist/components/VirtualList/VirtualList.stories.d.ts +15 -0
  91. package/dist/components/VirtualList/VirtualList.stories.d.ts.map +1 -0
  92. package/dist/components/VirtualList/styles.css.d.ts +28 -0
  93. package/dist/components/VirtualList/styles.css.d.ts.map +1 -0
  94. package/dist/index.css +1 -1
  95. package/dist/index.d.ts +3 -1
  96. package/dist/index.d.ts.map +1 -1
  97. package/dist/index.js +4935 -4260
  98. package/dist/index.js.map +1 -1
  99. package/dist/shared/ButtonHost.d.ts +11 -0
  100. package/dist/shared/ButtonHost.d.ts.map +1 -0
  101. package/dist/shared/pressableChild.d.ts +14 -0
  102. package/dist/shared/pressableChild.d.ts.map +1 -0
  103. package/dist/table.js +1 -1
  104. package/package.json +2 -2
  105. package/src/components/AppShell/AppShell.meta.ts +1 -1
  106. package/src/components/AppShell/AppShell.native.tsx +174 -0
  107. package/src/components/AppShell/native.css.ts +56 -0
  108. package/src/components/Button/Button.tsx +3 -3
  109. package/src/components/Drawer/Drawer.meta.ts +2 -1
  110. package/src/components/Drawer/Drawer.native.tsx +469 -0
  111. package/src/components/Drawer/Drawer.tsx +58 -145
  112. package/src/components/Drawer/DrawerScroll.stories.tsx +4 -54
  113. package/src/components/Drawer/DrawerSnap.stories.tsx +205 -0
  114. package/src/components/Drawer/native.css.ts +58 -0
  115. package/src/components/Drawer/parts.css.ts +125 -0
  116. package/src/components/Drawer/parts.tsx +110 -0
  117. package/src/components/Drawer/sheetPhysics.ts +103 -0
  118. package/src/components/Drawer/styles.css.ts +15 -74
  119. package/src/components/Drawer/touchInput.ts +81 -0
  120. package/src/components/Drawer/useDrawerRoot.ts +90 -0
  121. package/src/components/Drawer/useSheetHeights.ts +42 -0
  122. package/src/components/Drawer/useSwipeDismiss.ts +146 -23
  123. package/src/components/Menu/Menu.meta.ts +1 -1
  124. package/src/components/Menu/Menu.native.tsx +268 -0
  125. package/src/components/Menu/Menu.tsx +11 -2
  126. package/src/components/Menu/native.css.ts +18 -0
  127. package/src/components/Menu/useMenuOpen.ts +33 -0
  128. package/src/components/Menu/useMenuRoot.ts +7 -26
  129. package/src/components/Popover/Popover.meta.ts +1 -1
  130. package/src/components/Popover/Popover.native.tsx +205 -0
  131. package/src/components/Popover/native.css.ts +13 -0
  132. package/src/components/SideNav/Chevron.native.tsx +20 -0
  133. package/src/components/SideNav/Chevron.tsx +20 -0
  134. package/src/components/SideNav/SideNav.meta.ts +1 -1
  135. package/src/components/SideNav/SideNav.tsx +57 -41
  136. package/src/components/SideNav/styles.css.ts +8 -0
  137. package/src/components/SideNav/types.ts +6 -0
  138. package/src/components/SplitPane/SplitPane.meta.ts +40 -0
  139. package/src/components/SplitPane/SplitPane.native.tsx +47 -0
  140. package/src/components/SplitPane/SplitPane.stories.tsx +148 -0
  141. package/src/components/SplitPane/SplitPane.tsx +218 -0
  142. package/src/components/SplitPane/styles.css.ts +81 -0
  143. package/src/components/Tabs/TabLayout.native.tsx +9 -0
  144. package/src/components/Tabs/TabLayout.tsx +6 -0
  145. package/src/components/Tabs/Tabs.meta.ts +1 -1
  146. package/src/components/Tabs/Tabs.tsx +38 -136
  147. package/src/components/Tabs/indicator.ts +36 -0
  148. package/src/components/Tabs/styles.css.ts +7 -0
  149. package/src/components/Tabs/useTabIndicator.native.ts +35 -0
  150. package/src/components/Tabs/useTabIndicator.ts +49 -0
  151. package/src/components/Tabs/useTabKeys.native.ts +2 -0
  152. package/src/components/Tabs/useTabKeys.ts +99 -0
  153. package/src/components/Tag/RemoveGlyph.native.tsx +17 -0
  154. package/src/components/Tag/RemoveGlyph.tsx +10 -0
  155. package/src/components/Tag/Tag.meta.ts +1 -1
  156. package/src/components/Tag/Tag.native.tsx +70 -0
  157. package/src/components/Tag/Tag.tsx +9 -37
  158. package/src/components/Tag/styles.css.ts +9 -0
  159. package/src/components/Tag/types.ts +43 -0
  160. package/src/components/Toast/Toast.meta.ts +1 -1
  161. package/src/components/Toast/ToastProvider.native.tsx +226 -0
  162. package/src/components/Toast/ToastProvider.tsx +22 -84
  163. package/src/components/Toast/native.css.ts +78 -0
  164. package/src/components/Toast/store.ts +96 -0
  165. package/src/components/Toggle/Toggle.tsx +3 -3
  166. package/src/components/Tooltip/Tooltip.meta.ts +1 -1
  167. package/src/components/Tooltip/Tooltip.native.tsx +36 -0
  168. package/src/components/Tooltip/TooltipHint.ts +13 -0
  169. package/src/components/Tree/Tree.meta.ts +1 -1
  170. package/src/components/Tree/Tree.tsx +23 -135
  171. package/src/components/Tree/styles.css.ts +6 -0
  172. package/src/components/Tree/useTreeKeyboard.native.ts +5 -0
  173. package/src/components/Tree/useTreeKeyboard.ts +152 -0
  174. package/src/components/VirtualList/VirtualList.meta.ts +39 -0
  175. package/src/components/VirtualList/VirtualList.stories.tsx +95 -0
  176. package/src/components/VirtualList/VirtualList.tsx +103 -0
  177. package/src/components/VirtualList/styles.css.ts +31 -0
  178. package/src/hooks/useContainerBelow.native.ts +30 -0
  179. package/src/index.native.ts +33 -0
  180. package/src/index.ts +8 -1
  181. package/src/shared/ButtonHost.native.tsx +70 -0
  182. package/src/shared/ButtonHost.tsx +12 -0
  183. package/src/shared/portal/AnchoredOverlay.native.tsx +117 -0
  184. package/src/shared/pressableChild.ts +33 -0
  185. package/src/shared/useKeyboardHeight.native.ts +18 -0
  186. package/src/shared/useScreenReaderEnabled.native.ts +19 -0
  187. package/dist/Table-ColfpgKV.js +0 -1552
  188. package/dist/Table-ColfpgKV.js.map +0 -1
@@ -0,0 +1,36 @@
1
+ import {createContext, useContext} from 'react'
2
+ import type {Orientation} from '../../shared/types'
3
+
4
+ /** Where the sliding indicator sits along the list, in px. */
5
+ export interface IndicatorBox {
6
+ offset: number
7
+ size: number
8
+ }
9
+
10
+ /** A tab's box inside its list, as native `onLayout` reports it. */
11
+ export interface TabBox {
12
+ x: number
13
+ y: number
14
+ width: number
15
+ height: number
16
+ }
17
+
18
+ /** Native: each Tab reports its box here, so the List places the indicator. */
19
+ export const TabLayoutContext = createContext<((value: string, box: TabBox) => void) | null>(null)
20
+
21
+ export function useReportTabLayout() {
22
+ return useContext(TabLayoutContext)
23
+ }
24
+
25
+ export interface TabIndicatorInput {
26
+ listRef: React.RefObject<HTMLDivElement | null>
27
+ activeValue: string | null
28
+ orientation: Orientation
29
+ setIndicatorActive: (active: boolean) => void
30
+ }
31
+
32
+ export interface TabIndicator {
33
+ indicator: IndicatorBox | null
34
+ /** Native only: what the Tabs report their layout to. */
35
+ reportTabLayout: ((value: string, box: TabBox) => void) | null
36
+ }
@@ -32,6 +32,13 @@ export const styles = css.create({
32
32
  borderRightColor: colors.border,
33
33
  gap: 0,
34
34
  },
35
+ // RSD on native lays out only display: flex, not the web's inline-flex,
36
+ // and its Animated colour transition ends on a near-black colour: native
37
+ // tabs transition nothing that changes (colour and underline switch at once).
38
+ nativeFlex: {
39
+ display: 'flex',
40
+ transitionProperty: 'opacity',
41
+ },
35
42
  tab: {
36
43
  display: 'inline-flex',
37
44
  alignItems: 'center',
@@ -0,0 +1,35 @@
1
+ import {useCallback, useEffect, useState} from 'react'
2
+ import type {TabBox, TabIndicator, TabIndicatorInput} from './indicator'
3
+
4
+ /** Native: the indicator follows the active tab's `onLayout` box. */
5
+ export function useTabIndicator({
6
+ activeValue,
7
+ orientation,
8
+ setIndicatorActive,
9
+ }: TabIndicatorInput): TabIndicator {
10
+ const [boxes, setBoxes] = useState<ReadonlyMap<string, TabBox>>(() => new Map())
11
+ const reportTabLayout = useCallback((value: string, box: TabBox) => {
12
+ setBoxes((prev) => {
13
+ const old = prev.get(value)
14
+ if (
15
+ old &&
16
+ old.x === box.x &&
17
+ old.y === box.y &&
18
+ old.width === box.width &&
19
+ old.height === box.height
20
+ ) {
21
+ return prev
22
+ }
23
+ return new Map(prev).set(value, box)
24
+ })
25
+ }, [])
26
+ const box = activeValue ? boxes.get(activeValue) : undefined
27
+ const indicator = box
28
+ ? orientation === 'horizontal'
29
+ ? {offset: box.x, size: box.width}
30
+ : {offset: box.y, size: box.height}
31
+ : null
32
+ const active = indicator !== null
33
+ useEffect(() => setIndicatorActive(active), [active, setIndicatorActive])
34
+ return {indicator, reportTabLayout}
35
+ }
@@ -0,0 +1,49 @@
1
+ import {useLayoutEffect, useState} from 'react'
2
+ import type {IndicatorBox, TabIndicator, TabIndicatorInput} from './indicator'
3
+
4
+ /** Web: the active tab measured in the DOM, kept current by a ResizeObserver. */
5
+ export function useTabIndicator({
6
+ listRef,
7
+ activeValue,
8
+ orientation,
9
+ setIndicatorActive,
10
+ }: TabIndicatorInput): TabIndicator {
11
+ const [indicator, setIndicator] = useState<IndicatorBox | null>(null)
12
+
13
+ // Measure the active tab and drive the sliding indicator. useLayoutEffect so
14
+ // the first position is set before paint (the indicator element mounts already
15
+ // in place — no slide-in from zero; only later tab changes animate). Observes
16
+ // the active tab (its own size, e.g. a changing "(3)" count) and the list
17
+ // (container resize / reflow). A non-active sibling resizing while you're on
18
+ // another tab is the one case not caught; it self-corrects on the next
19
+ // selection or resize.
20
+ useLayoutEffect(() => {
21
+ const list = listRef.current
22
+ if (!list || !activeValue) {
23
+ setIndicator(null)
24
+ setIndicatorActive(false)
25
+ return
26
+ }
27
+ const el = list.querySelector<HTMLElement>(`[data-tab-value="${CSS.escape(activeValue)}"]`)
28
+ if (!el) {
29
+ setIndicator(null)
30
+ setIndicatorActive(false)
31
+ return
32
+ }
33
+ const measure = () => {
34
+ setIndicator(
35
+ orientation === 'horizontal'
36
+ ? {offset: el.offsetLeft, size: el.offsetWidth}
37
+ : {offset: el.offsetTop, size: el.offsetHeight},
38
+ )
39
+ setIndicatorActive(true)
40
+ }
41
+ measure()
42
+ const ro = new ResizeObserver(measure)
43
+ ro.observe(el)
44
+ ro.observe(list)
45
+ return () => ro.disconnect()
46
+ }, [listRef, activeValue, orientation, setIndicatorActive])
47
+
48
+ return {indicator, reportTabLayout: null}
49
+ }
@@ -0,0 +1,2 @@
1
+ /** Native has no arrow-key navigation (plan N0, Decision 6): each tab is its own stop. */
2
+ export function useTabKeys(_input: unknown) {}
@@ -0,0 +1,99 @@
1
+ import {useEffect} from 'react'
2
+ import type {Orientation} from '../../shared/types'
3
+
4
+ interface TabKeysInput {
5
+ listRef: React.RefObject<HTMLDivElement | null>
6
+ orientation: Orientation
7
+ activeValue: string | null
8
+ onSelect: (value: string) => void
9
+ tabsRef: React.RefObject<Map<string, boolean>>
10
+ orderRef: React.RefObject<string[]>
11
+ }
12
+
13
+ /** Web: arrow keys, Home and End move the selection and focus along the list. */
14
+ export function useTabKeys({
15
+ listRef,
16
+ orientation,
17
+ activeValue,
18
+ onSelect,
19
+ tabsRef,
20
+ orderRef,
21
+ }: TabKeysInput) {
22
+ useEffect(() => {
23
+ const el = listRef.current
24
+ if (!el) return
25
+ const listEl = el
26
+
27
+ function handleKeyDown(e: KeyboardEvent) {
28
+ const order = orderRef.current
29
+ const tabs = tabsRef.current
30
+ if (order.length === 0) return
31
+
32
+ const prevKey = orientation === 'horizontal' ? 'ArrowLeft' : 'ArrowUp'
33
+ const nextKey = orientation === 'horizontal' ? 'ArrowRight' : 'ArrowDown'
34
+
35
+ let targetValue: string | null = null
36
+
37
+ switch (e.key) {
38
+ case nextKey: {
39
+ e.preventDefault()
40
+ const currentIdx = activeValue ? order.indexOf(activeValue) : -1
41
+ for (let i = 1; i <= order.length; i++) {
42
+ const idx = (currentIdx + i) % order.length
43
+ const val = order[idx]
44
+ if (!tabs.get(val)) {
45
+ targetValue = val
46
+ break
47
+ }
48
+ }
49
+ break
50
+ }
51
+ case prevKey: {
52
+ e.preventDefault()
53
+ const currentIdx = activeValue ? order.indexOf(activeValue) : 0
54
+ for (let i = 1; i <= order.length; i++) {
55
+ const idx = (currentIdx - i + order.length) % order.length
56
+ const val = order[idx]
57
+ if (!tabs.get(val)) {
58
+ targetValue = val
59
+ break
60
+ }
61
+ }
62
+ break
63
+ }
64
+ case 'Home': {
65
+ e.preventDefault()
66
+ for (const val of order) {
67
+ if (!tabs.get(val)) {
68
+ targetValue = val
69
+ break
70
+ }
71
+ }
72
+ break
73
+ }
74
+ case 'End': {
75
+ e.preventDefault()
76
+ for (let i = order.length - 1; i >= 0; i--) {
77
+ if (!tabs.get(order[i])) {
78
+ targetValue = order[i]
79
+ break
80
+ }
81
+ }
82
+ break
83
+ }
84
+ }
85
+
86
+ if (targetValue) {
87
+ onSelect(targetValue)
88
+ // Focus the newly activated tab button
89
+ const tabEl = listEl.querySelector(
90
+ `[data-tab-value="${targetValue}"]`,
91
+ ) as HTMLElement | null
92
+ tabEl?.focus()
93
+ }
94
+ }
95
+
96
+ el.addEventListener('keydown', handleKeyDown)
97
+ return () => el.removeEventListener('keydown', handleKeyDown)
98
+ }, [listRef, orientation, activeValue, onSelect, tabsRef, orderRef])
99
+ }
@@ -0,0 +1,17 @@
1
+ import Svg, {Path} from 'react-native-svg'
2
+ import {useResolvedColors} from '../ThemeProvider/ThemeProvider'
3
+ import type {RemoveGlyphProps} from './types'
4
+
5
+ /**
6
+ * The same cross as the web's, drawn with react-native-svg (the native icon
7
+ * renderer, plan N0 Decision 9) in the tag's resolved text colour: a raw
8
+ * host has no CSS `currentColor` to inherit.
9
+ */
10
+ export function RemoveGlyph({size, color}: RemoveGlyphProps) {
11
+ const stroke = useResolvedColors()[color]
12
+ return (
13
+ <Svg width={size} height={size} viewBox="0 0 12 12" fill="none" aria-hidden>
14
+ <Path d="M3 3l6 6M9 3l-6 6" stroke={stroke} strokeWidth={1.5} strokeLinecap="round" />
15
+ </Svg>
16
+ )
17
+ }
@@ -0,0 +1,10 @@
1
+ import type {RemoveGlyphProps} from './types'
2
+
3
+ /** The tag's remove cross: 1.5 strokes on a 12 box, drawn in the tag's text colour. */
4
+ export function RemoveGlyph({size}: RemoveGlyphProps) {
5
+ return (
6
+ <svg width={size} height={size} viewBox="0 0 12 12" fill="none">
7
+ <path d="M3 3l6 6M9 3l-6 6" stroke="currentColor" strokeWidth={1.5} strokeLinecap="round" />
8
+ </svg>
9
+ )
10
+ }
@@ -2,7 +2,7 @@ import type {ComponentMeta} from '../component-meta'
2
2
 
3
3
  export const meta: ComponentMeta = {
4
4
  description:
5
- 'Interactive tag/chip with optional remove button. Works standalone or inside TagGroup for collection management. Visually similar to Badge but supports interaction.',
5
+ 'Interactive tag/chip with optional remove button. Works standalone or inside TagGroup for collection management. Visually similar to Badge but supports interaction. On React Native a Tag stands alone (TagGroup is web only): the label and, when removable, a remove button named from a string label.',
6
6
  whenToUse: [
7
7
  'Removable items in a list (emails, categories)',
8
8
  'Inside TagGroup for editable tag collections',
@@ -0,0 +1,70 @@
1
+ import {html} from 'react-strict-dom'
2
+ import {SIZES_PX} from '@duro-app/tokens/keys'
3
+ import {styles} from './styles.css'
4
+ import {RemoveGlyph} from './RemoveGlyph'
5
+ import {variantText, type TagProps} from './types'
6
+
7
+ export type {TagProps, TagSize, TagVariant} from './types'
8
+
9
+ const sizeMap = {
10
+ sm: styles.sizeSm,
11
+ md: styles.sizeMd,
12
+ } as const
13
+
14
+ const removableSizeMap = {
15
+ sm: styles.removableSm,
16
+ md: styles.removableMd,
17
+ } as const
18
+
19
+ /**
20
+ * Native Tag (plan N0, Slice B): the label and, when removable, the remove
21
+ * button. TagGroup is not ported (its grid keyboard model is web only,
22
+ * Decision 6), so `value` is unused here; the remove button's name comes from
23
+ * a string label.
24
+ */
25
+ export function Tag({
26
+ variant = 'default',
27
+ size = 'md',
28
+ removable = false,
29
+ onRemove,
30
+ disabled = false,
31
+ wrap = false,
32
+ children,
33
+ }: TagProps) {
34
+ const removeLabel = typeof children === 'string' ? `Remove ${children}` : 'Remove'
35
+ // A div, not the web's span: a native span is a Text, which would lay the
36
+ // remove button out inline, raised off the label's centre line.
37
+ return (
38
+ <html.div
39
+ style={[
40
+ styles.base,
41
+ styles.nativeFlex,
42
+ sizeMap[size],
43
+ removable && removableSizeMap[size],
44
+ styles[variant],
45
+ disabled && styles.disabled,
46
+ wrap && styles.wrap,
47
+ ]}
48
+ >
49
+ <html.span>{children}</html.span>
50
+ {removable && (
51
+ <html.button
52
+ type="button"
53
+ aria-label={removeLabel}
54
+ disabled={disabled}
55
+ onClick={disabled ? undefined : onRemove}
56
+ style={[
57
+ styles.removeButton,
58
+ styles.nativeButtonFlex,
59
+ disabled && styles.removeButtonDisabled,
60
+ ]}
61
+ >
62
+ <RemoveGlyph
63
+ size={size === 'sm' ? SIZES_PX.glyphXs : SIZES_PX.glyphSm}
64
+ color={variantText[variant]}
65
+ />
66
+ </html.button>
67
+ )}
68
+ </html.div>
69
+ )
70
+ }
@@ -1,31 +1,12 @@
1
- import {type ReactNode, useCallback, useRef} from 'react'
1
+ import {useCallback, useRef} from 'react'
2
2
  import {html} from 'react-strict-dom'
3
+ import {SIZES_PX} from '@duro-app/tokens/keys'
3
4
  import {useTagGroup} from '../TagGroup/TagGroupContext'
4
5
  import {styles} from './styles.css'
6
+ import {RemoveGlyph} from './RemoveGlyph'
7
+ import {variantText, type TagProps} from './types'
5
8
 
6
- export type TagVariant = 'default' | 'success' | 'warning' | 'error' | 'info'
7
- export type TagSize = 'sm' | 'md'
8
-
9
- interface TagProps {
10
- /** Value identifier — required when used inside TagGroup */
11
- value?: string
12
- variant?: TagVariant
13
- size?: TagSize
14
- /** Show remove button. Auto-derived from TagGroup context when inside one. */
15
- removable?: boolean
16
- /** Called when remove button is clicked. Auto-wired to TagGroup when inside one. */
17
- onRemove?: () => void
18
- disabled?: boolean
19
- /**
20
- * Allow the label to wrap / break instead of staying on one line. Tags are
21
- * `white-space: nowrap` by default (they read as pills); set this for long
22
- * identifier-style values (e.g. `lldap.group.member.remove`) sitting in a
23
- * constrained column, so they break within their cell instead of leaking
24
- * into the next one.
25
- */
26
- wrap?: boolean
27
- children: ReactNode
28
- }
9
+ export type {TagProps, TagSize, TagVariant} from './types'
29
10
 
30
11
  const sizeMap = {
31
12
  sm: styles.sizeSm,
@@ -112,19 +93,10 @@ export function Tag({
112
93
  tabIndex={-1}
113
94
  style={[styles.removeButton, disabled && styles.removeButtonDisabled]}
114
95
  >
115
- <svg
116
- width={size === 'sm' ? 10 : 12}
117
- height={size === 'sm' ? 10 : 12}
118
- viewBox="0 0 12 12"
119
- fill="none"
120
- >
121
- <path
122
- d="M3 3l6 6M9 3l-6 6"
123
- stroke="currentColor"
124
- strokeWidth={1.5}
125
- strokeLinecap="round"
126
- />
127
- </svg>
96
+ <RemoveGlyph
97
+ size={size === 'sm' ? SIZES_PX.glyphXs : SIZES_PX.glyphSm}
98
+ color={variantText[variant]}
99
+ />
128
100
  </html.button>
129
101
  </html.span>
130
102
  )}
@@ -110,6 +110,15 @@ export const styles = css.create({
110
110
  },
111
111
  outlineWidth: 0,
112
112
  },
113
+ // RSD on native lays out only display: flex, not the web's inline-flex.
114
+ nativeFlex: {
115
+ display: 'flex',
116
+ flexDirection: 'row',
117
+ alignSelf: 'flex-start',
118
+ },
119
+ nativeButtonFlex: {
120
+ display: 'flex',
121
+ },
113
122
  removeButtonDisabled: {
114
123
  cursor: 'not-allowed',
115
124
  opacity: 0.4,
@@ -0,0 +1,43 @@
1
+ import type {ReactNode} from 'react'
2
+ import type {ColorToken} from '@duro-app/tokens/keys'
3
+
4
+ export type TagVariant = 'default' | 'success' | 'warning' | 'error' | 'info'
5
+
6
+ /** Each variant's text colour, which the remove cross draws in. */
7
+ export const variantText = {
8
+ default: 'text',
9
+ success: 'successText',
10
+ warning: 'warningText',
11
+ error: 'errorText',
12
+ info: 'infoText',
13
+ } as const satisfies Record<TagVariant, ColorToken>
14
+
15
+ export type TagSize = 'sm' | 'md'
16
+
17
+ export interface TagProps {
18
+ /** Value identifier — required when used inside TagGroup */
19
+ value?: string
20
+ variant?: TagVariant
21
+ size?: TagSize
22
+ /** Show remove button. Auto-derived from TagGroup context when inside one. */
23
+ removable?: boolean
24
+ /** Called when remove button is clicked. Auto-wired to TagGroup when inside one. */
25
+ onRemove?: () => void
26
+ disabled?: boolean
27
+ /**
28
+ * Allow the label to wrap / break instead of staying on one line. Tags are
29
+ * `white-space: nowrap` by default (they read as pills); set this for long
30
+ * identifier-style values (e.g. `lldap.group.member.remove`) sitting in a
31
+ * constrained column, so they break within their cell instead of leaking
32
+ * into the next one.
33
+ */
34
+ wrap?: boolean
35
+ children: ReactNode
36
+ }
37
+
38
+ export interface RemoveGlyphProps {
39
+ /** px, from the glyph size tokens */
40
+ size: number
41
+ /** Native only: the web inherits the tag's text colour. */
42
+ color: ColorToken
43
+ }
@@ -2,7 +2,7 @@ import type {ComponentMeta} from '../component-meta'
2
2
 
3
3
  export const meta: ComponentMeta = {
4
4
  description:
5
- 'Transient, self-dismissing notification for the RESULT of an action (saved, sent, revoked, failed). Mount <ToastProvider> once at the app root and call useToast().toast() from anywhere. Toasts stack and portal above dialogs. Variants: success, info, warning, error. The countdown pauses while a toast is hovered or holds focus, and every toast has a close button (`dismissLabel` on the provider names it).',
5
+ 'Transient, self-dismissing notification for the RESULT of an action (saved, sent, revoked, failed). Mount <ToastProvider> once at the app root and call useToast().toast() from anywhere. Toasts stack and portal above dialogs. Variants: success, info, warning, error. The countdown pauses while a toast is hovered or holds focus, and every toast has a close button (`dismissLabel` on the provider names it). On React Native the same API draws through the portal host above every open overlay and the keyboard, as a card with a status-coloured edge, its action and its close in one row; a sideways swipe dismisses, a press holds the countdown, and so does an active screen reader (WCAG 2.2.1).',
6
6
  whenToUse: [
7
7
  'Confirming an action succeeded (e.g. "Grant revoked")',
8
8
  'Reporting an action failed, with the reason',