@dogsbay/docs-layout 0.2.0-beta.91 → 0.2.0-beta.93
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/DocsFooter.astro +15 -1
- package/src/docs-nav-client.ts +86 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dogsbay/docs-layout",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.93",
|
|
4
4
|
"description": "Standard documentation layout components for Dogsbay",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -29,10 +29,11 @@
|
|
|
29
29
|
"./json-ld": "./src/json-ld.ts"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@dogsbay/ui": "0.2.0-beta.
|
|
33
|
-
"@dogsbay/primitives": "0.2.0-beta.
|
|
32
|
+
"@dogsbay/ui": "0.2.0-beta.93",
|
|
33
|
+
"@dogsbay/primitives": "0.2.0-beta.93"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
+
"happy-dom": "^20.8.9",
|
|
36
37
|
"vitest": "^3.0.0"
|
|
37
38
|
},
|
|
38
39
|
"peerDependencies": {
|
package/src/DocsFooter.astro
CHANGED
|
@@ -37,6 +37,20 @@ const {
|
|
|
37
37
|
llmsLinkHref = "/llms.txt",
|
|
38
38
|
class: className,
|
|
39
39
|
} = Astro.props;
|
|
40
|
+
|
|
41
|
+
// `lastUpdated` is a canonical ISO string (e.g. "2026-06-16T00:00:00.000Z").
|
|
42
|
+
// Render it as a clean, locale-stable date; UTC avoids an off-by-one when the
|
|
43
|
+
// build host isn't on UTC. Non-date strings (e.g. a version/tag) pass through.
|
|
44
|
+
function formatDate(value: string): string {
|
|
45
|
+
const d = new Date(value);
|
|
46
|
+
if (Number.isNaN(d.getTime())) return value;
|
|
47
|
+
return d.toLocaleDateString("en-US", {
|
|
48
|
+
year: "numeric",
|
|
49
|
+
month: "long",
|
|
50
|
+
day: "numeric",
|
|
51
|
+
timeZone: "UTC",
|
|
52
|
+
});
|
|
53
|
+
}
|
|
40
54
|
---
|
|
41
55
|
|
|
42
56
|
<footer class:list={["mt-12 border-t pt-6", className]}>
|
|
@@ -49,7 +63,7 @@ const {
|
|
|
49
63
|
Edit this page
|
|
50
64
|
</a>
|
|
51
65
|
)}
|
|
52
|
-
{lastUpdated && <span>Last updated: {lastUpdated}</span>}
|
|
66
|
+
{lastUpdated && <span>Last updated: {formatDate(lastUpdated)}</span>}
|
|
53
67
|
</div>
|
|
54
68
|
)}
|
|
55
69
|
|
package/src/docs-nav-client.ts
CHANGED
|
@@ -64,7 +64,7 @@ function hasActiveDescendant(item: NavItem, current: string): boolean {
|
|
|
64
64
|
* sync. Padding is computed from `level` the same way (`8 + level*12`
|
|
65
65
|
* pixels) so indentation lines up across the same render.
|
|
66
66
|
*/
|
|
67
|
-
function renderItem(item: NavItem, current: string, level: number): HTMLLIElement {
|
|
67
|
+
export function renderItem(item: NavItem, current: string, level: number): HTMLLIElement {
|
|
68
68
|
const li = document.createElement("li");
|
|
69
69
|
li.dataset.sidebar = "nav-tree-item";
|
|
70
70
|
|
|
@@ -73,7 +73,79 @@ function renderItem(item: NavItem, current: string, level: number): HTMLLIElemen
|
|
|
73
73
|
const padLeft = `${8 + level * 12}px`;
|
|
74
74
|
const heightClass = level === 0 ? "h-8" : "h-7";
|
|
75
75
|
|
|
76
|
-
if (hasChildren) {
|
|
76
|
+
if (hasChildren && item.href) {
|
|
77
|
+
// A branch that is ALSO a page (section landing page) uses the APG
|
|
78
|
+
// "disclosure navigation" pattern: a real link (navigates) and a
|
|
79
|
+
// SEPARATE toggle <button> (expands/collapses), as siblings. Nesting
|
|
80
|
+
// a focusable <a> inside the interactive <summary> — as this did
|
|
81
|
+
// before — is an axe `nested-interactive` violation ("interactive
|
|
82
|
+
// controls must not be nested"). Native <details> can't hold a
|
|
83
|
+
// visible-when-collapsed header link without that nesting, so a
|
|
84
|
+
// page-branch drops <details> for link + button + sibling submenu.
|
|
85
|
+
const open = active || hasActiveDescendant(item, current);
|
|
86
|
+
const submenuId = `nav-sub-${item.href.replace(/[^a-z0-9]+/gi, "-")}-${level}`;
|
|
87
|
+
|
|
88
|
+
const row = document.createElement("div");
|
|
89
|
+
row.className = [
|
|
90
|
+
"flex w-full min-w-0 items-center gap-2 rounded-md text-sm text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
|
91
|
+
heightClass,
|
|
92
|
+
active ? "bg-sidebar-accent font-medium text-sidebar-accent-foreground" : "",
|
|
93
|
+
]
|
|
94
|
+
.filter(Boolean)
|
|
95
|
+
.join(" ");
|
|
96
|
+
row.style.paddingLeft = padLeft;
|
|
97
|
+
|
|
98
|
+
const toggle = document.createElement("button");
|
|
99
|
+
toggle.type = "button";
|
|
100
|
+
toggle.dataset.navToggle = "";
|
|
101
|
+
toggle.setAttribute("aria-controls", submenuId);
|
|
102
|
+
toggle.setAttribute("aria-expanded", String(open));
|
|
103
|
+
toggle.setAttribute("aria-label", `Toggle ${item.label} section`);
|
|
104
|
+
toggle.className =
|
|
105
|
+
"shrink-0 rounded outline-none ring-sidebar-ring focus-visible:ring-2";
|
|
106
|
+
toggle.innerHTML =
|
|
107
|
+
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4 shrink-0 transition-transform duration-200" data-chevron aria-hidden="true"><polyline points="9 18 15 12 9 6"/></svg>';
|
|
108
|
+
row.appendChild(toggle);
|
|
109
|
+
|
|
110
|
+
if (item.icon) {
|
|
111
|
+
const iconSpan = document.createElement("span");
|
|
112
|
+
iconSpan.className = "shrink-0 [&>svg]:size-4";
|
|
113
|
+
iconSpan.innerHTML = item.icon;
|
|
114
|
+
row.appendChild(iconSpan);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const link = document.createElement("a");
|
|
118
|
+
link.className =
|
|
119
|
+
"min-w-0 flex-1 truncate text-inherit no-underline outline-none ring-sidebar-ring focus-visible:ring-2";
|
|
120
|
+
link.href = item.href;
|
|
121
|
+
link.textContent = item.label;
|
|
122
|
+
link.dataset.navHref = item.href; // rehighlight keys off this
|
|
123
|
+
if (active) link.dataset.active = "true";
|
|
124
|
+
row.appendChild(link);
|
|
125
|
+
|
|
126
|
+
const submenu = document.createElement("ul");
|
|
127
|
+
submenu.id = submenuId;
|
|
128
|
+
submenu.dataset.navSubmenu = "";
|
|
129
|
+
submenu.className = "flex min-w-0 flex-col";
|
|
130
|
+
submenu.dataset.sidebar = "nav-tree";
|
|
131
|
+
submenu.dataset.level = String(level + 1);
|
|
132
|
+
for (const child of item.children!) {
|
|
133
|
+
submenu.appendChild(renderItem(child, current, level + 1));
|
|
134
|
+
}
|
|
135
|
+
if (!open) submenu.hidden = true;
|
|
136
|
+
|
|
137
|
+
toggle.addEventListener("click", () => {
|
|
138
|
+
const isOpen = toggle.getAttribute("aria-expanded") === "true";
|
|
139
|
+
toggle.setAttribute("aria-expanded", String(!isOpen));
|
|
140
|
+
submenu.hidden = isOpen;
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
li.appendChild(row);
|
|
144
|
+
li.appendChild(submenu);
|
|
145
|
+
} else if (hasChildren) {
|
|
146
|
+
// Pure grouping label (no href): native <details>/<summary> is fully
|
|
147
|
+
// accessible here — the summary is the only interactive control, and
|
|
148
|
+
// its label is a plain <span>. Zero-JS expand/collapse.
|
|
77
149
|
const details = document.createElement("details");
|
|
78
150
|
if (active || hasActiveDescendant(item, current)) details.open = true;
|
|
79
151
|
|
|
@@ -81,16 +153,11 @@ function renderItem(item: NavItem, current: string, level: number): HTMLLIElemen
|
|
|
81
153
|
summary.className = [
|
|
82
154
|
"flex w-full min-w-0 cursor-pointer items-center gap-2 rounded-md text-sm text-sidebar-foreground outline-none [list-style:none] ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&::-webkit-details-marker]:hidden",
|
|
83
155
|
heightClass,
|
|
84
|
-
active ? "bg-sidebar-accent font-medium text-sidebar-accent-foreground" : "",
|
|
85
156
|
]
|
|
86
157
|
.filter(Boolean)
|
|
87
158
|
.join(" ");
|
|
88
159
|
summary.style.paddingLeft = padLeft;
|
|
89
|
-
if (item.href) summary.dataset.navHref = item.href;
|
|
90
|
-
if (active) summary.dataset.active = "true";
|
|
91
160
|
|
|
92
|
-
// Chevron SVG — matches SidebarNavTree's rotation-on-open via CSS
|
|
93
|
-
// (`details[open] > summary [data-chevron] { transform: rotate(90deg); }`).
|
|
94
161
|
summary.innerHTML =
|
|
95
162
|
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4 shrink-0 transition-transform duration-200" data-chevron aria-hidden="true"><polyline points="9 18 15 12 9 6"/></svg>';
|
|
96
163
|
|
|
@@ -102,13 +169,12 @@ function renderItem(item: NavItem, current: string, level: number): HTMLLIElemen
|
|
|
102
169
|
}
|
|
103
170
|
|
|
104
171
|
const label = document.createElement("span");
|
|
105
|
-
label.className = "truncate";
|
|
172
|
+
label.className = "min-w-0 flex-1 truncate";
|
|
106
173
|
label.textContent = item.label;
|
|
107
174
|
summary.appendChild(label);
|
|
108
175
|
|
|
109
176
|
details.appendChild(summary);
|
|
110
177
|
|
|
111
|
-
// Recurse — nested `<ul>` mirrors SidebarNavTree's `<Astro.self>`.
|
|
112
178
|
const childUl = document.createElement("ul");
|
|
113
179
|
childUl.className = "flex min-w-0 flex-col";
|
|
114
180
|
childUl.dataset.sidebar = "nav-tree";
|
|
@@ -156,7 +222,7 @@ function renderItem(item: NavItem, current: string, level: number): HTMLLIElemen
|
|
|
156
222
|
return li;
|
|
157
223
|
}
|
|
158
224
|
|
|
159
|
-
function renderTree(items: NavItem[], current: string, root: HTMLElement): void {
|
|
225
|
+
export function renderTree(items: NavItem[], current: string, root: HTMLElement): void {
|
|
160
226
|
const ul = document.createElement("ul");
|
|
161
227
|
ul.className =
|
|
162
228
|
"flex min-w-0 flex-col w-full group-data-[collapsible=icon]:hidden";
|
|
@@ -199,7 +265,9 @@ function rehighlight(root: HTMLElement, current: string): void {
|
|
|
199
265
|
el.classList.remove(...ACTIVE_CLASSES);
|
|
200
266
|
}
|
|
201
267
|
}
|
|
202
|
-
// Expand ancestors of the new active item
|
|
268
|
+
// Expand ancestors of the new active item — both native <details>
|
|
269
|
+
// groups and the button-driven [data-nav-submenu] disclosures used by
|
|
270
|
+
// section-landing (href) branches.
|
|
203
271
|
const active = root.querySelector<HTMLElement>('[data-active="true"]');
|
|
204
272
|
if (active) {
|
|
205
273
|
let parent: HTMLElement | null = active.parentElement;
|
|
@@ -207,6 +275,13 @@ function rehighlight(root: HTMLElement, current: string): void {
|
|
|
207
275
|
if (parent.tagName === "DETAILS") {
|
|
208
276
|
(parent as HTMLDetailsElement).open = true;
|
|
209
277
|
}
|
|
278
|
+
if (parent.matches("[data-nav-submenu]")) {
|
|
279
|
+
parent.hidden = false;
|
|
280
|
+
const toggle = root.querySelector<HTMLElement>(
|
|
281
|
+
`[data-nav-toggle][aria-controls="${parent.id}"]`,
|
|
282
|
+
);
|
|
283
|
+
toggle?.setAttribute("aria-expanded", "true");
|
|
284
|
+
}
|
|
210
285
|
parent = parent.parentElement;
|
|
211
286
|
}
|
|
212
287
|
}
|