@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 +2 -2
- package/src/atoms/Popover.jsx +8 -4
- package/src/molecules/FieldRow.jsx +36 -12
- package/src/organisms/RecordManager.jsx +96 -68
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-component",
|
|
3
|
-
"version": "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.
|
|
27
|
+
"@kolkrabbi/kol-icons": "0.12.0"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"framer-motion": "^12.0.0",
|
package/src/atoms/Popover.jsx
CHANGED
|
@@ -68,11 +68,15 @@ export function usePopover({
|
|
|
68
68
|
middleware.push(
|
|
69
69
|
sizeMw({
|
|
70
70
|
apply({ rects, elements }) {
|
|
71
|
-
/*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
) :
|
|
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
|
-
) :
|
|
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
|
-
|
|
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
|
-
<
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
<
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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-
|
|
225
|
+
className: 'kol-table-cell-text w-14',
|
|
213
226
|
render: (row) => {
|
|
214
|
-
const
|
|
227
|
+
const i = rows.indexOf(row)
|
|
228
|
+
const id = rowId(row, i)
|
|
215
229
|
return (
|
|
216
|
-
<
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
{
|
|
242
|
-
{
|
|
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
|
-
|
|
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
|
-
|
|
276
|
+
onClear={() => { onQueryChange(''); setSearchOpen(false) }}
|
|
277
|
+
onBlur={() => { if (!query) setSearchOpen(false) }}
|
|
250
278
|
/>
|
|
251
|
-
)
|
|
252
|
-
|
|
253
|
-
|
|
279
|
+
) : (
|
|
280
|
+
<ToolbarIcon name="search" label="Search" onClick={() => setSearchOpen(true)} />
|
|
281
|
+
))}
|
|
254
282
|
{onOverflow && (
|
|
255
|
-
<
|
|
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
|
-
<
|
|
320
|
+
<ToolbarIcon name="more" label="More" onClick={() => onOverflow(record)} />
|
|
293
321
|
)}
|
|
294
322
|
{onPreview && (
|
|
295
|
-
<
|
|
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 && (
|