@kolkrabbi/kol-workshop 0.5.1 → 0.11.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 -4
- package/src/compositions/WorkshopDefaultSidebar.jsx +7 -31
- package/src/compositions/WorkshopSidebar.jsx +35 -60
- package/src/docs/DocKit.jsx +21 -4
- package/src/docs/DocsFrontmatter.jsx +86 -21
- package/src/docs/DocumentationReader.jsx +68 -79
- 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 +178 -0
- package/src/shell/ShellLayout.jsx +181 -25
- package/src/shell/ShellSidebar.jsx +50 -105
- package/src/shell/index.js +7 -0
- package/src/tags/TagModeContext.jsx +52 -6
- package/src/tags/TagModeOverlay.jsx +74 -115
- package/src/tags/TagPath.jsx +24 -0
- package/src/tags/index.js +1 -0
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { useState, useEffect } from 'react'
|
|
2
|
-
import {
|
|
3
|
-
import { Icon
|
|
2
|
+
import { useLocation } from 'react-router-dom'
|
|
3
|
+
import { Icon } from '@kolkrabbi/kol-component'
|
|
4
|
+
import RailSection from './RailSection.jsx'
|
|
5
|
+
import RailRow from './RailRow.jsx'
|
|
4
6
|
|
|
5
7
|
const getSectionRootPath = (route, basePath) => {
|
|
6
8
|
if (route.path !== undefined && route.path !== null) {
|
|
@@ -63,115 +65,58 @@ const ShellSidebar = ({ routes = [], basePath = '/', onNavigate, label = 'Naviga
|
|
|
63
65
|
setCollapsedSections((prev) => ({ ...prev, [route.id]: !prev[route.id] }))
|
|
64
66
|
}
|
|
65
67
|
|
|
68
|
+
/* No count at L1 — the eyebrow names a body of material and the tally lives
|
|
69
|
+
* on the groups inside it (user ruling 2026-08-01). RailSection refuses one
|
|
70
|
+
* at this rung, so there is nothing to compute here. */
|
|
66
71
|
return (
|
|
67
72
|
<div className="space-y-4">
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
{
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
)}
|
|
85
|
-
<Tooltip label={navCollapsed ? `Expand ${label}` : `Collapse ${label}`}>
|
|
86
|
-
<button
|
|
87
|
-
type="button"
|
|
88
|
-
onClick={handleToggle}
|
|
89
|
-
aria-label={navCollapsed ? `Expand ${label}` : `Collapse ${label}`}
|
|
90
|
-
aria-expanded={!navCollapsed}
|
|
91
|
-
className="flex items-center justify-center"
|
|
92
|
-
style={{ height: '16.5px', marginBottom: '8px' }}
|
|
93
|
-
>
|
|
94
|
-
<Icon
|
|
95
|
-
name="chevron-down"
|
|
96
|
-
size={10}
|
|
97
|
-
className={`stroke-[2.5] transition-transform ${navCollapsed ? '' : 'rotate-180'}`}
|
|
98
|
-
/>
|
|
99
|
-
</button>
|
|
100
|
-
</Tooltip>
|
|
101
|
-
</div>
|
|
73
|
+
<RailSection
|
|
74
|
+
level={1}
|
|
75
|
+
label={label}
|
|
76
|
+
to={labelTo}
|
|
77
|
+
collapsed={navCollapsed}
|
|
78
|
+
onToggle={handleToggle}
|
|
79
|
+
onNavigate={onNavigate}
|
|
80
|
+
>
|
|
81
|
+
<div className="space-y-4">
|
|
82
|
+
{routes.map((route) => {
|
|
83
|
+
/* A group with no children is not a group — it is a link. It used
|
|
84
|
+
* to render as a header anyway: a chevron that rotated over an
|
|
85
|
+
* empty body, no count, and no navigation, so clicking "Icons" or
|
|
86
|
+
* "Components" in the tree did nothing at all while looking like
|
|
87
|
+
* it should. `collapsible` is what that distinction is now. */
|
|
88
|
+
const hasChildren = route.children?.length > 0
|
|
102
89
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
return (
|
|
117
|
-
<div key={route.id} className="shell-nav-group">
|
|
118
|
-
{hasChildren ? (
|
|
119
|
-
<button
|
|
120
|
-
type="button"
|
|
121
|
-
className={headerClass}
|
|
122
|
-
onClick={() => handleSectionClick(route)}
|
|
123
|
-
aria-expanded={isExpanded}
|
|
90
|
+
return (
|
|
91
|
+
<div key={route.id} className="shell-nav-group">
|
|
92
|
+
<RailSection
|
|
93
|
+
level={2}
|
|
94
|
+
label={route.label}
|
|
95
|
+
count={hasChildren ? route.children.length : undefined}
|
|
96
|
+
to={hasChildren ? undefined : getSectionRootPath(route, basePath)}
|
|
97
|
+
collapsible={hasChildren}
|
|
98
|
+
collapsed={!!collapsedSections[route.id]}
|
|
99
|
+
onToggle={() => handleSectionClick(route)}
|
|
100
|
+
onNavigate={onNavigate}
|
|
101
|
+
icon={Icon}
|
|
124
102
|
>
|
|
125
|
-
<
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
size={12}
|
|
129
|
-
className={`transition-transform ${isExpanded ? 'rotate-90' : ''}`}
|
|
130
|
-
/>
|
|
131
|
-
{route.label}
|
|
132
|
-
</span>
|
|
133
|
-
<span className="kol-mono-14 text-subtle">({route.children.length})</span>
|
|
134
|
-
</button>
|
|
135
|
-
) : (
|
|
136
|
-
<Link
|
|
137
|
-
to={getSectionRootPath(route, basePath)}
|
|
138
|
-
className={headerClass}
|
|
139
|
-
onClick={onNavigate}
|
|
140
|
-
>
|
|
141
|
-
{/* aligned with the expandable rows' label column — the
|
|
142
|
-
* chevron's width, kept as space so the two kinds of row
|
|
143
|
-
* share a left edge */}
|
|
144
|
-
<span className="flex items-center gap-2">
|
|
145
|
-
<span aria-hidden="true" style={{ width: 12 }} />
|
|
146
|
-
{route.label}
|
|
147
|
-
</span>
|
|
148
|
-
</Link>
|
|
149
|
-
)}
|
|
150
|
-
|
|
151
|
-
{isExpanded && route.children?.length > 0 && (
|
|
152
|
-
<div className="shell-nav-items">
|
|
153
|
-
{route.children.map((child) => {
|
|
154
|
-
const childPath = getChildPath(child, basePath)
|
|
155
|
-
return (
|
|
156
|
-
<NavLink
|
|
103
|
+
<div className="shell-nav-items">
|
|
104
|
+
{(route.children ?? []).map((child) => (
|
|
105
|
+
<RailRow
|
|
157
106
|
key={child.id}
|
|
158
|
-
to={
|
|
159
|
-
|
|
160
|
-
className={({ isActive }) =>
|
|
161
|
-
`shell-nav-item kol-mono-14 ${isActive ? 'text-emphasis' : 'text-body'}`
|
|
162
|
-
}
|
|
163
|
-
onClick={onNavigate}
|
|
107
|
+
to={getChildPath(child, basePath)}
|
|
108
|
+
onNavigate={onNavigate}
|
|
164
109
|
>
|
|
165
|
-
|
|
166
|
-
</
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
</
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
)
|
|
173
|
-
|
|
174
|
-
</
|
|
110
|
+
{child.label}
|
|
111
|
+
</RailRow>
|
|
112
|
+
))}
|
|
113
|
+
</div>
|
|
114
|
+
</RailSection>
|
|
115
|
+
</div>
|
|
116
|
+
)
|
|
117
|
+
})}
|
|
118
|
+
</div>
|
|
119
|
+
</RailSection>
|
|
175
120
|
</div>
|
|
176
121
|
)
|
|
177
122
|
}
|
package/src/shell/index.js
CHANGED
|
@@ -9,3 +9,10 @@
|
|
|
9
9
|
export { default as ShellLayout } from './ShellLayout.jsx'
|
|
10
10
|
export { ShellTocContext, ShellFullHeightContext, ShellTocCollapsedContext } from './ShellLayout.jsx'
|
|
11
11
|
export { default as ShellSidebar } from './ShellSidebar.jsx'
|
|
12
|
+
/* RailSection — THE rail ladder. Every rail header at every rung comes from
|
|
13
|
+
* here, in both rails; see the file header for why a class alone was not
|
|
14
|
+
* enough. `pnpm validate:rails` R4 asserts it. */
|
|
15
|
+
export { default as RailSection } from './RailSection.jsx'
|
|
16
|
+
/* RailRow — THE L3 rail row. The look lives in `.shell-nav-item`; this owns the
|
|
17
|
+
* markup so nine hand-written utility stacks cannot come back. R4 asserts it. */
|
|
18
|
+
export { default as RailRow } from './RailRow.jsx'
|
|
@@ -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,102 +43,49 @@ 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
|
-
<Button
|
|
55
|
-
variant="outline"
|
|
56
|
-
quiet
|
|
57
|
-
size="sm"
|
|
58
|
-
iconOnly="x"
|
|
59
|
-
onClick={closeTagMode}
|
|
60
|
-
aria-label="Close tag mode"
|
|
61
|
-
/>
|
|
62
|
-
</Tooltip>
|
|
63
|
-
</div>
|
|
64
|
-
|
|
65
|
-
<div className="dash-card flex flex-col gap-8 -mt-4">
|
|
66
|
-
<div className="flex items-center gap-2" style={{ width: '100%', alignSelf: 'stretch' }}>
|
|
67
|
-
<Input
|
|
68
|
-
type="text"
|
|
69
|
-
aria-label="Search tags"
|
|
70
|
-
placeholder="Search tags…"
|
|
71
|
-
value={search}
|
|
72
|
-
onChange={(e) => setSearch(e.target.value)}
|
|
73
|
-
onKeyDown={(e) => {
|
|
74
|
-
if (e.key === 'Enter' && visibleTags.length > 0) {
|
|
75
|
-
toggleTag(visibleTags[0].tag)
|
|
76
|
-
setSearch('')
|
|
77
|
-
}
|
|
78
|
-
}}
|
|
79
|
-
size="md"
|
|
80
|
-
iconLeft="search"
|
|
81
|
-
className="w-full"
|
|
82
|
-
autoFocus
|
|
83
|
-
/>
|
|
84
|
-
{search && (
|
|
85
|
-
<Tooltip label="Clear search">
|
|
86
|
-
<Button
|
|
87
|
-
variant="outline"
|
|
88
|
-
quiet
|
|
89
|
-
size="sm"
|
|
90
|
-
iconOnly="x"
|
|
91
|
-
onClick={() => setSearch('')}
|
|
92
|
-
aria-label="Clear search"
|
|
93
|
-
/>
|
|
94
|
-
</Tooltip>
|
|
95
|
-
)}
|
|
96
|
-
</div>
|
|
97
|
-
|
|
98
|
-
<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">
|
|
99
64
|
{hasFilters && (
|
|
100
65
|
<>
|
|
101
66
|
<div className="flex items-center justify-between">
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
onClick={clearTags}
|
|
107
|
-
>
|
|
108
|
-
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
|
|
109
71
|
</Button>
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
variant="solid"
|
|
115
|
-
color={getTagColor(tag)}
|
|
116
|
-
size="md"
|
|
117
|
-
onRemove={() => removeTag(tag)}
|
|
118
|
-
>
|
|
119
|
-
{tag}
|
|
120
|
-
</Tag>
|
|
121
|
-
))}
|
|
122
|
-
</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. */}
|
|
123
76
|
</div>
|
|
124
77
|
<div className="border-t border-fg-08 my-4" />
|
|
125
78
|
</>
|
|
126
79
|
)}
|
|
127
80
|
|
|
128
|
-
{hasFilters
|
|
81
|
+
{/* `hasFilters` dropped from this condition too — with no tag
|
|
82
|
+
* active the graph renders the WHOLE map, which is the view worth
|
|
83
|
+
* opening cold. Gated, the button could be clicked and nothing
|
|
84
|
+
* changed. */}
|
|
85
|
+
{view === 'graph' ? (
|
|
129
86
|
<div>
|
|
130
87
|
<TagGraph
|
|
131
|
-
docs={filteredDocs}
|
|
88
|
+
docs={hasFilters ? filteredDocs : inventory}
|
|
132
89
|
allDocs={inventory}
|
|
133
90
|
activeTag={activeTag}
|
|
134
91
|
onTagClick={(tag) => toggleTag(tag)}
|
|
@@ -138,24 +95,28 @@ const TagModeOverlay = () => {
|
|
|
138
95
|
) : (
|
|
139
96
|
<>
|
|
140
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. */}
|
|
141
103
|
{visibleTags.map(({ tag, count }) => (
|
|
142
|
-
<
|
|
104
|
+
<RailRow
|
|
143
105
|
key={tag}
|
|
144
|
-
type="button"
|
|
145
|
-
className={`tag-list-item${activeTags.includes(tag) ? ' active' : ''}`}
|
|
146
106
|
onClick={() => toggleTag(tag)}
|
|
107
|
+
active={activeTags.includes(tag)}
|
|
108
|
+
trailing={count}
|
|
147
109
|
>
|
|
148
|
-
<
|
|
149
|
-
|
|
150
|
-
</button>
|
|
110
|
+
<TagPath tag={tag} />
|
|
111
|
+
</RailRow>
|
|
151
112
|
))}
|
|
152
113
|
{visibleTags.length === 0 && (
|
|
153
|
-
<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>
|
|
154
115
|
)}
|
|
155
116
|
</div>
|
|
156
117
|
|
|
157
118
|
{hasFilters && filteredDocs.length > 0 && (
|
|
158
|
-
<div className="
|
|
119
|
+
<div className="shell-nav-items pt-4 border-t border-fg-08">
|
|
159
120
|
{filteredDocs.map((d) => (
|
|
160
121
|
/* `d.href` wins over `docHref(d.id)` so ONE tag system can
|
|
161
122
|
* span content types that live in different URL spaces —
|
|
@@ -163,24 +124,22 @@ const TagModeOverlay = () => {
|
|
|
163
124
|
* at /components/:slug. Without it the inventory could
|
|
164
125
|
* only ever hold one kind of thing, which is why the
|
|
165
126
|
* graph saw the vault's 46 docs and nothing else. */
|
|
166
|
-
<
|
|
127
|
+
<RailRow
|
|
167
128
|
key={d.id}
|
|
168
129
|
to={d.href ?? docHref(d.id)}
|
|
169
|
-
|
|
170
|
-
|
|
130
|
+
trailing={extractDocNumber(d.id)}
|
|
131
|
+
onNavigate={closeTagMode}
|
|
171
132
|
>
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
</Link>
|
|
133
|
+
{cleanTitle(d.title, d.id)}
|
|
134
|
+
</RailRow>
|
|
175
135
|
))}
|
|
176
136
|
</div>
|
|
177
137
|
)}
|
|
178
138
|
</>
|
|
179
139
|
)}
|
|
180
140
|
</div>
|
|
181
|
-
</div>
|
|
182
141
|
</div>
|
|
183
|
-
</
|
|
142
|
+
</div>
|
|
184
143
|
)
|
|
185
144
|
}
|
|
186
145
|
|
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
<span className="text-fg-32">{parts.join('/')}/</span>
|
|
21
|
+
{leaf}
|
|
22
|
+
</>
|
|
23
|
+
)
|
|
24
|
+
}
|
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'
|