@kolkrabbi/kol-component 0.220.0 → 0.221.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.220.0",
3
+ "version": "0.221.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",
package/src/index.js CHANGED
@@ -152,7 +152,6 @@ export { default as ErrorBoundary } from './utilities/ErrorBoundary.jsx'
152
152
  export { default as FeatureSplit } from './organisms/FeatureSplit.jsx'
153
153
  export { default as FeaturedCarousel } from './organisms/FeaturedCarousel.jsx'
154
154
  export { default as FeaturesCardSection } from './organisms/FeaturesCardSection.jsx'
155
- export { default as FoundryCTA } from './organisms/FoundryCTA.jsx'
156
155
  export { default as SectionHero, default as FullBleedHero } from './organisms/SectionHero.jsx'
157
156
  export { default as SectionSplit } from './organisms/SectionSplit.jsx'
158
157
  export { default as SectionCards } from './organisms/SectionCards.jsx'
@@ -462,6 +462,11 @@ export default function ColumnBrowser({
462
462
  quickLook = null,
463
463
  onQuickLook,
464
464
  onPick,
465
+ /* `picked` — the file the CALLER holds picked (a key or an object; kol-client-olina 2026-09-25:
466
+ * a pick made in rows or grid was lost on the switch to columns, because this browser kept its
467
+ * own). Given, the browser opens on that file's level with its row selected and previewed, and
468
+ * follows it when it changes; absent (`undefined`), the pick is internal exactly as before. */
469
+ picked: pickedIn,
465
470
  urlOf,
466
471
  kindOf = defaultKindOf,
467
472
  kindLabel = DEFAULT_KIND_LABEL,
@@ -559,6 +564,16 @@ export default function ColumnBrowser({
559
564
  if (prefix !== level) { keepCursor.current = true; pickedByCollapse.current = true; onPrefix(level) }
560
565
  }
561
566
  const pickedByCollapse = useRef(false)
567
+ const pickedInKey = typeof pickedIn === 'string' ? pickedIn : pickedIn?.key ?? null
568
+ useEffect(() => {
569
+ if (pickedIn === undefined || (picked?.key ?? null) === pickedInKey) return
570
+ const o = typeof pickedIn === 'string' ? objects.find((x) => x.key === pickedIn) : pickedIn
571
+ if (!o) { setPicked(null); return }
572
+ const level = o.key.slice(0, o.key.lastIndexOf('/') + 1)
573
+ setPicked(o)
574
+ setCursor(seedCursor(level))
575
+ if (prefix !== level) { keepCursor.current = true; pickedByCollapse.current = true; onPrefix(level) }
576
+ }, [pickedInKey]) // eslint-disable-line react-hooks/exhaustive-deps
562
577
  useEffect(() => { if (pickedByCollapse.current) { pickedByCollapse.current = false; return } if (picked) pick(null) }, [prefix]) // eslint-disable-line react-hooks/exhaustive-deps
563
578
  useEffect(() => { setExpanded((prev) => (prev.size ? new Set() : prev)) }, [prefix])
564
579
  const levelsOf = (pfx) => {
@@ -580,6 +595,12 @@ export default function ColumnBrowser({
580
595
  * internal moves land on the same spot, so nothing jumps. */
581
596
  const seedCursor = (pfx) => {
582
597
  const lv = levelsOf(pfx)
598
+ /* a caller's `picked` file at this level is where the keyboard starts — on the file, not on
599
+ * the folder that opened its column */
600
+ if (pickedInKey?.startsWith(pfx) && !pickedInKey.slice(pfx.length).includes('/')) {
601
+ const idx = itemsAt(pfx).findIndex((it) => it.type === 'file' && it.o.key === pickedInKey)
602
+ if (idx >= 0) return { col: lv.length - 1, idx }
603
+ }
583
604
  if (lv.length < 2) return { col: 0, idx: 0 }
584
605
  const col = lv.length - 2
585
606
  const opened = lv[col + 1].slice(lv[col].length)
@@ -195,12 +195,19 @@ function useBucketLibrary({ client, bucket, defaults, settings: controlled, onSe
195
195
 
196
196
  /* ── shared pieces, kol-r2b2's ─────────────────────────────────────────────── */
197
197
 
198
- /* The frame is square until the file reports its size, then snaps to its nearest preset. */
199
- function ImageFrame({ src }) {
200
- const [ratio, setRatio] = useState('1 / 1')
198
+ /* The frame is square until the file reports its size, then snaps to its nearest preset. A photo
199
+ * near a preset fills it (`cover`, a few percent cropped); a vector or anything past the ladder's
200
+ * ends — a 2.8:1 wordmark, an 11:1 nav logo — shows whole (`contain`, kol-client-olina 2026-09-25). */
201
+ function ImageFrame({ src, vector }) {
202
+ const [frame, setFrame] = useState({ ratio: '1 / 1', contain: !!vector })
203
+ const onLoad = (e) => {
204
+ const { naturalWidth: w, naturalHeight: h } = e.target
205
+ const r = w && h ? w / h : 1
206
+ setFrame({ ratio: nearestRatio(w, h), contain: vector || r > 16 / 9 || r < 9 / 16 })
207
+ }
201
208
  return (
202
- <div className="w-full rounded overflow-hidden flex items-center justify-center" style={{ aspectRatio: ratio }}>
203
- <img src={src} alt="" className="w-full h-full object-cover" loading="lazy" onLoad={(e) => setRatio(nearestRatio(e.target.naturalWidth, e.target.naturalHeight))} />
209
+ <div className="w-full rounded overflow-hidden flex items-center justify-center" style={{ aspectRatio: frame.ratio }}>
210
+ <img src={src} alt="" className={`w-full h-full ${frame.contain ? 'object-contain' : 'object-cover'}`} loading="lazy" onLoad={onLoad} />
204
211
  </div>
205
212
  )
206
213
  }
@@ -1453,7 +1460,7 @@ export function MediaLibraryBrowse({
1453
1460
  <ColumnPreview key={previewFile.key} o={previewFile} urlOf={(o) => mediaUrl(o.key)} kindOf={kindOf} kindLabel={KIND_LABEL}
1454
1461
  formatSize={formatSize} formatDate={formatDate} width={previewWidth}
1455
1462
  renderPreview={(o) => {
1456
- if (isImage(o.contentType)) return <ImageFrame src={mediaUrl(o.key)} />
1463
+ if (isImage(o.contentType)) return <ImageFrame src={mediaUrl(o.key)} vector={o.contentType === 'image/svg+xml'} />
1457
1464
  const poster = posterFor(o.key, keySet)
1458
1465
  return <KindPreview o={o} urlOf={(x) => mediaUrl(x.key)} poster={poster ? mediaUrl(poster) : undefined} kindOf={kindOf} kindLabel={KIND_LABEL} />
1459
1466
  }} />
@@ -1631,6 +1638,7 @@ export function MediaLibraryBrowse({
1631
1638
  const real = rest.join('/')
1632
1639
  if (target && target.id !== bucketMeta.id) switchBucket(target.id, real); else setPrefix(real)
1633
1640
  }}
1641
+ picked={pickedFile ? { ...pickedFile, key: `${VROOT}${pickedFile.key}` } : null}
1634
1642
  onPick={(o) => setPickedFile(o ? { ...o, key: o.key.slice(VROOT.length) } : null)}
1635
1643
  quickLook={quickLook && { ...quickLook, files: quickLook.files.map((o) => ({ ...o, key: `${VROOT}${o.key}` })) }}
1636
1644
  onQuickLook={(q) => openQuickLook(q && { ...q, files: q.files.map((o) => ({ ...o, key: o.key.slice(VROOT.length) })) })}
@@ -1646,7 +1654,7 @@ export function MediaLibraryBrowse({
1646
1654
  stackView={stackView ?? settings.stackView ?? 'list'}
1647
1655
  renderPreview={(o) => {
1648
1656
  const real = { ...o, key: o.key.slice(VROOT.length) }
1649
- if (isImage(real.contentType)) return <ImageFrame src={mediaUrl(real.key)} />
1657
+ if (isImage(real.contentType)) return <ImageFrame src={mediaUrl(real.key)} vector={real.contentType === 'image/svg+xml'} />
1650
1658
  const poster = posterFor(real.key, keySet)
1651
1659
  return <KindPreview o={real} urlOf={(x) => mediaUrl(x.key)} poster={poster ? mediaUrl(poster) : undefined} kindOf={kindOf} kindLabel={KIND_LABEL} />
1652
1660
  }}
@@ -15,7 +15,7 @@ import { minHeightClass } from './sectionHeights.js'
15
15
  * TWO VARIANTS. `editorial` (default): the two-column band above. `centered`
16
16
  * (SectionHeroRound2, 2026-08-26): the quiet mid-page tier — a short rule, a
17
17
  * heading, a mono line and Buttons, centred — which was `FoundryCTA`, a second
18
- * way to render SectionText. FoundryCTA is its deprecated alias.
18
+ * way to render SectionText. FoundryCTA was its alias until 2026-09-25, when it dropped.
19
19
  *
20
20
  * @param {'full'|'80'|'60'|string} [height='60'] min-height on the family's ladder — full = 100dvh,
21
21
  * 80 = 70svh / 80vh, 60 = 50svh / 60vh (default), 40 = 35svh / 40vh; content stays vertically centred inside it
@@ -1,38 +0,0 @@
1
- import Button from '../atoms/Button.jsx'
2
- import SectionCta from './SectionCta.jsx'
3
-
4
- /* taxonomy-ok: nests component's Button */
5
-
6
- /**
7
- * @deprecated 2026-08-26 — `FoundryCTA` is `SectionCta variant="centered"`
8
- * (SectionHeroRound2: a second way to render SectionText). Same render; the
9
- * `action` object(s) become Buttons. Alias kept; removed at the next major.
10
- *
11
- * @param {ReactNode} heading · description
12
- * @param {Object|Array} action one {href, label, variant?, target?, rel?} or an array
13
- * @param {Function} onNavigate (href, event) => void
14
- */
15
- export default function FoundryCTA({ heading, description, action, onNavigate, className = '' }) {
16
- const actions = (Array.isArray(action) ? action : [action]).filter(Boolean)
17
- return (
18
- <SectionCta
19
- variant="centered"
20
- headline={heading}
21
- body={description}
22
- className={className}
23
- actions={actions.length > 0 ? actions.map((act, i) => (
24
- <Button
25
- key={i}
26
- variant={act.variant === 'secondary' ? 'outline' : 'primary'}
27
- size="md"
28
- href={act.href}
29
- target={act.target}
30
- rel={act.rel}
31
- onClick={onNavigate ? (e) => onNavigate(act.href, e) : undefined}
32
- >
33
- {act.label}
34
- </Button>
35
- )) : undefined}
36
- />
37
- )
38
- }