@kolkrabbi/kol-shell 0.48.0 → 0.50.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.
Files changed (2) hide show
  1. package/package.json +3 -3
  2. package/src/CatalogPage.jsx +69 -13
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolkrabbi/kol-shell",
3
- "version": "0.48.0",
3
+ "version": "0.50.0",
4
4
  "private": false,
5
5
  "description": "KOL application shell — fixed 48px NavRail + AppShell layout root, PageShell/PageHeader scaffolds, ContentFilters catalog organism, GridCard, SettingsScaffold, WalkthroughPanel, ShortcutsOverlay. App chrome (kol-framework owns site chrome). Nav items, content, shortcuts and settings are consumer-injected. Sits above @kolkrabbi/kol-{theme,component,framework}.",
6
6
  "license": "MIT",
@@ -12,7 +12,7 @@
12
12
  "./src/*": "./src/*"
13
13
  },
14
14
  "peerDependencies": {
15
- "@kolkrabbi/kol-component": ">=0.174.0",
15
+ "@kolkrabbi/kol-component": ">=0.178.0",
16
16
  "@kolkrabbi/kol-icons": ">=0.16.0",
17
17
  "@kolkrabbi/kol-theme": ">=0.138.0",
18
18
  "gsap": "^3.13.0",
@@ -20,8 +20,8 @@
20
20
  "react-dom": "^18.3.0 || ^19.0.0"
21
21
  },
22
22
  "devDependencies": {
23
+ "@kolkrabbi/kol-component": "^0.178.0",
23
24
  "@kolkrabbi/kol-icons": "^0.25.0",
24
- "@kolkrabbi/kol-component": "^0.177.0",
25
25
  "@kolkrabbi/kol-framework": "^0.42.0",
26
26
  "@kolkrabbi/kol-theme": "^0.138.0"
27
27
  },
@@ -1,5 +1,5 @@
1
1
  import { useCallback, useRef, useState } from 'react'
2
- import { ContentCard, ContentFilters, ContentRow } from '@kolkrabbi/kol-component'
2
+ import { ActionButton, ContentCard, ContentFilters, ContentRow, SizeOrDownload, formatSize } from '@kolkrabbi/kol-component'
3
3
  import PageShell from './PageShell.jsx'
4
4
  import { PageHeader } from '@kolkrabbi/kol-component'
5
5
  import WalkthroughPanel from './WalkthroughPanel.jsx'
@@ -37,6 +37,11 @@ import WalkthroughPanel from './WalkthroughPanel.jsx'
37
37
  * variant's A4, so nothing existing moves. Anything `ContentFilters` takes that the page has no
38
38
  * prop for goes through `filtersProps` — `mutuallyExclusiveFilters` was the gap.
39
39
  *
40
+ * @param {'catalog'|'shelf'} preset the whole page as one word (default `catalog` = today's defaults). `shelf` = a
41
+ * shelf of slide decks: capped · 3 tracks on a 280 floor · `slide` card and row · stacked
42
+ * list · `kol-tone-secondary` on the root · the All / Recent view strip; `toCard` returns
43
+ * the deck's fields and handlers — `title date bytes count cover href onNavigate onDownload
44
+ * onFavourite onDelete favourited` — and the page renders the slots. Explicit props win.
40
45
  * @param {Object} header PageHeader props — `{ title, subtitle, size, voice, eyebrow }`
41
46
  * @param {Array} items the objects `ContentFilters` filters and searches
42
47
  * @param {Function} toCard (item, { view, layout }) => card props (see above)
@@ -53,7 +58,8 @@ import WalkthroughPanel from './WalkthroughPanel.jsx'
53
58
  * `bleed` (default) fills the window — the app tier; `capped` centres on the
54
59
  * framework container — the site tier, so a brand book adopting this page keeps its geometry
55
60
  * @param {ReactNode} trailingActions the header's RIGHT slot (ContentFilters' — MediaLibrary's SELECT / FLAT); never empty beside the divider
56
- * @param {number} minColumn the grid track's floor in px (default 160); a shelf of three decks passes 280 the six-track ceiling still applies
61
+ * @param {number} minColumn the grid track's floor in px (default 160) — a minimum, never a count
62
+ * @param {number} maxColumns the grid's ceiling (default 6, the ruling); a three-deck shelf passes 3 and the tracks share the width
57
63
  * @param {string} cardVariant the grid card's variant, per page (default `catalog`); `toCard` may return `variant` per card
58
64
  * @param {'catalog'|'file'} rowVariant the list row (default `catalog`, the 36px no-thumb row); `file` = the 48px thumb row, for a catalog whose grid shows a cover —
59
65
  * its slots (`date` · `size` · `meta` · `selected`) come from `toCard` like everything else
@@ -81,18 +87,64 @@ function computeHiddenSet(items, expandedIdx) {
81
87
  return hiddenSet
82
88
  }
83
89
 
90
+ /* PRESETS — a whole page frozen as one word (slide-variant-and-shelf-preset,
91
+ * kol-client-olina 2026-09-03; user, after building /slide-deck by hand through
92
+ * eleven props: "this is a look I DO NOT want to do this again… this is a
93
+ * layout SET" · "freeze this so I can use AS IS again"). `catalog` is today's
94
+ * defaults, byte-identical. `shelf` is olina's /slide-deck as it stood, every
95
+ * value read off the render. An explicit prop always wins over its preset. */
96
+ const PRESETS = {
97
+ catalog: {},
98
+ shelf: {
99
+ width: 'capped', maxColumns: 3, minColumn: 280,
100
+ cardVariant: 'slide', rowVariant: 'slide', listLayout: 'stack',
101
+ /* one word tones every control on the page (tone-is-the-ground-axis) */
102
+ className: 'kol-tone-secondary',
103
+ /* the header's right slot is the view strip — ContentFilters' own idiom */
104
+ views: [{ value: 'all', label: 'All' }, { value: 'recent', label: 'Recent' }], defaultView: 'all',
105
+ },
106
+ }
107
+ const stop = (e) => { e.preventDefault(); e.stopPropagation() }
108
+ /* the shelf's DATA → the slide variant's slots, the media admin's idiom
109
+ * (MediaLibraryPages:704) verbatim from olina's /slide-deck: download is the
110
+ * control ON the image (chrome="media"), the plate carries star (toggle) and
111
+ * trash (confirm) in a column, the row carries download · star · trash inline;
112
+ * the size cell is SizeOrDownload; `count` is the slide count. A slot the
113
+ * consumer supplies itself is left alone. The consumer writes no JSX for
114
+ * actions — that is the point of the preset. */
115
+ function toSlideCard(c, { layout }) {
116
+ const { bytes, count, cover, favourited, onDownload, onFavourite, onDelete, downloadHref, ...rest } = c
117
+ const list = layout === 'list'
118
+ return {
119
+ ...rest,
120
+ media: rest.media ?? (cover ? <img src={cover} alt="" loading="lazy" /> : null),
121
+ size: rest.size ?? (bytes != null ? <SizeOrDownload href={downloadHref ?? '#'}>{formatSize(bytes)}</SizeOrDownload> : undefined),
122
+ meta: rest.meta ?? (count ? `${count} slides` : undefined),
123
+ control: rest.control ?? (!list && onDownload ? <ActionButton chrome="media" icon="download" confirmIcon="check" label="Download" confirmLabel="Downloaded" onAction={onDownload} /> : undefined),
124
+ actions: rest.actions ?? ((onFavourite || onDelete || (list && onDownload)) ? (
125
+ <div className={list ? 'flex items-center gap-2' : 'flex h-full flex-col items-center justify-between'} onClick={stop}>
126
+ {list && onDownload && <ActionButton chrome="inline" size="sm" icon="download" label="Download" onAction={onDownload} />}
127
+ {onFavourite && <ActionButton chrome="inline" size="sm" icon={favourited ? 'star-solid' : 'star'} label="Favourite" toggle onAction={onFavourite} />}
128
+ {onDelete && <ActionButton chrome="inline" size="sm" icon="trash" confirmIcon="check" label="Delete" confirmLabel="Deleted" onAction={onDelete} />}
129
+ </div>
130
+ ) : undefined),
131
+ }
132
+ }
133
+
84
134
  export default function CatalogPage({
135
+ /* FIRST, so every default below can read it */
136
+ preset = 'catalog',
85
137
  header,
86
- width = 'bleed',
138
+ width = PRESETS[preset]?.width ?? 'bleed',
87
139
  items = [],
88
140
  toCard,
89
141
  filtersTitle,
90
142
  filterGroups,
91
143
  searchKeys = ['title', 'name'],
92
- views,
144
+ views = PRESETS[preset]?.views,
93
145
  view,
94
146
  onViewChange,
95
- defaultView,
147
+ defaultView = PRESETS[preset]?.defaultView,
96
148
  layouts = LAYOUTS,
97
149
  defaultLayout = 'grid',
98
150
  walkthrough,
@@ -103,20 +155,24 @@ export default function CatalogPage({
103
155
  trailingActions,
104
156
  /* the grid track's floor. 160 is a 26-tile catalog's; a shelf of three decks
105
157
  wants ~280 so the cards read at all. A ceiling of 6 tracks still applies. */
106
- minColumn = 160,
158
+ minColumn = PRESETS[preset]?.minColumn ?? 160,
159
+ /* the ceiling — the six-column ruling is a MAXIMUM; a three-deck shelf says 3
160
+ (kol-client-olina 2026-09-03; user: "why are the cards so small? they could
161
+ use 2 columns each" — raising the floor to force it was backwards) */
162
+ maxColumns = PRESETS[preset]?.maxColumns ?? 6,
107
163
  /* the LIST row's variant. `catalog` (default) is the 36px GridCard row — title
108
164
  and detail on one line, no thumb — which is what every app-tier list was
109
165
  ruled on (CatalogPageMonitorParity). A catalog whose grid shows a cover
110
166
  passes `file`: the 48px thumb row, so the list shows the same cover small
111
167
  (kol-client-olina 2026-09-03; user, on the bare row: "ugly ugly ugly"). */
112
- rowVariant = 'catalog',
168
+ rowVariant = PRESETS[preset]?.rowVariant ?? 'catalog',
113
169
  /* the GRID card's variant, per page; `toCard` may return `variant` per card */
114
- cardVariant = 'catalog',
170
+ cardVariant = PRESETS[preset]?.cardVariant ?? 'catalog',
115
171
  /* the LIST's container. `grid` (default) is monitor's ruling — rows four across
116
172
  on a 240 floor, gap 8 (the documented `repeat(4, 1fr)`). `stack` is one row per
117
173
  line, which is what a list of files is (MediaLibrary stacks); olina's deck list
118
174
  rendered three 300px rows side by side (user: "why is it tiny?"). */
119
- listLayout = 'grid',
175
+ listLayout = PRESETS[preset]?.listLayout ?? 'grid',
120
176
  iconComponent,
121
177
  showCountOnlyWhenFiltering = true,
122
178
  filtersProps,
@@ -142,15 +198,15 @@ export default function CatalogPage({
142
198
  observer.current?.disconnect()
143
199
  observer.current = null
144
200
  if (!el) return
145
- const measure = (W) => setCols((c) => { const n = Math.max(1, Math.min(6, Math.floor((W + 24) / (minColumn + 24)))); return c === n ? c : n })
201
+ const measure = (W) => setCols((c) => { const n = Math.max(1, Math.min(maxColumns, Math.floor((W + 24) / (minColumn + 24)))); return c === n ? c : n })
146
202
  measure(el.getBoundingClientRect().width)
147
203
  const ro = new ResizeObserver(([entry]) => measure(entry.contentRect.width))
148
204
  ro.observe(el)
149
205
  observer.current = ro
150
- }, [minColumn])
206
+ }, [minColumn, maxColumns])
151
207
 
152
208
  return (
153
- <PageShell width={width} className={className} style={{ overflow: 'hidden', '--kol-catalog-n': cols ?? undefined, ...style }}>
209
+ <PageShell width={width} className={`${PRESETS[preset]?.className ?? ''} ${className}`.trim()} style={{ overflow: 'hidden', '--kol-catalog-n': cols ?? undefined, ...style }}>
154
210
  {header && <PageHeader {...header} />}
155
211
  <div style={{ flex: 1, position: 'relative' }}>
156
212
  {open && <WalkthroughPanel steps={walkthrough.steps} iconComponent={walkthrough.iconComponent} />}
@@ -171,7 +227,7 @@ export default function CatalogPage({
171
227
  defaultLayout={defaultLayout}
172
228
  renderItem={(rows, viewMode, layout) => {
173
229
  if (open) return null
174
- const cards = rows.map((item) => toCard ? toCard(item, { view: viewMode, layout }) : { title: item.title ?? item.name, detail: item.detail })
230
+ const cards = rows.map((item) => { const c = toCard ? toCard(item, { view: viewMode, layout }) : { title: item.title ?? item.name, detail: item.detail }; return preset === 'shelf' ? toSlideCard(c, { layout }) : c })
175
231
  const hidden = layout === 'list' ? new Set() : computeHiddenSet(rows, cards.findIndex((c) => c.expanded))
176
232
  /* COLS IS A CEILING, NOT A COMMAND (CatalogPageMobileColumns,
177
233
  * kol-monitor 2026-09-01) — the third home of the defect