@open-mercato/ui 0.6.4-develop.4178.1.aad9ddaa95 → 0.6.4-develop.4210.1.d412061cfe

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 (92) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +14 -0
  3. package/dist/backend/DataTable.js +28 -62
  4. package/dist/backend/DataTable.js.map +2 -2
  5. package/dist/backend/detail/AttachmentMetadataDialog.js +1 -1
  6. package/dist/backend/detail/AttachmentMetadataDialog.js.map +2 -2
  7. package/dist/primitives/activity-feed.js +196 -0
  8. package/dist/primitives/activity-feed.js.map +7 -0
  9. package/dist/primitives/avatar.js +140 -14
  10. package/dist/primitives/avatar.js.map +3 -3
  11. package/dist/primitives/badge.js +94 -5
  12. package/dist/primitives/badge.js.map +2 -2
  13. package/dist/primitives/button-group.js +52 -0
  14. package/dist/primitives/button-group.js.map +7 -0
  15. package/dist/primitives/color-picker.js +520 -0
  16. package/dist/primitives/color-picker.js.map +7 -0
  17. package/dist/primitives/command-menu.js +355 -0
  18. package/dist/primitives/command-menu.js.map +7 -0
  19. package/dist/primitives/dialog.js +210 -35
  20. package/dist/primitives/dialog.js.map +2 -2
  21. package/dist/primitives/drawer.js +219 -0
  22. package/dist/primitives/drawer.js.map +7 -0
  23. package/dist/primitives/notification-feed.js +213 -0
  24. package/dist/primitives/notification-feed.js.map +7 -0
  25. package/dist/primitives/notification.js +23 -2
  26. package/dist/primitives/notification.js.map +2 -2
  27. package/dist/primitives/pagination.js +245 -0
  28. package/dist/primitives/pagination.js.map +7 -0
  29. package/dist/primitives/progress.js +178 -5
  30. package/dist/primitives/progress.js.map +2 -2
  31. package/dist/primitives/rating.js +174 -0
  32. package/dist/primitives/rating.js.map +7 -0
  33. package/dist/primitives/scroll-area.js +82 -0
  34. package/dist/primitives/scroll-area.js.map +7 -0
  35. package/dist/primitives/segmented-control.js +101 -0
  36. package/dist/primitives/segmented-control.js.map +7 -0
  37. package/dist/primitives/separator.js +103 -4
  38. package/dist/primitives/separator.js.map +2 -2
  39. package/dist/primitives/slider.js +115 -0
  40. package/dist/primitives/slider.js.map +7 -0
  41. package/dist/primitives/step-indicator.js +187 -0
  42. package/dist/primitives/step-indicator.js.map +7 -0
  43. package/dist/primitives/table.js +102 -10
  44. package/dist/primitives/table.js.map +2 -2
  45. package/dist/primitives/tabs.js +122 -14
  46. package/dist/primitives/tabs.js.map +2 -2
  47. package/dist/primitives/time-picker.js +9 -11
  48. package/dist/primitives/time-picker.js.map +2 -2
  49. package/package.json +8 -4
  50. package/src/backend/DataTable.tsx +43 -60
  51. package/src/backend/detail/AttachmentMetadataDialog.tsx +1 -1
  52. package/src/primitives/__tests__/activity-feed.test.tsx +216 -0
  53. package/src/primitives/__tests__/avatar.test.tsx +131 -0
  54. package/src/primitives/__tests__/badge.test.tsx +189 -0
  55. package/src/primitives/__tests__/button-group.test.tsx +117 -0
  56. package/src/primitives/__tests__/color-picker.test.tsx +589 -0
  57. package/src/primitives/__tests__/command-menu.test.tsx +231 -0
  58. package/src/primitives/__tests__/dialog.test.tsx +290 -0
  59. package/src/primitives/__tests__/drawer.test.tsx +264 -0
  60. package/src/primitives/__tests__/notification-feed.test.tsx +303 -0
  61. package/src/primitives/__tests__/notification.test.tsx +102 -2
  62. package/src/primitives/__tests__/pagination.test.tsx +268 -0
  63. package/src/primitives/__tests__/progress.test.tsx +222 -0
  64. package/src/primitives/__tests__/rating.test.tsx +164 -0
  65. package/src/primitives/__tests__/scroll-area.test.tsx +109 -0
  66. package/src/primitives/__tests__/segmented-control.test.tsx +129 -0
  67. package/src/primitives/__tests__/separator.test.tsx +143 -0
  68. package/src/primitives/__tests__/slider.test.tsx +86 -0
  69. package/src/primitives/__tests__/step-indicator.test.tsx +205 -0
  70. package/src/primitives/__tests__/table.test.tsx +199 -0
  71. package/src/primitives/__tests__/tabs.test.tsx +139 -0
  72. package/src/primitives/activity-feed.tsx +326 -0
  73. package/src/primitives/avatar.tsx +225 -22
  74. package/src/primitives/badge.tsx +141 -6
  75. package/src/primitives/button-group.tsx +100 -0
  76. package/src/primitives/color-picker.tsx +693 -0
  77. package/src/primitives/command-menu.tsx +503 -0
  78. package/src/primitives/dialog.tsx +315 -51
  79. package/src/primitives/drawer.tsx +338 -0
  80. package/src/primitives/notification-feed.tsx +358 -0
  81. package/src/primitives/notification.tsx +42 -2
  82. package/src/primitives/pagination.tsx +376 -0
  83. package/src/primitives/progress.tsx +267 -6
  84. package/src/primitives/rating.tsx +245 -0
  85. package/src/primitives/scroll-area.tsx +169 -0
  86. package/src/primitives/segmented-control.tsx +162 -0
  87. package/src/primitives/separator.tsx +163 -3
  88. package/src/primitives/slider.tsx +160 -0
  89. package/src/primitives/step-indicator.tsx +287 -0
  90. package/src/primitives/table.tsx +142 -11
  91. package/src/primitives/tabs.tsx +174 -12
  92. package/src/primitives/time-picker.tsx +9 -11
@@ -0,0 +1,169 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'
5
+
6
+ import { cn } from '@open-mercato/shared/lib/utils'
7
+
8
+ /**
9
+ * DS-styled scrollable container. Wraps Radix `ScrollArea` with token-driven
10
+ * thumb / track styling that matches the rest of the design system instead of
11
+ * native OS scrollbars (which render inconsistently across macOS / Windows /
12
+ * Linux).
13
+ *
14
+ * No dedicated Figma node — DS Open Mercato library did not ship a `ScrollArea`
15
+ * master component at the time this primitive was authored. Styling is inferred
16
+ * from the DS scrollbar token decisions used elsewhere (muted-foreground thumb,
17
+ * transparent track, hover state, `rounded-full` thumb shape).
18
+ *
19
+ * Usage — primary single-element form:
20
+ *
21
+ * ```tsx
22
+ * <ScrollArea className="h-72">
23
+ * <div className="p-4">{children}</div>
24
+ * </ScrollArea>
25
+ * ```
26
+ *
27
+ * For advanced layouts that need separate access to the `Viewport`,
28
+ * `Scrollbar`, or `Corner`, use the compound API directly:
29
+ *
30
+ * ```tsx
31
+ * <ScrollAreaRoot className="h-72">
32
+ * <ScrollAreaViewport>{children}</ScrollAreaViewport>
33
+ * <ScrollAreaScrollbar orientation="vertical">
34
+ * <ScrollAreaThumb />
35
+ * </ScrollAreaScrollbar>
36
+ * <ScrollAreaCorner />
37
+ * </ScrollAreaRoot>
38
+ * ```
39
+ */
40
+
41
+ const ScrollAreaRoot = React.forwardRef<
42
+ React.ElementRef<typeof ScrollAreaPrimitive.Root>,
43
+ React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
44
+ >(({ className, ...props }, ref) => (
45
+ <ScrollAreaPrimitive.Root
46
+ ref={ref}
47
+ data-slot="scroll-area-root"
48
+ className={cn('relative overflow-hidden', className)}
49
+ {...props}
50
+ />
51
+ ))
52
+ ScrollAreaRoot.displayName = ScrollAreaPrimitive.Root.displayName
53
+
54
+ const ScrollAreaViewport = React.forwardRef<
55
+ React.ElementRef<typeof ScrollAreaPrimitive.Viewport>,
56
+ React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Viewport>
57
+ >(({ className, ...props }, ref) => (
58
+ <ScrollAreaPrimitive.Viewport
59
+ ref={ref}
60
+ data-slot="scroll-area-viewport"
61
+ className={cn('h-full w-full rounded-[inherit]', className)}
62
+ {...props}
63
+ />
64
+ ))
65
+ ScrollAreaViewport.displayName = ScrollAreaPrimitive.Viewport.displayName
66
+
67
+ const ScrollAreaScrollbar = React.forwardRef<
68
+ React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
69
+ React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
70
+ >(({ className, orientation = 'vertical', ...props }, ref) => (
71
+ <ScrollAreaPrimitive.ScrollAreaScrollbar
72
+ ref={ref}
73
+ orientation={orientation}
74
+ data-slot="scroll-area-scrollbar"
75
+ className={cn(
76
+ 'flex touch-none select-none transition-colors',
77
+ orientation === 'vertical' && 'h-full w-2.5 border-l border-l-transparent p-px',
78
+ orientation === 'horizontal' && 'h-2.5 w-full border-t border-t-transparent p-px',
79
+ className,
80
+ )}
81
+ {...props}
82
+ />
83
+ ))
84
+ ScrollAreaScrollbar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
85
+
86
+ const ScrollAreaThumb = React.forwardRef<
87
+ React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaThumb>,
88
+ React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaThumb>
89
+ >(({ className, ...props }, ref) => (
90
+ <ScrollAreaPrimitive.ScrollAreaThumb
91
+ ref={ref}
92
+ data-slot="scroll-area-thumb"
93
+ className={cn(
94
+ 'relative flex-1 rounded-full bg-muted-foreground/30 transition-colors hover:bg-muted-foreground/50',
95
+ className,
96
+ )}
97
+ {...props}
98
+ />
99
+ ))
100
+ ScrollAreaThumb.displayName = ScrollAreaPrimitive.ScrollAreaThumb.displayName
101
+
102
+ const ScrollAreaCorner = React.forwardRef<
103
+ React.ElementRef<typeof ScrollAreaPrimitive.Corner>,
104
+ React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Corner>
105
+ >(({ className, ...props }, ref) => (
106
+ <ScrollAreaPrimitive.Corner
107
+ ref={ref}
108
+ data-slot="scroll-area-corner"
109
+ className={cn('bg-transparent', className)}
110
+ {...props}
111
+ />
112
+ ))
113
+ ScrollAreaCorner.displayName = ScrollAreaPrimitive.Corner.displayName
114
+
115
+ type ScrollAreaProps = React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> & {
116
+ /**
117
+ * Which scrollbars to render. Defaults to `'vertical'` — the most common
118
+ * case. Use `'both'` for two-axis scroll (e.g. wide table preview) or
119
+ * `'horizontal'` for a horizontal-only carousel.
120
+ */
121
+ scrollbars?: 'vertical' | 'horizontal' | 'both'
122
+ /** Optional className applied to the inner `Viewport`. */
123
+ viewportClassName?: string
124
+ /** Optional className applied to every `Scrollbar`. */
125
+ scrollbarClassName?: string
126
+ /** Optional className applied to every `Thumb`. */
127
+ thumbClassName?: string
128
+ }
129
+
130
+ /**
131
+ * Convenience wrapper that composes Root + Viewport + one or two Scrollbars +
132
+ * Corner in a single element. Reach for `ScrollAreaRoot` + sub-components
133
+ * directly when the wrapper's defaults don't fit (e.g. you need conditional
134
+ * scrollbar rendering or a custom viewport).
135
+ */
136
+ const ScrollArea = React.forwardRef<
137
+ React.ElementRef<typeof ScrollAreaPrimitive.Root>,
138
+ ScrollAreaProps
139
+ >(
140
+ (
141
+ { children, className, scrollbars = 'vertical', viewportClassName, scrollbarClassName, thumbClassName, ...props },
142
+ ref,
143
+ ) => (
144
+ <ScrollAreaRoot ref={ref} className={className} {...props}>
145
+ <ScrollAreaViewport className={viewportClassName}>{children}</ScrollAreaViewport>
146
+ {(scrollbars === 'vertical' || scrollbars === 'both') ? (
147
+ <ScrollAreaScrollbar orientation="vertical" className={scrollbarClassName}>
148
+ <ScrollAreaThumb className={thumbClassName} />
149
+ </ScrollAreaScrollbar>
150
+ ) : null}
151
+ {(scrollbars === 'horizontal' || scrollbars === 'both') ? (
152
+ <ScrollAreaScrollbar orientation="horizontal" className={scrollbarClassName}>
153
+ <ScrollAreaThumb className={thumbClassName} />
154
+ </ScrollAreaScrollbar>
155
+ ) : null}
156
+ <ScrollAreaCorner />
157
+ </ScrollAreaRoot>
158
+ ),
159
+ )
160
+ ScrollArea.displayName = 'ScrollArea'
161
+
162
+ export {
163
+ ScrollArea,
164
+ ScrollAreaRoot,
165
+ ScrollAreaViewport,
166
+ ScrollAreaScrollbar,
167
+ ScrollAreaThumb,
168
+ ScrollAreaCorner,
169
+ }
@@ -0,0 +1,162 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import * as RadioGroupPrimitive from '@radix-ui/react-radio-group'
5
+ import { cva, type VariantProps } from 'class-variance-authority'
6
+
7
+ import { cn } from '@open-mercato/shared/lib/utils'
8
+
9
+ /**
10
+ * iOS-style segmented control per Figma `Switch / Chart / Cryptocurrency`
11
+ * (component set id `199963:1442` in DS Open Mercato). Renders a single
12
+ * track with N items where exactly one is selected at a time. Selecting
13
+ * a new item fires `onValueChange`.
14
+ *
15
+ * Use for **mutually-exclusive view state** — list page filters like
16
+ * "All / Active / Archived", chart period selectors, layout toggles
17
+ * (List / Grid). For *related actions* (each does something different),
18
+ * reach for `ButtonGroup` instead.
19
+ *
20
+ * Built on Radix `RadioGroup` so we inherit the radio-group ARIA contract
21
+ * (`role="radiogroup"`, `role="radio"` on items, arrow-key navigation,
22
+ * roving tabindex) for free. No new dependency — Radix RadioGroup is
23
+ * already installed via the `Radio` primitive.
24
+ *
25
+ * ```tsx
26
+ * const [view, setView] = React.useState('all')
27
+ * <SegmentedControl value={view} onValueChange={setView} aria-label="View filter">
28
+ * <SegmentedControlItem value="all">All</SegmentedControlItem>
29
+ * <SegmentedControlItem value="active">Active</SegmentedControlItem>
30
+ * <SegmentedControlItem value="archived">Archived</SegmentedControlItem>
31
+ * </SegmentedControl>
32
+ * ```
33
+ *
34
+ * Sizes:
35
+ * - `default` (h-8 / 32px) — standard toolbar density.
36
+ * - `sm` (h-7 / 28px) — tighter; pair with `text-xs`.
37
+ */
38
+
39
+ type SegmentedControlContextValue = {
40
+ size: 'sm' | 'default'
41
+ disabled?: boolean
42
+ }
43
+
44
+ const SegmentedControlContext = React.createContext<SegmentedControlContextValue>({
45
+ size: 'default',
46
+ disabled: false,
47
+ })
48
+
49
+ const trackVariants = cva(
50
+ // Pill-shaped track with subtle inner padding so selected items render
51
+ // a smaller inner pill (the iOS slide-thumb effect). Track is muted —
52
+ // selected item raises with bg-background + shadow-sm. We use full
53
+ // `bg-muted` (not /40) so the contrast between track and a selected
54
+ // bg-background item stays visible in the light theme; in dark mode
55
+ // the token already darkens further so the contrast holds.
56
+ //
57
+ // Height math (box-border on every element):
58
+ // default → track h-8 (32px) − 2px border − 2px padding (p-px ×2) = 28px → matches item h-7
59
+ // sm → track h-7 (28px) − 2px border − 2px padding (p-px ×2) = 24px → matches item h-6
60
+ // Using `p-0.5` (2px each side = 4px total) instead breaks both sizes by
61
+ // 2px so the selected pill clips top and bottom against the track border.
62
+ 'inline-flex w-fit gap-0 rounded-full border border-input bg-muted p-px transition-colors',
63
+ {
64
+ variants: {
65
+ size: {
66
+ sm: 'h-7',
67
+ default: 'h-8',
68
+ },
69
+ disabled: {
70
+ true: 'cursor-not-allowed opacity-60',
71
+ false: '',
72
+ },
73
+ },
74
+ defaultVariants: {
75
+ size: 'default',
76
+ disabled: false,
77
+ },
78
+ },
79
+ )
80
+
81
+ const itemVariants = cva(
82
+ // Items are pills that fill the track minus its 2px inner padding.
83
+ // Selected state lifts via bg-background + shadow-sm + font-semibold
84
+ // (the font-weight bump gives a secondary visual cue beyond color +
85
+ // shadow, important when the surrounding theme is high-key and the
86
+ // shadow alone is hard to read). Unselected text is muted; hover
87
+ // only nudges color (no bg change — keeps the track flat).
88
+ 'inline-flex items-center justify-center rounded-full font-medium ' +
89
+ 'transition-all outline-none focus-visible:shadow-focus ' +
90
+ 'disabled:cursor-not-allowed disabled:opacity-50 ' +
91
+ 'data-[state=checked]:bg-background data-[state=checked]:text-foreground data-[state=checked]:font-semibold data-[state=checked]:shadow-sm ' +
92
+ 'data-[state=unchecked]:bg-transparent data-[state=unchecked]:text-muted-foreground data-[state=unchecked]:hover:text-foreground',
93
+ {
94
+ variants: {
95
+ size: {
96
+ sm: 'h-6 px-2.5 text-xs',
97
+ default: 'h-7 px-3 text-sm',
98
+ },
99
+ },
100
+ defaultVariants: {
101
+ size: 'default',
102
+ },
103
+ },
104
+ )
105
+
106
+ export type SegmentedControlProps = Omit<
107
+ React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>,
108
+ 'orientation'
109
+ > &
110
+ VariantProps<typeof trackVariants> & {
111
+ /** Optional screen-reader label for the radio group. */
112
+ 'aria-label'?: string
113
+ }
114
+
115
+ export const SegmentedControl = React.forwardRef<
116
+ React.ElementRef<typeof RadioGroupPrimitive.Root>,
117
+ SegmentedControlProps
118
+ >(({ className, size, disabled, children, ...props }, ref) => {
119
+ const ctx = React.useMemo<SegmentedControlContextValue>(
120
+ () => ({ size: size ?? 'default', disabled: disabled ?? false }),
121
+ [size, disabled],
122
+ )
123
+ return (
124
+ <SegmentedControlContext.Provider value={ctx}>
125
+ <RadioGroupPrimitive.Root
126
+ ref={ref}
127
+ orientation="horizontal"
128
+ disabled={disabled ?? undefined}
129
+ data-slot="segmented-control"
130
+ className={cn(trackVariants({ size, disabled }), className)}
131
+ {...props}
132
+ >
133
+ {children}
134
+ </RadioGroupPrimitive.Root>
135
+ </SegmentedControlContext.Provider>
136
+ )
137
+ })
138
+ SegmentedControl.displayName = 'SegmentedControl'
139
+
140
+ export type SegmentedControlItemProps = React.ComponentPropsWithoutRef<
141
+ typeof RadioGroupPrimitive.Item
142
+ >
143
+
144
+ export const SegmentedControlItem = React.forwardRef<
145
+ React.ElementRef<typeof RadioGroupPrimitive.Item>,
146
+ SegmentedControlItemProps
147
+ >(({ className, children, ...props }, ref) => {
148
+ const { size } = React.useContext(SegmentedControlContext)
149
+ return (
150
+ <RadioGroupPrimitive.Item
151
+ ref={ref}
152
+ data-slot="segmented-control-item"
153
+ className={cn(itemVariants({ size }), className)}
154
+ {...props}
155
+ >
156
+ {children}
157
+ </RadioGroupPrimitive.Item>
158
+ )
159
+ })
160
+ SegmentedControlItem.displayName = 'SegmentedControlItem'
161
+
162
+ export { trackVariants as segmentedControlTrackVariants, itemVariants as segmentedControlItemVariants }
@@ -1,7 +1,167 @@
1
+ "use client"
2
+
1
3
  import * as React from 'react'
2
4
 
3
- export function Separator({ className = '', orientation = 'horizontal' }: { className?: string; orientation?: 'horizontal' | 'vertical' }) {
4
- const base = orientation === 'vertical' ? 'w-px h-full' : 'h-px w-full'
5
- return <div role="separator" aria-orientation={orientation} className={`${base} bg-border ${className}`} />
5
+ import { cn } from '@open-mercato/shared/lib/utils'
6
+
7
+ /**
8
+ * Visual divider — horizontal or vertical. Phase B.3 rewrite (Figma
9
+ * `Content Divider` page, componentSet `414:4401`) keeps the original
10
+ * bare-rule API and adds the labeled / dashed / section variants.
11
+ *
12
+ * ## Backward compatibility (5 import sites)
13
+ *
14
+ * All five existing consumers pass `<Separator />` or `<Separator
15
+ * className="my-4" />` and keep working verbatim:
16
+ *
17
+ * customers/.../companies/[id]/page.tsx
18
+ * workflows/.../tasks/[id]/page.tsx
19
+ * workflows/components/EdgeEditDialog.tsx
20
+ * workflows/components/mobile/MobileTaskForm.tsx
21
+ * data_sync/.../data-sync/page.tsx
22
+ *
23
+ * ## Variants per Figma `Content Divider [1.1]`
24
+ *
25
+ * plain — `<Separator />`
26
+ * labeled — `<Separator label="OR" />`
27
+ * labeled start — `<Separator label="OR" labelAlign="start" />`
28
+ * section header — `<Separator section>AMOUNT & ACCOUNT</Separator>`
29
+ * dashed — `<Separator variant="dashed" />`
30
+ *
31
+ * For the "rule + center button overlay" examples (Figma variants
32
+ * 6-9 — `+` button, navigation, "Add" button, button row) the
33
+ * primitive ships only the rule. Wrap your buttons in a parent
34
+ * positioned over the separator at the consumer level — adding an
35
+ * `overlay` slot to the primitive would multiply layout failure
36
+ * modes (button width, button height, button spacing) far more
37
+ * than the small win of saving the consumer wrap.
38
+ */
39
+
40
+ export type SeparatorProps = {
41
+ className?: string
42
+ orientation?: 'horizontal' | 'vertical'
43
+ /** Visual style of the rule. */
44
+ variant?: 'solid' | 'dashed'
45
+ /** Optional inline label. When set, the primitive renders as two
46
+ * rule halves with the label between them (horizontal only).
47
+ * Ignored for vertical orientation — pass plain children inside a
48
+ * vertical flex container instead. */
49
+ label?: React.ReactNode
50
+ /** Alignment of the label between the two rule halves.
51
+ * @default 'center' */
52
+ labelAlign?: 'center' | 'start' | 'end'
53
+ /** Section-header style — full-width bg-muted strip + uppercase
54
+ * label (Figma variant 5: "AMOUNT & ACCOUNT" content header). When
55
+ * `section` is true the `label` prop is rendered inside the strip;
56
+ * or pass content via `children`. Mutually exclusive with
57
+ * `label`/`labelAlign` (those use the inline-rule style). */
58
+ section?: boolean
59
+ /** Children — only consumed when `section` is true (the strip
60
+ * label can be JSX). Ignored for non-section variants. */
61
+ children?: React.ReactNode
62
+ }
63
+
64
+ // `solid` keeps the legacy `bg-border` painted-line approach for
65
+ // max backward compat with any consumers relying on box-model sizing.
66
+ // `dashed` requires `border-style` so it falls back to `border-t`
67
+ // with the painted line dropped via `h-0`.
68
+ const HORIZONTAL_RULE_CLASSES: Record<'solid' | 'dashed', string> = {
69
+ solid: 'h-px bg-border',
70
+ dashed: 'h-0 border-t border-dashed border-border',
71
+ }
72
+
73
+ const HORIZONTAL_LABELED_SEGMENT: Record<'solid' | 'dashed', string> = {
74
+ solid: 'h-px bg-border',
75
+ dashed: 'h-0 border-t border-dashed border-border',
6
76
  }
7
77
 
78
+ const VERTICAL_RULE_CLASSES: Record<'solid' | 'dashed', string> = {
79
+ solid: 'w-px bg-border',
80
+ dashed: 'w-0 border-l border-dashed border-border',
81
+ }
82
+
83
+ export function Separator({
84
+ className = '',
85
+ orientation = 'horizontal',
86
+ variant = 'solid',
87
+ label,
88
+ labelAlign = 'center',
89
+ section = false,
90
+ children,
91
+ }: SeparatorProps) {
92
+ // ---- Section header variant (Figma 5: AMOUNT & ACCOUNT) ----------
93
+ if (section) {
94
+ return (
95
+ <div
96
+ role="separator"
97
+ data-slot="separator"
98
+ data-variant="section"
99
+ aria-orientation="horizontal"
100
+ className={cn(
101
+ 'w-full bg-muted px-3 py-1.5 text-xs font-medium uppercase tracking-wide text-muted-foreground',
102
+ className,
103
+ )}
104
+ >
105
+ {label ?? children}
106
+ </div>
107
+ )
108
+ }
109
+
110
+ // ---- Vertical rule (no label support — Figma doesn't ship that
111
+ // pattern) -----------------------------------------------------------
112
+ if (orientation === 'vertical') {
113
+ return (
114
+ <div
115
+ role="separator"
116
+ aria-orientation="vertical"
117
+ data-slot="separator"
118
+ data-variant={variant}
119
+ className={cn('h-full', VERTICAL_RULE_CLASSES[variant], className)}
120
+ />
121
+ )
122
+ }
123
+
124
+ // ---- Horizontal rule with optional inline label -------------------
125
+ if (label !== undefined) {
126
+ const startSpan = labelAlign === 'start' ? 'w-6' : 'flex-1'
127
+ const endSpan = labelAlign === 'end' ? 'w-6' : 'flex-1'
128
+ return (
129
+ <div
130
+ role="separator"
131
+ aria-orientation="horizontal"
132
+ data-slot="separator"
133
+ data-variant={variant}
134
+ data-label-align={labelAlign}
135
+ className={cn('flex w-full items-center gap-3', className)}
136
+ >
137
+ <span
138
+ aria-hidden="true"
139
+ data-slot="separator-rule"
140
+ className={cn('shrink-0', HORIZONTAL_LABELED_SEGMENT[variant], startSpan)}
141
+ />
142
+ <span
143
+ data-slot="separator-label"
144
+ className="shrink-0 text-xs font-medium uppercase tracking-wide text-muted-foreground"
145
+ >
146
+ {label}
147
+ </span>
148
+ <span
149
+ aria-hidden="true"
150
+ data-slot="separator-rule"
151
+ className={cn('shrink-0', HORIZONTAL_LABELED_SEGMENT[variant], endSpan)}
152
+ />
153
+ </div>
154
+ )
155
+ }
156
+
157
+ // ---- Plain horizontal rule (original bare-line look) -------------
158
+ return (
159
+ <div
160
+ role="separator"
161
+ aria-orientation="horizontal"
162
+ data-slot="separator"
163
+ data-variant={variant}
164
+ className={cn('w-full', HORIZONTAL_RULE_CLASSES[variant], className)}
165
+ />
166
+ )
167
+ }
@@ -0,0 +1,160 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import * as SliderPrimitive from '@radix-ui/react-slider'
5
+ import { cva, type VariantProps } from 'class-variance-authority'
6
+
7
+ import { cn } from '@open-mercato/shared/lib/utils'
8
+
9
+ /**
10
+ * Numeric value selector — single value or two-thumb range. Used for price
11
+ * ranges, quantity selectors, opacity / brightness controls, threshold
12
+ * configuration. Built on Radix `@radix-ui/react-slider` so we inherit
13
+ * the slider ARIA contract (`role="slider"`, `aria-valuemin/max/now`,
14
+ * keyboard arrow / home / end navigation, RTL flip).
15
+ *
16
+ * No dedicated Figma node in the DS Open Mercato library at the time
17
+ * this primitive was authored — `Level Slider` in the file is an emoji
18
+ * icon, not a track-thumb component. Styling inferred from DS tokens
19
+ * (primary brand on the selected portion, muted track, contrasting
20
+ * thumb with focus ring) — see R4 in the v5 spec for the
21
+ * inferred-design protocol.
22
+ *
23
+ * Single thumb:
24
+ *
25
+ * ```tsx
26
+ * const [v, setV] = React.useState([20])
27
+ * <Slider value={v} onValueChange={setV} min={0} max={100} step={5} />
28
+ * ```
29
+ *
30
+ * Range (two thumbs):
31
+ *
32
+ * ```tsx
33
+ * const [range, setRange] = React.useState([10, 60])
34
+ * <Slider value={range} onValueChange={setRange} aria-label="Price range" />
35
+ * ```
36
+ *
37
+ * Optional `aria-label` should be provided when the slider's purpose
38
+ * is not obvious from surrounding context.
39
+ */
40
+
41
+ const sliderRootVariants = cva(
42
+ // Disabled cursor + greyed body via explicit disabled colors on the
43
+ // range and thumb (NOT a blanket opacity — opacity over indigo would
44
+ // bleed a faded indigo through the thumb, which the Figma disabled
45
+ // state explicitly avoids).
46
+ 'relative flex w-full touch-none select-none items-center data-[disabled]:cursor-not-allowed',
47
+ {
48
+ variants: {
49
+ orientation: {
50
+ horizontal: 'h-5',
51
+ vertical: 'h-full w-5 flex-col',
52
+ },
53
+ },
54
+ defaultVariants: { orientation: 'horizontal' },
55
+ },
56
+ )
57
+
58
+ const sliderTrackVariants = cva(
59
+ 'relative grow overflow-hidden rounded-full bg-muted',
60
+ {
61
+ variants: {
62
+ orientation: {
63
+ horizontal: 'h-1.5 w-full',
64
+ vertical: 'w-1.5 h-full',
65
+ },
66
+ },
67
+ defaultVariants: { orientation: 'horizontal' },
68
+ },
69
+ )
70
+
71
+ const sliderRangeVariants = cva(
72
+ // Enabled: indigo brand color (Figma `#6366F1`). Disabled: collapse to
73
+ // a neutral mid-grey so the range visually merges with the track (the
74
+ // Figma disabled state shows no indigo bleed at all).
75
+ 'absolute rounded-full bg-accent-indigo data-[disabled]:bg-muted-foreground/25',
76
+ {
77
+ variants: {
78
+ orientation: {
79
+ horizontal: 'h-full',
80
+ vertical: 'w-full',
81
+ },
82
+ },
83
+ defaultVariants: { orientation: 'horizontal' },
84
+ },
85
+ )
86
+
87
+ const sliderThumbVariants = cva(
88
+ // Figma `Slider [1.1]` (componentSet 2617:1169):
89
+ //
90
+ // Enabled state — 16×16 white circle with NO outer stroke and a
91
+ // centered 6×6 indigo dot. Separation from the track comes from a
92
+ // drop shadow, not a border. Outer = `bg-background` + `shadow-sm`,
93
+ // inner dot via `::after` so the thumb stays a single DOM node.
94
+ //
95
+ // Disabled state — same 16×16 white circle but now with a thin grey
96
+ // ring (the shadow is dropped entirely) and a grey inner dot. The
97
+ // border is what makes the thumb still legible against the track
98
+ // when shadow + indigo are both gone.
99
+ 'relative block size-4 rounded-full bg-background shadow-sm transition-shadow ' +
100
+ "after:content-[''] after:absolute after:left-1/2 after:top-1/2 after:size-1.5 after:-translate-x-1/2 after:-translate-y-1/2 after:rounded-full after:bg-accent-indigo " +
101
+ 'hover:shadow-md ' +
102
+ 'focus-visible:outline-none focus-visible:shadow-focus ' +
103
+ 'data-[disabled]:pointer-events-none data-[disabled]:shadow-none data-[disabled]:border data-[disabled]:border-muted-foreground/30 ' +
104
+ 'data-[disabled]:after:bg-muted-foreground/40',
105
+ )
106
+
107
+ export type SliderProps = React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root> &
108
+ VariantProps<typeof sliderRootVariants>
109
+
110
+ export const Slider = React.forwardRef<
111
+ React.ElementRef<typeof SliderPrimitive.Root>,
112
+ SliderProps
113
+ >(({ className, orientation, value, defaultValue, ...props }, ref) => {
114
+ // Derive the number of thumbs from the value/defaultValue length so we
115
+ // render the right number of `Thumb` nodes. Radix supports both
116
+ // single-thumb and range; the dom-level pattern is identical apart
117
+ // from the count of thumb children.
118
+ const thumbCount = (() => {
119
+ const v = (value ?? defaultValue) as number[] | undefined
120
+ if (Array.isArray(v) && v.length > 0) return v.length
121
+ return 1
122
+ })()
123
+
124
+ return (
125
+ <SliderPrimitive.Root
126
+ ref={ref}
127
+ orientation={orientation ?? 'horizontal'}
128
+ value={value}
129
+ defaultValue={defaultValue}
130
+ data-slot="slider-root"
131
+ className={cn(sliderRootVariants({ orientation }), className)}
132
+ {...props}
133
+ >
134
+ <SliderPrimitive.Track
135
+ data-slot="slider-track"
136
+ className={cn(sliderTrackVariants({ orientation }))}
137
+ >
138
+ <SliderPrimitive.Range
139
+ data-slot="slider-range"
140
+ className={cn(sliderRangeVariants({ orientation }))}
141
+ />
142
+ </SliderPrimitive.Track>
143
+ {Array.from({ length: thumbCount }).map((_, index) => (
144
+ <SliderPrimitive.Thumb
145
+ key={index}
146
+ data-slot="slider-thumb"
147
+ className={cn(sliderThumbVariants())}
148
+ />
149
+ ))}
150
+ </SliderPrimitive.Root>
151
+ )
152
+ })
153
+ Slider.displayName = 'Slider'
154
+
155
+ export {
156
+ sliderRootVariants,
157
+ sliderTrackVariants,
158
+ sliderRangeVariants,
159
+ sliderThumbVariants,
160
+ }