@kolkrabbi/kol-component 0.30.0 → 0.32.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.30.0",
3
+ "version": "0.32.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.11.1"
27
+ "@kolkrabbi/kol-icons": "0.12.1"
28
28
  },
29
29
  "peerDependencies": {
30
30
  "framer-motion": "^12.0.0",
@@ -74,9 +74,14 @@ export function usePopover({
74
74
  * narrow case). The panel is never NARROWER than the trigger, and
75
75
  * grows when its rows carry chrome the trigger doesn't (the
76
76
  * selected row's check). Sole consumer is Dropdown. */
77
+ /* width: max-content is LOAD-BEARING — an abs-positioned float
78
+ * with auto width shrink-to-fits its available space, and the
79
+ * w-full rows inside then blew the panel to ~2× (the broken
80
+ * dropdown, 2026-08-09 evening). max-content sizes it to the
81
+ * widest row; minWidth keeps the trigger as the floor. */
77
82
  Object.assign(elements.floating.style, {
78
83
  minWidth: `${rects.reference.width}px`,
79
- width: '',
84
+ width: 'max-content',
80
85
  })
81
86
  },
82
87
  })
@@ -50,28 +50,29 @@ export function StatusChip({ value, options = [], onChange, size = 'sm' }) {
50
50
  const norm = options.map((opt) => (typeof opt === 'object' ? opt : { value: opt, label: opt }))
51
51
  const activeOpt = norm.find((o) => o.value === value)
52
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 — as a pill with the tone
56
+ * tint. The caret is a direct child of the flex row, centred by
57
+ * items-center; it cannot sit off the label's middle. */
53
58
  const chip = (interactive) => (
54
- <Tag
55
- variant="primary"
56
- size={size}
57
- hash={false}
58
- active={interactive && open}
59
- className={`kol-status-chip${toneCls}`}
59
+ <button
60
+ type="button"
61
+ disabled={!interactive || undefined}
62
+ aria-expanded={interactive ? open : undefined}
63
+ className={`kol-status-chip${toneCls} kol-mono-12 inline-flex items-center gap-1 rounded-full border-0 px-3 py-1 leading-none${interactive ? ' cursor-pointer' : ''}`}
60
64
  onClick={interactive ? () => setOpen((o) => !o) : undefined}
61
65
  >
62
66
  {activeOpt?.label ?? value}
63
67
  {interactive && (
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
68
  <span
68
69
  className={`inline-flex shrink-0 transition-transform duration-150${open ? ' rotate-180' : ''}`}
69
70
  aria-hidden="true"
70
71
  >
71
- <Icon name="chevron-down" size={INDICATOR[size] ?? INDICATOR.sm} />
72
+ <Icon name="chevron-down" size={INDICATOR.sm} />
72
73
  </span>
73
74
  )}
74
- </Tag>
75
+ </button>
75
76
  )
76
77
  return (
77
78
  <>
@@ -113,9 +114,9 @@ export function StatusChip({ value, options = [], onChange, size = 'sm' }) {
113
114
  )
114
115
  }
115
116
 
116
- /* Media/file thumbnail — same small fixed-aspect rounded treatment as
117
- * MediaRow's thumb cell. */
118
- const THUMB_CLS = 'h-10 w-14 rounded object-cover bg-fg-04'
117
+ /* Media thumbnail — the reference panel's proportions (frame 39,
118
+ * 2026-08-09): a generous rounded tile, remove × inset at its corner. */
119
+ const THUMB_CLS = 'h-16 w-28 rounded-lg object-cover bg-fg-04'
119
120
 
120
121
  export default function FieldRow({
121
122
  label,
@@ -128,11 +129,32 @@ export default function FieldRow({
128
129
  placeholder,
129
130
  disabled = false,
130
131
  }) {
131
- const empty = onPick ? (
132
- <Button variant="nav" size="sm" iconOnly="plus" iconSize={12} onClick={onPick} aria-label={label} disabled={disabled} />
133
- ) : (
134
- <span className="kol-helper-10 text-meta">—</span>
135
- )
132
+ const dash = <span className="kol-helper-10 text-meta">—</span>
133
+ /* Empty MEDIA is a framed + slot, empty FILE is a Choose-file control
134
+ * the reference panel's grammar (user frames 2026-08-09), not a bare
135
+ * icon button. */
136
+ const mediaEmpty = onPick ? (
137
+ <button
138
+ type="button"
139
+ aria-label={label}
140
+ disabled={disabled}
141
+ onClick={onPick}
142
+ className="flex h-16 w-28 cursor-pointer items-center justify-center rounded-lg border border-fg-16 bg-fg-02 text-body transition-colors duration-150 hover:text-emphasis"
143
+ >
144
+ <Icon name="plus" size={16} />
145
+ </button>
146
+ ) : dash
147
+ const fileEmpty = onPick ? (
148
+ <button
149
+ type="button"
150
+ aria-label={label}
151
+ disabled={disabled}
152
+ onClick={onPick}
153
+ className="kol-control kol-control--filled kol-control-sm kol-mono-12 w-full cursor-pointer text-left text-meta"
154
+ >
155
+ Choose file…
156
+ </button>
157
+ ) : dash
136
158
 
137
159
  let control = null
138
160
  if (type === 'text') {
@@ -163,12 +185,12 @@ export default function FieldRow({
163
185
  iconOnly="x"
164
186
  iconSize={10}
165
187
  aria-label={label}
166
- className="absolute -top-2 -right-2 opacity-0 group-hover:opacity-100 focus-visible:opacity-100"
188
+ className="absolute top-1 right-1 opacity-0 group-hover:opacity-100 focus-visible:opacity-100"
167
189
  onClick={() => onChange(null)}
168
190
  />
169
191
  )}
170
192
  </span>
171
- ) : empty
193
+ ) : mediaEmpty
172
194
  } else if (type === 'file') {
173
195
  control = value ? (
174
196
  /* kol-tag--data: a filename renders VERBATIM — the chip uppercase is
@@ -176,7 +198,7 @@ export default function FieldRow({
176
198
  <Tag variant="secondary" size="sm" hash={false} className="kol-tag--data" onRemove={onChange ? () => onChange(null) : undefined}>
177
199
  {value.name ?? value}
178
200
  </Tag>
179
- ) : empty
201
+ ) : fileEmpty
180
202
  }
181
203
 
182
204
  return (
@@ -25,9 +25,11 @@ const ToolbarIcon = ({ name, label, onClick, active = false, triggerClassName })
25
25
  /* PRESSED gets the box (user ruling, reference frame 2026-08-09): a
26
26
  * snug rounded fill hugging the glyph — "container relative to the
27
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'}`}
28
+ /* 20px glyph, half-step pad "TOO SMALL, TOO MUCH PADDING" (user
29
+ * frames 2026-08-09): the reference icons nearly fill their box. */
30
+ className={`inline-flex cursor-pointer rounded-sm border-0 p-0.5 transition-colors duration-150 ${active ? 'bg-fg-08 text-emphasis' : 'bg-transparent text-body hover:text-emphasis'}`}
29
31
  >
30
- <Icon name={name} size={16} />
32
+ <Icon name={name} size={20} />
31
33
  </button>
32
34
  </Tooltip>
33
35
  )
@@ -147,17 +149,19 @@ export default function RecordManager({
147
149
  return {
148
150
  ...col,
149
151
  render: (row) => (
150
- <span className="inline-flex items-center gap-2 min-w-0 max-w-full">
152
+ /* the open affordance is the reference's BOXED toggle-overlay
153
+ * button, pinned at the cell's end — not an inline icon riding
154
+ * the text (user frames 2026-08-09). Hover-revealed per row. */
155
+ <span className="flex items-center justify-between gap-2 min-w-0 max-w-full">
151
156
  <span className="truncate text-emphasis">{col.render ? col.render(row) : row[col.accessor]}</span>
152
- <Button
153
- variant="nav"
154
- size="sm"
155
- iconOnly="maximize"
156
- iconSize={12}
157
- aria-label={typeof col.header === 'string' ? col.header : 'Open'}
158
- className="shrink-0 opacity-0 group-hover:opacity-100 focus-visible:opacity-100"
157
+ <button
158
+ type="button"
159
+ 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"
159
161
  onClick={() => openRecord(row)}
160
- />
162
+ >
163
+ <Icon name="toggle-overlay" size={18} />
164
+ </button>
161
165
  </span>
162
166
  ),
163
167
  }
@@ -202,55 +206,52 @@ export default function RecordManager({
202
206
  return col
203
207
  })
204
208
 
205
- const prefix = []
206
- if (onReorder) {
207
- prefix.push({
208
- accessor: '__handle',
209
- header: '',
210
- className: 'kol-table-cell-text w-8',
211
- render: (row) => {
212
- const i = rows.indexOf(row)
213
- return (
214
- /* REST-VISIBLE (2026-08-09 — "Im looking for this handle"): the
215
- * reference draws the grip dim at rest; hover-only hid the whole
216
- * reorder affordance. */
217
- <button
218
- type="button"
219
- aria-label={reorderLabel(i + 1)}
220
- className="cursor-grab touch-none bg-transparent border-0 p-0 text-meta hover:text-emphasis"
221
- onPointerDown={(e) => startDrag(e, i)}
222
- >
223
- <Icon name="drag-handle" size={14} />
224
- </button>
225
- )
226
- },
227
- })
228
- }
229
- prefix.push({
230
- accessor: '__select',
209
+ /* ONE LEADING FIELD (reference row anatomy, user frames 2026-08-09:
210
+ * "reorder and the toggle is the same field — look at the sizes"): the
211
+ * grip and the row checkbox share a single cell at matched sizes. The
212
+ * grip is rest-visible, dim; the header keeps only the select-all box,
213
+ * with the grip column held as space so the checkboxes align. */
214
+ const prefix = [{
215
+ accessor: '__lead',
231
216
  header: (
232
- <ToggleCheckbox
233
- checked={rows.length > 0 && selected.size === rows.length}
234
- onChange={() =>
235
- updateSelection(selected.size === rows.length ? new Set() : new Set(rows.map(rowId)))
236
- }
237
- />
217
+ <span className="inline-flex items-center gap-2">
218
+ {onReorder && <span className="w-4" aria-hidden="true" />}
219
+ <ToggleCheckbox
220
+ checked={rows.length > 0 && selected.size === rows.length}
221
+ onChange={() =>
222
+ updateSelection(selected.size === rows.length ? new Set() : new Set(rows.map(rowId)))
223
+ }
224
+ />
225
+ </span>
238
226
  ),
239
- className: 'kol-table-cell-text w-8',
227
+ className: 'kol-table-cell-text w-14',
240
228
  render: (row) => {
241
- const id = rowId(row, rows.indexOf(row))
229
+ const i = rows.indexOf(row)
230
+ const id = rowId(row, i)
242
231
  return (
243
- <ToggleCheckbox
244
- checked={selected.has(id)}
245
- onChange={() => {
246
- const next = new Set(selected)
247
- next.has(id) ? next.delete(id) : next.add(id)
248
- updateSelection(next)
249
- }}
250
- />
232
+ <span className="inline-flex items-center gap-2">
233
+ {onReorder && (
234
+ <button
235
+ type="button"
236
+ aria-label={reorderLabel(i + 1)}
237
+ className="inline-flex w-4 cursor-grab touch-none justify-center border-0 bg-transparent p-0 text-meta hover:text-emphasis"
238
+ onPointerDown={(e) => startDrag(e, i)}
239
+ >
240
+ <Icon name="drag-handle" size={16} />
241
+ </button>
242
+ )}
243
+ <ToggleCheckbox
244
+ checked={selected.has(id)}
245
+ onChange={() => {
246
+ const next = new Set(selected)
247
+ next.has(id) ? next.delete(id) : next.add(id)
248
+ updateSelection(next)
249
+ }}
250
+ />
251
+ </span>
251
252
  )
252
253
  },
253
- })
254
+ }]
254
255
  return [...prefix, ...resolved]
255
256
  }, [columns, rows, selected, onReorder, drag]) // eslint-disable-line react-hooks/exhaustive-deps
256
257
 
@@ -290,6 +291,7 @@ export default function RecordManager({
290
291
  columns={builtColumns}
291
292
  rows={rows}
292
293
  width="column"
294
+ compact
293
295
  rowClassName={(row, i) =>
294
296
  `group${drag && i === drag.from ? ' opacity-40' : ''}${
295
297
  drag && i === drag.over && drag.over !== drag.from ? ' bg-fg-04' : ''
@@ -33,7 +33,7 @@ const WIDTHS = {
33
33
  column: '',
34
34
  }
35
35
 
36
- const Table = ({ caption, columns, rows, variant = 'default', className = '', width = 'panel', rowClassName }) => {
36
+ const Table = ({ caption, columns, rows, variant = 'default', className = '', width = 'panel', rowClassName, compact = false }) => {
37
37
  const [sort, setSort] = useState({ key: null, dir: null })
38
38
 
39
39
  const cycle = (key) =>
@@ -56,7 +56,9 @@ const Table = ({ caption, columns, rows, variant = 'default', className = '', wi
56
56
  }, [rows, columns, sort])
57
57
 
58
58
  const variantClass = variant === 'simple' ? 'kol-table--simple' : ''
59
- const wrapperClass = ['kol-table-wrapper', variantClass, WIDTHS[width] ?? WIDTHS.panel, className].filter(Boolean).join(' ')
59
+ /* compact record-surface density (RecordManager, 2026-08-09): the default
60
+ * 12/16 cell box read as bloated rows against the reference table. */
61
+ const wrapperClass = ['kol-table-wrapper', variantClass, compact && 'kol-table--compact', WIDTHS[width] ?? WIDTHS.panel, className].filter(Boolean).join(' ')
60
62
  return (
61
63
  <div className={wrapperClass}>
62
64
  <table className="kol-table">