@kolkrabbi/kol-workshop 0.6.0 → 0.18.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 +5 -5
- package/src/compositions/WorkshopDefaultSidebar.jsx +8 -32
- package/src/compositions/WorkshopSidebar.jsx +39 -64
- package/src/docs/DocKit.jsx +21 -4
- package/src/docs/DocsFrontmatter.jsx +147 -26
- package/src/docs/DocumentationReader.jsx +57 -141
- package/src/engine/doc-helpers.js +31 -0
- package/src/engine/index.js +2 -1
- package/src/engine/parse-markdown.js +5 -1
- package/src/shell/RailRow.jsx +85 -0
- package/src/shell/RailSection.jsx +183 -0
- package/src/shell/RightRail.jsx +168 -0
- package/src/shell/ShellLayout.jsx +117 -41
- package/src/shell/ShellSidebar.jsx +61 -106
- package/src/shell/index.js +11 -0
- package/src/tags/TagModeContext.jsx +52 -6
- package/src/tags/TagModeOverlay.jsx +69 -120
- package/src/tags/TagPath.jsx +27 -0
- package/src/tags/index.js +1 -0
|
@@ -22,14 +22,43 @@ export const TagModeProvider = ({
|
|
|
22
22
|
docHref = defaultDocHref,
|
|
23
23
|
tagHref = defaultTagHref
|
|
24
24
|
}) => {
|
|
25
|
-
|
|
25
|
+
/* `view` lives in the CONTEXT, not in the overlay (2026-08-01). The overlay
|
|
26
|
+
* held its own `viewMode` state, so the graph could only be reached by
|
|
27
|
+
* finding an unlabelled glyph inside the already-open overlay — a caller
|
|
28
|
+
* could open tag mode but not say which mode. The rail's "Graph view" row
|
|
29
|
+
* needs exactly that. */
|
|
30
|
+
const [state, setState] = useState({ isOpen: false, activeTags: [], view: 'list', text: '', expanded: false })
|
|
31
|
+
|
|
32
|
+
/* ONE QUERY (user ruling 2026-08-01: "a system that accepts both tags and
|
|
33
|
+
* keywords"). `text` + `activeTags` are two facets of the SAME query, not
|
|
34
|
+
* two searches — ShellLayout used to hold `searchQuery` in its own local
|
|
35
|
+
* state while tags lived here, which is the whole reason the app had two
|
|
36
|
+
* surfaces. `expanded` is the palette's second state: Enter commits the
|
|
37
|
+
* query and opens the results body, Escape collapses it. */
|
|
38
|
+
const openTagMode = useCallback((tag = null, { view = 'list', expanded = false } = {}) => {
|
|
39
|
+
setState((prev) => ({
|
|
40
|
+
isOpen: true,
|
|
41
|
+
activeTags: tag ? [tag] : prev.activeTags,
|
|
42
|
+
view,
|
|
43
|
+
text: '',
|
|
44
|
+
expanded: expanded || !!tag,
|
|
45
|
+
}))
|
|
46
|
+
}, [])
|
|
47
|
+
|
|
48
|
+
const setText = useCallback((text) => {
|
|
49
|
+
setState((prev) => ({ ...prev, isOpen: true, text }))
|
|
50
|
+
}, [])
|
|
51
|
+
|
|
52
|
+
const setExpanded = useCallback((expanded) => {
|
|
53
|
+
setState((prev) => ({ ...prev, expanded }))
|
|
54
|
+
}, [])
|
|
26
55
|
|
|
27
|
-
const
|
|
28
|
-
setState({
|
|
56
|
+
const setView = useCallback((view) => {
|
|
57
|
+
setState((prev) => ({ ...prev, view }))
|
|
29
58
|
}, [])
|
|
30
59
|
|
|
31
60
|
const closeTagMode = useCallback(() => {
|
|
32
|
-
setState({ isOpen: false, activeTags: [] })
|
|
61
|
+
setState({ isOpen: false, activeTags: [], view: 'list', text: '', expanded: false })
|
|
33
62
|
}, [])
|
|
34
63
|
|
|
35
64
|
const toggleTag = useCallback((tag) => {
|
|
@@ -63,7 +92,7 @@ export const TagModeProvider = ({
|
|
|
63
92
|
// Close on any route change
|
|
64
93
|
const location = useLocation()
|
|
65
94
|
useEffect(() => {
|
|
66
|
-
if (state.isOpen) setState({ isOpen: false, activeTags: [] })
|
|
95
|
+
if (state.isOpen) setState({ isOpen: false, activeTags: [], view: 'list', text: '', expanded: false })
|
|
67
96
|
}, [location.pathname])
|
|
68
97
|
|
|
69
98
|
useEffect(() => {
|
|
@@ -79,9 +108,19 @@ export const TagModeProvider = ({
|
|
|
79
108
|
}, [state.isOpen, closeTagMode])
|
|
80
109
|
|
|
81
110
|
const value = useMemo(() => ({
|
|
111
|
+
/* Lets a consumer tell a real provider from the inert fallback, so
|
|
112
|
+
* ShellLayout can own its own palette state when mounted without one
|
|
113
|
+
* instead of silently no-op'ing every keystroke. */
|
|
114
|
+
isProvided: true,
|
|
82
115
|
isOpen: state.isOpen,
|
|
83
116
|
activeTags: state.activeTags,
|
|
84
117
|
activeTag,
|
|
118
|
+
view: state.view,
|
|
119
|
+
setView,
|
|
120
|
+
text: state.text,
|
|
121
|
+
setText,
|
|
122
|
+
expanded: state.expanded,
|
|
123
|
+
setExpanded,
|
|
85
124
|
openTagMode,
|
|
86
125
|
closeTagMode,
|
|
87
126
|
toggleTag,
|
|
@@ -91,7 +130,7 @@ export const TagModeProvider = ({
|
|
|
91
130
|
inventory,
|
|
92
131
|
docHref,
|
|
93
132
|
tagHref
|
|
94
|
-
}), [state.isOpen, state.activeTags, activeTag, openTagMode, closeTagMode, toggleTag, removeTag, clearTags, setActiveTag, inventory, docHref, tagHref])
|
|
133
|
+
}), [state.isOpen, state.activeTags, state.view, state.text, state.expanded, setView, setText, setExpanded, activeTag, openTagMode, closeTagMode, toggleTag, removeTag, clearTags, setActiveTag, inventory, docHref, tagHref])
|
|
95
134
|
|
|
96
135
|
return (
|
|
97
136
|
<TagModeContext.Provider value={value}>
|
|
@@ -106,9 +145,16 @@ export const TagModeProvider = ({
|
|
|
106
145
|
*/
|
|
107
146
|
const noop = () => {}
|
|
108
147
|
const DEFAULT_TAG_MODE = {
|
|
148
|
+
isProvided: false,
|
|
109
149
|
isOpen: false,
|
|
110
150
|
activeTags: [],
|
|
111
151
|
activeTag: null,
|
|
152
|
+
view: 'list',
|
|
153
|
+
setView: noop,
|
|
154
|
+
text: '',
|
|
155
|
+
setText: noop,
|
|
156
|
+
expanded: false,
|
|
157
|
+
setExpanded: noop,
|
|
112
158
|
openTagMode: noop,
|
|
113
159
|
closeTagMode: noop,
|
|
114
160
|
toggleTag: noop,
|
|
@@ -1,26 +1,36 @@
|
|
|
1
|
-
import { useMemo
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
2
|
import { Link } from 'react-router-dom'
|
|
3
|
-
import {
|
|
3
|
+
import { Button, Tooltip } from '@kolkrabbi/kol-component'
|
|
4
|
+
import TagPath from './TagPath.jsx'
|
|
4
5
|
import { useTagMode } from './TagModeContext.jsx'
|
|
5
6
|
import TagGraph from './TagGraph.jsx'
|
|
6
|
-
import
|
|
7
|
+
import RailRow from '../shell/RailRow.jsx'
|
|
8
|
+
import { extractDocNumber, cleanTitle } from '../engine/index.js'
|
|
7
9
|
import { buildTagCounts } from '../engine/tags.js'
|
|
8
10
|
|
|
11
|
+
/**
|
|
12
|
+
* TagModeOverlay - the tag BROWSER: the list and the graph. It does not search.
|
|
13
|
+
*
|
|
14
|
+
* ONE SEARCH (user ruling 2026-08-01: "search SHOULD be ONE system not 2").
|
|
15
|
+
* This component owned a second one - a raw `Input` plus
|
|
16
|
+
* `tag.toLowerCase().includes(q)` in a useMemo - over a corpus the shell's own
|
|
17
|
+
* index already covered. Two matchers, two behaviours, and this one never
|
|
18
|
+
* called the engine's `matchSearchItems` at all. Deleted rather than aligned:
|
|
19
|
+
* tags are rows in the shell search index now, so the one modal finds them
|
|
20
|
+
* like it finds everything else.
|
|
21
|
+
*
|
|
22
|
+
* `view` comes from the CONTEXT, not local state - the rail's "Graph view" row
|
|
23
|
+
* has to say which mode it wants before this component exists.
|
|
24
|
+
*/
|
|
9
25
|
const TagModeOverlay = () => {
|
|
10
|
-
const { activeTags, activeTag, toggleTag,
|
|
11
|
-
const [viewMode, setViewMode] = useState('list')
|
|
12
|
-
const [search, setSearch] = useState('')
|
|
26
|
+
const { activeTags, activeTag, toggleTag, clearTags, closeTagMode, inventory, docHref, tagHref, view, setView } = useTagMode()
|
|
13
27
|
|
|
14
28
|
const allTagsWithCount = useMemo(() => buildTagCounts(inventory), [inventory])
|
|
15
29
|
|
|
16
|
-
const visibleTags = useMemo(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
tags = tags.filter(({ tag }) => tag.toLowerCase().includes(q))
|
|
21
|
-
}
|
|
22
|
-
return tags
|
|
23
|
-
}, [allTagsWithCount, search, activeTags])
|
|
30
|
+
const visibleTags = useMemo(
|
|
31
|
+
() => allTagsWithCount.filter(({ tag }) => !activeTags.includes(tag)),
|
|
32
|
+
[allTagsWithCount, activeTags]
|
|
33
|
+
)
|
|
24
34
|
|
|
25
35
|
const filteredDocs = useMemo(() => {
|
|
26
36
|
if (activeTags.length === 0) return []
|
|
@@ -33,99 +43,36 @@ const TagModeOverlay = () => {
|
|
|
33
43
|
const hasFilters = activeTags.length > 0
|
|
34
44
|
|
|
35
45
|
return (
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
iconOnly={viewMode === 'graph' ? 'view-list' : 'polygon'}
|
|
55
|
-
onClick={() => setViewMode(viewMode === 'graph' ? 'list' : 'graph')}
|
|
56
|
-
aria-label={viewMode === 'graph' ? 'List view' : 'Graph view'}
|
|
57
|
-
/>
|
|
58
|
-
</Tooltip>
|
|
59
|
-
<Tooltip label="Close tag mode">
|
|
60
|
-
<Button
|
|
61
|
-
variant="outline"
|
|
62
|
-
quiet
|
|
63
|
-
size="sm"
|
|
64
|
-
iconOnly="x"
|
|
65
|
-
onClick={closeTagMode}
|
|
66
|
-
aria-label="Close tag mode"
|
|
67
|
-
/>
|
|
68
|
-
</Tooltip>
|
|
69
|
-
</div>
|
|
70
|
-
|
|
71
|
-
<div className="dash-card flex flex-col gap-8 -mt-4">
|
|
72
|
-
<div className="flex items-center gap-2" style={{ width: '100%', alignSelf: 'stretch' }}>
|
|
73
|
-
<Input
|
|
74
|
-
type="text"
|
|
75
|
-
aria-label="Search tags"
|
|
76
|
-
placeholder="Search tags…"
|
|
77
|
-
value={search}
|
|
78
|
-
onChange={(e) => setSearch(e.target.value)}
|
|
79
|
-
onKeyDown={(e) => {
|
|
80
|
-
if (e.key === 'Enter' && visibleTags.length > 0) {
|
|
81
|
-
toggleTag(visibleTags[0].tag)
|
|
82
|
-
setSearch('')
|
|
83
|
-
}
|
|
84
|
-
}}
|
|
85
|
-
size="md"
|
|
86
|
-
iconLeft="search"
|
|
87
|
-
className="w-full"
|
|
88
|
-
autoFocus
|
|
89
|
-
/>
|
|
90
|
-
{search && (
|
|
91
|
-
<Tooltip label="Clear search">
|
|
92
|
-
<Button
|
|
93
|
-
variant="outline"
|
|
94
|
-
quiet
|
|
95
|
-
size="sm"
|
|
96
|
-
iconOnly="x"
|
|
97
|
-
onClick={() => setSearch('')}
|
|
98
|
-
aria-label="Clear search"
|
|
99
|
-
/>
|
|
100
|
-
</Tooltip>
|
|
101
|
-
)}
|
|
102
|
-
</div>
|
|
103
|
-
|
|
104
|
-
<div className="flex flex-col gap-4 px-10">
|
|
46
|
+
/* NOT AN OVERLAY ANY MORE (user ruling 2026-08-01). This is the EXPANDED
|
|
47
|
+
* BODY of ShellSearchOverlay, rendered inside its panel — so it owns no
|
|
48
|
+
* scrim, no panel chrome and no close button. It was a sibling overlay
|
|
49
|
+
* with its own everything, which is what "two systems" meant. */
|
|
50
|
+
/* PLAIN BODY (user 2026-08-01). `.docs-article` is the PROSE wrapper — it
|
|
51
|
+
* centred every row and imposed a reading measure on a filter list, which
|
|
52
|
+
* is why "everything is center aligned". A results body is chrome, not
|
|
53
|
+
* prose. The node/graph button is gone too: the rail's Quick actions owns
|
|
54
|
+
* that entry, and a bare unlabelled hex floating at the corner was "not
|
|
55
|
+
* the place for it". */
|
|
56
|
+
<div className="flex flex-col gap-4 py-4">
|
|
57
|
+
<div className="flex flex-col gap-4">
|
|
58
|
+
{/* THE SEARCH BOX IS GONE (user ruling 2026-08-01). It was a raw
|
|
59
|
+
* `Input` driving `tag.toLowerCase().includes(q)` - a second search
|
|
60
|
+
* system over a corpus the shell index already holds. Tags are rows
|
|
61
|
+
* in that index now, so the ONE modal searches them. This overlay
|
|
62
|
+
* browses: the full list, and the graph. */}
|
|
63
|
+
<div className="flex flex-col gap-4">
|
|
105
64
|
{hasFilters && (
|
|
106
65
|
<>
|
|
107
66
|
<div className="flex items-center justify-between">
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
onClick={clearTags}
|
|
113
|
-
>
|
|
114
|
-
clear filters
|
|
67
|
+
{/* PRIMARY (user 2026-08-01). `outline quiet` is the
|
|
68
|
+
* recessive rung — this is the one action in the body. */}
|
|
69
|
+
<Button variant="primary" size="sm" onClick={clearTags}>
|
|
70
|
+
Clear filters
|
|
115
71
|
</Button>
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
variant="solid"
|
|
121
|
-
color={getTagColor(tag)}
|
|
122
|
-
size="md"
|
|
123
|
-
onRemove={() => removeTag(tag)}
|
|
124
|
-
>
|
|
125
|
-
{tag}
|
|
126
|
-
</Tag>
|
|
127
|
-
))}
|
|
128
|
-
</div>
|
|
72
|
+
{/* NO CHIPS HERE (2026-08-01). The palette renders the
|
|
73
|
+
* active tags in its input row — this block repeated them
|
|
74
|
+
* inside the body, so one filter showed as two chips. The
|
|
75
|
+
* query's facets belong to the query's input. */}
|
|
129
76
|
</div>
|
|
130
77
|
<div className="border-t border-fg-08 my-4" />
|
|
131
78
|
</>
|
|
@@ -135,7 +82,7 @@ const TagModeOverlay = () => {
|
|
|
135
82
|
* active the graph renders the WHOLE map, which is the view worth
|
|
136
83
|
* opening cold. Gated, the button could be clicked and nothing
|
|
137
84
|
* changed. */}
|
|
138
|
-
{
|
|
85
|
+
{view === 'graph' ? (
|
|
139
86
|
<div>
|
|
140
87
|
<TagGraph
|
|
141
88
|
docs={hasFilters ? filteredDocs : inventory}
|
|
@@ -148,24 +95,28 @@ const TagModeOverlay = () => {
|
|
|
148
95
|
) : (
|
|
149
96
|
<>
|
|
150
97
|
<div className="flex flex-col">
|
|
98
|
+
{/* RAIL ROWS (user 2026-08-01: "reference the sidebars,
|
|
99
|
+
* dont introduce a different text treatment here"). These
|
|
100
|
+
* wore `.tag-list-item` / `.tag-list-count`, which have NO
|
|
101
|
+
* CSS RULE ANYWHERE — that is the whole reason they centred
|
|
102
|
+
* and carried no type. RailRow already is this row. */}
|
|
151
103
|
{visibleTags.map(({ tag, count }) => (
|
|
152
|
-
<
|
|
104
|
+
<RailRow
|
|
153
105
|
key={tag}
|
|
154
|
-
type="button"
|
|
155
|
-
className={`tag-list-item${activeTags.includes(tag) ? ' active' : ''}`}
|
|
156
106
|
onClick={() => toggleTag(tag)}
|
|
107
|
+
active={activeTags.includes(tag)}
|
|
108
|
+
trailing={count}
|
|
157
109
|
>
|
|
158
|
-
<
|
|
159
|
-
|
|
160
|
-
</button>
|
|
110
|
+
<TagPath tag={tag} />
|
|
111
|
+
</RailRow>
|
|
161
112
|
))}
|
|
162
113
|
{visibleTags.length === 0 && (
|
|
163
|
-
<p className="text-fg-48 kol-mono-12 py-4">No tags
|
|
114
|
+
<p className="text-fg-48 kol-mono-12 py-4">No tags</p>
|
|
164
115
|
)}
|
|
165
116
|
</div>
|
|
166
117
|
|
|
167
118
|
{hasFilters && filteredDocs.length > 0 && (
|
|
168
|
-
<div className="
|
|
119
|
+
<div className="shell-nav-items pt-4 border-t border-fg-08">
|
|
169
120
|
{filteredDocs.map((d) => (
|
|
170
121
|
/* `d.href` wins over `docHref(d.id)` so ONE tag system can
|
|
171
122
|
* span content types that live in different URL spaces —
|
|
@@ -173,24 +124,22 @@ const TagModeOverlay = () => {
|
|
|
173
124
|
* at /components/:slug. Without it the inventory could
|
|
174
125
|
* only ever hold one kind of thing, which is why the
|
|
175
126
|
* graph saw the vault's 46 docs and nothing else. */
|
|
176
|
-
<
|
|
127
|
+
<RailRow
|
|
177
128
|
key={d.id}
|
|
178
129
|
to={d.href ?? docHref(d.id)}
|
|
179
|
-
|
|
180
|
-
|
|
130
|
+
trailing={extractDocNumber(d.id)}
|
|
131
|
+
onNavigate={closeTagMode}
|
|
181
132
|
>
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
</Link>
|
|
133
|
+
{cleanTitle(d.title, d.id)}
|
|
134
|
+
</RailRow>
|
|
185
135
|
))}
|
|
186
136
|
</div>
|
|
187
137
|
)}
|
|
188
138
|
</>
|
|
189
139
|
)}
|
|
190
140
|
</div>
|
|
191
|
-
</div>
|
|
192
141
|
</div>
|
|
193
|
-
</
|
|
142
|
+
</div>
|
|
194
143
|
)
|
|
195
144
|
}
|
|
196
145
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TagPath — a tag is a PATH, not a string.
|
|
3
|
+
*
|
|
4
|
+
* User ruling 2026-08-01: "nested tags… its a feature not a shape."
|
|
5
|
+
* `domain/components/atoms` printed flat, so the namespace read as noise
|
|
6
|
+
* inside the name. The namespace dims and the LEAF carries — the same
|
|
7
|
+
* hierarchy the taxonomy already declares (ten top-level namespaces,
|
|
8
|
+
* `engine/doc-helpers.js` NAMESPACE_COLORS).
|
|
9
|
+
*
|
|
10
|
+
* Extracted out of TagModeOverlay 2026-08-01 when the frontmatter panel
|
|
11
|
+
* became the second surface that needed it — every place a tag renders reads
|
|
12
|
+
* from here rather than growing a second spelling.
|
|
13
|
+
*/
|
|
14
|
+
export default function TagPath({ tag }) {
|
|
15
|
+
const parts = String(tag).replace(/^#/, '').split('/')
|
|
16
|
+
const leaf = parts.pop()
|
|
17
|
+
if (!parts.length) return leaf
|
|
18
|
+
return (
|
|
19
|
+
<>
|
|
20
|
+
{/* 64, not 32 (user ruling 2026-08-01). The namespace dims — it does not
|
|
21
|
+
* disappear. At 32 the prefix dropped so far under the leaf that a
|
|
22
|
+
* nested tag read as a one-word tag with dirt in front of it. */}
|
|
23
|
+
<span className="text-fg-64">{parts.join('/')}/</span>
|
|
24
|
+
{leaf}
|
|
25
|
+
</>
|
|
26
|
+
)
|
|
27
|
+
}
|
package/src/tags/index.js
CHANGED
|
@@ -9,3 +9,4 @@ export { TagModeProvider, useTagMode } from './TagModeContext.jsx'
|
|
|
9
9
|
export { default as TagModeGate } from './TagModeGate.jsx'
|
|
10
10
|
export { default as TagModeOverlay } from './TagModeOverlay.jsx'
|
|
11
11
|
export { default as TagGraph } from './TagGraph.jsx'
|
|
12
|
+
export { default as TagPath } from './TagPath.jsx'
|