@exxatdesignux/ui 0.0.5
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/package.json +72 -0
- package/src/components/ui/avatar.tsx +384 -0
- package/src/components/ui/badge.tsx +49 -0
- package/src/components/ui/banner.tsx +364 -0
- package/src/components/ui/breadcrumb.tsx +120 -0
- package/src/components/ui/button.tsx +66 -0
- package/src/components/ui/calendar.tsx +220 -0
- package/src/components/ui/card.tsx +136 -0
- package/src/components/ui/chart.tsx +378 -0
- package/src/components/ui/checkbox.tsx +160 -0
- package/src/components/ui/coach-mark.tsx +361 -0
- package/src/components/ui/collapsible.tsx +33 -0
- package/src/components/ui/command.tsx +232 -0
- package/src/components/ui/date-picker-field.tsx +186 -0
- package/src/components/ui/dialog.tsx +171 -0
- package/src/components/ui/drag-handle-grip.tsx +10 -0
- package/src/components/ui/drawer.tsx +134 -0
- package/src/components/ui/dropdown-menu.tsx +422 -0
- package/src/components/ui/field.tsx +238 -0
- package/src/components/ui/form.tsx +137 -0
- package/src/components/ui/input-group.tsx +156 -0
- package/src/components/ui/input-mask.tsx +135 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/kbd.tsx +55 -0
- package/src/components/ui/label.tsx +25 -0
- package/src/components/ui/payment-card-fields.tsx +65 -0
- package/src/components/ui/popover.tsx +46 -0
- package/src/components/ui/radio-group.tsx +217 -0
- package/src/components/ui/select.tsx +191 -0
- package/src/components/ui/selection-tile-grid.tsx +246 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +147 -0
- package/src/components/ui/sidebar.tsx +716 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/sonner.tsx +39 -0
- package/src/components/ui/status-badge.tsx +109 -0
- package/src/components/ui/table.tsx +117 -0
- package/src/components/ui/tabs.tsx +90 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tip.tsx +21 -0
- package/src/components/ui/toggle-group.tsx +89 -0
- package/src/components/ui/toggle-switch.tsx +31 -0
- package/src/components/ui/toggle.tsx +48 -0
- package/src/components/ui/tooltip.tsx +59 -0
- package/src/components/ui/view-segmented-control.tsx +160 -0
- package/src/globals.css +1795 -0
- package/src/hooks/.gitkeep +0 -0
- package/src/hooks/use-app-theme.ts +172 -0
- package/src/hooks/use-coach-mark.ts +342 -0
- package/src/hooks/use-mobile.ts +31 -0
- package/src/hooks/use-mod-key-label.ts +29 -0
- package/src/index.ts +55 -0
- package/src/lib/compose-refs.ts +15 -0
- package/src/lib/date-filter.ts +67 -0
- package/src/lib/utils.ts +6 -0
- package/src/theme/apply-windows-contrast-theme.ts +29 -0
- package/src/theme/windows-contrast-theme.json +147 -0
- package/src/theme.css +1130 -0
- package/src/types/react-payment-inputs.d.ts +20 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Segmented “view tabs” control — same visual language as `ListPageTemplate` views toolbar
|
|
5
|
+
* (`bg-muted/60` pill). Uses `role="radiogroup"` + `role="radio"` for exclusive choice (1.3.1).
|
|
6
|
+
*
|
|
7
|
+
* Keyboard: Arrow Left/Right (or Up/Down), Home, End — see onRadioKeyDown.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as React from "react"
|
|
11
|
+
import { cn } from "../../lib/utils"
|
|
12
|
+
import { Tip } from "./tip"
|
|
13
|
+
|
|
14
|
+
export function viewSegmentedToolbarClass(className?: string) {
|
|
15
|
+
return cn(
|
|
16
|
+
"inline-flex items-center gap-0.5 rounded-lg bg-muted/60 p-[3px]",
|
|
17
|
+
className,
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function viewSegmentedButtonClass(
|
|
22
|
+
isActive: boolean,
|
|
23
|
+
opts?: { iconOnly?: boolean },
|
|
24
|
+
) {
|
|
25
|
+
return cn(
|
|
26
|
+
"inline-flex items-center rounded-md transition-all whitespace-nowrap",
|
|
27
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1",
|
|
28
|
+
opts?.iconOnly
|
|
29
|
+
? "size-8 min-h-8 min-w-8 shrink-0 justify-center p-0 text-xs"
|
|
30
|
+
: "gap-1.5 px-2.5 py-1 text-xs min-h-8",
|
|
31
|
+
isActive
|
|
32
|
+
? "bg-background text-foreground font-medium shadow-sm"
|
|
33
|
+
: "text-muted-foreground hover:text-interactive-hover-foreground",
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ViewSegmentOption<T extends string = string> {
|
|
38
|
+
value: T
|
|
39
|
+
label: string
|
|
40
|
+
/** Full `className` for Font Awesome icon (e.g. `fa-light fa-chart-bar`) */
|
|
41
|
+
icon?: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ViewSegmentedControlProps<T extends string = string> {
|
|
45
|
+
value: T
|
|
46
|
+
onValueChange: (value: T) => void
|
|
47
|
+
options: readonly ViewSegmentOption<T>[]
|
|
48
|
+
/** Accessible name for the group (required — names the radiogroup) */
|
|
49
|
+
"aria-label": string
|
|
50
|
+
/** Optional description id for `aria-describedby` (e.g. helper text) */
|
|
51
|
+
"aria-describedby"?: string
|
|
52
|
+
/** Icon-only triggers (labels in `sr-only` or visible text) */
|
|
53
|
+
iconOnly?: boolean
|
|
54
|
+
className?: string
|
|
55
|
+
/** Tooltip on each segment (defaults to `iconOnly` — recommended for icon-only) */
|
|
56
|
+
showTooltips?: boolean
|
|
57
|
+
/** Tooltip position */
|
|
58
|
+
tooltipSide?: "top" | "bottom" | "left" | "right"
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function ViewSegmentedControl<T extends string>({
|
|
62
|
+
value,
|
|
63
|
+
onValueChange,
|
|
64
|
+
options,
|
|
65
|
+
"aria-label": ariaLabel,
|
|
66
|
+
"aria-describedby": ariaDescribedBy,
|
|
67
|
+
iconOnly = false,
|
|
68
|
+
className,
|
|
69
|
+
showTooltips,
|
|
70
|
+
tooltipSide = "top",
|
|
71
|
+
}: ViewSegmentedControlProps<T>) {
|
|
72
|
+
const tips = showTooltips ?? iconOnly
|
|
73
|
+
const itemRefs = React.useRef<(HTMLButtonElement | null)[]>([])
|
|
74
|
+
|
|
75
|
+
React.useLayoutEffect(() => {
|
|
76
|
+
itemRefs.current = itemRefs.current.slice(0, options.length)
|
|
77
|
+
}, [options.length])
|
|
78
|
+
|
|
79
|
+
const focusIndex = React.useCallback(
|
|
80
|
+
(index: number) => {
|
|
81
|
+
const len = options.length
|
|
82
|
+
if (len === 0) return
|
|
83
|
+
const i = ((index % len) + len) % len
|
|
84
|
+
onValueChange(options[i].value)
|
|
85
|
+
requestAnimationFrame(() => {
|
|
86
|
+
itemRefs.current[i]?.focus()
|
|
87
|
+
})
|
|
88
|
+
},
|
|
89
|
+
[onValueChange, options],
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
const onRadioKeyDown = React.useCallback(
|
|
93
|
+
(e: React.KeyboardEvent<HTMLButtonElement>, index: number) => {
|
|
94
|
+
const len = options.length
|
|
95
|
+
if (len === 0) return
|
|
96
|
+
if (e.key === "ArrowRight" || e.key === "ArrowDown") {
|
|
97
|
+
e.preventDefault()
|
|
98
|
+
focusIndex(index + 1)
|
|
99
|
+
} else if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
|
|
100
|
+
e.preventDefault()
|
|
101
|
+
focusIndex(index - 1)
|
|
102
|
+
} else if (e.key === "Home") {
|
|
103
|
+
e.preventDefault()
|
|
104
|
+
focusIndex(0)
|
|
105
|
+
} else if (e.key === "End") {
|
|
106
|
+
e.preventDefault()
|
|
107
|
+
focusIndex(len - 1)
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
[focusIndex, options.length],
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<div
|
|
115
|
+
role="radiogroup"
|
|
116
|
+
aria-label={ariaLabel}
|
|
117
|
+
aria-describedby={ariaDescribedBy}
|
|
118
|
+
data-slot="view-segmented-toolbar"
|
|
119
|
+
className={cn(viewSegmentedToolbarClass(), "w-fit min-w-0 shrink-0", className)}
|
|
120
|
+
>
|
|
121
|
+
{options.map((opt, index) => {
|
|
122
|
+
const isActive = opt.value === value
|
|
123
|
+
const tabIndex = isActive ? 0 : -1
|
|
124
|
+
|
|
125
|
+
const button = (
|
|
126
|
+
<button
|
|
127
|
+
ref={el => {
|
|
128
|
+
itemRefs.current[index] = el
|
|
129
|
+
}}
|
|
130
|
+
type="button"
|
|
131
|
+
role="radio"
|
|
132
|
+
aria-checked={isActive}
|
|
133
|
+
aria-label={iconOnly ? opt.label : undefined}
|
|
134
|
+
tabIndex={tabIndex}
|
|
135
|
+
onKeyDown={e => onRadioKeyDown(e, index)}
|
|
136
|
+
onClick={() => onValueChange(opt.value)}
|
|
137
|
+
data-slot="view-segmented-item"
|
|
138
|
+
className={viewSegmentedButtonClass(isActive, { iconOnly })}
|
|
139
|
+
>
|
|
140
|
+
{opt.icon ? (
|
|
141
|
+
<i
|
|
142
|
+
className={cn(opt.icon, iconOnly ? "text-[13px]" : "text-xs")}
|
|
143
|
+
aria-hidden="true"
|
|
144
|
+
/>
|
|
145
|
+
) : null}
|
|
146
|
+
{!iconOnly ? opt.label : null}
|
|
147
|
+
</button>
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
return tips ? (
|
|
151
|
+
<Tip key={opt.value} label={opt.label} side={tooltipSide}>
|
|
152
|
+
{button}
|
|
153
|
+
</Tip>
|
|
154
|
+
) : (
|
|
155
|
+
<React.Fragment key={opt.value}>{button}</React.Fragment>
|
|
156
|
+
)
|
|
157
|
+
})}
|
|
158
|
+
</div>
|
|
159
|
+
)
|
|
160
|
+
}
|