@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.
- package/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +14 -0
- package/dist/backend/DataTable.js +28 -62
- package/dist/backend/DataTable.js.map +2 -2
- package/dist/backend/detail/AttachmentMetadataDialog.js +1 -1
- package/dist/backend/detail/AttachmentMetadataDialog.js.map +2 -2
- package/dist/primitives/activity-feed.js +196 -0
- package/dist/primitives/activity-feed.js.map +7 -0
- package/dist/primitives/avatar.js +140 -14
- package/dist/primitives/avatar.js.map +3 -3
- package/dist/primitives/badge.js +94 -5
- package/dist/primitives/badge.js.map +2 -2
- package/dist/primitives/button-group.js +52 -0
- package/dist/primitives/button-group.js.map +7 -0
- package/dist/primitives/color-picker.js +520 -0
- package/dist/primitives/color-picker.js.map +7 -0
- package/dist/primitives/command-menu.js +355 -0
- package/dist/primitives/command-menu.js.map +7 -0
- package/dist/primitives/dialog.js +210 -35
- package/dist/primitives/dialog.js.map +2 -2
- package/dist/primitives/drawer.js +219 -0
- package/dist/primitives/drawer.js.map +7 -0
- package/dist/primitives/notification-feed.js +213 -0
- package/dist/primitives/notification-feed.js.map +7 -0
- package/dist/primitives/notification.js +23 -2
- package/dist/primitives/notification.js.map +2 -2
- package/dist/primitives/pagination.js +245 -0
- package/dist/primitives/pagination.js.map +7 -0
- package/dist/primitives/progress.js +178 -5
- package/dist/primitives/progress.js.map +2 -2
- package/dist/primitives/rating.js +174 -0
- package/dist/primitives/rating.js.map +7 -0
- package/dist/primitives/scroll-area.js +82 -0
- package/dist/primitives/scroll-area.js.map +7 -0
- package/dist/primitives/segmented-control.js +101 -0
- package/dist/primitives/segmented-control.js.map +7 -0
- package/dist/primitives/separator.js +103 -4
- package/dist/primitives/separator.js.map +2 -2
- package/dist/primitives/slider.js +115 -0
- package/dist/primitives/slider.js.map +7 -0
- package/dist/primitives/step-indicator.js +187 -0
- package/dist/primitives/step-indicator.js.map +7 -0
- package/dist/primitives/table.js +102 -10
- package/dist/primitives/table.js.map +2 -2
- package/dist/primitives/tabs.js +122 -14
- package/dist/primitives/tabs.js.map +2 -2
- package/dist/primitives/time-picker.js +9 -11
- package/dist/primitives/time-picker.js.map +2 -2
- package/package.json +8 -4
- package/src/backend/DataTable.tsx +43 -60
- package/src/backend/detail/AttachmentMetadataDialog.tsx +1 -1
- package/src/primitives/__tests__/activity-feed.test.tsx +216 -0
- package/src/primitives/__tests__/avatar.test.tsx +131 -0
- package/src/primitives/__tests__/badge.test.tsx +189 -0
- package/src/primitives/__tests__/button-group.test.tsx +117 -0
- package/src/primitives/__tests__/color-picker.test.tsx +589 -0
- package/src/primitives/__tests__/command-menu.test.tsx +231 -0
- package/src/primitives/__tests__/dialog.test.tsx +290 -0
- package/src/primitives/__tests__/drawer.test.tsx +264 -0
- package/src/primitives/__tests__/notification-feed.test.tsx +303 -0
- package/src/primitives/__tests__/notification.test.tsx +102 -2
- package/src/primitives/__tests__/pagination.test.tsx +268 -0
- package/src/primitives/__tests__/progress.test.tsx +222 -0
- package/src/primitives/__tests__/rating.test.tsx +164 -0
- package/src/primitives/__tests__/scroll-area.test.tsx +109 -0
- package/src/primitives/__tests__/segmented-control.test.tsx +129 -0
- package/src/primitives/__tests__/separator.test.tsx +143 -0
- package/src/primitives/__tests__/slider.test.tsx +86 -0
- package/src/primitives/__tests__/step-indicator.test.tsx +205 -0
- package/src/primitives/__tests__/table.test.tsx +199 -0
- package/src/primitives/__tests__/tabs.test.tsx +139 -0
- package/src/primitives/activity-feed.tsx +326 -0
- package/src/primitives/avatar.tsx +225 -22
- package/src/primitives/badge.tsx +141 -6
- package/src/primitives/button-group.tsx +100 -0
- package/src/primitives/color-picker.tsx +693 -0
- package/src/primitives/command-menu.tsx +503 -0
- package/src/primitives/dialog.tsx +315 -51
- package/src/primitives/drawer.tsx +338 -0
- package/src/primitives/notification-feed.tsx +358 -0
- package/src/primitives/notification.tsx +42 -2
- package/src/primitives/pagination.tsx +376 -0
- package/src/primitives/progress.tsx +267 -6
- package/src/primitives/rating.tsx +245 -0
- package/src/primitives/scroll-area.tsx +169 -0
- package/src/primitives/segmented-control.tsx +162 -0
- package/src/primitives/separator.tsx +163 -3
- package/src/primitives/slider.tsx +160 -0
- package/src/primitives/step-indicator.tsx +287 -0
- package/src/primitives/table.tsx +142 -11
- package/src/primitives/tabs.tsx +174 -12
- 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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
+
}
|