@kolkrabbi/kol-component 0.29.0 → 0.31.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.29.0",
3
+ "version": "0.31.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",
@@ -24,7 +24,7 @@
24
24
  "@floating-ui/react": "^0.27.19",
25
25
  "embla-carousel-react": "^8.6.0",
26
26
  "react-syntax-highlighter": "^16.1.1",
27
- "@kolkrabbi/kol-icons": "0.10.0"
27
+ "@kolkrabbi/kol-icons": "0.12.0"
28
28
  },
29
29
  "peerDependencies": {
30
30
  "framer-motion": "^12.0.0",
@@ -68,11 +68,15 @@ export function usePopover({
68
68
  middleware.push(
69
69
  sizeMw({
70
70
  apply({ rects, elements }) {
71
- /* EXACT width, not a floor (2026-08-09) "one piece means one
72
- * width": with minWidth alone, a wide row let the panel outgrow the
73
- * trigger it claims to continue. Sole consumer is Dropdown. */
71
+ /* FLOOR again, not exact (2026-08-09 evening, user: "if you have
72
+ * to truncate 'centered' then its too narrow" overrides the
73
+ * morning's exact-width reading of the one-piece ruling for the
74
+ * narrow case). The panel is never NARROWER than the trigger, and
75
+ * grows when its rows carry chrome the trigger doesn't (the
76
+ * selected row's check). Sole consumer is Dropdown. */
74
77
  Object.assign(elements.floating.style, {
75
- width: `${rects.reference.width}px`,
78
+ minWidth: `${rects.reference.width}px`,
79
+ width: '',
76
80
  })
77
81
  },
78
82
  })
@@ -61,12 +61,15 @@ export function StatusChip({ value, options = [], onChange, size = 'sm' }) {
61
61
  >
62
62
  {activeOpt?.label ?? value}
63
63
  {interactive && (
64
- <Icon
65
- name="chevron-down"
66
- size={INDICATOR[size] ?? INDICATOR.sm}
67
- className={`transition-transform duration-150${open ? ' rotate-180' : ''}`}
64
+ /* plain inline-flex wrapper — Icon's own element can render
65
+ * inline-block and ride the baseline (the misaligned caret,
66
+ * 2026-08-09); the flex span centres it against the label. */
67
+ <span
68
+ className={`inline-flex shrink-0 transition-transform duration-150${open ? ' rotate-180' : ''}`}
68
69
  aria-hidden="true"
69
- />
70
+ >
71
+ <Icon name="chevron-down" size={INDICATOR[size] ?? INDICATOR.sm} />
72
+ </span>
70
73
  )}
71
74
  </Tag>
72
75
  )
@@ -125,11 +128,32 @@ export default function FieldRow({
125
128
  placeholder,
126
129
  disabled = false,
127
130
  }) {
128
- const empty = onPick ? (
129
- <Button variant="nav" size="sm" iconOnly="plus" iconSize={12} onClick={onPick} aria-label={label} disabled={disabled} />
130
- ) : (
131
- <span className="kol-helper-10 text-meta">—</span>
132
- )
131
+ const dash = <span className="kol-helper-10 text-meta">—</span>
132
+ /* Empty MEDIA is a framed + slot, empty FILE is a Choose-file control
133
+ * the reference panel's grammar (user frames 2026-08-09), not a bare
134
+ * icon button. */
135
+ const mediaEmpty = onPick ? (
136
+ <button
137
+ type="button"
138
+ aria-label={label}
139
+ disabled={disabled}
140
+ onClick={onPick}
141
+ className="flex h-14 w-20 cursor-pointer items-center justify-center rounded border border-fg-16 bg-fg-02 text-body transition-colors duration-150 hover:text-emphasis"
142
+ >
143
+ <Icon name="plus" size={14} />
144
+ </button>
145
+ ) : dash
146
+ const fileEmpty = onPick ? (
147
+ <button
148
+ type="button"
149
+ aria-label={label}
150
+ disabled={disabled}
151
+ onClick={onPick}
152
+ className="kol-control kol-control--filled kol-control-sm kol-mono-12 w-full cursor-pointer text-left text-meta"
153
+ >
154
+ Choose file…
155
+ </button>
156
+ ) : dash
133
157
 
134
158
  let control = null
135
159
  if (type === 'text') {
@@ -165,7 +189,7 @@ export default function FieldRow({
165
189
  />
166
190
  )}
167
191
  </span>
168
- ) : empty
192
+ ) : mediaEmpty
169
193
  } else if (type === 'file') {
170
194
  control = value ? (
171
195
  /* kol-tag--data: a filename renders VERBATIM — the chip uppercase is
@@ -173,7 +197,7 @@ export default function FieldRow({
173
197
  <Tag variant="secondary" size="sm" hash={false} className="kol-tag--data" onRemove={onChange ? () => onChange(null) : undefined}>
174
198
  {value.name ?? value}
175
199
  </Tag>
176
- ) : empty
200
+ ) : fileEmpty
177
201
  }
178
202
 
179
203
  return (
@@ -7,9 +7,31 @@ import ToggleCheckbox from '../atoms/ToggleCheckbox'
7
7
  import ShellDrawer from '../molecules/ShellDrawer'
8
8
  import Dropdown from '../molecules/Dropdown'
9
9
  import FieldRow, { StatusChip } from '../molecules/FieldRow'
10
+ import { Tooltip } from '../atoms/Popover'
10
11
  import Table from './Table'
11
12
  import MediaLibrary from './MediaLibrary'
12
13
 
14
+ /* TOOLBAR ICONS ARE BARE GLYPHS (user ruling 2026-08-09, verbatim: "did I
15
+ * ask for a button?? … the only thing that should hover is full opacity and
16
+ * popover"): no Button chrome, no fill, no box — body ink at rest, emphasis
17
+ * on hover, a Tooltip as the name. */
18
+ const ToolbarIcon = ({ name, label, onClick, active = false, triggerClassName }) => (
19
+ <Tooltip label={label} placement="bottom" triggerClassName={triggerClassName}>
20
+ <button
21
+ type="button"
22
+ aria-label={label}
23
+ aria-pressed={active}
24
+ onClick={onClick}
25
+ /* PRESSED gets the box (user ruling, reference frame 2026-08-09): a
26
+ * snug rounded fill hugging the glyph — "container relative to the
27
+ * icon, and only when pressed". Rest is bare; hover is ink only. */
28
+ className={`inline-flex cursor-pointer rounded-sm border-0 p-1 transition-colors duration-150 ${active ? 'bg-fg-08 text-emphasis' : 'bg-transparent text-body hover:text-emphasis'}`}
29
+ >
30
+ <Icon name={name} size={16} />
31
+ </button>
32
+ </Tooltip>
33
+ )
34
+
13
35
  /**
14
36
  * RecordManager — full-screen CMS record surface (lobby: RecordManager,
15
37
  * reference: Framer CMS). A reorderable record table plus a slide-over detail
@@ -61,9 +83,14 @@ export default function RecordManager({
61
83
  onOverflow,
62
84
  mediaClient,
63
85
  reorderLabel = (n) => `Reorder row ${n}`,
86
+ /* pressed states for the toolbar's sort/filter controls — the consumer owns
87
+ * those panels, so it tells the toolbar which control is open */
88
+ sortActive = false,
89
+ filterActive = false,
64
90
  className = '',
65
91
  }) {
66
92
  const wrapRef = useRef(null)
93
+ const [searchOpen, setSearchOpen] = useState(false)
67
94
  const dragRef = useRef(null)
68
95
  const [drag, setDrag] = useState(null) // { from, over, x, y } during a drag
69
96
  const [active, setActive] = useState(null)
@@ -120,17 +147,19 @@ export default function RecordManager({
120
147
  return {
121
148
  ...col,
122
149
  render: (row) => (
123
- <span className="inline-flex items-center gap-2 min-w-0 max-w-full">
150
+ /* the open affordance is the reference's BOXED toggle-overlay
151
+ * button, pinned at the cell's end — not an inline icon riding
152
+ * the text (user frames 2026-08-09). Hover-revealed per row. */
153
+ <span className="flex items-center justify-between gap-2 min-w-0 max-w-full">
124
154
  <span className="truncate text-emphasis">{col.render ? col.render(row) : row[col.accessor]}</span>
125
- <Button
126
- variant="nav"
127
- size="sm"
128
- iconOnly="maximize"
129
- iconSize={12}
130
- aria-label={typeof col.header === 'string' ? col.header : 'Open'}
131
- className="shrink-0 opacity-0 group-hover:opacity-100 focus-visible:opacity-100"
155
+ <button
156
+ type="button"
157
+ aria-label="Toggle overlay"
158
+ className="shrink-0 inline-flex cursor-pointer rounded-sm 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"
132
159
  onClick={() => openRecord(row)}
133
- />
160
+ >
161
+ <Icon name="toggle-overlay" size={14} />
162
+ </button>
134
163
  </span>
135
164
  ),
136
165
  }
@@ -175,55 +204,52 @@ export default function RecordManager({
175
204
  return col
176
205
  })
177
206
 
178
- const prefix = []
179
- if (onReorder) {
180
- prefix.push({
181
- accessor: '__handle',
182
- header: '',
183
- className: 'kol-table-cell-text w-8',
184
- render: (row) => {
185
- const i = rows.indexOf(row)
186
- return (
187
- /* REST-VISIBLE (2026-08-09 — "Im looking for this handle"): the
188
- * reference draws the grip dim at rest; hover-only hid the whole
189
- * reorder affordance. */
190
- <button
191
- type="button"
192
- aria-label={reorderLabel(i + 1)}
193
- className="cursor-grab touch-none bg-transparent border-0 p-0 text-meta hover:text-emphasis"
194
- onPointerDown={(e) => startDrag(e, i)}
195
- >
196
- <Icon name="drag-handle" size={14} />
197
- </button>
198
- )
199
- },
200
- })
201
- }
202
- prefix.push({
203
- accessor: '__select',
207
+ /* ONE LEADING FIELD (reference row anatomy, user frames 2026-08-09:
208
+ * "reorder and the toggle is the same field — look at the sizes"): the
209
+ * grip and the row checkbox share a single cell at matched sizes. The
210
+ * grip is rest-visible, dim; the header keeps only the select-all box,
211
+ * with the grip column held as space so the checkboxes align. */
212
+ const prefix = [{
213
+ accessor: '__lead',
204
214
  header: (
205
- <ToggleCheckbox
206
- checked={rows.length > 0 && selected.size === rows.length}
207
- onChange={() =>
208
- updateSelection(selected.size === rows.length ? new Set() : new Set(rows.map(rowId)))
209
- }
210
- />
215
+ <span className="inline-flex items-center gap-2">
216
+ {onReorder && <span className="w-4" aria-hidden="true" />}
217
+ <ToggleCheckbox
218
+ checked={rows.length > 0 && selected.size === rows.length}
219
+ onChange={() =>
220
+ updateSelection(selected.size === rows.length ? new Set() : new Set(rows.map(rowId)))
221
+ }
222
+ />
223
+ </span>
211
224
  ),
212
- className: 'kol-table-cell-text w-8',
225
+ className: 'kol-table-cell-text w-14',
213
226
  render: (row) => {
214
- const id = rowId(row, rows.indexOf(row))
227
+ const i = rows.indexOf(row)
228
+ const id = rowId(row, i)
215
229
  return (
216
- <ToggleCheckbox
217
- checked={selected.has(id)}
218
- onChange={() => {
219
- const next = new Set(selected)
220
- next.has(id) ? next.delete(id) : next.add(id)
221
- updateSelection(next)
222
- }}
223
- />
230
+ <span className="inline-flex items-center gap-2">
231
+ {onReorder && (
232
+ <button
233
+ type="button"
234
+ aria-label={reorderLabel(i + 1)}
235
+ className="inline-flex w-4 cursor-grab touch-none justify-center border-0 bg-transparent p-0 text-meta hover:text-emphasis"
236
+ onPointerDown={(e) => startDrag(e, i)}
237
+ >
238
+ <Icon name="drag-handle" size={16} />
239
+ </button>
240
+ )}
241
+ <ToggleCheckbox
242
+ checked={selected.has(id)}
243
+ onChange={() => {
244
+ const next = new Set(selected)
245
+ next.has(id) ? next.delete(id) : next.add(id)
246
+ updateSelection(next)
247
+ }}
248
+ />
249
+ </span>
224
250
  )
225
251
  },
226
- })
252
+ }]
227
253
  return [...prefix, ...resolved]
228
254
  }, [columns, rows, selected, onReorder, drag]) // eslint-disable-line react-hooks/exhaustive-deps
229
255
 
@@ -233,26 +259,28 @@ export default function RecordManager({
233
259
  return (
234
260
  <div ref={wrapRef} className={`flex flex-col min-w-0 ${className}`}>
235
261
  {hasToolbar && (
236
- <div className="flex items-center gap-1 pb-3">
237
- {onAdd && <Button variant="nav" size="md" iconOnly="plus" iconSize={14} onClick={onAdd} aria-label="Add" />}
238
- {/* the reference cluster (user screenshots 2026-08-09): ↑↓ pair,
239
- * lines, and search as an ICON that expands into the field
240
- * (SearchInput's expanding mode) not an always-open input. */}
241
- {onSortToggle && <Button variant="nav" size="md" iconOnly="arrows-vertical" iconSize={14} onClick={onSortToggle} aria-label="Sort" />}
242
- {onFilterToggle && <Button variant="nav" size="md" iconOnly="filter-lines" iconSize={14} onClick={onFilterToggle} aria-label="Filter" />}
243
- {onQueryChange && (
262
+ /* the reference cluster (user screenshots 2026-08-09): bare + · ↑↓ ·
263
+ * · 🔍, search expanding into a COMPACT field on press, … far right */
264
+ <div className="flex items-center gap-2 pb-3">
265
+ {onAdd && <ToolbarIcon name="plus" label="Add" onClick={onAdd} />}
266
+ {onSortToggle && <ToolbarIcon name="arrows-vertical" label="Sort" active={sortActive} onClick={onSortToggle} />}
267
+ {onFilterToggle && <ToolbarIcon name="filter-lines" label="Filter" active={filterActive} onClick={onFilterToggle} />}
268
+ {onQueryChange && (searchOpen || query ? (
244
269
  <SearchInput
245
- expanding
270
+ autoFocus
271
+ size="sm"
272
+ className="w-56"
246
273
  value={query ?? ''}
247
274
  onChange={(e) => onQueryChange(e?.target ? e.target.value : e)}
248
275
  placeholder={searchPlaceholder}
249
- size="sm"
276
+ onClear={() => { onQueryChange(''); setSearchOpen(false) }}
277
+ onBlur={() => { if (!query) setSearchOpen(false) }}
250
278
  />
251
- )}
252
- {/* trailing overflow the reference toolbar's far-right … (surface
253
- * scope: called with null, vs the drawer header's per-record call) */}
279
+ ) : (
280
+ <ToolbarIcon name="search" label="Search" onClick={() => setSearchOpen(true)} />
281
+ ))}
254
282
  {onOverflow && (
255
- <Button variant="nav" size="md" iconOnly="more" iconSize={14} className="ml-auto" onClick={() => onOverflow(null)} aria-label="More" />
283
+ <ToolbarIcon name="more" label="More" triggerClassName="ml-auto" onClick={() => onOverflow(null)} />
256
284
  )}
257
285
  </div>
258
286
  )}
@@ -289,10 +317,10 @@ export default function RecordManager({
289
317
  header={
290
318
  <div className="flex items-center gap-2 min-w-0">
291
319
  {onOverflow && (
292
- <Button variant="nav" size="md" iconOnly="more" iconSize={14} onClick={() => onOverflow(record)} aria-label="More" />
320
+ <ToolbarIcon name="more" label="More" onClick={() => onOverflow(record)} />
293
321
  )}
294
322
  {onPreview && (
295
- <Button variant="nav" size="md" iconOnly="play" iconSize={14} onClick={() => onPreview(record)} aria-label="Preview" />
323
+ <ToolbarIcon name="play" label="Preview" onClick={() => onPreview(record)} />
296
324
  )}
297
325
  {saveState != null && <span className="kol-helper-10 text-meta ml-auto truncate">{saveState}</span>}
298
326
  {onPublish && (