minka-ds 0.1.5 → 0.1.8

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.1.5",
3
+ "version": "0.1.8",
4
4
  "description": "Minka product design system — tokenized component library",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -52,7 +52,7 @@ function FilterChip(props: FilterChipProps) {
52
52
  {values.map((value, i) => (
53
53
  <span
54
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)]"
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
56
  >
57
57
  {value.label}
58
58
  <button
@@ -21,13 +21,14 @@ import { Tabs, TabsList, TabsTrigger } from "./tabs"
21
21
  interface FilterCategory {
22
22
  id: string
23
23
  label: string
24
- type?: "list" | "date" | "amount"
24
+ type?: "list" | "date" | "amount" | "hours"
25
25
  values?: string[]
26
26
  renderValue?: (value: string) => React.ReactNode
27
27
  }
28
28
 
29
29
  type AmountValue = { exact: number } | { min?: number; max?: number }
30
- type CategoryValue = string | DateRange | AmountValue
30
+ type HoursValue = { from: string; to: string }
31
+ type CategoryValue = string | DateRange | AmountValue | HoursValue
31
32
 
32
33
 
33
34
  type Step = 1 | 2 | 3
@@ -58,10 +59,13 @@ function FilterCombobox({
58
59
  const [amountExact, setAmountExact] = React.useState("")
59
60
  const [amountMin, setAmountMin] = React.useState("")
60
61
  const [amountMax, setAmountMax] = React.useState("")
62
+ const [hoursInput, setHoursInput] = React.useState("")
63
+ const [hoursInputTo, setHoursInputTo] = React.useState("")
61
64
  const [search, setSearch] = React.useState("")
62
65
 
63
66
  const containerRef = React.useRef<HTMLDivElement>(null)
64
67
  const searchRef = React.useRef<HTMLInputElement>(null)
68
+ const isSingle = categories.length === 1
65
69
 
66
70
  // Close on outside click
67
71
  React.useEffect(() => {
@@ -89,6 +93,20 @@ function FilterCombobox({
89
93
  setAmountExact("")
90
94
  setAmountMin("")
91
95
  setAmountMax("")
96
+ setHoursInput("")
97
+ setHoursInputTo("")
98
+ }
99
+
100
+ function handleToggle() {
101
+ if (open) {
102
+ handleClose()
103
+ return
104
+ }
105
+ // Single-category: skip step 1 and jump straight to options
106
+ if (isSingle) {
107
+ openCategory(categories[0])
108
+ }
109
+ setOpen(true)
92
110
  }
93
111
 
94
112
  function openCategory(cat: FilterCategory) {
@@ -104,6 +122,16 @@ function FilterCombobox({
104
122
  return
105
123
  }
106
124
 
125
+ if (cat.type === "hours") {
126
+ setSelectedCategory(cat)
127
+ setSelectedValues(new Set())
128
+ setHoursInput("")
129
+ setHoursInputTo("")
130
+ setSearch("")
131
+ setStep(2)
132
+ return
133
+ }
134
+
107
135
  if (cat.type === "amount") {
108
136
  const custom = existing.find((v): v is AmountValue =>
109
137
  typeof v === "object" && ("exact" in v || "min" in v || "max" in v)
@@ -147,6 +175,12 @@ function FilterCombobox({
147
175
  handleClose()
148
176
  }
149
177
 
178
+ function applyCustomHours() {
179
+ if (!selectedCategory || !hoursInput || !hoursInputTo) return
180
+ onApply(selectedCategory.id, [{ from: hoursInput, to: hoursInputTo }])
181
+ handleClose()
182
+ }
183
+
150
184
  function applyCustomAmount() {
151
185
  if (!selectedCategory) return
152
186
  let value: AmountValue
@@ -177,6 +211,7 @@ function FilterCombobox({
177
211
 
178
212
  const isDate = selectedCategory?.type === "date"
179
213
  const isAmount = selectedCategory?.type === "amount"
214
+ const isHours = selectedCategory?.type === "hours"
180
215
 
181
216
  const step2AllValues = selectedCategory?.values ?? []
182
217
 
@@ -193,8 +228,8 @@ function FilterCombobox({
193
228
 
194
229
  return (
195
230
  <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)}>
231
+ {trigger ? trigger({ open, onClick: handleToggle }) : (
232
+ <Button variant="default" size="sm" className="h-7 text-caption gap-1.5 px-2.5" onClick={handleToggle}>
198
233
  <PlusIcon className="size-3.5" />
199
234
  Add filter
200
235
  </Button>
@@ -204,11 +239,11 @@ function FilterCombobox({
204
239
  <div className={cn(
205
240
  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
241
  "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"
242
+ "[z-index:var(--z-floating)]",
243
+ step === 3 && isDate ? "w-auto" : step === 3 && isHours ? "w-80" : "w-56"
209
244
  )}>
210
245
 
211
- {/* Step 1 — category list */}
246
+ {/* Step 1 — category list (multi-category mode only) */}
212
247
  {step === 1 && (
213
248
  <ul className="p-1">
214
249
  {categories.map(cat => (
@@ -222,10 +257,12 @@ function FilterCombobox({
222
257
  {/* Step 2 — value list */}
223
258
  {step === 2 && selectedCategory && (
224
259
  <>
225
- <StepHeader
226
- title={selectedCategory.label}
227
- onBack={() => { setStep(1); setSearch("") }}
228
- />
260
+ {!isSingle && (
261
+ <StepHeader
262
+ title={selectedCategory.label}
263
+ onBack={() => { setStep(1); setSearch("") }}
264
+ />
265
+ )}
229
266
  {showSearch && (
230
267
  <div className="px-1 pt-1">
231
268
  <SearchInput ref={searchRef} value={search} onChange={setSearch} />
@@ -236,17 +273,28 @@ function FilterCombobox({
236
273
  ? <EmptyRow />
237
274
  : step2Filtered.map(value => (
238
275
  <li key={value}>
239
- <CheckRow
240
- checked={selectedValues.has(value)}
241
- onToggle={() => toggleValue(value)}
242
- >
243
- {selectedCategory.renderValue?.(value) ?? value}
244
- </CheckRow>
276
+ {selectedCategory.type === "hours" ? (
277
+ <PickerRow onClick={() => { onApply(selectedCategory.id, [value]); handleClose() }}>
278
+ {selectedCategory.renderValue?.(value) ?? value}
279
+ </PickerRow>
280
+ ) : (
281
+ <CheckRow
282
+ checked={selectedValues.has(value)}
283
+ onToggle={() => toggleValue(value)}
284
+ >
285
+ {selectedCategory.renderValue?.(value) ?? value}
286
+ </CheckRow>
287
+ )}
245
288
  </li>
246
289
  ))
247
290
  }
291
+ {selectedCategory.type === "hours" && (
292
+ <li>
293
+ <PickerRow onClick={() => setStep(3)}>Custom range</PickerRow>
294
+ </li>
295
+ )}
248
296
  </ul>
249
- {selectedValues.size > 0 && (
297
+ {selectedValues.size > 0 && selectedCategory.type !== "hours" && (
250
298
  <div className="p-1">
251
299
  <Button size="sm" className="w-full" onClick={applyValues}>
252
300
  Apply
@@ -259,10 +307,12 @@ function FilterCombobox({
259
307
  {/* Step 3 — custom date range */}
260
308
  {step === 3 && isDate && (
261
309
  <>
262
- <StepHeader
263
- title={selectedCategory?.label ?? "Date"}
264
- onBack={() => setStep(1)}
265
- />
310
+ {!isSingle && (
311
+ <StepHeader
312
+ title={selectedCategory?.label ?? "Date"}
313
+ onBack={() => setStep(1)}
314
+ />
315
+ )}
266
316
  <div className="p-1">
267
317
  <Calendar mode="range" selected={dateRange} onSelect={setDateRange} numberOfMonths={1} />
268
318
  </div>
@@ -276,14 +326,49 @@ function FilterCombobox({
276
326
  </>
277
327
  )}
278
328
 
279
- {/* Step 3 — custom amount (no step 2 for amount — opens here directly) */}
280
- {step === 3 && isAmount && (
329
+ {/* Step 3 — hours custom range */}
330
+ {step === 3 && isHours && (
281
331
  <>
282
332
  <StepHeader
283
- title="Amount"
284
- onBack={() => { setStep(1) }}
333
+ title="Custom range"
334
+ onBack={() => setStep(2)}
285
335
  />
286
- <div className="px-2 pb-2 flex flex-col gap-2">
336
+ <div className="p-2 flex flex-col gap-2">
337
+ <div className="flex items-center gap-2">
338
+ <Input
339
+ type="time"
340
+ value={hoursInput}
341
+ onChange={e => setHoursInput(e.target.value)}
342
+ className="flex-1"
343
+ />
344
+ <span className="text-body-sm text-[var(--color-text-muted)] shrink-0">–</span>
345
+ <Input
346
+ type="time"
347
+ value={hoursInputTo}
348
+ onChange={e => setHoursInputTo(e.target.value)}
349
+ className="flex-1"
350
+ />
351
+ </div>
352
+ {hoursInput !== "" && hoursInputTo !== "" &&
353
+ !isNaN(parseFloat(hoursInput)) && !isNaN(parseFloat(hoursInputTo)) && (
354
+ <Button size="sm" className="w-full" onClick={applyCustomHours}>
355
+ Apply
356
+ </Button>
357
+ )}
358
+ </div>
359
+ </>
360
+ )}
361
+
362
+ {/* Step 3 — custom amount */}
363
+ {step === 3 && isAmount && (
364
+ <>
365
+ {!isSingle && (
366
+ <StepHeader
367
+ title="Amount"
368
+ onBack={() => setStep(1)}
369
+ />
370
+ )}
371
+ <div className="p-2 flex flex-col gap-2">
287
372
  <Tabs
288
373
  value={amountMode}
289
374
  onValueChange={v => setAmountMode(v as "exact" | "range")}
@@ -427,4 +512,4 @@ function EmptyRow() {
427
512
  // ── Exports ────────────────────────────────────────────────────────────────────
428
513
 
429
514
  export { FilterCombobox }
430
- export type { FilterCategory, CategoryValue, AmountValue }
515
+ export type { FilterCategory, CategoryValue, AmountValue, HoursValue }
@@ -52,6 +52,7 @@ interface SearchBarProps {
52
52
  onRemoveFilter?: (categoryId: string, value: CategoryValue) => void
53
53
  onClearFilters?: () => void
54
54
  filterValueLabel?: (categoryId: string, value: CategoryValue) => string
55
+ alwaysShowFilterBar?: boolean
55
56
 
56
57
  children?: React.ReactNode
57
58
  className?: string
@@ -73,11 +74,13 @@ function SearchBar({
73
74
  onRemoveFilter,
74
75
  onClearFilters,
75
76
  filterValueLabel = defaultFilterValueLabel,
77
+ alwaysShowFilterBar = false,
76
78
  children,
77
79
  className,
78
80
  }: SearchBarProps) {
79
81
  const hasActiveFilters = Object.values(activeFilters).some(v => v.length > 0)
80
82
  const hasFilterCategories = filterCategories.length > 0
83
+ const showFilterBar = hasActiveFilters || alwaysShowFilterBar
81
84
 
82
85
  return (
83
86
  <div data-search-bar className={cn("relative flex flex-col", className)}>
@@ -86,7 +89,7 @@ function SearchBar({
86
89
  <InputGroup
87
90
  className={cn(
88
91
  "h-12",
89
- hasActiveFilters && "[border-bottom-left-radius:0] [border-bottom-right-radius:0]"
92
+ showFilterBar && "[border-bottom-left-radius:0] [border-bottom-right-radius:0]"
90
93
  )}
91
94
  >
92
95
  <InputGroupAddon align="inline-start">
@@ -101,15 +104,15 @@ function SearchBar({
101
104
  onFocus={onFocus}
102
105
  autoComplete="off"
103
106
  />
104
- {(!!value || (hasFilterCategories && !hasActiveFilters) || (!value && !!kbdHint)) && (
107
+ {(!!value || (hasFilterCategories && !hasActiveFilters && !alwaysShowFilterBar) || (!value && !!kbdHint)) && (
105
108
  <InputGroupAddon align="inline-end">
106
109
  {value && (
107
110
  <InputGroupButton size="sm" variant="ghost" onClick={() => onChange("")} className="text-[var(--color-text-muted)] hover:text-[var(--color-text-default)]">
108
111
  <XIcon className="size-4" />
109
112
  </InputGroupButton>
110
113
  )}
111
- {!value && kbdHint && !hasFilterCategories && <Kbd>{kbdHint}</Kbd>}
112
- {hasFilterCategories && !hasActiveFilters && (
114
+ {!value && kbdHint && (!hasFilterCategories || alwaysShowFilterBar) && <Kbd>{kbdHint}</Kbd>}
115
+ {hasFilterCategories && !hasActiveFilters && !alwaysShowFilterBar && (
113
116
  <>
114
117
  {!value && kbdHint && <Kbd>{kbdHint}</Kbd>}
115
118
  <span className="h-4 w-px bg-[var(--color-border-default)]" />
@@ -130,41 +133,102 @@ function SearchBar({
130
133
  )}
131
134
  </InputGroup>
132
135
 
133
- {/* Filter bar — visible only when filters are active */}
134
- {hasActiveFilters && (
136
+ {/* Filter bar */}
137
+ {showFilterBar && (
135
138
  <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">
136
- {Object.entries(activeFilters)
137
- .filter(([, vals]) => vals.length > 0)
138
- .map(([categoryId, values]) => {
139
- const cat = filterCategories.find(c => c.id === categoryId)
140
- return (
139
+ {alwaysShowFilterBar ? (
140
+ // Always-open mode: one trigger per category, active ones show chips
141
+ <>
142
+ <span className="text-caption text-[var(--color-text-default)] shrink-0">Filters:</span>
143
+ {filterCategories.map(cat => {
144
+ const activeVals = activeFilters[cat.id] ?? []
145
+ const hasActive = activeVals.length > 0
146
+
147
+ if (hasActive) {
148
+ return (
149
+ <FilterCombobox
150
+ key={cat.id}
151
+ categories={[cat]}
152
+ onApply={onApplyFilter ?? (() => {})}
153
+ activeFilters={activeFilters}
154
+ trigger={({ onClick }) => (
155
+ <FilterChip
156
+ label={cat.label}
157
+ values={activeVals.map(v => ({
158
+ label: filterValueLabel(cat.id, v),
159
+ onRemove: () => onRemoveFilter?.(cat.id, v),
160
+ }))}
161
+ onLabelClick={onClick}
162
+ />
163
+ )}
164
+ />
165
+ )
166
+ }
167
+
168
+ return (
169
+ <FilterCombobox
170
+ key={cat.id}
171
+ categories={[cat]}
172
+ onApply={onApplyFilter ?? (() => {})}
173
+ activeFilters={activeFilters}
174
+ trigger={({ onClick }) => (
175
+ <button
176
+ type="button"
177
+ onClick={onClick}
178
+ 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-muted)] hover:text-[var(--color-text-default)] hover:border-[var(--color-border-hover,var(--color-border-default))] transition-colors"
179
+ >
180
+ {cat.label}
181
+ <PlusIcon className="size-3 shrink-0" />
182
+ </button>
183
+ )}
184
+ />
185
+ )
186
+ })}
187
+ {hasActiveFilters && (
141
188
  <FilterChip
142
- key={categoryId}
143
- label={cat?.label ?? categoryId}
144
- values={values.map(v => ({
145
- label: filterValueLabel(categoryId, v),
146
- onRemove: () => onRemoveFilter?.(categoryId, v),
147
- }))}
148
- onLabelClick={() => {}}
189
+ variant="clear-all"
190
+ className="ml-auto"
191
+ onClear={onClearFilters ?? (() => {})}
149
192
  />
150
- )
151
- })}
152
- <FilterCombobox
153
- categories={filterCategories}
154
- onApply={onApplyFilter ?? (() => {})}
155
- activeFilters={activeFilters}
156
- trigger={({ onClick }) => (
157
- <Button variant="default" size="sm" className="h-7 text-caption gap-1.5 px-2.5" onClick={onClick}>
158
- <PlusIcon className="size-3.5" />
159
- Add
160
- </Button>
161
- )}
162
- />
163
- <FilterChip
164
- variant="clear-all"
165
- className="ml-auto"
166
- onClear={onClearFilters ?? (() => {})}
167
- />
193
+ )}
194
+ </>
195
+ ) : (
196
+ // Legacy mode: active chips + generic Add button
197
+ <>
198
+ {Object.entries(activeFilters)
199
+ .filter(([, vals]) => vals.length > 0)
200
+ .map(([categoryId, values]) => {
201
+ const cat = filterCategories.find(c => c.id === categoryId)
202
+ return (
203
+ <FilterChip
204
+ key={categoryId}
205
+ label={cat?.label ?? categoryId}
206
+ values={values.map(v => ({
207
+ label: filterValueLabel(categoryId, v),
208
+ onRemove: () => onRemoveFilter?.(categoryId, v),
209
+ }))}
210
+ onLabelClick={() => {}}
211
+ />
212
+ )
213
+ })}
214
+ <FilterCombobox
215
+ categories={filterCategories}
216
+ onApply={onApplyFilter ?? (() => {})}
217
+ activeFilters={activeFilters}
218
+ trigger={({ onClick }) => (
219
+ <Button variant="default" size="sm" className="h-7 text-caption gap-1.5 px-2.5" onClick={onClick}>
220
+ <PlusIcon className="size-3.5" />
221
+ Add
222
+ </Button>
223
+ )}
224
+ />
225
+ <FilterChip
226
+ variant="clear-all"
227
+ className="ml-auto"
228
+ onClear={onClearFilters ?? (() => {})}
229
+ />
230
+ </>
231
+ )}
168
232
  </div>
169
233
  )}
170
234
 
@@ -22,6 +22,18 @@
22
22
 
23
23
  /* --- Color scales --- */
24
24
 
25
+ --primitive-beige-50: oklch(0.990 0.008 98);
26
+ --primitive-beige-100: oklch(0.982 0.030 98);
27
+ --primitive-beige-200: oklch(0.968 0.052 98);
28
+ --primitive-beige-300: oklch(0.948 0.072 98);
29
+ --primitive-beige-400: oklch(0.918 0.083 98);
30
+ --primitive-beige-500: oklch(0.876 0.085 98);
31
+ --primitive-beige-600: oklch(0.792 0.076 98);
32
+ --primitive-beige-700: oklch(0.680 0.062 98);
33
+ --primitive-beige-800: oklch(0.480 0.044 98);
34
+ --primitive-beige-900: oklch(0.310 0.028 98);
35
+ --primitive-beige-950: oklch(0.230 0.020 98);
36
+
25
37
  --primitive-red-50: oklch(0.95 0.03 28);
26
38
  --primitive-red-100: oklch(0.93 0.05 28);
27
39
  --primitive-red-200: oklch(0.89 0.07 28);
@@ -58,9 +70,9 @@
58
70
  --primitive-yellow-900: oklch(0.3 0.07 98);
59
71
  --primitive-yellow-950: oklch(0.25 0.06 98);
60
72
 
61
- --primitive-lulo-50: oklch(0.94 0.24 112);
62
- --primitive-lulo-100: oklch(0.92 0.23 112);
63
- --primitive-lulo-200: oklch(0.87 0.22 112);
73
+ --primitive-lulo-50: oklch(0.967 0.099 118);
74
+ --primitive-lulo-100: oklch(0.918 0.136 116);
75
+ --primitive-lulo-200: oklch(0.869 0.173 114);
64
76
  --primitive-lulo-300: oklch(0.82 0.21 112);
65
77
  --primitive-lulo-400: oklch(0.76 0.2 112);
66
78
  --primitive-lulo-500: oklch(0.7 0.18 112);
@@ -106,6 +118,18 @@
106
118
  --primitive-sea-900: oklch(0.29 0.07 220);
107
119
  --primitive-sea-950: oklch(0.24 0.06 220);
108
120
 
121
+ --primitive-slate-50: oklch(0.950 0.012 218);
122
+ --primitive-slate-100: oklch(0.932 0.020 218);
123
+ --primitive-slate-200: oklch(0.898 0.032 218);
124
+ --primitive-slate-300: oklch(0.848 0.042 218);
125
+ --primitive-slate-400: oklch(0.762 0.054 218);
126
+ --primitive-slate-500: oklch(0.656 0.063 218);
127
+ --primitive-slate-600: oklch(0.560 0.054 218);
128
+ --primitive-slate-700: oklch(0.472 0.046 218);
129
+ --primitive-slate-800: oklch(0.352 0.034 218);
130
+ --primitive-slate-900: oklch(0.248 0.023 218);
131
+ --primitive-slate-950: oklch(0.198 0.017 218);
132
+
109
133
  --primitive-blue-50: oklch(0.95 0.03 257);
110
134
  --primitive-blue-100: oklch(0.93 0.04 257);
111
135
  --primitive-blue-200: oklch(0.88 0.06 257);