@kolkrabbi/kol-component 0.26.1 → 0.28.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.28.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",
|
package/src/atoms/CopyButton.jsx
CHANGED
|
@@ -1,25 +1,27 @@
|
|
|
1
1
|
import { useState } from 'react'
|
|
2
|
+
import { Icon } from '@kolkrabbi/kol-icons'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
|
-
* CopyButton — copy-to-clipboard
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* CopyButton — THE copy-to-clipboard control (2026-08-09 user ruling): the
|
|
6
|
+
* 32×32 icon button — `copy` glyph flipping to `check` for 2s on copied,
|
|
7
|
+
* no text label. This is the button CodeBlock carried privately since the
|
|
8
|
+
* 2026-07-28 elder replication, promoted to the one shared atom; the old
|
|
9
|
+
* Copy/Copied label chip (one-off SVGs outside the icon set) is retired.
|
|
10
|
+
* Chrome comes from .kol-copy-btn (kol-theme); parents add their own
|
|
11
|
+
* positioning class (e.g. CodeBlock's .kol-codeblock-copy).
|
|
8
12
|
*
|
|
9
13
|
* Props:
|
|
10
14
|
* text — string (or () => string) written to the clipboard
|
|
11
|
-
* label — show the Copy/Copied text next to the icon (default true;
|
|
12
|
-
* false = icon-only for tight spots)
|
|
13
15
|
* className — extra classes (positioning etc.)
|
|
14
16
|
*/
|
|
15
|
-
export default function CopyButton({ text,
|
|
17
|
+
export default function CopyButton({ text, className = '', ...props }) {
|
|
16
18
|
const [copied, setCopied] = useState(false)
|
|
17
19
|
|
|
18
20
|
const onCopy = async () => {
|
|
19
21
|
try {
|
|
20
22
|
await navigator.clipboard.writeText(typeof text === 'function' ? text() : String(text ?? ''))
|
|
21
23
|
setCopied(true)
|
|
22
|
-
setTimeout(() => setCopied(false),
|
|
24
|
+
setTimeout(() => setCopied(false), 2000)
|
|
23
25
|
} catch {
|
|
24
26
|
/* clipboard blocked — silent */
|
|
25
27
|
}
|
|
@@ -34,17 +36,7 @@ export default function CopyButton({ text, label = true, className = '', ...prop
|
|
|
34
36
|
title={copied ? 'Copied' : 'Copy'}
|
|
35
37
|
{...props}
|
|
36
38
|
>
|
|
37
|
-
{copied ?
|
|
38
|
-
<svg viewBox="0 0 20 20" width="14" height="14" aria-hidden="true">
|
|
39
|
-
<path d="M5 10 L9 14 L15 6" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" />
|
|
40
|
-
</svg>
|
|
41
|
-
) : (
|
|
42
|
-
<svg viewBox="0 0 20 20" width="14" height="14" aria-hidden="true">
|
|
43
|
-
<rect x="6" y="3" width="10" height="12" rx="1" fill="none" stroke="currentColor" strokeWidth="1.4" />
|
|
44
|
-
<rect x="4" y="6" width="10" height="12" rx="1" fill="none" stroke="currentColor" strokeWidth="1.4" />
|
|
45
|
-
</svg>
|
|
46
|
-
)}
|
|
47
|
-
{label && <span>{copied ? 'Copied' : 'Copy'}</span>}
|
|
39
|
+
<Icon name={copied ? 'check' : 'copy'} size={16} />
|
|
48
40
|
</button>
|
|
49
41
|
)
|
|
50
42
|
}
|
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
import { useState } from 'react'
|
|
2
1
|
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
|
|
3
2
|
import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism'
|
|
4
|
-
import
|
|
3
|
+
import CopyButton from '../atoms/CopyButton.jsx'
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* CodeBlock — REPLICATED from the elder reference
|
|
8
7
|
* (kol-website/packages/ui/src/molecules/CodeBlock.jsx, 2026-07-28 user
|
|
9
8
|
* mandate): react-syntax-highlighter (Prism) with the oneDark theme flattened
|
|
10
9
|
* onto KOL chrome — transparent bg, 14px/1.6 mono, no text-shadow — a single
|
|
11
|
-
* filename-or-language chip, and
|
|
12
|
-
*
|
|
10
|
+
* filename-or-language chip, and the CopyButton atom positioned in-frame
|
|
11
|
+
* (.kol-codeblock-copy). Chrome lives in kol-theme (kol-components-molecules.css).
|
|
13
12
|
*
|
|
14
13
|
* Input shapes (first match wins per field):
|
|
15
14
|
* • Portable Text: `value={{ code, language, filename }}`
|
|
@@ -25,12 +24,6 @@ import { Icon } from '@kolkrabbi/kol-icons'
|
|
|
25
24
|
* @param {boolean} [bare] drop the FRAME; the host owns it. Not a size.
|
|
26
25
|
*/
|
|
27
26
|
|
|
28
|
-
const CheckMarkIcon = () => (
|
|
29
|
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
30
|
-
<path d="M19.4697 6.41987C19.7626 6.12697 20.2373 6.12697 20.5302 6.41987C20.8231 6.71276 20.8231 7.18752 20.5302 7.48041L10.9443 17.0663C9.87038 18.1401 8.12951 18.1401 7.05561 17.0663L3.46967 13.4804C3.17678 13.1875 3.17678 12.7128 3.46967 12.4199C3.76256 12.127 4.23732 12.127 4.53022 12.4199L8.11615 16.0058C8.60427 16.4938 9.39561 16.4938 9.88373 16.0058L19.4697 6.41987Z" fill="currentColor"/>
|
|
31
|
-
</svg>
|
|
32
|
-
)
|
|
33
|
-
|
|
34
27
|
const syntaxTheme = (foregroundToken = 80) => ({
|
|
35
28
|
...oneDark,
|
|
36
29
|
'pre[class*="language-"]': {
|
|
@@ -76,22 +69,10 @@ const syntaxTheme = (foregroundToken = 80) => ({
|
|
|
76
69
|
* on both axes. Size is INDEPENDENT of `bare`: bare removes the frame, size
|
|
77
70
|
* sets the box, and a bare block still has one. */
|
|
78
71
|
export default function CodeBlock({ children, code: codeProp, language: languageProp, filename: filenameProp, value, bare = false, size = 'md' }) {
|
|
79
|
-
const [copied, setCopied] = useState(false)
|
|
80
|
-
|
|
81
72
|
const code = String(value?.code ?? codeProp ?? children ?? '')
|
|
82
73
|
const language = value?.language ?? languageProp ?? 'text'
|
|
83
74
|
const filename = value?.filename ?? filenameProp
|
|
84
75
|
|
|
85
|
-
const handleCopy = async () => {
|
|
86
|
-
try {
|
|
87
|
-
await navigator.clipboard.writeText(code)
|
|
88
|
-
setCopied(true)
|
|
89
|
-
setTimeout(() => setCopied(false), 2000)
|
|
90
|
-
} catch {
|
|
91
|
-
/* clipboard blocked — silent */
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
76
|
return (
|
|
96
77
|
<div className={bare ? '' : 'kol-codeblock-wrapper'}>
|
|
97
78
|
<div className={`kol-codeblock kol-codeblock--${size}${bare ? ' kol-codeblock--bare' : ''}`}>
|
|
@@ -124,15 +105,7 @@ export default function CodeBlock({ children, code: codeProp, language: language
|
|
|
124
105
|
>
|
|
125
106
|
{code}
|
|
126
107
|
</SyntaxHighlighter>
|
|
127
|
-
<
|
|
128
|
-
type="button"
|
|
129
|
-
className="kol-codeblock-copy"
|
|
130
|
-
onClick={handleCopy}
|
|
131
|
-
aria-label={copied ? 'Code copied!' : 'Copy code'}
|
|
132
|
-
title={copied ? 'Code copied!' : 'Copy code'}
|
|
133
|
-
>
|
|
134
|
-
{copied ? <CheckMarkIcon /> : <Icon name="copy" size={16} />}
|
|
135
|
-
</button>
|
|
108
|
+
<CopyButton text={code} className="kol-codeblock-copy" />
|
|
136
109
|
</div>
|
|
137
110
|
</div>
|
|
138
111
|
)
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { useState } from 'react'
|
|
2
|
+
import { Icon } from '@kolkrabbi/kol-icons'
|
|
2
3
|
import Input from '../atoms/Input'
|
|
3
4
|
import Tag from '../atoms/Tag'
|
|
4
5
|
import Button from '../atoms/Button'
|
|
5
6
|
import { usePopover, PopoverPanel } from '../atoms/Popover'
|
|
7
|
+
import { INDICATOR } from '../hooks/glyphLadders.js'
|
|
6
8
|
import Dropdown from './Dropdown'
|
|
7
9
|
|
|
8
10
|
/**
|
|
@@ -23,11 +25,19 @@ import Dropdown from './Dropdown'
|
|
|
23
25
|
* object back through onChange untouched.
|
|
24
26
|
*/
|
|
25
27
|
|
|
26
|
-
/* StatusChip — the interactive status control ("Live ▾" in the reference
|
|
28
|
+
/* StatusChip — the interactive status control ("Live ▾" in the reference,
|
|
29
|
+
* rebuilt to it 2026-08-09 after the user's screenshots: the chip carries a
|
|
30
|
+
* TONE tint + a trailing caret at the INDICATOR rung, and the open panel is
|
|
31
|
+
* the reference's grammar — current chip on top, hairline, then option rows
|
|
32
|
+
* with a leading check column).
|
|
27
33
|
* Chip taxonomy (Tag source, 2026-07-30): interactive → Tag, never Pill. The
|
|
28
34
|
* menu is usePopover/PopoverPanel — the same primitive Dropdown builds on —
|
|
29
35
|
* with click handled by the Tag itself (click: false here) so the chip stays
|
|
30
|
-
* the only trigger. Shared by FieldRow and RecordManager's status column.
|
|
36
|
+
* the only trigger. Shared by FieldRow and RecordManager's status column.
|
|
37
|
+
*
|
|
38
|
+
* Options: strings, or { value, label, tone } — tone ∈ success|warning|error|
|
|
39
|
+
* 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. */
|
|
31
41
|
export function StatusChip({ value, options = [], onChange, size = 'sm' }) {
|
|
32
42
|
const [open, setOpen] = useState(false)
|
|
33
43
|
const popover = usePopover({
|
|
@@ -37,30 +47,58 @@ export function StatusChip({ value, options = [], onChange, size = 'sm' }) {
|
|
|
37
47
|
click: false,
|
|
38
48
|
role: 'listbox',
|
|
39
49
|
})
|
|
50
|
+
const norm = options.map((opt) => (typeof opt === 'object' ? opt : { value: opt, label: opt }))
|
|
51
|
+
const activeOpt = norm.find((o) => o.value === value)
|
|
52
|
+
const toneCls = activeOpt?.tone ? ` kol-status-chip--${activeOpt.tone}` : ''
|
|
53
|
+
const chip = (interactive) => (
|
|
54
|
+
<Tag
|
|
55
|
+
variant="primary"
|
|
56
|
+
size={size}
|
|
57
|
+
hash={false}
|
|
58
|
+
active={interactive && open}
|
|
59
|
+
className={`kol-status-chip${toneCls}`}
|
|
60
|
+
onClick={interactive ? () => setOpen((o) => !o) : undefined}
|
|
61
|
+
>
|
|
62
|
+
{activeOpt?.label ?? value}
|
|
63
|
+
{interactive && (
|
|
64
|
+
<Icon
|
|
65
|
+
name="chevron-down"
|
|
66
|
+
size={INDICATOR[size] ?? INDICATOR.sm}
|
|
67
|
+
className={`transition-transform duration-150${open ? ' rotate-180' : ''}`}
|
|
68
|
+
aria-hidden="true"
|
|
69
|
+
/>
|
|
70
|
+
)}
|
|
71
|
+
</Tag>
|
|
72
|
+
)
|
|
40
73
|
return (
|
|
41
74
|
<>
|
|
42
75
|
<span ref={popover.refs.setReference} {...popover.getReferenceProps()} className="inline-flex">
|
|
43
|
-
|
|
44
|
-
{value}
|
|
45
|
-
</Tag>
|
|
76
|
+
{chip(true)}
|
|
46
77
|
</span>
|
|
47
78
|
<PopoverPanel popover={popover}>
|
|
79
|
+
{/* the reference panel: current value as its chip, a hairline, then
|
|
80
|
+
* the option rows — check column always reserved so labels align */}
|
|
81
|
+
<div className="px-2 pt-2">{chip(false)}</div>
|
|
82
|
+
<div className="mt-2 border-t border-fg-08" />
|
|
48
83
|
<ul className="flex flex-col p-1 m-0 list-none">
|
|
49
|
-
{
|
|
50
|
-
const label = typeof opt === 'object' ? opt.label : opt
|
|
51
|
-
const val = typeof opt === 'object' ? opt.value : opt
|
|
84
|
+
{norm.map(({ value: val, label }) => {
|
|
52
85
|
return (
|
|
53
86
|
<li key={val}>
|
|
54
87
|
<button
|
|
55
88
|
type="button"
|
|
56
89
|
role="option"
|
|
57
90
|
aria-selected={val === value}
|
|
58
|
-
className={`kol-helper-12 w-full text-left
|
|
91
|
+
className={`kol-helper-12 w-full text-left pr-3 py-2 bg-transparent border-0 cursor-pointer rounded-sm hover:bg-fg-08 inline-flex items-center ${val === value ? 'text-emphasis' : 'text-body'}`}
|
|
59
92
|
onClick={() => {
|
|
60
93
|
onChange?.(val)
|
|
61
94
|
setOpen(false)
|
|
62
95
|
}}
|
|
63
96
|
>
|
|
97
|
+
{/* check column always reserved — labels align whether or
|
|
98
|
+
* not a row carries the mark (reference grammar) */}
|
|
99
|
+
<span className="inline-flex w-7 shrink-0 justify-center" aria-hidden="true">
|
|
100
|
+
{val === value && <Icon name="check" size={12} />}
|
|
101
|
+
</span>
|
|
64
102
|
{label}
|
|
65
103
|
</button>
|
|
66
104
|
</li>
|
|
@@ -96,7 +134,10 @@ export default function FieldRow({
|
|
|
96
134
|
let control = null
|
|
97
135
|
if (type === 'text') {
|
|
98
136
|
control = (
|
|
137
|
+
/* sm — record rows are compact chrome (the reference's density); md
|
|
138
|
+
* read as a form field ("input is large? why?", 2026-08-09). */
|
|
99
139
|
<Input
|
|
140
|
+
size="sm"
|
|
100
141
|
value={value ?? ''}
|
|
101
142
|
onChange={(e) => onChange?.(e?.target ? e.target.value : e)}
|
|
102
143
|
placeholder={placeholder}
|
|
@@ -184,10 +184,13 @@ export default function RecordManager({
|
|
|
184
184
|
render: (row) => {
|
|
185
185
|
const i = rows.indexOf(row)
|
|
186
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. */}
|
|
187
190
|
<button
|
|
188
191
|
type="button"
|
|
189
192
|
aria-label={reorderLabel(i + 1)}
|
|
190
|
-
className="cursor-grab touch-none bg-transparent border-0 p-0 text-meta hover:text-emphasis
|
|
193
|
+
className="cursor-grab touch-none bg-transparent border-0 p-0 text-meta hover:text-emphasis"
|
|
191
194
|
onPointerDown={(e) => startDrag(e, i)}
|
|
192
195
|
>
|
|
193
196
|
<Icon name="resize-grip" size={14} />
|
|
@@ -243,6 +246,11 @@ export default function RecordManager({
|
|
|
243
246
|
className="ml-2"
|
|
244
247
|
/>
|
|
245
248
|
)}
|
|
249
|
+
{/* trailing overflow — the reference toolbar's far-right … (surface
|
|
250
|
+
* scope: called with null, vs the drawer header's per-record call) */}
|
|
251
|
+
{onOverflow && (
|
|
252
|
+
<Button variant="nav" size="md" iconOnly="more" iconSize={14} className="ml-auto" onClick={() => onOverflow(null)} aria-label="More" />
|
|
253
|
+
)}
|
|
246
254
|
</div>
|
|
247
255
|
)}
|
|
248
256
|
|