@owdproject/react-theme-win95 0.0.0-stage → 4.0.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,81 @@
1
- # Temporary Holding Version
1
+ # @owdproject/react-theme-win95
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Windows 95 theme for **Open Web Desktop**, built with **React** and
4
+ [React95](https://react95.io).
5
+
6
+ - **Engine**: [`@owdproject/core`](https://github.com/owdproject/core) — headless windows/apps/workspaces.
7
+ - **Bridges**: [`@owdproject/react`](https://github.com/owdproject/core) — `useSyncExternalStore` hooks.
8
+ - **UI**: [`@owdproject/react-kit-react95`](../owd-kit-react95) — [React95](https://react95.io) `@react95/core` integration.
9
+
10
+ This is a **minimal vertical slice**: a working desktop shell (background, draggable/resizable
11
+ React95 windows, task bar, start menu) plus two demo apps. It proves the whole React chain works
12
+ end-to-end on top of the OWD engine.
13
+
14
+ ## Run it
15
+
16
+ ```bash
17
+ # from the repo root (client/)
18
+ pnpm install
19
+ pnpm --filter @owdproject/react-theme-win95 dev
20
+ # open http://localhost:5173
21
+ ```
22
+
23
+ ## Use it
24
+
25
+ ```tsx
26
+ import { createDesktop } from '@owdproject/core'
27
+ import { DesktopShell } from '@owdproject/react-theme-win95'
28
+ import '@owdproject/react-kit-react95/styles.css'
29
+ import '@owdproject/react-theme-win95/styles.css'
30
+
31
+ const desktop = createDesktop()
32
+
33
+ export default function App() {
34
+ return <DesktopShell desktop={desktop} watermark="React win95" />
35
+ }
36
+ ```
37
+
38
+ ### Custom apps
39
+
40
+ ```tsx
41
+ import type { DesktopApp } from '@owdproject/react-theme-win95'
42
+ import { Notepad } from '@react95/icons'
43
+
44
+ const apps: DesktopApp[] = [
45
+ {
46
+ id: 'hello',
47
+ title: 'Hello',
48
+ icon: <Notepad variant="16x16_4" />,
49
+ defaultSize: { width: 420, height: 300 },
50
+ component: ({ window, desktop }) => (
51
+ <div style={{ padding: 8 }}>
52
+ <p>Window {window.id}</p>
53
+ <button onClick={() => desktop.windows.closeWindow(window.id)}>Close</button>
54
+ </div>
55
+ ),
56
+ },
57
+ ]
58
+ ```
59
+
60
+ ## Architecture
61
+
62
+ | File | Role |
63
+ | --- | --- |
64
+ | `src/DesktopShell.tsx` | Root shell: workarea, background, windows, start menu, task bar. |
65
+ | `src/components/DesktopWindow.tsx` | One window. Reads state from `useWindowInstance`, moves/resizes via `useDesktopDrag` / `useDesktopResize`. Commits geometry back to the core on release. |
66
+ | `src/components/SystemBar.tsx` | Task bar (React95 `Frame` + `Button`), driven by `useWindowManager`. |
67
+ | `src/components/StartMenuPanel.tsx` | Start menu (React95 `List`). |
68
+ | `src/apps/*` | Demo apps (`About`, `Notepad`). |
69
+ | `src/theme.css` | Layout + app styles (visual primitives come from the kit). |
70
+ | `playground/` | Vite + React playground app. |
71
+
72
+ `@owdproject/core` is the single source of truth for window geometry and focus; React95 only
73
+ renders the chrome, and `@owdproject/react` bridges the immutable core snapshots to React.
74
+
75
+ > ℹ️ The playground Vite config includes a `build.rollupOptions.treeshake.moduleSideEffects`
76
+ > rule for `@react95/*` (they ship no `sideEffects: false`), otherwise all 1000+ React95 icons
77
+ > get bundled. Copy it into your own app's Vite config.
78
+
79
+ ## License
80
+
81
+ MIT
package/package.json CHANGED
@@ -1,6 +1,57 @@
1
1
  {
2
2
  "name": "@owdproject/react-theme-win95",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "4.0.0-alpha.3",
4
+ "description": "Windows 95 theme for Open Web Desktop built with React, @owdproject/core and @owdproject/react.",
5
+ "license": "MIT",
6
+ "author": {
7
+ "name": "Diego Villa",
8
+ "url": "https://github.com/dxlliv"
9
+ },
10
+ "type": "module",
11
+ "main": "./src/index.ts",
12
+ "module": "./src/index.ts",
13
+ "types": "./src/index.ts",
14
+ "exports": {
15
+ ".": {
16
+ "types": "./src/index.ts",
17
+ "import": "./src/index.ts",
18
+ "default": "./src/index.ts"
19
+ },
20
+ "./styles.css": "./src/theme.css",
21
+ "./package.json": "./package.json"
22
+ },
23
+ "files": [
24
+ "src",
25
+ "playground"
26
+ ],
27
+ "scripts": {
28
+ "dev": "vite --config playground/vite.config.ts",
29
+ "build": "vite build --config playground/vite.config.ts",
30
+ "preview": "vite preview --config playground/vite.config.ts",
31
+ "typecheck": "tsc --noEmit -p tsconfig.json"
32
+ },
33
+ "dependencies": {
34
+ "@owdproject/core": "^4.0.0-alpha.3",
35
+ "@owdproject/react-kit-react95": "^4.0.0-alpha.3",
36
+ "@owdproject/react": "^4.0.0-alpha.3",
37
+ "@react95/icons": "^2.8.0"
38
+ },
39
+ "peerDependencies": {
40
+ "react": ">=18.0.0",
41
+ "react-dom": ">=18.0.0"
42
+ },
43
+ "devDependencies": {
44
+ "@types/react": "^19.0.0",
45
+ "@types/react-dom": "^19.0.0",
46
+ "@vitejs/plugin-react": "^4.3.4",
47
+ "react": "^19.0.0",
48
+ "react-dom": "^19.0.0",
49
+ "typescript": "~6.0.3",
50
+ "vite": "^6.0.0"
51
+ },
52
+ "publishConfig": {
53
+ "access": "public",
54
+ "tag": "alpha",
55
+ "registry": "https://registry.npmjs.org/"
56
+ }
57
+ }
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Open Web Desktop — React win95 theme</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,30 @@
1
+ import { useEffect, useRef } from 'react'
2
+ import { createDesktop, type Desktop } from '@owdproject/core'
3
+ import { DesktopShell } from '../../src'
4
+
5
+ export function App() {
6
+ const desktopRef = useRef<Desktop | null>(null)
7
+ if (!desktopRef.current) {
8
+ desktopRef.current = createDesktop({
9
+ workspaces: ['workspace-1'],
10
+ workarea: { x: 0, y: 0, width: 0, height: 0 },
11
+ })
12
+ }
13
+ const desktop = desktopRef.current
14
+
15
+ // Open a welcome window on first mount.
16
+ useEffect(() => {
17
+ desktop.windows.createWindow({
18
+ id: 'about-welcome',
19
+ model: 'about',
20
+ applicationId: 'about',
21
+ config: {
22
+ title: 'About Open Web Desktop',
23
+ size: { width: 380, height: 240 },
24
+ position: { x: 72, y: 56 },
25
+ },
26
+ })
27
+ }, [desktop])
28
+
29
+ return <DesktopShell desktop={desktop} watermark="React win95" />
30
+ }
@@ -0,0 +1,14 @@
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import '@owdproject/react-kit-react95/styles.css'
4
+ import '../../src/theme.css'
5
+ import { App } from './App'
6
+
7
+ const container = document.getElementById('root')
8
+ if (!container) throw new Error('#root not found')
9
+
10
+ createRoot(container).render(
11
+ <StrictMode>
12
+ <App />
13
+ </StrictMode>,
14
+ )
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "jsx": "react-jsx",
8
+ "strict": true,
9
+ "skipLibCheck": true,
10
+ "esModuleInterop": true,
11
+ "types": ["vite/client"],
12
+ "noEmit": true
13
+ },
14
+ "include": ["src/**/*", "vite.config.ts"]
15
+ }
@@ -0,0 +1,43 @@
1
+ import { fileURLToPath, URL } from 'node:url'
2
+ import { defineConfig } from 'vite'
3
+ import react from '@vitejs/plugin-react'
4
+
5
+ const here = fileURLToPath(new URL('.', import.meta.url))
6
+ // pnpm workspace root (client/)
7
+ const workspaceRoot = fileURLToPath(new URL('../../..', import.meta.url))
8
+
9
+ export default defineConfig({
10
+ root: here,
11
+ plugins: [react()],
12
+ build: {
13
+ outDir: '../dist',
14
+ emptyOutDir: true,
15
+ rollupOptions: {
16
+ treeshake: {
17
+ // @react95/* ships no `sideEffects: false`, so Rollup otherwise keeps
18
+ // every icon/component module (and its CSS). Prune pure-JS modules but
19
+ // keep `.css`/`.css.mjs` so component styles still load.
20
+ moduleSideEffects(id) {
21
+ if (id.includes('@react95/')) return id.includes('.css')
22
+ return true
23
+ },
24
+ },
25
+ },
26
+ },
27
+ server: {
28
+ port: 5173,
29
+ fs: {
30
+ // Allow serving sources of the linked workspace packages.
31
+ allow: [workspaceRoot],
32
+ },
33
+ },
34
+ optimizeDeps: {
35
+ // Workspace packages are consumed as raw TypeScript sources.
36
+ exclude: [
37
+ '@owdproject/core',
38
+ '@owdproject/react',
39
+ '@owdproject/react-kit-react95',
40
+ '@owdproject/react-theme-win95',
41
+ ],
42
+ },
43
+ })
@@ -0,0 +1,92 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react'
2
+ import type { Desktop } from '@owdproject/core'
3
+ import { useWindowManager } from '@owdproject/react'
4
+ import { React95Provider } from '@owdproject/react-kit-react95'
5
+ import { DesktopRuntimeContext } from './desktop-context'
6
+ import { DesktopWindow } from './components/DesktopWindow'
7
+ import { SystemBar } from './components/SystemBar'
8
+ import { StartMenuPanel } from './components/StartMenuPanel'
9
+ import { builtinApps } from './apps'
10
+ import type { DesktopApp } from './types'
11
+
12
+ const SYSTEMBAR_HEIGHT = 28
13
+
14
+ export interface DesktopShellProps {
15
+ /** A `@owdproject/core` Desktop instance. */
16
+ desktop: Desktop
17
+ /** Applications exposed in the start menu / task bar. */
18
+ apps?: DesktopApp[]
19
+ /** Centered desktop watermark text. */
20
+ watermark?: ReactNode
21
+ }
22
+
23
+ /**
24
+ * Root Windows 95 desktop shell for React.
25
+ * Composes `@owdproject/react-kit-react95` chrome with `@owdproject/react` hooks
26
+ * and the headless `@owdproject/core` engine.
27
+ */
28
+ export function DesktopShell({ desktop, apps = builtinApps, watermark = 'Open Web Desktop' }: DesktopShellProps) {
29
+ const rootRef = useRef<HTMLDivElement>(null)
30
+ const [startOpen, setStartOpen] = useState(false)
31
+ const idRef = useRef(0)
32
+ const { windows } = useWindowManager(desktop.windows)
33
+
34
+ const getWorkarea = useCallback(() => {
35
+ const el = rootRef.current
36
+ const width = el?.clientWidth ?? (typeof window !== 'undefined' ? window.innerWidth : 1024)
37
+ const height = (el?.clientHeight ?? (typeof window !== 'undefined' ? window.innerHeight : 768)) - SYSTEMBAR_HEIGHT
38
+ return { x: 0, y: 0, width, height }
39
+ }, [])
40
+
41
+ useEffect(() => {
42
+ const sync = () => desktop.workspaces.setWorkarea(getWorkarea())
43
+ sync()
44
+ window.addEventListener('resize', sync)
45
+ return () => window.removeEventListener('resize', sync)
46
+ }, [desktop, getWorkarea])
47
+
48
+ const runtime = useMemo(() => ({ desktop, getWorkarea }), [desktop, getWorkarea])
49
+
50
+ const launch = (app: DesktopApp) => {
51
+ idRef.current += 1
52
+ const count = windows.length
53
+ const size = app.defaultSize ?? { width: 480, height: 340 }
54
+ desktop.windows.createWindow({
55
+ id: `${app.id}-${idRef.current}`,
56
+ model: app.id,
57
+ applicationId: app.id,
58
+ config: {
59
+ title: app.title,
60
+ size,
61
+ position: { x: 48 + (count % 7) * 26, y: 40 + (count % 7) * 26 },
62
+ },
63
+ })
64
+ setStartOpen(false)
65
+ }
66
+
67
+ const activeWindows = windows.filter((win) => win.state.active !== false)
68
+ const appById = new Map(apps.map((app) => [app.id, app]))
69
+
70
+ return (
71
+ <DesktopRuntimeContext.Provider value={runtime}>
72
+ <React95Provider>
73
+ <div ref={rootRef} className="owd-shell" onPointerDown={() => setStartOpen(false)}>
74
+ <div className="owd-shell__desktop r95-owd-desktop">
75
+ {watermark ? <div className="owd-shell__watermark">{watermark}</div> : null}
76
+ {activeWindows.map((win) => (
77
+ <DesktopWindow key={win.id} window={win} app={appById.get(win.model)} />
78
+ ))}
79
+ </div>
80
+
81
+ {startOpen ? (
82
+ <div className="owd-shell__startmenu" onPointerDown={(event) => event.stopPropagation()}>
83
+ <StartMenuPanel apps={apps} onLaunch={launch} />
84
+ </div>
85
+ ) : null}
86
+
87
+ <SystemBar apps={apps} startActive={startOpen} onStartClick={() => setStartOpen((open) => !open)} />
88
+ </div>
89
+ </React95Provider>
90
+ </DesktopRuntimeContext.Provider>
91
+ )
92
+ }
@@ -0,0 +1,27 @@
1
+ import { Computer } from '@react95/icons'
2
+ import { Button } from '@owdproject/react-kit-react95'
3
+ import type { DesktopAppRenderProps } from '../types'
4
+
5
+ export function AboutApp({ window, desktop }: DesktopAppRenderProps) {
6
+ return (
7
+ <div className="owd-about">
8
+ <div className="owd-about__banner">
9
+ <Computer variant="32x32_4" />
10
+ <div>
11
+ <strong>Open Web Desktop</strong>
12
+ <div className="owd-about__small">React theme — win95 (React95)</div>
13
+ </div>
14
+ </div>
15
+ <hr className="owd-about__divider" />
16
+ <p className="owd-about__text">
17
+ Running on <code>@owdproject/core</code> + <code>@owdproject/react</code> +{' '}
18
+ <code>@react95/core</code>
19
+ <br />
20
+ Window <code>{window.id}</code> · model <code>{window.model}</code>
21
+ </p>
22
+ <div className="owd-about__actions">
23
+ <Button onClick={() => desktop.windows.closeWindow(window.id)}>OK</Button>
24
+ </div>
25
+ </div>
26
+ )
27
+ }
@@ -0,0 +1,24 @@
1
+ import { useState } from 'react'
2
+ import type { DesktopAppRenderProps } from '../types'
3
+
4
+ export function NotepadApp(_props: DesktopAppRenderProps) {
5
+ const [text, setText] = useState(
6
+ 'Hello from a React window.\r\n\r\nDrag the title bar to move me, or grab an edge to resize.',
7
+ )
8
+ return (
9
+ <div className="owd-notepad">
10
+ <div className="owd-notepad__menu">
11
+ <span>File</span>
12
+ <span>Edit</span>
13
+ <span>Search</span>
14
+ <span>Help</span>
15
+ </div>
16
+ <textarea
17
+ className="owd-notepad__area"
18
+ value={text}
19
+ spellCheck={false}
20
+ onChange={(event) => setText(event.target.value)}
21
+ />
22
+ </div>
23
+ )
24
+ }
@@ -0,0 +1,24 @@
1
+ import type { DesktopApp } from '../types'
2
+ import { InfoBubble, Notepad } from '@react95/icons'
3
+ import { AboutApp } from './About'
4
+ import { NotepadApp } from './Notepad'
5
+
6
+ /** Built-in apps shipped with the win95 React theme. */
7
+ export const builtinApps: DesktopApp[] = [
8
+ {
9
+ id: 'about',
10
+ title: 'About Open Web Desktop',
11
+ icon: <InfoBubble variant="32x32_4" />,
12
+ defaultSize: { width: 380, height: 240 },
13
+ component: AboutApp,
14
+ },
15
+ {
16
+ id: 'notepad',
17
+ title: 'Untitled - Notepad',
18
+ icon: <Notepad variant="16x16_4" />,
19
+ defaultSize: { width: 480, height: 340 },
20
+ component: NotepadApp,
21
+ },
22
+ ]
23
+
24
+ export { AboutApp, NotepadApp }
@@ -0,0 +1,133 @@
1
+ import { useReducer, useRef, type CSSProperties } from 'react'
2
+ import type { ResizeDirection, WindowBounds, WindowInstance, WindowState } from '@owdproject/core'
3
+ import { useDesktopDrag, useDesktopResize, useWindowInstance } from '@owdproject/react'
4
+ import { OwdWindow } from '@owdproject/react-kit-react95'
5
+ import { useDesktopRuntime } from '../desktop-context'
6
+ import type { DesktopApp } from '../types'
7
+
8
+ const FALLBACK_SIZE = { width: 480, height: 340 }
9
+
10
+ function readBounds(state: WindowState, fallback: { width: number; height: number }): WindowBounds {
11
+ const pos = state.position ?? {}
12
+ const size = state.size ?? {}
13
+ return {
14
+ x: pos.x ?? 0,
15
+ y: pos.y ?? 0,
16
+ width: typeof size.width === 'number' ? size.width : fallback.width,
17
+ height: typeof size.height === 'number' ? size.height : fallback.height,
18
+ }
19
+ }
20
+
21
+ export interface DesktopWindowProps {
22
+ window: WindowInstance
23
+ app?: DesktopApp
24
+ }
25
+
26
+ /**
27
+ * A single desktop window: reads live state from `@owdproject/react`'s
28
+ * `useWindowInstance`, drives move/resize through `@owdproject/core`'s pointer
29
+ * sessions (`useDesktopDrag` / `useDesktopResize`) and renders React95 window
30
+ * chrome via the kit's `OwdWindow`.
31
+ *
32
+ * While dragging, geometry is kept in local state for smoothness and committed
33
+ * back to the core engine on release.
34
+ */
35
+ export function DesktopWindow({ window: instance, app }: DesktopWindowProps) {
36
+ const { desktop, getWorkarea } = useDesktopRuntime()
37
+ const { state } = useWindowInstance(instance)
38
+ const fallback = app?.defaultSize ?? FALLBACK_SIZE
39
+
40
+ const liveBounds = useRef<WindowBounds | null>(null)
41
+ const [, forceRender] = useReducer((n: number) => n + 1, 0)
42
+
43
+ const commit = (bounds: WindowBounds) => {
44
+ liveBounds.current = null
45
+ instance.setPosition({ x: bounds.x, y: bounds.y })
46
+ instance.setSize({ width: bounds.width, height: bounds.height })
47
+ forceRender()
48
+ }
49
+
50
+ const { startDrag } = useDesktopDrag({
51
+ getBounds: () => readBounds(instance.getSnapshot(), fallback),
52
+ getWorkarea,
53
+ onMove: (bounds) => {
54
+ liveBounds.current = bounds
55
+ forceRender()
56
+ },
57
+ onEnd: commit,
58
+ onCancel: () => {
59
+ liveBounds.current = null
60
+ forceRender()
61
+ },
62
+ })
63
+
64
+ const { startResize } = useDesktopResize({
65
+ getBounds: () => readBounds(instance.getSnapshot(), fallback),
66
+ minWidth: 180,
67
+ minHeight: 120,
68
+ onMove: (bounds) => {
69
+ liveBounds.current = bounds
70
+ forceRender()
71
+ },
72
+ onEnd: commit,
73
+ onCancel: () => {
74
+ liveBounds.current = null
75
+ forceRender()
76
+ },
77
+ })
78
+
79
+ const bounds = liveBounds.current ?? readBounds(state, fallback)
80
+ const maximized = state.maximized === true
81
+
82
+ const style: CSSProperties = {
83
+ left: bounds.x,
84
+ top: bounds.y,
85
+ width: bounds.width,
86
+ height: bounds.height,
87
+ zIndex: state.position?.z ?? 0,
88
+ }
89
+
90
+ const focus = () => desktop.windows.focusWindow(instance.id)
91
+ const AppComponent = app?.component
92
+
93
+ return (
94
+ <OwdWindow
95
+ title={app?.title ?? instance.config.title ?? instance.id}
96
+ icon={app?.icon}
97
+ active={state.focused}
98
+ maximized={maximized}
99
+ resizable={state.resizable !== false}
100
+ minimizable={state.minimizable !== false}
101
+ maximizable={state.maximizable !== false}
102
+ closable={state.destroyable !== false}
103
+ style={style}
104
+ onPointerDownCapture={focus}
105
+ onTitlePointerDown={(event) => {
106
+ if (event.button !== 0) return
107
+ focus()
108
+ startDrag(event.nativeEvent)
109
+ }}
110
+ onTitleDoubleClick={() => {
111
+ if (state.maximizable === false) return
112
+ instance.setLayout(maximized ? 'normal' : 'maximize', getWorkarea())
113
+ }}
114
+ onResizePointerDown={(event, direction) => {
115
+ if (event.button !== 0) return
116
+ focus()
117
+ startResize(event.nativeEvent, direction as ResizeDirection)
118
+ }}
119
+ onClose={() => desktop.windows.closeWindow(instance.id)}
120
+ onMinimize={() => instance.minimize()}
121
+ onToggleMaximize={() => {
122
+ if (state.maximizable === false) return
123
+ instance.setLayout(maximized ? 'normal' : 'maximize', getWorkarea())
124
+ }}
125
+ >
126
+ {AppComponent ? (
127
+ <AppComponent window={instance} desktop={desktop} />
128
+ ) : (
129
+ <div className="owd-window__empty">No component registered for “{instance.model}”.</div>
130
+ )}
131
+ </OwdWindow>
132
+ )
133
+ }
@@ -0,0 +1,32 @@
1
+ import { PowerOff } from '@react95/icons'
2
+ import { List } from '@owdproject/react-kit-react95'
3
+ import type { DesktopApp } from '../types'
4
+
5
+ export interface StartMenuPanelProps {
6
+ apps: DesktopApp[]
7
+ onLaunch: (app: DesktopApp) => void
8
+ }
9
+
10
+ /** Start menu built with React95 `List` + `List.Item`. */
11
+ export function StartMenuPanel({ apps, onLaunch }: StartMenuPanelProps) {
12
+ return (
13
+ <div className="owd-startmenu">
14
+ <div className="owd-startmenu__rail">
15
+ <span>
16
+ Open Web <strong>Desktop</strong>
17
+ </span>
18
+ </div>
19
+ <List className="owd-startmenu__list">
20
+ {apps.map((app) => (
21
+ <List.Item key={app.id} icon={app.icon} onClick={() => onLaunch(app)}>
22
+ {app.title}
23
+ </List.Item>
24
+ ))}
25
+ <List.Divider />
26
+ <List.Item icon={<PowerOff variant="16x16_4" />} disabled>
27
+ Shut Down…
28
+ </List.Item>
29
+ </List>
30
+ </div>
31
+ )
32
+ }
@@ -0,0 +1,73 @@
1
+ import { useEffect, useState } from 'react'
2
+ import { Logo } from '@react95/icons'
3
+ import { Button, Frame } from '@owdproject/react-kit-react95'
4
+ import { useWindowManager } from '@owdproject/react'
5
+ import { useDesktopRuntime } from '../desktop-context'
6
+ import type { DesktopApp } from '../types'
7
+
8
+ function useClock() {
9
+ const [now, setNow] = useState(() => new Date())
10
+ useEffect(() => {
11
+ const id = setInterval(() => setNow(new Date()), 10_000)
12
+ return () => clearInterval(id)
13
+ }, [])
14
+ return now
15
+ }
16
+
17
+ export interface SystemBarProps {
18
+ apps: DesktopApp[]
19
+ onStartClick: () => void
20
+ startActive: boolean
21
+ }
22
+
23
+ /**
24
+ * Bottom system bar built from React95 `Frame` + `Button` primitives.
25
+ * Unlike React95's own `TaskBar`, it is driven by the OWD window manager
26
+ * (`useWindowManager`), so it lists real `@owdproject/core` windows.
27
+ */
28
+ export function SystemBar({ apps, onStartClick, startActive }: SystemBarProps) {
29
+ const { desktop } = useDesktopRuntime()
30
+ const { windows, focused } = useWindowManager(desktop.windows)
31
+ const now = useClock()
32
+
33
+ const appById = new Map(apps.map((app) => [app.id, app]))
34
+ const clock = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
35
+
36
+ return (
37
+ <Frame className="owd-taskbar">
38
+ <Button
39
+ className={`owd-taskbar__start${startActive ? ' is-active' : ''}`}
40
+ onClick={onStartClick}
41
+ aria-pressed={startActive}
42
+ >
43
+ <Logo variant="16x16_4" />
44
+ <span className="owd-taskbar__start-label">Start</span>
45
+ </Button>
46
+
47
+ <Frame className="owd-taskbar__tasks">
48
+ {windows.map((win) => {
49
+ const app = appById.get(win.model)
50
+ const title = app?.title ?? win.config.title ?? win.id
51
+ const isActive = focused?.id === win.id
52
+ return (
53
+ <Button
54
+ key={win.id}
55
+ className={`owd-task${isActive ? ' is-active' : ''}`}
56
+ title={title}
57
+ onClick={() => {
58
+ if (win.state.active === false) win.unminimize()
59
+ desktop.windows.focusWindow(win.id)
60
+ }}
61
+ >
62
+ <span className="owd-task__label">{title}</span>
63
+ </Button>
64
+ )
65
+ })}
66
+ </Frame>
67
+
68
+ <Frame className="owd-taskbar__tray">
69
+ <span className="owd-taskbar__clock">{clock}</span>
70
+ </Frame>
71
+ </Frame>
72
+ )
73
+ }
@@ -0,0 +1,18 @@
1
+ import { createContext, useContext } from 'react'
2
+ import type { Desktop, DesktopWorkareaRect } from '@owdproject/core'
3
+
4
+ export interface DesktopRuntime {
5
+ desktop: Desktop
6
+ /** Current usable desktop area (excluding the system bar), in shell coordinates. */
7
+ getWorkarea: () => DesktopWorkareaRect
8
+ }
9
+
10
+ export const DesktopRuntimeContext = createContext<DesktopRuntime | null>(null)
11
+
12
+ export function useDesktopRuntime(): DesktopRuntime {
13
+ const runtime = useContext(DesktopRuntimeContext)
14
+ if (!runtime) {
15
+ throw new Error('[owd-react-theme-win95] useDesktopRuntime() must be used inside <DesktopShell>.')
16
+ }
17
+ return runtime
18
+ }
package/src/index.ts ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * @owdproject/react-theme-win95
3
+ * Windows 95 Open Web Desktop theme for React.
4
+ *
5
+ * Built on the headless `@owdproject/core` engine, the `@owdproject/react`
6
+ * hooks and the `@owdproject/react-kit-react95` visual primitives.
7
+ */
8
+
9
+ export { DesktopShell, type DesktopShellProps } from './DesktopShell'
10
+ export { DesktopWindow, type DesktopWindowProps } from './components/DesktopWindow'
11
+ export { SystemBar, type SystemBarProps } from './components/SystemBar'
12
+ export { StartMenuPanel, type StartMenuPanelProps } from './components/StartMenuPanel'
13
+ export { useDesktopRuntime, DesktopRuntimeContext, type DesktopRuntime } from './desktop-context'
14
+ export { builtinApps, AboutApp, NotepadApp } from './apps'
15
+ export type { DesktopApp, DesktopAppRenderProps } from './types'
16
+
17
+ // Re-export the kit primitives so consumers get one entry point.
18
+ export * from '@owdproject/react-kit-react95'
package/src/theme.css ADDED
@@ -0,0 +1,217 @@
1
+ /*
2
+ * @owdproject/react-theme-win95 — theme layout & app styles.
3
+ * Visual primitives come from @react95/core (via @owdproject/react-kit-react95).
4
+ */
5
+
6
+ .owd-shell {
7
+ position: fixed;
8
+ inset: 0;
9
+ overflow: hidden;
10
+ }
11
+
12
+ .owd-shell__desktop {
13
+ position: absolute;
14
+ inset: 0 0 28px 0;
15
+ overflow: hidden;
16
+ }
17
+
18
+ .owd-shell__watermark {
19
+ position: absolute;
20
+ right: 16px;
21
+ bottom: 16px;
22
+ color: rgba(255, 255, 255, 0.6);
23
+ font-weight: bold;
24
+ text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.35);
25
+ pointer-events: none;
26
+ }
27
+
28
+ .owd-shell__startmenu {
29
+ position: absolute;
30
+ left: 2px;
31
+ bottom: 28px;
32
+ z-index: 100000;
33
+ }
34
+
35
+ /* ----------------------------------------------------------- System bar */
36
+
37
+ .owd-taskbar {
38
+ position: fixed;
39
+ left: 0;
40
+ right: 0;
41
+ bottom: 0;
42
+ height: 28px;
43
+ display: flex;
44
+ align-items: center;
45
+ gap: 4px;
46
+ padding: 2px;
47
+ z-index: 90000;
48
+ background: var(--r95-color-material, #c3c7cb);
49
+ box-shadow:
50
+ inset 1px 1px 0 var(--r95-color-borderLightest, #fff),
51
+ inset -1px -1px 0 var(--r95-color-borderDark, #868a8e);
52
+ }
53
+
54
+ .owd-taskbar__start {
55
+ display: inline-flex;
56
+ align-items: center;
57
+ gap: 4px;
58
+ font-weight: bold;
59
+ flex: 0 0 auto;
60
+ }
61
+
62
+ .owd-taskbar__start.is-active {
63
+ padding-top: 2px;
64
+ padding-left: 2px;
65
+ }
66
+
67
+ .owd-taskbar__start-label {
68
+ line-height: 1;
69
+ }
70
+
71
+ .owd-taskbar__tasks {
72
+ flex: 1 1 auto;
73
+ min-width: 0;
74
+ display: flex;
75
+ align-items: center;
76
+ gap: 3px;
77
+ background: transparent;
78
+ box-shadow: none;
79
+ overflow: hidden;
80
+ }
81
+
82
+ .owd-task {
83
+ display: inline-flex;
84
+ align-items: center;
85
+ min-width: 110px;
86
+ max-width: 168px;
87
+ overflow: hidden;
88
+ }
89
+
90
+ .owd-task.is-active {
91
+ box-shadow:
92
+ inset 1px 1px 0 var(--r95-color-borderDark, #868a8e),
93
+ inset -1px -1px 0 var(--r95-color-borderLightest, #fff);
94
+ background-color: var(--r95-color-borderLight, #d2d2d2);
95
+ }
96
+
97
+ .owd-task__label {
98
+ overflow: hidden;
99
+ text-overflow: ellipsis;
100
+ white-space: nowrap;
101
+ }
102
+
103
+ .owd-taskbar__tray {
104
+ flex: 0 0 auto;
105
+ display: flex;
106
+ align-items: center;
107
+ padding: 0 6px;
108
+ background: transparent;
109
+ box-shadow: none;
110
+ }
111
+
112
+ .owd-taskbar__clock {
113
+ padding: 0 2px;
114
+ }
115
+
116
+ /* ------------------------------------------------------------ Start menu */
117
+
118
+ .owd-startmenu {
119
+ display: flex;
120
+ background: var(--r95-color-material, #c3c7cb);
121
+ border: 2px solid;
122
+ border-color: var(--r95-color-borderLight) var(--r95-color-borderDarkest)
123
+ var(--r95-color-borderDarkest) var(--r95-color-borderLight);
124
+ box-shadow: inset 1px 1px 0 var(--r95-color-borderLightest);
125
+ }
126
+
127
+ .owd-startmenu__rail {
128
+ width: 26px;
129
+ background: linear-gradient(180deg, #000e7a, #1084d0);
130
+ color: #fff;
131
+ writing-mode: vertical-rl;
132
+ transform: rotate(180deg);
133
+ display: flex;
134
+ align-items: flex-end;
135
+ justify-content: center;
136
+ padding: 8px 0;
137
+ font-weight: bold;
138
+ letter-spacing: 1px;
139
+ }
140
+
141
+ .owd-startmenu__list {
142
+ border: 0;
143
+ box-shadow: none;
144
+ background: transparent;
145
+ min-width: 190px;
146
+ }
147
+
148
+ /* ------------------------------------------------------- App: About box */
149
+
150
+ .owd-about {
151
+ display: flex;
152
+ flex-direction: column;
153
+ gap: 8px;
154
+ height: 100%;
155
+ padding: 4px;
156
+ user-select: text;
157
+ }
158
+
159
+ .owd-about__banner {
160
+ display: flex;
161
+ align-items: center;
162
+ gap: 10px;
163
+ }
164
+
165
+ .owd-about__small {
166
+ color: #404040;
167
+ }
168
+
169
+ .owd-about__divider {
170
+ width: 100%;
171
+ border: 0;
172
+ border-top: 1px solid var(--r95-color-borderDark);
173
+ border-bottom: 1px solid var(--r95-color-borderLight);
174
+ }
175
+
176
+ .owd-about__text {
177
+ margin: 0;
178
+ line-height: 1.5;
179
+ }
180
+
181
+ .owd-about__actions {
182
+ margin-top: auto;
183
+ display: flex;
184
+ justify-content: flex-end;
185
+ }
186
+
187
+ /* --------------------------------------------------------- App: Notepad */
188
+
189
+ .owd-notepad {
190
+ display: flex;
191
+ flex-direction: column;
192
+ height: 100%;
193
+ }
194
+
195
+ .owd-notepad__menu {
196
+ display: flex;
197
+ gap: 14px;
198
+ padding: 2px 6px;
199
+ }
200
+
201
+ .owd-notepad__area {
202
+ flex: 1 1 auto;
203
+ width: 100%;
204
+ resize: none;
205
+ border: 0;
206
+ outline: none;
207
+ padding: 4px;
208
+ background: #fff;
209
+ font-family: 'Lucida Console', 'Courier New', monospace;
210
+ font-size: 12px;
211
+ line-height: 1.4;
212
+ }
213
+
214
+ .owd-window__empty {
215
+ padding: 8px;
216
+ color: #404040;
217
+ }
package/src/types.ts ADDED
@@ -0,0 +1,18 @@
1
+ import type { ComponentType, ReactElement } from 'react'
2
+ import type { Desktop, WindowInstance } from '@owdproject/core'
3
+
4
+ /** Props passed to every application window component. */
5
+ export interface DesktopAppRenderProps {
6
+ window: WindowInstance
7
+ desktop: Desktop
8
+ }
9
+
10
+ /** A React application that can be launched from the start menu / task bar. */
11
+ export interface DesktopApp {
12
+ id: string
13
+ title: string
14
+ /** React95 icon used in the start menu, window title bar and task bar. */
15
+ icon?: ReactElement
16
+ defaultSize?: { width: number; height: number }
17
+ component: ComponentType<DesktopAppRenderProps>
18
+ }