@kolkrabbi/kol-component 0.32.4 → 0.33.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.33.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",
|
|
@@ -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
|
|
@@ -37,8 +34,13 @@ import Dropdown from './Dropdown'
|
|
|
37
34
|
*
|
|
38
35
|
* Options: strings, or { value, label, tone } — tone ∈ success|warning|error|
|
|
39
36
|
* info maps onto the --ui-* ladder (.kol-status-chip--* in kol-theme); the
|
|
40
|
-
* chip wears the ACTIVE option's tone. No tone → the plain Tag look.
|
|
41
|
-
|
|
37
|
+
* chip wears the ACTIVE option's tone. No tone → the plain Tag look.
|
|
38
|
+
*
|
|
39
|
+
* variant="primary" — the toneless pill wears the Dropdown-primary fill
|
|
40
|
+
* instead of the neutral mix (RecordManager's select columns, user frame
|
|
41
|
+
* 2026-08-09: "use status pill, but same look as dropdown primary"). An
|
|
42
|
+
* option's tone always outranks the variant. */
|
|
43
|
+
export function StatusChip({ value, options = [], onChange, variant = 'neutral', size = 'sm' }) {
|
|
42
44
|
const [open, setOpen] = useState(false)
|
|
43
45
|
const popover = usePopover({
|
|
44
46
|
open,
|
|
@@ -49,20 +51,21 @@ export function StatusChip({ value, options = [], onChange, size = 'sm' }) {
|
|
|
49
51
|
})
|
|
50
52
|
const norm = options.map((opt) => (typeof opt === 'object' ? opt : { value: opt, label: opt }))
|
|
51
53
|
const activeOpt = norm.find((o) => o.value === value)
|
|
52
|
-
const toneCls = activeOpt?.tone
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
54
|
+
const toneCls = activeOpt?.tone
|
|
55
|
+
? ` kol-status-chip--${activeOpt.tone}`
|
|
56
|
+
: variant === 'primary' ? ' kol-status-chip--primary' : ''
|
|
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. */
|
|
60
63
|
const chip = (interactive) => (
|
|
61
64
|
<button
|
|
62
65
|
type="button"
|
|
63
66
|
disabled={!interactive || undefined}
|
|
64
67
|
aria-expanded={interactive ? open : undefined}
|
|
65
|
-
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' : ''}`}
|
|
66
69
|
onClick={interactive ? () => setOpen((o) => !o) : undefined}
|
|
67
70
|
>
|
|
68
71
|
{activeOpt?.label ?? value}
|
|
@@ -71,7 +74,7 @@ export function StatusChip({ value, options = [], onChange, size = 'sm' }) {
|
|
|
71
74
|
className={`inline-flex shrink-0 transition-transform duration-150${open ? ' rotate-180' : ''}`}
|
|
72
75
|
aria-hidden="true"
|
|
73
76
|
>
|
|
74
|
-
<Icon name="chevron-down" size={
|
|
77
|
+
<Icon name="chevron-down" size={10} />
|
|
75
78
|
</span>
|
|
76
79
|
)}
|
|
77
80
|
</button>
|
|
@@ -116,9 +119,10 @@ export function StatusChip({ value, options = [], onChange, size = 'sm' }) {
|
|
|
116
119
|
)
|
|
117
120
|
}
|
|
118
121
|
|
|
119
|
-
/* Media thumbnail — the reference panel
|
|
120
|
-
*
|
|
121
|
-
|
|
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'
|
|
122
126
|
|
|
123
127
|
export default function FieldRow({
|
|
124
128
|
label,
|
|
@@ -142,7 +146,9 @@ export default function FieldRow({
|
|
|
142
146
|
aria-label={label}
|
|
143
147
|
disabled={disabled || !onPick}
|
|
144
148
|
onClick={onPick}
|
|
145
|
-
|
|
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"
|
|
146
152
|
>
|
|
147
153
|
<Icon name="plus" size={16} />
|
|
148
154
|
</button>
|
|
@@ -155,15 +161,16 @@ export default function FieldRow({
|
|
|
155
161
|
onClick={onPick}
|
|
156
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"
|
|
157
163
|
>
|
|
158
|
-
Choose
|
|
164
|
+
Choose File...
|
|
159
165
|
</button>
|
|
160
166
|
)
|
|
161
167
|
|
|
162
168
|
let control = null
|
|
163
169
|
if (type === 'text') {
|
|
164
170
|
control = (
|
|
165
|
-
/* sm —
|
|
166
|
-
*
|
|
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"). */
|
|
167
174
|
<Input
|
|
168
175
|
size="sm"
|
|
169
176
|
value={value ?? ''}
|
|
@@ -176,40 +183,60 @@ export default function FieldRow({
|
|
|
176
183
|
} else if (type === 'status') {
|
|
177
184
|
control = <StatusChip value={value} options={options} onChange={onChange} />
|
|
178
185
|
} else if (type === 'select') {
|
|
179
|
-
|
|
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" />
|
|
180
189
|
} else if (type === 'media') {
|
|
181
190
|
control = value ? (
|
|
182
|
-
<span className="
|
|
191
|
+
<span className="relative inline-flex">
|
|
183
192
|
<img src={value.thumb ?? value.url} alt={value.name ?? ''} className={THUMB_CLS} />
|
|
184
193
|
{onChange && (
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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"
|
|
190
199
|
aria-label={label}
|
|
191
|
-
className="absolute top-1 right-1 opacity-0 group-hover:opacity-100 focus-visible:opacity-100"
|
|
192
200
|
onClick={() => onChange(null)}
|
|
193
|
-
|
|
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>
|
|
194
205
|
)}
|
|
195
206
|
</span>
|
|
196
207
|
) : mediaEmpty
|
|
197
208
|
} else if (type === 'file') {
|
|
198
209
|
control = value ? (
|
|
199
|
-
/*
|
|
200
|
-
*
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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>
|
|
204
226
|
) : fileEmpty
|
|
205
227
|
}
|
|
206
228
|
|
|
207
229
|
return (
|
|
208
|
-
|
|
209
|
-
|
|
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>
|
|
210
237
|
<div className="min-w-0">{control}</div>
|
|
211
238
|
{hint != null && (
|
|
212
|
-
<div className="col-start-2 kol-helper-
|
|
239
|
+
<div className="col-start-2 kol-helper-12 text-meta pt-2 truncate">{hint}</div>
|
|
213
240
|
)}
|
|
214
241
|
</div>
|
|
215
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,8 +4,8 @@ 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
|
-
import Dropdown from '../molecules/Dropdown'
|
|
9
9
|
import FieldRow, { StatusChip } from '../molecules/FieldRow'
|
|
10
10
|
import { Tooltip } from '../atoms/Popover'
|
|
11
11
|
import Table from './Table'
|
|
@@ -101,7 +101,14 @@ export default function RecordManager({
|
|
|
101
101
|
|
|
102
102
|
const rowId = (row, i) => row.id ?? i
|
|
103
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
|
+
|
|
104
109
|
const openRecord = (row) => {
|
|
110
|
+
const w = wrapRef.current?.getBoundingClientRect().width
|
|
111
|
+
setPanelW(w ? Math.round(w * 0.6) : null)
|
|
105
112
|
setActive(row)
|
|
106
113
|
onSelectRow?.(row)
|
|
107
114
|
}
|
|
@@ -154,14 +161,20 @@ export default function RecordManager({
|
|
|
154
161
|
* the text (user frames 2026-08-09). Hover-revealed per row. */
|
|
155
162
|
<span className="flex items-center justify-between gap-2 min-w-0 max-w-full">
|
|
156
163
|
<span className="truncate text-emphasis">{col.render ? col.render(row) : row[col.accessor]}</span>
|
|
157
|
-
|
|
158
|
-
|
|
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-0.5 = the 2px. */}
|
|
168
|
+
<IconFrame
|
|
169
|
+
name="toggle-overlay"
|
|
170
|
+
variant="outline"
|
|
171
|
+
size="sm"
|
|
172
|
+
iconSize="100%"
|
|
159
173
|
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"
|
|
161
174
|
onClick={() => openRecord(row)}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
175
|
+
style={{ padding: 3 }}
|
|
176
|
+
className="opacity-0 group-hover:opacity-100 focus-visible:opacity-100"
|
|
177
|
+
/>
|
|
165
178
|
</span>
|
|
166
179
|
),
|
|
167
180
|
}
|
|
@@ -181,10 +194,14 @@ export default function RecordManager({
|
|
|
181
194
|
if (col.type === 'select') {
|
|
182
195
|
return {
|
|
183
196
|
...col,
|
|
197
|
+
/* the reference's select cell ("Center ▾") is a PILL, not the square
|
|
198
|
+
* Dropdown trigger — the StatusChip apparatus in its NEUTRAL grey,
|
|
199
|
+
* the same chip Archived wears (user ruling 2026-08-09: "grey like
|
|
200
|
+
* 'archived'"). Panel select fields stay Dropdown. */
|
|
184
201
|
render: (row) => (
|
|
185
|
-
<
|
|
186
|
-
options={col.options ?? []}
|
|
202
|
+
<StatusChip
|
|
187
203
|
value={row[col.accessor]}
|
|
204
|
+
options={col.options ?? []}
|
|
188
205
|
onChange={(next) => col.onSelectChange?.(row, next)}
|
|
189
206
|
/>
|
|
190
207
|
),
|
|
@@ -196,7 +213,9 @@ export default function RecordManager({
|
|
|
196
213
|
render: (row) => {
|
|
197
214
|
const media = row[col.accessor]
|
|
198
215
|
return media ? (
|
|
199
|
-
|
|
216
|
+
/* measured off the reference frame: the tile is chip-height —
|
|
217
|
+
* ~24px tall, 2:1 — not a 40px block (user re-call 2026-08-09) */
|
|
218
|
+
<img src={media.thumb ?? media.url} alt={media.name ?? ''} className="h-6 w-12 rounded object-cover bg-fg-04" />
|
|
200
219
|
) : (
|
|
201
220
|
<span className="kol-helper-10 text-meta">—</span>
|
|
202
221
|
)
|
|
@@ -292,8 +311,10 @@ export default function RecordManager({
|
|
|
292
311
|
rows={rows}
|
|
293
312
|
width="column"
|
|
294
313
|
compact
|
|
314
|
+
/* align-middle: cell content centres in the row (reference grammar,
|
|
315
|
+
* user frame 2026-08-09) — scoped here; Table's base stays v-top */
|
|
295
316
|
rowClassName={(row, i) =>
|
|
296
|
-
`group${drag && i === drag.from ? ' opacity-40' : ''}${
|
|
317
|
+
`group align-middle${drag && i === drag.from ? ' opacity-40' : ''}${
|
|
297
318
|
drag && i === drag.over && drag.over !== drag.from ? ' bg-fg-04' : ''
|
|
298
319
|
}`
|
|
299
320
|
}
|
|
@@ -316,8 +337,9 @@ export default function RecordManager({
|
|
|
316
337
|
open={!!active}
|
|
317
338
|
onClose={() => setActive(null)}
|
|
318
339
|
side="right"
|
|
319
|
-
width=
|
|
340
|
+
width={panelW ? `${panelW}px` : '50vw'}
|
|
320
341
|
closeSide="start"
|
|
342
|
+
backdrop={false}
|
|
321
343
|
header={
|
|
322
344
|
/* reference header anatomy (frame 45, 2026-08-09): × leads the row
|
|
323
345
|
* (closeSide above); every action clusters at the far end —
|
|
@@ -339,7 +361,9 @@ export default function RecordManager({
|
|
|
339
361
|
}
|
|
340
362
|
>
|
|
341
363
|
{record && (
|
|
342
|
-
|
|
364
|
+
/* border-t: the reference rules EVERY seam — including above the
|
|
365
|
+
* first row; each FieldRow carries its own border-b (frame 2026-08-09) */
|
|
366
|
+
<div className="flex flex-col border-t border-fg-08">
|
|
343
367
|
{fields.map((f) => (
|
|
344
368
|
<FieldRow
|
|
345
369
|
key={f.key}
|