@kolkrabbi/kol-component 0.32.5 → 0.33.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.
|
|
3
|
+
"version": "0.33.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",
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import { useState } from 'react'
|
|
2
2
|
import { Icon } from '@kolkrabbi/kol-icons'
|
|
3
3
|
import Input from '../atoms/Input'
|
|
4
|
-
import Tag from '../atoms/Tag'
|
|
5
|
-
import Button from '../atoms/Button'
|
|
6
4
|
import { usePopover, PopoverPanel } from '../atoms/Popover'
|
|
7
|
-
import { INDICATOR } from '../hooks/glyphLadders.js'
|
|
8
5
|
import Dropdown from './Dropdown'
|
|
9
6
|
|
|
10
7
|
/**
|
|
@@ -27,7 +24,7 @@ import Dropdown from './Dropdown'
|
|
|
27
24
|
|
|
28
25
|
/* StatusChip — the interactive status control ("Live ▾" in the reference,
|
|
29
26
|
* rebuilt to it 2026-08-09 after the user's screenshots: the chip carries a
|
|
30
|
-
* TONE tint + a trailing
|
|
27
|
+
* TONE tint + a trailing chevron, and the open panel is
|
|
31
28
|
* the reference's grammar — current chip on top, hairline, then option rows
|
|
32
29
|
* with a leading check column).
|
|
33
30
|
* Chip taxonomy (Tag source, 2026-07-30): interactive → Tag, never Pill. The
|
|
@@ -57,19 +54,18 @@ export function StatusChip({ value, options = [], onChange, variant = 'neutral',
|
|
|
57
54
|
const toneCls = activeOpt?.tone
|
|
58
55
|
? ` kol-status-chip--${activeOpt.tone}`
|
|
59
56
|
: variant === 'primary' ? ' kol-status-chip--primary' : ''
|
|
60
|
-
/*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
* the label's middle. */
|
|
57
|
+
/* CHIP BOX: px-2 is the user's exact value (2026-08-09: "x padding 2
|
|
58
|
+
* (8px)"). py-0.5 — the same-day py-0 collapsed the pill to the bare line
|
|
59
|
+
* box, and the reference chip ("why cant you make the chip look like
|
|
60
|
+
* this?") carries visible air; 2px is the minimum that restores it.
|
|
61
|
+
* mono-12 at its own 16px leading. The chevron is a direct child of the flex
|
|
62
|
+
* row, centred by items-center; it cannot sit off the label's middle. */
|
|
67
63
|
const chip = (interactive) => (
|
|
68
64
|
<button
|
|
69
65
|
type="button"
|
|
70
66
|
disabled={!interactive || undefined}
|
|
71
67
|
aria-expanded={interactive ? open : undefined}
|
|
72
|
-
className={`kol-status-chip${toneCls} kol-mono-12 inline-flex items-center gap-
|
|
68
|
+
className={`kol-status-chip${toneCls} kol-mono-12 inline-flex items-center gap-2 rounded-full border-0 px-2 py-0.5${interactive ? ' cursor-pointer' : ''}`}
|
|
73
69
|
onClick={interactive ? () => setOpen((o) => !o) : undefined}
|
|
74
70
|
>
|
|
75
71
|
{activeOpt?.label ?? value}
|
|
@@ -78,7 +74,7 @@ export function StatusChip({ value, options = [], onChange, variant = 'neutral',
|
|
|
78
74
|
className={`inline-flex shrink-0 transition-transform duration-150${open ? ' rotate-180' : ''}`}
|
|
79
75
|
aria-hidden="true"
|
|
80
76
|
>
|
|
81
|
-
<Icon name="chevron-down" size={
|
|
77
|
+
<Icon name="chevron-down" size={10} />
|
|
82
78
|
</span>
|
|
83
79
|
)}
|
|
84
80
|
</button>
|
|
@@ -123,9 +119,10 @@ export function StatusChip({ value, options = [], onChange, variant = 'neutral',
|
|
|
123
119
|
)
|
|
124
120
|
}
|
|
125
121
|
|
|
126
|
-
/* Media thumbnail — the reference panel
|
|
127
|
-
*
|
|
128
|
-
|
|
122
|
+
/* Media thumbnail — measured off the reference panel (2026-08-09): 136×72,
|
|
123
|
+
* remove × riding the corner. Radius is the system's 4px — the repo ships
|
|
124
|
+
* 4px or full, nothing between. */
|
|
125
|
+
const THUMB_CLS = 'h-18 w-34 rounded object-cover bg-fg-04'
|
|
129
126
|
|
|
130
127
|
export default function FieldRow({
|
|
131
128
|
label,
|
|
@@ -149,7 +146,9 @@ export default function FieldRow({
|
|
|
149
146
|
aria-label={label}
|
|
150
147
|
disabled={disabled || !onPick}
|
|
151
148
|
onClick={onPick}
|
|
152
|
-
|
|
149
|
+
/* reference empty tile: FILLED, no border — a plain grey slab with the
|
|
150
|
+
* + centred (frame 2026-08-09) */
|
|
151
|
+
className="flex h-18 w-34 items-center justify-center rounded border-0 bg-fg-04 text-body transition-colors duration-150 enabled:cursor-pointer enabled:hover:text-emphasis disabled:opacity-60"
|
|
153
152
|
>
|
|
154
153
|
<Icon name="plus" size={16} />
|
|
155
154
|
</button>
|
|
@@ -162,15 +161,16 @@ export default function FieldRow({
|
|
|
162
161
|
onClick={onPick}
|
|
163
162
|
className="kol-control kol-control--filled kol-control-sm kol-mono-12 w-full text-left text-meta enabled:cursor-pointer disabled:opacity-60"
|
|
164
163
|
>
|
|
165
|
-
Choose
|
|
164
|
+
Choose File...
|
|
166
165
|
</button>
|
|
167
166
|
)
|
|
168
167
|
|
|
169
168
|
let control = null
|
|
170
169
|
if (type === 'text') {
|
|
171
170
|
control = (
|
|
172
|
-
/* sm —
|
|
173
|
-
*
|
|
171
|
+
/* sm — the repo's control rung, no special box for this component
|
|
172
|
+
* (user ruling 2026-08-09: "why would this component be different
|
|
173
|
+
* than every other rule in the repo"). */
|
|
174
174
|
<Input
|
|
175
175
|
size="sm"
|
|
176
176
|
value={value ?? ''}
|
|
@@ -183,40 +183,60 @@ export default function FieldRow({
|
|
|
183
183
|
} else if (type === 'status') {
|
|
184
184
|
control = <StatusChip value={value} options={options} onChange={onChange} />
|
|
185
185
|
} else if (type === 'select') {
|
|
186
|
-
|
|
186
|
+
/* full-width — the reference's focus selects span the control column,
|
|
187
|
+
* caret at the far edge (panel frame 2026-08-09) */
|
|
188
|
+
control = <Dropdown options={options} value={value} onChange={onChange} className="w-full" />
|
|
187
189
|
} else if (type === 'media') {
|
|
188
190
|
control = value ? (
|
|
189
|
-
<span className="
|
|
191
|
+
<span className="relative inline-flex">
|
|
190
192
|
<img src={value.thumb ?? value.url} alt={value.name ?? ''} className={THUMB_CLS} />
|
|
191
193
|
{onChange && (
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
194
|
+
/* the reference badge: ALWAYS visible, a small dark disc riding the
|
|
195
|
+
* tile's corner — scrim ink is theme-invariant over imagery (panel
|
|
196
|
+
* frame 2026-08-09; hover-reveal repealed) */
|
|
197
|
+
<button
|
|
198
|
+
type="button"
|
|
197
199
|
aria-label={label}
|
|
198
|
-
className="absolute top-1 right-1 opacity-0 group-hover:opacity-100 focus-visible:opacity-100"
|
|
199
200
|
onClick={() => onChange(null)}
|
|
200
|
-
|
|
201
|
+
className="absolute top-1 right-1 inline-flex h-5 w-5 cursor-pointer items-center justify-center rounded-full border-0 kol-overlay-scrim text-white"
|
|
202
|
+
>
|
|
203
|
+
<Icon name="x" size={10} />
|
|
204
|
+
</button>
|
|
201
205
|
)}
|
|
202
206
|
</span>
|
|
203
207
|
) : mediaEmpty
|
|
204
208
|
} else if (type === 'file') {
|
|
205
209
|
control = value ? (
|
|
206
|
-
/*
|
|
207
|
-
*
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
210
|
+
/* the reference's file value is a FULL-WIDTH filled row — filename at
|
|
211
|
+
* the leading edge (verbatim, data is never case-transformed), × at the
|
|
212
|
+
* far end (panel frame 2026-08-09; the kol-tag--data chip repealed) */
|
|
213
|
+
<span className="kol-control kol-control--filled kol-control-sm kol-mono-12 flex w-full items-center gap-2">
|
|
214
|
+
<span className="min-w-0 flex-1 truncate">{value.name ?? value}</span>
|
|
215
|
+
{onChange && (
|
|
216
|
+
<button
|
|
217
|
+
type="button"
|
|
218
|
+
aria-label={label}
|
|
219
|
+
onClick={() => onChange(null)}
|
|
220
|
+
className="inline-flex shrink-0 cursor-pointer border-0 bg-transparent p-0 text-body hover:text-emphasis"
|
|
221
|
+
>
|
|
222
|
+
<Icon name="x" size={12} />
|
|
223
|
+
</button>
|
|
224
|
+
)}
|
|
225
|
+
</span>
|
|
211
226
|
) : fileEmpty
|
|
212
227
|
}
|
|
213
228
|
|
|
214
229
|
return (
|
|
215
|
-
|
|
216
|
-
|
|
230
|
+
/* reference rhythm (2026-08-09): roomy rows on hairlines, narrow label
|
|
231
|
+
* column, labels white; media rows top-align their label to the tile.
|
|
232
|
+
* Controls stay on the repo's sm rung — no special box here. */
|
|
233
|
+
/* .kol-field-row carries the grid anatomy in kol-theme (package chrome
|
|
234
|
+
* never rides arbitrary utilities — they can miss generation) */
|
|
235
|
+
<div className="kol-field-row items-center gap-x-6 py-3 border-b border-fg-08">
|
|
236
|
+
<div className={`kol-mono-12 text-emphasis${type === 'media' ? ' self-start pt-1' : ''}`}>{label}</div>
|
|
217
237
|
<div className="min-w-0">{control}</div>
|
|
218
238
|
{hint != null && (
|
|
219
|
-
<div className="col-start-2 kol-helper-
|
|
239
|
+
<div className="col-start-2 kol-helper-12 text-meta pt-2 truncate">{hint}</div>
|
|
220
240
|
)}
|
|
221
241
|
</div>
|
|
222
242
|
)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from 'react'
|
|
2
2
|
import { createPortal } from 'react-dom'
|
|
3
|
+
import { Icon } from '@kolkrabbi/kol-icons'
|
|
3
4
|
import Button from '../atoms/Button.jsx'
|
|
4
5
|
import usePrefersReducedMotion from '../hooks/usePrefersReducedMotion.js'
|
|
5
6
|
|
|
@@ -28,6 +29,7 @@ const FOCUSABLE =
|
|
|
28
29
|
* @param {string} side 'left' | 'right' — edge the panel slides from
|
|
29
30
|
* @param {number|string} width panel width (px number or CSS length); omit for full-width sheet
|
|
30
31
|
* @param {ReactNode} header header-row content beside the close button (replaces the source's baked-in wordmark)
|
|
32
|
+
* @param {boolean} backdrop render the dimming scrim (default true); false = panel alone, no darken/blur, close via × / Esc
|
|
31
33
|
* @param {ReactNode} children scrollable panel body
|
|
32
34
|
* @param {string} className extra classes on the panel
|
|
33
35
|
*/
|
|
@@ -38,6 +40,7 @@ export default function ShellDrawer({
|
|
|
38
40
|
width,
|
|
39
41
|
header,
|
|
40
42
|
closeSide = 'end',
|
|
43
|
+
backdrop = true,
|
|
41
44
|
children,
|
|
42
45
|
className = '',
|
|
43
46
|
}) {
|
|
@@ -134,26 +137,40 @@ export default function ShellDrawer({
|
|
|
134
137
|
|
|
135
138
|
return createPortal(
|
|
136
139
|
<>
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
140
|
+
{backdrop && (
|
|
141
|
+
<div
|
|
142
|
+
className={`fixed inset-0 z-[100] kol-overlay-scrim ${motionBackdrop}`}
|
|
143
|
+
onClick={onClose}
|
|
144
|
+
aria-hidden="true"
|
|
145
|
+
/>
|
|
146
|
+
)}
|
|
142
147
|
<div
|
|
143
148
|
ref={panelRef}
|
|
144
149
|
role="dialog"
|
|
145
150
|
aria-modal="true"
|
|
146
151
|
tabIndex={-1}
|
|
147
|
-
className={`fixed inset-y-0 z-[200] flex max-w-full flex-col bg-surface-primary px-4 py-4
|
|
152
|
+
className={`fixed inset-y-0 z-[200] flex max-w-full flex-col bg-surface-primary px-4 py-4 outline-none md:px-5 lg:px-6 ${backdrop ? 'shadow-2xl' : ''} ${
|
|
148
153
|
side === 'right' ? 'right-0 border-l' : 'left-0 border-r'
|
|
149
154
|
} border-fg-08 ${width == null ? 'w-full' : ''} ${motionPanel} ${className}`}
|
|
150
155
|
style={width != null ? { width: typeof width === 'number' ? `${width}px` : width } : undefined}
|
|
151
156
|
>
|
|
152
|
-
|
|
157
|
+
{/* closeSide="start": the reference sets the × glyph ~9px deeper than
|
|
158
|
+
* the label column's edge, with extra top air (both reference frames
|
|
159
|
+
* 2026-08-09 measure the same inset) */}
|
|
160
|
+
<div className={`mb-6 flex items-center gap-4${closeSide === 'start' ? ' pt-3 pl-2' : ''}`}>
|
|
153
161
|
{/* closeSide="start" — the record-surface anatomy (reference frame,
|
|
154
162
|
* 2026-08-09): × leads, the header's actions sit at the far end. */}
|
|
155
163
|
{closeSide === 'start' && (
|
|
156
|
-
|
|
164
|
+
/* bare glyph, no container (user 2026-08-09: "the X close icon
|
|
165
|
+
* does not need a container") — body ink at rest, emphasis on hover */
|
|
166
|
+
<button
|
|
167
|
+
type="button"
|
|
168
|
+
aria-label="Close"
|
|
169
|
+
onClick={onClose}
|
|
170
|
+
className="inline-flex shrink-0 cursor-pointer border-0 bg-transparent p-0 text-body transition-colors duration-150 hover:text-emphasis"
|
|
171
|
+
>
|
|
172
|
+
<Icon name="x" size={18} />
|
|
173
|
+
</button>
|
|
157
174
|
)}
|
|
158
175
|
{header != null && <div className="min-w-0 flex-1">{header}</div>}
|
|
159
176
|
{/* The box has an owner (2026-08-01). This hand-wrote the icon-button
|
|
@@ -4,6 +4,7 @@ import { Icon } from '@kolkrabbi/kol-icons'
|
|
|
4
4
|
import Button from '../atoms/Button'
|
|
5
5
|
import SearchInput from '../molecules/SearchInput'
|
|
6
6
|
import ToggleCheckbox from '../atoms/ToggleCheckbox'
|
|
7
|
+
import IconFrame from '../atoms/IconFrame'
|
|
7
8
|
import ShellDrawer from '../molecules/ShellDrawer'
|
|
8
9
|
import FieldRow, { StatusChip } from '../molecules/FieldRow'
|
|
9
10
|
import { Tooltip } from '../atoms/Popover'
|
|
@@ -100,7 +101,14 @@ export default function RecordManager({
|
|
|
100
101
|
|
|
101
102
|
const rowId = (row, i) => row.id ?? i
|
|
102
103
|
|
|
104
|
+
/* panel = 60% of the table area (user ruling 2026-08-09) — the surface's
|
|
105
|
+
* own width IS the table pane, measured at open. ponytail: not re-measured
|
|
106
|
+
* on live resize. */
|
|
107
|
+
const [panelW, setPanelW] = useState(null)
|
|
108
|
+
|
|
103
109
|
const openRecord = (row) => {
|
|
110
|
+
const w = wrapRef.current?.getBoundingClientRect().width
|
|
111
|
+
setPanelW(w ? Math.round(w * 0.6) : null)
|
|
104
112
|
setActive(row)
|
|
105
113
|
onSelectRow?.(row)
|
|
106
114
|
}
|
|
@@ -153,14 +161,22 @@ export default function RecordManager({
|
|
|
153
161
|
* the text (user frames 2026-08-09). Hover-revealed per row. */
|
|
154
162
|
<span className="flex items-center justify-between gap-2 min-w-0 max-w-full">
|
|
155
163
|
<span className="truncate text-emphasis">{col.render ? col.render(row) : row[col.accessor]}</span>
|
|
156
|
-
|
|
157
|
-
|
|
164
|
+
{/* the ICON CONTAINER is IconFrame (2026-08-01 ruling), box-model
|
|
165
|
+
* proper (user 2026-08-09, "padding should control the size"):
|
|
166
|
+
* the box stays pinned, the glyph FILLS the content box, and
|
|
167
|
+
* padding is the only inset knob — p-[3px] on user instruction
|
|
168
|
+
* (2026-08-09, "use 3px … use [3px]"); arbitrary utilities have
|
|
169
|
+
* missed consumer generation before — if this renders unpadded
|
|
170
|
+
* in a consumer, this class is why. */}
|
|
171
|
+
<IconFrame
|
|
172
|
+
name="toggle-overlay"
|
|
173
|
+
variant="outline"
|
|
174
|
+
size="sm"
|
|
175
|
+
iconSize="100%"
|
|
158
176
|
aria-label="Toggle overlay"
|
|
159
|
-
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"
|
|
160
177
|
onClick={() => openRecord(row)}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
</button>
|
|
178
|
+
className="p-[3px] opacity-0 group-hover:opacity-100 focus-visible:opacity-100"
|
|
179
|
+
/>
|
|
164
180
|
</span>
|
|
165
181
|
),
|
|
166
182
|
}
|
|
@@ -181,12 +197,11 @@ export default function RecordManager({
|
|
|
181
197
|
return {
|
|
182
198
|
...col,
|
|
183
199
|
/* the reference's select cell ("Center ▾") is a PILL, not the square
|
|
184
|
-
* Dropdown trigger — the StatusChip apparatus
|
|
185
|
-
*
|
|
186
|
-
*
|
|
200
|
+
* Dropdown trigger — the StatusChip apparatus in its NEUTRAL grey,
|
|
201
|
+
* the same chip Archived wears (user ruling 2026-08-09: "grey like
|
|
202
|
+
* 'archived'"). Panel select fields stay Dropdown. */
|
|
187
203
|
render: (row) => (
|
|
188
204
|
<StatusChip
|
|
189
|
-
variant="primary"
|
|
190
205
|
value={row[col.accessor]}
|
|
191
206
|
options={col.options ?? []}
|
|
192
207
|
onChange={(next) => col.onSelectChange?.(row, next)}
|
|
@@ -200,8 +215,9 @@ export default function RecordManager({
|
|
|
200
215
|
render: (row) => {
|
|
201
216
|
const media = row[col.accessor]
|
|
202
217
|
return media ? (
|
|
203
|
-
/*
|
|
204
|
-
|
|
218
|
+
/* measured off the reference frame: the tile is chip-height —
|
|
219
|
+
* ~24px tall, 2:1 — not a 40px block (user re-call 2026-08-09) */
|
|
220
|
+
<img src={media.thumb ?? media.url} alt={media.name ?? ''} className="h-6 w-12 rounded object-cover bg-fg-04" />
|
|
205
221
|
) : (
|
|
206
222
|
<span className="kol-helper-10 text-meta">—</span>
|
|
207
223
|
)
|
|
@@ -323,8 +339,9 @@ export default function RecordManager({
|
|
|
323
339
|
open={!!active}
|
|
324
340
|
onClose={() => setActive(null)}
|
|
325
341
|
side="right"
|
|
326
|
-
width=
|
|
342
|
+
width={panelW ? `${panelW}px` : '50vw'}
|
|
327
343
|
closeSide="start"
|
|
344
|
+
backdrop={false}
|
|
328
345
|
header={
|
|
329
346
|
/* reference header anatomy (frame 45, 2026-08-09): × leads the row
|
|
330
347
|
* (closeSide above); every action clusters at the far end —
|
|
@@ -346,7 +363,9 @@ export default function RecordManager({
|
|
|
346
363
|
}
|
|
347
364
|
>
|
|
348
365
|
{record && (
|
|
349
|
-
|
|
366
|
+
/* border-t: the reference rules EVERY seam — including above the
|
|
367
|
+
* first row; each FieldRow carries its own border-b (frame 2026-08-09) */
|
|
368
|
+
<div className="flex flex-col border-t border-fg-08">
|
|
350
369
|
{fields.map((f) => (
|
|
351
370
|
<FieldRow
|
|
352
371
|
key={f.key}
|