@proteos/ui 0.31.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 (226) hide show
  1. package/LICENSE +105 -0
  2. package/dist/chunk-IKAX76MB.js +113 -0
  3. package/dist/chunk-IKAX76MB.js.map +1 -0
  4. package/dist/chunk-MWAXK66H.cjs +119 -0
  5. package/dist/chunk-MWAXK66H.cjs.map +1 -0
  6. package/dist/index.cjs +6246 -0
  7. package/dist/index.cjs.map +1 -0
  8. package/dist/index.d.cts +2126 -0
  9. package/dist/index.d.ts +2126 -0
  10. package/dist/index.js +6069 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/pdf-view-3ZMMS6ML.cjs +95 -0
  13. package/dist/pdf-view-3ZMMS6ML.cjs.map +1 -0
  14. package/dist/pdf-view-BV5ZD6OC.js +93 -0
  15. package/dist/pdf-view-BV5ZD6OC.js.map +1 -0
  16. package/package.json +96 -0
  17. package/src/components/attachment-chip/attachment-chip.tsx +129 -0
  18. package/src/components/attachment-chip/index.ts +2 -0
  19. package/src/components/attribute-type-icon/attribute-type-icon.stories.tsx +70 -0
  20. package/src/components/attribute-type-icon/attribute-type-icon.tsx +125 -0
  21. package/src/components/attribute-type-icon/index.ts +8 -0
  22. package/src/components/avatar/avatar.stories.tsx +42 -0
  23. package/src/components/avatar/avatar.tsx +48 -0
  24. package/src/components/avatar/index.ts +2 -0
  25. package/src/components/avatar-group/avatar-group.test.tsx +38 -0
  26. package/src/components/avatar-group/avatar-group.tsx +60 -0
  27. package/src/components/avatar-group/index.ts +2 -0
  28. package/src/components/badge/badge.stories.tsx +62 -0
  29. package/src/components/badge/badge.test.tsx +36 -0
  30. package/src/components/badge/badge.tsx +54 -0
  31. package/src/components/badge/index.ts +2 -0
  32. package/src/components/badge-multi-select/badge-multi-select.stories.tsx +177 -0
  33. package/src/components/badge-multi-select/badge-multi-select.test.tsx +155 -0
  34. package/src/components/badge-multi-select/badge-multi-select.tsx +272 -0
  35. package/src/components/badge-multi-select/index.ts +2 -0
  36. package/src/components/breadcrumbs/breadcrumbs.stories.tsx +43 -0
  37. package/src/components/breadcrumbs/breadcrumbs.tsx +86 -0
  38. package/src/components/breadcrumbs/index.ts +2 -0
  39. package/src/components/button/button.stories.tsx +290 -0
  40. package/src/components/button/button.test.tsx +59 -0
  41. package/src/components/button/button.tsx +86 -0
  42. package/src/components/button/index.ts +2 -0
  43. package/src/components/card/card.stories.tsx +351 -0
  44. package/src/components/card/card.test.tsx +141 -0
  45. package/src/components/card/card.tsx +173 -0
  46. package/src/components/card/index.ts +13 -0
  47. package/src/components/checkbox/checkbox-group.tsx +52 -0
  48. package/src/components/checkbox/checkbox.stories.tsx +48 -0
  49. package/src/components/checkbox/checkbox.test.tsx +50 -0
  50. package/src/components/checkbox/checkbox.tsx +71 -0
  51. package/src/components/checkbox/index.ts +4 -0
  52. package/src/components/combobox/combobox.stories.tsx +97 -0
  53. package/src/components/combobox/combobox.test.tsx +123 -0
  54. package/src/components/combobox/combobox.tsx +239 -0
  55. package/src/components/combobox/index.ts +2 -0
  56. package/src/components/command/command-hint.tsx +46 -0
  57. package/src/components/command/command.stories.tsx +88 -0
  58. package/src/components/command/command.test.tsx +102 -0
  59. package/src/components/command/command.tsx +76 -0
  60. package/src/components/command/index.ts +16 -0
  61. package/src/components/currency-input/currency-input.tsx +192 -0
  62. package/src/components/currency-input/index.ts +6 -0
  63. package/src/components/data-table/cells.stories.tsx +120 -0
  64. package/src/components/data-table/cells.tsx +165 -0
  65. package/src/components/data-table/data-table.stories.tsx +371 -0
  66. package/src/components/data-table/data-table.tsx +556 -0
  67. package/src/components/data-table/index.ts +22 -0
  68. package/src/components/data-table/make-actions-column.ts +30 -0
  69. package/src/components/data-table/states/data-table-empty.tsx +40 -0
  70. package/src/components/data-table/states/data-table-error.tsx +30 -0
  71. package/src/components/data-table/states/data-table-skeleton.tsx +42 -0
  72. package/src/components/data-table/use-infinite-scroll-sentinel.ts +51 -0
  73. package/src/components/date-picker/calendar-internals.tsx +138 -0
  74. package/src/components/date-picker/date-picker.stories.tsx +78 -0
  75. package/src/components/date-picker/date-picker.test.tsx +71 -0
  76. package/src/components/date-picker/date-picker.tsx +140 -0
  77. package/src/components/date-picker/index.ts +2 -0
  78. package/src/components/date-time-picker/date-time-picker.stories.tsx +96 -0
  79. package/src/components/date-time-picker/date-time-picker.test.tsx +87 -0
  80. package/src/components/date-time-picker/date-time-picker.tsx +203 -0
  81. package/src/components/date-time-picker/index.ts +2 -0
  82. package/src/components/empty-state/empty-state.tsx +63 -0
  83. package/src/components/empty-state/index.ts +2 -0
  84. package/src/components/error-animation/error-animation.stories.tsx +50 -0
  85. package/src/components/error-animation/error-animation.test.tsx +22 -0
  86. package/src/components/error-animation/error-animation.tsx +26 -0
  87. package/src/components/error-animation/index.ts +2 -0
  88. package/src/components/field/field.tsx +142 -0
  89. package/src/components/field/index.ts +8 -0
  90. package/src/components/file-field/file-field.tsx +285 -0
  91. package/src/components/file-field/index.ts +2 -0
  92. package/src/components/file-viewer/fallback-card.tsx +68 -0
  93. package/src/components/file-viewer/file-viewer.stories.tsx +67 -0
  94. package/src/components/file-viewer/file-viewer.test.tsx +81 -0
  95. package/src/components/file-viewer/file-viewer.tsx +116 -0
  96. package/src/components/file-viewer/image-view.tsx +38 -0
  97. package/src/components/file-viewer/index.ts +7 -0
  98. package/src/components/file-viewer/pdf-view.tsx +122 -0
  99. package/src/components/file-viewer/video-view.tsx +16 -0
  100. package/src/components/image-attachment/image-attachment.stories.tsx +39 -0
  101. package/src/components/image-attachment/image-attachment.test.tsx +54 -0
  102. package/src/components/image-attachment/image-attachment.tsx +77 -0
  103. package/src/components/image-attachment/index.ts +3 -0
  104. package/src/components/image-attachment/is-previewable-image-name.ts +14 -0
  105. package/src/components/image-lightbox/image-lightbox.stories.tsx +38 -0
  106. package/src/components/image-lightbox/image-lightbox.test.tsx +72 -0
  107. package/src/components/image-lightbox/image-lightbox.tsx +92 -0
  108. package/src/components/image-lightbox/index.ts +2 -0
  109. package/src/components/input/index.ts +2 -0
  110. package/src/components/input/input.stories.tsx +71 -0
  111. package/src/components/input/input.test.tsx +36 -0
  112. package/src/components/input/input.tsx +76 -0
  113. package/src/components/kbd/index.ts +2 -0
  114. package/src/components/kbd/kbd.stories.tsx +34 -0
  115. package/src/components/kbd/kbd.tsx +27 -0
  116. package/src/components/menu/index.ts +2 -0
  117. package/src/components/menu/menu.tsx +121 -0
  118. package/src/components/modal/index.ts +10 -0
  119. package/src/components/modal/modal.stories.tsx +153 -0
  120. package/src/components/modal/modal.test.tsx +93 -0
  121. package/src/components/modal/modal.tsx +149 -0
  122. package/src/components/multi-select/index.ts +2 -0
  123. package/src/components/multi-select/multi-select.stories.tsx +211 -0
  124. package/src/components/multi-select/multi-select.test.tsx +151 -0
  125. package/src/components/multi-select/multi-select.tsx +339 -0
  126. package/src/components/nav/index.ts +7 -0
  127. package/src/components/nav/nav.stories.tsx +139 -0
  128. package/src/components/nav/nav.tsx +246 -0
  129. package/src/components/popover/index.ts +8 -0
  130. package/src/components/popover/popover.stories.tsx +92 -0
  131. package/src/components/popover/popover.tsx +135 -0
  132. package/src/components/progress-bar/index.ts +2 -0
  133. package/src/components/progress-bar/progress-bar.stories.tsx +75 -0
  134. package/src/components/progress-bar/progress-bar.test.tsx +29 -0
  135. package/src/components/progress-bar/progress-bar.tsx +57 -0
  136. package/src/components/radio-group/index.ts +2 -0
  137. package/src/components/radio-group/radio-group.stories.tsx +113 -0
  138. package/src/components/radio-group/radio-group.test.tsx +87 -0
  139. package/src/components/radio-group/radio-group.tsx +104 -0
  140. package/src/components/record-filter/attribute-picker.tsx +74 -0
  141. package/src/components/record-filter/chip-value.tsx +171 -0
  142. package/src/components/record-filter/filter-row.tsx +262 -0
  143. package/src/components/record-filter/filter-value-input.tsx +351 -0
  144. package/src/components/record-filter/group-add-controls.tsx +77 -0
  145. package/src/components/record-filter/group-body.tsx +173 -0
  146. package/src/components/record-filter/group-mutations.test.ts +104 -0
  147. package/src/components/record-filter/group-mutations.ts +116 -0
  148. package/src/components/record-filter/index.ts +58 -0
  149. package/src/components/record-filter/nested-group.tsx +138 -0
  150. package/src/components/record-filter/operator-picker.tsx +62 -0
  151. package/src/components/record-filter/operators.test.ts +105 -0
  152. package/src/components/record-filter/operators.ts +97 -0
  153. package/src/components/record-filter/record-filter-builder-panel.tsx +236 -0
  154. package/src/components/record-filter/record-filter-button.tsx +126 -0
  155. package/src/components/record-filter/record-filter-chips.tsx +527 -0
  156. package/src/components/record-filter/record-filter-editor.tsx +91 -0
  157. package/src/components/record-filter/record-filter.stories.tsx +196 -0
  158. package/src/components/record-filter/record-filter.test.tsx +437 -0
  159. package/src/components/record-filter/show-details-store.ts +72 -0
  160. package/src/components/record-filter/types.ts +150 -0
  161. package/src/components/segmented-control/index.ts +6 -0
  162. package/src/components/segmented-control/segmented-control.stories.tsx +67 -0
  163. package/src/components/segmented-control/segmented-control.test.tsx +47 -0
  164. package/src/components/segmented-control/segmented-control.tsx +105 -0
  165. package/src/components/select/index.ts +2 -0
  166. package/src/components/select/select.stories.tsx +76 -0
  167. package/src/components/select/select.test.tsx +56 -0
  168. package/src/components/select/select.tsx +159 -0
  169. package/src/components/separator/index.ts +2 -0
  170. package/src/components/separator/separator.stories.tsx +73 -0
  171. package/src/components/separator/separator.test.tsx +29 -0
  172. package/src/components/separator/separator.tsx +51 -0
  173. package/src/components/skeleton/index.ts +2 -0
  174. package/src/components/skeleton/skeleton.stories.tsx +68 -0
  175. package/src/components/skeleton/skeleton.test.tsx +38 -0
  176. package/src/components/skeleton/skeleton.tsx +43 -0
  177. package/src/components/success-animation/index.ts +2 -0
  178. package/src/components/success-animation/success-animation.stories.tsx +50 -0
  179. package/src/components/success-animation/success-animation.test.tsx +22 -0
  180. package/src/components/success-animation/success-animation.tsx +26 -0
  181. package/src/components/switch/index.ts +2 -0
  182. package/src/components/switch/switch.stories.tsx +60 -0
  183. package/src/components/switch/switch.test.tsx +50 -0
  184. package/src/components/switch/switch.tsx +49 -0
  185. package/src/components/table/index.ts +25 -0
  186. package/src/components/table/table.stories.tsx +306 -0
  187. package/src/components/table/table.test.tsx +115 -0
  188. package/src/components/table/table.tsx +543 -0
  189. package/src/components/tabs/index.ts +8 -0
  190. package/src/components/tabs/tabs.tsx +141 -0
  191. package/src/components/tag-input/index.ts +2 -0
  192. package/src/components/tag-input/tag-input.stories.tsx +73 -0
  193. package/src/components/tag-input/tag-input.test.tsx +80 -0
  194. package/src/components/tag-input/tag-input.tsx +193 -0
  195. package/src/components/textarea/index.ts +2 -0
  196. package/src/components/textarea/textarea.stories.tsx +75 -0
  197. package/src/components/textarea/textarea.test.tsx +41 -0
  198. package/src/components/textarea/textarea.tsx +86 -0
  199. package/src/components/time-field/index.ts +2 -0
  200. package/src/components/time-field/time-field.stories.tsx +76 -0
  201. package/src/components/time-field/time-field.test.tsx +51 -0
  202. package/src/components/time-field/time-field.tsx +98 -0
  203. package/src/components/tooltip/index.ts +2 -0
  204. package/src/components/tooltip/tooltip.stories.tsx +176 -0
  205. package/src/components/tooltip/tooltip.test.tsx +52 -0
  206. package/src/components/tooltip/tooltip.tsx +56 -0
  207. package/src/index.ts +47 -0
  208. package/src/lib/cn.ts +6 -0
  209. package/src/lib/commands/discovery-store.test.tsx +76 -0
  210. package/src/lib/commands/discovery-store.ts +110 -0
  211. package/src/lib/commands/index.ts +9 -0
  212. package/src/lib/commands/match-event.test.ts +99 -0
  213. package/src/lib/commands/match-event.ts +31 -0
  214. package/src/lib/commands/parse-combo.test.ts +65 -0
  215. package/src/lib/commands/parse-combo.ts +48 -0
  216. package/src/lib/commands/platform.ts +27 -0
  217. package/src/lib/commands/protected-combos.test.ts +18 -0
  218. package/src/lib/commands/protected-combos.ts +38 -0
  219. package/src/lib/commands/registry.test.ts +97 -0
  220. package/src/lib/commands/registry.ts +59 -0
  221. package/src/lib/commands/use-command.test.tsx +91 -0
  222. package/src/lib/commands/use-command.ts +32 -0
  223. package/src/lib/commands/use-escape-key.test.tsx +97 -0
  224. package/src/lib/commands/use-escape-key.ts +62 -0
  225. package/src/styles.css +321 -0
  226. package/src/test/setup.ts +9 -0
@@ -0,0 +1,543 @@
1
+ import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine'
2
+ import { draggable, dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'
3
+ import {
4
+ attachClosestEdge,
5
+ type Edge,
6
+ extractClosestEdge,
7
+ } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge'
8
+ import { GripVertical, X } from 'lucide-react'
9
+ import {
10
+ type CSSProperties,
11
+ type HTMLAttributes,
12
+ type MouseEvent,
13
+ type Ref,
14
+ useEffect,
15
+ useRef,
16
+ useState,
17
+ } from 'react'
18
+ import { tv, type VariantProps } from 'tailwind-variants'
19
+
20
+ import { cn } from '../../lib/cn'
21
+
22
+ /* =========================================================================
23
+ Table — pure presentational chrome.
24
+ Div + ARIA grid roles (not semantic <table>) so virtualizers can position
25
+ rows absolutely. The slot pattern mirrors Card: each slot owns its own
26
+ spacing; the root carries no padding. The same primitives compose for
27
+ short non-virtualized tables and for the virtualized DataTable layer.
28
+ ========================================================================= */
29
+
30
+ const tableStyles = tv({
31
+ base: ['flex flex-col overflow-hidden min-h-0', 'text-[14px] text-ink-2'],
32
+ variants: {
33
+ variant: {
34
+ // Tables sit on the page bg, not the raised surface color — rows read
35
+ // as part of the page, framed by the panel border (Tonio's call, 2026-07).
36
+ panel: ['bg-bg border border-rule rounded-lg', 'shadow-[var(--shadow-rest)]'],
37
+ bare: '',
38
+ },
39
+ },
40
+ defaultVariants: { variant: 'panel' },
41
+ })
42
+
43
+ export type TableVariants = VariantProps<typeof tableStyles>
44
+ export type TableProps = HTMLAttributes<HTMLDivElement> &
45
+ TableVariants & {
46
+ 'aria-label'?: string
47
+ }
48
+
49
+ export function Table({ variant, className, ...props }: TableProps) {
50
+ return <div role="grid" {...props} className={cn(tableStyles({ variant }), className)} />
51
+ }
52
+
53
+ /* ---------- Header row ---------------------------------------------------- */
54
+
55
+ const tableHeaderStyles = tv({
56
+ base: [
57
+ 'flex items-stretch shrink-0',
58
+ 'bg-[color-mix(in_oklab,var(--color-surface-2)_60%,var(--color-surface))]',
59
+ 'border-b border-rule',
60
+ ],
61
+ variants: {
62
+ sticky: { true: 'sticky top-0 z-[2]', false: '' },
63
+ },
64
+ defaultVariants: { sticky: false },
65
+ })
66
+
67
+ export type TableHeaderProps = HTMLAttributes<HTMLDivElement> & {
68
+ sticky?: boolean
69
+ }
70
+
71
+ export function TableHeader({ sticky, className, ...props }: TableHeaderProps) {
72
+ return <div role="row" {...props} className={cn(tableHeaderStyles({ sticky }), className)} />
73
+ }
74
+
75
+ /* ---------- Header cell --------------------------------------------------- */
76
+
77
+ export const tableHeaderCellStyles = tv({
78
+ base: [
79
+ // `relative` is the default positioning context (for the drag-edge
80
+ // indicator + sort affordance). Declared as a class — not an inline
81
+ // style — so the `sticky` variant below can override it; an inline
82
+ // `position` would always win and a sticky gutter header could never
83
+ // freeze.
84
+ 'relative',
85
+ 'flex items-center gap-[7px] shrink-0 select-none',
86
+ 'px-[14px] h-[36px]',
87
+ 'border-r border-rule-2 last:border-r-0',
88
+ 'font-mono text-[10.5px] font-normal uppercase tracking-[0.08em]',
89
+ 'text-ink-4',
90
+ 'transition-[background-color,color] duration-150',
91
+ ],
92
+ variants: {
93
+ align: {
94
+ left: 'justify-start text-left',
95
+ right: 'justify-end text-right',
96
+ center: 'justify-center text-center',
97
+ },
98
+ interactive: {
99
+ true: [
100
+ 'cursor-pointer outline-none appearance-none border-0 border-r border-rule-2 last:border-r-0 bg-transparent',
101
+ 'hover:bg-surface-2 hover:text-ink-2',
102
+ 'focus-visible:shadow-[var(--shadow-focus)]',
103
+ ],
104
+ false: '',
105
+ },
106
+ active: {
107
+ true: 'text-ink',
108
+ false: '',
109
+ },
110
+ sticky: {
111
+ left: 'sticky left-0 z-[3] bg-[color-mix(in_oklab,var(--color-surface-2)_60%,var(--color-surface))]',
112
+ right: 'sticky right-0 z-[3] bg-[color-mix(in_oklab,var(--color-surface-2)_60%,var(--color-surface))]',
113
+ false: '',
114
+ },
115
+ },
116
+ defaultVariants: { align: 'left', interactive: false, active: false, sticky: false },
117
+ })
118
+
119
+ export type SortDirection = 'asc' | 'desc' | null
120
+
121
+ /** Discriminator stored on every column drag payload. */
122
+ export const COLUMN_DRAG_KIND = 'proteos:table:column' as const
123
+
124
+ export type ColumnDragData = {
125
+ kind: typeof COLUMN_DRAG_KIND
126
+ columnId: string
127
+ }
128
+
129
+ export function isColumnDragData(
130
+ data: Record<string | symbol, unknown>,
131
+ ): data is ColumnDragData & Record<string | symbol, unknown> {
132
+ return data.kind === COLUMN_DRAG_KIND && typeof data.columnId === 'string'
133
+ }
134
+
135
+ type DragState =
136
+ | { kind: 'idle' }
137
+ | { kind: 'dragging' }
138
+ | { kind: 'over'; edge: Extract<Edge, 'left' | 'right'> }
139
+
140
+ export type TableHeaderCellProps = Omit<HTMLAttributes<HTMLDivElement>, 'onClick'> & {
141
+ width?: number | string
142
+ align?: 'left' | 'right' | 'center'
143
+ /** Pin the cell to the left or right edge during horizontal scroll. */
144
+ sticky?: 'left' | 'right' | false
145
+ /**
146
+ * When non-null, renders a sort affordance and treats clicks on the cell
147
+ * body as a sort toggle. `null` (the default) is "not sortable".
148
+ */
149
+ sortDirection?: SortDirection
150
+ onSortToggle?: () => void
151
+ /**
152
+ * Make the header draggable for column reorder. Requires `columnId`.
153
+ * The cell registers pragmatic-dnd `draggable` + `dropTargetForElements`
154
+ * with left/right edge detection; the actual reorder runs in a global
155
+ * `monitorForElements` (e.g. inside DataTable) by reading the published
156
+ * `ColumnDragData`.
157
+ */
158
+ draggable?: boolean
159
+ columnId?: string
160
+ /**
161
+ * When provided, renders a trailing `×` button. Click events
162
+ * `stopPropagation` so removal never starts a drag or toggles sort.
163
+ */
164
+ onRemove?: () => void
165
+ /** Aria label fragment used in the remove button label and any drag a11y. */
166
+ ariaLabel?: string
167
+ }
168
+
169
+ export function TableHeaderCell({
170
+ width,
171
+ align = 'left',
172
+ className,
173
+ style,
174
+ children,
175
+ sortDirection = null,
176
+ onSortToggle,
177
+ draggable: isDraggable = false,
178
+ columnId,
179
+ onRemove,
180
+ ariaLabel,
181
+ sticky = false,
182
+ ...rest
183
+ }: TableHeaderCellProps) {
184
+ const cellRef = useRef<HTMLDivElement | null>(null)
185
+ const handleRef = useRef<HTMLSpanElement | null>(null)
186
+ const [dragState, setDragState] = useState<DragState>({ kind: 'idle' })
187
+
188
+ useEffect(() => {
189
+ if (!isDraggable || !columnId) return
190
+ const cell = cellRef.current
191
+ const handle = handleRef.current
192
+ if (!cell || !handle) return
193
+
194
+ const data: ColumnDragData = { kind: COLUMN_DRAG_KIND, columnId }
195
+
196
+ return combine(
197
+ draggable({
198
+ element: cell,
199
+ dragHandle: handle,
200
+ getInitialData: () => ({ ...data }),
201
+ onDragStart: () => setDragState({ kind: 'dragging' }),
202
+ onDrop: () => setDragState({ kind: 'idle' }),
203
+ }),
204
+ dropTargetForElements({
205
+ element: cell,
206
+ canDrop: ({ source }) =>
207
+ isColumnDragData(source.data) && source.data.columnId !== columnId,
208
+ getData: ({ input, element }) =>
209
+ attachClosestEdge(
210
+ { ...data },
211
+ { input, element, allowedEdges: ['left', 'right'] },
212
+ ),
213
+ getIsSticky: () => true,
214
+ onDragEnter: ({ self }) => {
215
+ const edge = extractClosestEdge(self.data)
216
+ if (edge === 'left' || edge === 'right') setDragState({ kind: 'over', edge })
217
+ },
218
+ onDrag: ({ self }) => {
219
+ const edge = extractClosestEdge(self.data)
220
+ if (edge === 'left' || edge === 'right') {
221
+ setDragState((prev) =>
222
+ prev.kind === 'over' && prev.edge === edge ? prev : { kind: 'over', edge },
223
+ )
224
+ }
225
+ },
226
+ onDragLeave: () => setDragState({ kind: 'idle' }),
227
+ onDrop: () => setDragState({ kind: 'idle' }),
228
+ }),
229
+ )
230
+ }, [isDraggable, columnId])
231
+
232
+ const widthStyle: CSSProperties = width != null ? { width, flexBasis: width } : {}
233
+ const isSortable = !!onSortToggle
234
+ const active = sortDirection !== null
235
+
236
+ const handleClick = (e: MouseEvent<HTMLDivElement>) => {
237
+ if (!isSortable) return
238
+ const target = e.target as HTMLElement
239
+ if (target.closest('[data-dnd-handle="true"]')) return
240
+ if (target.closest('[data-column-remove="true"]')) return
241
+ onSortToggle?.()
242
+ }
243
+
244
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
245
+ if (!isSortable) return
246
+ if (e.key === 'Enter' || e.key === ' ') {
247
+ e.preventDefault()
248
+ onSortToggle?.()
249
+ }
250
+ }
251
+
252
+ const ariaSort: 'ascending' | 'descending' | 'none' | undefined = isSortable
253
+ ? sortDirection === 'asc'
254
+ ? 'ascending'
255
+ : sortDirection === 'desc'
256
+ ? 'descending'
257
+ : 'none'
258
+ : undefined
259
+
260
+ return (
261
+ <div
262
+ {...rest}
263
+ ref={cellRef}
264
+ role="columnheader"
265
+ aria-sort={ariaSort}
266
+ tabIndex={isSortable ? 0 : undefined}
267
+ style={{ ...widthStyle, ...style }}
268
+ onClick={handleClick}
269
+ onKeyDown={handleKeyDown}
270
+ className={cn(
271
+ tableHeaderCellStyles({ align, interactive: isSortable, active, sticky }),
272
+ 'group/header',
273
+ dragState.kind === 'dragging' ? 'opacity-40' : '',
274
+ className,
275
+ )}
276
+ >
277
+ {isDraggable && columnId ? (
278
+ <span
279
+ ref={handleRef}
280
+ data-dnd-handle="true"
281
+ aria-hidden="true"
282
+ className={cn(
283
+ 'inline-flex items-center justify-center shrink-0 -ml-1.5',
284
+ 'cursor-grab active:cursor-grabbing text-ink-4',
285
+ 'opacity-0 group-hover/header:opacity-100 transition-opacity',
286
+ )}
287
+ >
288
+ <GripVertical className="size-3" />
289
+ </span>
290
+ ) : null}
291
+
292
+ <span
293
+ className={cn(
294
+ 'flex items-center gap-[7px] min-w-0 flex-1',
295
+ align === 'right' ? 'justify-end' : align === 'center' ? 'justify-center' : 'justify-start',
296
+ )}
297
+ >
298
+ <span className="truncate">{children}</span>
299
+ {isSortable ? <SortIcon dir={sortDirection} active={active} /> : null}
300
+ </span>
301
+
302
+ {onRemove ? (
303
+ <button
304
+ type="button"
305
+ data-column-remove="true"
306
+ aria-label={`Remove column ${ariaLabel ?? 'column'}`}
307
+ onClick={(e) => {
308
+ e.stopPropagation()
309
+ onRemove()
310
+ }}
311
+ className={cn(
312
+ 'inline-flex items-center justify-center shrink-0 size-4 rounded',
313
+ 'text-ink-4 hover:text-ink hover:bg-surface-2',
314
+ 'opacity-0 group-hover/header:opacity-100 focus-visible:opacity-100',
315
+ 'transition-opacity outline-none focus-visible:shadow-[var(--shadow-focus)]',
316
+ )}
317
+ >
318
+ <X className="size-3" />
319
+ </button>
320
+ ) : null}
321
+
322
+ {dragState.kind === 'over' ? (
323
+ <div
324
+ aria-hidden="true"
325
+ className={cn(
326
+ 'pointer-events-none absolute inset-y-0 z-10 w-0.5 bg-accent',
327
+ dragState.edge === 'left' ? '-left-px' : '-right-px',
328
+ )}
329
+ />
330
+ ) : null}
331
+ </div>
332
+ )
333
+ }
334
+
335
+ function SortIcon({ dir, active }: { dir: SortDirection; active: boolean }) {
336
+ return (
337
+ <svg
338
+ width="8"
339
+ height="11"
340
+ viewBox="0 0 8 11"
341
+ aria-hidden="true"
342
+ className={cn('shrink-0', active ? 'text-accent' : 'text-ink-4')}
343
+ >
344
+ <path d="M4 0.5 L7.5 4 L0.5 4 Z" fill="currentColor" opacity={dir === 'asc' ? 1 : 0.22} />
345
+ <path d="M4 10.5 L0.5 7 L7.5 7 Z" fill="currentColor" opacity={dir === 'desc' ? 1 : 0.22} />
346
+ </svg>
347
+ )
348
+ }
349
+
350
+ /**
351
+ * Trailing flexible spacer so the header row fills the full row width when
352
+ * the sum of column widths is less than the viewport. Matches the design's
353
+ * `pt-th--spacer`.
354
+ */
355
+ export function TableHeaderSpacer({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
356
+ return (
357
+ <div
358
+ aria-hidden="true"
359
+ {...props}
360
+ className={cn('flex-1 h-[36px] border-r-0', 'bg-transparent pointer-events-none', className)}
361
+ />
362
+ )
363
+ }
364
+
365
+ /* ---------- Body / scroll container -------------------------------------- */
366
+
367
+ /* Scrollbar chrome shared by `TableBody` (standalone short-table scroller)
368
+ and `TableScroller` (the virtualized DataTable's single both-axes scroller). */
369
+ const scrollbarChrome = [
370
+ '[scrollbar-width:thin]',
371
+ '[scrollbar-color:var(--color-rule)_transparent]',
372
+ '[&::-webkit-scrollbar]:size-3',
373
+ '[&::-webkit-scrollbar-thumb]:rounded-pill',
374
+ '[&::-webkit-scrollbar-thumb]:bg-rule',
375
+ '[&::-webkit-scrollbar-thumb]:border-[3px]',
376
+ '[&::-webkit-scrollbar-thumb]:border-solid',
377
+ '[&::-webkit-scrollbar-thumb]:border-bg',
378
+ '[&::-webkit-scrollbar-thumb:hover]:bg-ink-4',
379
+ '[&::-webkit-scrollbar-corner]:bg-bg',
380
+ ] as const
381
+
382
+ const tableBodyStyles = tv({
383
+ base: ['flex-1 min-h-0 overflow-auto relative', ...scrollbarChrome],
384
+ })
385
+
386
+ export type TableBodyProps = HTMLAttributes<HTMLDivElement> & {
387
+ ref?: Ref<HTMLDivElement>
388
+ }
389
+
390
+ export function TableBody({ className, ref, ...props }: TableBodyProps) {
391
+ return <div role="rowgroup" ref={ref} {...props} className={cn(tableBodyStyles(), className)} />
392
+ }
393
+
394
+ /* ---------- Scroller ------------------------------------------------------ */
395
+
396
+ /* `TableScroller` — the single scroll container for the virtualized
397
+ DataTable. It scrolls on BOTH axes so a min-width content track (sticky
398
+ header + rows) scrolls horizontally as one unit, the sticky header pins to
399
+ the top on vertical scroll, and any `sticky left-0` gutter column freezes
400
+ to the left on horizontal scroll (Excel-style). `relative` makes it the
401
+ offsetParent so the virtualizer can measure the row list's offset (the
402
+ sticky header's height) for its `scrollMargin`. Presentational: ARIA sees
403
+ through it to the header row + rowgroup it contains. */
404
+ const tableScrollerStyles = tv({
405
+ base: ['relative flex-1 min-h-0 overflow-auto', ...scrollbarChrome],
406
+ })
407
+
408
+ export function TableScroller({ className, ref, ...props }: TableBodyProps) {
409
+ return (
410
+ <div role="presentation" ref={ref} {...props} className={cn(tableScrollerStyles(), className)} />
411
+ )
412
+ }
413
+
414
+ /* ---------- Row ----------------------------------------------------------- */
415
+
416
+ /* Stripe + hover tint rows by mixing ink into the page bg they sit on. Ink is
417
+ always the opposing pole of bg, so one formula holds in every theme —
418
+ stripes darken on paper/light and lighten on dark. surface-2 can't do this:
419
+ it's a surface-relative step and collapses into bg in the light theme.
420
+ Strength is a per-theme knob (--row-stripe/--row-hover in styles.css) since a
421
+ near-white base shows a given delta more; --row-hover > --row-stripe keeps
422
+ the states ordered in every theme. */
423
+ const rowStripeBg = 'bg-[color-mix(in_oklab,var(--color-ink)_var(--row-stripe),var(--color-bg))]'
424
+ const rowHoverBg =
425
+ 'hover:bg-[color-mix(in_oklab,var(--color-ink)_var(--row-hover),var(--color-bg))]'
426
+
427
+ const tableRowStyles = tv({
428
+ base: ['flex items-stretch', 'border-b border-rule-2 last:border-b-0', 'bg-bg'],
429
+ variants: {
430
+ alt: {
431
+ true: rowStripeBg,
432
+ false: '',
433
+ },
434
+ interactive: {
435
+ true: `cursor-pointer ${rowHoverBg}`,
436
+ false: rowHoverBg,
437
+ },
438
+ absolute: {
439
+ true: 'absolute left-0 right-0',
440
+ false: '',
441
+ },
442
+ },
443
+ compoundVariants: [{ alt: true, interactive: true, class: rowHoverBg }],
444
+ defaultVariants: { alt: false, interactive: false, absolute: false },
445
+ })
446
+
447
+ export type TableRowProps = HTMLAttributes<HTMLDivElement> & {
448
+ alt?: boolean
449
+ interactive?: boolean
450
+ /**
451
+ * Position the row absolutely (used by virtualizers that translate rows
452
+ * into place via inline `style.transform`).
453
+ */
454
+ absolute?: boolean
455
+ onRowClick?: (e: MouseEvent<HTMLDivElement>) => void
456
+ }
457
+
458
+ export function TableRow({
459
+ alt,
460
+ interactive,
461
+ absolute,
462
+ className,
463
+ onRowClick,
464
+ onClick,
465
+ ...props
466
+ }: TableRowProps) {
467
+ return (
468
+ <div
469
+ role="row"
470
+ {...props}
471
+ onClick={(e) => {
472
+ onClick?.(e)
473
+ onRowClick?.(e)
474
+ }}
475
+ className={cn(tableRowStyles({ alt, interactive, absolute }), className)}
476
+ />
477
+ )
478
+ }
479
+
480
+ /* ---------- Cell ---------------------------------------------------------- */
481
+
482
+ export const tableCellStyles = tv({
483
+ base: [
484
+ 'flex items-center shrink-0 min-w-0',
485
+ 'px-[14px]',
486
+ 'whitespace-nowrap overflow-hidden text-ellipsis',
487
+ 'font-sans text-[13px] tracking-[-0.005em]',
488
+ 'text-ink-2',
489
+ ],
490
+ variants: {
491
+ align: {
492
+ left: 'justify-start text-left',
493
+ right: 'justify-end text-right [font-variant-numeric:tabular-nums]',
494
+ center: 'justify-center text-center',
495
+ },
496
+ mono: {
497
+ true: 'font-mono text-[12.5px] [font-feature-settings:"tnum","zero"]',
498
+ false: '',
499
+ },
500
+ ink: {
501
+ true: 'text-ink',
502
+ false: '',
503
+ },
504
+ sticky: {
505
+ // Sticky cells inherit the row's background via bg-inherit so the
506
+ // alternating-zebra + hover states pass through.
507
+ left: 'sticky left-0 z-[1] bg-inherit',
508
+ right: 'sticky right-0 z-[1] bg-inherit',
509
+ false: '',
510
+ },
511
+ },
512
+ defaultVariants: { align: 'left', mono: false, ink: false, sticky: false },
513
+ })
514
+
515
+ export type TableCellProps = HTMLAttributes<HTMLDivElement> & {
516
+ width?: number | string
517
+ align?: 'left' | 'right' | 'center'
518
+ mono?: boolean
519
+ ink?: boolean
520
+ /** Pin the cell to the left or right edge during horizontal scroll. */
521
+ sticky?: 'left' | 'right' | false
522
+ }
523
+
524
+ export function TableCell({
525
+ width,
526
+ align,
527
+ mono,
528
+ ink,
529
+ sticky,
530
+ className,
531
+ style,
532
+ ...props
533
+ }: TableCellProps) {
534
+ const widthStyle: CSSProperties = width != null ? { width, flexBasis: width } : {}
535
+ return (
536
+ <div
537
+ role="gridcell"
538
+ {...props}
539
+ style={{ ...widthStyle, ...style }}
540
+ className={cn(tableCellStyles({ align, mono, ink, sticky }), className)}
541
+ />
542
+ )
543
+ }
@@ -0,0 +1,8 @@
1
+ export type {
2
+ TabsListProps,
3
+ TabsPanelProps,
4
+ TabsRootProps,
5
+ TabsTriggerProps,
6
+ TabsVariants,
7
+ } from './tabs'
8
+ export { Tabs, tabsStyles } from './tabs'
@@ -0,0 +1,141 @@
1
+ import type { ReactNode } from 'react'
2
+ import {
3
+ Tab as AriaTab,
4
+ TabList as AriaTabList,
5
+ TabPanel as AriaTabPanel,
6
+ Tabs as AriaTabs,
7
+ type TabListProps as AriaTabListProps,
8
+ type TabPanelProps as AriaTabPanelProps,
9
+ type TabProps as AriaTabProps,
10
+ type TabsProps as AriaTabsProps,
11
+ } from 'react-aria-components'
12
+ import { tv, type VariantProps } from 'tailwind-variants'
13
+
14
+ import { cn } from '../../lib/cn'
15
+
16
+ /* =========================================================================
17
+ Tabs — generic tab strip wrapping RAC `<Tabs>` / `<TabList>` / `<Tab>` /
18
+ `<TabPanel>`. Chrome only — no product knowledge. Used in the page
19
+ renderer's `TabsElement` and the page-designer inspector (`Style /
20
+ Settings / Rules`).
21
+
22
+ Sub-component form (`<Tabs.Root>` / `<Tabs.List>` / `<Tabs.Trigger>` /
23
+ `<Tabs.Panel>`) so each tab's content participates separately in the
24
+ designer drop-zone tree. The compound names follow the lib's existing
25
+ slot-style components (Card, Modal).
26
+ ========================================================================= */
27
+
28
+ export const tabsStyles = tv({
29
+ slots: {
30
+ root: 'flex flex-col gap-3 outline-none',
31
+ list: 'flex gap-[18px] border-b border-rule pl-0.5',
32
+ trigger: [
33
+ 'cursor-pointer outline-none select-none',
34
+ 'border-b-[1.5px] -mb-px transition-[color,border-color] duration-150',
35
+ 'text-ink-3 border-transparent',
36
+ 'data-[hovered]:text-ink',
37
+ 'data-[selected]:text-ink data-[selected]:border-accent',
38
+ 'data-[disabled]:opacity-45 data-[disabled]:cursor-not-allowed',
39
+ 'data-[focus-visible]:shadow-[var(--shadow-focus)] data-[focus-visible]:rounded-sm',
40
+ ],
41
+ panel: 'outline-none',
42
+ },
43
+ variants: {
44
+ size: {
45
+ sm: {
46
+ list: 'gap-3',
47
+ trigger:
48
+ 'py-1.5 font-mono text-[11px] uppercase tracking-[0.08em]',
49
+ },
50
+ md: {
51
+ list: 'gap-[18px]',
52
+ trigger: 'py-2 text-[13px]',
53
+ },
54
+ },
55
+ },
56
+ defaultVariants: { size: 'md' },
57
+ })
58
+
59
+ export type TabsVariants = VariantProps<typeof tabsStyles>
60
+
61
+ export type TabsRootProps = Omit<AriaTabsProps, 'children'> &
62
+ TabsVariants & {
63
+ children?: ReactNode
64
+ }
65
+
66
+ function Root({ size, className, children, ...props }: TabsRootProps) {
67
+ const styles = tabsStyles({ size })
68
+ return (
69
+ <AriaTabs
70
+ {...props}
71
+ className={(rp) =>
72
+ cn(styles.root(), typeof className === 'function' ? className(rp) : className)
73
+ }
74
+ >
75
+ {children}
76
+ </AriaTabs>
77
+ )
78
+ }
79
+
80
+ export type TabsListProps = Omit<AriaTabListProps<object>, 'children'> &
81
+ TabsVariants & {
82
+ children?: ReactNode
83
+ }
84
+
85
+ function List({ size, className, children, ...props }: TabsListProps) {
86
+ const styles = tabsStyles({ size })
87
+ return (
88
+ <AriaTabList
89
+ {...(props as AriaTabListProps<object>)}
90
+ className={(rp) =>
91
+ cn(styles.list(), typeof className === 'function' ? className(rp) : className)
92
+ }
93
+ >
94
+ {children as never}
95
+ </AriaTabList>
96
+ )
97
+ }
98
+
99
+ export type TabsTriggerProps = Omit<AriaTabProps, 'children'> &
100
+ TabsVariants & {
101
+ children?: ReactNode
102
+ }
103
+
104
+ function Trigger({ size, className, children, ...props }: TabsTriggerProps) {
105
+ const styles = tabsStyles({ size })
106
+ return (
107
+ <AriaTab
108
+ {...props}
109
+ className={(rp) =>
110
+ cn(styles.trigger(), typeof className === 'function' ? className(rp) : className)
111
+ }
112
+ >
113
+ {children}
114
+ </AriaTab>
115
+ )
116
+ }
117
+
118
+ export type TabsPanelProps = Omit<AriaTabPanelProps, 'children'> & {
119
+ children?: ReactNode
120
+ }
121
+
122
+ function Panel({ className, children, ...props }: TabsPanelProps) {
123
+ const styles = tabsStyles({})
124
+ return (
125
+ <AriaTabPanel
126
+ {...props}
127
+ className={(rp) =>
128
+ cn(styles.panel(), typeof className === 'function' ? className(rp) : className)
129
+ }
130
+ >
131
+ {children}
132
+ </AriaTabPanel>
133
+ )
134
+ }
135
+
136
+ export const Tabs = Object.assign(Root, {
137
+ Root,
138
+ List,
139
+ Trigger,
140
+ Panel,
141
+ })
@@ -0,0 +1,2 @@
1
+ export type { TagInputProps } from './tag-input'
2
+ export { TagInput } from './tag-input'