@kolkrabbi/kol-component 0.163.0 → 0.165.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.163.0",
3
+ "version": "0.165.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",
@@ -94,11 +94,15 @@ const COL_ICON = { image: 'image', video: 'video', audio: 'file', playlist: 'vid
94
94
 
95
95
  /* ColumnBrowserChromeCorrections (kol-r2b2 2026-08-28, four user rulings measured live on admin.):
96
96
  * every column keeps its right edge, the last included — without it the browser reads as an
97
- * open-sided container when the columns do not fill the box (Finder closes it); a column's ONLY
98
- * row keeps its bottom hairline (`only:`) its last row is also its first, and a missing
99
- * hairline beside neighbours that show one reads as a defect; and ONE ink for folders and files
100
- * the `bg-fg-04` fill alone marks selection ("why is folder full opacity but not files?").
101
- * `muted` stays a prop for other callers; the file rows no longer pass it.
97
+ * open-sided container when the columns do not fill the box (Finder closes it); and ONE ink for
98
+ * folders and files the fill alone marks selection ("why is folder full opacity but not
99
+ * files?"). `muted` stays a prop for other callers; the file rows no longer pass it.
100
+ * NO ROW DIVIDERS (BrowsePageRulingsAndSeams, kol-r2b2 2026-09-02): the `border-b last:border-b-0
101
+ * only:border-b` utilities and their inline `borderColor` are gone. The row is a PILL now — a
102
+ * constant 4px inset on every row so selecting never shifts the layout, radius-sm on the fill —
103
+ * and a hairline under a rounded pill draws the box the pill is trying not to be. The shape lives
104
+ * in kol-components-molecules.css with the fill, not as utilities racing it (ARCHITECTURE §5);
105
+ * this supersedes the `only:` hairline half of the 08-28 corrections, by the same repo's ruling.
102
106
  * STATE CLASSES (ColumnBrowserSeams, kol-r2b2 2026-08-28): `is-selected` / `is-cursor` on the row,
103
107
  * and the fill LIVES IN THE THEME now (kol-components-molecules.css) instead of a `bg-fg-04`
104
108
  * utility — the only hook a consumer had for "selected" was that utility, and a restyle that
@@ -111,11 +115,10 @@ function Row({ icon, label, active, cursor = false, trailing, onClick, muted = f
111
115
  /* Row metrics are the DS Table's (kol-components-organisms.css .kol-table-cell-*):
112
116
  * 12px 16px padding, mono 12, an oq-08 hairline between rows, none after the last. */
113
117
  /* `cursor` = the keyboard row, drawn with the hover fill so ↑/↓ always shows where you are. */
114
- className={`kol-column-browser-row flex items-center gap-2 px-4 py-3 border-b last:border-b-0 only:border-b cursor-pointer transition-colors${
118
+ className={`kol-column-browser-row flex items-center gap-2 px-4 py-3 cursor-pointer transition-colors${
115
119
  active ? ' is-selected' : ''}${cursor ? ' is-cursor' : ''} ${
116
120
  active || cursor || !muted ? 'text-fg-default' : 'text-fg-48'
117
121
  }`}
118
- style={{ borderColor: 'var(--kol-oq-08)' }}
119
122
  onClick={onClick}
120
123
  >
121
124
  <span className="w-5 shrink-0 flex items-center justify-center text-fg-48">
@@ -34,6 +34,10 @@ import IconFrame from '../atoms/IconFrame.jsx'
34
34
  * @param {ReactNode} props.belowActions — the RIGHT half of the below-divider row, beside the count
35
35
  * (kol-r2b2 2026-08-27: the sort group, once SELECT/FLAT moved up into the header strip)
36
36
  * @param {ReactNode} props.trailingActions — the header's RIGHT slot, where the view strip sits
37
+ * @param {'auto'|'header'|'below'} props.trailingPlacement — WHERE `trailingActions` sit (the showcase's icons page,
38
+ * 2026-09-02: brand's ground + guide cluster in this slot measured 223px and scrolled `main` sideways at 390):
39
+ * `auto` (default) the header from `md`, their OWN line under the divider below it — full width, wrapping; `header` / `below` pin it.
40
+ * The same rung `viewPlacement` and LIST/GRID have.
37
41
  * (kol-r2b2 2026-08-27: a consumer's own controls — sort, flat, select — belong there;
38
42
  * `headerActions` is the left group beside search and was never that)
39
43
  * @param {Function} props.onFilterChange — optional callback when filters change
@@ -101,6 +105,7 @@ const ContentFilters = ({
101
105
  searchKeys = ['label', 'name', 'title', 'type'],
102
106
  headerActions,
103
107
  trailingActions,
108
+ trailingPlacement = 'auto',
104
109
  leadingActions,
105
110
  belowActions,
106
111
  showCountOnlyWhenFiltering = false,
@@ -346,7 +351,7 @@ const ContentFilters = ({
346
351
  * for exactly this (lobby ruling 2026-07-30 — "icons only, NO
347
352
  * states"); the span here was the same defect that promoted it. */}
348
353
  {/* the icon gap matches the specimen header — 16 from md (FoundrySpecimenSections, 2026-08-27) */}
349
- <h2 className="flex items-center gap-2 md:gap-4">
354
+ <h2 className={`flex items-center gap-2 md:gap-4 ${searchOpen ? 'max-md:hidden' : ''}`}>
350
355
  {titleIcon && <IconFrame name={titleIcon} variant="secondary" size="md" />}
351
356
  {/* `pr-4` by rule (ContentFiltersTitleGap, kol-website 2026-08-27): the
352
357
  * divider sat 24px from the title's text edge but 32 from the
@@ -359,8 +364,13 @@ const ContentFilters = ({
359
364
  * spending 40px of a 390px viewport on one divider. */}
360
365
  <span className={`${titleClassName} pr-2 md:pr-4`} style={titleUppercase ? { textTransform: 'uppercase', letterSpacing: 1 } : undefined}>{title}</span>
361
366
  </h2>
362
- <Divider variant="vertical" />
363
- <div className="flex items-center gap-1">
367
+ <Divider variant="vertical" className={searchOpen ? 'max-md:hidden' : ''} />
368
+ {/* THE OPEN SEARCH TAKES THE ROW BELOW `md` (2026-09-02, the showcase
369
+ * held to its own law): the pill is a fixed 200px, and beside the
370
+ * title at 390 it scrolled `main` sideways. Title and divider step
371
+ * aside while searching and return on close; the group grows to the
372
+ * row so the pill fits. */}
373
+ <div className="flex items-center gap-1 min-w-0 max-md:flex-1">
364
374
  {/* IconFrame, not a kol-btn with its chrome cancelled inline.
365
375
  * 05-control-chrome.md:109 — "any icon-only control in chrome is
366
376
  * IconFrame; nothing hand-writes the square". This wore
@@ -405,6 +415,7 @@ const ContentFilters = ({
405
415
  value={searchText}
406
416
  onChange={(e) => setSearchText(e.target.value)}
407
417
  expandedWidth={200}
418
+ className="max-w-full"
408
419
  triggerLabel="Search"
409
420
  /* NO placeholder. The field opens from a glyph you just clicked —
410
421
  * the caret is the affordance, and a greyed "Search…" sitting in
@@ -433,10 +444,17 @@ const ContentFilters = ({
433
444
  </div>
434
445
 
435
446
  <div className="flex items-center gap-6">
436
- {trailingActions}
437
- {/* The divider between a consumer's trailing controls and the strip is
438
- * the organism's, as it is on the left between title and icons. */}
439
- {trailingActions && layoutPlacement === 'header' && layoutStrip && <Divider variant="vertical" />}
447
+ {/* the consumer's trailing controls ride the header from `md` under
448
+ * `auto`; below it they move to the below-divider row (same rung as
449
+ * the view strip and LIST/GRID) so the header row never scrolls */}
450
+ {trailingActions && trailingPlacement !== 'below' && (
451
+ <div className={`${trailingPlacement === 'auto' ? 'hidden md:flex' : 'flex'} items-center gap-6`}>
452
+ {trailingActions}
453
+ {/* The divider between a consumer's trailing controls and the strip is
454
+ * the organism's, as it is on the left between title and icons. */}
455
+ {layoutPlacement === 'header' && layoutStrip && <Divider variant="vertical" />}
456
+ </div>
457
+ )}
440
458
  {layoutPlacement === 'header' && layoutStrip}
441
459
  {/* RECENT / SAVED is the SAME STRIP as LIST / GRID, not a ViewToggle.
442
460
  * Read off kol-monitor's original (_tmp/2026-08-15-shell-adoption/
@@ -466,6 +484,14 @@ const ContentFilters = ({
466
484
  {viewStrip('flex flex-wrap gap-y-2')}
467
485
  </div>
468
486
  )}
487
+ {/* the consumer's trailing controls: their OWN line below md (`auto`) or
488
+ * always (`below`) — full width, wrapping. In the below row's right
489
+ * group they measured 223px beside LIST/GRID in a 342px track. */}
490
+ {trailingActions && trailingPlacement !== 'header' && (
491
+ <div className={`${trailingPlacement === 'auto' ? 'md:hidden ' : ''}mb-3 flex flex-wrap items-center gap-6 gap-y-2`}>
492
+ {trailingActions}
493
+ </div>
494
+ )}
469
495
 
470
496
  {/* BELOW the divider: the filter GROUPS only. Left-aligned columns —
471
497
  * label above values — visible only while the filter toggle is open.
@@ -223,13 +223,13 @@ function MediaInspector({ files, index, onClose, onPrev, onNext, mediaUrl, downl
223
223
  /* Display settings for the ACTIVE bucket — kol-r2b2's drawer wiring on the DS
224
224
  * organism (the composition the user locked 2026-08-27). Every control sets a
225
225
  * default, never a gate. */
226
- function MediaSettings({ bucketMeta, settings, onChange, onReset, onClose, profile }) {
226
+ function MediaSettings({ bucketMeta, settings, onChange, onReset, onClose, profile, settingsFooter }) {
227
227
  const set = (patch) => onChange({ ...settings, ...patch })
228
228
  const toggleKind = (k) => set({ kinds: settings.kinds.includes(k) ? settings.kinds.filter((x) => x !== k) : [...settings.kinds, k] })
229
229
  const noVariants = profile.variantSets === 0
230
230
  const noSegments = profile.segments === 0
231
231
  return (
232
- <SettingsPanel variant="drawer" title="Display settings" onClose={onClose} footer={<SettingsFooter onReset={onReset} />}>
232
+ <SettingsPanel variant="drawer" title="Display settings" onClose={onClose} footer={<SettingsFooter onReset={onReset}>{settingsFooter}</SettingsFooter>}>
233
233
  <LabeledControlSection label="Structure" rowGap={1} divided>
234
234
  <SettingsRow label="Columns" hint="Finder-style columns instead of folder rows">
235
235
  <SettingsSwitch label="Columns" on={(settings.folderView ?? 'rows') === 'columns'} onChange={(v) => set({ folderView: v ? 'columns' : 'rows' })} />
@@ -299,7 +299,7 @@ const profileOf = (objects, rawFiles, systemCount) => ({
299
299
  /* ══ BROWSE — folder / files ═══════════════════════════════════════════════ */
300
300
  export function MediaLibraryBrowse({
301
301
  client, title = 'MEDIA', bucket, onBucketChange, prefix: prefixProp, onPrefix, defaults, settings: settingsProp, onSettingsChange,
302
- folderTree, headerActions, refreshKey, onOpen, autoFocus = false, className = '',
302
+ folderTree, headerActions, refreshKey, onOpen, autoFocus = false, settingsFooter, className = '',
303
303
  }) {
304
304
  const [ownPrefix, setOwnPrefix] = useState('')
305
305
  const prefix = prefixProp ?? ownPrefix
@@ -399,7 +399,7 @@ export function MediaLibraryBrowse({
399
399
  onNext={() => setQuickLook((q) => ({ ...q, index: (q.index + 1) % q.files.length }))} />
400
400
  )}
401
401
  {settingsOpen && (
402
- <MediaSettings bucketMeta={bucketMeta} settings={settings} profile={profile} onChange={setSettings} onReset={() => setSettings(null)} onClose={() => setSettingsOpen(false)} />
402
+ <MediaSettings bucketMeta={bucketMeta} settings={settings} profile={profile} onChange={setSettings} onReset={() => setSettings(null)} onClose={() => setSettingsOpen(false)} settingsFooter={settingsFooter} />
403
403
  )}
404
404
 
405
405
  {folderView === 'columns' ? (
@@ -468,7 +468,7 @@ export function MediaLibraryBrowse({
468
468
  /* ══ LIBRARY — the content-filters wall ═══════════════════════════════════ */
469
469
  export function MediaLibraryLibrary({
470
470
  client, title = 'MEDIA', bucket, onBucketChange, prefix = '', defaults, settings: settingsProp, onSettingsChange,
471
- headerActions, refreshKey, header = true, stats = true, className = '',
471
+ headerActions, refreshKey, header = true, stats = true, settingsFooter, className = '',
472
472
  }) {
473
473
  const [ownBucket, setOwnBucket] = useState(bucket)
474
474
  const bucketId = bucket ?? ownBucket
@@ -633,7 +633,7 @@ export function MediaLibraryLibrary({
633
633
  onBucket={(v) => { if (v === 'all') return; if (bucket == null) setOwnBucket(v); onBucketChange?.(v) }} onSettings={() => setSettingsOpen(true)} />
634
634
  )}
635
635
  {settingsOpen && (
636
- <MediaSettings bucketMeta={bucketMeta} settings={settings} profile={profile} onChange={setSettings} onReset={() => setSettings(null)} onClose={() => setSettingsOpen(false)} />
636
+ <MediaSettings bucketMeta={bucketMeta} settings={settings} profile={profile} onChange={setSettings} onReset={() => setSettings(null)} onClose={() => setSettingsOpen(false)} settingsFooter={settingsFooter} />
637
637
  )}
638
638
 
639
639
  {/* `stats={false}` (ColumnBrowserSeams, kol-r2b2 2026-08-28) — beside `header={false}`: a browse
@@ -238,12 +238,23 @@ export function SettingsChipRow({ options = [], selected = [], onToggle, allChip
238
238
 
239
239
  /** SettingsFooter — a Divider, then one `IconFrame refresh` (primary · sm) at the
240
240
  * right = reset to defaults (SettingsPanelApproved, 2026-08-27). No status word,
241
- * no text button. `customised` is accepted for compatibility and unused. */
242
- export function SettingsFooter({ onReset, resetLabel = 'Reset to defaults' }) {
241
+ * no text button. `customised` is accepted for compatibility and unused.
242
+ *
243
+ * `children` ride the SAME row, before reset (BrowsePageRulingsAndSeams,
244
+ * kol-r2b2 2026-09-02): a consumer wanting one more control in that footer had
245
+ * no way in, so kol-r2b2 ran a `MutationObserver` on `document.body` watching
246
+ * for `[aria-label="Reset to defaults"]` and portalled a theme chip into its
247
+ * parent — a copy string in a `querySelector`, watching the whole document, to
248
+ * place one button. The slot is the seam that ends it.
249
+ *
250
+ * `gap-2` on the row (user 2026-08-28): `flex justify-end` with no gap is right
251
+ * for one control and wrong for two — the chip and the reset icon touched. */
252
+ export function SettingsFooter({ onReset, resetLabel = 'Reset to defaults', children }) {
243
253
  return (
244
254
  <div className="flex flex-col gap-4">
245
255
  <Divider />
246
- <div className="flex justify-end">
256
+ <div className="flex justify-end items-center gap-2">
257
+ {children}
247
258
  {onReset && <IconFrame name="refresh" variant="primary" size="sm" onClick={onReset} title={resetLabel} aria-label={resetLabel} />}
248
259
  </div>
249
260
  </div>