@orbytes/astrolab 0.3.0 → 0.4.0-next.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/README.md +109 -64
- package/defaults.mjs +65 -0
- package/dist/core/virtual-module/virtual-routes.js +12 -1
- package/docs/PIN-CONTRACT.md +8 -0
- package/docs/PIN.md +29 -19
- package/index.d.ts +40 -16
- package/index.mjs +32 -10
- package/package.json +6 -2
- package/src/Home.astro +167 -264
- package/src/LabHead.astro +37 -1047
- package/src/chrome/ComponentCard.astro +69 -0
- package/src/chrome/Icon.astro +21 -0
- package/src/chrome/LICENSE-icons +43 -0
- package/src/chrome/Nav.astro +105 -0
- package/src/chrome/Panel.astro +104 -0
- package/src/chrome/Shell.astro +106 -0
- package/src/chrome/Sprite.astro +23 -0
- package/src/chrome/StoryView.astro +251 -0
- package/src/chrome/Tree.astro +83 -0
- package/src/chrome/ViewportControls.astro +98 -0
- package/src/chrome/ViewportStage.astro +32 -0
- package/src/chrome/fonts/OFL.txt +93 -0
- package/src/chrome/fonts/inter-latin-wght-normal.woff2 +0 -0
- package/src/chrome/icons.ts +59 -0
- package/src/chrome/marks-client.ts +102 -0
- package/src/chrome/model.ts +142 -0
- package/src/chrome/pins-data.ts +30 -0
- package/src/chrome/shell-client.ts +372 -0
- package/src/chrome/site-data.ts +230 -0
- package/src/chrome/trees.ts +152 -0
- package/src/chrome/viewport-client.ts +579 -0
- package/src/chrome/views/Assets.astro +110 -0
- package/src/chrome/views/Pages.astro +178 -0
- package/src/chrome/views/Placeholder.astro +37 -0
- package/src/chrome/views/Tasks.astro +107 -0
- package/src/core/LICENSE-astrobook +16 -0
- package/src/core/lib/components/home.astro +4 -2
- package/src/core/lib/pages/story.astro +12 -10
- package/src/core/virtual-module/virtual-routes.ts +20 -4
- package/src/pin/board.mjs +389 -175
- package/src/pin/index.mjs +33 -2
- package/src/pin/toolbar.js +1 -1
- package/src/shell/Browse.astro +106 -353
- package/src/shell/Viewport.astro +22 -1315
- package/src/shell/lab-index.ts +23 -14
- package/src/ui/components/app.astro +5 -7
- package/src/ui/components/theme-script.astro +13 -2
- package/src/ui/lab.css +2152 -370
- package/virtual.d.ts +13 -0
- package/src/shell/CardGrid.astro +0 -297
- package/src/ui/components/build-path.ts +0 -13
- package/src/ui/components/build-tree.ts +0 -108
- package/src/ui/components/collapse-duration.ts +0 -28
- package/src/ui/components/compress-terms.ts +0 -10
- package/src/ui/components/dashboard-layout.astro +0 -39
- package/src/ui/components/home.astro +0 -65
- package/src/ui/components/layout.astro +0 -110
- package/src/ui/components/sidebar-button-fullscreen.astro +0 -38
- package/src/ui/components/sidebar-button-search.astro +0 -23
- package/src/ui/components/sidebar-button-theme.astro +0 -9
- package/src/ui/components/sidebar-button.astro +0 -24
- package/src/ui/components/sidebar-resize-handle.astro +0 -74
- package/src/ui/components/sidebar-search-panel.astro +0 -41
- package/src/ui/components/sidebar-search-script.ts +0 -103
- package/src/ui/components/sidebar-title.astro +0 -17
- package/src/ui/components/sidebar-tree-node.astro +0 -143
- package/src/ui/components/sidebar-tree.astro +0 -84
- package/src/ui/components/sidebar.astro +0 -29
- package/src/ui/components/theme-toggle.astro +0 -63
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// The site's own pages and images, read off disk for the Pages and Assets views. Server-side only.
|
|
2
|
+
//
|
|
3
|
+
// Pages come from the same walk the live derivation uses (../shell/live-files.mjs), so the Pages
|
|
4
|
+
// tree and the live pills can never disagree about what a page is or what it mounts.
|
|
5
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { root } from "astro:config/server";
|
|
9
|
+
import { pageComponentFiles, pageRoute, sitePages, templateOf } from "../shell/live-files.mjs";
|
|
10
|
+
import type { TreeNode } from "./model";
|
|
11
|
+
import { hrefs } from "./model";
|
|
12
|
+
|
|
13
|
+
export const rootDir = fileURLToPath(root);
|
|
14
|
+
|
|
15
|
+
export interface SitePage {
|
|
16
|
+
/** repo-relative, e.g. src/pages/legal/terms.astro */
|
|
17
|
+
file: string;
|
|
18
|
+
/** "/", "/legal/terms", "/blog/[slug]" */
|
|
19
|
+
route: string;
|
|
20
|
+
/** the lab URL that shows it */
|
|
21
|
+
href: string;
|
|
22
|
+
label: string;
|
|
23
|
+
/** the first `title="…"` in its template — usually the layout's title prop */
|
|
24
|
+
title: string | null;
|
|
25
|
+
/** a route with a parameter cannot be shown without one of its generated paths */
|
|
26
|
+
dynamic: boolean;
|
|
27
|
+
/** the components it mounts, in document order (repo-relative) */
|
|
28
|
+
mounts: string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** "terms-and-conditions" → "Terms and conditions"; the home page is "Home". */
|
|
32
|
+
const labelOf = (route: string) => {
|
|
33
|
+
if (route === "/") return "Home";
|
|
34
|
+
const last = route.split("/").filter(Boolean).pop() ?? route;
|
|
35
|
+
if (last.startsWith("[")) return last;
|
|
36
|
+
const words = last.replace(/[-_]+/g, " ").trim();
|
|
37
|
+
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const titleOf = (file: string): string | null => {
|
|
41
|
+
try {
|
|
42
|
+
const template = templateOf(readFileSync(path.join(rootDir, file), "utf8"));
|
|
43
|
+
const m = template.match(/\btitle=["']([^"']{1,160})["']/);
|
|
44
|
+
return m ? m[1]! : null;
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const sitePagesModel = (): SitePage[] =>
|
|
51
|
+
sitePages(rootDir).map((file) => {
|
|
52
|
+
const route = pageRoute(file);
|
|
53
|
+
return {
|
|
54
|
+
file,
|
|
55
|
+
route,
|
|
56
|
+
href: hrefs.page(route),
|
|
57
|
+
label: labelOf(route),
|
|
58
|
+
title: titleOf(file),
|
|
59
|
+
dynamic: route.includes("["),
|
|
60
|
+
mounts: pageComponentFiles(rootDir, file),
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The Pages tree, nested by folder the way the routes nest. A folder that has its own index page
|
|
66
|
+
* is both a group and a link (Figma: `Services` with `Page 1` and `Page 2` under it).
|
|
67
|
+
*/
|
|
68
|
+
export const pagesTree = (pages: SitePage[], currentRoute?: string): TreeNode[] => {
|
|
69
|
+
interface Branch {
|
|
70
|
+
node: TreeNode;
|
|
71
|
+
kids: Map<string, Branch>;
|
|
72
|
+
}
|
|
73
|
+
const top: Branch = { node: { id: "pages", label: "" }, kids: new Map() };
|
|
74
|
+
const nodeFor = (page: SitePage): TreeNode => ({
|
|
75
|
+
id: `page:${page.route}`,
|
|
76
|
+
label: page.label,
|
|
77
|
+
href: page.dynamic ? undefined : page.href,
|
|
78
|
+
icon: "page",
|
|
79
|
+
current: page.route === currentRoute,
|
|
80
|
+
disabled: page.dynamic,
|
|
81
|
+
title: page.dynamic
|
|
82
|
+
? "A dynamic route — the lab needs one of its generated paths to show it"
|
|
83
|
+
: (page.title ?? page.route),
|
|
84
|
+
search: `${page.route} ${page.title ?? ""} ${page.file}`.toLowerCase(),
|
|
85
|
+
facets: page.dynamic ? "dynamic" : page.mounts.length ? "mounts" : "static",
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const home = pages.find((page) => page.route === "/");
|
|
89
|
+
if (home) top.kids.set("", { node: nodeFor(home), kids: new Map() });
|
|
90
|
+
|
|
91
|
+
for (const page of pages) {
|
|
92
|
+
if (page.route === "/") continue;
|
|
93
|
+
const segments = page.route.split("/").filter(Boolean);
|
|
94
|
+
let branch = top;
|
|
95
|
+
let prefix = "";
|
|
96
|
+
for (const [i, segment] of segments.entries()) {
|
|
97
|
+
prefix += `/${segment}`;
|
|
98
|
+
let next = branch.kids.get(segment);
|
|
99
|
+
const isPage = i === segments.length - 1;
|
|
100
|
+
if (!next) {
|
|
101
|
+
next = {
|
|
102
|
+
node: isPage
|
|
103
|
+
? nodeFor(page)
|
|
104
|
+
: { id: `dir:${prefix}`, label: labelOf(prefix), icon: "folder", search: prefix.toLowerCase() },
|
|
105
|
+
kids: new Map(),
|
|
106
|
+
};
|
|
107
|
+
branch.kids.set(segment, next);
|
|
108
|
+
} else if (isPage) {
|
|
109
|
+
// A folder we already made turns out to have its own index page: it becomes that page.
|
|
110
|
+
next.node = { ...nodeFor(page), id: next.node.id };
|
|
111
|
+
}
|
|
112
|
+
branch = next;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const build = (branch: Branch): TreeNode[] =>
|
|
117
|
+
[...branch.kids.values()].map(({ node, kids }) => {
|
|
118
|
+
if (!kids.size) return node;
|
|
119
|
+
const children = build({ node, kids });
|
|
120
|
+
return { ...node, icon: node.href ? "page" : "folder", children };
|
|
121
|
+
});
|
|
122
|
+
return build(top);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// ---- assets -------------------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
export interface SiteAsset {
|
|
128
|
+
/** repo-relative */
|
|
129
|
+
file: string;
|
|
130
|
+
/** for anchors: `asset-src-assets-images-hero-aurora-webp` */
|
|
131
|
+
id: string;
|
|
132
|
+
name: string;
|
|
133
|
+
folder: string;
|
|
134
|
+
where: "src" | "public";
|
|
135
|
+
bytes: number;
|
|
136
|
+
ext: string;
|
|
137
|
+
/** repo-relative files that reference it */
|
|
138
|
+
usedBy: string[];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const IMAGE = /\.(png|jpe?g|webp|avif|gif|svg)$/i;
|
|
142
|
+
const SOURCE = /\.(astro|[cm]?[jt]sx?|mdx?|css|scss|sass|less|json|html|ya?ml)$/i;
|
|
143
|
+
const SKIP_DIRS = new Set(["node_modules", "dist", ".git", ".astro"]);
|
|
144
|
+
|
|
145
|
+
const walk = (dir: string, test: RegExp, out: string[] = []): string[] => {
|
|
146
|
+
const abs = path.join(rootDir, dir);
|
|
147
|
+
if (!existsSync(abs)) return out;
|
|
148
|
+
for (const entry of readdirSync(abs, { withFileTypes: true })) {
|
|
149
|
+
if (entry.name.startsWith(".") || SKIP_DIRS.has(entry.name)) continue;
|
|
150
|
+
const rel = `${dir}/${entry.name}`;
|
|
151
|
+
if (entry.isDirectory()) walk(rel, test, out);
|
|
152
|
+
else if (test.test(entry.name)) out.push(rel);
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Every image under src/ and public/, with who references it. "References" is textual: a source
|
|
159
|
+
* file under src/ that contains the image's `folder/name` (or, for a file at the top of its tree,
|
|
160
|
+
* its name). That covers an `import` of an src/ asset and a `/images/x.png` string for a public one,
|
|
161
|
+
* and it is exactly as good as a grep — a name built at runtime from parts is not seen. Good enough
|
|
162
|
+
* to find what nothing uses, which is the question this view answers.
|
|
163
|
+
*/
|
|
164
|
+
export const siteAssetsModel = (): SiteAsset[] => {
|
|
165
|
+
const files = [...walk("src", IMAGE), ...walk("public", IMAGE)];
|
|
166
|
+
const sources = walk("src", SOURCE).map((file) => ({
|
|
167
|
+
file,
|
|
168
|
+
text: (() => {
|
|
169
|
+
try {
|
|
170
|
+
return readFileSync(path.join(rootDir, file), "utf8");
|
|
171
|
+
} catch {
|
|
172
|
+
return "";
|
|
173
|
+
}
|
|
174
|
+
})(),
|
|
175
|
+
}));
|
|
176
|
+
return files
|
|
177
|
+
.map((file) => {
|
|
178
|
+
const name = path.posix.basename(file);
|
|
179
|
+
const parent = path.posix.basename(path.posix.dirname(file));
|
|
180
|
+
const where = file.startsWith("public/") ? "public" : "src";
|
|
181
|
+
const needle = parent && parent !== "src" && parent !== "public" ? `${parent}/${name}` : name;
|
|
182
|
+
let bytes = 0;
|
|
183
|
+
try {
|
|
184
|
+
bytes = statSync(path.join(rootDir, file)).size;
|
|
185
|
+
} catch {
|
|
186
|
+
/* vanished between the walk and the stat */
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
file,
|
|
190
|
+
id: `asset-${file.replace(/[^a-z0-9]+/gi, "-").toLowerCase()}`,
|
|
191
|
+
name,
|
|
192
|
+
folder: path.posix.dirname(file),
|
|
193
|
+
where: where as SiteAsset["where"],
|
|
194
|
+
bytes,
|
|
195
|
+
ext: path.posix.extname(name).slice(1).toLowerCase(),
|
|
196
|
+
usedBy: sources.filter((s) => s.file !== file && s.text.includes(needle)).map((s) => s.file),
|
|
197
|
+
};
|
|
198
|
+
})
|
|
199
|
+
.sort((a, b) => a.file.localeCompare(b.file));
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
export const assetsTree = (assets: SiteAsset[]): TreeNode[] => {
|
|
203
|
+
const folders = new Map<string, SiteAsset[]>();
|
|
204
|
+
for (const asset of assets) {
|
|
205
|
+
const list = folders.get(asset.folder) ?? [];
|
|
206
|
+
list.push(asset);
|
|
207
|
+
folders.set(asset.folder, list);
|
|
208
|
+
}
|
|
209
|
+
return [...folders.entries()].map(([folder, list]) => ({
|
|
210
|
+
id: `assets:${folder}`,
|
|
211
|
+
label: folder.replace(/^(src|public)\/(assets\/)?/, "") || folder,
|
|
212
|
+
icon: "folder" as const,
|
|
213
|
+
title: folder,
|
|
214
|
+
count: list.length,
|
|
215
|
+
search: folder.toLowerCase(),
|
|
216
|
+
children: list.map((asset) => ({
|
|
217
|
+
id: asset.id,
|
|
218
|
+
label: asset.name,
|
|
219
|
+
href: `#${asset.id}`,
|
|
220
|
+
icon: "assets" as const,
|
|
221
|
+
dot: asset.usedBy.length ? undefined : ("muted" as const),
|
|
222
|
+
dotTitle: asset.usedBy.length ? undefined : "nothing references it",
|
|
223
|
+
facets: asset.usedBy.length ? "used" : "unused",
|
|
224
|
+
search: `${asset.file} ${asset.ext}`.toLowerCase(),
|
|
225
|
+
})),
|
|
226
|
+
}));
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
export const formatBytes = (bytes: number) =>
|
|
230
|
+
bytes < 1024 ? `${bytes} B` : bytes < 1024 * 1024 ? `${(bytes / 1024).toFixed(0)} KB` : `${(bytes / 1048576).toFixed(1)} MB`;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// Level 2's trees, built from the lab index and the site's files. Server-side only.
|
|
2
|
+
//
|
|
3
|
+
// THE TREE STOPS AT THE COMPONENT (decided 2026-09-24). A component's stories are tabs in the
|
|
4
|
+
// navbar, not rows here, so a leaf is one stories module and links to its first story. Folders
|
|
5
|
+
// above it are the directories under the tier, with one fold: a directory holding exactly one
|
|
6
|
+
// module and nothing else IS that module, so `Section01Hero › V1 › Section01Hero` reads
|
|
7
|
+
// `Section01Hero › V1`.
|
|
8
|
+
import type { LabItem, LabIndex } from "../shell/lab-index";
|
|
9
|
+
import { livePillText } from "../shell/lab-index";
|
|
10
|
+
import type { TreeNode } from "./model";
|
|
11
|
+
|
|
12
|
+
/** One stories module — a component — with its stories in file order. */
|
|
13
|
+
export interface LabComponent {
|
|
14
|
+
moduleId: string;
|
|
15
|
+
moduleName: string;
|
|
16
|
+
moduleFile: string;
|
|
17
|
+
componentFile: string | null;
|
|
18
|
+
directory: string;
|
|
19
|
+
tier: string;
|
|
20
|
+
section: string | null;
|
|
21
|
+
version: string | null;
|
|
22
|
+
stories: LabItem[];
|
|
23
|
+
live: boolean;
|
|
24
|
+
usedBy: string[];
|
|
25
|
+
responsive: LabItem["responsive"];
|
|
26
|
+
tags: string[];
|
|
27
|
+
summary: string | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const componentsOf = (index: LabIndex): LabComponent[] => {
|
|
31
|
+
const byModule = new Map<string, LabComponent>();
|
|
32
|
+
for (const item of index.items) {
|
|
33
|
+
let mod = byModule.get(item.moduleId);
|
|
34
|
+
if (!mod) {
|
|
35
|
+
mod = {
|
|
36
|
+
moduleId: item.moduleId,
|
|
37
|
+
moduleName: item.moduleName,
|
|
38
|
+
moduleFile: item.moduleFile,
|
|
39
|
+
componentFile: item.componentFile,
|
|
40
|
+
directory: item.directory,
|
|
41
|
+
tier: item.tier,
|
|
42
|
+
section: item.section,
|
|
43
|
+
version: item.version,
|
|
44
|
+
stories: [],
|
|
45
|
+
live: false,
|
|
46
|
+
usedBy: item.usedBy,
|
|
47
|
+
responsive: item.responsive,
|
|
48
|
+
tags: item.tags,
|
|
49
|
+
summary: item.summary,
|
|
50
|
+
};
|
|
51
|
+
byModule.set(item.moduleId, mod);
|
|
52
|
+
}
|
|
53
|
+
mod.stories.push(item);
|
|
54
|
+
if (item.live) mod.live = true;
|
|
55
|
+
}
|
|
56
|
+
return [...byModule.values()];
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** What the dot, the filter and the search know about a component. */
|
|
60
|
+
export const componentState = (mod: LabComponent) => {
|
|
61
|
+
const first = mod.stories[0]!;
|
|
62
|
+
const live = mod.live ? livePillText(mod.stories.find((s) => s.live) ?? first) : null;
|
|
63
|
+
const facets = [
|
|
64
|
+
mod.live ? "live" : mod.usedBy.length ? "used" : "unused",
|
|
65
|
+
mod.responsive ? (mod.responsive.done ? "responsive" : "unresponsive") : "",
|
|
66
|
+
].filter(Boolean);
|
|
67
|
+
return {
|
|
68
|
+
dot: mod.live ? ("live" as const) : mod.usedBy.length ? ("used" as const) : undefined,
|
|
69
|
+
dotTitle: live ?? (mod.usedBy.length ? `used by ${mod.usedBy.length}` : undefined),
|
|
70
|
+
facets: facets.join(" "),
|
|
71
|
+
search: [
|
|
72
|
+
mod.moduleName,
|
|
73
|
+
mod.section,
|
|
74
|
+
mod.version,
|
|
75
|
+
mod.tier,
|
|
76
|
+
first.livePage,
|
|
77
|
+
...facets,
|
|
78
|
+
...mod.tags,
|
|
79
|
+
...mod.stories.map((s) => s.storyName),
|
|
80
|
+
]
|
|
81
|
+
.filter(Boolean)
|
|
82
|
+
.join(" ")
|
|
83
|
+
.toLowerCase(),
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
interface Dir {
|
|
88
|
+
name: string;
|
|
89
|
+
path: string;
|
|
90
|
+
dirs: Map<string, Dir>;
|
|
91
|
+
mods: LabComponent[];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** The tree for one tier: its directories, folded, down to its components. */
|
|
95
|
+
export const tierTree = (index: LabIndex, tier: string, currentModuleId?: string): TreeNode[] => {
|
|
96
|
+
const mods = componentsOf(index).filter((mod) => mod.tier === tier);
|
|
97
|
+
const top: Dir = { name: tier, path: tier, dirs: new Map(), mods: [] };
|
|
98
|
+
for (const mod of mods) {
|
|
99
|
+
const segments = mod.directory.split("/").slice(1);
|
|
100
|
+
let dir = top;
|
|
101
|
+
for (const segment of segments) {
|
|
102
|
+
let next = dir.dirs.get(segment);
|
|
103
|
+
if (!next) {
|
|
104
|
+
next = { name: segment, path: `${dir.path}/${segment}`, dirs: new Map(), mods: [] };
|
|
105
|
+
dir.dirs.set(segment, next);
|
|
106
|
+
}
|
|
107
|
+
dir = next;
|
|
108
|
+
}
|
|
109
|
+
dir.mods.push(mod);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const leaf = (mod: LabComponent, label = mod.moduleName): TreeNode => {
|
|
113
|
+
const state = componentState(mod);
|
|
114
|
+
return {
|
|
115
|
+
id: `mod:${mod.moduleId}`,
|
|
116
|
+
label,
|
|
117
|
+
href: mod.stories[0]!.dashboardUrl,
|
|
118
|
+
icon: "component",
|
|
119
|
+
current: mod.moduleId === currentModuleId,
|
|
120
|
+
dot: state.dot,
|
|
121
|
+
dotTitle: state.dotTitle,
|
|
122
|
+
facets: state.facets,
|
|
123
|
+
search: state.search,
|
|
124
|
+
title: label === mod.moduleName ? undefined : mod.moduleName,
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const nodesOf = (dir: Dir): TreeNode[] => {
|
|
129
|
+
const out: TreeNode[] = [];
|
|
130
|
+
for (const child of [...dir.dirs.values()].sort((a, b) => a.name.localeCompare(b.name))) {
|
|
131
|
+
if (child.dirs.size === 0 && child.mods.length === 1) {
|
|
132
|
+
out.push(leaf(child.mods[0]!, child.name));
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const children = nodesOf(child);
|
|
136
|
+
const liveCount = children.filter((c) => c.dot === "live").length;
|
|
137
|
+
out.push({
|
|
138
|
+
id: `dir:${child.path}`,
|
|
139
|
+
label: child.name,
|
|
140
|
+
icon: "folder",
|
|
141
|
+
children,
|
|
142
|
+
dot: liveCount ? "live" : undefined,
|
|
143
|
+
dotTitle: liveCount ? `${liveCount} live` : undefined,
|
|
144
|
+
search: child.name.toLowerCase(),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
for (const mod of dir.mods) out.push(leaf(mod));
|
|
148
|
+
return out;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
return nodesOf(top);
|
|
152
|
+
};
|