@kolkrabbi/kol-component 0.55.0 → 0.57.0
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 +1 -1
- package/src/organisms/ContentFilters.jsx +71 -78
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-component",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.57.0",
|
|
4
4
|
"description": "KOL design-system components — atoms through organisms, emitting canonical kol-* classes. Pairs with @kolkrabbi/kol-theme for styling.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { useState, useMemo
|
|
1
|
+
import { useState, useMemo } from 'react'
|
|
2
2
|
import Tag from '../atoms/Tag.jsx'
|
|
3
3
|
import Divider from '../atoms/Divider.jsx'
|
|
4
4
|
import Button from '../atoms/Button.jsx'
|
|
5
5
|
import { Icon } from '@kolkrabbi/kol-icons'
|
|
6
6
|
import { glyphSize } from '../hooks/glyphLadders.js'
|
|
7
|
+
import SearchInput from '../molecules/SearchInput.jsx'
|
|
7
8
|
import IconFrame from '../atoms/IconFrame.jsx'
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -54,6 +55,16 @@ const ContentFilters = ({
|
|
|
54
55
|
defaultViewMode = 'list',
|
|
55
56
|
layoutOptions,
|
|
56
57
|
defaultLayout = 'grid',
|
|
58
|
+
/* WHERE the LIST/GRID strip sits — the two arrangements kol-monitor and
|
|
59
|
+
* kol-website each settled on, made interchangeable (user ruling 2026-08-15).
|
|
60
|
+
*
|
|
61
|
+
* 'below' header carries RECENT/SAVED; LIST/GRID sits below the divider
|
|
62
|
+
* beside the count. monitor's shape, and the default.
|
|
63
|
+
* 'header' LIST/GRID rides the header row instead.
|
|
64
|
+
*
|
|
65
|
+
* "N of N" is below the divider in BOTH — it is a count of what the filters
|
|
66
|
+
* did, so it belongs with them, and it only renders while they are open. */
|
|
67
|
+
layoutPlacement = 'below',
|
|
57
68
|
onFilterChange,
|
|
58
69
|
mutuallyExclusiveFilters = [],
|
|
59
70
|
customFilterKeys = [],
|
|
@@ -89,11 +100,7 @@ const ContentFilters = ({
|
|
|
89
100
|
const [layout, setLayout] = useState(defaultLayout)
|
|
90
101
|
const [searchOpen, setSearchOpen] = useState(false)
|
|
91
102
|
const [searchText, setSearchText] = useState('')
|
|
92
|
-
const searchRef = useRef(null)
|
|
93
103
|
|
|
94
|
-
useEffect(() => {
|
|
95
|
-
if (searchOpen && searchRef.current) searchRef.current.focus()
|
|
96
|
-
}, [searchOpen])
|
|
97
104
|
|
|
98
105
|
const toggleFilter = (filterType, value) => {
|
|
99
106
|
const newFilters = new Set(activeFilters)
|
|
@@ -172,8 +179,28 @@ const ContentFilters = ({
|
|
|
172
179
|
* as the component tier's ONE documented exception to the no-casing law,
|
|
173
180
|
* because a filter value is data with no authoring site. Consumers must not
|
|
174
181
|
* uppercase these themselves. */
|
|
182
|
+
/* A group either STACKS its values in a narrow column or WRAPS them across
|
|
183
|
+
* the room it is given (`group.stack`). Both shapes are live on kol-website's
|
|
184
|
+
* /work: a short closed set like Type reads as a column you scan down, while
|
|
185
|
+
* ~45 tags must wrap or they run off the page. One shape for both meant the
|
|
186
|
+
* short group ate a full row it did not need. */
|
|
187
|
+
const layoutStrip = layoutOptions ? (
|
|
188
|
+
<div className="flex items-center gap-4">
|
|
189
|
+
{layoutOptions.map((opt) => (
|
|
190
|
+
<span
|
|
191
|
+
key={opt.value}
|
|
192
|
+
onClick={() => setLayout(opt.value)}
|
|
193
|
+
className={`${layoutClassName} cursor-pointer select-none ${layout === opt.value ? stripActiveClassName : stripRestClassName}`}
|
|
194
|
+
style={{ letterSpacing: 1 }}
|
|
195
|
+
>
|
|
196
|
+
{opt.label}
|
|
197
|
+
</span>
|
|
198
|
+
))}
|
|
199
|
+
</div>
|
|
200
|
+
) : null
|
|
201
|
+
|
|
175
202
|
const renderFilterGroup = (group) => (
|
|
176
|
-
<div key={group.key} className=
|
|
203
|
+
<div key={group.key} className={`flex flex-col gap-3 ${group.stack ? 'shrink-0' : 'min-w-0 flex-1'}`}>
|
|
177
204
|
{/* THE CATEGORY LABEL IS THE ACTIVE INK — `kol-helper-12` at `text-fg-96`,
|
|
178
205
|
* the same full opacity a SELECTED layout item wears (user ruling
|
|
179
206
|
* 2026-08-15: "TAGS and other categories are ACTIVE state full opacity").
|
|
@@ -187,7 +214,7 @@ const ContentFilters = ({
|
|
|
187
214
|
<h4 className={labelClassName} style={labelUppercase ? { textTransform: 'uppercase' } : undefined}>
|
|
188
215
|
{group.label}
|
|
189
216
|
</h4>
|
|
190
|
-
<div className=
|
|
217
|
+
<div className={group.stack ? 'flex flex-col items-start gap-2' : 'flex flex-wrap gap-2'}>
|
|
191
218
|
{group.values.map((value) => (
|
|
192
219
|
<Tag
|
|
193
220
|
key={value}
|
|
@@ -235,46 +262,24 @@ const ContentFilters = ({
|
|
|
235
262
|
* stop and the one ActionButton's docstring already names. */}
|
|
236
263
|
<IconSeam name="filter" size={glyphSize('md', true)} />
|
|
237
264
|
</button>
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}}
|
|
251
|
-
|
|
252
|
-
{
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
className="kol-expand-content flex items-center justify-center flex-shrink-0"
|
|
257
|
-
style={{
|
|
258
|
-
opacity: searchOpen ? 0 : 1,
|
|
259
|
-
position: searchOpen ? 'absolute' : 'relative',
|
|
260
|
-
}}
|
|
261
|
-
>
|
|
262
|
-
<IconSeam name="search" size={glyphSize('md', true)} />
|
|
263
|
-
</span>
|
|
264
|
-
{searchOpen && (
|
|
265
|
-
<input
|
|
266
|
-
ref={searchRef}
|
|
267
|
-
type="text"
|
|
268
|
-
value={searchText}
|
|
269
|
-
onChange={(e) => setSearchText(e.target.value)}
|
|
270
|
-
onClick={(e) => e.stopPropagation()}
|
|
271
|
-
placeholder=""
|
|
272
|
-
className="bg-transparent outline-none kol-helper-12 flex-1 text-fg-80 caret-current px-4"
|
|
273
|
-
onBlur={() => { if (!searchText) setSearchOpen(false) }}
|
|
274
|
-
onKeyDown={(e) => { if (e.key === 'Escape') { setSearchOpen(false); setSearchText('') } }}
|
|
275
|
-
/>
|
|
276
|
-
)}
|
|
277
|
-
</div>
|
|
265
|
+
{/* THE SEARCH IS `SearchInput expanding` — the DS component, fully
|
|
266
|
+
* round, the same pill the nav shelf uses (user ruling
|
|
267
|
+
* 2026-08-15). This organism hand-rolled its own: a rounded-sm box
|
|
268
|
+
* with its own width animation, its own input, its own Escape and
|
|
269
|
+
* blur handling — a second search field the DS could not see, in
|
|
270
|
+
* a different shape from the one every other surface serves.
|
|
271
|
+
*
|
|
272
|
+
* Open state stays HERE because the filter row reads it (the count
|
|
273
|
+
* shows while searching); SearchInput takes it controlled. */}
|
|
274
|
+
<SearchInput
|
|
275
|
+
expanding
|
|
276
|
+
open={searchOpen}
|
|
277
|
+
onOpenChange={(next) => { setSearchOpen(next); if (!next) setSearchText('') }}
|
|
278
|
+
value={searchText}
|
|
279
|
+
onChange={(e) => setSearchText(e.target.value)}
|
|
280
|
+
expandedWidth={200}
|
|
281
|
+
triggerLabel="Search"
|
|
282
|
+
/>
|
|
278
283
|
{headerActions}
|
|
279
284
|
</div>
|
|
280
285
|
{activeFilters.size > 0 && (
|
|
@@ -289,34 +294,7 @@ const ContentFilters = ({
|
|
|
289
294
|
</div>
|
|
290
295
|
|
|
291
296
|
<div className="flex items-center gap-8">
|
|
292
|
-
{
|
|
293
|
-
*
|
|
294
|
-
* "N of N" appears ONLY while the filter panel is open (user ruling
|
|
295
|
-
* 2026-08-15) — with nothing filtered it always reads "12 of 12",
|
|
296
|
-
* which is a number that has never once told anyone anything. It
|
|
297
|
-
* earns its place the moment a filter can change it.
|
|
298
|
-
*
|
|
299
|
-
* Ink stays `text-fg-64`: static information, not a toggle, so it
|
|
300
|
-
* takes neither the 96 active nor the 32 rest. */}
|
|
301
|
-
{showCount && isExpanded && (
|
|
302
|
-
<span className={countClassName} style={{ letterSpacing: 1 }}>
|
|
303
|
-
{filteredItems.length} of {totalCount}
|
|
304
|
-
</span>
|
|
305
|
-
)}
|
|
306
|
-
{layoutOptions && (
|
|
307
|
-
<div className="flex items-center gap-4">
|
|
308
|
-
{layoutOptions.map((opt) => (
|
|
309
|
-
<span
|
|
310
|
-
key={opt.value}
|
|
311
|
-
onClick={() => setLayout(opt.value)}
|
|
312
|
-
className={`${layoutClassName} cursor-pointer select-none ${layout === opt.value ? stripActiveClassName : stripRestClassName}`}
|
|
313
|
-
style={{ letterSpacing: 1 }}
|
|
314
|
-
>
|
|
315
|
-
{opt.label}
|
|
316
|
-
</span>
|
|
317
|
-
))}
|
|
318
|
-
</div>
|
|
319
|
-
)}
|
|
297
|
+
{layoutPlacement === 'header' && layoutStrip}
|
|
320
298
|
{/* RECENT / SAVED is the SAME STRIP as LIST / GRID, not a ViewToggle.
|
|
321
299
|
* Read off kol-monitor's original (_tmp/2026-08-15-shell-adoption/
|
|
322
300
|
* ContentFilters.jsx:225-232): inline spans, `kol-helper-14`,
|
|
@@ -355,9 +333,9 @@ const ContentFilters = ({
|
|
|
355
333
|
* expanding filter group if it is not in the same row as one. */}
|
|
356
334
|
{isExpanded && (
|
|
357
335
|
<div className="flex items-start justify-between gap-16 pb-4">
|
|
358
|
-
<div className="flex items-start gap-16">
|
|
359
|
-
{
|
|
360
|
-
{
|
|
336
|
+
<div className="flex min-w-0 flex-1 items-start gap-16">
|
|
337
|
+
{filterGroups.map((group) => renderFilterGroup(group))}
|
|
338
|
+
{activeFilters.size > 0 && (
|
|
361
339
|
<button
|
|
362
340
|
type="button"
|
|
363
341
|
onClick={clearAllFilters}
|
|
@@ -368,6 +346,21 @@ const ContentFilters = ({
|
|
|
368
346
|
</button>
|
|
369
347
|
)}
|
|
370
348
|
</div>
|
|
349
|
+
|
|
350
|
+
{/* BELOW the divider: the count, and LIST/GRID when the arrangement
|
|
351
|
+
* puts it here. The count is a report of what the filters did, so it
|
|
352
|
+
* lives with them and appears only while they are open — unfiltered
|
|
353
|
+
* it always reads "N of N", a number that has never told anyone
|
|
354
|
+
* anything. Ink stays `text-fg-64`: static information, not a
|
|
355
|
+
* toggle, so it takes neither the 96 active nor the 32 rest. */}
|
|
356
|
+
<div className="flex flex-shrink-0 flex-col items-end gap-4">
|
|
357
|
+
{showCount && (
|
|
358
|
+
<span className={countClassName} style={{ letterSpacing: 1 }}>
|
|
359
|
+
{filteredItems.length} of {totalCount}
|
|
360
|
+
</span>
|
|
361
|
+
)}
|
|
362
|
+
{layoutPlacement === 'below' && layoutStrip}
|
|
363
|
+
</div>
|
|
371
364
|
</div>
|
|
372
365
|
)}
|
|
373
366
|
|