@dontfeedmachines/fieldkit 0.0.0-stage → 0.4.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.
Files changed (50) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/LICENSE +203 -0
  3. package/NOTICE +131 -0
  4. package/README.md +12 -2
  5. package/css/base.css +150 -0
  6. package/package.json +43 -3
  7. package/pouches/sutures/astro/AnsiLogo.astro +51 -0
  8. package/pouches/sutures/astro/Barcode.astro +19 -0
  9. package/pouches/sutures/astro/BomTable.astro +44 -0
  10. package/pouches/sutures/astro/Breadcrumb.astro +56 -0
  11. package/pouches/sutures/astro/BuildStep.astro +42 -0
  12. package/pouches/sutures/astro/BuildSteps.astro +63 -0
  13. package/pouches/sutures/astro/Button.astro +5 -0
  14. package/pouches/sutures/astro/CTA.astro +58 -0
  15. package/pouches/sutures/astro/Difficulty.astro +27 -0
  16. package/pouches/sutures/astro/Footer.astro +80 -0
  17. package/pouches/sutures/astro/Glossary.astro +64 -0
  18. package/pouches/sutures/astro/Grid.astro +54 -0
  19. package/pouches/sutures/astro/Header.astro +270 -0
  20. package/pouches/sutures/astro/Hero.astro +38 -0
  21. package/pouches/sutures/astro/ImageMod.astro +66 -0
  22. package/pouches/sutures/astro/InteractiveCircuit.astro +192 -0
  23. package/pouches/sutures/astro/LegalWarning.astro +18 -0
  24. package/pouches/sutures/astro/MobileMenuToggle.astro +105 -0
  25. package/pouches/sutures/astro/PageFrame.astro +166 -0
  26. package/pouches/sutures/astro/Search.astro +541 -0
  27. package/pouches/sutures/astro/Section.astro +14 -0
  28. package/pouches/sutures/astro/SidebarNav.astro +106 -0
  29. package/pouches/sutures/astro/SidebarRestorePoint.astro +10 -0
  30. package/pouches/sutures/astro/SidebarSublist.astro +257 -0
  31. package/pouches/sutures/astro/SiteTitle.astro +65 -0
  32. package/pouches/sutures/astro/SpecTable.astro +64 -0
  33. package/pouches/sutures/astro/TeletextChar.astro +51 -0
  34. package/pouches/sutures/astro/ThemeSwitch.astro +93 -0
  35. package/pouches/sutures/astro/ThreatLevel.astro +77 -0
  36. package/pouches/sutures/astro/ThreatModel.astro +32 -0
  37. package/pouches/sutures/astro/TwoColumnContent.astro +84 -0
  38. package/pouches/sutures/astro/Win.astro +13 -0
  39. package/pouches/sutures/astro/index.ts +21 -0
  40. package/src/elements.ts +404 -0
  41. package/src/lib/scrapeview.ts +61 -0
  42. package/src/lib/seal.ts +157 -0
  43. package/src/lib/teletext-chars.ts +723 -0
  44. package/src/node/astro.ts +121 -0
  45. package/src/node/registry.ts +221 -0
  46. package/src/node/seal.ts +7 -0
  47. package/src/wiki/ansi.ts +15 -0
  48. package/src/wiki/config.ts +5 -0
  49. package/src/wiki/teletext.ts +21 -0
  50. package/src/wiki/textConverter.ts +16 -0
@@ -0,0 +1,77 @@
1
+ ---
2
+ // DFM custom components
3
+ export interface Props {
4
+ level: "low" | "medium" | "high" | "critical";
5
+ title?: string;
6
+ }
7
+
8
+ const { level, title } = Astro.props;
9
+
10
+ const config = {
11
+ low: { color: "var(--sl-color-green, #4ade80)", icon: "✓", defaultTitle: "OPSEC: LOW RISK" },
12
+ medium: { color: "var(--sl-color-orange, #fb923c)", icon: "⚠", defaultTitle: "OPSEC: MEDIUM RISK" },
13
+ high: { color: "var(--sl-color-red, #f87171)", icon: "☠", defaultTitle: "OPSEC: HIGH RISK" },
14
+ critical: { color: "#ff0000", icon: "☢", defaultTitle: "OPSEC: CRITICAL THREAT" },
15
+ };
16
+
17
+ const current = config[level];
18
+ ---
19
+ <div class={`threat-level-box threat-${level}`}>
20
+ <div class="threat-header" style={`color: ${current.color}; border-bottom-color: ${current.color};`}>
21
+ <span class="threat-icon">{current.icon}</span>
22
+ <span class="threat-title">{title || current.defaultTitle}</span>
23
+ </div>
24
+ <div class="threat-content">
25
+ <slot />
26
+ </div>
27
+ </div>
28
+
29
+ <style>
30
+ .threat-level-box {
31
+ margin: 2rem 0;
32
+ border: 2px solid;
33
+ border-radius: 0.5rem;
34
+ background: var(--sl-color-black, #111);
35
+ position: relative;
36
+ overflow: hidden;
37
+ }
38
+
39
+ /* hazard stripes for critical */
40
+ .threat-critical::before {
41
+ content: '';
42
+ position: absolute;
43
+ top: 0; left: 0; right: 0;
44
+ height: 6px;
45
+ background: repeating-linear-gradient(45deg, #000, #000 10px, #ff0000 10px, #ff0000 20px);
46
+ }
47
+
48
+ .threat-low { border-color: var(--sl-color-green, #4ade80); }
49
+ .threat-medium { border-color: var(--sl-color-orange, #fb923c); }
50
+ .threat-high { border-color: var(--sl-color-red, #f87171); }
51
+ .threat-critical { border-color: #ff0000; }
52
+
53
+ .threat-header {
54
+ display: flex;
55
+ align-items: center;
56
+ gap: 0.75rem;
57
+ padding: 0.75rem 1rem;
58
+ border-bottom: 1px solid;
59
+ background: var(--sl-color-gray-6, #1a1a1a);
60
+ font-weight: 700;
61
+ letter-spacing: 0.05em;
62
+ }
63
+
64
+ .threat-icon {
65
+ font-size: 1.25rem;
66
+ }
67
+
68
+ .threat-content {
69
+ padding: 1rem;
70
+ color: var(--sl-color-gray-2, #ddd);
71
+ line-height: 1.6;
72
+ }
73
+
74
+ .threat-content :global(p:last-child) {
75
+ margin-bottom: 0;
76
+ }
77
+ </style>
@@ -0,0 +1,32 @@
1
+ ---
2
+ // DFM custom components
3
+ export interface Props {
4
+ vectors: string[];
5
+ }
6
+ const { vectors } = Astro.props;
7
+
8
+ const allVectors = [
9
+ { id: 'acoustic', label: 'Acoustic' },
10
+ { id: 'rf', label: 'RF / Wireless' },
11
+ { id: 'visual', label: 'Visual / Optical' },
12
+ { id: 'network', label: 'Network / Digital' }
13
+ ];
14
+ ---
15
+ <div class="not-content my-6">
16
+ <h4 class="text-sm font-bold uppercase tracking-widest text-zinc-400 mb-3">Mitigated Threat Vectors</h4>
17
+ <div class="flex flex-wrap gap-2">
18
+ {allVectors.map(v => {
19
+ const active = vectors.includes(v.id);
20
+ return (
21
+ <div class={`flex items-center gap-2 px-3 py-1.5 rounded-full text-xs font-semibold ${active ? 'bg-emerald-900/30 text-emerald-400 border border-emerald-800' : 'bg-zinc-800/30 text-zinc-600 border border-zinc-800'}`}>
22
+ {active ? (
23
+ <svg class="w-3 h-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>
24
+ ) : (
25
+ <svg class="w-3 h-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
26
+ )}
27
+ {v.label}
28
+ </div>
29
+ )
30
+ })}
31
+ </div>
32
+ </div>
@@ -0,0 +1,84 @@
1
+ ---
2
+ import Breadcrumb from "./Breadcrumb.astro";
3
+ ---
4
+
5
+ <div class="content-container">
6
+ <div class="main-pane">
7
+ {
8
+ Astro.locals.starlightRoute.hasSidebar && (
9
+ <div class="breadcrumb-container">
10
+ <Breadcrumb />
11
+ </div>
12
+ )
13
+ }
14
+
15
+ <slot />
16
+ </div>
17
+ {
18
+ Astro.locals.starlightRoute.toc && (
19
+ <aside class="right-sidebar-container print:hidden">
20
+ <div class="right-sidebar">
21
+ <slot name="right-sidebar" />
22
+ </div>
23
+ </aside>
24
+ )
25
+ }
26
+ </div>
27
+
28
+ <style>
29
+ @layer starlight.core {
30
+ .content-container {
31
+ width: 100%;
32
+ overflow: hidden;
33
+ opacity: 0;
34
+ transition: opacity 0.3s ease-in-out;
35
+ }
36
+
37
+ .main-pane {
38
+ width: 100%;
39
+ }
40
+ :global([data-has-sidebar][data-has-toc]) .main-pane {
41
+ overflow: hidden;
42
+ }
43
+
44
+ @media (min-width: 72rem) {
45
+ :global([data-has-sidebar][data-has-toc]) .content-container {
46
+ display: grid;
47
+ grid-template-columns: 1fr auto;
48
+ overflow: visible;
49
+ }
50
+ .right-sidebar-container {
51
+ order: 2;
52
+ position: relative;
53
+ }
54
+
55
+ .right-sidebar {
56
+ position: sticky;
57
+ inset-block: calc(var(--sl-nav-height) + 110px) 0;
58
+ width: 100%;
59
+ height: 80vh;
60
+ overflow-y: auto;
61
+ scrollbar-width: none;
62
+ }
63
+
64
+ :global([data-has-sidebar][data-has-toc]) .main-pane {
65
+ --sl-content-margin-inline: auto 0;
66
+
67
+ order: 1;
68
+ overflow: hidden;
69
+ border-left: 1px solid var(--sl-color-hairline);
70
+ border-right: 1px solid var(--sl-color-hairline);
71
+ }
72
+ }
73
+ }
74
+ </style>
75
+
76
+ <script>
77
+ const contentContainer = document.querySelector(
78
+ ".content-container"
79
+ ) as HTMLElement;
80
+
81
+ if (contentContainer) {
82
+ contentContainer.style.opacity = "1";
83
+ }
84
+ </script>
@@ -0,0 +1,13 @@
1
+ ---
2
+ // DFM custom components
3
+ interface Props { title: string; n?: string; id?: string; class?: string; tone?: "pink" | "mint" | "lemon" | "sky" }
4
+ const { title, n, id, class: cls, tone } = Astro.props;
5
+ ---
6
+ <section class:list={["win", tone && `t-${tone}`, cls]} id={id}>
7
+ <header class="win-bar">
8
+ <span class="win-t">{title}</span>
9
+ {n && <span class="win-n">{n}</span>}
10
+ <span class="win-ctl" aria-hidden="true"><i>_</i><i>□</i><i>×</i></span>
11
+ </header>
12
+ <div class="win-in"><slot /></div>
13
+ </section>
@@ -0,0 +1,21 @@
1
+ // barrel for @dontfeedmachines/fieldkit/astro. re-exports the generic shared .astro files as named
2
+ // exports, the same way @astrojs/starlight/components does, so authoring surfaces can do
3
+ // `import { ThreatLevel, SpecTable } from '@dontfeedmachines/fieldkit/astro'`.
4
+ // the wiki set (Header, Hero, Footer, PageFrame and their helpers) needs Starlight and
5
+ // the fieldkitWiki integration, so it is not in the barrel; import it by path, e.g.
6
+ // `@dontfeedmachines/fieldkit/astro/Header.astro`.
7
+ export { default as BomTable } from "./BomTable.astro";
8
+ export { default as BuildStep } from "./BuildStep.astro";
9
+ export { default as BuildSteps } from "./BuildSteps.astro";
10
+ export { default as Difficulty } from "./Difficulty.astro";
11
+ export { default as Glossary } from "./Glossary.astro";
12
+ export { default as Grid } from "./Grid.astro";
13
+ export { default as ImageMod } from "./ImageMod.astro";
14
+ export { default as InteractiveCircuit } from "./InteractiveCircuit.astro";
15
+ export { default as LegalWarning } from "./LegalWarning.astro";
16
+ export { default as SpecTable } from "./SpecTable.astro";
17
+ export { default as ThreatLevel } from "./ThreatLevel.astro";
18
+ export { default as ThreatModel } from "./ThreatModel.astro";
19
+ export { default as Win } from "./Win.astro";
20
+ export { default as Barcode } from "./Barcode.astro";
21
+ export { default as TeletextChar } from "./TeletextChar.astro";
@@ -0,0 +1,404 @@
1
+ import { ttCharSpec, ttMosaicSpec } from "./lib/teletext-chars";
2
+
3
+ export type SectorId = "dfm" | "dfm-tools" | "humanlayer" | "stopthescrape" | "synthstop" |
4
+ "synthstop" | "sweetspot" | "poisongarden" | "porchlights" | "the-leash" | "voidops" | "flockbuster";
5
+
6
+ const SECTORS: { id: SectorId; label: string; href: string; devPort?: number }[] = [
7
+ { id: "dfm", label: "DFM", href: "https://dontfeedmachines.com/" },
8
+ { id: "dfm-tools", label: "DFM.TOOLS", href: "http://dfm.tools" },
9
+ { id: "humanlayer", label: "HUMANLAYER", href: "https://humanlayer.vip/" },
10
+ { id: "stopthescrape", label: "STOPTHESCRAPE", href: "https://stopthescrape.com/" },
11
+ { id: "synthstop", label: "SYNTHSTOP", href: "https://synths.top/" },
12
+ { id: "sweetspot", label: "SWEETSPOT", href: "https://s-w-e-e-t.spot/" },
13
+ { id: "poisongarden", label: "POISONGARDEN", href: "http://poisoned.work" },
14
+ { id: "porchlights", label: "PORCHLIGHTS", href: "https://porchlights.fyi" },
15
+ { id: "the-leash", label: "THE LEASH", href: "https://leash.dontfeedmachines.com" },
16
+ { id: "voidops", label: "VOIDOPS", href: "https://voidops.observer" },
17
+ { id: "flockbuster", label: "FLOCKBUSTER", href: "https://flockbuster.org" },
18
+ ];
19
+
20
+ const isLocal = () => ["localhost", "127.0.0.1", "[::1]"].includes(location.hostname);
21
+
22
+ export function sectorHref(id: SectorId): string {
23
+ const s = SECTORS.find((x) => x.id === id)!;
24
+ if (isLocal()) {
25
+ if (id === "bloodoath") return "https://bloodoath.dev";
26
+ }
27
+ return s.href;
28
+ }
29
+
30
+ function store(key: string, value?: string | null): string | null {
31
+ try {
32
+ if (value === undefined) return localStorage.getItem(key);
33
+ if (value === null) localStorage.removeItem(key);
34
+ else localStorage.setItem(key, value);
35
+ } catch {}
36
+ return null;
37
+ }
38
+
39
+ export function setPlain(on: boolean) {
40
+ document.documentElement.toggleAttribute("data-plain", on);
41
+ store("dfm:plain", on ? "1" : null);
42
+ document.dispatchEvent(new CustomEvent("dfm:plain", { detail: { on } }));
43
+ }
44
+
45
+ export function setLens(on: boolean) {
46
+ document.documentElement.toggleAttribute("data-lens", on);
47
+ document.dispatchEvent(new CustomEvent("dfm:lens", { detail: { on } }));
48
+ }
49
+
50
+ const lensOn = () => document.documentElement.hasAttribute("data-lens");
51
+ const plainOn = () => document.documentElement.hasAttribute("data-plain");
52
+
53
+ class SectorBar extends HTMLElement {
54
+ connectedCallback() {
55
+ const current = this.getAttribute("current");
56
+ const nav = document.createElement("nav");
57
+ nav.setAttribute("aria-label", "dfm sectors");
58
+ for (const s of SECTORS) {
59
+ const a = document.createElement("a");
60
+ a.href = sectorHref(s.id);
61
+ a.textContent = s.label;
62
+ if (s.id === current) a.setAttribute("aria-current", "page");
63
+ nav.append(a);
64
+ }
65
+ const keys = document.createElement("button");
66
+ keys.className = "keys";
67
+ keys.type = "button";
68
+ keys.textContent = "? KEYS";
69
+ keys.addEventListener("click", () => document.querySelector("dfm-keys")?.toggleAttribute("open"));
70
+ nav.append(keys);
71
+ this.replaceChildren(nav);
72
+ }
73
+ }
74
+
75
+ class CodeSlip extends HTMLElement {
76
+ #pre!: HTMLPreElement;
77
+ #count!: HTMLSpanElement;
78
+ #stamp!: HTMLSpanElement;
79
+ #obs?: MutationObserver;
80
+
81
+ connectedCallback() {
82
+ if (this.#pre) return;
83
+ const existing = this.querySelector("pre");
84
+ const pre = existing ?? document.createElement("pre");
85
+ const name = this.getAttribute("name") ?? "output.txt";
86
+ const wrap = document.createElement("div");
87
+ wrap.className = "slip";
88
+ const head = document.createElement("div");
89
+ head.className = "slip-head";
90
+ const label = document.createElement("span");
91
+ label.className = "slip-name";
92
+ label.textContent = name;
93
+ this.#count = document.createElement("span");
94
+ this.#count.className = "slip-count";
95
+ const copy = document.createElement("button");
96
+ copy.type = "button";
97
+ copy.textContent = "COPY";
98
+ copy.addEventListener("click", () => this.copy());
99
+ const dl = document.createElement("button");
100
+ dl.type = "button";
101
+ dl.textContent = "DOWNLOAD";
102
+ dl.addEventListener("click", () => this.download());
103
+ head.append(label, this.#count, copy, dl);
104
+ this.#stamp = document.createElement("span");
105
+ this.#stamp.className = "slip-stamp";
106
+ this.#stamp.setAttribute("aria-hidden", "true");
107
+ this.#stamp.textContent = this.getAttribute("stamp") ?? "COPIED";
108
+ wrap.append(head, pre);
109
+ this.replaceChildren(wrap, this.#stamp);
110
+ this.#pre = pre;
111
+ this.#obs = new MutationObserver(() => this.#recount());
112
+ this.#obs.observe(pre, { childList: true, characterData: true, subtree: true });
113
+ this.#recount();
114
+ }
115
+
116
+ disconnectedCallback() {
117
+ this.#obs?.disconnect();
118
+ }
119
+
120
+ set text(v: string) {
121
+ if (this.#pre) this.#pre.textContent = v;
122
+ }
123
+ get text() {
124
+ return this.#pre?.textContent ?? "";
125
+ }
126
+
127
+ #recount() {
128
+ const lines = this.text ? this.text.split("\n").length : 0;
129
+ this.#count.textContent = `${lines} line${lines === 1 ? "" : "s"}`;
130
+ }
131
+
132
+ async copy() {
133
+ try {
134
+ await navigator.clipboard.writeText(this.text);
135
+ } catch {
136
+ const r = document.createRange();
137
+ r.selectNodeContents(this.#pre);
138
+ const sel = getSelection();
139
+ sel?.removeAllRanges();
140
+ sel?.addRange(r);
141
+ }
142
+ this.#stamp.classList.add("on");
143
+ setTimeout(() => this.#stamp.classList.remove("on"), 900);
144
+ }
145
+
146
+ download() {
147
+ const name = this.getAttribute("name") ?? "output.txt";
148
+ const blob = new Blob([this.text], { type: "text/plain" });
149
+ const a = document.createElement("a");
150
+ a.href = URL.createObjectURL(blob);
151
+ a.download = name;
152
+ a.click();
153
+ setTimeout(() => URL.revokeObjectURL(a.href), 1000);
154
+ }
155
+ }
156
+
157
+ type IndexEntry = { title: string; href: string; kind: string; text?: string };
158
+
159
+ class Palette extends HTMLElement {
160
+ #items: IndexEntry[] | null = null;
161
+ #input!: HTMLInputElement;
162
+ #list!: HTMLUListElement;
163
+ #sel = 0;
164
+ #shown: IndexEntry[] = [];
165
+ #ret: Element | null = null;
166
+
167
+ connectedCallback() {
168
+ if (this.#input) return;
169
+ this.setAttribute("role", "dialog");
170
+ this.setAttribute("aria-label", "search this site");
171
+ const box = document.createElement("div");
172
+ box.className = "pal";
173
+ this.#input = document.createElement("input");
174
+ this.#input.type = "search";
175
+ this.#input.placeholder = this.getAttribute("placeholder") ?? "search pages and tools";
176
+ this.#input.setAttribute("aria-label", "search");
177
+ this.#list = document.createElement("ul");
178
+ this.#list.setAttribute("role", "listbox");
179
+ box.append(this.#input, this.#list);
180
+ this.replaceChildren(box);
181
+ this.#input.addEventListener("input", () => this.#filter());
182
+ this.#input.addEventListener("keydown", (e) => this.#key(e));
183
+ this.addEventListener("click", (e) => {
184
+ if (e.target === this) this.close();
185
+ });
186
+ }
187
+
188
+ async open() {
189
+ this.#ret = document.activeElement;
190
+ this.setAttribute("open", "");
191
+ this.#input.value = "";
192
+ if (!this.#items) {
193
+ const src = this.getAttribute("src") ?? "/search.json";
194
+ try {
195
+ // a "#id" src reads an inline json script, for pages whose CSP forbids fetch
196
+ if (src.startsWith("#")) this.#items = JSON.parse(document.querySelector(src)?.textContent ?? "[]");
197
+ else this.#items = await (await fetch(src)).json();
198
+ } catch {
199
+ this.#items = [];
200
+ }
201
+ }
202
+ this.#filter();
203
+ this.#input.focus();
204
+ }
205
+
206
+ close() {
207
+ this.removeAttribute("open");
208
+ (this.#ret as HTMLElement | null)?.focus?.();
209
+ }
210
+
211
+ #filter() {
212
+ const q = this.#input.value.trim().toLowerCase();
213
+ const all = this.#items ?? [];
214
+ const terms = q.split(/\s+/).filter(Boolean);
215
+ this.#shown = (terms.length
216
+ ? all.filter((it) => {
217
+ const hay = `${it.title} ${it.kind} ${it.text ?? ""}`.toLowerCase();
218
+ return terms.every((t) => hay.includes(t));
219
+ })
220
+ : all
221
+ ).slice(0, 40);
222
+ this.#sel = 0;
223
+ this.#render();
224
+ }
225
+
226
+ #render() {
227
+ this.#list.replaceChildren();
228
+ if (!this.#shown.length) {
229
+ const li = document.createElement("li");
230
+ li.className = "pal-empty";
231
+ li.textContent = "nothing matches. try fewer words.";
232
+ this.#list.append(li);
233
+ return;
234
+ }
235
+ this.#shown.forEach((it, i) => {
236
+ const li = document.createElement("li");
237
+ li.setAttribute("role", "option");
238
+ li.setAttribute("aria-selected", String(i === this.#sel));
239
+ const a = document.createElement("a");
240
+ a.href = it.href;
241
+ const t = document.createElement("span");
242
+ t.textContent = it.title;
243
+ const k = document.createElement("small");
244
+ k.textContent = it.kind;
245
+ a.append(t, k);
246
+ li.append(a);
247
+ this.#list.append(li);
248
+ });
249
+ this.#list.children[this.#sel]?.scrollIntoView({ block: "nearest" });
250
+ }
251
+
252
+ #key(e: KeyboardEvent) {
253
+ if (e.key === "Escape") {
254
+ e.preventDefault();
255
+ this.close();
256
+ } else if (e.key === "ArrowDown") {
257
+ e.preventDefault();
258
+ this.#sel = Math.min(this.#sel + 1, this.#shown.length - 1);
259
+ this.#render();
260
+ } else if (e.key === "ArrowUp") {
261
+ e.preventDefault();
262
+ this.#sel = Math.max(this.#sel - 1, 0);
263
+ this.#render();
264
+ } else if (e.key === "Enter") {
265
+ const it = this.#shown[this.#sel];
266
+ if (it) location.href = it.href;
267
+ }
268
+ }
269
+ }
270
+
271
+ class KeysHelp extends HTMLElement {
272
+ connectedCallback() {
273
+ if (this.childElementCount) return;
274
+ const lens = this.getAttribute("lens") ?? "LENS";
275
+ const plain = this.getAttribute("plain") ?? "PLAIN MODE";
276
+ this.setAttribute("role", "dialog");
277
+ this.setAttribute("aria-label", "keyboard keys");
278
+ const card = document.createElement("div");
279
+ card.className = "keys-card";
280
+ const rows: [string, string][] = [
281
+ ["L", lens],
282
+ ["P", plain],
283
+ ["/", "search"],
284
+ ["?", "this list"],
285
+ ["ESC", "close"],
286
+ ];
287
+ const dl = document.createElement("dl");
288
+ for (const [k, v] of rows) {
289
+ const dt = document.createElement("dt");
290
+ const kbd = document.createElement("kbd");
291
+ kbd.textContent = k;
292
+ dt.append(kbd);
293
+ const dd = document.createElement("dd");
294
+ dd.textContent = v;
295
+ dl.append(dt, dd);
296
+ }
297
+ const h = document.createElement("strong");
298
+ h.textContent = "KEYS";
299
+ card.append(h, dl);
300
+ this.append(card);
301
+ this.addEventListener("click", () => this.removeAttribute("open"));
302
+ }
303
+ }
304
+
305
+ class TeletextCharElement extends HTMLElement {
306
+ static observedAttributes = ["name", "char", "mosaic", "separated", "size", "label"];
307
+
308
+ connectedCallback() {
309
+ this.#render();
310
+ }
311
+
312
+ attributeChangedCallback() {
313
+ if (this.isConnected) this.#render();
314
+ }
315
+
316
+ #render() {
317
+ const mosaic = this.getAttribute("mosaic");
318
+ const isMosaic = mosaic !== null;
319
+ const spec = isMosaic
320
+ ? ttMosaicSpec(mosaic, this.hasAttribute("separated"))
321
+ : ttCharSpec(this.getAttribute("name") ?? this.getAttribute("char") ?? "");
322
+ if (!spec) {
323
+ this.textContent = this.getAttribute("char") ?? this.getAttribute("name") ?? "";
324
+ return;
325
+ }
326
+ const ns = "http://www.w3.org/2000/svg";
327
+ const svg = document.createElementNS(ns, "svg");
328
+ const h = this.getAttribute("size") ?? (isMosaic ? "var(--tt-mosaic-height,1em)" : "var(--tt-char-size,0.85em)");
329
+ const w = isMosaic ? `calc((${h}) * 2 / 3)` : h;
330
+ svg.setAttribute("class", isMosaic ? "tt-char tt-mosaic" : "tt-char");
331
+ svg.setAttribute("viewBox", `0 0 ${spec.w} ${spec.h}`);
332
+ svg.setAttribute("fill", "currentColor");
333
+ svg.setAttribute("shape-rendering", "crispEdges");
334
+ svg.style.display = "inline-block";
335
+ svg.style.width = w;
336
+ svg.style.height = h;
337
+ svg.style.verticalAlign = `calc(0.5cap - (${h}) / 2 + var(--tt-char-voffset, -0.02em))`;
338
+ svg.style.flex = "none";
339
+ const label = this.getAttribute("label");
340
+ if (label) {
341
+ svg.setAttribute("role", "img");
342
+ svg.setAttribute("aria-label", label);
343
+ } else {
344
+ svg.setAttribute("aria-hidden", "true");
345
+ }
346
+ const path = document.createElementNS(ns, "path");
347
+ path.setAttribute("d", spec.d);
348
+ svg.append(path);
349
+ this.replaceChildren(svg);
350
+ }
351
+ }
352
+
353
+ function typing(t: EventTarget | null) {
354
+ const el = t as HTMLElement | null;
355
+ if (!el) return false;
356
+ return el.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName);
357
+ }
358
+
359
+ let booted = false;
360
+
361
+ export function boot() {
362
+ if (booted) return;
363
+ booted = true;
364
+ customElements.get("dfm-sector-bar") || customElements.define("dfm-sector-bar", SectorBar);
365
+ customElements.get("dfm-code-slip") || customElements.define("dfm-code-slip", CodeSlip);
366
+ customElements.get("dfm-palette") || customElements.define("dfm-palette", Palette);
367
+ customElements.get("dfm-keys") || customElements.define("dfm-keys", KeysHelp);
368
+ customElements.get("dfm-tt-char") || customElements.define("dfm-tt-char", TeletextCharElement);
369
+
370
+ if (store("dfm:plain") === "1") document.documentElement.setAttribute("data-plain", "");
371
+
372
+ document.addEventListener("click", (e) => {
373
+ const t = (e.target as HTMLElement | null)?.closest?.("[data-toggle]");
374
+ if (!t) return;
375
+ const what = t.getAttribute("data-toggle");
376
+ if (what === "plain") setPlain(!plainOn());
377
+ if (what === "lens") setLens(!lensOn());
378
+ });
379
+
380
+ document.addEventListener("keydown", (e) => {
381
+ if (e.metaKey || e.ctrlKey || e.altKey) return;
382
+ const pal = document.querySelector<Palette>("dfm-palette");
383
+ const keys = document.querySelector("dfm-keys");
384
+ if (e.key === "Escape") {
385
+ if (pal?.hasAttribute("open")) pal.close();
386
+ keys?.removeAttribute("open");
387
+ return;
388
+ }
389
+ if (typing(e.target)) return;
390
+ const k = e.key.toLowerCase();
391
+ if (k === "/") {
392
+ e.preventDefault();
393
+ pal?.open();
394
+ } else if (k === "?") {
395
+ keys?.toggleAttribute("open");
396
+ } else if (k === "l") {
397
+ setLens(!lensOn());
398
+ } else if (k === "p") {
399
+ setPlain(!plainOn());
400
+ }
401
+ });
402
+ }
403
+
404
+ export { lensOn, plainOn };