@stack-mode/site-manager-kit 0.0.0-stage → 1.44.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/README.md +606 -3
- package/audit/ts-hooks.mjs +68 -0
- package/audit/wiring-audit.d.mts +37 -0
- package/audit/wiring-audit.mjs +604 -0
- package/bin/site-manager-kit.mjs +10 -0
- package/package.json +72 -6
- package/src/analytics.tsx +23 -0
- package/src/announcements.ts +13 -0
- package/src/attribution-fields.ts +52 -0
- package/src/attribution.tsx +47 -0
- package/src/blog.ts +122 -0
- package/src/chat-route.ts +136 -0
- package/src/chat-types.ts +76 -0
- package/src/client.ts +116 -0
- package/src/config.ts +78 -0
- package/src/copy-keys-route.ts +21 -0
- package/src/copy-keys.ts +77 -0
- package/src/data.ts +189 -0
- package/src/downloads.ts +7 -0
- package/src/faq.ts +21 -0
- package/src/floor-plans.ts +45 -0
- package/src/form-guard.tsx +37 -0
- package/src/form-token.ts +33 -0
- package/src/form-types.ts +13 -0
- package/src/forms.ts +76 -0
- package/src/glossary.ts +19 -0
- package/src/health-route.ts +34 -0
- package/src/image.ts +12 -0
- package/src/index.ts +36 -0
- package/src/indexnow.ts +40 -0
- package/src/listings.ts +38 -0
- package/src/metadata.ts +55 -0
- package/src/money.ts +10 -0
- package/src/not-found-reporter.tsx +16 -0
- package/src/not-found.ts +53 -0
- package/src/payment.ts +70 -0
- package/src/phone-input.tsx +26 -0
- package/src/phone.ts +37 -0
- package/src/preview-banner.tsx +49 -0
- package/src/preview-route.ts +48 -0
- package/src/preview.ts +94 -0
- package/src/redirects.ts +57 -0
- package/src/residents.ts +23 -0
- package/src/revalidate-route.ts +34 -0
- package/src/robots.ts +26 -0
- package/src/rss-route.ts +26 -0
- package/src/services.ts +52 -0
- package/src/signature.ts +23 -0
- package/src/site-chat-panel.tsx +537 -0
- package/src/site-chat.tsx +26 -0
- package/src/sitemap.ts +26 -0
- package/src/structured-data.tsx +207 -0
- package/src/types.ts +580 -0
- package/src/version.ts +1 -0
- package/src/visual-accents.ts +126 -0
- package/src/visual-bridge-client.tsx +1070 -0
- package/src/visual-bridge.tsx +30 -0
- package/src/visual-clicks.ts +13 -0
- package/src/visual-lists.ts +37 -0
- package/src/visual-match.ts +77 -0
- package/src/visual-messages.ts +160 -0
- package/src/visual-mode.ts +18 -0
- package/src/visual-preview.ts +69 -0
- package/src/visual-tags.ts +90 -0
- package/src/visual.ts +59 -0
- package/src/waitlist-form.ts +39 -0
- package/src/waitlist.ts +48 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Gold accent words in a heading: one text value plus the ranges of it shown in the accent color, marked with the
|
|
3
|
+
* Accent control, never typed markup. Stored as `{ accents: [[start, end], ...] }` in the key's valueJson beside its
|
|
4
|
+
* text, so every reader of the text keeps working. Pure and safe in the browser. Site Manager has the same
|
|
5
|
+
* rules (src/modules/site_copy/accents.ts there); change both together.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type Accent = [number, number];
|
|
9
|
+
export type AccentRun = { text: string; accent: boolean };
|
|
10
|
+
|
|
11
|
+
const MAX_RANGES = 50;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The ranges, checked against the text: whole numbers inside it, empty ones dropped, sorted, and overlapping or
|
|
15
|
+
* touching ones merged. Null when the shape isn't a list of [start, end] pairs.
|
|
16
|
+
*/
|
|
17
|
+
export function normalizeAccents(text: string, accents: unknown): Accent[] | null {
|
|
18
|
+
if (!Array.isArray(accents) || accents.length > MAX_RANGES) return null;
|
|
19
|
+
const out: Accent[] = [];
|
|
20
|
+
for (const a of accents) {
|
|
21
|
+
if (!Array.isArray(a) || a.length !== 2 || !a.every((n) => Number.isInteger(n))) return null;
|
|
22
|
+
const start = Math.max(0, Math.min(a[0] as number, text.length));
|
|
23
|
+
const end = Math.max(0, Math.min(a[1] as number, text.length));
|
|
24
|
+
if (end > start) out.push([start, end]);
|
|
25
|
+
}
|
|
26
|
+
out.sort((x, y) => x[0] - y[0]);
|
|
27
|
+
const merged: Accent[] = [];
|
|
28
|
+
for (const r of out) {
|
|
29
|
+
const last = merged[merged.length - 1];
|
|
30
|
+
if (last && r[0] <= last[1]) last[1] = Math.max(last[1], r[1]);
|
|
31
|
+
else merged.push([r[0], r[1]]);
|
|
32
|
+
}
|
|
33
|
+
return merged;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The text as runs, plain and accented, in order. Empty runs are left out. */
|
|
37
|
+
export function accentRuns(text: string, accents: Accent[]): AccentRun[] {
|
|
38
|
+
const runs: AccentRun[] = [];
|
|
39
|
+
let at = 0;
|
|
40
|
+
for (const [s, e] of accents) {
|
|
41
|
+
if (s > at) runs.push({ text: text.slice(at, s), accent: false });
|
|
42
|
+
runs.push({ text: text.slice(s, e), accent: true });
|
|
43
|
+
at = e;
|
|
44
|
+
}
|
|
45
|
+
if (at < text.length) runs.push({ text: text.slice(at), accent: false });
|
|
46
|
+
return runs.filter((r) => r.text.length > 0);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The accents stored with a text value, or undefined when it has none (an older heading split into two keys). */
|
|
50
|
+
export function storedAccents(valueJson: unknown, text: string): Accent[] | undefined {
|
|
51
|
+
const a = (valueJson as { accents?: unknown } | null)?.accents;
|
|
52
|
+
if (a === undefined) return undefined;
|
|
53
|
+
return normalizeAccents(text, a) ?? [];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Runs back to one text and its ranges, the way the heading editor reads what was typed. */
|
|
57
|
+
export function fromRuns(runs: AccentRun[]): { text: string; accents: Accent[] } {
|
|
58
|
+
let text = "";
|
|
59
|
+
const accents: Accent[] = [];
|
|
60
|
+
for (const r of runs) {
|
|
61
|
+
if (r.accent && r.text) accents.push([text.length, text.length + r.text.length]);
|
|
62
|
+
text += r.text;
|
|
63
|
+
}
|
|
64
|
+
return { text, accents: normalizeAccents(text, accents) ?? [] };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** A text value's gold words as runs, for a site's heading component: one plain run when it has none. */
|
|
68
|
+
export function headingRuns(value: { text: string | null; accents?: Accent[] | null } | null | undefined): AccentRun[] {
|
|
69
|
+
const text = value?.text ?? "";
|
|
70
|
+
return accentRuns(text, normalizeAccents(text, value?.accents ?? []) ?? []);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/*
|
|
74
|
+
* Bold, italic and gold together: a text value may mark words as important (strong, rendered <strong>), emphasized
|
|
75
|
+
* (em, rendered <em>) and gold (accents, a styled span). All are ranges of the plain text, never markup.
|
|
76
|
+
*/
|
|
77
|
+
export type Mark = "accent" | "strong" | "em";
|
|
78
|
+
export type MarkedRun = { text: string; accent: boolean; strong: boolean; em: boolean };
|
|
79
|
+
export type Marks = { accents?: Accent[] | null; strong?: Accent[] | null; em?: Accent[] | null };
|
|
80
|
+
|
|
81
|
+
/** The text as runs split wherever a mark starts or ends. Empty runs are left out. */
|
|
82
|
+
export function markedRuns(text: string, marks: Marks): MarkedRun[] {
|
|
83
|
+
const accents = normalizeAccents(text, marks.accents ?? []) ?? [];
|
|
84
|
+
const strong = normalizeAccents(text, marks.strong ?? []) ?? [];
|
|
85
|
+
const em = normalizeAccents(text, marks.em ?? []) ?? [];
|
|
86
|
+
const cuts = new Set<number>([0, text.length]);
|
|
87
|
+
for (const [s, e] of [...accents, ...strong, ...em]) {
|
|
88
|
+
cuts.add(s);
|
|
89
|
+
cuts.add(e);
|
|
90
|
+
}
|
|
91
|
+
const points = [...cuts].sort((a, b) => a - b);
|
|
92
|
+
const inside = (ranges: Accent[], i: number) => ranges.some(([s, e]) => i >= s && i < e);
|
|
93
|
+
const runs: MarkedRun[] = [];
|
|
94
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
95
|
+
const from = points[i]!;
|
|
96
|
+
const to = points[i + 1]!;
|
|
97
|
+
if (to <= from) continue;
|
|
98
|
+
const run = { text: text.slice(from, to), accent: inside(accents, from), strong: inside(strong, from), em: inside(em, from) };
|
|
99
|
+
const last = runs[runs.length - 1];
|
|
100
|
+
if (last && last.accent === run.accent && last.strong === run.strong && last.em === run.em) last.text += run.text;
|
|
101
|
+
else runs.push(run);
|
|
102
|
+
}
|
|
103
|
+
return runs;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Runs back to one text and its ranges of each mark, the way the editor reads what was typed. */
|
|
107
|
+
export function fromMarkedRuns(runs: { text: string; accent?: boolean; strong?: boolean; em?: boolean }[]): { text: string; accents: Accent[]; strong: Accent[]; em: Accent[] } {
|
|
108
|
+
let text = "";
|
|
109
|
+
const accents: Accent[] = [];
|
|
110
|
+
const strong: Accent[] = [];
|
|
111
|
+
const em: Accent[] = [];
|
|
112
|
+
for (const r of runs) {
|
|
113
|
+
const span: Accent = [text.length, text.length + r.text.length];
|
|
114
|
+
if (r.text && r.accent) accents.push(span);
|
|
115
|
+
if (r.text && r.strong) strong.push(span);
|
|
116
|
+
if (r.text && r.em) em.push(span);
|
|
117
|
+
text += r.text;
|
|
118
|
+
}
|
|
119
|
+
const n = (x: Accent[]) => normalizeAccents(text, x) ?? [];
|
|
120
|
+
return { text, accents: n(accents), strong: n(strong), em: n(em) };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** A text value as runs with every mark, for a site's text components: one plain run when it has none. */
|
|
124
|
+
export function textRuns(value: { text: string | null; accents?: Accent[] | null; strong?: Accent[] | null; em?: Accent[] | null } | null | undefined): MarkedRun[] {
|
|
125
|
+
return markedRuns(value?.text ?? "", value ?? {});
|
|
126
|
+
}
|