@kolkrabbi/kol-component 0.31.0 → 0.32.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.31.0",
3
+ "version": "0.32.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",
@@ -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.12.0"
27
+ "@kolkrabbi/kol-icons": "0.12.1"
28
28
  },
29
29
  "peerDependencies": {
30
30
  "framer-motion": "^12.0.0",
@@ -74,9 +74,15 @@ 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
+ /* EXACT width — the one-piece fusion restored (2026-08-09 night).
78
+ * The truncation that briefly justified a floor is solved at the
79
+ * TRIGGER instead: the ghost stack reserves the panel row's check
80
+ * column (.kol-dd-ghost slack), so the one shared width fits every
81
+ * row. Letting the panel size itself was the broken middle: an
82
+ * abs-positioned float with auto width sizes against available
83
+ * space, not content. Sole consumer is Dropdown. */
77
84
  Object.assign(elements.floating.style, {
78
- minWidth: `${rects.reference.width}px`,
79
- width: '',
85
+ width: `${rects.reference.width}px`,
80
86
  })
81
87
  },
82
88
  })
@@ -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-2 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,32 +129,33 @@ export default function FieldRow({
128
129
  placeholder,
129
130
  disabled = false,
130
131
  }) {
131
- const dash = <span className="kol-helper-10 text-meta">—</span>
132
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 ? (
133
+ * the reference panel's grammar (user frames 2026-08-09), not a bare icon
134
+ * button and never a dash: the affordance renders with or without a
135
+ * picker (disabled when no onPick), because the reference shows the slot
136
+ * itself as the field's resting state. */
137
+ const mediaEmpty = (
136
138
  <button
137
139
  type="button"
138
140
  aria-label={label}
139
- disabled={disabled}
141
+ disabled={disabled || !onPick}
140
142
  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"
143
+ 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"
142
144
  >
143
- <Icon name="plus" size={14} />
145
+ <Icon name="plus" size={16} />
144
146
  </button>
145
- ) : dash
146
- const fileEmpty = onPick ? (
147
+ )
148
+ const fileEmpty = (
147
149
  <button
148
150
  type="button"
149
151
  aria-label={label}
150
- disabled={disabled}
152
+ disabled={disabled || !onPick}
151
153
  onClick={onPick}
152
- className="kol-control kol-control--filled kol-control-sm kol-mono-12 w-full cursor-pointer text-left text-meta"
154
+ className="kol-control kol-control--filled kol-control-sm kol-mono-12 w-full text-left text-meta enabled:cursor-pointer disabled:opacity-60"
153
155
  >
154
156
  Choose file…
155
157
  </button>
156
- ) : dash
158
+ )
157
159
 
158
160
  let control = null
159
161
  if (type === 'text') {
@@ -184,7 +186,7 @@ export default function FieldRow({
184
186
  iconOnly="x"
185
187
  iconSize={10}
186
188
  aria-label={label}
187
- className="absolute -top-2 -right-2 opacity-0 group-hover:opacity-100 focus-visible:opacity-100"
189
+ className="absolute top-1 right-1 opacity-0 group-hover:opacity-100 focus-visible:opacity-100"
188
190
  onClick={() => onChange(null)}
189
191
  />
190
192
  )}
@@ -201,7 +203,7 @@ export default function FieldRow({
201
203
  }
202
204
 
203
205
  return (
204
- <div className="grid grid-cols-[minmax(0,10rem)_minmax(0,1fr)] items-center gap-x-6 py-3 border-b border-fg-08">
206
+ <div className="grid grid-cols-[minmax(0,10rem)_minmax(0,1fr)] items-center gap-x-6 py-4 border-b border-fg-08">
205
207
  <div className="kol-mono-12 text-body">{label}</div>
206
208
  <div className="min-w-0">{control}</div>
207
209
  {hint != null && (
@@ -37,6 +37,7 @@ export default function ShellDrawer({
37
37
  side = 'left',
38
38
  width,
39
39
  header,
40
+ closeSide = 'end',
40
41
  children,
41
42
  className = '',
42
43
  }) {
@@ -149,6 +150,11 @@ export default function ShellDrawer({
149
150
  style={width != null ? { width: typeof width === 'number' ? `${width}px` : width } : undefined}
150
151
  >
151
152
  <div className="mb-6 flex items-center gap-4">
153
+ {/* closeSide="start" — the record-surface anatomy (reference frame,
154
+ * 2026-08-09): × leads, the header's actions sit at the far end. */}
155
+ {closeSide === 'start' && (
156
+ <Button variant="nav" size="md" iconOnly="x" iconSize={14} onClick={onClose} aria-label="Close" className="shrink-0" />
157
+ )}
152
158
  {header != null && <div className="min-w-0 flex-1">{header}</div>}
153
159
  {/* The box has an owner (2026-08-01). This hand-wrote the icon-button
154
160
  * square and its hover wash; `Button variant="nav"` IS that string.
@@ -156,15 +162,17 @@ export default function ShellDrawer({
156
162
  * heavier than a drawer close wants, and Button documents iconSize
157
163
  * for exactly the cases the ladder cannot serve. The SQUARE is what
158
164
  * needed an owner, and it now has one. */}
159
- <Button
160
- variant="nav"
161
- size="md"
162
- iconOnly="x"
163
- iconSize={14}
164
- onClick={onClose}
165
- aria-label="Close"
166
- className="ml-auto shrink-0"
167
- />
165
+ {closeSide !== 'start' && (
166
+ <Button
167
+ variant="nav"
168
+ size="md"
169
+ iconOnly="x"
170
+ iconSize={14}
171
+ onClick={onClose}
172
+ aria-label="Close"
173
+ className="ml-auto shrink-0"
174
+ />
175
+ )}
168
176
  </div>
169
177
  <div className="flex-1 overflow-y-auto pr-1" style={{ overflowAnchor: 'none' }}>
170
178
  {children}
@@ -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
  )
@@ -155,10 +157,10 @@ export default function RecordManager({
155
157
  <button
156
158
  type="button"
157
159
  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"
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
  >
161
- <Icon name="toggle-overlay" size={14} />
163
+ <Icon name="toggle-overlay" size={18} />
162
164
  </button>
163
165
  </span>
164
166
  ),
@@ -289,6 +291,7 @@ export default function RecordManager({
289
291
  columns={builtColumns}
290
292
  rows={rows}
291
293
  width="column"
294
+ compact
292
295
  rowClassName={(row, i) =>
293
296
  `group${drag && i === drag.from ? ' opacity-40' : ''}${
294
297
  drag && i === drag.over && drag.over !== drag.from ? ' bg-fg-04' : ''
@@ -314,17 +317,21 @@ export default function RecordManager({
314
317
  onClose={() => setActive(null)}
315
318
  side="right"
316
319
  width="min(37.5rem, 92vw)"
320
+ closeSide="start"
317
321
  header={
318
- <div className="flex items-center gap-2 min-w-0">
322
+ /* reference header anatomy (frame 45, 2026-08-09): × leads the row
323
+ * (closeSide above); every action clusters at the far end —
324
+ * … ▶ save-state Publish. */
325
+ <div className="flex items-center justify-end gap-2 min-w-0">
319
326
  {onOverflow && (
320
327
  <ToolbarIcon name="more" label="More" onClick={() => onOverflow(record)} />
321
328
  )}
322
329
  {onPreview && (
323
330
  <ToolbarIcon name="play" label="Preview" onClick={() => onPreview(record)} />
324
331
  )}
325
- {saveState != null && <span className="kol-helper-10 text-meta ml-auto truncate">{saveState}</span>}
332
+ {saveState != null && <span className="kol-helper-10 text-meta truncate">{saveState}</span>}
326
333
  {onPublish && (
327
- <Button variant="primary" size="sm" className={saveState == null ? 'ml-auto' : ''} onClick={() => onPublish(record)}>
334
+ <Button variant="primary" size="sm" onClick={() => onPublish(record)}>
328
335
  {publishLabel}
329
336
  </Button>
330
337
  )}
@@ -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">