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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minka-ds",
3
- "version": "0.3.2",
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
- return (
59
- <span
60
- className={cn(
61
- "text-body-sm text-[var(--color-text-default)] tabular-nums tracking-tight",
62
- className
63
- )}
64
- >
65
- {children}
66
- </span>
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: () => void }[]
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
- <span
54
- key={i}
55
- 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)]"
56
- >
57
- {value.label}
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={value.onRemove}
61
- className="text-[var(--color-text-muted)] hover:text-[var(--color-text-default)] transition-colors"
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
- <XIcon className="size-3" />
75
+ {value.label}
65
76
  </button>
66
- </span>
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 = selectedCategory?.type === "date"
215
- const isAmount = selectedCategory?.type === "amount"
216
- const isHours = selectedCategory?.type === "hours"
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-action-ghost-hover)]",
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"