website-xp-phone 1.5.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.
Files changed (123) hide show
  1. package/.astro/content-assets.mjs +1 -0
  2. package/.astro/content-modules.mjs +1 -0
  3. package/.astro/content.d.ts +199 -0
  4. package/.astro/data-store.json +1 -0
  5. package/.astro/settings.json +8 -0
  6. package/.astro/types.d.ts +1 -0
  7. package/.devcontainer/devcontainer.json +23 -0
  8. package/.env.firebase.example +8 -0
  9. package/.firebaserc +5 -0
  10. package/.gitattributes +2 -0
  11. package/.github/copilot-instructions.md +131 -0
  12. package/.github/dependabot.yml +11 -0
  13. package/.github/workflows/ci.yml +45 -0
  14. package/.github/workflows/deploy-admin.yml +48 -0
  15. package/.github/workflows/static.yml +43 -0
  16. package/.gitmodules +5 -0
  17. package/FIREBASE_SETUP.md +69 -0
  18. package/README.md +63 -0
  19. package/SECURITY.md +11 -0
  20. package/admin/Admin.csproj +7 -0
  21. package/admin/Dockerfile +14 -0
  22. package/admin/Program.cs +8 -0
  23. package/deploy-admin-cloud-run.md +229 -0
  24. package/eslint.config.js +28 -0
  25. package/firebase.json +5 -0
  26. package/firestore.rules +29 -0
  27. package/index.html +52 -0
  28. package/package.json +48 -0
  29. package/pagerts_output.json +1 -0
  30. package/public/5.html +967 -0
  31. package/public/BAHNSCHRIFT.TTF +0 -0
  32. package/public/Beep.ogg +0 -0
  33. package/public/Clippy.png +0 -0
  34. package/public/Layered Network Security Model for Home Networks (slides).pdf +0 -0
  35. package/public/Layered Network Security Model for Home Networks.pdf +0 -0
  36. package/public/TODO.pdf +0 -0
  37. package/public/WoW_Config.zip +3 -0
  38. package/public/addons/energy-swing.txt +1 -0
  39. package/public/addons/lego-yoda-death-readme.txt +11 -0
  40. package/public/addons/lego-yoda-death.mp3 +0 -0
  41. package/public/addons/mana-blast.txt +1 -0
  42. package/public/addons/rage-volley.txt +1 -0
  43. package/public/addons/rueg-cell.txt +1 -0
  44. package/public/addons/rueg-elvui-profile.txt +1 -0
  45. package/public/addons/rueg-grid2.txt +214 -0
  46. package/public/addons/rueg-plater-smol.txt +1 -0
  47. package/public/addons/rueg-plater.txt +1 -0
  48. package/public/addons/rueg-wa-druid.txt +1 -0
  49. package/public/addons/rueg-wa-priest.txt +1 -0
  50. package/public/addons/rueg-wa-rogue.txt +1 -0
  51. package/public/addons/rueg-wa-shaman.txt +1 -0
  52. package/public/addons/rueg-wa-warrior.txt +1 -0
  53. package/public/addons/spirit-smash.txt +1 -0
  54. package/public/avatar.jpg +0 -0
  55. package/public/avatar.png +0 -0
  56. package/public/crunchy_kick.ogg +0 -0
  57. package/public/documents/resume.html +312 -0
  58. package/public/favicon.ico +0 -0
  59. package/public/images/Ateric1.png +0 -0
  60. package/public/images/Ateric2.png +0 -0
  61. package/public/images/equal1.png +0 -0
  62. package/public/images/hyperawareofwhatacatis.png +0 -0
  63. package/public/images/kogg1.png +0 -0
  64. package/public/images/kogg2.png +0 -0
  65. package/public/images/rueg1.png +0 -0
  66. package/public/images/rueg2.png +0 -0
  67. package/public/incorrect_responses.txt +126 -0
  68. package/public/loading.css +51 -0
  69. package/public/resume.pdf +0 -0
  70. package/public/robots.txt +9 -0
  71. package/public/soundcloud.json +57 -0
  72. package/public/spinner.svg +12 -0
  73. package/public/tada.wav +0 -0
  74. package/public/yooh.mp3 +0 -0
  75. package/render.yaml +5 -0
  76. package/scripts/ensure-blog-worktree.mjs +24 -0
  77. package/scripts/generate-soundcloud-json.mjs +198 -0
  78. package/scripts/git-worktree-helper.mjs +122 -0
  79. package/scripts/hoist-dev-blog-local.mjs +149 -0
  80. package/scripts/music-schema.mjs +56 -0
  81. package/scripts/publish-soundcloud-json.mjs +32 -0
  82. package/scripts/sync-music-links-from-worktree.mjs +32 -0
  83. package/src/App.tsx +1500 -0
  84. package/src/addons.json +76 -0
  85. package/src/components/Addon.tsx +223 -0
  86. package/src/components/BlogContent.tsx +103 -0
  87. package/src/components/CopyToClipboardButton.tsx +21 -0
  88. package/src/components/MenuBar.tsx +151 -0
  89. package/src/components/MenuBarWithContext.tsx +6 -0
  90. package/src/components/Modal.tsx +17 -0
  91. package/src/components/MusicContent.tsx +309 -0
  92. package/src/components/NavBarController.tsx +55 -0
  93. package/src/components/NavBarControllerWrapper.tsx +13 -0
  94. package/src/components/Page.tsx +56 -0
  95. package/src/components/SitemapContent.tsx +125 -0
  96. package/src/contacts.json +32 -0
  97. package/src/env.d.ts +13 -0
  98. package/src/lib/assistantStateMachine.ts +80 -0
  99. package/src/lib/audioOverlap.ts +99 -0
  100. package/src/lib/keyboardInputUtils.ts +182 -0
  101. package/src/lib/musicSchema.ts +85 -0
  102. package/src/lib/naggingAssistantClient.ts +241 -0
  103. package/src/lib/resumeAnalytics.ts +163 -0
  104. package/src/main.tsx +35 -0
  105. package/src/pages.json +50 -0
  106. package/src/sections.json +243 -0
  107. package/src/src+addons.zip +3 -0
  108. package/src/styles/main.css +465 -0
  109. package/src/utils/blogSecurity.ts +87 -0
  110. package/src/utils/menuItems.ts +33 -0
  111. package/src/windowing/MinimizedSections.tsx +86 -0
  112. package/src/windowing/Section.tsx +586 -0
  113. package/src/windowing/context.tsx +13 -0
  114. package/src/windowing/hooks.ts +10 -0
  115. package/src/windowing/index.ts +7 -0
  116. package/src/windowing/provider.tsx +74 -0
  117. package/src/windowing/server.ts +3 -0
  118. package/src/windowing/types.ts +33 -0
  119. package/src/windowing/utils.ts +135 -0
  120. package/tests/generate-soundcloud-json.test.mjs +63 -0
  121. package/tests/music-schema.test.mjs +53 -0
  122. package/tsconfig.json +26 -0
  123. package/vite.config.ts +304 -0
@@ -0,0 +1,76 @@
1
+ {
2
+ "heading": "Welcome to Rueg's UI homepage",
3
+ "content": [
4
+ {
5
+ "heading": "On ElvUI and Malware hidden within game addons",
6
+ "content": "Do not use ElvUI. [ Read more... ](https://akinevz.com/blog/?post=twelfth-post-discussion-security-wow-addons)",
7
+ "status": "notice",
8
+ "link": "https://akinevz.com/blog/?post=twelfth-post-discussion-security-wow-addons"
9
+ },
10
+ {
11
+ "heading": "Rueg WeakAura Rogue (v3.0.2)",
12
+ "content": "Weak Aura tracker for GCD, energy tickrate, and range",
13
+ "status": "available",
14
+ "link": "/addons/energy-swing.txt"
15
+ },
16
+ {
17
+ "heading": "Rueg WeakAura Mage (v3.0.0)",
18
+ "content": "Weak Aura tracker for GCD, energy tickrate, and range",
19
+ "status": "available",
20
+ "link": "/addons/mana-blast.txt"
21
+ },
22
+ {
23
+ "heading": "Rueg WeakAura Priest (v5.0.1)",
24
+ "content": "Weak Aura tracker for GCD, mana tickrate, and range",
25
+ "status": "available",
26
+ "link": "/addons/rueg-wa-priest.txt"
27
+ },
28
+ {
29
+ "heading": "Rueg WeakAura Warrior (v2.3.1)",
30
+ "content": "Weak Aura tracker for GCD, rage starvation, and range",
31
+ "status": "available",
32
+ "link": "/addons/rage-volley.txt"
33
+ },
34
+ {
35
+ "heading": "Rueg WeakAura Druid (v5.0.0)",
36
+ "content": "Weak Aura tracker for swing, energy recharge, and range",
37
+ "status": "available",
38
+ "link": "/addons/rueg-wa-druid.txt"
39
+ },
40
+ {
41
+ "heading": "Rueg WeakAura Shaman (v4.0.0)",
42
+ "content": "Weak Aura tracker for swing, energy recharge, and range",
43
+ "status": "available",
44
+ "link": "/addons/spirit-smash.txt"
45
+ },
46
+ {
47
+ "heading": "Rueg Cell (v1.0)",
48
+ "content": "Cell preset with right-handed layout + stats in chat.",
49
+ "status": "available",
50
+ "link": "/addons/rueg-cell.txt"
51
+ },
52
+ {
53
+ "heading": "Rueg Grid2 (v1.0)",
54
+ "content": "Grid2 preset with buff indicators.",
55
+ "status": "available (unmaintained)",
56
+ "link": "/addons/rueg-grid2.txt"
57
+ },
58
+ {
59
+ "heading": "Rueg Plater Bars (v2.2)",
60
+ "content": "Profile for Plater with Blizzard-like nameplates. Large-format",
61
+ "status": "available",
62
+ "link": "/addons/rueg-plater.txt"
63
+ },
64
+ {
65
+ "heading": "Rueg Plater Small Bars (v2.1)",
66
+ "content": "Profile for Plater with Blizzard-like nameplates.",
67
+ "status": "available",
68
+ "link": "/addons/rueg-plater-smol.txt"
69
+ },
70
+ {
71
+ "heading": "Rueg Plater Compatible Repack (v0.1)",
72
+ "content": "Addon list for RP coming soon.",
73
+ "status": "soon (tm)"
74
+ }
75
+ ]
76
+ }
@@ -0,0 +1,223 @@
1
+ import { CopyToClipboardButton } from "./CopyToClipboardButton.tsx";
2
+ import type { Heading, SectionProps } from "../windowing";
3
+ import { useRef, useState } from "react";
4
+ import { playLayeredAudio } from "../lib/audioOverlap";
5
+ import Markdown, { type Options as ReactMarkdownOptions } from "react-markdown";
6
+ import rehypeRaw from "rehype-raw";
7
+ import rehypeSanitize, { defaultSchema } from "rehype-sanitize";
8
+
9
+ export type AddonProps = SectionProps & {
10
+ status?: string | undefined;
11
+ text?: string | undefined;
12
+ link?: string | undefined;
13
+ content?: string | (string | AddonProps)[] | undefined;
14
+ };
15
+
16
+ export type AddonContent = string | (string | AddonProps)[];
17
+
18
+ const markdownSanitizeSchema: unknown = {
19
+ ...defaultSchema,
20
+ tagNames: [...(defaultSchema.tagNames || []), "iframe"],
21
+ attributes: {
22
+ ...defaultSchema.attributes,
23
+ a: [...(defaultSchema.attributes?.a || []), ["target"], ["rel"]],
24
+ img: [...(defaultSchema.attributes?.img || []), ["loading"], ["decoding"]],
25
+ iframe: [
26
+ ["title"],
27
+ ["src"],
28
+ ["width"],
29
+ ["height"],
30
+ ["style"],
31
+ ["scrolling"],
32
+ ["loading"],
33
+ ["allow"],
34
+ ["allowfullscreen"],
35
+ ["referrerpolicy"],
36
+ ["frameborder"],
37
+ ],
38
+ },
39
+ };
40
+
41
+ const markdownRehypePlugins = [
42
+ rehypeRaw,
43
+ [rehypeSanitize, markdownSanitizeSchema],
44
+ ] as ReactMarkdownOptions["rehypePlugins"];
45
+
46
+ const markdownComponents = {
47
+ img: (props: React.ImgHTMLAttributes<HTMLImageElement>) => (
48
+ <img
49
+ {...props}
50
+ style={{ maxWidth: "100%", height: "auto", ...(props.style ?? {}) }}
51
+ />
52
+ ),
53
+ };
54
+
55
+ function renderHeading(heading: Heading, link?: string) {
56
+ return <RenderLink link={link} text={heading} />;
57
+ }
58
+
59
+ function renderContent(content: AddonContent, status?: string, text?: string) {
60
+ if (typeof content === "string")
61
+ return (
62
+ <ul>
63
+ <li key={0}>
64
+ <Markdown
65
+ rehypePlugins={markdownRehypePlugins}
66
+ components={markdownComponents}
67
+ >
68
+ {content}
69
+ </Markdown>
70
+ </li>
71
+ {renderAddon(status, text)}
72
+ </ul>
73
+ );
74
+ return (
75
+ <ul>
76
+ {content.map((text, index) =>
77
+ typeof text == "string" ? (
78
+ <li key={index}>
79
+ <Markdown
80
+ rehypePlugins={markdownRehypePlugins}
81
+ components={markdownComponents}
82
+ >
83
+ {text}
84
+ </Markdown>
85
+ </li>
86
+ ) : (
87
+ <Addon key={index} {...text} />
88
+ ),
89
+ )}
90
+ </ul>
91
+ );
92
+ }
93
+
94
+ function renderStatus(status: string) {
95
+ return (
96
+ <li className="addon">
97
+ status: <em>{status}</em>
98
+ </li>
99
+ );
100
+ }
101
+
102
+ function RenderLink(props: { link: string | undefined; text: string }) {
103
+ const { link, text } = props;
104
+ if (link) {
105
+ return (
106
+ <a href={link} target="_blank">
107
+ {text}
108
+ </a>
109
+ );
110
+ }
111
+ return <>{text}</>;
112
+ }
113
+
114
+ function renderAddon(status?: string, text?: string) {
115
+ const elements = [];
116
+ if (status) {
117
+ elements.push(<span key="status">{renderStatus(status)}</span>);
118
+ }
119
+ if (text) {
120
+ elements.push(
121
+ <span key="text">
122
+ <CopyToClipboardButton content={text} />
123
+ </span>,
124
+ );
125
+ }
126
+ return elements;
127
+ }
128
+
129
+ const playSound = (clickCount: number) => {
130
+ const probability = 1 / Math.log(clickCount + Math.E);
131
+ if (Math.random() < probability) {
132
+ playLayeredAudio("/crunchy_kick.ogg");
133
+ window.dispatchEvent(new CustomEvent("crunchy-kick-played"));
134
+ }
135
+ };
136
+
137
+ type WindowPanelProps = AddonProps & {
138
+ asList?: boolean;
139
+ };
140
+
141
+ const WindowPanel = ({
142
+ heading,
143
+ content,
144
+ link,
145
+ status,
146
+ text,
147
+ className,
148
+ children,
149
+ }: WindowPanelProps) => {
150
+ const hasHeading = !!heading;
151
+ const hasContent = !!content;
152
+ const [isMaximized, setIsMaximized] = useState(false);
153
+ const closeClickCountRef = useRef(0);
154
+
155
+ const handleMaximize = () => {
156
+ setIsMaximized(!isMaximized);
157
+ };
158
+
159
+ const handleClose = () => {
160
+ if (isMaximized) {
161
+ setIsMaximized(false);
162
+ } else {
163
+ closeClickCountRef.current += 1;
164
+ playSound(closeClickCountRef.current);
165
+ }
166
+ };
167
+
168
+ const windowContent = (
169
+ <div className={`window ${className || ""}`}>
170
+ {hasHeading ? (
171
+ <div className="title-bar">
172
+ <div className="title-bar-text">
173
+ {renderHeading(heading, link)}
174
+ </div>
175
+ <div className="title-bar-controls">
176
+ <button aria-label="Minimize"></button>
177
+ <button aria-label="Maximize" onClick={handleMaximize}></button>
178
+ <button aria-label="Close" onClick={handleClose}></button>
179
+ </div>
180
+ </div>
181
+ ) : null}
182
+ <div className="window-body">
183
+ {hasContent
184
+ ? renderContent(content, status, text)
185
+ : null}
186
+ {children}
187
+ </div>
188
+ </div>
189
+ );
190
+
191
+ return (
192
+ <>
193
+ <div
194
+ className="addon-wrapper"
195
+ style={{ visibility: isMaximized ? "hidden" : "visible" }}
196
+ >
197
+ {windowContent}
198
+ </div>
199
+ {isMaximized && (
200
+ <div
201
+ style={{
202
+ position: "fixed",
203
+ top: "50%",
204
+ left: "50%",
205
+ transform: "translate(-50%, -50%)",
206
+ zIndex: 9999,
207
+ maxWidth: "90vw",
208
+ maxHeight: "90vh",
209
+ overflow: "auto",
210
+ }}
211
+ >
212
+ {windowContent}
213
+ </div>
214
+ )}
215
+ </>
216
+ );
217
+ };
218
+
219
+ export const AddonList = (props: AddonProps) => (
220
+ <WindowPanel {...props} asList={true} />
221
+ );
222
+
223
+ export const Addon = (props: AddonProps) => <WindowPanel {...props} />;
@@ -0,0 +1,103 @@
1
+ import { useEffect, useState } from "react";
2
+
3
+ import { PageContent } from "./Page";
4
+ import { processContent } from "../windowing/utils";
5
+ import {
6
+ getRuntimeBlogPostsHost,
7
+ resolveTrustedBlogAssetUrl,
8
+ } from "../utils/blogSecurity";
9
+ import type { PageMetadata, SectionProps } from "../windowing";
10
+
11
+ type BlogState = {
12
+ sections?: SectionProps | SectionProps[];
13
+ metadata: PageMetadata;
14
+ error?: string;
15
+ };
16
+
17
+ const LOADING_SECTION: SectionProps = {
18
+ heading: "Loading...",
19
+ content: ["![Loading spinner](/spinner.svg)", "Fetching posts from raw.githubusercontent.com"],
20
+ };
21
+
22
+ function isSectionPayload(
23
+ value: unknown,
24
+ ): value is SectionProps | SectionProps[] {
25
+ if (!value || typeof value !== "object") return false;
26
+ if (Array.isArray(value)) return true;
27
+ return "heading" in value || "content" in value;
28
+ }
29
+
30
+ const BLOG_POSTS_HOST = getRuntimeBlogPostsHost(
31
+ Boolean(import.meta.env.DEV),
32
+ typeof window !== "undefined" ? window.location.origin : undefined,
33
+ );
34
+
35
+ const BLOG_POSTS_URL = resolveTrustedBlogAssetUrl("posts.json", BLOG_POSTS_HOST);
36
+
37
+ function buildBlogState(content: SectionProps | SectionProps[]): BlogState {
38
+ const { processed, metadata } = processContent(content);
39
+ return {
40
+ sections: processed,
41
+ metadata: { sections: metadata },
42
+ };
43
+ }
44
+
45
+ export default function BlogContent() {
46
+ const [blogState, setBlogState] = useState<BlogState>(() =>
47
+ buildBlogState(LOADING_SECTION),
48
+ );
49
+
50
+ useEffect(() => {
51
+ let cancelled = false;
52
+
53
+ const load = async () => {
54
+ try {
55
+ const response = await fetch(BLOG_POSTS_URL, {
56
+ method: "GET",
57
+ cache: "no-store",
58
+ headers: {
59
+ Accept: "application/json",
60
+ },
61
+ });
62
+
63
+ if (!response.ok) {
64
+ throw new Error(`HTTP ${response.status}`);
65
+ }
66
+
67
+ const payload: unknown = await response.json();
68
+ if (!isSectionPayload(payload)) {
69
+ throw new Error("Invalid posts schema");
70
+ }
71
+
72
+ if (!cancelled) {
73
+ setBlogState(buildBlogState(payload));
74
+ }
75
+ } catch (error) {
76
+ if (!cancelled) {
77
+ const message =
78
+ error instanceof Error ? error.message : "Unknown error";
79
+ setBlogState({
80
+ metadata: { sections: [] },
81
+ error: `Failed to fetch remote posts (${message}).`,
82
+ });
83
+ }
84
+ }
85
+ };
86
+
87
+ load();
88
+
89
+ return () => {
90
+ cancelled = true;
91
+ };
92
+ }, []);
93
+
94
+ return (
95
+ <>
96
+ {blogState.error ? <p className="status-bar">{blogState.error}</p> : null}
97
+ <PageContent
98
+ {...(blogState.sections ? { sections: blogState.sections } : {})}
99
+ pageMetadata={blogState.metadata}
100
+ />
101
+ </>
102
+ );
103
+ }
@@ -0,0 +1,21 @@
1
+ import { toast } from 'react-toastify';
2
+
3
+ interface CopyToClipboardButtonProps {
4
+ content: string;
5
+ onClick?: () => void;
6
+ }
7
+
8
+ export const CopyToClipboardButton = (props: CopyToClipboardButtonProps) => {
9
+ const handleClick = async () => {
10
+ await navigator.clipboard.writeText(props.content).then(() => {
11
+ toast('Content copied to clipboard!');
12
+ }).catch((error) => {
13
+ toast('Failed to copy content: ', error);
14
+ });
15
+ if (props.onClick) {
16
+ props.onClick();
17
+ }
18
+ };
19
+
20
+ return <button onClick={handleClick}>Copy to Clipboard</button>;
21
+ };
@@ -0,0 +1,151 @@
1
+ import { useEffect, useMemo } from "react";
2
+ import { generateMenuItems, type MenuItem } from "../utils/menuItems";
3
+ import pages from "../pages.json";
4
+
5
+ type Link = {
6
+ label: string;
7
+ href: string;
8
+ };
9
+
10
+ type Props = {
11
+ links?: Link[];
12
+ additionalLinks?: MenuItem[];
13
+ onNavigate?: (href: string) => void;
14
+ onMenuAction?: (href: string) => boolean;
15
+ };
16
+
17
+ const PAGE_LINKS = (pages as Array<{ path: string; menuLabel?: string }>).map(
18
+ (page) => ({
19
+ url: page.path,
20
+ ...(page.menuLabel ? { label: page.menuLabel } : {}),
21
+ }),
22
+ );
23
+
24
+ const isTypingTarget = (target: EventTarget | null) => {
25
+ if (!(target instanceof HTMLElement)) {
26
+ return false;
27
+ }
28
+
29
+ return (
30
+ target.tagName === "INPUT" ||
31
+ target.tagName === "TEXTAREA" ||
32
+ target.tagName === "SELECT" ||
33
+ target.isContentEditable
34
+ );
35
+ };
36
+
37
+ const isInternalPath = (href: string) => href.startsWith("/");
38
+
39
+ const SITEMAP_HREF = "/sitemap";
40
+
41
+ const hasAdminCookie = () =>
42
+ typeof document !== "undefined" &&
43
+ document.cookie
44
+ .split("; ")
45
+ .some((cookie) => cookie === "admin=akinevz");
46
+
47
+ const filterHiddenMenuItems = (menuItems: MenuItem[]) => {
48
+ if (hasAdminCookie()) {
49
+ return menuItems;
50
+ }
51
+
52
+ return menuItems.filter((item) => item.href !== SITEMAP_HREF);
53
+ };
54
+
55
+ export default function MenuBar({
56
+ links,
57
+ additionalLinks = [],
58
+ onNavigate,
59
+ onMenuAction,
60
+ }: Props) {
61
+ const menuItems = useMemo(() => {
62
+ if (links) {
63
+ return filterHiddenMenuItems(
64
+ links.map((link) => ({ label: link.label, href: link.href })),
65
+ );
66
+ }
67
+
68
+ return filterHiddenMenuItems(generateMenuItems(PAGE_LINKS, additionalLinks));
69
+ }, [links, additionalLinks]);
70
+
71
+ useEffect(() => {
72
+ const handleKeyDown = (event: KeyboardEvent) => {
73
+ if (isTypingTarget(event.target)) {
74
+ return;
75
+ }
76
+
77
+ if (event.ctrlKey || event.metaKey || event.altKey) {
78
+ return;
79
+ }
80
+
81
+ if (event.key.length !== 1) {
82
+ return;
83
+ }
84
+
85
+ const key = event.key.toLowerCase();
86
+ const item = menuItems.find(
87
+ (menuItem) => menuItem.label.charAt(0).toLowerCase() === key,
88
+ );
89
+
90
+ if (!item) {
91
+ return;
92
+ }
93
+
94
+ if (onMenuAction?.(item.href)) {
95
+ event.preventDefault();
96
+ return;
97
+ }
98
+
99
+ if (onNavigate && isInternalPath(item.href)) {
100
+ event.preventDefault();
101
+ onNavigate(item.href);
102
+ }
103
+ };
104
+
105
+ document.addEventListener("keydown", handleKeyDown);
106
+ return () => {
107
+ document.removeEventListener("keydown", handleKeyDown);
108
+ };
109
+ }, [menuItems, onMenuAction, onNavigate]);
110
+
111
+ return (
112
+ <div className="menu-bar">
113
+ <menu role="menubar">
114
+ {menuItems.map((item) => (
115
+ <li key={item.href} role="none">
116
+ <a
117
+ href={item.href}
118
+ role="menuitem"
119
+ data-key={item.label.charAt(0).toLowerCase()}
120
+ onClick={(event) => {
121
+ if (
122
+ event.defaultPrevented ||
123
+ event.button !== 0 ||
124
+ event.metaKey ||
125
+ event.ctrlKey ||
126
+ event.shiftKey ||
127
+ event.altKey
128
+ ) {
129
+ return;
130
+ }
131
+
132
+ if (onMenuAction?.(item.href)) {
133
+ event.preventDefault();
134
+ return;
135
+ }
136
+
137
+ if (onNavigate && isInternalPath(item.href)) {
138
+ event.preventDefault();
139
+ onNavigate(item.href);
140
+ }
141
+ }}
142
+ >
143
+ <span className="menu-underline">{item.label.charAt(0)}</span>
144
+ {item.label.slice(1)}
145
+ </a>
146
+ </li>
147
+ ))}
148
+ </menu>
149
+ </div>
150
+ );
151
+ }
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ import { MinimizedSections } from '../windowing';
3
+
4
+ export const MenuBarWithContext: React.FC = () => {
5
+ return <MinimizedSections />;
6
+ };
@@ -0,0 +1,17 @@
1
+
2
+ interface ModalProps {
3
+ message: string;
4
+ }
5
+
6
+ const Modal = ({ message }: ModalProps) => {
7
+ return (
8
+ <div id="myModal" className="modal">
9
+ <div className="modal-content">
10
+ <span className="close">&times;</span>
11
+ <p>{message}</p>
12
+ </div>
13
+ </div>
14
+ );
15
+ };
16
+
17
+ export default Modal;