minka-ds 0.3.2 → 0.3.3
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 +4 -1
- package/src/components/ui/cell.tsx +14 -10
- package/src/components/ui/dialog.tsx +1 -1
- package/src/components/ui/filter-chip.tsx +24 -13
- package/src/components/ui/filter-combobox.tsx +59 -6
- package/src/components/ui/search-bar.tsx +11 -1
- package/src/components/ui/tab-count.tsx +23 -0
- package/src/components/ui/table.tsx +1 -1
- package/src/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "minka-ds",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "Minka product design system — tokenized component library",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"files": [
|
|
@@ -26,5 +26,8 @@
|
|
|
26
26
|
"sonner": "^2.0.7",
|
|
27
27
|
"tailwind-merge": "^3.5.0",
|
|
28
28
|
"tw-animate-css": "^1.4.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"culori": "^4.0.2"
|
|
29
32
|
}
|
|
30
33
|
}
|
|
@@ -55,16 +55,20 @@ interface AmountCellProps {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
function AmountCell({ children, className }: AmountCellProps) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
{
|
|
66
|
-
|
|
67
|
-
|
|
58
|
+
const base = cn("text-body-sm text-[var(--color-text-default)] tabular-nums", className)
|
|
59
|
+
|
|
60
|
+
// PP Neue Montreal's $ glyph has a wide right sidebearing that creates a
|
|
61
|
+
// visible gap before digits. Split it into its own box with a negative
|
|
62
|
+
// margin so we can close just that gap without touching digit spacing.
|
|
63
|
+
if (typeof children === "string" && children.startsWith("$")) {
|
|
64
|
+
return (
|
|
65
|
+
<span className={base}>
|
|
66
|
+
<span className="inline-block -mr-[0.05em]">$</span>{children.slice(1)}
|
|
67
|
+
</span>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return <span className={base}>{children}</span>
|
|
68
72
|
}
|
|
69
73
|
|
|
70
74
|
// ── BadgeCell ─────────────────────────────────────────────────────────────────
|
|
@@ -39,7 +39,7 @@ function DialogOverlay({
|
|
|
39
39
|
<DialogPrimitive.Overlay
|
|
40
40
|
data-slot="dialog-overlay"
|
|
41
41
|
className={cn(
|
|
42
|
-
"fixed inset-0 [z-index:var(--z-modal)] bg-[var(--color-bg-backdrop)] data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0",
|
|
42
|
+
"fixed inset-0 [z-index:var(--z-modal)] bg-[var(--color-bg-backdrop-blur)] backdrop-blur-sm data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0",
|
|
43
43
|
className
|
|
44
44
|
)}
|
|
45
45
|
{...props}
|
|
@@ -8,7 +8,7 @@ type FilterChipProps =
|
|
|
8
8
|
| {
|
|
9
9
|
variant?: "filter"
|
|
10
10
|
label: string
|
|
11
|
-
values: { label: string; onRemove
|
|
11
|
+
values: { label: string; onRemove?: () => void }[]
|
|
12
12
|
onLabelClick?: () => void
|
|
13
13
|
className?: string
|
|
14
14
|
}
|
|
@@ -49,22 +49,33 @@ function FilterChip(props: FilterChipProps) {
|
|
|
49
49
|
>
|
|
50
50
|
{label}:
|
|
51
51
|
</button>
|
|
52
|
-
{values.map((value, i) =>
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
52
|
+
{values.map((value, i) =>
|
|
53
|
+
value.onRemove ? (
|
|
54
|
+
<span
|
|
55
|
+
key={i}
|
|
56
|
+
className="inline-flex items-center gap-1 rounded-full border border-[var(--color-border-default)] bg-[var(--color-bg-base)] px-2 py-0.5 text-caption text-[var(--color-text-default)]"
|
|
57
|
+
>
|
|
58
|
+
{value.label}
|
|
59
|
+
<button
|
|
60
|
+
type="button"
|
|
61
|
+
onClick={value.onRemove}
|
|
62
|
+
className="text-[var(--color-text-muted)] hover:text-[var(--color-text-default)] transition-colors"
|
|
63
|
+
aria-label={`Remove ${value.label}`}
|
|
64
|
+
>
|
|
65
|
+
<XIcon className="size-3" />
|
|
66
|
+
</button>
|
|
67
|
+
</span>
|
|
68
|
+
) : (
|
|
58
69
|
<button
|
|
70
|
+
key={i}
|
|
59
71
|
type="button"
|
|
60
|
-
onClick={
|
|
61
|
-
className="text-[var(--color-text-
|
|
62
|
-
aria-label={`Remove ${value.label}`}
|
|
72
|
+
onClick={onLabelClick}
|
|
73
|
+
className="inline-flex items-center gap-1 rounded-full border border-[var(--color-border-default)] bg-[var(--color-bg-base)] px-2 py-0.5 text-caption text-[var(--color-text-default)] hover:border-[var(--color-border-strong)] transition-colors"
|
|
63
74
|
>
|
|
64
|
-
|
|
75
|
+
{value.label}
|
|
65
76
|
</button>
|
|
66
|
-
|
|
67
|
-
)
|
|
77
|
+
)
|
|
78
|
+
)}
|
|
68
79
|
</div>
|
|
69
80
|
)
|
|
70
81
|
}
|
|
@@ -15,20 +15,22 @@ import { Button } from "./button"
|
|
|
15
15
|
import { Calendar } from "./calendar"
|
|
16
16
|
import { Input } from "./input"
|
|
17
17
|
import { Tabs, TabsList, TabsTrigger } from "./tabs"
|
|
18
|
+
import { DateTimeRangePicker, type DateTimeRange } from "./date-time-range-picker"
|
|
18
19
|
|
|
19
20
|
// ── Types ──────────────────────────────────────────────────────────────────────
|
|
20
21
|
|
|
21
22
|
interface FilterCategory {
|
|
22
23
|
id: string
|
|
23
24
|
label: string
|
|
24
|
-
type?: "list" | "date" | "amount" | "hours"
|
|
25
|
+
type?: "list" | "date" | "amount" | "hours" | "datetime"
|
|
25
26
|
values?: string[]
|
|
27
|
+
maxRangeDays?: number
|
|
26
28
|
renderValue?: (value: string) => React.ReactNode
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
type AmountValue = { exact: number } | { min?: number; max?: number }
|
|
30
32
|
type HoursValue = { from: string; to: string }
|
|
31
|
-
type CategoryValue = string | DateRange | AmountValue | HoursValue
|
|
33
|
+
type CategoryValue = string | DateRange | AmountValue | HoursValue | DateTimeRange
|
|
32
34
|
|
|
33
35
|
|
|
34
36
|
type Step = 1 | 2 | 3
|
|
@@ -63,6 +65,7 @@ function FilterCombobox({
|
|
|
63
65
|
const [amountMax, setAmountMax] = React.useState("")
|
|
64
66
|
const [hoursInput, setHoursInput] = React.useState("")
|
|
65
67
|
const [hoursInputTo, setHoursInputTo] = React.useState("")
|
|
68
|
+
const [datetimeValue, setDatetimeValue] = React.useState<DateTimeRange | null>(null)
|
|
66
69
|
const [search, setSearch] = React.useState("")
|
|
67
70
|
|
|
68
71
|
const containerRef = React.useRef<HTMLDivElement>(null)
|
|
@@ -97,6 +100,7 @@ function FilterCombobox({
|
|
|
97
100
|
setAmountMax("")
|
|
98
101
|
setHoursInput("")
|
|
99
102
|
setHoursInputTo("")
|
|
103
|
+
setDatetimeValue(null)
|
|
100
104
|
}
|
|
101
105
|
|
|
102
106
|
function handleToggle() {
|
|
@@ -134,6 +138,18 @@ function FilterCombobox({
|
|
|
134
138
|
return
|
|
135
139
|
}
|
|
136
140
|
|
|
141
|
+
if (cat.type === "datetime") {
|
|
142
|
+
const custom = existing.find((v): v is DateTimeRange =>
|
|
143
|
+
typeof v === "object" && "startTime" in v
|
|
144
|
+
) ?? null
|
|
145
|
+
setSelectedCategory(cat)
|
|
146
|
+
setDatetimeValue(custom)
|
|
147
|
+
setSelectedValues(new Set())
|
|
148
|
+
setSearch("")
|
|
149
|
+
setStep(3)
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
|
|
137
153
|
if (cat.type === "amount") {
|
|
138
154
|
const custom = existing.find((v): v is AmountValue =>
|
|
139
155
|
typeof v === "object" && ("exact" in v || "min" in v || "max" in v)
|
|
@@ -177,6 +193,12 @@ function FilterCombobox({
|
|
|
177
193
|
handleClose()
|
|
178
194
|
}
|
|
179
195
|
|
|
196
|
+
function applyDatetime() {
|
|
197
|
+
if (!selectedCategory || !datetimeValue?.from || !datetimeValue?.to) return
|
|
198
|
+
onApply(selectedCategory.id, [datetimeValue])
|
|
199
|
+
handleClose()
|
|
200
|
+
}
|
|
201
|
+
|
|
180
202
|
function applyCustomHours() {
|
|
181
203
|
if (!selectedCategory || !hoursInput || !hoursInputTo) return
|
|
182
204
|
onApply(selectedCategory.id, [{ from: hoursInput, to: hoursInputTo }])
|
|
@@ -211,9 +233,10 @@ function FilterCombobox({
|
|
|
211
233
|
|
|
212
234
|
// ── Derived values ───────────────────────────────────────────────────────────
|
|
213
235
|
|
|
214
|
-
const isDate
|
|
215
|
-
const isAmount
|
|
216
|
-
const isHours
|
|
236
|
+
const isDate = selectedCategory?.type === "date"
|
|
237
|
+
const isAmount = selectedCategory?.type === "amount"
|
|
238
|
+
const isHours = selectedCategory?.type === "hours"
|
|
239
|
+
const isDatetime = selectedCategory?.type === "datetime"
|
|
217
240
|
|
|
218
241
|
const step2AllValues = selectedCategory?.values ?? []
|
|
219
242
|
|
|
@@ -242,7 +265,7 @@ function FilterCombobox({
|
|
|
242
265
|
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)]",
|
|
243
266
|
"bg-[var(--color-bg-overlay)] shadow-[var(--shadow-popover)] ring-1 ring-[var(--color-border-subtle)]",
|
|
244
267
|
"[z-index:var(--z-floating)]",
|
|
245
|
-
step === 3 && isDate ? "w-auto" : step === 3 && isHours ? "w-80" : "w-56"
|
|
268
|
+
step === 3 && (isDate || isDatetime) ? "w-auto" : step === 3 && isHours ? "w-80" : "w-56"
|
|
246
269
|
)}>
|
|
247
270
|
|
|
248
271
|
{/* Step 1 — category list (multi-category mode only) */}
|
|
@@ -365,6 +388,35 @@ function FilterCombobox({
|
|
|
365
388
|
</>
|
|
366
389
|
)}
|
|
367
390
|
|
|
391
|
+
{/* Step 3 — datetime range */}
|
|
392
|
+
{step === 3 && isDatetime && (
|
|
393
|
+
<>
|
|
394
|
+
{!isSingle && (
|
|
395
|
+
<StepHeader
|
|
396
|
+
title={selectedCategory?.label ?? "Date range"}
|
|
397
|
+
onBack={() => setStep(1)}
|
|
398
|
+
/>
|
|
399
|
+
)}
|
|
400
|
+
<div className="p-1">
|
|
401
|
+
<DateTimeRangePicker
|
|
402
|
+
value={datetimeValue}
|
|
403
|
+
onChange={setDatetimeValue}
|
|
404
|
+
maxRangeDays={selectedCategory?.maxRangeDays}
|
|
405
|
+
/>
|
|
406
|
+
</div>
|
|
407
|
+
<div className="p-1">
|
|
408
|
+
<Button
|
|
409
|
+
size="sm"
|
|
410
|
+
className="w-full"
|
|
411
|
+
disabled={!datetimeValue?.from || !datetimeValue?.to}
|
|
412
|
+
onClick={applyDatetime}
|
|
413
|
+
>
|
|
414
|
+
Apply
|
|
415
|
+
</Button>
|
|
416
|
+
</div>
|
|
417
|
+
</>
|
|
418
|
+
)}
|
|
419
|
+
|
|
368
420
|
{/* Step 3 — custom amount */}
|
|
369
421
|
{step === 3 && isAmount && (
|
|
370
422
|
<>
|
|
@@ -520,3 +572,4 @@ function EmptyRow() {
|
|
|
520
572
|
|
|
521
573
|
export { FilterCombobox }
|
|
522
574
|
export type { FilterCategory, CategoryValue, AmountValue, HoursValue }
|
|
575
|
+
export type { DateTimeRange } from "./date-time-range-picker"
|
|
@@ -5,6 +5,7 @@ import { PlusIcon, SearchIcon, XIcon } from "lucide-react"
|
|
|
5
5
|
import type { DateRange } from "react-day-picker"
|
|
6
6
|
|
|
7
7
|
import { cn } from "../../lib/utils"
|
|
8
|
+
import type { DateTimeRange } from "./date-time-range-picker"
|
|
8
9
|
import { Button } from "./button"
|
|
9
10
|
import { FilterChip } from "./filter-chip"
|
|
10
11
|
import { FilterCombobox } from "./filter-combobox"
|
|
@@ -14,8 +15,17 @@ import { Kbd } from "./kbd"
|
|
|
14
15
|
|
|
15
16
|
// ── Default label formatter ────────────────────────────────────────────────────
|
|
16
17
|
|
|
18
|
+
function formatTime(t: string): string {
|
|
19
|
+
return t || "00:00"
|
|
20
|
+
}
|
|
21
|
+
|
|
17
22
|
function defaultFilterValueLabel(_categoryId: string, value: CategoryValue): string {
|
|
18
23
|
if (typeof value === "string") return value
|
|
24
|
+
if (typeof value === "object" && "startTime" in value) {
|
|
25
|
+
const v = value as DateTimeRange
|
|
26
|
+
const fmtDate = (d: Date) => d.toLocaleDateString("default", { month: "short", day: "numeric" })
|
|
27
|
+
return `${fmtDate(v.from)} ${formatTime(v.startTime)} – ${fmtDate(v.to)} ${formatTime(v.endTime)}`
|
|
28
|
+
}
|
|
19
29
|
if (typeof value === "object" && "from" in value) {
|
|
20
30
|
const v = value as DateRange
|
|
21
31
|
if (!v.from) return ""
|
|
@@ -158,7 +168,7 @@ function SearchBar({
|
|
|
158
168
|
label={cat.label}
|
|
159
169
|
values={activeVals.map(v => ({
|
|
160
170
|
label: filterValueLabel(cat.id, v),
|
|
161
|
-
onRemove: () => onRemoveFilter?.(cat.id, v),
|
|
171
|
+
onRemove: cat.type === "datetime" ? undefined : () => onRemoveFilter?.(cat.id, v),
|
|
162
172
|
}))}
|
|
163
173
|
onLabelClick={onClick}
|
|
164
174
|
/>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cn } from "../../lib/utils"
|
|
3
|
+
|
|
4
|
+
interface TabCountProps {
|
|
5
|
+
count: number
|
|
6
|
+
className?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function TabCount({ count, className }: TabCountProps) {
|
|
10
|
+
return (
|
|
11
|
+
<span
|
|
12
|
+
className={cn(
|
|
13
|
+
"inline-flex items-center justify-center rounded-full bg-[var(--color-bg-disabled)] text-[var(--color-text-default)] text-[10px] font-semibold leading-none size-4 shrink-0",
|
|
14
|
+
className
|
|
15
|
+
)}
|
|
16
|
+
>
|
|
17
|
+
{count}
|
|
18
|
+
</span>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export { TabCount }
|
|
23
|
+
export type { TabCountProps }
|
|
@@ -73,7 +73,7 @@ function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
|
|
73
73
|
<tr
|
|
74
74
|
data-slot="table-row"
|
|
75
75
|
className={cn(
|
|
76
|
-
"border-b border-[var(--color-border-subtle)] transition-colors hover:bg-[var(--color-bg-table-hover)] has-aria-expanded:bg-[var(--color-bg-table-hover)] data-[state=selected]:bg-[var(--color-
|
|
76
|
+
"border-b border-[var(--color-border-subtle)] transition-colors hover:bg-[var(--color-bg-table-hover)] has-aria-expanded:bg-[var(--color-bg-table-hover)] data-[state=selected]:bg-[var(--color-bg-table-selected)]",
|
|
77
77
|
className
|
|
78
78
|
)}
|
|
79
79
|
{...props}
|
package/src/index.ts
CHANGED
|
@@ -36,6 +36,7 @@ export * from "./components/ui/sheet"
|
|
|
36
36
|
export * from "./components/ui/sidebar"
|
|
37
37
|
export * from "./components/ui/skeleton"
|
|
38
38
|
export * from "./components/ui/table"
|
|
39
|
+
export * from "./components/ui/tab-count"
|
|
39
40
|
export * from "./components/ui/tabs"
|
|
40
41
|
export * from "./components/ui/textarea"
|
|
41
42
|
export * from "./components/ui/tooltip"
|