kitfly 0.2.3 → 0.2.4
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/CHANGELOG.md +23 -0
- package/README.md +13 -11
- package/VERSION +1 -1
- package/dist/_raw/content/reference/gantt-widget.md +468 -0
- package/dist/_raw/content/reference/plugins.md +157 -2
- package/dist/content/deployment/preflight.html +5 -6
- package/dist/content/deployment/recipes/aws-s3.html +5 -6
- package/dist/content/deployment/recipes/cloudflare-pages.html +5 -6
- package/dist/content/deployment/recipes/cloudflare-r2.html +5 -6
- package/dist/content/deployment/recipes/fly-io.html +5 -6
- package/dist/content/deployment/recipes/github-pages.html +5 -6
- package/dist/content/deployment/recipes/netlify.html +5 -6
- package/dist/content/deployment/recipes/vercel.html +5 -6
- package/dist/content/deployment/secrets-and-env-vars.html +5 -6
- package/dist/content/deployment.html +5 -6
- package/dist/content/guide/approaches.html +5 -6
- package/dist/content/guide/branding.html +5 -6
- package/dist/content/guide/data-driven-content.html +5 -6
- package/dist/content/guide/features.html +5 -6
- package/dist/content/guide/getting-started.html +5 -6
- package/dist/content/guide/kitfly-overview.html +5 -6
- package/dist/content/reference/configuration.html +5 -6
- package/dist/content/reference/design-catalog.html +5 -6
- package/dist/content/reference/environment-variables.html +5 -6
- package/dist/content/reference/gantt-widget.html +899 -0
- package/dist/content/reference/glossary.html +5 -6
- package/dist/content/reference/key-concepts.html +5 -6
- package/dist/content/reference/plugins.html +245 -9
- package/dist/content/reference/slides-authoring-guidelines.html +5 -6
- package/dist/content/reference/structure.html +5 -6
- package/dist/content/reference.html +5 -6
- package/dist/content/templates/crucible.html +5 -6
- package/dist/content/templates/handbook.html +5 -6
- package/dist/content/templates/minimal.html +5 -6
- package/dist/content/templates/overview.html +5 -6
- package/dist/content/templates/pipeline.html +5 -6
- package/dist/content/templates/productbook.html +5 -6
- package/dist/content/templates/runbook.html +5 -6
- package/dist/content/templates/servicebook.html +5 -6
- package/dist/content-index.json +10 -2
- package/dist/docs/decisions/ADR-0001-minimalist-site-code.html +5 -6
- package/dist/docs/decisions/ADR-0002-ai-accessibility.html +5 -6
- package/dist/docs/decisions/ADR-0003-single-file-bundle.html +5 -6
- package/dist/docs/decisions/ADR-0004-bun-runtime.html +5 -6
- package/dist/docs/decisions/ADR-0005-plugin-contract-and-distribution.html +5 -6
- package/dist/docs/decisions/ADR-0006-data-driven-content.html +5 -6
- package/dist/docs/decisions/DDR-0001-viewport-locked-layout.html +5 -6
- package/dist/docs/decisions/DDR-0002-theme-system.html +5 -6
- package/dist/docs/decisions/DDR-0003-bounded-logo-slot.html +5 -6
- package/dist/docs/decisions/DDR-0004-slides-rendering-model.html +5 -6
- package/dist/docs/decisions/DDR-0005-deterministic-layout-boundary.html +5 -6
- package/dist/docs/userguide/cli/build.html +5 -6
- package/dist/docs/userguide/cli/bundle.html +5 -6
- package/dist/docs/userguide/cli/dev.html +5 -6
- package/dist/docs/userguide/cli/init.html +5 -6
- package/dist/docs/userguide/cli/servers.html +5 -6
- package/dist/docs/userguide/cli/stop.html +5 -6
- package/dist/docs/userguide/cli/update.html +5 -6
- package/dist/docs/userguide/cli/version.html +5 -6
- package/dist/docs/userguide/cli.html +5 -6
- package/dist/docs/userguide/sharing.html +5 -6
- package/dist/index.html +5 -6
- package/dist/llms.txt +3 -3
- package/dist/provenance.json +4 -5
- package/dist/reports/license-inventory.csv +199 -0
- package/dist/schemas/plugin-registry.schema.html +5 -6
- package/dist/schemas/plugin-schemas-notes.html +5 -6
- package/dist/schemas/plugin.schema.html +5 -6
- package/dist/schemas/plugins.schema.html +5 -6
- package/dist/schemas/v0/common.schema.html +5 -6
- package/dist/schemas/v0/plugin-registry.schema.html +5 -6
- package/dist/schemas/v0/plugin.schema.html +5 -6
- package/dist/schemas/v0/plugins.schema.html +5 -6
- package/dist/schemas/v0/site.schema.html +5 -6
- package/dist/schemas/v0/theme.schema.html +5 -6
- package/dist/schemas.html +5 -6
- package/package.json +1 -1
- package/plugins-dist/planning-visuals.css +261 -0
- package/plugins-dist/planning-visuals.js +669 -0
- package/registry/plugins.yaml +15 -1
- package/scripts/build-all.ts +5 -0
- package/scripts/build.ts +73 -11
- package/scripts/bundle.ts +73 -10
- package/scripts/dev.ts +49 -5
- package/scripts/embed-docs.ts +119 -0
- package/src/__tests__/build.test.ts +124 -0
- package/src/__tests__/bundle.test.ts +61 -0
- package/src/__tests__/docs.test.ts +117 -0
- package/src/__tests__/fixtures/fences/planning-visuals/invalid/bad-month-format.md +10 -0
- package/src/__tests__/fixtures/fences/planning-visuals/invalid/marker-format-mismatch.md +13 -0
- package/src/__tests__/fixtures/fences/planning-visuals/invalid/milestone-format-mismatch.md +13 -0
- package/src/__tests__/fixtures/fences/planning-visuals/invalid/missing-tracks.md +5 -0
- package/src/__tests__/fixtures/fences/planning-visuals/invalid/track-reversed.md +10 -0
- package/src/__tests__/fixtures/fences/planning-visuals/valid/markers-basic.md +15 -0
- package/src/__tests__/fixtures/fences/planning-visuals/valid/markers-no-milestones.md +13 -0
- package/src/__tests__/fixtures/fences/planning-visuals/valid/month-basic.md +16 -0
- package/src/__tests__/fixtures/fences/planning-visuals/valid/no-milestones.md +10 -0
- package/src/__tests__/fixtures/fences/planning-visuals/valid/week-basic.md +20 -0
- package/src/__tests__/planning-visuals-fence-contract.test.ts +28 -0
- package/src/__tests__/planning-visuals-runtime-regressions.bun.test.ts +68 -0
- package/src/__tests__/planning-visuals-runtime.bun.test.ts +192 -0
- package/src/__tests__/shared.test.ts +121 -0
- package/src/cli.ts +113 -18
- package/src/commands/docs.ts +71 -0
- package/src/generated/embedded-docs.ts +2384 -0
- package/src/server-registry.ts +50 -10
- package/src/shared.ts +449 -25
|
@@ -0,0 +1,669 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
const BLOCK_RE = /^:::\s*([a-z0-9-]+)\s*$/i;
|
|
3
|
+
const CLOSE_RE = /^:::\s*$/;
|
|
4
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
5
|
+
const WEEK_MS = 7 * DAY_MS;
|
|
6
|
+
const ISO_WEEK_ANCHOR_DAY = -3; // 1969-12-29 (Monday of 1970-W01)
|
|
7
|
+
const MONTH_NAMES = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
8
|
+
|
|
9
|
+
function parseScalar(raw) {
|
|
10
|
+
const value = String(raw ?? "").trim().replace(/\s*:::\s*$/, "").trim();
|
|
11
|
+
if (!value) return "";
|
|
12
|
+
const quoted = value.match(/^"(.*)"$/) || value.match(/^'(.*)'$/);
|
|
13
|
+
return quoted ? quoted[1] : value;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function parseLinesToObject(lines) {
|
|
17
|
+
const out = {};
|
|
18
|
+
let pendingKey = null;
|
|
19
|
+
let pendingObj = null;
|
|
20
|
+
|
|
21
|
+
for (const rawLine of lines) {
|
|
22
|
+
const line = String(rawLine ?? "");
|
|
23
|
+
const trimmed = line.trim();
|
|
24
|
+
if (!trimmed || trimmed === ":::" || trimmed.startsWith("#")) continue;
|
|
25
|
+
|
|
26
|
+
const kv = line.match(/^([a-z0-9_-]+)\s*:\s*(.*)$/i);
|
|
27
|
+
if (kv) {
|
|
28
|
+
const key = kv[1].toLowerCase();
|
|
29
|
+
const value = kv[2];
|
|
30
|
+
if (value) {
|
|
31
|
+
out[key] = parseScalar(value);
|
|
32
|
+
pendingKey = null;
|
|
33
|
+
pendingObj = null;
|
|
34
|
+
} else {
|
|
35
|
+
pendingKey = key;
|
|
36
|
+
pendingObj = null;
|
|
37
|
+
if (!Array.isArray(out[key])) out[key] = [];
|
|
38
|
+
}
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const item = line.match(/^ {2}-\s+(.+)$/);
|
|
43
|
+
if (item && pendingKey) {
|
|
44
|
+
const list = Array.isArray(out[pendingKey]) ? out[pendingKey] : [];
|
|
45
|
+
out[pendingKey] = list;
|
|
46
|
+
const objectKV = item[1].match(/^([a-z0-9_-]+)\s*:\s*(.+)$/i);
|
|
47
|
+
let pushed = null;
|
|
48
|
+
if (objectKV) {
|
|
49
|
+
pendingObj = { [objectKV[1].toLowerCase()]: parseScalar(objectKV[2]) };
|
|
50
|
+
pushed = pendingObj;
|
|
51
|
+
} else {
|
|
52
|
+
pendingObj = null;
|
|
53
|
+
pushed = parseScalar(item[1]);
|
|
54
|
+
}
|
|
55
|
+
list.push(pushed);
|
|
56
|
+
if (pendingKey === "tracks" || pendingKey === "milestones") {
|
|
57
|
+
out.__rowOrder = Array.isArray(out.__rowOrder) ? out.__rowOrder : [];
|
|
58
|
+
out.__rowOrder.push({ kind: pendingKey.slice(0, -1), index: list.length - 1 });
|
|
59
|
+
}
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const cont = line.match(/^ {4}([a-z0-9_-]+)\s*:\s*(.+)$/i);
|
|
64
|
+
if (cont && pendingObj) {
|
|
65
|
+
pendingObj[cont[1].toLowerCase()] = parseScalar(cont[2]);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function parseFence(text) {
|
|
73
|
+
const raw = String(text ?? "");
|
|
74
|
+
const trimmed = raw.trim();
|
|
75
|
+
if (!trimmed.startsWith(":::")) return null;
|
|
76
|
+
const lines = trimmed.split(/\r?\n/);
|
|
77
|
+
if (lines.length < 2) return null;
|
|
78
|
+
const head = lines[0].trim();
|
|
79
|
+
const tail = lines[lines.length - 1].trim();
|
|
80
|
+
const m = head.match(BLOCK_RE);
|
|
81
|
+
if (!m || !CLOSE_RE.test(tail)) return null;
|
|
82
|
+
const type = m[1].toLowerCase();
|
|
83
|
+
if (type !== "gantt") return null;
|
|
84
|
+
return { type, data: parseLinesToObject(lines.slice(1, -1)) };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function parseListItemText(rawText) {
|
|
88
|
+
const raw = String(rawText || "").trim();
|
|
89
|
+
const lines = raw
|
|
90
|
+
.split(/\r?\n/)
|
|
91
|
+
.map((line) => line.trim())
|
|
92
|
+
.filter((line) => line && line !== ":::");
|
|
93
|
+
const obj = {};
|
|
94
|
+
let switchListKey = null;
|
|
95
|
+
for (const line of lines) {
|
|
96
|
+
const kv = line.match(/^([a-z0-9_-]+)\s*:\s*(.+)$/i);
|
|
97
|
+
if (kv) {
|
|
98
|
+
obj[kv[1].toLowerCase()] = parseScalar(kv[2]);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const keyOnly = line.match(/^([a-z0-9_-]+)\s*:\s*$/i);
|
|
102
|
+
if (!keyOnly) continue;
|
|
103
|
+
const key = keyOnly[1].toLowerCase();
|
|
104
|
+
if (key === "tracks" || key === "milestones" || key === "markers") {
|
|
105
|
+
switchListKey = key;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const item = Object.keys(obj).length > 0 ? obj : { label: parseScalar(raw) };
|
|
109
|
+
return { item, switchListKey };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function parseListItemObject(li) {
|
|
113
|
+
const parts = [];
|
|
114
|
+
for (const child of li.querySelectorAll(":scope > p")) {
|
|
115
|
+
const text = (child.textContent || "").trim();
|
|
116
|
+
if (text) parts.push(text);
|
|
117
|
+
}
|
|
118
|
+
const raw = parts.length ? parts.join("\n") : (li.textContent || "").trim();
|
|
119
|
+
return parseListItemText(raw);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function parseGanttNodes(firstLines, between, endNode) {
|
|
123
|
+
const out = parseLinesToObject(firstLines);
|
|
124
|
+
let pendingKey = null;
|
|
125
|
+
|
|
126
|
+
for (const [key, value] of Object.entries(out)) {
|
|
127
|
+
if (Array.isArray(value)) pendingKey = key;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const nodes = [...between, endNode];
|
|
131
|
+
for (const node of nodes) {
|
|
132
|
+
const tag = String(node.tagName || "").toUpperCase();
|
|
133
|
+
if (tag === "P") {
|
|
134
|
+
const lines = (node.textContent || "").split(/\r?\n/);
|
|
135
|
+
for (const rawLine of lines) {
|
|
136
|
+
const line = rawLine.trim();
|
|
137
|
+
if (!line || line === ":::" || line.startsWith("#")) continue;
|
|
138
|
+
const kv = line.match(/^([a-z0-9_-]+)\s*:\s*(.*)$/i);
|
|
139
|
+
if (!kv) continue;
|
|
140
|
+
const key = kv[1].toLowerCase();
|
|
141
|
+
const value = kv[2];
|
|
142
|
+
if (value) {
|
|
143
|
+
out[key] = parseScalar(value);
|
|
144
|
+
pendingKey = null;
|
|
145
|
+
} else {
|
|
146
|
+
pendingKey = key;
|
|
147
|
+
if (!Array.isArray(out[key])) out[key] = [];
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if ((tag === "UL" || tag === "OL") && pendingKey) {
|
|
154
|
+
let activeListKey = pendingKey;
|
|
155
|
+
for (const li of node.querySelectorAll(":scope > li")) {
|
|
156
|
+
const parsed = parseListItemObject(li);
|
|
157
|
+
const list = Array.isArray(out[activeListKey]) ? out[activeListKey] : [];
|
|
158
|
+
out[activeListKey] = list;
|
|
159
|
+
list.push(parsed.item);
|
|
160
|
+
if (activeListKey === "tracks" || activeListKey === "milestones") {
|
|
161
|
+
out.__rowOrder = Array.isArray(out.__rowOrder) ? out.__rowOrder : [];
|
|
162
|
+
out.__rowOrder.push({ kind: activeListKey.slice(0, -1), index: list.length - 1 });
|
|
163
|
+
}
|
|
164
|
+
if (parsed.switchListKey) {
|
|
165
|
+
activeListKey = parsed.switchListKey;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
pendingKey = null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function isoWeekMondayUtcMs(year, week) {
|
|
176
|
+
const jan4 = new Date(Date.UTC(year, 0, 4));
|
|
177
|
+
const jan4Weekday = (jan4.getUTCDay() + 6) % 7;
|
|
178
|
+
const weekOneMonday = jan4.getTime() - jan4Weekday * DAY_MS;
|
|
179
|
+
return weekOneMonday + (week - 1) * WEEK_MS;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function isoWeekFromDayOrdinal(dayOrdinal) {
|
|
183
|
+
const date = new Date(dayOrdinal * DAY_MS);
|
|
184
|
+
const day = (date.getUTCDay() + 6) % 7;
|
|
185
|
+
const thursday = new Date(date.getTime() + (3 - day) * DAY_MS);
|
|
186
|
+
const year = thursday.getUTCFullYear();
|
|
187
|
+
const firstThursday = new Date(Date.UTC(year, 0, 4));
|
|
188
|
+
const firstThursdayDay = (firstThursday.getUTCDay() + 6) % 7;
|
|
189
|
+
const firstThursdayOrdinal = Math.floor(firstThursday.getTime() / DAY_MS) + (3 - firstThursdayDay);
|
|
190
|
+
const week = Math.floor((Math.floor(thursday.getTime() / DAY_MS) - firstThursdayOrdinal) / 7) + 1;
|
|
191
|
+
return { year, week };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function isoWeeksInYear(year) {
|
|
195
|
+
const dec28 = new Date(Date.UTC(year, 11, 28));
|
|
196
|
+
const info = isoWeekFromDayOrdinal(Math.floor(dec28.getTime() / DAY_MS));
|
|
197
|
+
return info.week;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function parseWeekOrdinal(value) {
|
|
201
|
+
const match = String(value || "").trim().match(/^(\d{4})-W(\d{2})$/i);
|
|
202
|
+
if (!match) return null;
|
|
203
|
+
const year = Number.parseInt(match[1], 10);
|
|
204
|
+
const week = Number.parseInt(match[2], 10);
|
|
205
|
+
if (week < 1 || week > isoWeeksInYear(year)) return null;
|
|
206
|
+
const mondayDayOrdinal = Math.floor(isoWeekMondayUtcMs(year, week) / DAY_MS);
|
|
207
|
+
return Math.floor((mondayDayOrdinal - ISO_WEEK_ANCHOR_DAY) / 7);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function parseMonthOrdinal(value) {
|
|
211
|
+
const match = String(value || "").trim().match(/^(\d{4})-(\d{2})$/);
|
|
212
|
+
if (!match) return null;
|
|
213
|
+
const year = Number.parseInt(match[1], 10);
|
|
214
|
+
const month = Number.parseInt(match[2], 10);
|
|
215
|
+
if (month < 1 || month > 12) return null;
|
|
216
|
+
return year * 12 + (month - 1);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function daysInMonthUtc(year, month) {
|
|
220
|
+
return new Date(Date.UTC(year, month, 0)).getUTCDate();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function parseMonthMarkerPosition(value) {
|
|
224
|
+
const monthOrdinal = parseMonthOrdinal(value);
|
|
225
|
+
if (monthOrdinal != null) return monthOrdinal + 0.5;
|
|
226
|
+
|
|
227
|
+
const match = String(value || "").trim().match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
228
|
+
if (!match) return null;
|
|
229
|
+
const year = Number.parseInt(match[1], 10);
|
|
230
|
+
const month = Number.parseInt(match[2], 10);
|
|
231
|
+
const day = Number.parseInt(match[3], 10);
|
|
232
|
+
if (month < 1 || month > 12) return null;
|
|
233
|
+
const dim = daysInMonthUtc(year, month);
|
|
234
|
+
if (day < 1 || day > dim) return null;
|
|
235
|
+
const ordinal = year * 12 + (month - 1);
|
|
236
|
+
return ordinal + (day - 0.5) / dim;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function parseUnitOrdinal(value, unit) {
|
|
240
|
+
if (unit === "week") return parseWeekOrdinal(value);
|
|
241
|
+
if (unit === "month") return parseMonthOrdinal(value);
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function parseMarkerPosition(value, unit) {
|
|
246
|
+
if (unit === "week") {
|
|
247
|
+
const ordinal = parseWeekOrdinal(value);
|
|
248
|
+
return ordinal == null ? null : ordinal + 0.5;
|
|
249
|
+
}
|
|
250
|
+
if (unit === "month") return parseMonthMarkerPosition(value);
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function weekLabelFromOrdinal(ordinal) {
|
|
255
|
+
const mondayDayOrdinal = ISO_WEEK_ANCHOR_DAY + ordinal * 7;
|
|
256
|
+
const info = isoWeekFromDayOrdinal(mondayDayOrdinal);
|
|
257
|
+
return { year: info.year, week: info.week, label: `W${String(info.week).padStart(2, "0")}` };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function monthLabelFromOrdinal(ordinal) {
|
|
261
|
+
const year = Math.floor(ordinal / 12);
|
|
262
|
+
const month = (ordinal % 12) + 1;
|
|
263
|
+
return { year, month, label: MONTH_NAMES[month - 1] || `M${month}` };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function weekAxisContextLabel(startInfo, endInfo) {
|
|
267
|
+
if (!startInfo || !endInfo) return "";
|
|
268
|
+
const startWeek = `W${String(startInfo.week).padStart(2, "0")}`;
|
|
269
|
+
const endWeek = `W${String(endInfo.week).padStart(2, "0")}`;
|
|
270
|
+
if (startInfo.year === endInfo.year) {
|
|
271
|
+
return `ISO Weeks ${startWeek}-${endWeek} (${startInfo.year})`;
|
|
272
|
+
}
|
|
273
|
+
return `ISO Weeks ${startWeek} '${String(startInfo.year).slice(-2)}-${endWeek} '${String(
|
|
274
|
+
endInfo.year,
|
|
275
|
+
).slice(-2)}`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function weekLabelStepForUnits(totalUnits) {
|
|
279
|
+
if (totalUnits > 24) return 4;
|
|
280
|
+
if (totalUnits > 16) return 2;
|
|
281
|
+
return 1;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function buildAxisCellLabel(unit, info, prev, index, totalUnits) {
|
|
285
|
+
const isEdge = index === 0 || index === totalUnits - 1;
|
|
286
|
+
if (unit === "week") {
|
|
287
|
+
const step = weekLabelStepForUnits(totalUnits);
|
|
288
|
+
const show = index % step === 0 || isEdge;
|
|
289
|
+
if (!show) return "";
|
|
290
|
+
const yearChanged = !prev || prev.year !== info.year;
|
|
291
|
+
const weekNumber = String(info.week).padStart(2, "0");
|
|
292
|
+
const base = `W${weekNumber}`;
|
|
293
|
+
const suffix = isEdge || yearChanged ? ` '${String(info.year).slice(-2)}` : "";
|
|
294
|
+
return `${base}${suffix}`;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const yearChanged = !prev || prev.year !== info.year;
|
|
298
|
+
const showYear = info.month === 1 || yearChanged;
|
|
299
|
+
const suffix = showYear ? ` '${String(info.year).slice(-2)}` : "";
|
|
300
|
+
return `${info.label}${suffix}`;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function toPositiveInt(value, fallback) {
|
|
304
|
+
const parsed = Number.parseInt(String(value ?? ""), 10);
|
|
305
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
|
|
306
|
+
return parsed;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function toDepth(value, fallback) {
|
|
310
|
+
const parsed = Number.parseInt(String(value ?? ""), 10);
|
|
311
|
+
if (!Number.isFinite(parsed) || parsed < 1) return fallback;
|
|
312
|
+
return parsed;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function estimateMarkerLabelRem(label) {
|
|
316
|
+
const length = String(label || "").trim().length;
|
|
317
|
+
const estimated = length * 0.38 + 1.9;
|
|
318
|
+
return Math.max(4.2, Math.min(10, estimated));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function assignMarkerLabelLanes(markers) {
|
|
322
|
+
const sorted = markers
|
|
323
|
+
.map((marker, index) => ({ marker, index }))
|
|
324
|
+
.sort((a, b) => a.marker.left - b.marker.left);
|
|
325
|
+
|
|
326
|
+
const laneEndPct = [];
|
|
327
|
+
let maxLanes = 1;
|
|
328
|
+
for (const entry of sorted) {
|
|
329
|
+
const marker = entry.marker;
|
|
330
|
+
const widthPct = (estimateMarkerLabelRem(marker.label) / 26) * 100;
|
|
331
|
+
const flipped = marker.left > 85;
|
|
332
|
+
const start = flipped ? marker.left - widthPct : marker.left;
|
|
333
|
+
const end = flipped ? marker.left : marker.left + widthPct;
|
|
334
|
+
|
|
335
|
+
let lane = 0;
|
|
336
|
+
while (lane < laneEndPct.length && start <= laneEndPct[lane] + 1.2) {
|
|
337
|
+
lane += 1;
|
|
338
|
+
}
|
|
339
|
+
if (lane === laneEndPct.length) laneEndPct.push(-Infinity);
|
|
340
|
+
laneEndPct[lane] = end;
|
|
341
|
+
marker.__flipped = flipped;
|
|
342
|
+
marker.__lane = lane;
|
|
343
|
+
maxLanes = Math.max(maxLanes, lane + 1);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return { markers, laneCount: maxLanes };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function clampRange(start, end, axisStart, axisEnd) {
|
|
350
|
+
const clampedStart = Math.max(start, axisStart);
|
|
351
|
+
const clampedEnd = Math.min(end, axisEnd);
|
|
352
|
+
if (clampedEnd < clampedStart) return null;
|
|
353
|
+
return { start: clampedStart, end: clampedEnd };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function el(tag, className, text) {
|
|
357
|
+
const node = document.createElement(tag);
|
|
358
|
+
if (className) node.className = className;
|
|
359
|
+
if (text != null && text !== "") node.textContent = String(text);
|
|
360
|
+
return node;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function renderGantt(rawData) {
|
|
364
|
+
const data = rawData && typeof rawData === "object" ? rawData : {};
|
|
365
|
+
const unit = String(data["time-unit"] || "week").trim().toLowerCase();
|
|
366
|
+
const axisStart = parseUnitOrdinal(data["time-start"], unit);
|
|
367
|
+
const axisEnd = parseUnitOrdinal(data["time-end"], unit);
|
|
368
|
+
if (axisStart == null || axisEnd == null || axisEnd < axisStart) return null;
|
|
369
|
+
|
|
370
|
+
const totalUnits = axisEnd - axisStart + 1;
|
|
371
|
+
const maxDepth = toPositiveInt(data["max-depth"], Number.MAX_SAFE_INTEGER);
|
|
372
|
+
const maxTracks = toPositiveInt(data["max-tracks"], Number.MAX_SAFE_INTEGER);
|
|
373
|
+
const todayOrdinal = parseUnitOrdinal(data.today, unit);
|
|
374
|
+
|
|
375
|
+
const tracks = Array.isArray(data.tracks) ? data.tracks : [];
|
|
376
|
+
const milestones = Array.isArray(data.milestones) ? data.milestones : [];
|
|
377
|
+
const rawMarkers = Array.isArray(data.markers) ? data.markers : [];
|
|
378
|
+
const chartMarkers = [];
|
|
379
|
+
for (const m of rawMarkers) {
|
|
380
|
+
const item = m && typeof m === "object" ? m : {};
|
|
381
|
+
const markerPosition = parseMarkerPosition(item.date, unit);
|
|
382
|
+
if (markerPosition == null || markerPosition < axisStart || markerPosition > axisEnd + 1)
|
|
383
|
+
continue;
|
|
384
|
+
chartMarkers.push({
|
|
385
|
+
label: String(item.label || "").trim(),
|
|
386
|
+
left: ((markerPosition - axisStart) / totalUnits) * 100,
|
|
387
|
+
color: String(item.color || "").trim(),
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
const markerLayout = assignMarkerLabelLanes(chartMarkers);
|
|
391
|
+
const trackRows = [];
|
|
392
|
+
for (const track of tracks) {
|
|
393
|
+
const item = track && typeof track === "object" ? track : {};
|
|
394
|
+
trackRows.push({
|
|
395
|
+
kind: "track",
|
|
396
|
+
label: String(item.label || "").trim(),
|
|
397
|
+
depth: toDepth(item.depth, 1),
|
|
398
|
+
start: parseUnitOrdinal(item.start, unit),
|
|
399
|
+
end: parseUnitOrdinal(item.end, unit),
|
|
400
|
+
status: String(item.status || "planned").trim().toLowerCase(),
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
const milestoneRows = [];
|
|
404
|
+
for (const milestone of milestones) {
|
|
405
|
+
const item = milestone && typeof milestone === "object" ? milestone : {};
|
|
406
|
+
milestoneRows.push({
|
|
407
|
+
kind: "milestone",
|
|
408
|
+
label: String(item.label || "").trim(),
|
|
409
|
+
depth: toDepth(item.depth, 1),
|
|
410
|
+
date: parseUnitOrdinal(item.date, unit),
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
const rows = [];
|
|
414
|
+
const rowOrder = Array.isArray(data.__rowOrder) ? data.__rowOrder : [];
|
|
415
|
+
if (rowOrder.length > 0) {
|
|
416
|
+
for (const entry of rowOrder) {
|
|
417
|
+
if (!entry || typeof entry !== "object") continue;
|
|
418
|
+
if (entry.kind === "track" && Number.isInteger(entry.index) && trackRows[entry.index]) {
|
|
419
|
+
rows.push(trackRows[entry.index]);
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
if (
|
|
423
|
+
entry.kind === "milestone" &&
|
|
424
|
+
Number.isInteger(entry.index) &&
|
|
425
|
+
milestoneRows[entry.index]
|
|
426
|
+
) {
|
|
427
|
+
rows.push(milestoneRows[entry.index]);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
} else {
|
|
431
|
+
rows.push(...trackRows, ...milestoneRows);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const visibleRows = rows.filter((row) => row.depth <= maxDepth);
|
|
435
|
+
const hiddenCount = visibleRows.length > maxTracks ? visibleRows.length - maxTracks : 0;
|
|
436
|
+
const renderedRows = hiddenCount > 0 ? visibleRows.slice(0, maxTracks) : visibleRows;
|
|
437
|
+
|
|
438
|
+
const root = el("div", "kitfly-visual kitfly-planning-gantt");
|
|
439
|
+
root.setAttribute("data-kitfly-visual", "gantt");
|
|
440
|
+
root.style.setProperty("--kitfly-gantt-units", String(totalUnits));
|
|
441
|
+
root.classList.add(unit === "week" ? "is-week" : "is-month");
|
|
442
|
+
root.style.setProperty("--kitfly-marker-lanes", String(markerLayout.laneCount));
|
|
443
|
+
|
|
444
|
+
const weekLabelStep = unit === "week" ? weekLabelStepForUnits(totalUnits) : 1;
|
|
445
|
+
const weekCompact = unit === "week" && weekLabelStep > 1;
|
|
446
|
+
if (weekCompact) root.classList.add("is-week-compact");
|
|
447
|
+
if (unit === "week" && weekLabelStep > 2) root.classList.add("is-week-ultra-compact");
|
|
448
|
+
if (unit === "week") root.classList.add("has-week-context");
|
|
449
|
+
|
|
450
|
+
const label = String(data.label || "").trim();
|
|
451
|
+
if (label) {
|
|
452
|
+
root.appendChild(el("div", "kitfly-gantt-title", label));
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (unit === "week") {
|
|
456
|
+
const axisContextRow = el("div", "kitfly-gantt-row kitfly-gantt-axis-context-row");
|
|
457
|
+
axisContextRow.appendChild(el("div", "kitfly-gantt-label kitfly-gantt-axis-label", ""));
|
|
458
|
+
const context = el("div", "kitfly-gantt-axis-context");
|
|
459
|
+
const startInfo = weekLabelFromOrdinal(axisStart);
|
|
460
|
+
const endInfo = weekLabelFromOrdinal(axisEnd);
|
|
461
|
+
context.textContent = weekAxisContextLabel(startInfo, endInfo);
|
|
462
|
+
axisContextRow.appendChild(context);
|
|
463
|
+
root.appendChild(axisContextRow);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (chartMarkers.length > 0) {
|
|
467
|
+
const annoRow = el("div", "kitfly-gantt-row kitfly-gantt-marker-row");
|
|
468
|
+
annoRow.appendChild(el("div", "kitfly-gantt-label", ""));
|
|
469
|
+
const annoArea = el("div", "kitfly-gantt-marker-area");
|
|
470
|
+
for (const cm of chartMarkers) {
|
|
471
|
+
const lbl = el("div", "kitfly-gantt-marker-label", cm.label);
|
|
472
|
+
lbl.style.left = `${cm.left}%`;
|
|
473
|
+
lbl.style.setProperty("--kitfly-marker-lane", String(cm.__lane || 0));
|
|
474
|
+
if (cm.color) lbl.style.setProperty("--kitfly-marker-color", cm.color);
|
|
475
|
+
if (cm.__flipped) lbl.classList.add("is-flipped");
|
|
476
|
+
annoArea.appendChild(lbl);
|
|
477
|
+
}
|
|
478
|
+
annoRow.appendChild(annoArea);
|
|
479
|
+
root.appendChild(annoRow);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const axisRow = el("div", "kitfly-gantt-row kitfly-gantt-axis-row");
|
|
483
|
+
axisRow.appendChild(el("div", "kitfly-gantt-label kitfly-gantt-axis-label", ""));
|
|
484
|
+
const axis = el("div", "kitfly-gantt-axis");
|
|
485
|
+
for (let i = 0; i < totalUnits; i++) {
|
|
486
|
+
const ordinal = axisStart + i;
|
|
487
|
+
const cell = el("div", "kitfly-gantt-axis-cell");
|
|
488
|
+
if (i === 0 || i === totalUnits - 1) cell.classList.add("is-edge");
|
|
489
|
+
const info = unit === "week" ? weekLabelFromOrdinal(ordinal) : monthLabelFromOrdinal(ordinal);
|
|
490
|
+
const prev = i > 0 ? (unit === "week" ? weekLabelFromOrdinal(ordinal - 1) : monthLabelFromOrdinal(ordinal - 1)) : null;
|
|
491
|
+
const text = buildAxisCellLabel(unit, info, prev, i, totalUnits);
|
|
492
|
+
if (text) {
|
|
493
|
+
cell.textContent = text;
|
|
494
|
+
cell.classList.add("is-labeled");
|
|
495
|
+
}
|
|
496
|
+
axis.appendChild(cell);
|
|
497
|
+
}
|
|
498
|
+
if (todayOrdinal != null && todayOrdinal >= axisStart && todayOrdinal <= axisEnd) {
|
|
499
|
+
const today = el("div", "kitfly-gantt-today");
|
|
500
|
+
const left = ((todayOrdinal - axisStart + 0.5) / totalUnits) * 100;
|
|
501
|
+
today.style.left = `${left}%`;
|
|
502
|
+
axis.appendChild(today);
|
|
503
|
+
}
|
|
504
|
+
for (const cm of chartMarkers) {
|
|
505
|
+
const line = el("div", "kitfly-gantt-marker-line");
|
|
506
|
+
line.style.left = `${cm.left}%`;
|
|
507
|
+
if (cm.color) line.style.setProperty("--kitfly-marker-color", cm.color);
|
|
508
|
+
axis.appendChild(line);
|
|
509
|
+
}
|
|
510
|
+
axisRow.appendChild(axis);
|
|
511
|
+
root.appendChild(axisRow);
|
|
512
|
+
|
|
513
|
+
for (const row of renderedRows) {
|
|
514
|
+
const rowEl = el("div", "kitfly-gantt-row");
|
|
515
|
+
const labelEl = el("div", "kitfly-gantt-label", row.label || "");
|
|
516
|
+
labelEl.style.setProperty("--kitfly-gantt-depth", String(row.depth || 1));
|
|
517
|
+
labelEl.classList.add(`depth-${Math.min(Math.max(row.depth || 1, 1), 3)}`);
|
|
518
|
+
rowEl.appendChild(labelEl);
|
|
519
|
+
|
|
520
|
+
const chartEl = el("div", "kitfly-gantt-chart");
|
|
521
|
+
chartEl.style.setProperty("--kitfly-gantt-units", String(totalUnits));
|
|
522
|
+
|
|
523
|
+
if (todayOrdinal != null && todayOrdinal >= axisStart && todayOrdinal <= axisEnd) {
|
|
524
|
+
const today = el("div", "kitfly-gantt-today");
|
|
525
|
+
const left = ((todayOrdinal - axisStart + 0.5) / totalUnits) * 100;
|
|
526
|
+
today.style.left = `${left}%`;
|
|
527
|
+
chartEl.appendChild(today);
|
|
528
|
+
}
|
|
529
|
+
for (const cm of chartMarkers) {
|
|
530
|
+
const line = el("div", "kitfly-gantt-marker-line");
|
|
531
|
+
line.style.left = `${cm.left}%`;
|
|
532
|
+
if (cm.color) line.style.setProperty("--kitfly-marker-color", cm.color);
|
|
533
|
+
chartEl.appendChild(line);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (row.kind === "track" && row.start != null && row.end != null) {
|
|
537
|
+
const clipped = clampRange(row.start, row.end, axisStart, axisEnd);
|
|
538
|
+
if (clipped) {
|
|
539
|
+
const bar = el("div", "kitfly-gantt-bar");
|
|
540
|
+
const status = ["planned", "active", "complete", "blocked"].includes(row.status)
|
|
541
|
+
? row.status
|
|
542
|
+
: "planned";
|
|
543
|
+
bar.classList.add(`is-${status}`);
|
|
544
|
+
bar.classList.add(`depth-${Math.min(Math.max(row.depth || 1, 1), 3)}`);
|
|
545
|
+
const left = ((clipped.start - axisStart) / totalUnits) * 100;
|
|
546
|
+
const width = ((clipped.end - clipped.start + 1) / totalUnits) * 100;
|
|
547
|
+
bar.style.left = `${left}%`;
|
|
548
|
+
bar.style.width = `${width}%`;
|
|
549
|
+
chartEl.appendChild(bar);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (row.kind === "milestone" && row.date != null && row.date >= axisStart && row.date <= axisEnd) {
|
|
554
|
+
const marker = el("div", "kitfly-gantt-milestone");
|
|
555
|
+
marker.classList.add(`depth-${Math.min(Math.max(row.depth || 1, 1), 3)}`);
|
|
556
|
+
const left = ((row.date - axisStart + 0.5) / totalUnits) * 100;
|
|
557
|
+
marker.style.left = `${left}%`;
|
|
558
|
+
chartEl.appendChild(marker);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
rowEl.appendChild(chartEl);
|
|
562
|
+
root.appendChild(rowEl);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (hiddenCount > 0) {
|
|
566
|
+
const moreRow = el("div", "kitfly-gantt-row kitfly-gantt-overflow-row");
|
|
567
|
+
moreRow.appendChild(el("div", "kitfly-gantt-label", `+${hiddenCount} more`));
|
|
568
|
+
moreRow.appendChild(el("div", "kitfly-gantt-chart"));
|
|
569
|
+
root.appendChild(moreRow);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return root;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function tryReplaceSingleNode(node) {
|
|
576
|
+
const parsed = parseFence(node.textContent || "");
|
|
577
|
+
if (!parsed || parsed.type !== "gantt") return false;
|
|
578
|
+
const rendered = renderGantt(parsed.data);
|
|
579
|
+
if (!rendered) return false;
|
|
580
|
+
node.replaceWith(rendered);
|
|
581
|
+
return true;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function tryReplaceFragmentedFence(start) {
|
|
585
|
+
const startText = String(start.textContent || "");
|
|
586
|
+
const lines = startText.split(/\r?\n/);
|
|
587
|
+
const firstLine = (lines[0] || "").trim();
|
|
588
|
+
const open = firstLine.match(BLOCK_RE);
|
|
589
|
+
if (!open || open[1].toLowerCase() !== "gantt") return false;
|
|
590
|
+
|
|
591
|
+
const between = [];
|
|
592
|
+
let endNode = null;
|
|
593
|
+
let cur = start.nextElementSibling;
|
|
594
|
+
while (cur) {
|
|
595
|
+
const hasClose = String(cur.textContent || "")
|
|
596
|
+
.split(/\r?\n/)
|
|
597
|
+
.map((line) => line.trim())
|
|
598
|
+
.some((line) => line === ":::");
|
|
599
|
+
if (hasClose) {
|
|
600
|
+
endNode = cur;
|
|
601
|
+
break;
|
|
602
|
+
}
|
|
603
|
+
between.push(cur);
|
|
604
|
+
cur = cur.nextElementSibling;
|
|
605
|
+
}
|
|
606
|
+
if (!endNode) return false;
|
|
607
|
+
|
|
608
|
+
const data = parseGanttNodes(lines.slice(1), between, endNode);
|
|
609
|
+
const rendered = renderGantt(data);
|
|
610
|
+
if (!rendered) return false;
|
|
611
|
+
|
|
612
|
+
start.parentNode.insertBefore(rendered, start);
|
|
613
|
+
const toRemove = [start, ...between, endNode];
|
|
614
|
+
for (const node of toRemove) node.remove();
|
|
615
|
+
return true;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function apply(root) {
|
|
619
|
+
const containers = root.querySelectorAll(".slide, .content");
|
|
620
|
+
for (const container of containers) {
|
|
621
|
+
for (const node of container.querySelectorAll("p, pre, code")) {
|
|
622
|
+
if (node.closest(".kitfly-planning-gantt")) continue;
|
|
623
|
+
tryReplaceSingleNode(node);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
let changed = true;
|
|
627
|
+
while (changed) {
|
|
628
|
+
changed = false;
|
|
629
|
+
const paragraphs = container.querySelectorAll("p");
|
|
630
|
+
for (const p of paragraphs) {
|
|
631
|
+
if (p.closest(".kitfly-planning-gantt")) continue;
|
|
632
|
+
const text = (p.textContent || "").trim();
|
|
633
|
+
if (!text.startsWith(":::")) continue;
|
|
634
|
+
if (tryReplaceFragmentedFence(p)) {
|
|
635
|
+
changed = true;
|
|
636
|
+
break;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (typeof document === "undefined") {
|
|
644
|
+
globalThis.__kitflyPlanningVisualsTest = {
|
|
645
|
+
parseFence,
|
|
646
|
+
parseGanttNodesWithFirstLines: (firstLines, between, endNode) =>
|
|
647
|
+
parseGanttNodes(firstLines, between, endNode),
|
|
648
|
+
buildAxisCellLabel,
|
|
649
|
+
weekAxisContextLabel,
|
|
650
|
+
weekLabelStepForUnits,
|
|
651
|
+
parseListItemText,
|
|
652
|
+
assignMarkerLabelLanes,
|
|
653
|
+
parseMarkerPosition,
|
|
654
|
+
parseUnitOrdinal,
|
|
655
|
+
weekLabelFromOrdinal,
|
|
656
|
+
monthLabelFromOrdinal,
|
|
657
|
+
};
|
|
658
|
+
} else {
|
|
659
|
+
function start() {
|
|
660
|
+
apply(document);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
if (document.readyState === "loading") {
|
|
664
|
+
document.addEventListener("DOMContentLoaded", start);
|
|
665
|
+
} else {
|
|
666
|
+
start();
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
})();
|
package/registry/plugins.yaml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# yaml-language-server: $schema=../schemas/v0/plugin-registry.schema.json
|
|
2
2
|
version: 1
|
|
3
|
-
updated: "2026-
|
|
3
|
+
updated: "2026-03-03"
|
|
4
4
|
baseUrl: "https://github.com/3leaps/kitfly/releases/download"
|
|
5
5
|
# Canonical plugin registry (v0.2.1)
|
|
6
6
|
plugins:
|
|
@@ -58,3 +58,17 @@ plugins:
|
|
|
58
58
|
js: "plugins-dist/slides-charts-lite.js"
|
|
59
59
|
assetSha256:
|
|
60
60
|
js: "sha256:ea1afbe48bc94c617af1ee2d53febcaa8d48e83253094a1cbef598f77ec15013"
|
|
61
|
+
planning-visuals:
|
|
62
|
+
name: "Planning Visuals"
|
|
63
|
+
description: "Planning and timeline widgets for Kitfly (gantt charts, roadmaps)"
|
|
64
|
+
version: "0.2.4"
|
|
65
|
+
contract: "1"
|
|
66
|
+
kitfly: ">=0.2.4 <1.0.0"
|
|
67
|
+
license: MIT
|
|
68
|
+
verified: true
|
|
69
|
+
assets:
|
|
70
|
+
js: "plugins-dist/planning-visuals.js"
|
|
71
|
+
css: "plugins-dist/planning-visuals.css"
|
|
72
|
+
assetSha256:
|
|
73
|
+
js: "sha256:76d65904bb192ce6f0561a477b76fb31075d917b42217c649db66ccb73627463"
|
|
74
|
+
css: "sha256:97d8a2e606d10a4814037cea7ea032e5af83a656f2e6203291add8c8cd819aea"
|