minka-ds 0.1.2 → 0.1.4

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 CHANGED
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "name": "minka-ds",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Minka product design system — tokenized component library",
5
5
  "license": "MIT",
6
- "files": ["src", "tokens"],
6
+ "files": [
7
+ "src",
8
+ "tokens"
9
+ ],
7
10
  "exports": {
8
11
  ".": "./src/index.ts",
9
12
  "./tokens/*": "./tokens/*"
@@ -19,6 +22,7 @@
19
22
  "clsx": "^2.1.1",
20
23
  "lucide-react": "^1.14.0",
21
24
  "radix-ui": "^1.4.3",
25
+ "react-day-picker": "^10.0.0",
22
26
  "tailwind-merge": "^3.5.0",
23
27
  "tw-animate-css": "^1.4.0"
24
28
  }
@@ -0,0 +1,64 @@
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+
4
+ import { cn } from "../../lib/utils"
5
+
6
+ const alertVariants = cva(
7
+ "relative w-full [border-radius:var(--radius-card)] border px-4 py-3 text-body-sm grid grid-cols-[0_1fr] has-[>svg]:grid-cols-[1rem_1fr] gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:shrink-0",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default:
12
+ "bg-[var(--color-bg-raised)] border-[var(--color-border-default)] text-[var(--color-text-default)] [&>svg]:text-[var(--color-text-muted)]",
13
+ info:
14
+ "bg-[var(--color-bg-info)] border-[var(--color-border-info)] text-[var(--color-text-default)] [&>svg]:text-[var(--color-feedback-info)]",
15
+ success:
16
+ "bg-[var(--color-bg-success)] border-[var(--color-border-success)] text-[var(--color-text-default)] [&>svg]:text-[var(--color-feedback-success)]",
17
+ warning:
18
+ "bg-[var(--color-bg-warning)] border-[var(--color-border-warning)] text-[var(--color-text-default)] [&>svg]:text-[var(--color-text-default)]",
19
+ error:
20
+ "bg-[var(--color-bg-error)] border-[var(--color-border-error)] text-[var(--color-text-default)] [&>svg]:text-[var(--color-feedback-error)]",
21
+ },
22
+ },
23
+ defaultVariants: {
24
+ variant: "default",
25
+ },
26
+ }
27
+ )
28
+
29
+ function Alert({
30
+ className,
31
+ variant,
32
+ ...props
33
+ }: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
34
+ return (
35
+ <div
36
+ data-slot="alert"
37
+ role="alert"
38
+ className={cn(alertVariants({ variant }), className)}
39
+ {...props}
40
+ />
41
+ )
42
+ }
43
+
44
+ function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
45
+ return (
46
+ <div
47
+ data-slot="alert-title"
48
+ className={cn("col-start-2 text-label", className)}
49
+ {...props}
50
+ />
51
+ )
52
+ }
53
+
54
+ function AlertDescription({ className, ...props }: React.ComponentProps<"div">) {
55
+ return (
56
+ <div
57
+ data-slot="alert-description"
58
+ className={cn("col-start-2 text-caption-light text-[var(--color-text-default)] [&_p]:leading-relaxed", className)}
59
+ {...props}
60
+ />
61
+ )
62
+ }
63
+
64
+ export { Alert, AlertTitle, AlertDescription }
@@ -0,0 +1,188 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import {
5
+ ChevronDownIcon,
6
+ ChevronLeftIcon,
7
+ ChevronRightIcon,
8
+ } from "lucide-react"
9
+ import {
10
+ DayPicker,
11
+ getDefaultClassNames,
12
+ type DayButton,
13
+ } from "react-day-picker"
14
+
15
+ import { cn } from "../../lib/utils"
16
+ import { Button, buttonVariants } from "./button"
17
+
18
+ function Calendar({
19
+ className,
20
+ classNames,
21
+ showOutsideDays = true,
22
+ captionLayout = "dropdown",
23
+ numberOfMonths,
24
+ formatters,
25
+ components,
26
+ ...props
27
+ }: React.ComponentProps<typeof DayPicker> & {
28
+ buttonVariant?: React.ComponentProps<typeof Button>["variant"]
29
+ }) {
30
+ const defaultClassNames = getDefaultClassNames()
31
+ const resolvedMonths = numberOfMonths ?? (props.mode === "range" ? 2 : 1)
32
+
33
+ return (
34
+ <DayPicker
35
+ showOutsideDays={showOutsideDays}
36
+ captionLayout={captionLayout}
37
+ numberOfMonths={resolvedMonths}
38
+ className={cn(
39
+ "group/calendar bg-[var(--color-bg-raised)] rounded-[var(--radius-card)] p-3 [--cell-size:--spacing(9)]",
40
+ className
41
+ )}
42
+ formatters={{
43
+ formatMonthDropdown: (date) =>
44
+ date.toLocaleString("default", { month: "short" }),
45
+ ...formatters,
46
+ }}
47
+ classNames={{
48
+ root: cn("w-fit", defaultClassNames.root),
49
+ months: cn("relative flex flex-col gap-4 md:flex-row", defaultClassNames.months),
50
+ month: cn("flex w-full flex-col gap-4", defaultClassNames.month),
51
+
52
+ // ── Nav ─────────────────────────────────────────────────────────────
53
+ nav: cn(
54
+ "absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1",
55
+ defaultClassNames.nav
56
+ ),
57
+ button_previous: cn(
58
+ buttonVariants({ variant: "ghost", size: "icon-sm" }),
59
+ "size-(--cell-size) select-none aria-disabled:opacity-40",
60
+ defaultClassNames.button_previous
61
+ ),
62
+ button_next: cn(
63
+ buttonVariants({ variant: "ghost", size: "icon-sm" }),
64
+ "size-(--cell-size) select-none aria-disabled:opacity-40",
65
+ defaultClassNames.button_next
66
+ ),
67
+
68
+ // ── Caption ──────────────────────────────────────────────────────────
69
+ month_caption: cn(
70
+ "flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)",
71
+ defaultClassNames.month_caption
72
+ ),
73
+ caption_label: cn(
74
+ "font-medium select-none",
75
+ captionLayout === "label"
76
+ ? "text-body-sm"
77
+ : "flex h-8 items-center gap-1 [border-radius:var(--radius-input)] pr-1 pl-2 text-body-sm hover:bg-[var(--color-action-ghost-hover)] transition-colors [&>svg]:size-3.5 [&>svg]:text-[var(--color-text-muted)]",
78
+ defaultClassNames.caption_label
79
+ ),
80
+
81
+ // ── Dropdowns ────────────────────────────────────────────────────────
82
+ dropdowns: cn(
83
+ "flex h-(--cell-size) w-full items-center justify-center gap-1.5",
84
+ defaultClassNames.dropdowns
85
+ ),
86
+ dropdown_root: cn(
87
+ "relative [border-radius:var(--radius-button)] hover:bg-[var(--color-action-ghost-hover)] transition-colors",
88
+ defaultClassNames.dropdown_root
89
+ ),
90
+ dropdown: cn(
91
+ "absolute inset-0 opacity-0 cursor-pointer",
92
+ defaultClassNames.dropdown
93
+ ),
94
+
95
+ // ── Grid ─────────────────────────────────────────────────────────────
96
+ month_grid: "w-full border-collapse",
97
+ weekdays: cn("flex", defaultClassNames.weekdays),
98
+ weekday: cn(
99
+ "flex-1 text-caption text-[var(--color-text-muted)] text-center select-none",
100
+ defaultClassNames.weekday
101
+ ),
102
+ week: cn("mt-2 flex w-full", defaultClassNames.week),
103
+
104
+ // ── Day cells ────────────────────────────────────────────────────────
105
+ day: cn(
106
+ "group/day relative aspect-square h-full w-full p-0 text-center select-none",
107
+ "[&:last-child[data-selected=true]_button]:rounded-r-[var(--radius-input)]",
108
+ props.showWeekNumber
109
+ ? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-[var(--radius-input)]"
110
+ : "[&:first-child[data-selected=true]_button]:rounded-l-[var(--radius-input)]",
111
+ defaultClassNames.day
112
+ ),
113
+ range_start: cn("rounded-l-[var(--radius-input)] bg-[var(--color-action-ghost-hover)]", defaultClassNames.range_start),
114
+ range_middle: cn("rounded-none", defaultClassNames.range_middle),
115
+ range_end: cn("rounded-r-[var(--radius-input)] bg-[var(--color-action-ghost-hover)]", defaultClassNames.range_end),
116
+ today: cn(
117
+ "rounded-[var(--radius-input)] bg-[var(--color-action-ghost-hover)] text-[var(--color-text-default)] data-[selected=true]:rounded-none",
118
+ defaultClassNames.today
119
+ ),
120
+ outside: cn("text-[var(--color-text-hint)] aria-selected:text-[var(--color-text-hint)]", defaultClassNames.outside),
121
+ disabled: cn("text-[var(--color-text-disabled)] opacity-50", defaultClassNames.disabled),
122
+ hidden: cn("invisible", defaultClassNames.hidden),
123
+ ...classNames,
124
+ }}
125
+ components={{
126
+ Root: ({ className, rootRef, ...props }) => (
127
+ <div data-slot="calendar" ref={rootRef} className={cn(className)} {...props} />
128
+ ),
129
+ Chevron: ({ className, orientation, ...props }) => {
130
+ if (orientation === "left") return <ChevronLeftIcon className={cn("size-4", className)} {...props} />
131
+ if (orientation === "right") return <ChevronRightIcon className={cn("size-4", className)} {...props} />
132
+ return <ChevronDownIcon className={cn("size-4", className)} {...props} />
133
+ },
134
+ DayButton: CalendarDayButton,
135
+ ...components,
136
+ }}
137
+ {...props}
138
+ />
139
+ )
140
+ }
141
+
142
+ function CalendarDayButton({
143
+ className,
144
+ day,
145
+ modifiers,
146
+ ...props
147
+ }: React.ComponentProps<typeof DayButton>) {
148
+ const defaultClassNames = getDefaultClassNames()
149
+ const ref = React.useRef<HTMLButtonElement>(null)
150
+
151
+ React.useEffect(() => {
152
+ if (modifiers.focused) ref.current?.focus()
153
+ }, [modifiers.focused])
154
+
155
+ return (
156
+ <Button
157
+ ref={ref}
158
+ variant="ghost"
159
+ size="icon"
160
+ data-day={day.date.toLocaleDateString()}
161
+ data-selected-single={
162
+ modifiers.selected &&
163
+ !modifiers.range_start &&
164
+ !modifiers.range_end &&
165
+ !modifiers.range_middle
166
+ }
167
+ data-range-start={modifiers.range_start}
168
+ data-range-end={modifiers.range_end}
169
+ data-range-middle={modifiers.range_middle}
170
+ className={cn(
171
+ "flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none",
172
+ modifiers.selected || modifiers.today ? "text-body-sm" : "text-body-sm-light",
173
+ modifiers.outside && "text-[var(--color-text-hint)] hover:text-[var(--color-text-hint)]",
174
+ "group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10",
175
+ "group-data-[focused=true]/day:border-[var(--color-border-focus)] group-data-[focused=true]/day:ring-[3px] group-data-[focused=true]/day:ring-[var(--color-border-focus)]/50",
176
+ "data-[selected-single=true]:bg-[var(--color-action-primary-default)] data-[selected-single=true]:text-[var(--color-action-primary-foreground)]",
177
+ "data-[range-start=true]:rounded-l-[var(--radius-input)] data-[range-start=true]:bg-[var(--color-action-primary-default)] data-[range-start=true]:text-[var(--color-action-primary-foreground)]",
178
+ "data-[range-end=true]:rounded-r-[var(--radius-input)] data-[range-end=true]:bg-[var(--color-action-primary-default)] data-[range-end=true]:text-[var(--color-action-primary-foreground)]",
179
+ "data-[range-middle=true]:rounded-none data-[range-middle=true]:bg-[var(--color-action-ghost-hover)] data-[range-middle=true]:text-[var(--color-text-default)]",
180
+ defaultClassNames.day,
181
+ className
182
+ )}
183
+ {...props}
184
+ />
185
+ )
186
+ }
187
+
188
+ export { Calendar, CalendarDayButton }
@@ -155,7 +155,7 @@ function DataTable<TData, TValue>({
155
155
  >
156
156
 
157
157
  <Table className="[&_th:first-child]:pl-4 [&_td:first-child]:pl-4">
158
- <TableHeader className="sticky top-0 z-10 bg-[var(--color-bg-base)]">
158
+ <TableHeader className="sticky top-0 [z-index:var(--z-sticky)] bg-[var(--color-bg-base)]">
159
159
  {table.getHeaderGroups().map((headerGroup) => (
160
160
  <TableRow key={headerGroup.id}>
161
161
  {headerGroup.headers.map((header, index) => (
@@ -0,0 +1,72 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { XIcon } from "lucide-react"
5
+ import { cn } from "../../lib/utils"
6
+
7
+ type FilterChipProps =
8
+ | {
9
+ variant?: "filter"
10
+ label: string
11
+ values: { label: string; onRemove: () => void }[]
12
+ onLabelClick?: () => void
13
+ className?: string
14
+ }
15
+ | {
16
+ variant: "clear-all"
17
+ onClear: () => void
18
+ className?: string
19
+ }
20
+
21
+ function FilterChip(props: FilterChipProps) {
22
+ if (props.variant === "clear-all") {
23
+ const { onClear, className } = props
24
+ return (
25
+ <button
26
+ type="button"
27
+ onClick={onClear}
28
+ className={cn(
29
+ "text-caption text-[var(--color-text-muted)] underline-offset-2 hover:underline cursor-pointer",
30
+ className
31
+ )}
32
+ >
33
+ Clear all
34
+ </button>
35
+ )
36
+ }
37
+
38
+ const { label, values, onLabelClick, className } = props
39
+
40
+ return (
41
+ <div className={cn("flex items-center gap-1.5", className)}>
42
+ <button
43
+ type="button"
44
+ onClick={onLabelClick}
45
+ className={cn(
46
+ "text-caption text-[var(--color-text-muted)] underline-offset-2 hover:underline cursor-pointer",
47
+ !onLabelClick && "pointer-events-none"
48
+ )}
49
+ >
50
+ {label}:
51
+ </button>
52
+ {values.map((value, i) => (
53
+ <span
54
+ key={i}
55
+ className="inline-flex items-center gap-1 rounded-full border border-[var(--color-border-default)] px-2 py-0.5 text-caption text-[var(--color-text-default)]"
56
+ >
57
+ {value.label}
58
+ <button
59
+ type="button"
60
+ onClick={value.onRemove}
61
+ className="text-[var(--color-text-muted)] hover:text-[var(--color-text-default)] transition-colors"
62
+ aria-label={`Remove ${value.label}`}
63
+ >
64
+ <XIcon className="size-3" />
65
+ </button>
66
+ </span>
67
+ ))}
68
+ </div>
69
+ )
70
+ }
71
+
72
+ export { FilterChip }
@@ -0,0 +1,430 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import {
5
+ CheckIcon,
6
+ ChevronLeftIcon,
7
+ ChevronRightIcon,
8
+ PlusIcon,
9
+ SearchIcon,
10
+ } from "lucide-react"
11
+ import type { DateRange } from "react-day-picker"
12
+
13
+ import { cn } from "../../lib/utils"
14
+ import { Button } from "./button"
15
+ import { Calendar } from "./calendar"
16
+ import { Input } from "./input"
17
+ import { Tabs, TabsList, TabsTrigger } from "./tabs"
18
+
19
+ // ── Types ──────────────────────────────────────────────────────────────────────
20
+
21
+ interface FilterCategory {
22
+ id: string
23
+ label: string
24
+ type?: "list" | "date" | "amount"
25
+ values?: string[]
26
+ renderValue?: (value: string) => React.ReactNode
27
+ }
28
+
29
+ type AmountValue = { exact: number } | { min?: number; max?: number }
30
+ type CategoryValue = string | DateRange | AmountValue
31
+
32
+
33
+ type Step = 1 | 2 | 3
34
+
35
+ // ── Main component ─────────────────────────────────────────────────────────────
36
+
37
+ function FilterCombobox({
38
+ categories,
39
+ onApply,
40
+ activeFilters = {},
41
+ trigger,
42
+ dropdownAlign = "left",
43
+ className,
44
+ }: {
45
+ categories: FilterCategory[]
46
+ onApply: (categoryId: string, values: CategoryValue[]) => void
47
+ activeFilters?: Record<string, CategoryValue[]>
48
+ trigger?: (props: { open: boolean; onClick: () => void }) => React.ReactNode
49
+ dropdownAlign?: "left" | "right"
50
+ className?: string
51
+ }) {
52
+ const [open, setOpen] = React.useState(false)
53
+ const [step, setStep] = React.useState<Step>(1)
54
+ const [selectedCategory, setSelectedCategory] = React.useState<FilterCategory | null>(null)
55
+ const [selectedValues, setSelectedValues] = React.useState<Set<string>>(new Set())
56
+ const [dateRange, setDateRange] = React.useState<DateRange | undefined>()
57
+ const [amountMode, setAmountMode] = React.useState<"exact" | "range">("exact")
58
+ const [amountExact, setAmountExact] = React.useState("")
59
+ const [amountMin, setAmountMin] = React.useState("")
60
+ const [amountMax, setAmountMax] = React.useState("")
61
+ const [search, setSearch] = React.useState("")
62
+
63
+ const containerRef = React.useRef<HTMLDivElement>(null)
64
+ const searchRef = React.useRef<HTMLInputElement>(null)
65
+
66
+ // Close on outside click
67
+ React.useEffect(() => {
68
+ if (!open) return
69
+ const handler = (e: MouseEvent) => {
70
+ if (!containerRef.current?.contains(e.target as Node)) handleClose()
71
+ }
72
+ document.addEventListener("mousedown", handler)
73
+ return () => document.removeEventListener("mousedown", handler)
74
+ }, [open])
75
+
76
+ // Auto-focus search when entering step 2
77
+ React.useEffect(() => {
78
+ if (open && step === 2) setTimeout(() => searchRef.current?.focus(), 30)
79
+ }, [open, step])
80
+
81
+ function handleClose() {
82
+ setOpen(false)
83
+ setStep(1)
84
+ setSelectedCategory(null)
85
+ setSelectedValues(new Set())
86
+ setDateRange(undefined)
87
+ setSearch("")
88
+ setAmountMode("exact")
89
+ setAmountExact("")
90
+ setAmountMin("")
91
+ setAmountMax("")
92
+ }
93
+
94
+ function openCategory(cat: FilterCategory) {
95
+ const existing = activeFilters[cat.id] ?? []
96
+
97
+ if (cat.type === "date") {
98
+ const custom = existing.find((v): v is DateRange => typeof v === "object" && "from" in v)
99
+ setSelectedCategory(cat)
100
+ setDateRange(custom)
101
+ setSelectedValues(new Set())
102
+ setSearch("")
103
+ setStep(3)
104
+ return
105
+ }
106
+
107
+ if (cat.type === "amount") {
108
+ const custom = existing.find((v): v is AmountValue =>
109
+ typeof v === "object" && ("exact" in v || "min" in v || "max" in v)
110
+ )
111
+ setSelectedCategory(cat)
112
+ setSelectedValues(new Set())
113
+ setSearch("")
114
+ if (custom) {
115
+ if ("exact" in custom) {
116
+ setAmountMode("exact")
117
+ setAmountExact(String(custom.exact))
118
+ setAmountMin("")
119
+ setAmountMax("")
120
+ } else {
121
+ const r = custom as { min?: number; max?: number }
122
+ setAmountMode("range")
123
+ setAmountExact("")
124
+ setAmountMin(r.min != null ? String(r.min) : "")
125
+ setAmountMax(r.max != null ? String(r.max) : "")
126
+ }
127
+ }
128
+ setStep(3)
129
+ return
130
+ }
131
+
132
+ setSelectedValues(new Set(existing.filter((v): v is string => typeof v === "string")))
133
+ setSelectedCategory(cat)
134
+ setSearch("")
135
+ setStep(2)
136
+ }
137
+
138
+ function applyValues() {
139
+ if (!selectedCategory) return
140
+ onApply(selectedCategory.id, Array.from(selectedValues))
141
+ handleClose()
142
+ }
143
+
144
+ function applyCustomDate() {
145
+ if (!selectedCategory || !dateRange?.from) return
146
+ onApply(selectedCategory.id, [dateRange])
147
+ handleClose()
148
+ }
149
+
150
+ function applyCustomAmount() {
151
+ if (!selectedCategory) return
152
+ let value: AmountValue
153
+ if (amountMode === "exact") {
154
+ const n = parseFloat(amountExact)
155
+ if (isNaN(n)) return
156
+ value = { exact: n }
157
+ } else {
158
+ const min = amountMin !== "" ? parseFloat(amountMin) : undefined
159
+ const max = amountMax !== "" ? parseFloat(amountMax) : undefined
160
+ if (min == null && max == null) return
161
+ value = { min, max }
162
+ }
163
+ onApply(selectedCategory.id, [value])
164
+ handleClose()
165
+ }
166
+
167
+ function toggleValue(value: string, singleSelect = false) {
168
+ setSelectedValues(prev => {
169
+ if (singleSelect) return prev.has(value) ? new Set() : new Set([value])
170
+ const next = new Set(prev)
171
+ next.has(value) ? next.delete(value) : next.add(value)
172
+ return next
173
+ })
174
+ }
175
+
176
+ // ── Derived values ───────────────────────────────────────────────────────────
177
+
178
+ const isDate = selectedCategory?.type === "date"
179
+ const isAmount = selectedCategory?.type === "amount"
180
+
181
+ const step2AllValues = selectedCategory?.values ?? []
182
+
183
+ const showSearch = step2AllValues.length > 4
184
+ const step2Filtered = showSearch
185
+ ? step2AllValues.filter(v => v.toLowerCase().includes(search.toLowerCase()))
186
+ : step2AllValues
187
+
188
+ const canApplyAmount = amountMode === "exact"
189
+ ? amountExact !== "" && !isNaN(parseFloat(amountExact))
190
+ : amountMin !== "" || amountMax !== ""
191
+
192
+ // ── Render ───────────────────────────────────────────────────────────────────
193
+
194
+ return (
195
+ <div ref={containerRef} className={cn("relative inline-block", className)}>
196
+ {trigger ? trigger({ open, onClick: () => setOpen(v => !v) }) : (
197
+ <Button variant="default" size="sm" className="h-7 text-caption gap-1.5 px-2.5" onClick={() => setOpen(v => !v)}>
198
+ <PlusIcon className="size-3.5" />
199
+ Add filter
200
+ </Button>
201
+ )}
202
+
203
+ {open && (
204
+ <div className={cn(
205
+ dropdownAlign === "right" ? "absolute right-0 top-full mt-1.5 overflow-hidden [border-radius:var(--radius-popover)]" : "absolute left-0 top-full mt-1.5 overflow-hidden [border-radius:var(--radius-popover)]",
206
+ "bg-[var(--color-bg-overlay)] shadow-[var(--shadow-popover)] ring-1 ring-[var(--color-border-subtle)]",
207
+ "[z-index:var(--z-dropdown)]",
208
+ step === 3 && isDate ? "w-auto" : "w-56"
209
+ )}>
210
+
211
+ {/* Step 1 — category list */}
212
+ {step === 1 && (
213
+ <ul className="p-1">
214
+ {categories.map(cat => (
215
+ <li key={cat.id}>
216
+ <PickerRow onClick={() => openCategory(cat)}>{cat.label}</PickerRow>
217
+ </li>
218
+ ))}
219
+ </ul>
220
+ )}
221
+
222
+ {/* Step 2 — value list */}
223
+ {step === 2 && selectedCategory && (
224
+ <>
225
+ <StepHeader
226
+ title={selectedCategory.label}
227
+ onBack={() => { setStep(1); setSearch("") }}
228
+ />
229
+ {showSearch && (
230
+ <div className="px-1 pt-1">
231
+ <SearchInput ref={searchRef} value={search} onChange={setSearch} />
232
+ </div>
233
+ )}
234
+ <ul className="max-h-52 overflow-y-auto p-1">
235
+ {step2Filtered.length === 0
236
+ ? <EmptyRow />
237
+ : step2Filtered.map(value => (
238
+ <li key={value}>
239
+ <CheckRow
240
+ checked={selectedValues.has(value)}
241
+ onToggle={() => toggleValue(value)}
242
+ >
243
+ {selectedCategory.renderValue?.(value) ?? value}
244
+ </CheckRow>
245
+ </li>
246
+ ))
247
+ }
248
+ </ul>
249
+ {selectedValues.size > 0 && (
250
+ <div className="p-1">
251
+ <Button size="sm" className="w-full" onClick={applyValues}>
252
+ Apply
253
+ </Button>
254
+ </div>
255
+ )}
256
+ </>
257
+ )}
258
+
259
+ {/* Step 3 — custom date range */}
260
+ {step === 3 && isDate && (
261
+ <>
262
+ <StepHeader
263
+ title={selectedCategory?.label ?? "Date"}
264
+ onBack={() => setStep(1)}
265
+ />
266
+ <div className="p-1">
267
+ <Calendar mode="range" selected={dateRange} onSelect={setDateRange} numberOfMonths={1} />
268
+ </div>
269
+ {dateRange?.from && (
270
+ <div className="p-1">
271
+ <Button size="sm" className="w-full" onClick={applyCustomDate}>
272
+ Apply
273
+ </Button>
274
+ </div>
275
+ )}
276
+ </>
277
+ )}
278
+
279
+ {/* Step 3 — custom amount (no step 2 for amount — opens here directly) */}
280
+ {step === 3 && isAmount && (
281
+ <>
282
+ <StepHeader
283
+ title="Amount"
284
+ onBack={() => { setStep(1) }}
285
+ />
286
+ <div className="px-2 pb-2 flex flex-col gap-2">
287
+ <Tabs
288
+ value={amountMode}
289
+ onValueChange={v => setAmountMode(v as "exact" | "range")}
290
+ className="w-full"
291
+ >
292
+ <TabsList className="w-full">
293
+ <TabsTrigger value="exact" className="flex-1">Exact</TabsTrigger>
294
+ <TabsTrigger value="range" className="flex-1">Range</TabsTrigger>
295
+ </TabsList>
296
+ </Tabs>
297
+
298
+ {amountMode === "exact" ? (
299
+ <div className="relative">
300
+ <span className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-body-sm text-[var(--color-text-muted)]">$</span>
301
+ <Input
302
+ type="number"
303
+ min="0"
304
+ value={amountExact}
305
+ onChange={e => setAmountExact(e.target.value)}
306
+ placeholder="0"
307
+ className="pl-6 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
308
+ />
309
+ </div>
310
+ ) : (
311
+ <div className="flex items-center gap-2">
312
+ <div className="relative flex-1">
313
+ <span className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-body-sm text-[var(--color-text-muted)]">$</span>
314
+ <Input
315
+ type="number"
316
+ min="0"
317
+ value={amountMin}
318
+ onChange={e => setAmountMin(e.target.value)}
319
+ placeholder="Min"
320
+ className="pl-6 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
321
+ />
322
+ </div>
323
+ <span className="text-body-sm text-[var(--color-text-muted)] shrink-0">–</span>
324
+ <div className="relative flex-1">
325
+ <span className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-body-sm text-[var(--color-text-muted)]">$</span>
326
+ <Input
327
+ type="number"
328
+ min="0"
329
+ value={amountMax}
330
+ onChange={e => setAmountMax(e.target.value)}
331
+ placeholder="Max"
332
+ className="pl-6 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
333
+ />
334
+ </div>
335
+ </div>
336
+ )}
337
+
338
+ {canApplyAmount && (
339
+ <Button size="sm" className="w-full" onClick={applyCustomAmount}>
340
+ Apply
341
+ </Button>
342
+ )}
343
+ </div>
344
+ </>
345
+ )}
346
+
347
+ </div>
348
+ )}
349
+ </div>
350
+ )
351
+ }
352
+
353
+ // ── Internal sub-components ────────────────────────────────────────────────────
354
+
355
+ const SearchInput = React.forwardRef<
356
+ HTMLInputElement,
357
+ { value: string; onChange: (v: string) => void }
358
+ >(({ value, onChange }, ref) => (
359
+ <div className="flex h-8 items-center gap-2 [border-radius:var(--radius-input)] border border-[var(--color-border-default)]/30 bg-[var(--color-bg-canvas)]/60 px-2">
360
+ <SearchIcon className="size-3.5 shrink-0 text-[var(--color-text-muted)]" />
361
+ <input
362
+ ref={ref}
363
+ value={value}
364
+ onChange={e => onChange(e.target.value)}
365
+ placeholder="Search…"
366
+ className="min-w-0 flex-1 bg-transparent text-body-sm outline-none placeholder:text-[var(--color-text-hint)]"
367
+ />
368
+ </div>
369
+ ))
370
+ SearchInput.displayName = "SearchInput"
371
+
372
+ function StepHeader({ title, onBack }: { title: string; onBack: () => void }) {
373
+ return (
374
+ <div className="flex items-center gap-1 px-1 pt-1">
375
+ <button
376
+ type="button"
377
+ onClick={onBack}
378
+ className="flex items-center justify-center [border-radius:var(--radius-tag)] p-1 text-[var(--color-text-muted)] hover:bg-[var(--color-action-ghost-hover)] transition-colors"
379
+ >
380
+ <ChevronLeftIcon className="size-4" />
381
+ </button>
382
+ <span className="text-body-sm font-medium text-[var(--color-text-default)]">{title}</span>
383
+ </div>
384
+ )
385
+ }
386
+
387
+ function PickerRow({ children, onClick }: { children: React.ReactNode; onClick: () => void }) {
388
+ return (
389
+ <button
390
+ type="button"
391
+ onClick={onClick}
392
+ className="relative flex w-full items-center gap-2 [border-radius:var(--radius-tag)] py-1.5 pl-2 pr-8 text-body-sm text-[var(--color-text-default)] hover:bg-[var(--color-action-ghost-hover)] transition-colors"
393
+ >
394
+ {children}
395
+ <ChevronRightIcon className="pointer-events-none absolute right-2 size-3.5 text-[var(--color-text-muted)]" />
396
+ </button>
397
+ )
398
+ }
399
+
400
+ function CheckRow({
401
+ children,
402
+ checked,
403
+ onToggle,
404
+ }: {
405
+ children: React.ReactNode
406
+ checked: boolean
407
+ onToggle: () => void
408
+ }) {
409
+ return (
410
+ <button
411
+ type="button"
412
+ onClick={onToggle}
413
+ className="relative flex w-full items-center gap-2 [border-radius:var(--radius-tag)] py-1.5 pl-2 pr-8 text-body-sm text-[var(--color-text-default)] hover:bg-[var(--color-action-ghost-hover)] transition-colors"
414
+ >
415
+ {children}
416
+ <span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center">
417
+ {checked && <CheckIcon className="size-4 text-[var(--color-text-default)]" />}
418
+ </span>
419
+ </button>
420
+ )
421
+ }
422
+
423
+ function EmptyRow() {
424
+ return <li className="py-2 text-center text-body-sm text-[var(--color-text-muted)]">No results</li>
425
+ }
426
+
427
+ // ── Exports ────────────────────────────────────────────────────────────────────
428
+
429
+ export { FilterCombobox }
430
+ export type { FilterCategory, CategoryValue, AmountValue }
@@ -0,0 +1,179 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { PlusIcon, SearchIcon, XIcon } from "lucide-react"
5
+ import type { DateRange } from "react-day-picker"
6
+
7
+ import { cn } from "../../lib/utils"
8
+ import { Button } from "./button"
9
+ import { FilterChip } from "./filter-chip"
10
+ import { FilterCombobox } from "./filter-combobox"
11
+ import type { FilterCategory, CategoryValue } from "./filter-combobox"
12
+ import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "./input-group"
13
+ import { Kbd } from "./kbd"
14
+
15
+ // ── Default label formatter ────────────────────────────────────────────────────
16
+
17
+ function defaultFilterValueLabel(_categoryId: string, value: CategoryValue): string {
18
+ if (typeof value === "string") return value
19
+ if (typeof value === "object" && "from" in value) {
20
+ const v = value as DateRange
21
+ if (!v.from) return ""
22
+ const from = v.from.toLocaleDateString("default", { month: "short", day: "numeric" })
23
+ const to = v.to?.toLocaleDateString("default", { month: "short", day: "numeric" })
24
+ return to ? `${from} – ${to}` : `From ${from}`
25
+ }
26
+ if (typeof value === "object" && "exact" in value) {
27
+ return (value as { exact: number }).exact.toLocaleString("en-US")
28
+ }
29
+ if (typeof value === "object") {
30
+ const { min, max } = value as { min?: number; max?: number }
31
+ if (min != null && max != null) return `${min.toLocaleString()} – ${max.toLocaleString()}`
32
+ if (min != null) return `> ${min.toLocaleString()}`
33
+ if (max != null) return `< ${max.toLocaleString()}`
34
+ }
35
+ return ""
36
+ }
37
+
38
+ // ── Types ──────────────────────────────────────────────────────────────────────
39
+
40
+ interface SearchBarProps {
41
+ placeholder?: string
42
+ value: string
43
+ onChange: (value: string) => void
44
+ onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void
45
+ onFocus?: () => void
46
+ inputRef?: React.RefObject<HTMLInputElement | null>
47
+ kbdHint?: React.ReactNode
48
+
49
+ filterCategories?: FilterCategory[]
50
+ activeFilters?: Record<string, CategoryValue[]>
51
+ onApplyFilter?: (categoryId: string, values: CategoryValue[]) => void
52
+ onRemoveFilter?: (categoryId: string, value: CategoryValue) => void
53
+ onClearFilters?: () => void
54
+ filterValueLabel?: (categoryId: string, value: CategoryValue) => string
55
+
56
+ children?: React.ReactNode
57
+ className?: string
58
+ }
59
+
60
+ // ── Component ──────────────────────────────────────────────────────────────────
61
+
62
+ function SearchBar({
63
+ placeholder,
64
+ value,
65
+ onChange,
66
+ onKeyDown,
67
+ onFocus,
68
+ inputRef,
69
+ kbdHint,
70
+ filterCategories = [],
71
+ activeFilters = {},
72
+ onApplyFilter,
73
+ onRemoveFilter,
74
+ onClearFilters,
75
+ filterValueLabel = defaultFilterValueLabel,
76
+ children,
77
+ className,
78
+ }: SearchBarProps) {
79
+ const hasActiveFilters = Object.values(activeFilters).some(v => v.length > 0)
80
+ const hasFilterCategories = filterCategories.length > 0
81
+
82
+ return (
83
+ <div data-search-bar className={cn("relative flex flex-col", className)}>
84
+
85
+ {/* Search input row */}
86
+ <InputGroup
87
+ className={cn(
88
+ "h-12",
89
+ hasActiveFilters && "[border-bottom-left-radius:0] [border-bottom-right-radius:0]"
90
+ )}
91
+ >
92
+ <InputGroupAddon align="inline-start">
93
+ <SearchIcon className="size-4" />
94
+ </InputGroupAddon>
95
+ <InputGroupInput
96
+ ref={inputRef}
97
+ placeholder={placeholder}
98
+ value={value}
99
+ onChange={e => onChange(e.target.value)}
100
+ onKeyDown={onKeyDown}
101
+ onFocus={onFocus}
102
+ autoComplete="off"
103
+ />
104
+ {(!!value || (hasFilterCategories && !hasActiveFilters)) && (
105
+ <InputGroupAddon align="inline-end">
106
+ {value && (
107
+ <InputGroupButton size="sm" variant="ghost" onClick={() => onChange("")} className="text-[var(--color-text-muted)] hover:text-[var(--color-text-default)]">
108
+ <XIcon className="size-4" />
109
+ </InputGroupButton>
110
+ )}
111
+ {hasFilterCategories && !hasActiveFilters && (
112
+ <>
113
+ {!value && kbdHint && <Kbd>{kbdHint}</Kbd>}
114
+ <span className="h-4 w-px bg-[var(--color-border-default)]" />
115
+ <FilterCombobox
116
+ categories={filterCategories}
117
+ onApply={onApplyFilter ?? (() => {})}
118
+ activeFilters={activeFilters}
119
+ dropdownAlign="right"
120
+ trigger={({ onClick }) => (
121
+ <InputGroupButton size="sm" variant="ghost" onClick={onClick} className="text-[var(--color-text-muted)] hover:text-[var(--color-text-default)]">
122
+ Advanced search
123
+ </InputGroupButton>
124
+ )}
125
+ />
126
+ </>
127
+ )}
128
+ </InputGroupAddon>
129
+ )}
130
+ </InputGroup>
131
+
132
+ {/* Filter bar — visible only when filters are active */}
133
+ {hasActiveFilters && (
134
+ <div className="flex flex-wrap items-center gap-3 [border-bottom-left-radius:var(--radius-card)] [border-bottom-right-radius:var(--radius-card)] border border-t-0 border-[var(--color-border-default)] bg-[var(--color-bg-raised)] px-3 py-2.5">
135
+ {Object.entries(activeFilters)
136
+ .filter(([, vals]) => vals.length > 0)
137
+ .map(([categoryId, values]) => {
138
+ const cat = filterCategories.find(c => c.id === categoryId)
139
+ return (
140
+ <FilterChip
141
+ key={categoryId}
142
+ label={cat?.label ?? categoryId}
143
+ values={values.map(v => ({
144
+ label: filterValueLabel(categoryId, v),
145
+ onRemove: () => onRemoveFilter?.(categoryId, v),
146
+ }))}
147
+ onLabelClick={() => {}}
148
+ />
149
+ )
150
+ })}
151
+ <FilterCombobox
152
+ categories={filterCategories}
153
+ onApply={onApplyFilter ?? (() => {})}
154
+ activeFilters={activeFilters}
155
+ trigger={({ onClick }) => (
156
+ <Button variant="default" size="sm" className="h-7 text-caption gap-1.5 px-2.5" onClick={onClick}>
157
+ <PlusIcon className="size-3.5" />
158
+ Add
159
+ </Button>
160
+ )}
161
+ />
162
+ <FilterChip
163
+ variant="clear-all"
164
+ className="ml-auto"
165
+ onClear={onClearFilters ?? (() => {})}
166
+ />
167
+ </div>
168
+ )}
169
+
170
+ {/* Results dropdown — consumer-provided slot */}
171
+ {children}
172
+ </div>
173
+ )
174
+ }
175
+
176
+ // ── Exports ────────────────────────────────────────────────────────────────────
177
+
178
+ export { SearchBar }
179
+ export type { SearchBarProps }
package/src/index.ts CHANGED
@@ -5,9 +5,11 @@ export { cn } from "./lib/utils"
5
5
  export { usePlatform } from "./hooks/use-platform"
6
6
 
7
7
  // Components
8
+ export * from "./components/ui/alert"
8
9
  export * from "./components/ui/badge"
9
- export * from "./components/ui/cell"
10
10
  export * from "./components/ui/breadcrumb"
11
+ export * from "./components/ui/calendar"
12
+ export * from "./components/ui/cell"
11
13
  export * from "./components/ui/button"
12
14
  export * from "./components/ui/button-group"
13
15
  export * from "./components/ui/card"
@@ -16,6 +18,9 @@ export * from "./components/ui/combobox"
16
18
  export * from "./components/ui/data-table"
17
19
  export * from "./components/ui/dialog"
18
20
  export * from "./components/ui/dropdown-menu"
21
+ export * from "./components/ui/filter-chip"
22
+ export * from "./components/ui/filter-combobox"
23
+ export * from "./components/ui/search-bar"
19
24
  export * from "./components/ui/input"
20
25
  export * from "./components/ui/input-group"
21
26
  export * from "./components/ui/kbd"
@@ -218,9 +218,10 @@
218
218
  --primitive-shadow-xl: 0 20px 25px oklch(0 0 0 / 0.10), 0 8px 10px oklch(0 0 0 / 0.04);
219
219
 
220
220
  /* --- Z-index scale --- */
221
- --primitive-z-base: 0;
222
- --primitive-z-raised: 10;
223
- --primitive-z-overlay: 100;
221
+ --primitive-z-base: 0;
222
+ --primitive-z-raised: 10;
223
+ --primitive-z-dropdown: 20;
224
+ --primitive-z-overlay: 100;
224
225
  --primitive-z-modal: 200;
225
226
  --primitive-z-toast: 300;
226
227
  --primitive-z-tooltip: 400;
@@ -3,6 +3,11 @@
3
3
  utilities are available alongside the base text styles defined in globals.css.
4
4
 
5
5
  Does NOT set font-family — that's the consuming app's responsibility.
6
+
7
+ Serif variants come in two tiers:
8
+ base — mirrors the sans scale exactly (same pixel size)
9
+ -lg — one primitive step up, to compensate for serif's optical smallness
10
+ Weight is fixed at 400 — display serifs typically have no bold cut.
6
11
  ────────────────────────────────────────────────────────────────────────── */
7
12
  @layer utilities {
8
13
  .text-body-lg-light {
@@ -41,4 +46,139 @@
41
46
  line-height: var(--primitive-line-height-normal);
42
47
  letter-spacing: var(--primitive-letter-spacing-normal);
43
48
  }
49
+
50
+ /* ── Serif variants ──────────────────────────────────────────────────────
51
+ base: same size as the sans equivalent
52
+ -lg: one primitive step up for optical compensation */
53
+
54
+ /* Headings — base */
55
+ .text-heading-1-serif {
56
+ font-size: var(--primitive-font-size-4xl); /* 36px */
57
+ font-weight: var(--primitive-font-weight-400);
58
+ line-height: var(--primitive-line-height-tight);
59
+ letter-spacing: var(--primitive-letter-spacing-normal);
60
+ font-family: var(--font-serif);
61
+ }
62
+ .text-heading-2-serif {
63
+ font-size: var(--primitive-font-size-3xl); /* 30px */
64
+ font-weight: var(--primitive-font-weight-400);
65
+ line-height: var(--primitive-line-height-snug);
66
+ letter-spacing: var(--primitive-letter-spacing-normal);
67
+ font-family: var(--font-serif);
68
+ }
69
+ .text-heading-3-serif {
70
+ font-size: var(--primitive-font-size-2xl); /* 24px */
71
+ font-weight: var(--primitive-font-weight-400);
72
+ line-height: var(--primitive-line-height-snug);
73
+ letter-spacing: var(--primitive-letter-spacing-normal);
74
+ font-family: var(--font-serif);
75
+ }
76
+ .text-heading-4-serif {
77
+ font-size: var(--primitive-font-size-xl); /* 20px */
78
+ font-weight: var(--primitive-font-weight-400);
79
+ line-height: var(--primitive-line-height-snug);
80
+ letter-spacing: var(--primitive-letter-spacing-normal);
81
+ font-family: var(--font-serif);
82
+ }
83
+
84
+ /* Headings — lg */
85
+ .text-heading-1-lg-serif {
86
+ font-size: var(--primitive-font-size-5xl); /* 48px */
87
+ font-weight: var(--primitive-font-weight-400);
88
+ line-height: var(--primitive-line-height-tight);
89
+ letter-spacing: var(--primitive-letter-spacing-normal);
90
+ font-family: var(--font-serif);
91
+ }
92
+ .text-heading-2-lg-serif {
93
+ font-size: var(--primitive-font-size-4xl); /* 36px */
94
+ font-weight: var(--primitive-font-weight-400);
95
+ line-height: var(--primitive-line-height-snug);
96
+ letter-spacing: var(--primitive-letter-spacing-normal);
97
+ font-family: var(--font-serif);
98
+ }
99
+ .text-heading-3-lg-serif {
100
+ font-size: var(--primitive-font-size-3xl); /* 30px */
101
+ font-weight: var(--primitive-font-weight-400);
102
+ line-height: var(--primitive-line-height-snug);
103
+ letter-spacing: var(--primitive-letter-spacing-normal);
104
+ font-family: var(--font-serif);
105
+ }
106
+ .text-heading-4-lg-serif {
107
+ font-size: var(--primitive-font-size-2xl); /* 24px */
108
+ font-weight: var(--primitive-font-weight-400);
109
+ line-height: var(--primitive-line-height-snug);
110
+ letter-spacing: var(--primitive-letter-spacing-normal);
111
+ font-family: var(--font-serif);
112
+ }
113
+
114
+ /* Body — base */
115
+ .text-body-lg-serif {
116
+ font-size: var(--primitive-font-size-lg); /* 18px */
117
+ font-weight: var(--primitive-font-weight-400);
118
+ line-height: var(--primitive-line-height-normal);
119
+ letter-spacing: var(--primitive-letter-spacing-normal);
120
+ font-family: var(--font-serif);
121
+ }
122
+ .text-body-serif {
123
+ font-size: var(--primitive-font-size-base); /* 16px */
124
+ font-weight: var(--primitive-font-weight-400);
125
+ line-height: var(--primitive-line-height-normal);
126
+ letter-spacing: var(--primitive-letter-spacing-normal);
127
+ font-family: var(--font-serif);
128
+ }
129
+ .text-body-sm-serif {
130
+ font-size: var(--primitive-font-size-sm); /* 14px */
131
+ font-weight: var(--primitive-font-weight-400);
132
+ line-height: var(--primitive-line-height-normal);
133
+ letter-spacing: var(--primitive-letter-spacing-normal);
134
+ font-family: var(--font-serif);
135
+ }
136
+
137
+ /* Body — lg */
138
+ .text-body-xl-serif {
139
+ font-size: var(--primitive-font-size-xl); /* 20px — body-lg stepped up */
140
+ font-weight: var(--primitive-font-weight-400);
141
+ line-height: var(--primitive-line-height-normal);
142
+ letter-spacing: var(--primitive-letter-spacing-normal);
143
+ font-family: var(--font-serif);
144
+ }
145
+ .text-body-sm-lg-serif {
146
+ font-size: var(--primitive-font-size-base); /* 16px — body-sm stepped up */
147
+ font-weight: var(--primitive-font-weight-400);
148
+ line-height: var(--primitive-line-height-normal);
149
+ letter-spacing: var(--primitive-letter-spacing-normal);
150
+ font-family: var(--font-serif);
151
+ }
152
+
153
+ /* Caption — base */
154
+ .text-caption-serif {
155
+ font-size: var(--primitive-font-size-xs); /* 12px */
156
+ font-weight: var(--primitive-font-weight-400);
157
+ line-height: var(--primitive-line-height-normal);
158
+ letter-spacing: var(--primitive-letter-spacing-normal);
159
+ font-family: var(--font-serif);
160
+ }
161
+ .text-caption-sm-serif {
162
+ font-size: var(--primitive-font-size-2xs); /* 10px */
163
+ font-weight: var(--primitive-font-weight-400);
164
+ line-height: var(--primitive-line-height-normal);
165
+ letter-spacing: var(--primitive-letter-spacing-normal);
166
+ font-family: var(--font-serif);
167
+ }
168
+
169
+ /* Caption — lg */
170
+ .text-caption-lg-serif {
171
+ font-size: var(--primitive-font-size-sm); /* 14px — caption stepped up */
172
+ font-weight: var(--primitive-font-weight-400);
173
+ line-height: var(--primitive-line-height-normal);
174
+ letter-spacing: var(--primitive-letter-spacing-normal);
175
+ font-family: var(--font-serif);
176
+ }
177
+ .text-caption-sm-lg-serif {
178
+ font-size: var(--primitive-font-size-xs); /* 12px — caption-sm stepped up */
179
+ font-weight: var(--primitive-font-weight-400);
180
+ line-height: var(--primitive-line-height-normal);
181
+ letter-spacing: var(--primitive-letter-spacing-normal);
182
+ font-family: var(--font-serif);
183
+ }
44
184
  }