@kolkrabbi/kol-shell 0.49.0 → 0.51.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-shell",
3
- "version": "0.49.0",
3
+ "version": "0.51.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.179.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)
@@ -82,18 +87,64 @@ function computeHiddenSet(items, expandedIdx) {
82
87
  return hiddenSet
83
88
  }
84
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
+
85
134
  export default function CatalogPage({
135
+ /* FIRST, so every default below can read it */
136
+ preset = 'catalog',
86
137
  header,
87
- width = 'bleed',
138
+ width = PRESETS[preset]?.width ?? 'bleed',
88
139
  items = [],
89
140
  toCard,
90
141
  filtersTitle,
91
142
  filterGroups,
92
143
  searchKeys = ['title', 'name'],
93
- views,
144
+ views = PRESETS[preset]?.views,
94
145
  view,
95
146
  onViewChange,
96
- defaultView,
147
+ defaultView = PRESETS[preset]?.defaultView,
97
148
  layouts = LAYOUTS,
98
149
  defaultLayout = 'grid',
99
150
  walkthrough,
@@ -104,24 +155,24 @@ export default function CatalogPage({
104
155
  trailingActions,
105
156
  /* the grid track's floor. 160 is a 26-tile catalog's; a shelf of three decks
106
157
  wants ~280 so the cards read at all. A ceiling of 6 tracks still applies. */
107
- minColumn = 160,
158
+ minColumn = PRESETS[preset]?.minColumn ?? 160,
108
159
  /* the ceiling — the six-column ruling is a MAXIMUM; a three-deck shelf says 3
109
160
  (kol-client-olina 2026-09-03; user: "why are the cards so small? they could
110
161
  use 2 columns each" — raising the floor to force it was backwards) */
111
- maxColumns = 6,
162
+ maxColumns = PRESETS[preset]?.maxColumns ?? 6,
112
163
  /* the LIST row's variant. `catalog` (default) is the 36px GridCard row — title
113
164
  and detail on one line, no thumb — which is what every app-tier list was
114
165
  ruled on (CatalogPageMonitorParity). A catalog whose grid shows a cover
115
166
  passes `file`: the 48px thumb row, so the list shows the same cover small
116
167
  (kol-client-olina 2026-09-03; user, on the bare row: "ugly ugly ugly"). */
117
- rowVariant = 'catalog',
168
+ rowVariant = PRESETS[preset]?.rowVariant ?? 'catalog',
118
169
  /* the GRID card's variant, per page; `toCard` may return `variant` per card */
119
- cardVariant = 'catalog',
170
+ cardVariant = PRESETS[preset]?.cardVariant ?? 'catalog',
120
171
  /* the LIST's container. `grid` (default) is monitor's ruling — rows four across
121
172
  on a 240 floor, gap 8 (the documented `repeat(4, 1fr)`). `stack` is one row per
122
173
  line, which is what a list of files is (MediaLibrary stacks); olina's deck list
123
174
  rendered three 300px rows side by side (user: "why is it tiny?"). */
124
- listLayout = 'grid',
175
+ listLayout = PRESETS[preset]?.listLayout ?? 'grid',
125
176
  iconComponent,
126
177
  showCountOnlyWhenFiltering = true,
127
178
  filtersProps,
@@ -155,7 +206,7 @@ export default function CatalogPage({
155
206
  }, [minColumn, maxColumns])
156
207
 
157
208
  return (
158
- <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 }}>
159
210
  {header && <PageHeader {...header} />}
160
211
  <div style={{ flex: 1, position: 'relative' }}>
161
212
  {open && <WalkthroughPanel steps={walkthrough.steps} iconComponent={walkthrough.iconComponent} />}
@@ -176,7 +227,7 @@ export default function CatalogPage({
176
227
  defaultLayout={defaultLayout}
177
228
  renderItem={(rows, viewMode, layout) => {
178
229
  if (open) return null
179
- 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 })
180
231
  const hidden = layout === 'list' ? new Set() : computeHiddenSet(rows, cards.findIndex((c) => c.expanded))
181
232
  /* COLS IS A CEILING, NOT A COMMAND (CatalogPageMobileColumns,
182
233
  * kol-monitor 2026-09-01) — the third home of the defect
@@ -58,8 +58,12 @@ export default function ShortcutsOverlay({ shortcuts = [], onClose }) {
58
58
  return (
59
59
  <div
60
60
  onClick={onClose}
61
- className="fixed inset-0 select-none bg-fg-inverse-08"
62
- style={{ display: 'grid', placeItems: 'center', backdropFilter: 'blur(2px)', zIndex: 'var(--kol-z-modal)' }}
61
+ /* THE scrim (overlay-scrim-outliers, kol-client-olina 2026-09-03; user: "we
62
+ removed the blur and put just color on the background … we missed this").
63
+ This drew its own — an 8 % inverse wash plus a 2px blur — after the
64
+ 2026-09-01 no-blur ruling and the one-tint-48 class. */
65
+ className="fixed inset-0 select-none kol-overlay-scrim"
66
+ style={{ display: 'grid', placeItems: 'center', zIndex: 'var(--kol-z-modal)' }}
63
67
  >
64
68
  <div className="text-fg-64 kol-helper-12 bg-surface-primary border border-fg-16" style={{ display: 'grid', gridTemplateColumns: 'auto auto', gap: '10px 62px', padding: 24, borderRadius: 4 }}>
65
69
  {sectioned
@@ -34,8 +34,10 @@ export default function TouchDeviceOverlay({ appName = 'This app', message }) {
34
34
  try { localStorage.setItem(STORAGE_KEY, '1') } catch { /* storage blocked */ }
35
35
  setDismissed(true)
36
36
  }
37
+ /* THE scrim, on the z ladder (overlay-scrim-outliers, 2026-09-03): was an 8 %
38
+ inverse wash + a 2px blur on a literal 100 */
37
39
  return (
38
- <div className="fixed inset-0 select-none bg-fg-inverse-08" style={{ display: 'grid', placeItems: 'center', backdropFilter: 'blur(2px)', zIndex: 100 }}>
40
+ <div className="fixed inset-0 select-none kol-overlay-scrim" style={{ display: 'grid', placeItems: 'center', zIndex: 'var(--kol-z-modal)' }}>
39
41
  <div className="bg-surface-primary border border-oq-08" style={{ width: 360, borderRadius: 4, padding: '20px 20px 16px', display: 'flex', flexDirection: 'column', gap: 12 }}>
40
42
  <div className="kol-helper-14 text-fg-96">Desktop recommended</div>
41
43
  <div className="kol-mono-12 text-fg-64">