@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.
|
|
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.
|
|
27
|
+
"@kolkrabbi/kol-icons": "0.12.1"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"framer-motion": "^12.0.0",
|
package/src/atoms/Popover.jsx
CHANGED
|
@@ -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
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
72
|
+
<Icon name="chevron-down" size={INDICATOR.sm} />
|
|
72
73
|
</span>
|
|
73
74
|
)}
|
|
74
|
-
</
|
|
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
|
|
117
|
-
*
|
|
118
|
-
const THUMB_CLS = 'h-
|
|
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
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
|
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
|
-
) :
|
|
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
|
-
) :
|
|
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
|
-
|
|
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={
|
|
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
|
-
|
|
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
|
-
<
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
<
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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-
|
|
227
|
+
className: 'kol-table-cell-text w-14',
|
|
240
228
|
render: (row) => {
|
|
241
|
-
const
|
|
229
|
+
const i = rows.indexOf(row)
|
|
230
|
+
const id = rowId(row, i)
|
|
242
231
|
return (
|
|
243
|
-
<
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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' : ''
|
package/src/organisms/Table.jsx
CHANGED
|
@@ -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
|
-
|
|
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">
|