@kolkrabbi/kol-component 0.32.5 → 0.33.1

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.32.5",
3
+ "version": "0.33.1",
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,10 +1,7 @@
1
1
  import { useState } from 'react'
2
2
  import { Icon } from '@kolkrabbi/kol-icons'
3
3
  import Input from '../atoms/Input'
4
- import Tag from '../atoms/Tag'
5
- import Button from '../atoms/Button'
6
4
  import { usePopover, PopoverPanel } from '../atoms/Popover'
7
- import { INDICATOR } from '../hooks/glyphLadders.js'
8
5
  import Dropdown from './Dropdown'
9
6
 
10
7
  /**
@@ -27,7 +24,7 @@ import Dropdown from './Dropdown'
27
24
 
28
25
  /* StatusChip — the interactive status control ("Live ▾" in the reference,
29
26
  * rebuilt to it 2026-08-09 after the user's screenshots: the chip carries a
30
- * TONE tint + a trailing caret at the INDICATOR rung, and the open panel is
27
+ * TONE tint + a trailing chevron, and the open panel is
31
28
  * the reference's grammar — current chip on top, hairline, then option rows
32
29
  * with a leading check column).
33
30
  * Chip taxonomy (Tag source, 2026-07-30): interactive → Tag, never Pill. The
@@ -57,19 +54,18 @@ export function StatusChip({ value, options = [], onChange, variant = 'neutral',
57
54
  const toneCls = activeOpt?.tone
58
55
  ? ` kol-status-chip--${activeOpt.tone}`
59
56
  : variant === 'primary' ? ' kol-status-chip--primary' : ''
60
- /* BUILT ON THE COVER-FOCUS SPECS (user ruling 2026-08-09: "same component
61
- * as cover focus"): the chip wears the Dropdown-sm trigger metrics the
62
- * kol-btn-sm box (py-1 px-3) and mono-12 type at its OWN 16px leading, so
63
- * the pill stands the trigger's 24px (a leading-none had shrunk it to 20;
64
- * user re-call same day: "like dropdown small in size"). The caret is a
65
- * direct child of the flex row, centred by items-center; it cannot sit off
66
- * the label's middle. */
57
+ /* CHIP BOX: px-2 is the user's exact value (2026-08-09: "x padding 2
58
+ * (8px)"). py-0.5 — the same-day py-0 collapsed the pill to the bare line
59
+ * box, and the reference chip ("why cant you make the chip look like
60
+ * this?") carries visible air; 2px is the minimum that restores it.
61
+ * mono-12 at its own 16px leading. The chevron is a direct child of the flex
62
+ * row, centred by items-center; it cannot sit off the label's middle. */
67
63
  const chip = (interactive) => (
68
64
  <button
69
65
  type="button"
70
66
  disabled={!interactive || undefined}
71
67
  aria-expanded={interactive ? open : undefined}
72
- className={`kol-status-chip${toneCls} kol-mono-12 inline-flex items-center gap-1 rounded-full border-0 px-3 py-1${interactive ? ' cursor-pointer' : ''}`}
68
+ className={`kol-status-chip${toneCls} kol-mono-12 inline-flex items-center gap-2 rounded-full border-0 px-2 py-0.5${interactive ? ' cursor-pointer' : ''}`}
73
69
  onClick={interactive ? () => setOpen((o) => !o) : undefined}
74
70
  >
75
71
  {activeOpt?.label ?? value}
@@ -78,7 +74,7 @@ export function StatusChip({ value, options = [], onChange, variant = 'neutral',
78
74
  className={`inline-flex shrink-0 transition-transform duration-150${open ? ' rotate-180' : ''}`}
79
75
  aria-hidden="true"
80
76
  >
81
- <Icon name="chevron-down" size={INDICATOR.sm} />
77
+ <Icon name="chevron-down" size={10} />
82
78
  </span>
83
79
  )}
84
80
  </button>
@@ -123,9 +119,10 @@ export function StatusChip({ value, options = [], onChange, variant = 'neutral',
123
119
  )
124
120
  }
125
121
 
126
- /* Media thumbnail — the reference panel's proportions (frame 39,
127
- * 2026-08-09): a generous rounded tile, remove × inset at its corner. */
128
- const THUMB_CLS = 'h-16 w-28 rounded-lg object-cover bg-fg-04'
122
+ /* Media thumbnail — measured off the reference panel (2026-08-09): 136×72,
123
+ * remove × riding the corner. Radius is the system's 4px the repo ships
124
+ * 4px or full, nothing between. */
125
+ const THUMB_CLS = 'h-18 w-34 rounded object-cover bg-fg-04'
129
126
 
130
127
  export default function FieldRow({
131
128
  label,
@@ -149,7 +146,9 @@ export default function FieldRow({
149
146
  aria-label={label}
150
147
  disabled={disabled || !onPick}
151
148
  onClick={onPick}
152
- className="flex h-16 w-28 items-center justify-center rounded-lg border border-fg-16 bg-fg-02 text-body transition-colors duration-150 enabled:cursor-pointer enabled:hover:text-emphasis disabled:opacity-60"
149
+ /* reference empty tile: FILLED, no border a plain grey slab with the
150
+ * + centred (frame 2026-08-09) */
151
+ className="flex h-18 w-34 items-center justify-center rounded border-0 bg-fg-04 text-body transition-colors duration-150 enabled:cursor-pointer enabled:hover:text-emphasis disabled:opacity-60"
153
152
  >
154
153
  <Icon name="plus" size={16} />
155
154
  </button>
@@ -162,15 +161,16 @@ export default function FieldRow({
162
161
  onClick={onPick}
163
162
  className="kol-control kol-control--filled kol-control-sm kol-mono-12 w-full text-left text-meta enabled:cursor-pointer disabled:opacity-60"
164
163
  >
165
- Choose file…
164
+ Choose File...
166
165
  </button>
167
166
  )
168
167
 
169
168
  let control = null
170
169
  if (type === 'text') {
171
170
  control = (
172
- /* sm — record rows are compact chrome (the reference's density); md
173
- * read as a form field ("input is large? why?", 2026-08-09). */
171
+ /* sm — the repo's control rung, no special box for this component
172
+ * (user ruling 2026-08-09: "why would this component be different
173
+ * than every other rule in the repo"). */
174
174
  <Input
175
175
  size="sm"
176
176
  value={value ?? ''}
@@ -183,40 +183,60 @@ export default function FieldRow({
183
183
  } else if (type === 'status') {
184
184
  control = <StatusChip value={value} options={options} onChange={onChange} />
185
185
  } else if (type === 'select') {
186
- control = <Dropdown options={options} value={value} onChange={onChange} />
186
+ /* full-width the reference's focus selects span the control column,
187
+ * caret at the far edge (panel frame 2026-08-09) */
188
+ control = <Dropdown options={options} value={value} onChange={onChange} className="w-full" />
187
189
  } else if (type === 'media') {
188
190
  control = value ? (
189
- <span className="group relative inline-flex">
191
+ <span className="relative inline-flex">
190
192
  <img src={value.thumb ?? value.url} alt={value.name ?? ''} className={THUMB_CLS} />
191
193
  {onChange && (
192
- <Button
193
- variant="nav"
194
- size="sm"
195
- iconOnly="x"
196
- iconSize={10}
194
+ /* the reference badge: ALWAYS visible, a small dark disc riding the
195
+ * tile's corner — scrim ink is theme-invariant over imagery (panel
196
+ * frame 2026-08-09; hover-reveal repealed) */
197
+ <button
198
+ type="button"
197
199
  aria-label={label}
198
- className="absolute top-1 right-1 opacity-0 group-hover:opacity-100 focus-visible:opacity-100"
199
200
  onClick={() => onChange(null)}
200
- />
201
+ className="absolute top-1 right-1 inline-flex h-5 w-5 cursor-pointer items-center justify-center rounded-full border-0 kol-overlay-scrim text-white"
202
+ >
203
+ <Icon name="x" size={10} />
204
+ </button>
201
205
  )}
202
206
  </span>
203
207
  ) : mediaEmpty
204
208
  } else if (type === 'file') {
205
209
  control = value ? (
206
- /* kol-tag--data: a filename renders VERBATIMthe chip uppercase is
207
- * for label strings, and transforming data misquotes it (2026-08-09). */
208
- <Tag variant="secondary" size="sm" hash={false} className="kol-tag--data" onRemove={onChange ? () => onChange(null) : undefined}>
209
- {value.name ?? value}
210
- </Tag>
210
+ /* the reference's file value is a FULL-WIDTH filled rowfilename at
211
+ * the leading edge (verbatim, data is never case-transformed), × at the
212
+ * far end (panel frame 2026-08-09; the kol-tag--data chip repealed) */
213
+ <span className="kol-control kol-control--filled kol-control-sm kol-mono-12 flex w-full items-center gap-2">
214
+ <span className="min-w-0 flex-1 truncate">{value.name ?? value}</span>
215
+ {onChange && (
216
+ <button
217
+ type="button"
218
+ aria-label={label}
219
+ onClick={() => onChange(null)}
220
+ className="inline-flex shrink-0 cursor-pointer border-0 bg-transparent p-0 text-body hover:text-emphasis"
221
+ >
222
+ <Icon name="x" size={12} />
223
+ </button>
224
+ )}
225
+ </span>
211
226
  ) : fileEmpty
212
227
  }
213
228
 
214
229
  return (
215
- <div className="grid grid-cols-[minmax(0,10rem)_minmax(0,1fr)] items-center gap-x-6 py-4 border-b border-fg-08">
216
- <div className="kol-mono-12 text-body">{label}</div>
230
+ /* reference rhythm (2026-08-09): roomy rows on hairlines, narrow label
231
+ * column, labels white; media rows top-align their label to the tile.
232
+ * Controls stay on the repo's sm rung — no special box here. */
233
+ /* .kol-field-row carries the grid anatomy in kol-theme (package chrome
234
+ * never rides arbitrary utilities — they can miss generation) */
235
+ <div className="kol-field-row items-center gap-x-6 py-3 border-b border-fg-08">
236
+ <div className={`kol-mono-12 text-emphasis${type === 'media' ? ' self-start pt-1' : ''}`}>{label}</div>
217
237
  <div className="min-w-0">{control}</div>
218
238
  {hint != null && (
219
- <div className="col-start-2 kol-helper-10 text-meta pt-1 truncate">{hint}</div>
239
+ <div className="col-start-2 kol-helper-12 text-meta pt-2 truncate">{hint}</div>
220
240
  )}
221
241
  </div>
222
242
  )
@@ -1,5 +1,6 @@
1
1
  import { useEffect, useRef, useState } from 'react'
2
2
  import { createPortal } from 'react-dom'
3
+ import { Icon } from '@kolkrabbi/kol-icons'
3
4
  import Button from '../atoms/Button.jsx'
4
5
  import usePrefersReducedMotion from '../hooks/usePrefersReducedMotion.js'
5
6
 
@@ -28,6 +29,7 @@ const FOCUSABLE =
28
29
  * @param {string} side 'left' | 'right' — edge the panel slides from
29
30
  * @param {number|string} width panel width (px number or CSS length); omit for full-width sheet
30
31
  * @param {ReactNode} header header-row content beside the close button (replaces the source's baked-in wordmark)
32
+ * @param {boolean} backdrop render the dimming scrim (default true); false = panel alone, no darken/blur, close via × / Esc
31
33
  * @param {ReactNode} children scrollable panel body
32
34
  * @param {string} className extra classes on the panel
33
35
  */
@@ -38,6 +40,7 @@ export default function ShellDrawer({
38
40
  width,
39
41
  header,
40
42
  closeSide = 'end',
43
+ backdrop = true,
41
44
  children,
42
45
  className = '',
43
46
  }) {
@@ -134,26 +137,40 @@ export default function ShellDrawer({
134
137
 
135
138
  return createPortal(
136
139
  <>
137
- <div
138
- className={`fixed inset-0 z-[100] kol-overlay-scrim ${motionBackdrop}`}
139
- onClick={onClose}
140
- aria-hidden="true"
141
- />
140
+ {backdrop && (
141
+ <div
142
+ className={`fixed inset-0 z-[100] kol-overlay-scrim ${motionBackdrop}`}
143
+ onClick={onClose}
144
+ aria-hidden="true"
145
+ />
146
+ )}
142
147
  <div
143
148
  ref={panelRef}
144
149
  role="dialog"
145
150
  aria-modal="true"
146
151
  tabIndex={-1}
147
- className={`fixed inset-y-0 z-[200] flex max-w-full flex-col bg-surface-primary px-4 py-4 shadow-2xl outline-none md:px-5 lg:px-6 ${
152
+ className={`fixed inset-y-0 z-[200] flex max-w-full flex-col bg-surface-primary px-4 py-4 outline-none md:px-5 lg:px-6 ${backdrop ? 'shadow-2xl' : ''} ${
148
153
  side === 'right' ? 'right-0 border-l' : 'left-0 border-r'
149
154
  } border-fg-08 ${width == null ? 'w-full' : ''} ${motionPanel} ${className}`}
150
155
  style={width != null ? { width: typeof width === 'number' ? `${width}px` : width } : undefined}
151
156
  >
152
- <div className="mb-6 flex items-center gap-4">
157
+ {/* closeSide="start": the reference sets the × glyph ~9px deeper than
158
+ * the label column's edge, with extra top air (both reference frames
159
+ * 2026-08-09 measure the same inset) */}
160
+ <div className={`mb-6 flex items-center gap-4${closeSide === 'start' ? ' pt-3 pl-2' : ''}`}>
153
161
  {/* closeSide="start" — the record-surface anatomy (reference frame,
154
162
  * 2026-08-09): × leads, the header's actions sit at the far end. */}
155
163
  {closeSide === 'start' && (
156
- <Button variant="nav" size="md" iconOnly="x" iconSize={14} onClick={onClose} aria-label="Close" className="shrink-0" />
164
+ /* bare glyph, no container (user 2026-08-09: "the X close icon
165
+ * does not need a container") — body ink at rest, emphasis on hover */
166
+ <button
167
+ type="button"
168
+ aria-label="Close"
169
+ onClick={onClose}
170
+ className="inline-flex shrink-0 cursor-pointer border-0 bg-transparent p-0 text-body transition-colors duration-150 hover:text-emphasis"
171
+ >
172
+ <Icon name="x" size={18} />
173
+ </button>
157
174
  )}
158
175
  {header != null && <div className="min-w-0 flex-1">{header}</div>}
159
176
  {/* The box has an owner (2026-08-01). This hand-wrote the icon-button
@@ -4,6 +4,7 @@ import { Icon } from '@kolkrabbi/kol-icons'
4
4
  import Button from '../atoms/Button'
5
5
  import SearchInput from '../molecules/SearchInput'
6
6
  import ToggleCheckbox from '../atoms/ToggleCheckbox'
7
+ import IconFrame from '../atoms/IconFrame'
7
8
  import ShellDrawer from '../molecules/ShellDrawer'
8
9
  import FieldRow, { StatusChip } from '../molecules/FieldRow'
9
10
  import { Tooltip } from '../atoms/Popover'
@@ -100,7 +101,14 @@ export default function RecordManager({
100
101
 
101
102
  const rowId = (row, i) => row.id ?? i
102
103
 
104
+ /* panel = 60% of the table area (user ruling 2026-08-09) — the surface's
105
+ * own width IS the table pane, measured at open. ponytail: not re-measured
106
+ * on live resize. */
107
+ const [panelW, setPanelW] = useState(null)
108
+
103
109
  const openRecord = (row) => {
110
+ const w = wrapRef.current?.getBoundingClientRect().width
111
+ setPanelW(w ? Math.round(w * 0.6) : null)
104
112
  setActive(row)
105
113
  onSelectRow?.(row)
106
114
  }
@@ -153,14 +161,22 @@ export default function RecordManager({
153
161
  * the text (user frames 2026-08-09). Hover-revealed per row. */
154
162
  <span className="flex items-center justify-between gap-2 min-w-0 max-w-full">
155
163
  <span className="truncate text-emphasis">{col.render ? col.render(row) : row[col.accessor]}</span>
156
- <button
157
- type="button"
164
+ {/* the ICON CONTAINER is IconFrame (2026-08-01 ruling), box-model
165
+ * proper (user 2026-08-09, "padding should control the size"):
166
+ * the box stays pinned, the glyph FILLS the content box, and
167
+ * padding is the only inset knob — p-[3px] on user instruction
168
+ * (2026-08-09, "use 3px … use [3px]"); arbitrary utilities have
169
+ * missed consumer generation before — if this renders unpadded
170
+ * in a consumer, this class is why. */}
171
+ <IconFrame
172
+ name="toggle-overlay"
173
+ variant="outline"
174
+ size="sm"
175
+ iconSize="100%"
158
176
  aria-label="Toggle overlay"
159
- className="shrink-0 inline-flex cursor-pointer rounded-md border border-fg-16 bg-transparent p-1 text-body opacity-0 transition-opacity duration-150 group-hover:opacity-100 focus-visible:opacity-100 hover:text-emphasis"
160
177
  onClick={() => openRecord(row)}
161
- >
162
- <Icon name="toggle-overlay" size={18} />
163
- </button>
178
+ className="p-[3px] opacity-0 group-hover:opacity-100 focus-visible:opacity-100"
179
+ />
164
180
  </span>
165
181
  ),
166
182
  }
@@ -181,12 +197,11 @@ export default function RecordManager({
181
197
  return {
182
198
  ...col,
183
199
  /* the reference's select cell ("Center ▾") is a PILL, not the square
184
- * Dropdown trigger — the StatusChip apparatus wearing the Dropdown-
185
- * primary fill (user frame 2026-08-09: "use status pill, but same
186
- * look as dropdown primary"). Panel select fields stay Dropdown. */
200
+ * Dropdown trigger — the StatusChip apparatus in its NEUTRAL grey,
201
+ * the same chip Archived wears (user ruling 2026-08-09: "grey like
202
+ * 'archived'"). Panel select fields stay Dropdown. */
187
203
  render: (row) => (
188
204
  <StatusChip
189
- variant="primary"
190
205
  value={row[col.accessor]}
191
206
  options={col.options ?? []}
192
207
  onChange={(next) => col.onSelectChange?.(row, next)}
@@ -200,8 +215,9 @@ export default function RecordManager({
200
215
  render: (row) => {
201
216
  const media = row[col.accessor]
202
217
  return media ? (
203
- /* 2:1 landscape (user frame 2026-08-09: "more landscape") */
204
- <img src={media.thumb ?? media.url} alt={media.name ?? ''} className="h-10 w-20 rounded object-cover bg-fg-04" />
218
+ /* measured off the reference frame: the tile is chip-height —
219
+ * ~24px tall, 2:1 not a 40px block (user re-call 2026-08-09) */
220
+ <img src={media.thumb ?? media.url} alt={media.name ?? ''} className="h-6 w-12 rounded object-cover bg-fg-04" />
205
221
  ) : (
206
222
  <span className="kol-helper-10 text-meta">—</span>
207
223
  )
@@ -323,8 +339,9 @@ export default function RecordManager({
323
339
  open={!!active}
324
340
  onClose={() => setActive(null)}
325
341
  side="right"
326
- width="min(37.5rem, 92vw)"
342
+ width={panelW ? `${panelW}px` : '50vw'}
327
343
  closeSide="start"
344
+ backdrop={false}
328
345
  header={
329
346
  /* reference header anatomy (frame 45, 2026-08-09): × leads the row
330
347
  * (closeSide above); every action clusters at the far end —
@@ -346,7 +363,9 @@ export default function RecordManager({
346
363
  }
347
364
  >
348
365
  {record && (
349
- <div className="flex flex-col">
366
+ /* border-t: the reference rules EVERY seam — including above the
367
+ * first row; each FieldRow carries its own border-b (frame 2026-08-09) */
368
+ <div className="flex flex-col border-t border-fg-08">
350
369
  {fields.map((f) => (
351
370
  <FieldRow
352
371
  key={f.key}