@kolkrabbi/kol-component 0.32.4 → 0.33.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.32.4",
3
+ "version": "0.33.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",
@@ -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
@@ -37,8 +34,13 @@ import Dropdown from './Dropdown'
37
34
  *
38
35
  * Options: strings, or { value, label, tone } — tone ∈ success|warning|error|
39
36
  * info maps onto the --ui-* ladder (.kol-status-chip--* in kol-theme); the
40
- * chip wears the ACTIVE option's tone. No tone → the plain Tag look. */
41
- export function StatusChip({ value, options = [], onChange, size = 'sm' }) {
37
+ * chip wears the ACTIVE option's tone. No tone → the plain Tag look.
38
+ *
39
+ * variant="primary" — the toneless pill wears the Dropdown-primary fill
40
+ * instead of the neutral mix (RecordManager's select columns, user frame
41
+ * 2026-08-09: "use status pill, but same look as dropdown primary"). An
42
+ * option's tone always outranks the variant. */
43
+ export function StatusChip({ value, options = [], onChange, variant = 'neutral', size = 'sm' }) {
42
44
  const [open, setOpen] = useState(false)
43
45
  const popover = usePopover({
44
46
  open,
@@ -49,20 +51,21 @@ export function StatusChip({ value, options = [], onChange, size = 'sm' }) {
49
51
  })
50
52
  const norm = options.map((opt) => (typeof opt === 'object' ? opt : { value: opt, label: opt }))
51
53
  const activeOpt = norm.find((o) => o.value === value)
52
- const toneCls = activeOpt?.tone ? ` kol-status-chip--${activeOpt.tone}` : ''
53
- /* BUILT ON THE COVER-FOCUS SPECS (user ruling 2026-08-09: "same component
54
- * as cover focus"): the chip wears the Dropdown-sm trigger metrics — the
55
- * kol-btn-sm box (py-1 px-3) and mono-12 type at its OWN 16px leading, so
56
- * the pill stands the trigger's 24px (a leading-none had shrunk it to 20;
57
- * user re-call same day: "like dropdown small in size"). The caret is a
58
- * direct child of the flex row, centred by items-center; it cannot sit off
59
- * the label's middle. */
54
+ const toneCls = activeOpt?.tone
55
+ ? ` kol-status-chip--${activeOpt.tone}`
56
+ : variant === 'primary' ? ' kol-status-chip--primary' : ''
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. */
60
63
  const chip = (interactive) => (
61
64
  <button
62
65
  type="button"
63
66
  disabled={!interactive || undefined}
64
67
  aria-expanded={interactive ? open : undefined}
65
- 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' : ''}`}
66
69
  onClick={interactive ? () => setOpen((o) => !o) : undefined}
67
70
  >
68
71
  {activeOpt?.label ?? value}
@@ -71,7 +74,7 @@ export function StatusChip({ value, options = [], onChange, size = 'sm' }) {
71
74
  className={`inline-flex shrink-0 transition-transform duration-150${open ? ' rotate-180' : ''}`}
72
75
  aria-hidden="true"
73
76
  >
74
- <Icon name="chevron-down" size={INDICATOR.sm} />
77
+ <Icon name="chevron-down" size={10} />
75
78
  </span>
76
79
  )}
77
80
  </button>
@@ -116,9 +119,10 @@ export function StatusChip({ value, options = [], onChange, size = 'sm' }) {
116
119
  )
117
120
  }
118
121
 
119
- /* Media thumbnail — the reference panel's proportions (frame 39,
120
- * 2026-08-09): a generous rounded tile, remove × inset at its corner. */
121
- 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'
122
126
 
123
127
  export default function FieldRow({
124
128
  label,
@@ -142,7 +146,9 @@ export default function FieldRow({
142
146
  aria-label={label}
143
147
  disabled={disabled || !onPick}
144
148
  onClick={onPick}
145
- 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"
146
152
  >
147
153
  <Icon name="plus" size={16} />
148
154
  </button>
@@ -155,15 +161,16 @@ export default function FieldRow({
155
161
  onClick={onPick}
156
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"
157
163
  >
158
- Choose file…
164
+ Choose File...
159
165
  </button>
160
166
  )
161
167
 
162
168
  let control = null
163
169
  if (type === 'text') {
164
170
  control = (
165
- /* sm — record rows are compact chrome (the reference's density); md
166
- * 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"). */
167
174
  <Input
168
175
  size="sm"
169
176
  value={value ?? ''}
@@ -176,40 +183,60 @@ export default function FieldRow({
176
183
  } else if (type === 'status') {
177
184
  control = <StatusChip value={value} options={options} onChange={onChange} />
178
185
  } else if (type === 'select') {
179
- 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" />
180
189
  } else if (type === 'media') {
181
190
  control = value ? (
182
- <span className="group relative inline-flex">
191
+ <span className="relative inline-flex">
183
192
  <img src={value.thumb ?? value.url} alt={value.name ?? ''} className={THUMB_CLS} />
184
193
  {onChange && (
185
- <Button
186
- variant="nav"
187
- size="sm"
188
- iconOnly="x"
189
- 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"
190
199
  aria-label={label}
191
- className="absolute top-1 right-1 opacity-0 group-hover:opacity-100 focus-visible:opacity-100"
192
200
  onClick={() => onChange(null)}
193
- />
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>
194
205
  )}
195
206
  </span>
196
207
  ) : mediaEmpty
197
208
  } else if (type === 'file') {
198
209
  control = value ? (
199
- /* kol-tag--data: a filename renders VERBATIMthe chip uppercase is
200
- * for label strings, and transforming data misquotes it (2026-08-09). */
201
- <Tag variant="secondary" size="sm" hash={false} className="kol-tag--data" onRemove={onChange ? () => onChange(null) : undefined}>
202
- {value.name ?? value}
203
- </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>
204
226
  ) : fileEmpty
205
227
  }
206
228
 
207
229
  return (
208
- <div className="grid grid-cols-[minmax(0,10rem)_minmax(0,1fr)] items-center gap-x-6 py-4 border-b border-fg-08">
209
- <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>
210
237
  <div className="min-w-0">{control}</div>
211
238
  {hint != null && (
212
- <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>
213
240
  )}
214
241
  </div>
215
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,8 +4,8 @@ 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
- import Dropdown from '../molecules/Dropdown'
9
9
  import FieldRow, { StatusChip } from '../molecules/FieldRow'
10
10
  import { Tooltip } from '../atoms/Popover'
11
11
  import Table from './Table'
@@ -101,7 +101,14 @@ export default function RecordManager({
101
101
 
102
102
  const rowId = (row, i) => row.id ?? i
103
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
+
104
109
  const openRecord = (row) => {
110
+ const w = wrapRef.current?.getBoundingClientRect().width
111
+ setPanelW(w ? Math.round(w * 0.6) : null)
105
112
  setActive(row)
106
113
  onSelectRow?.(row)
107
114
  }
@@ -154,14 +161,20 @@ export default function RecordManager({
154
161
  * the text (user frames 2026-08-09). Hover-revealed per row. */
155
162
  <span className="flex items-center justify-between gap-2 min-w-0 max-w-full">
156
163
  <span className="truncate text-emphasis">{col.render ? col.render(row) : row[col.accessor]}</span>
157
- <button
158
- 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-0.5 = the 2px. */}
168
+ <IconFrame
169
+ name="toggle-overlay"
170
+ variant="outline"
171
+ size="sm"
172
+ iconSize="100%"
159
173
  aria-label="Toggle overlay"
160
- 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"
161
174
  onClick={() => openRecord(row)}
162
- >
163
- <Icon name="toggle-overlay" size={18} />
164
- </button>
175
+ style={{ padding: 3 }}
176
+ className="opacity-0 group-hover:opacity-100 focus-visible:opacity-100"
177
+ />
165
178
  </span>
166
179
  ),
167
180
  }
@@ -181,10 +194,14 @@ export default function RecordManager({
181
194
  if (col.type === 'select') {
182
195
  return {
183
196
  ...col,
197
+ /* the reference's select cell ("Center ▾") is a PILL, not the square
198
+ * Dropdown trigger — the StatusChip apparatus in its NEUTRAL grey,
199
+ * the same chip Archived wears (user ruling 2026-08-09: "grey like
200
+ * 'archived'"). Panel select fields stay Dropdown. */
184
201
  render: (row) => (
185
- <Dropdown
186
- options={col.options ?? []}
202
+ <StatusChip
187
203
  value={row[col.accessor]}
204
+ options={col.options ?? []}
188
205
  onChange={(next) => col.onSelectChange?.(row, next)}
189
206
  />
190
207
  ),
@@ -196,7 +213,9 @@ export default function RecordManager({
196
213
  render: (row) => {
197
214
  const media = row[col.accessor]
198
215
  return media ? (
199
- <img src={media.thumb ?? media.url} alt={media.name ?? ''} className="h-10 w-14 rounded object-cover bg-fg-04" />
216
+ /* measured off the reference frame: the tile is chip-height
217
+ * ~24px tall, 2:1 — not a 40px block (user re-call 2026-08-09) */
218
+ <img src={media.thumb ?? media.url} alt={media.name ?? ''} className="h-6 w-12 rounded object-cover bg-fg-04" />
200
219
  ) : (
201
220
  <span className="kol-helper-10 text-meta">—</span>
202
221
  )
@@ -292,8 +311,10 @@ export default function RecordManager({
292
311
  rows={rows}
293
312
  width="column"
294
313
  compact
314
+ /* align-middle: cell content centres in the row (reference grammar,
315
+ * user frame 2026-08-09) — scoped here; Table's base stays v-top */
295
316
  rowClassName={(row, i) =>
296
- `group${drag && i === drag.from ? ' opacity-40' : ''}${
317
+ `group align-middle${drag && i === drag.from ? ' opacity-40' : ''}${
297
318
  drag && i === drag.over && drag.over !== drag.from ? ' bg-fg-04' : ''
298
319
  }`
299
320
  }
@@ -316,8 +337,9 @@ export default function RecordManager({
316
337
  open={!!active}
317
338
  onClose={() => setActive(null)}
318
339
  side="right"
319
- width="min(37.5rem, 92vw)"
340
+ width={panelW ? `${panelW}px` : '50vw'}
320
341
  closeSide="start"
342
+ backdrop={false}
321
343
  header={
322
344
  /* reference header anatomy (frame 45, 2026-08-09): × leads the row
323
345
  * (closeSide above); every action clusters at the far end —
@@ -339,7 +361,9 @@ export default function RecordManager({
339
361
  }
340
362
  >
341
363
  {record && (
342
- <div className="flex flex-col">
364
+ /* border-t: the reference rules EVERY seam — including above the
365
+ * first row; each FieldRow carries its own border-b (frame 2026-08-09) */
366
+ <div className="flex flex-col border-t border-fg-08">
343
367
  {fields.map((f) => (
344
368
  <FieldRow
345
369
  key={f.key}