@kolkrabbi/kol-component 0.56.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolkrabbi/kol-component",
3
- "version": "0.56.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, useRef, useEffect } from 'react'
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)
@@ -177,6 +184,21 @@ const ContentFilters = ({
177
184
  * /work: a short closed set like Type reads as a column you scan down, while
178
185
  * ~45 tags must wrap or they run off the page. One shape for both meant the
179
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
+
180
202
  const renderFilterGroup = (group) => (
181
203
  <div key={group.key} className={`flex flex-col gap-3 ${group.stack ? 'shrink-0' : 'min-w-0 flex-1'}`}>
182
204
  {/* THE CATEGORY LABEL IS THE ACTIVE INK — `kol-helper-12` at `text-fg-96`,
@@ -240,46 +262,24 @@ const ContentFilters = ({
240
262
  * stop and the one ActionButton's docstring already names. */}
241
263
  <IconSeam name="filter" size={glyphSize('md', true)} />
242
264
  </button>
243
- <div
244
- className="kol-expand flex items-center justify-center rounded-sm cursor-pointer hover:bg-fg-04"
245
- style={{
246
- height: 32,
247
- width: searchOpen ? 200 : 32,
248
- background: searchOpen ? 'var(--kol-surface-secondary)' : 'transparent',
249
- border: 'none',
250
- overflow: 'hidden',
251
- }}
252
- onClick={() => {
253
- if (searchOpen) { setSearchOpen(false); setSearchText('') }
254
- else setSearchOpen(true)
255
- }}
256
- >
257
- {/* Same fake-button defect as the title icon: a decorative glyph
258
- * wearing kol-btn chrome. The clickable thing is the wrapper
259
- * div, not this span. */}
260
- <span
261
- className="kol-expand-content flex items-center justify-center flex-shrink-0"
262
- style={{
263
- opacity: searchOpen ? 0 : 1,
264
- position: searchOpen ? 'absolute' : 'relative',
265
- }}
266
- >
267
- <IconSeam name="search" size={glyphSize('md', true)} />
268
- </span>
269
- {searchOpen && (
270
- <input
271
- ref={searchRef}
272
- type="text"
273
- value={searchText}
274
- onChange={(e) => setSearchText(e.target.value)}
275
- onClick={(e) => e.stopPropagation()}
276
- placeholder=""
277
- className="bg-transparent outline-none kol-helper-12 flex-1 text-fg-80 caret-current px-4"
278
- onBlur={() => { if (!searchText) setSearchOpen(false) }}
279
- onKeyDown={(e) => { if (e.key === 'Escape') { setSearchOpen(false); setSearchText('') } }}
280
- />
281
- )}
282
- </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
+ />
283
283
  {headerActions}
284
284
  </div>
285
285
  {activeFilters.size > 0 && (
@@ -294,34 +294,7 @@ const ContentFilters = ({
294
294
  </div>
295
295
 
296
296
  <div className="flex items-center gap-8">
297
- {/* The count and LIST/GRID ride the HEADER row, above the divider.
298
- *
299
- * "N of N" appears ONLY while the filter panel is open (user ruling
300
- * 2026-08-15) — with nothing filtered it always reads "12 of 12",
301
- * which is a number that has never once told anyone anything. It
302
- * earns its place the moment a filter can change it.
303
- *
304
- * Ink stays `text-fg-64`: static information, not a toggle, so it
305
- * takes neither the 96 active nor the 32 rest. */}
306
- {showCount && isExpanded && (
307
- <span className={countClassName} style={{ letterSpacing: 1 }}>
308
- {filteredItems.length} of {totalCount}
309
- </span>
310
- )}
311
- {layoutOptions && (
312
- <div className="flex items-center gap-4">
313
- {layoutOptions.map((opt) => (
314
- <span
315
- key={opt.value}
316
- onClick={() => setLayout(opt.value)}
317
- className={`${layoutClassName} cursor-pointer select-none ${layout === opt.value ? stripActiveClassName : stripRestClassName}`}
318
- style={{ letterSpacing: 1 }}
319
- >
320
- {opt.label}
321
- </span>
322
- ))}
323
- </div>
324
- )}
297
+ {layoutPlacement === 'header' && layoutStrip}
325
298
  {/* RECENT / SAVED is the SAME STRIP as LIST / GRID, not a ViewToggle.
326
299
  * Read off kol-monitor's original (_tmp/2026-08-15-shell-adoption/
327
300
  * ContentFilters.jsx:225-232): inline spans, `kol-helper-14`,
@@ -360,9 +333,9 @@ const ContentFilters = ({
360
333
  * expanding filter group if it is not in the same row as one. */}
361
334
  {isExpanded && (
362
335
  <div className="flex items-start justify-between gap-16 pb-4">
363
- <div className="flex items-start gap-16">
364
- {isExpanded && filterGroups.map((group) => renderFilterGroup(group))}
365
- {isExpanded && activeFilters.size > 0 && (
336
+ <div className="flex min-w-0 flex-1 items-start gap-16">
337
+ {filterGroups.map((group) => renderFilterGroup(group))}
338
+ {activeFilters.size > 0 && (
366
339
  <button
367
340
  type="button"
368
341
  onClick={clearAllFilters}
@@ -373,6 +346,21 @@ const ContentFilters = ({
373
346
  </button>
374
347
  )}
375
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>
376
364
  </div>
377
365
  )}
378
366