@kolkrabbi/kol-component 0.226.0 → 0.228.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 +4 -3
- package/src/atoms/Input.jsx +7 -0
- package/src/atoms/SegmentedToggle.jsx +5 -2
- package/src/index.js +3 -1
- package/src/molecules/InspectorSection.jsx +20 -1
- package/src/molecules/KindPreview.jsx +1 -1
- package/src/molecules/SplitToolButton.jsx +1 -1
- package/src/organisms/DocumentEditor.jsx +1 -1
- package/src/organisms/MediaLibraryPages.jsx +1 -1
- package/src/organisms/ShellSearchOverlay.jsx +28 -3
- package/src/organisms/ShortcutsOverlay.jsx +100 -0
- package/src/organisms/ToolPalette.jsx +1 -1
- package/src/utilities/frontmatter.js +0 -76
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-component",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.228.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,8 @@
|
|
|
24
24
|
"@floating-ui/react": "^0.27.19",
|
|
25
25
|
"embla-carousel-react": "^8.6.0",
|
|
26
26
|
"pdfjs-dist": "^6.3.289",
|
|
27
|
-
"react-syntax-highlighter": "^16.1.1"
|
|
27
|
+
"react-syntax-highlighter": "^16.1.1",
|
|
28
|
+
"@kolkrabbi/kol-markdown": "^0.1.0"
|
|
28
29
|
},
|
|
29
30
|
"peerDependencies": {
|
|
30
31
|
"@kolkrabbi/kol-icons": ">=0.22.0",
|
|
@@ -35,7 +36,7 @@
|
|
|
35
36
|
"react-dom": "^18.3.0 || ^19.0.0"
|
|
36
37
|
},
|
|
37
38
|
"devDependencies": {
|
|
38
|
-
"@kolkrabbi/kol-icons": "^0.
|
|
39
|
+
"@kolkrabbi/kol-icons": "^0.29.0"
|
|
39
40
|
},
|
|
40
41
|
"files": [
|
|
41
42
|
"src",
|
package/src/atoms/Input.jsx
CHANGED
|
@@ -56,6 +56,9 @@ import { glyphSize } from '../hooks/glyphLadders.js'
|
|
|
56
56
|
* iconLeft/prefix — the paint-bar anatomy ([swatch] FFFFFF is ONE
|
|
57
57
|
* container, not two boxes; ColorSwatchFieldSizing 2026-08-12). The
|
|
58
58
|
* consumer owns the node's sizing; the shell's padding frames it.
|
|
59
|
+
* slotRight — its trailing mirror: an interactive node inside the shell, after the unit /
|
|
60
|
+
* suffix — a combo field's chevron (the design editor's type size, 2026-09-27: the chevron
|
|
61
|
+
* sat OUTSIDE the field as a second control). Not aria-hidden: it is a control.
|
|
59
62
|
*
|
|
60
63
|
* Chrome (bg/border/padding/transition/disabled) comes from .kol-control;
|
|
61
64
|
* Input owns prefix/suffix/icon layout + the inner <input> styling.
|
|
@@ -75,6 +78,7 @@ export default function Input({
|
|
|
75
78
|
prefix,
|
|
76
79
|
suffix,
|
|
77
80
|
slotLeft,
|
|
81
|
+
slotRight,
|
|
78
82
|
affordance,
|
|
79
83
|
unit,
|
|
80
84
|
iconLeft,
|
|
@@ -217,6 +221,9 @@ export default function Input({
|
|
|
217
221
|
{suffix !== undefined && (
|
|
218
222
|
<span aria-hidden="true" className="text-meta pl-1 shrink-0">{suffix}</span>
|
|
219
223
|
)}
|
|
224
|
+
{slotRight && (
|
|
225
|
+
<span className="flex items-center shrink-0 pl-1 ml-auto">{slotRight}</span>
|
|
226
|
+
)}
|
|
220
227
|
</label>
|
|
221
228
|
)
|
|
222
229
|
}
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
* strip (canvas alignment, transform cluster); onChange is the
|
|
30
30
|
* action dispatch.
|
|
31
31
|
* onChange — handler (newValue) => void
|
|
32
|
-
* options — [{ value, label, ariaLabel? }]
|
|
32
|
+
* options — [{ value, label, ariaLabel?, tooltip? }] — a cell whose label is not a string
|
|
33
|
+
* (a glyph) shows its `ariaLabel` as a KOL Tooltip; `tooltip` overrides it
|
|
33
34
|
* variant — 'default' (shipped chrome: shared outer stroke + dividers,
|
|
34
35
|
* filled active cell) | 'filled' (the state law's tiles: every
|
|
35
36
|
* cell a surface-secondary tile with 1px transparent gaps, NO
|
|
@@ -53,6 +54,7 @@
|
|
|
53
54
|
* className — additional classes on the outer shell
|
|
54
55
|
*/
|
|
55
56
|
import { toneClass } from '../utilities/tone.js'
|
|
57
|
+
import { Tooltip } from '../utilities/Popover.jsx'
|
|
56
58
|
|
|
57
59
|
export default function SegmentedToggle({ value, onChange, options = [], variant = 'default', size = 'md', tone = 'default', ariaLabel, className = '' }) {
|
|
58
60
|
const cellType = { xs: 'kol-mono-8', sm: 'kol-mono-12', md: 'kol-mono-14', lg: 'kol-mono-16' }[size]
|
|
@@ -86,8 +88,8 @@ export default function SegmentedToggle({ value, onChange, options = [], variant
|
|
|
86
88
|
{options.map((opt, i) => {
|
|
87
89
|
const isActive = !stateless && opt.value === value
|
|
88
90
|
return (
|
|
91
|
+
<Tooltip key={opt.value} asChild label={opt.tooltip ?? (typeof opt.label === 'string' ? null : opt.ariaLabel)}>
|
|
89
92
|
<button
|
|
90
|
-
key={opt.value}
|
|
91
93
|
type="button"
|
|
92
94
|
role={stateless ? undefined : 'radio'}
|
|
93
95
|
aria-checked={stateless ? undefined : isActive}
|
|
@@ -98,6 +100,7 @@ export default function SegmentedToggle({ value, onChange, options = [], variant
|
|
|
98
100
|
>
|
|
99
101
|
{opt.label}
|
|
100
102
|
</button>
|
|
103
|
+
</Tooltip>
|
|
101
104
|
)
|
|
102
105
|
})}
|
|
103
106
|
</div>
|
package/src/index.js
CHANGED
|
@@ -116,6 +116,8 @@ export { default as PropertyInput } from './molecules/PropertyInput.jsx'
|
|
|
116
116
|
export { default as ShapeDropdown } from './molecules/ShapeDropdown.jsx'
|
|
117
117
|
export { default as ShellDrawer } from './molecules/ShellDrawer.jsx'
|
|
118
118
|
export { default as ShellSearchOverlay } from './organisms/ShellSearchOverlay.jsx'
|
|
119
|
+
/* the keymap sheet — moved in from kol-shell 2026-09-28 so the workshop shell and the apps open ONE sheet on S */
|
|
120
|
+
export { default as ShortcutsOverlay } from './organisms/ShortcutsOverlay.jsx'
|
|
119
121
|
export { default as Slider } from './molecules/Slider.jsx'
|
|
120
122
|
export { default as SpecList } from './molecules/SpecList.jsx'
|
|
121
123
|
export { default as SpectrumControls, HueStrip, SBSquare, WheelTriangle } from './organisms/SpectrumControls.jsx'
|
|
@@ -188,7 +190,7 @@ export { default as RowMenuButton } from './molecules/RowMenuButton.jsx'
|
|
|
188
190
|
export { default as DocPage } from './molecules/DocPage.jsx'
|
|
189
191
|
export { default as DocFrontmatter } from './molecules/DocFrontmatter.jsx'
|
|
190
192
|
export { default as ProfileCard } from './molecules/ProfileCard.jsx'
|
|
191
|
-
export { parseFrontmatter, splitFrontmatter, joinFrontmatter } from '
|
|
193
|
+
export { parseFrontmatter, splitFrontmatter, joinFrontmatter } from '@kolkrabbi/kol-markdown'
|
|
192
194
|
export { readDraft, writeDraft, clearDraft, listDrafts, moveDrafts, DRAFTS_EVENT } from './utilities/localDrafts.js'
|
|
193
195
|
export { readCover } from './utilities/id3.js'
|
|
194
196
|
export { kindOf, extOf, isSystemFile, KINDS, KIND_LABEL, DEFAULT_KINDS, isSegment, groupSegments, groupVariants, posterFor, partition } from './utilities/mediaKinds.js'
|
|
@@ -18,12 +18,31 @@
|
|
|
18
18
|
* declaring it — that is the upgrade path if a consumer ever needs the stack
|
|
19
19
|
* to vary the rule per-gap. One prop is a smaller API than a new component.
|
|
20
20
|
*
|
|
21
|
+
* `pane` (editor inspector rebuild, 2026-09-27 — user: "why are we not sectioning off the panes?
|
|
22
|
+
* saying 'this is type' 'this is transform'"): the Affinity panel shape instead of a labelled form
|
|
23
|
+
* group — a titled header strip, a padded body, and a FULL-WIDTH rule between panes. Mount panes
|
|
24
|
+
* in a container with no horizontal padding; each pane pads itself, so its rule runs edge to edge.
|
|
25
|
+
*
|
|
21
26
|
* @param {ReactNode} label the group's name
|
|
22
27
|
* @param {ReactNode} actions controls on the label's row, right-aligned
|
|
23
28
|
* @param {boolean} divided a hairline between divided siblings
|
|
29
|
+
* @param {boolean} pane the panel shape: header strip + body, full-width rules between panes
|
|
24
30
|
* @param {string} className
|
|
25
31
|
*/
|
|
26
|
-
export default function InspectorSection({ label, actions = null, children, divided = false, className = '' }) {
|
|
32
|
+
export default function InspectorSection({ label, actions = null, children, divided = false, pane = false, className = '' }) {
|
|
33
|
+
if (pane) {
|
|
34
|
+
return (
|
|
35
|
+
<section className={`kol-inspector-pane ${className}`.trim()}>
|
|
36
|
+
{(label || actions) && (
|
|
37
|
+
<header className="kol-inspector-pane-head">
|
|
38
|
+
{label && <h3 className="kol-inspector-pane-title">{label}</h3>}
|
|
39
|
+
{actions}
|
|
40
|
+
</header>
|
|
41
|
+
)}
|
|
42
|
+
<div className="kol-inspector-pane-body">{children}</div>
|
|
43
|
+
</section>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
27
46
|
return (
|
|
28
47
|
<div className={`flex flex-col gap-2${divided ? ' kol-section--divided' : ''} ${className}`}>
|
|
29
48
|
{/* `actions` — controls on the label's row, right-aligned (Figma's eye / plus on Fill and
|
|
@@ -7,7 +7,7 @@ import FileIcon from '../atoms/FileIcon.jsx'
|
|
|
7
7
|
import PdfPage, { PdfDocument } from './PdfPage.jsx'
|
|
8
8
|
import { kindOf as defaultKindOf, extOf as defaultExtOf, KIND_LABEL } from '../utilities/mediaKinds.js'
|
|
9
9
|
import markdownToHtml from '../utilities/markdownToHtml.js'
|
|
10
|
-
import { parseFrontmatter } from '
|
|
10
|
+
import { parseFrontmatter } from '@kolkrabbi/kol-markdown'
|
|
11
11
|
import DocPage from './DocPage.jsx'
|
|
12
12
|
|
|
13
13
|
/* taxonomy-ok: molecule — nests the DS media atoms + CodeBlock (relative). */
|
|
@@ -140,7 +140,7 @@ const SplitToolButton = ({
|
|
|
140
140
|
popover={popover}
|
|
141
141
|
panel={false}
|
|
142
142
|
focus={false}
|
|
143
|
-
className="w-max bg-surface-secondary border border-
|
|
143
|
+
className="w-max bg-surface-secondary border border-oq-08 rounded shadow-lg"
|
|
144
144
|
>
|
|
145
145
|
{variants.map((variant) => {
|
|
146
146
|
const isActive = active && variant.id === value
|
|
@@ -7,7 +7,7 @@ import ViewToggle from '../atoms/ViewToggle.jsx'
|
|
|
7
7
|
import QuickLookFrame from '../molecules/QuickLookFrame.jsx'
|
|
8
8
|
import KindPreview from '../molecules/KindPreview.jsx'
|
|
9
9
|
import FullscreenOverlay from '../utilities/FullscreenOverlay.jsx'
|
|
10
|
-
import { splitFrontmatter, joinFrontmatter } from '
|
|
10
|
+
import { splitFrontmatter, joinFrontmatter } from '@kolkrabbi/kol-markdown'
|
|
11
11
|
import { readDraft, writeDraft, clearDraft } from '../utilities/localDrafts.js'
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -34,7 +34,7 @@ import { useModal } from '../molecules/Modal.jsx'
|
|
|
34
34
|
import ContentFilters from './ContentFilters.jsx'
|
|
35
35
|
import DocumentEditor from './DocumentEditor.jsx'
|
|
36
36
|
import { listDrafts, moveDrafts, DRAFTS_EVENT } from '../utilities/localDrafts.js'
|
|
37
|
-
import { parseFrontmatter } from '
|
|
37
|
+
import { parseFrontmatter } from '@kolkrabbi/kol-markdown'
|
|
38
38
|
import ShellSearchOverlay from './ShellSearchOverlay.jsx'
|
|
39
39
|
import ColumnBrowser, { isFileDrag, Preview as ColumnPreview, SelectionPreview } from './ColumnBrowser.jsx'
|
|
40
40
|
import SettingsPanel, { SettingsSwitch, SettingsChoice, SettingsMulti, SettingsFooter } from './SettingsPanel.jsx'
|
|
@@ -10,7 +10,21 @@ import Tag from '../atoms/Tag.jsx'
|
|
|
10
10
|
* @param {string} label full row label
|
|
11
11
|
* @param {string} query current query (empty / no match → plain label)
|
|
12
12
|
*/
|
|
13
|
-
export function HighlightMatch({ label, query }) {
|
|
13
|
+
export function HighlightMatch({ label, query, ranges }) {
|
|
14
|
+
/* An engine that already knows where it matched (kol-search's `highlights`)
|
|
15
|
+
* passes `ranges` — every hit, not just the first slice of the raw query. */
|
|
16
|
+
if (Array.isArray(ranges)) {
|
|
17
|
+
if (!ranges.length) return <span>{label}</span>
|
|
18
|
+
const out = []
|
|
19
|
+
let at = 0
|
|
20
|
+
ranges.forEach(([s, e], i) => {
|
|
21
|
+
if (s > at) out.push(<span key={`t${i}`}>{label.slice(at, s)}</span>)
|
|
22
|
+
out.push(<span key={`m${i}`} className="text-fg underline decoration-2 underline-offset-[3px]">{label.slice(s, e)}</span>)
|
|
23
|
+
at = e
|
|
24
|
+
})
|
|
25
|
+
if (at < label.length) out.push(<span key="rest">{label.slice(at)}</span>)
|
|
26
|
+
return <>{out}</>
|
|
27
|
+
}
|
|
14
28
|
const idx = query ? label.toLowerCase().indexOf(query.toLowerCase()) : -1
|
|
15
29
|
if (idx === -1) return <span>{label}</span>
|
|
16
30
|
return (
|
|
@@ -47,6 +61,10 @@ export function HighlightMatch({ label, query }) {
|
|
|
47
61
|
* @param {Function} onQueryChange (string) => void — input change
|
|
48
62
|
* @param {Function} onSelect (item) => void — row click / Enter; consumer navigates
|
|
49
63
|
* @param {string} placeholder input placeholder
|
|
64
|
+
* @param {string} [enterLabel] what Enter does when no row is picked — shown as
|
|
65
|
+
* the panel's last line, so Enter is never a surprise
|
|
66
|
+
* (e.g. `All results for “q”`). Rows may carry
|
|
67
|
+
* `highlights` ([start, end] ranges) from the engine.
|
|
50
68
|
*/
|
|
51
69
|
export default function ShellSearchOverlay({
|
|
52
70
|
open,
|
|
@@ -65,6 +83,7 @@ export default function ShellSearchOverlay({
|
|
|
65
83
|
onQueryChange,
|
|
66
84
|
onSelect,
|
|
67
85
|
placeholder = 'Search…',
|
|
86
|
+
enterLabel,
|
|
68
87
|
}) {
|
|
69
88
|
const panelRef = useRef(null)
|
|
70
89
|
const listRef = useRef(null)
|
|
@@ -210,12 +229,12 @@ export default function ShellSearchOverlay({
|
|
|
210
229
|
onClick={() => select(item)}
|
|
211
230
|
onMouseEnter={() => { setActiveIndex(i); setNavigated(true) }}
|
|
212
231
|
className={`flex items-center gap-2 px-4 py-1.5 cursor-pointer kol-mono-14 transition-colors ${
|
|
213
|
-
i === active ? 'bg-fg-08 text-fg' : 'text-fg-
|
|
232
|
+
i === active ? 'bg-fg-08 text-fg' : 'text-fg-80'
|
|
214
233
|
}`}
|
|
215
234
|
>
|
|
216
235
|
<span className="flex flex-col min-w-0">
|
|
217
236
|
<span className="truncate">
|
|
218
|
-
<HighlightMatch label={item.label} query={query} />
|
|
237
|
+
<HighlightMatch label={item.label} query={query} ranges={item.highlights} />
|
|
219
238
|
</span>
|
|
220
239
|
{item.hint && (
|
|
221
240
|
<span className="kol-mono-12 text-fg-48 truncate">{item.hint}</span>
|
|
@@ -228,6 +247,12 @@ export default function ShellSearchOverlay({
|
|
|
228
247
|
))}
|
|
229
248
|
</ul>
|
|
230
249
|
)}
|
|
250
|
+
{!expanded && enterLabel && query && (
|
|
251
|
+
<p className="flex items-center gap-2 border-t border-fg-08 px-4 py-2 kol-helper-12 text-fg-64">
|
|
252
|
+
<kbd className="kol-helper-12 rounded-[var(--kol-radius-sm)] bg-fg-08 px-1.5 py-0.5 text-fg">↵</kbd>
|
|
253
|
+
{enterLabel}
|
|
254
|
+
</p>
|
|
255
|
+
)}
|
|
231
256
|
</div>
|
|
232
257
|
</div>
|
|
233
258
|
)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { useEffect } from 'react'
|
|
2
|
+
import SettingsSections from './SettingsSections.jsx'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ShortcutsOverlay — the keyboard-shortcut sheet: blurred scrim, centred
|
|
6
|
+
* panel, a 2-col grid (label · keys), Esc / backdrop-click close.
|
|
7
|
+
* Ported from the shared cut (mirror's, "copied from kol-monitor").
|
|
8
|
+
*
|
|
9
|
+
* `shortcuts` is a prop — feed the SAME array your settings page renders
|
|
10
|
+
* (both repos hand-maintained the list twice and both pairs drifted).
|
|
11
|
+
* Stacks at `--kol-z-modal`, above the rail's sticky tier.
|
|
12
|
+
*
|
|
13
|
+
* TWO FORMS, DETECTED ON SHAPE (ShortcutsOverlaySections, kol-fxr 2026-08-15):
|
|
14
|
+
*
|
|
15
|
+
* flat `[{ label, keys }]` — one grid, as shipped
|
|
16
|
+
* sectioned `[{ section, items: [{label,keys}] }]` — headings between groups
|
|
17
|
+
*
|
|
18
|
+
* The flat form renders EXACTLY as it did before, so no existing caller moves.
|
|
19
|
+
* The sectioned form exists because a real keymap is grouped — kol-fxr's is
|
|
20
|
+
* Edit · Selection · Layer · Tools · View, and its `shortcutsBySection()`
|
|
21
|
+
* already emits this shape — and flattening it to adopt this component would
|
|
22
|
+
* have lost the grouping. That consumer kept a 99-line local overlay instead,
|
|
23
|
+
* which is the duplication this component exists to end.
|
|
24
|
+
*
|
|
25
|
+
* ONE COMPONENT WITH THE SETTINGS PAGE AND THE DRAWER (user, 2026-09-03:
|
|
26
|
+
* *"align it more to the settings sidebar"* → *"lets normalise a component
|
|
27
|
+
* they can grab"*). This sheet and `SettingsShortcuts` render the SAME keymap
|
|
28
|
+
* array and had drifted into two anatomies — a bespoke two-column grid with a
|
|
29
|
+
* plain `text-fg-32` heading and hand-rolled `<span>` pairs here, against
|
|
30
|
+
* eyebrow sections over `SettingsRow`s there, down to the combo being `fg-96`
|
|
31
|
+
* in one and `fg-32` in the other. Both go through `SettingsSections` now, the
|
|
32
|
+
* same call the settings page and the settings drawer use, so a shortcut reads
|
|
33
|
+
* identically wherever it is shown and a change to the row lands in all of
|
|
34
|
+
* them. The FRAME stays each one's own: a scrim and a centred sheet here, a
|
|
35
|
+
* six-column grid on the page, a right-anchored drawer over the thing you are
|
|
36
|
+
* looking at.
|
|
37
|
+
*
|
|
38
|
+
* `keys` is a DISPLAY STRING and is never bound — this component shows a
|
|
39
|
+
* keymap, it does not own one. Formatting a combo (⌘⇧Z) is the consumer's,
|
|
40
|
+
* next to wherever the binding actually lives.
|
|
41
|
+
*
|
|
42
|
+
* @param {Array} props.shortcuts - `[{ label, combo }]` or `[{ section, items: [{ id, label, combo }] }]` — the same array `SettingsShortcuts` takes (ShellHomeSystemMonitorGaps, kol-monitor 2026-08-27: the overlay read `keys` while the settings block read `combo`; `combo` is the name, `keys` is tolerated for a release)
|
|
43
|
+
* @param {Function} props.onClose
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
const isSectioned = (list) => Array.isArray(list?.[0]?.items)
|
|
47
|
+
|
|
48
|
+
/* A shortcut as a settings ROW: the label on the left, the combo as the row's
|
|
49
|
+
* value. `labelWidth="auto"` makes the label yield and the combo hug — a combo
|
|
50
|
+
* is one token by nature and must not wrap. */
|
|
51
|
+
const toRow = ({ label, combo, keys }) => ({
|
|
52
|
+
label,
|
|
53
|
+
align: 'fill',
|
|
54
|
+
labelWidth: 'auto',
|
|
55
|
+
value: <span className="text-fg-32 kol-helper-12 whitespace-nowrap">{combo ?? keys}</span>,
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
export default function ShortcutsOverlay({ shortcuts = [], onClose }) {
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
const onKey = (e) => { if (e.key === 'Escape') onClose?.() }
|
|
61
|
+
window.addEventListener('keydown', onKey)
|
|
62
|
+
return () => window.removeEventListener('keydown', onKey)
|
|
63
|
+
}, [onClose])
|
|
64
|
+
|
|
65
|
+
const sectioned = isSectioned(shortcuts)
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div
|
|
69
|
+
onClick={onClose}
|
|
70
|
+
/* THE scrim (overlay-scrim-outliers, kol-client-olina 2026-09-03; user: "we
|
|
71
|
+
removed the blur and put just color on the background … we missed this").
|
|
72
|
+
This drew its own — an 8 % inverse wash plus a 2px blur — after the
|
|
73
|
+
2026-09-01 no-blur ruling and the one-tint-48 class. */
|
|
74
|
+
className="fixed inset-0 select-none kol-overlay-scrim"
|
|
75
|
+
style={{ display: 'grid', placeItems: 'center', zIndex: 'var(--kol-z-modal)' }}
|
|
76
|
+
>
|
|
77
|
+
{/* `bg-oq-04`, not `bg-surface-primary` (user, 2026-09-03: *"this is too
|
|
78
|
+
bright"*). Over a near-black editor canvas the raised page surface
|
|
79
|
+
reads as a lit slab, and an `fg-16` stroke on top of it was the
|
|
80
|
+
brightest edge on screen. The OPAQUE ramp is right here: the sheet
|
|
81
|
+
must not let the canvas through, and `oq-04` is a 4 % lift off the
|
|
82
|
+
ground rather than a surface from the page's own ladder — so the
|
|
83
|
+
panel separates from the scrimmed canvas without a stroke. */}
|
|
84
|
+
<div
|
|
85
|
+
/* THE SHEET FITS THE WINDOW (editor review #4, 2026-09-27): a long keymap — the design
|
|
86
|
+
* editor's is six sections — ran past both edges of the viewport with no way to reach the
|
|
87
|
+
* rest. Capped to the window less a margin, scrolling inside. */
|
|
88
|
+
className="kol-shortcuts-panel text-fg-64 kol-helper-12 bg-oq-04 flex flex-col gap-6 overflow-y-auto"
|
|
89
|
+
style={{ padding: 24, borderRadius: 4, maxHeight: 'calc(100dvh - 48px)' }}
|
|
90
|
+
onClick={(e) => e.stopPropagation()}
|
|
91
|
+
>
|
|
92
|
+
<SettingsSections
|
|
93
|
+
sections={sectioned
|
|
94
|
+
? shortcuts.map(({ section, items = [] }) => ({ label: section, rowGap: 1, rows: items.map(toRow) }))
|
|
95
|
+
: [{ rowGap: 1, rows: shortcuts.map(toRow) }]}
|
|
96
|
+
/>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
@@ -43,7 +43,7 @@ const ToolPalette = ({
|
|
|
43
43
|
iconComponent = Icon,
|
|
44
44
|
className = '',
|
|
45
45
|
}) => (
|
|
46
|
-
<div className={`flex items-center gap-1 min-w-0 overflow-x-auto ${className}`.trim()}>
|
|
46
|
+
<div className={`kol-tool-palette flex items-center gap-1 min-w-0 overflow-x-auto ${className}`.trim()}>
|
|
47
47
|
{items.map((it, i) => {
|
|
48
48
|
if (it.kind === 'divider') return <Divider key={`d${i}`} variant="vertical" height={20} className="mx-1.5 shrink-0" />
|
|
49
49
|
if (it.kind === 'split') {
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* parseFrontmatter — the workshop engine's handrolled YAML-subset parser,
|
|
3
|
-
* verbatim (`packages/workshop/src/engine/frontmatter.js`; DocPageAndKindShowcase,
|
|
4
|
-
* kol-r2b2 2026-08-27): `key: value`, block lists (` - item`), inline `[a, b]`
|
|
5
|
-
* tags. Keys are lowercased. No gray-matter / js-yaml.
|
|
6
|
-
*/
|
|
7
|
-
export function parseFrontmatter(raw) {
|
|
8
|
-
const metadata = {}
|
|
9
|
-
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/)
|
|
10
|
-
if (!match) return metadata
|
|
11
|
-
const lines = match[1].split(/\r?\n/)
|
|
12
|
-
for (let i = 0; i < lines.length; i++) {
|
|
13
|
-
const kv = lines[i].match(/^([A-Za-z][A-Za-z0-9 -]*):\s*(.*)$/)
|
|
14
|
-
if (!kv) continue
|
|
15
|
-
const key = kv[1].toLowerCase()
|
|
16
|
-
const value = kv[2].trim()
|
|
17
|
-
if (!value) {
|
|
18
|
-
const items = []
|
|
19
|
-
while (i + 1 < lines.length && lines[i + 1].match(/^\s+-\s+/)) { i++; items.push(lines[i].replace(/^\s+-\s+/, '').trim()) }
|
|
20
|
-
metadata[key] = items.length ? items : ''
|
|
21
|
-
} else {
|
|
22
|
-
metadata[key] = value
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
if (typeof metadata.tags === 'string' && metadata.tags.startsWith('[')) {
|
|
26
|
-
metadata.tags = metadata.tags.slice(1, -1).split(',').map((t) => t.trim()).filter(Boolean)
|
|
27
|
-
}
|
|
28
|
-
return metadata
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* splitFrontmatter / joinFrontmatter — the EDITOR's pair (DocumentEditor, 2026-09-26). Where
|
|
33
|
-
* `parseFrontmatter` reads for display (keys lowercased, lists flattened), these round-trip a file:
|
|
34
|
-
* keys keep their case and their order, a list stays a list, and the body is untouched — so opening
|
|
35
|
-
* and saving a document without touching the form changes nothing but what was edited.
|
|
36
|
-
*
|
|
37
|
-
* `fields` is `[[key, value]]`, value a string or an array of strings.
|
|
38
|
-
*/
|
|
39
|
-
const unquote = (v) => {
|
|
40
|
-
if (v.length > 1 && v.startsWith('"') && v.endsWith('"')) { try { return JSON.parse(v) } catch { return v.slice(1, -1) } }
|
|
41
|
-
if (v.length > 1 && v.startsWith("'") && v.endsWith("'")) return v.slice(1, -1).replace(/''/g, "'")
|
|
42
|
-
return v
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function splitFrontmatter(raw = '') {
|
|
46
|
-
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/)
|
|
47
|
-
if (!match) return { has: false, fields: [], body: raw }
|
|
48
|
-
const fields = []
|
|
49
|
-
const lines = match[1].split(/\r?\n/)
|
|
50
|
-
for (let i = 0; i < lines.length; i++) {
|
|
51
|
-
const kv = lines[i].match(/^([A-Za-z_][\w -]*):\s*(.*)$/)
|
|
52
|
-
if (!kv) continue
|
|
53
|
-
let value = kv[2].trim()
|
|
54
|
-
if (!value) {
|
|
55
|
-
const items = []
|
|
56
|
-
while (i + 1 < lines.length && /^\s+-\s+/.test(lines[i + 1])) { i++; items.push(lines[i].replace(/^\s+-\s+/, '').trim()) }
|
|
57
|
-
value = items.length ? items : ''
|
|
58
|
-
} else if (value.startsWith('[') && value.endsWith(']')) {
|
|
59
|
-
value = value.slice(1, -1).split(',').map((t) => unquote(t.trim())).filter(Boolean)
|
|
60
|
-
} else {
|
|
61
|
-
value = unquote(value)
|
|
62
|
-
}
|
|
63
|
-
fields.push([kv[1], value])
|
|
64
|
-
}
|
|
65
|
-
return { has: true, fields, body: raw.slice(match[0].length) }
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/* bare when YAML reads it back as the same string; quoted otherwise (a colon, a leading #, quotes) */
|
|
69
|
-
const yamlScalar = (v) => (/^[\w./@+-][\w ./@+,-]*$/.test(v) || v === '' ? v : JSON.stringify(v))
|
|
70
|
-
|
|
71
|
-
export function joinFrontmatter(fields, body = '') {
|
|
72
|
-
const rows = fields.filter(([key]) => String(key).trim())
|
|
73
|
-
if (!rows.length) return body
|
|
74
|
-
const yaml = rows.map(([key, v]) => (Array.isArray(v) ? `${key}: [${v.map(yamlScalar).join(', ')}]` : `${key}: ${yamlScalar(String(v ?? ''))}`)).join('\n')
|
|
75
|
-
return `---\n${yaml}\n---\n${body.startsWith('\n') ? body.slice(1) : body}`
|
|
76
|
-
}
|