diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6197a1af..abcd660d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -67,6 +67,9 @@ jobs: with: clang-format-version: '20' check-path: 'src' + + - name: Verify Page Formatting + run: cd page && npx --yes prettier . --check build-apiset-dumper: name: Build API Set Dumper diff --git a/page/.prettierignore b/page/.prettierignore new file mode 100644 index 00000000..e17b023c --- /dev/null +++ b/page/.prettierignore @@ -0,0 +1,2 @@ +# Ignore artifacts: +dist diff --git a/page/.prettierrc b/page/.prettierrc new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/page/.prettierrc @@ -0,0 +1 @@ +{} diff --git a/page/components.json b/page/components.json index 73afbdbc..13e1db0b 100644 --- a/page/components.json +++ b/page/components.json @@ -18,4 +18,4 @@ "hooks": "@/hooks" }, "iconLibrary": "lucide" -} \ No newline at end of file +} diff --git a/page/eslint.config.js b/page/eslint.config.js index 092408a9..79a552ea 100644 --- a/page/eslint.config.js +++ b/page/eslint.config.js @@ -1,28 +1,28 @@ -import js from '@eslint/js' -import globals from 'globals' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import tseslint from 'typescript-eslint' +import js from "@eslint/js"; +import globals from "globals"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import tseslint from "typescript-eslint"; export default tseslint.config( - { ignores: ['dist'] }, + { ignores: ["dist"] }, { extends: [js.configs.recommended, ...tseslint.configs.recommended], - files: ['**/*.{ts,tsx}'], + files: ["**/*.{ts,tsx}"], languageOptions: { ecmaVersion: 2020, globals: globals.browser, }, plugins: { - 'react-hooks': reactHooks, - 'react-refresh': reactRefresh, + "react-hooks": reactHooks, + "react-refresh": reactRefresh, }, rules: { ...reactHooks.configs.recommended.rules, - 'react-refresh/only-export-components': [ - 'warn', + "react-refresh/only-export-components": [ + "warn", { allowConstantExport: true }, ], }, }, -) +); diff --git a/page/index.html b/page/index.html index 0393cae0..13e01b85 100644 --- a/page/index.html +++ b/page/index.html @@ -3,22 +3,58 @@ - - + + Sogen - Windows User Space Emulator - - - - + + + + - + - - - + + +
diff --git a/page/package-lock.json b/page/package-lock.json index 2a8edaff..965ccf18 100644 --- a/page/package-lock.json +++ b/page/package-lock.json @@ -44,6 +44,7 @@ "eslint-plugin-react-hooks": "^6.0.0", "eslint-plugin-react-refresh": "^0.4.20", "globals": "^16.0.0", + "prettier": "3.5.3", "typescript": "~5.8.3", "typescript-eslint": "^8.31.0", "vite": "^6.3.1" @@ -4424,6 +4425,22 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", diff --git a/page/package.json b/page/package.json index 54a9862a..76dc86a0 100644 --- a/page/package.json +++ b/page/package.json @@ -46,6 +46,7 @@ "eslint-plugin-react-hooks": "^6.0.0", "eslint-plugin-react-refresh": "^0.4.20", "globals": "^16.0.0", + "prettier": "3.5.3", "typescript": "~5.8.3", "typescript-eslint": "^8.31.0", "vite": "^6.3.1" diff --git a/page/public/emulator-worker.js b/page/public/emulator-worker.js index ed047e63..65af92a1 100644 --- a/page/public/emulator-worker.js +++ b/page/public/emulator-worker.js @@ -1,57 +1,57 @@ -var logLines = []; -var lastFlush = new Date().getTime(); - -onmessage = async (event) => { - const data = event.data; - if (data.message == "run") { - const payload = data.data; - runEmulation(payload.filesystem, payload.file, payload.options); - } -}; - -function flushLines() { - const lines = logLines; - logLines = []; - lastFlush = new Date().getTime(); - postMessage({ message: "log", data: lines }); -} - -function logLine(text) { - logLines.push(text); - - const now = new Date().getTime(); - - if(lastFlush + 15 < now) { - flushLines(); - } -} - -function runEmulation(filesystem, file, options) { - globalThis.Module = { - arguments: [...options, "-e", "./root", file], - onRuntimeInitialized: function () { - filesystem.forEach(e => { - if (e.name.endsWith("/")) { - FS.mkdir(e.name.slice(0, -1)); - } else { - const dirs = e.name.split("/") - const file = dirs.pop(); - const buffer = new Uint8Array(e.data); - if (FS.analyzePath(e.name).exists) { - FS.unlink(e.name); - } - FS.createDataFile("/" + dirs.join('/'), file, buffer, true, true); - } - }) - }, - print: logLine, - printErr: logLine, - postRun: () => { - flushLines(); - postMessage({ message: "end", data: null }); - self.close(); - }, - }; - - importScripts('./analyzer.js?1'); -} +var logLines = []; +var lastFlush = new Date().getTime(); + +onmessage = async (event) => { + const data = event.data; + if (data.message == "run") { + const payload = data.data; + runEmulation(payload.filesystem, payload.file, payload.options); + } +}; + +function flushLines() { + const lines = logLines; + logLines = []; + lastFlush = new Date().getTime(); + postMessage({ message: "log", data: lines }); +} + +function logLine(text) { + logLines.push(text); + + const now = new Date().getTime(); + + if (lastFlush + 15 < now) { + flushLines(); + } +} + +function runEmulation(filesystem, file, options) { + globalThis.Module = { + arguments: [...options, "-e", "./root", file], + onRuntimeInitialized: function () { + filesystem.forEach((e) => { + if (e.name.endsWith("/")) { + FS.mkdir(e.name.slice(0, -1)); + } else { + const dirs = e.name.split("/"); + const file = dirs.pop(); + const buffer = new Uint8Array(e.data); + if (FS.analyzePath(e.name).exists) { + FS.unlink(e.name); + } + FS.createDataFile("/" + dirs.join("/"), file, buffer, true, true); + } + }); + }, + print: logLine, + printErr: logLine, + postRun: () => { + flushLines(); + postMessage({ message: "end", data: null }); + self.close(); + }, + }; + + importScripts("./analyzer.js?1"); +} diff --git a/page/src/App.css b/page/src/App.css index 5d4ad00b..8cab0f2d 100644 --- a/page/src/App.css +++ b/page/src/App.css @@ -1,70 +1,70 @@ -@media (pointer:fine) { - ::-webkit-scrollbar { - width: 20px; - } - - ::-webkit-scrollbar-track { - background-color: transparent; - } - - ::-webkit-scrollbar-thumb { - background-color: rgba(97, 97, 97, 0.7); - border-radius: 20px; - min-height: 50px; - border: 6px solid transparent; - background-clip: content-box; - transition: all 0.1s linear; - } - - ::-webkit-scrollbar-thumb:hover { - background-color: rgba(97, 97, 97, 0.9); - } -} - -button { - cursor: pointer; -} - -.terminal-output { - line-height: 1.5; - font-weight: 600; - font-size: 1.05em; - font-family: monospace; - height: 100%; -} - -.terminal-black { - color: #0C0C0C; -} - -.terminal-red { - color: #FF3131; -} - -.terminal-green { - color: #86C000; -} - -.terminal-yellow { - color: #FFB940; -} - -.terminal-blue { - color: #3A96DD; -} - -.terminal-cyan { - color: #00ADF7; -} - -.terminal-pink { - color: #9750DD; -} - -.terminal-white { - color: #ECECEC; -} - -.terminal-dark-gray { - color: rgb(81, 81, 81); -} +@media (pointer: fine) { + ::-webkit-scrollbar { + width: 20px; + } + + ::-webkit-scrollbar-track { + background-color: transparent; + } + + ::-webkit-scrollbar-thumb { + background-color: rgba(97, 97, 97, 0.7); + border-radius: 20px; + min-height: 50px; + border: 6px solid transparent; + background-clip: content-box; + transition: all 0.1s linear; + } + + ::-webkit-scrollbar-thumb:hover { + background-color: rgba(97, 97, 97, 0.9); + } +} + +button { + cursor: pointer; +} + +.terminal-output { + line-height: 1.5; + font-weight: 600; + font-size: 1.05em; + font-family: monospace; + height: 100%; +} + +.terminal-black { + color: #0c0c0c; +} + +.terminal-red { + color: #ff3131; +} + +.terminal-green { + color: #86c000; +} + +.terminal-yellow { + color: #ffb940; +} + +.terminal-blue { + color: #3a96dd; +} + +.terminal-cyan { + color: #00adf7; +} + +.terminal-pink { + color: #9750dd; +} + +.terminal-white { + color: #ececec; +} + +.terminal-dark-gray { + color: rgb(81, 81, 81); +} diff --git a/page/src/Header.tsx b/page/src/Header.tsx index 61475bdf..973b14e7 100644 --- a/page/src/Header.tsx +++ b/page/src/Header.tsx @@ -1,11 +1,12 @@ -import { Helmet } from 'react-helmet'; +import { Helmet } from "react-helmet"; export interface HeaderProps { title: string; description: string; } -const image = "https://repository-images.githubusercontent.com/842883987/9e56f43b-4afd-464d-85b9-d7e555751a39"; +const image = + "https://repository-images.githubusercontent.com/842883987/9e56f43b-4afd-464d-85b9-d7e555751a39"; export function Header(props: HeaderProps) { return ( diff --git a/page/src/Playground.tsx b/page/src/Playground.tsx index e479fa4f..e86061e7 100644 --- a/page/src/Playground.tsx +++ b/page/src/Playground.tsx @@ -97,7 +97,10 @@ export function Playground() { return ( <> -
+
diff --git a/page/src/components/app-sidebar.tsx b/page/src/components/app-sidebar.tsx index 08967930..de7b708c 100644 --- a/page/src/components/app-sidebar.tsx +++ b/page/src/components/app-sidebar.tsx @@ -1,53 +1,51 @@ -import * as React from "react" - -import { - Sidebar, - SidebarContent, - SidebarGroup, - SidebarGroupLabel, - SidebarHeader, - /*SidebarMenu, - SidebarGroupContent, - SidebarMenuButton, - SidebarMenuItem,*/ - SidebarRail, -} from "@/components/ui/sidebar" - -// This is sample data. -const data = { - navMain: [ - { - title: "TODO", - }, - ], -} - -export function AppSidebar({ ...props }: React.ComponentProps) { - return ( - - - Filesystem - - - {/* We create a SidebarGroup for each parent. */} - {data.navMain.map((item) => ( - - {item.title} - {/* - - {item.items.map((item) => ( - - - {item.title} - - - ))} - - */} - - ))} - - - - ) -} +import * as React from "react"; + +import { + Sidebar, + SidebarContent, + SidebarGroup, + SidebarGroupLabel, + SidebarHeader, + /*SidebarMenu, + SidebarGroupContent, + SidebarMenuButton, + SidebarMenuItem,*/ + SidebarRail, +} from "@/components/ui/sidebar"; + +// This is sample data. +const data = { + navMain: [ + { + title: "TODO", + }, + ], +}; + +export function AppSidebar({ ...props }: React.ComponentProps) { + return ( + + Filesystem + + {/* We create a SidebarGroup for each parent. */} + {data.navMain.map((item) => ( + + {item.title} + {/* + + {item.items.map((item) => ( + + + {item.title} + + + ))} + + */} + + ))} + + + + ); +} diff --git a/page/src/components/output.tsx b/page/src/components/output.tsx index 5188cd95..a9c3677d 100644 --- a/page/src/components/output.tsx +++ b/page/src/components/output.tsx @@ -1,244 +1,255 @@ -import React from 'react'; -import { FixedSizeList as List } from 'react-window'; - -interface OutputProps { } - -interface ColorState { - color: string; -} - -interface OutputState extends ColorState { - lines: LogLine[]; -} - -interface FullOutputState extends OutputState { - grouper: OutputGrouper; - height: number, - width: number, -} - -interface LogLine { - text: string; - classNames: string; -}; - -function removeSubstringFromStart(str: string, substring: string): string { - if (str.startsWith(substring)) { - return str.slice(substring.length); - } - return str; -} - -function removeSubstringFromEnd(str: string, substring: string): string { - if (str.endsWith(substring)) { - return str.slice(0, -substring.length); - } - return str; -} - -function removeSpanFromStart(str: string, color: string) { - const pattern = /^/; - const match = str.match(pattern); - - if (match) { - const terminalValue = match[1]; - const cleanedString = str.replace(pattern, ''); - return [cleanedString, terminalValue]; - } - - return [str, color]; -} - -function extractColor(line: string, colorState: ColorState) { - while (true) { - const newLine = removeSubstringFromStart(line, ""); - if (newLine == line) { - break - } - - line = newLine; - colorState.color = ''; - } - - const [nextLine, color] = removeSpanFromStart(line, colorState.color); - - const finalLine = removeSubstringFromEnd(nextLine, ""); - if (finalLine != nextLine) { - colorState.color = ''; - } else { - colorState.color = color; - } - - return [finalLine, color]; -} - -function renderLine(line: string, colorState: ColorState) { - const [newLine, color] = extractColor(line, colorState); - - return { - text: newLine, - classNames: 'whitespace-nowrap block ' + color - }; -} - -function renderLines(lines: string[], color: string): OutputState { - var state: ColorState = { - color - }; - - const resultLines = lines.map(line => renderLine(line, state)); - - return { - lines: resultLines, - color: state.color, - }; -} - -function mergeLines(previousState: OutputState, newLines: string[]): OutputState { - const result = renderLines(newLines, previousState.color); - return { lines: previousState.lines.concat(result.lines), color: result.color }; -} - -class OutputGrouper { - private lines: string[]; - private flushQueued: boolean; - handler: (lines: string[]) => void; - - constructor() { - this.lines = []; - this.flushQueued = false; - this.handler = () => { }; - } - - clear() { - this.lines = []; - this.flushQueued = false; - } - - flush() { - const lines = this.lines; - this.lines = []; - this.handler(lines); - } - - queueFlush() { - if (this.flushQueued) { - return false; - } - - this.flushQueued = true; - - requestAnimationFrame(() => { - if (!this.flushQueued) { - return; - } - - this.flushQueued = false; - this.flush(); - }); - } - - storeLines(lines: string[]) { - this.lines = this.lines.concat(lines); - this.queueFlush(); - } -} - -export class Output extends React.Component { - private outputRef: React.RefObject; - private listRef: React.RefObject; - private resizeObserver: ResizeObserver; - - constructor(props: OutputProps) { - super(props); - - this.clear = this.clear.bind(this); - this.logLine = this.logLine.bind(this); - this.logLines = this.logLines.bind(this); - this.updateDimensions = this.updateDimensions.bind(this); - - this.outputRef = React.createRef(); - this.listRef = React.createRef(); - this.resizeObserver = new ResizeObserver(this.updateDimensions); - - this.state = { - lines: [], - color: '', - grouper: new OutputGrouper(), - height: 10, - width: 10, - }; - - this.state.grouper.handler = (lines: string[]) => { - this.setState((s) => mergeLines(s, lines)); - }; - } - - componentDidMount() { - this.updateDimensions(); - - if (this.outputRef.current) { - this.resizeObserver.observe(this.outputRef.current); - } - } - - componentWillUnmount() { - this.resizeObserver.disconnect(); - } - - componentDidUpdate(_: OutputProps, prevState: FullOutputState) { - if (prevState.lines.length == this.state.lines.length || !this.listRef.current) { - return; - } - - this.listRef.current.scrollToItem(this.state.lines.length - 1); - } - - clear() { - this.state.grouper.clear(); - this.setState({ - lines: [], - color: '', - }); - } - - updateDimensions() { - if (!this.outputRef.current) { - return; - } - - this.setState({ - width: this.outputRef.current.offsetWidth, - height: this.outputRef.current.offsetHeight, - }); - } - - logLines(lines: string[]) { - this.state.grouper.storeLines(lines); - } - - logLine(line: string) { - this.logLines([line]); - } - - render() { - return ( -
- - {({ index, style }) => { - const line = this.state.lines[index]; - return ( - - {line.text} - - ) - }} - -
- ); - } -} +import React from "react"; +import { FixedSizeList as List } from "react-window"; + +interface OutputProps {} + +interface ColorState { + color: string; +} + +interface OutputState extends ColorState { + lines: LogLine[]; +} + +interface FullOutputState extends OutputState { + grouper: OutputGrouper; + height: number; + width: number; +} + +interface LogLine { + text: string; + classNames: string; +} + +function removeSubstringFromStart(str: string, substring: string): string { + if (str.startsWith(substring)) { + return str.slice(substring.length); + } + return str; +} + +function removeSubstringFromEnd(str: string, substring: string): string { + if (str.endsWith(substring)) { + return str.slice(0, -substring.length); + } + return str; +} + +function removeSpanFromStart(str: string, color: string) { + const pattern = /^/; + const match = str.match(pattern); + + if (match) { + const terminalValue = match[1]; + const cleanedString = str.replace(pattern, ""); + return [cleanedString, terminalValue]; + } + + return [str, color]; +} + +function extractColor(line: string, colorState: ColorState) { + while (true) { + const newLine = removeSubstringFromStart(line, ""); + if (newLine == line) { + break; + } + + line = newLine; + colorState.color = ""; + } + + const [nextLine, color] = removeSpanFromStart(line, colorState.color); + + const finalLine = removeSubstringFromEnd(nextLine, ""); + if (finalLine != nextLine) { + colorState.color = ""; + } else { + colorState.color = color; + } + + return [finalLine, color]; +} + +function renderLine(line: string, colorState: ColorState) { + const [newLine, color] = extractColor(line, colorState); + + return { + text: newLine, + classNames: "whitespace-nowrap block " + color, + }; +} + +function renderLines(lines: string[], color: string): OutputState { + var state: ColorState = { + color, + }; + + const resultLines = lines.map((line) => renderLine(line, state)); + + return { + lines: resultLines, + color: state.color, + }; +} + +function mergeLines( + previousState: OutputState, + newLines: string[], +): OutputState { + const result = renderLines(newLines, previousState.color); + return { + lines: previousState.lines.concat(result.lines), + color: result.color, + }; +} + +class OutputGrouper { + private lines: string[]; + private flushQueued: boolean; + handler: (lines: string[]) => void; + + constructor() { + this.lines = []; + this.flushQueued = false; + this.handler = () => {}; + } + + clear() { + this.lines = []; + this.flushQueued = false; + } + + flush() { + const lines = this.lines; + this.lines = []; + this.handler(lines); + } + + queueFlush() { + if (this.flushQueued) { + return false; + } + + this.flushQueued = true; + + requestAnimationFrame(() => { + if (!this.flushQueued) { + return; + } + + this.flushQueued = false; + this.flush(); + }); + } + + storeLines(lines: string[]) { + this.lines = this.lines.concat(lines); + this.queueFlush(); + } +} + +export class Output extends React.Component { + private outputRef: React.RefObject; + private listRef: React.RefObject; + private resizeObserver: ResizeObserver; + + constructor(props: OutputProps) { + super(props); + + this.clear = this.clear.bind(this); + this.logLine = this.logLine.bind(this); + this.logLines = this.logLines.bind(this); + this.updateDimensions = this.updateDimensions.bind(this); + + this.outputRef = React.createRef(); + this.listRef = React.createRef(); + this.resizeObserver = new ResizeObserver(this.updateDimensions); + + this.state = { + lines: [], + color: "", + grouper: new OutputGrouper(), + height: 10, + width: 10, + }; + + this.state.grouper.handler = (lines: string[]) => { + this.setState((s) => mergeLines(s, lines)); + }; + } + + componentDidMount() { + this.updateDimensions(); + + if (this.outputRef.current) { + this.resizeObserver.observe(this.outputRef.current); + } + } + + componentWillUnmount() { + this.resizeObserver.disconnect(); + } + + componentDidUpdate(_: OutputProps, prevState: FullOutputState) { + if ( + prevState.lines.length == this.state.lines.length || + !this.listRef.current + ) { + return; + } + + this.listRef.current.scrollToItem(this.state.lines.length - 1); + } + + clear() { + this.state.grouper.clear(); + this.setState({ + lines: [], + color: "", + }); + } + + updateDimensions() { + if (!this.outputRef.current) { + return; + } + + this.setState({ + width: this.outputRef.current.offsetWidth, + height: this.outputRef.current.offsetHeight, + }); + } + + logLines(lines: string[]) { + this.state.grouper.storeLines(lines); + } + + logLine(line: string) { + this.logLines([line]); + } + + render() { + return ( +
+ + {({ index, style }) => { + const line = this.state.lines[index]; + return ( + + {line.text} + + ); + }} + +
+ ); + } +} diff --git a/page/src/components/settings-menu.tsx b/page/src/components/settings-menu.tsx index c42476ad..aa80d528 100644 --- a/page/src/components/settings-menu.tsx +++ b/page/src/components/settings-menu.tsx @@ -1,72 +1,87 @@ -import React from "react"; -import { Checkbox } from "./ui/checkbox"; -import { Label } from "./ui/label"; - -import { Settings } from "@/settings"; - -interface SettingsMenuProps { - settings: Settings; - onChange: (settings: Settings) => void; - }; - -export class SettingsMenu extends React.Component { - - constructor(props: SettingsMenuProps) { - super(props); - this.getSettings = this.getSettings.bind(this); - this.state = props.settings; - } - - getSettings() { - return this.state; - } - - updateSettings(settings: Settings) { - this.setState(() => settings); - } - - componentDidUpdate() { - this.props.onChange(this.state); - } - - render() { - return ( -
-
-

Settings

-

- Set the settings for the emulation. -

-
- -
- { - this.setState({ verbose: checked }); - }} /> - -
- -
- { - this.setState({ concise: checked }); - }} /> - -
- -
- { - this.setState({ silent: checked }); - }} /> - -
- -
- { - this.setState({ bufferStdout: checked }); - }} /> - -
-
- ); - } -}; +import React from "react"; +import { Checkbox } from "./ui/checkbox"; +import { Label } from "./ui/label"; + +import { Settings } from "@/settings"; + +interface SettingsMenuProps { + settings: Settings; + onChange: (settings: Settings) => void; +} + +export class SettingsMenu extends React.Component { + constructor(props: SettingsMenuProps) { + super(props); + this.getSettings = this.getSettings.bind(this); + this.state = props.settings; + } + + getSettings() { + return this.state; + } + + updateSettings(settings: Settings) { + this.setState(() => settings); + } + + componentDidUpdate() { + this.props.onChange(this.state); + } + + render() { + return ( +
+
+

Settings

+

+ Set the settings for the emulation. +

+
+ +
+ { + this.setState({ verbose: checked }); + }} + /> + +
+ +
+ { + this.setState({ concise: checked }); + }} + /> + +
+ +
+ { + this.setState({ silent: checked }); + }} + /> + +
+ +
+ { + this.setState({ bufferStdout: checked }); + }} + /> + +
+
+ ); + } +} diff --git a/page/src/components/status-indicator.tsx b/page/src/components/status-indicator.tsx index c12b497d..4bd469b5 100644 --- a/page/src/components/status-indicator.tsx +++ b/page/src/components/status-indicator.tsx @@ -1,21 +1,28 @@ -import { Badge } from '@/components/ui/badge' -import { CircleFill } from 'react-bootstrap-icons'; - -export interface StatusIndicatorProps { - running: boolean; -}; - -export function StatusIndicator(props: StatusIndicatorProps) { - - const getText = () => { - return props.running ? " Running" : " Stopped"; - }; - - const getColor = () => { - return props.running ? "bg-lime-600" : "bg-orange-600"; - } - - return ( - {getText()} - ); -} \ No newline at end of file +import { Badge } from "@/components/ui/badge"; +import { CircleFill } from "react-bootstrap-icons"; + +export interface StatusIndicatorProps { + running: boolean; +} + +export function StatusIndicator(props: StatusIndicatorProps) { + const getText = () => { + return props.running ? " Running" : " Stopped"; + }; + + const getColor = () => { + return props.running ? "bg-lime-600" : "bg-orange-600"; + }; + + return ( + + + {getText()} + + ); +} diff --git a/page/src/components/theme-provider.tsx b/page/src/components/theme-provider.tsx index 9d0f2dbc..e18440d7 100644 --- a/page/src/components/theme-provider.tsx +++ b/page/src/components/theme-provider.tsx @@ -1,73 +1,73 @@ -import { createContext, useContext, useEffect, useState } from "react" - -type Theme = "dark" | "light" | "system" - -type ThemeProviderProps = { - children: React.ReactNode - defaultTheme?: Theme - storageKey?: string -} - -type ThemeProviderState = { - theme: Theme - setTheme: (theme: Theme) => void -} - -const initialState: ThemeProviderState = { - theme: "system", - setTheme: () => null, -} - -const ThemeProviderContext = createContext(initialState) - -export function ThemeProvider({ - children, - defaultTheme = "system", - storageKey = "vite-ui-theme", - ...props -}: ThemeProviderProps) { - const [theme, setTheme] = useState( - () => (localStorage.getItem(storageKey) as Theme) || defaultTheme - ) - - useEffect(() => { - const root = window.document.documentElement - - root.classList.remove("light", "dark") - - if (theme === "system") { - const systemTheme = window.matchMedia("(prefers-color-scheme: dark)") - .matches - ? "dark" - : "light" - - root.classList.add(systemTheme) - return - } - - root.classList.add(theme) - }, [theme]) - - const value = { - theme, - setTheme: (theme: Theme) => { - localStorage.setItem(storageKey, theme) - setTheme(theme) - }, - } - - return ( - - {children} - - ) -} - -export const useTheme = () => { - const context = useContext(ThemeProviderContext) - - if (context === undefined) - throw new Error("useTheme must be used within a ThemeProvider") - - return context -} +import { createContext, useContext, useEffect, useState } from "react"; + +type Theme = "dark" | "light" | "system"; + +type ThemeProviderProps = { + children: React.ReactNode; + defaultTheme?: Theme; + storageKey?: string; +}; + +type ThemeProviderState = { + theme: Theme; + setTheme: (theme: Theme) => void; +}; + +const initialState: ThemeProviderState = { + theme: "system", + setTheme: () => null, +}; + +const ThemeProviderContext = createContext(initialState); + +export function ThemeProvider({ + children, + defaultTheme = "system", + storageKey = "vite-ui-theme", + ...props +}: ThemeProviderProps) { + const [theme, setTheme] = useState( + () => (localStorage.getItem(storageKey) as Theme) || defaultTheme, + ); + + useEffect(() => { + const root = window.document.documentElement; + + root.classList.remove("light", "dark"); + + if (theme === "system") { + const systemTheme = window.matchMedia("(prefers-color-scheme: dark)") + .matches + ? "dark" + : "light"; + + root.classList.add(systemTheme); + return; + } + + root.classList.add(theme); + }, [theme]); + + const value = { + theme, + setTheme: (theme: Theme) => { + localStorage.setItem(storageKey, theme); + setTheme(theme); + }, + }; + + return ( + + {children} + + ); +} + +export const useTheme = () => { + const context = useContext(ThemeProviderContext); + + if (context === undefined) + throw new Error("useTheme must be used within a ThemeProvider"); + + return context; +}; diff --git a/page/src/components/ui/badge.tsx b/page/src/components/ui/badge.tsx index 02054139..46f988c2 100644 --- a/page/src/components/ui/badge.tsx +++ b/page/src/components/ui/badge.tsx @@ -1,8 +1,8 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { cva, type VariantProps } from "class-variance-authority" +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const badgeVariants = cva( "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", @@ -22,8 +22,8 @@ const badgeVariants = cva( defaultVariants: { variant: "default", }, - } -) + }, +); function Badge({ className, @@ -32,7 +32,7 @@ function Badge({ ...props }: React.ComponentProps<"span"> & VariantProps & { asChild?: boolean }) { - const Comp = asChild ? Slot : "span" + const Comp = asChild ? Slot : "span"; return ( - ) + ); } -export { Badge, badgeVariants } +export { Badge, badgeVariants }; diff --git a/page/src/components/ui/breadcrumb.tsx b/page/src/components/ui/breadcrumb.tsx index eb88f321..f63ae19a 100644 --- a/page/src/components/ui/breadcrumb.tsx +++ b/page/src/components/ui/breadcrumb.tsx @@ -1,11 +1,11 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { ChevronRight, MoreHorizontal } from "lucide-react" +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { ChevronRight, MoreHorizontal } from "lucide-react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; function Breadcrumb({ ...props }: React.ComponentProps<"nav">) { - return