@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,270 @@
1
+ ---
2
+ import SiteTitle from "./SiteTitle.astro";
3
+ import { config, getTranslations } from "../../../src/wiki/config.ts";
4
+ import ThemeSwitch from "./ThemeSwitch.astro";
5
+ import Search from "./Search.astro";
6
+ import LanguageSelect from "@astrojs/starlight/components/LanguageSelect.astro";
7
+ import { teletextPages, FASTEXT } from "../../../src/wiki/teletext.ts";
8
+
9
+ // get current locale from URL
10
+ const locale = Astro.currentLocale || "en";
11
+
12
+ const menu = await getTranslations(locale || "en");
13
+
14
+ const { settings, navigation_button } = config;
15
+ const { main }: { main: any[] } = menu;
16
+ const pathname = Astro.request.url;
17
+
18
+ const hasSidebar = Astro.locals.starlightRoute.hasSidebar;
19
+
20
+ const routeSidebar = Astro.locals.starlightRoute.sidebar;
21
+ const pages = teletextPages(routeSidebar);
22
+ const holdsCurrent = (item: any): boolean => item?.isCurrent || (Array.isArray(item?.entries) && item.entries.some(holdsCurrent));
23
+ const groupIndex = routeSidebar.findIndex(holdsCurrent);
24
+ const pageNo = !hasSidebar ? "P100" : groupIndex >= 0 ? `P${200 + groupIndex * 100}` : "P---";
25
+ ---
26
+
27
+ <div class="tt-status container" aria-hidden="true">
28
+ <span class="tt-status-page" data-tt-page>{pageNo}</span>
29
+ <span class="tt-status-name">{config.site.status_name}</span>
30
+ <span class="tt-status-clock" data-tt-clock></span>
31
+ </div>
32
+
33
+ <nav class="navbar container">
34
+ <!-- logo -->
35
+ <div class="order-0">
36
+ <SiteTitle />
37
+ </div>
38
+ <!-- navbar toggler -->
39
+ <input id="nav-toggle" type="checkbox" class="hidden" />
40
+ <label
41
+ for="nav-toggle"
42
+ class="order-3 cursor-pointer flex items-center lg:hidden dark:text-white lg:order-1 hamburger-menu"
43
+ >
44
+ <svg id="show-button" class="h-6 fill-current block" viewBox="0 0 20 20">
45
+ <title>Menu Open</title>
46
+ <path d="M0 3h20v2H0V3z m0 6h20v2H0V9z m0 6h20v2H0V0z"></path>
47
+ </svg>
48
+ <svg id="hide-button" class="h-6 fill-current hidden" viewBox="0 0 20 20">
49
+ <title>Menu Close</title>
50
+ <polygon
51
+ points="11 9 22 9 22 11 11 11 11 22 9 22 9 11 -2 11 -2 9 9 9 9 -2 11 -2"
52
+ transform="rotate(45 10 10)"></polygon>
53
+ </svg>
54
+ </label>
55
+ <!-- /navbar toggler -->
56
+ <ul
57
+ id="nav-menu"
58
+ class="navbar-nav order-3 hidden w-full pb-6 lg:order-1 lg:flex lg:w-auto lg:space-x-2 lg:pb-0 xl:space-x-8"
59
+ >
60
+ {
61
+ main.map((menu) => (
62
+ <>
63
+ {menu.hasChildren ? (
64
+ <li class="nav-item nav-dropdown group relative">
65
+ <span
66
+ class={`nav-link flex! gap-1! items-center justify-center ${
67
+ menu.children
68
+ ?.map(({ url }: { url: string }) => url)
69
+ .includes(pathname) ||
70
+ menu.children
71
+ ?.map(({ url }: { url: string }) => `${url}/`)
72
+ .includes(pathname)
73
+ ? "active"
74
+ : ""
75
+ }`}
76
+ >
77
+ {menu.name}
78
+ <svg class="h-4 w-4 fill-current" viewBox="0 0 20 20">
79
+ <path d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z" />
80
+ </svg>
81
+ </span>
82
+ <ul class="nav-dropdown-list hidden group-hover:block lg:invisible lg:absolute lg:block lg:opacity-0 lg:group-hover:visible lg:group-hover:opacity-100">
83
+ {menu.children?.map((child: { url: string; name: string; hasChildren?: boolean; children?: { url: string; name: string }[] }) =>
84
+ child.hasChildren && child.children?.length ? (
85
+ <li class="nav-dropdown-item nav-subdropdown">
86
+ <span class="nav-dropdown-link nav-subdropdown-toggle">
87
+ {child.url ? <a href={child.url} class="nav-subdropdown-title">{child.name}</a> : child.name}
88
+ <svg class="nav-subdropdown-arrow h-4 w-4 fill-current" viewBox="0 0 20 20">
89
+ <path d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z" />
90
+ </svg>
91
+ </span>
92
+ <ul class="nav-dropdown-list nav-subdropdown-list">
93
+ {child.children.map((sub) => (
94
+ <li class="nav-dropdown-item">
95
+ <a
96
+ href={sub.url}
97
+ aria-label={sub.name}
98
+ class={`nav-dropdown-link block ${
99
+ (pathname === `${sub.url}/` || pathname === sub.url) && "active"
100
+ }`}
101
+ >
102
+ {sub.name}
103
+ </a>
104
+ </li>
105
+ ))}
106
+ </ul>
107
+ </li>
108
+ ) : (
109
+ <li class="nav-dropdown-item">
110
+ <a
111
+ href={child.url}
112
+ aria-label={child.name}
113
+ class={`nav-dropdown-link block ${
114
+ (pathname === `${child.url}/` ||
115
+ pathname === child.url) &&
116
+ "active"
117
+ }`}
118
+ >
119
+ {child.name}
120
+ </a>
121
+ </li>
122
+ )
123
+ )}
124
+ </ul>
125
+ </li>
126
+ ) : (
127
+ <li class="nav-item">
128
+ <a
129
+ href={menu.url}
130
+ class={`nav-link block ${
131
+ (pathname === `${menu.url}/` || pathname === menu.url) &&
132
+ "active"
133
+ }`}
134
+ >
135
+ {menu.name}
136
+ </a>
137
+ </li>
138
+ )}
139
+ </>
140
+ ))
141
+ }
142
+ {
143
+ navigation_button.enable && (
144
+ <li class="mt-4 inline-block lg:hidden">
145
+ <a
146
+ class="btn btn-outline-primary btn-sm"
147
+ href={navigation_button.link}
148
+ >
149
+ {navigation_button.label}
150
+ </a>
151
+ </li>
152
+ )
153
+ }
154
+ </ul>
155
+
156
+ <div
157
+ class="order-1 ml-auto flex items-center md:order-2 lg:ml-0 navbar-actions"
158
+ >
159
+ {
160
+ hasSidebar && settings.search && (
161
+ <div class="search-bar">
162
+ <Search />
163
+ </div>
164
+ )
165
+ }
166
+
167
+ {!hasSidebar && <div class="mr-4"><LanguageSelect /></div> }
168
+ {settings.theme_switcher && <ThemeSwitch className="lg:mx-6 mr-10" />}
169
+
170
+ {
171
+ navigation_button.enable && (
172
+ <a
173
+ class="btn btn-outline-primary hidden! lg:inline-block!"
174
+ href={navigation_button.link}
175
+ >
176
+ {navigation_button.translations
177
+ ? // @ts-ignore
178
+ navigation_button.translations[locale] || navigation_button.label
179
+ : navigation_button.label}
180
+ </a>
181
+ )
182
+ }
183
+ </div>
184
+ </nav>
185
+
186
+ <nav class="tt-fastext container" aria-label="sections">
187
+ {pages.slice(0, 4).map((p, i) => (
188
+ <a class={`tt-fx tt-fx-${FASTEXT[i]}`} href={p.href}>{p.label}<span class="tt-fx-page">{p.page}</span></a>
189
+ ))}
190
+ </nav>
191
+
192
+ <script type="application/json" id="tt-pages" set:html={JSON.stringify(pages.map((p) => [p.page, p.href]))}></script>
193
+
194
+ <script>
195
+ const clock = document.querySelector<HTMLElement>("[data-tt-clock]");
196
+ const tick = () => {
197
+ if (!clock) return;
198
+ const d = new Date();
199
+ const day = d.toLocaleDateString("en-GB", { weekday: "short", day: "2-digit", month: "short" }).replace(/,/g, "");
200
+ clock.textContent = `${day} ${d.toTimeString().slice(0, 8)}`;
201
+ };
202
+ tick();
203
+ setInterval(tick, 1000);
204
+
205
+ const table = new Map<number, string>([[100, "/"], ...JSON.parse(document.getElementById("tt-pages")?.textContent || "[]")]);
206
+ const pageEl = document.querySelector<HTMLElement>("[data-tt-page]");
207
+ const resting = pageEl?.textContent ?? "";
208
+ let typed = "";
209
+ let reset: number | undefined;
210
+ window.addEventListener("keydown", (e) => {
211
+ const t = e.target as HTMLElement;
212
+ if (e.metaKey || e.ctrlKey || e.altKey || t.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName)) return;
213
+ if (!/^[0-9]$/.test(e.key)) return;
214
+ typed += e.key;
215
+ clearTimeout(reset);
216
+ if (pageEl) pageEl.textContent = `P${typed.padEnd(3, "-")}`;
217
+ if (typed.length < 3) {
218
+ reset = window.setTimeout(() => { typed = ""; if (pageEl) pageEl.textContent = resting; }, 2500);
219
+ return;
220
+ }
221
+ const href = table.get(Number(typed));
222
+ typed = "";
223
+ if (href) location.href = href;
224
+ else if (pageEl) {
225
+ pageEl.textContent = "NO PAGE";
226
+ reset = window.setTimeout(() => { if (pageEl) pageEl.textContent = resting; }, 1500);
227
+ }
228
+ });
229
+ </script>
230
+
231
+ <script>
232
+ // add header bg after scroll
233
+ const nav = document.querySelector(".navbar");
234
+ const sidebar = document.querySelector(".sidebar-nav");
235
+
236
+ window.onscroll = function () {
237
+ if (
238
+ document.body.scrollTop > 10 ||
239
+ document.documentElement.scrollTop > 10
240
+ ) {
241
+ nav?.parentElement?.classList.add("navbar-scrolled");
242
+ sidebar?.classList.add("navbar-scrolled");
243
+ } else {
244
+ nav?.parentElement?.classList.remove("navbar-scrolled");
245
+ sidebar?.classList.remove("navbar-scrolled");
246
+ }
247
+ };
248
+
249
+ // mobile navigation dropdown toggle
250
+ const dropdownMenuToggler = document.querySelectorAll(
251
+ ".nav-dropdown > .nav-link"
252
+ ) as NodeListOf<HTMLElement>;
253
+
254
+ dropdownMenuToggler.forEach((toggler: HTMLElement) => {
255
+ toggler?.addEventListener("click", (e: MouseEvent) => {
256
+ (e?.target as HTMLElement)
257
+ ?.closest(".nav-item")
258
+ ?.classList.toggle("active");
259
+ });
260
+ });
261
+
262
+ // nested dropdowns fly out on hover at desktop widths, tap to expand on mobile
263
+ document.querySelectorAll<HTMLElement>(".nav-subdropdown-toggle").forEach((toggler) => {
264
+ toggler.addEventListener("click", (e: MouseEvent) => {
265
+ if ((e.target as HTMLElement).closest("a")) return;
266
+ e.stopPropagation();
267
+ toggler.closest(".nav-subdropdown")?.classList.toggle("active");
268
+ });
269
+ });
270
+ </script>
@@ -0,0 +1,38 @@
1
+ ---
2
+ import Search from "./Search.astro";
3
+ import { teletextPages } from "../../../src/wiki/teletext.ts";
4
+
5
+ const { data } = Astro.locals.starlightRoute.entry;
6
+ const { title = data.title, tagline } = data.hero || {};
7
+ const { sidebar } = Astro.locals.starlightRoute;
8
+ const pages = teletextPages(sidebar);
9
+ const is404 = Astro.locals.starlightRoute?.id === "404";
10
+ ---
11
+
12
+ <section class="tt-hero">
13
+ <p class="tt-pagehead"><span class="tt-blue">P100</span> INDEX</p>
14
+ {title && <h1 class="tt-title" set:html={title} />}
15
+ {tagline && <p class="tt-tagline" set:html={tagline} />}
16
+
17
+ {
18
+ !is404 && (
19
+ <>
20
+ <div class="tt-search search-bar">
21
+ <Search />
22
+ </div>
23
+ <ol class="tt-index" aria-label="sections by page number">
24
+ {pages.map((p) => (
25
+ <li>
26
+ <a href={p.href}>
27
+ <span class="tt-index-name">{p.label}</span>
28
+ <span class="tt-leader" aria-hidden="true" />
29
+ <span class="tt-index-page">P{p.page}</span>
30
+ </a>
31
+ </li>
32
+ ))}
33
+ </ol>
34
+ <p class="tt-hint">type a page number anywhere to jump to it. / opens search.</p>
35
+ </>
36
+ )
37
+ }
38
+ </section>
@@ -0,0 +1,66 @@
1
+ ---
2
+ // DFM custom components
3
+ import type { ImageMetadata } from "astro";
4
+ import { Image } from "astro:assets";
5
+
6
+ interface Props {
7
+ src: string;
8
+ alt: string;
9
+ width: number;
10
+ height: number;
11
+ loading?: "eager" | "lazy" | null | undefined;
12
+ decoding?: "async" | "auto" | "sync" | null | undefined;
13
+ fetchpriority?: "high" | "low" | "auto" | null | undefined;
14
+ format?: "auto" | "avif" | "jpeg" | "png" | "svg" | "webp";
15
+ class?: string;
16
+ style?: any;
17
+ fit?: "cover" | "contain" | "fill" | "none" | "scale-down";
18
+ }
19
+
20
+ let {
21
+ src,
22
+ alt,
23
+ width,
24
+ height,
25
+ loading,
26
+ decoding,
27
+ fetchpriority,
28
+ class: className,
29
+ format,
30
+ style,
31
+ fit,
32
+ } = Astro.props;
33
+
34
+
35
+
36
+ const images = import.meta.glob("/src/assets/**/*.{jpeg,jpg,png,gif,svg,webp}");
37
+ const normalizedSrc = src.startsWith("/") ? src : `/${src.replace(/^\.\//, "")}`;
38
+
39
+ const isValidPath = images[normalizedSrc] ? true : false;
40
+
41
+
42
+ !isValidPath &&
43
+ console.error(
44
+ `\x1b[31mImage not found - ${src}.\x1b[0m Make sure the image is in the /src/assets folder.`,
45
+ );
46
+
47
+
48
+ ---
49
+
50
+ {
51
+ isValidPath && (
52
+ <Image
53
+ src={images[normalizedSrc]() as Promise<{ default: ImageMetadata }>}
54
+ alt={alt}
55
+ width={width}
56
+ height={height}
57
+ loading={loading}
58
+ decoding={decoding}
59
+ fetchpriority={fetchpriority}
60
+ class={className}
61
+ format={format}
62
+ style={style}
63
+ fit={fit ? fit : "contain"}
64
+ />
65
+ )
66
+ }
@@ -0,0 +1,192 @@
1
+ ---
2
+ // DFM custom components
3
+ export interface Props {
4
+ title?: string;
5
+ svgPath: string; // path to raw SVG content or could be passed via slot
6
+ }
7
+
8
+ const { title = "Circuit Diagram" } = Astro.props;
9
+ ---
10
+ <div class="interactive-circuit">
11
+ <div class="circuit-header">
12
+ <h3>{title}</h3>
13
+ <div class="circuit-controls">
14
+ <button class="circuit-btn" data-action="zoom-in">+</button>
15
+ <button class="circuit-btn" data-action="zoom-out">-</button>
16
+ <button class="circuit-btn" data-action="reset">Reset</button>
17
+ </div>
18
+ </div>
19
+
20
+ <div class="circuit-viewport">
21
+ <div class="circuit-content" id="circuit-container">
22
+ <slot />
23
+ </div>
24
+
25
+ <div class="circuit-info-panel" id="circuit-info">
26
+ <h4 id="info-title">Component Info</h4>
27
+ <p id="info-desc">Click any highlighted component on the diagram to see its details.</p>
28
+ </div>
29
+ </div>
30
+ </div>
31
+
32
+ <script>
33
+ class InteractiveCircuit {
34
+ constructor() {
35
+ this.container = document.getElementById('circuit-container');
36
+ this.infoPanel = document.getElementById('circuit-info');
37
+ this.infoTitle = document.getElementById('info-title');
38
+ this.infoDesc = document.getElementById('info-desc');
39
+ this.scale = 1;
40
+
41
+ this.init();
42
+ }
43
+
44
+ init() {
45
+ if (!this.container) return;
46
+
47
+ document.querySelectorAll('.circuit-btn').forEach(btn => {
48
+ btn.addEventListener('click', (e) => {
49
+ const action = (e.target as HTMLButtonElement).dataset.action;
50
+ if (action === 'zoom-in') this.scale = Math.min(this.scale + 0.2, 3);
51
+ if (action === 'zoom-out') this.scale = Math.max(this.scale - 0.2, 0.5);
52
+ if (action === 'reset') this.scale = 1;
53
+
54
+ this.container.style.transform = `scale(${this.scale})`;
55
+ });
56
+ });
57
+
58
+ // interactive svg targets require class 'interactive-node' with data-title and data-desc
59
+ const nodes = this.container.querySelectorAll('.interactive-node');
60
+ nodes.forEach(node => {
61
+ node.addEventListener('click', (e) => {
62
+ nodes.forEach(n => n.classList.remove('active-node'));
63
+
64
+ const target = e.currentTarget as SVGElement;
65
+ target.classList.add('active-node');
66
+
67
+ if (this.infoTitle && this.infoDesc) {
68
+ this.infoTitle.textContent = target.dataset.title || 'Unknown Component';
69
+ this.infoDesc.textContent = target.dataset.desc || 'No description available.';
70
+ this.infoPanel.classList.add('visible');
71
+ }
72
+ });
73
+ });
74
+ }
75
+ }
76
+
77
+ document.addEventListener('DOMContentLoaded', () => new InteractiveCircuit());
78
+ </script>
79
+
80
+ <style>
81
+ .interactive-circuit {
82
+ border: 1px solid var(--sl-color-gray-5, #444);
83
+ border-radius: 0.5rem;
84
+ background: var(--sl-color-black, #111);
85
+ margin: 2rem 0;
86
+ overflow: hidden;
87
+ }
88
+
89
+ .circuit-header {
90
+ display: flex;
91
+ justify-content: space-between;
92
+ align-items: center;
93
+ padding: 1rem;
94
+ background: var(--sl-color-gray-6, #1a1a1a);
95
+ border-bottom: 1px solid var(--sl-color-gray-5, #444);
96
+ }
97
+
98
+ .circuit-header h3 {
99
+ margin: 0;
100
+ font-size: 1.1rem;
101
+ color: var(--sl-color-white, #fff);
102
+ }
103
+
104
+ .circuit-controls {
105
+ display: flex;
106
+ gap: 0.5rem;
107
+ }
108
+
109
+ .circuit-btn {
110
+ background: var(--sl-color-gray-5, #333);
111
+ color: var(--sl-color-white, #fff);
112
+ border: none;
113
+ border-radius: 4px;
114
+ padding: 0.25rem 0.75rem;
115
+ cursor: pointer;
116
+ font-weight: bold;
117
+ transition: background 0.2s;
118
+ }
119
+
120
+ .circuit-btn:hover {
121
+ background: var(--sl-color-gray-4, #555);
122
+ }
123
+
124
+ .circuit-viewport {
125
+ position: relative;
126
+ width: 100%;
127
+ height: 400px;
128
+ overflow: hidden;
129
+ background: #000;
130
+ display: flex;
131
+ align-items: center;
132
+ justify-content: center;
133
+ }
134
+
135
+ .circuit-content {
136
+ transition: transform 0.2s ease;
137
+ width: 100%;
138
+ height: 100%;
139
+ display: flex;
140
+ align-items: center;
141
+ justify-content: center;
142
+ }
143
+
144
+ .circuit-content :global(svg) {
145
+ max-width: 100%;
146
+ max-height: 100%;
147
+ }
148
+
149
+ .circuit-content :global(.interactive-node) {
150
+ cursor: pointer;
151
+ transition: all 0.2s ease;
152
+ }
153
+
154
+ .circuit-content :global(.interactive-node:hover) {
155
+ filter: drop-shadow(0 0 5px var(--sl-color-accent, #3b82f6));
156
+ }
157
+
158
+ .circuit-content :global(.interactive-node.active-node) {
159
+ filter: drop-shadow(0 0 8px var(--sl-color-green, #4ade80));
160
+ }
161
+
162
+ .circuit-info-panel {
163
+ position: absolute;
164
+ bottom: 1rem;
165
+ right: 1rem;
166
+ width: 300px;
167
+ background: rgba(17, 17, 17, 0.95);
168
+ border: 1px solid var(--sl-color-gray-5, #444);
169
+ border-radius: 0.5rem;
170
+ padding: 1rem;
171
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.5);
172
+ transform: translateY(120%);
173
+ transition: transform 0.3s ease;
174
+ }
175
+
176
+ .circuit-info-panel.visible {
177
+ transform: translateY(0);
178
+ }
179
+
180
+ .circuit-info-panel h4 {
181
+ margin: 0 0 0.5rem 0;
182
+ color: var(--sl-color-accent, #3b82f6);
183
+ font-size: 1rem;
184
+ }
185
+
186
+ .circuit-info-panel p {
187
+ margin: 0;
188
+ font-size: 0.9rem;
189
+ color: var(--sl-color-gray-2, #ccc);
190
+ line-height: 1.4;
191
+ }
192
+ </style>
@@ -0,0 +1,18 @@
1
+ ---
2
+ // DFM custom components
3
+ export interface Props {
4
+ jurisdiction?: string;
5
+ }
6
+ const { jurisdiction = "your local jurisdiction" } = Astro.props;
7
+ ---
8
+ <div class="not-content border-l-4 border-red-600 bg-red-900/20 p-4 my-6 rounded-r-md">
9
+ <div class="flex items-center gap-2 text-red-500 font-bold mb-2">
10
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>
11
+ LEGAL DISCLAIMER
12
+ </div>
13
+ <p class="text-sm m-0 text-red-200">
14
+ The deployment of this hardware may violate laws in <strong>{jurisdiction}</strong>.
15
+ This information is provided strictly for educational and defensive auditing purposes.
16
+ You are solely responsible for compliance with local communications and privacy legislation.
17
+ </p>
18
+ </div>
@@ -0,0 +1,105 @@
1
+ ---
2
+ import {Icon} from '@astrojs/starlight/components';
3
+ ---
4
+
5
+ <starlight-menu-button class="print:hidden">
6
+ <button
7
+ aria-expanded="false"
8
+ aria-label={Astro.locals.t('menuButton.accessibleLabel')}
9
+ aria-controls="starlight__sidebar"
10
+ class="sl-flex md:sl-hidden"
11
+ >
12
+ <Icon name="bars" class="open-menu" />
13
+ <Icon name="close" class="close-menu" />
14
+ </button>
15
+ </starlight-menu-button>
16
+
17
+ <script>
18
+ class StarlightMenuButton extends HTMLElement {
19
+ btn = this.querySelector('button')!;
20
+
21
+ constructor() {
22
+ super();
23
+ this.btn.addEventListener('click', () => this.toggleExpanded());
24
+
25
+ const parentNav = this.closest('nav');
26
+ if (parentNav) {
27
+ parentNav.addEventListener('keyup', (e) => this.closeOnEscape(e));
28
+ }
29
+ }
30
+
31
+ setExpanded(expanded: boolean) {
32
+ this.setAttribute('aria-expanded', String(expanded));
33
+ document.body.toggleAttribute('data-mobile-menu-expanded', expanded);
34
+ }
35
+
36
+ toggleExpanded() {
37
+ this.setExpanded(this.getAttribute('aria-expanded') !== 'true');
38
+ }
39
+
40
+ closeOnEscape(e: KeyboardEvent) {
41
+ if (e.code === 'Escape') {
42
+ this.setExpanded(false);
43
+ this.btn.focus();
44
+ }
45
+ }
46
+ }
47
+
48
+ customElements.define('starlight-menu-button', StarlightMenuButton);
49
+ </script>
50
+
51
+ <style>
52
+ @layer starlight.core {
53
+ button {
54
+ position: fixed;
55
+ top: calc((var(--sl-nav-height) - var(--sl-menu-button-size)) / 2);
56
+ inset-inline-end: var(--sl-nav-pad-x);
57
+ z-index: var(--sl-z-index-navbar);
58
+ border: 0;
59
+ border-radius: 50%;
60
+ width: var(--sl-menu-button-size);
61
+ height: var(--sl-menu-button-size);
62
+ padding: 0.5rem;
63
+ background-color: var(--sl-color-white);
64
+ color: var(--sl-color-black);
65
+ box-shadow: var(--sl-shadow-md);
66
+ cursor: pointer;
67
+ }
68
+
69
+ [aria-expanded='true'] button {
70
+ background-color: var(--sl-color-gray-2);
71
+ box-shadow: none;
72
+ }
73
+
74
+ [aria-expanded='true'] button .open-menu {
75
+ display: none;
76
+ }
77
+
78
+ :not([aria-expanded='true']) button .close-menu {
79
+ display: none;
80
+ }
81
+
82
+ :global([data-theme='light']) button {
83
+ background-color: var(--sl-color-black);
84
+ color: var(--sl-color-white);
85
+ }
86
+
87
+ :global([data-theme='light']) [aria-expanded='true'] button {
88
+ background-color: var(--sl-color-gray-5);
89
+ }
90
+ }
91
+ </style>
92
+
93
+ <style is:global>
94
+ @layer starlight.core {
95
+ [data-mobile-menu-expanded] {
96
+ overflow: hidden;
97
+ }
98
+
99
+ @media (min-width: 50rem) {
100
+ [data-mobile-menu-expanded] {
101
+ overflow: auto;
102
+ }
103
+ }
104
+ }
105
+ </style>