@carlesandres/house 0.4.13 → 0.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 (86) hide show
  1. package/CHANGELOG.md +32 -2
  2. package/README.md +10 -8
  3. package/dist/bin.js +92 -0
  4. package/dist/index.js +10005 -0
  5. package/package.json +15 -3
  6. package/src/Browser.tsx +0 -1210
  7. package/src/CommandPalette.tsx +0 -214
  8. package/src/Footer.tsx +0 -258
  9. package/src/Header.tsx +0 -71
  10. package/src/PromptRow.tsx +0 -51
  11. package/src/Spinner.tsx +0 -39
  12. package/src/StatusIndicator.tsx +0 -55
  13. package/src/StatusPopover.tsx +0 -166
  14. package/src/brand.ts +0 -7
  15. package/src/cli/argv.ts +0 -179
  16. package/src/commands/buildCommands.ts +0 -98
  17. package/src/commands/paletteOnlyCommands.ts +0 -27
  18. package/src/commands/score.ts +0 -78
  19. package/src/commands/types.ts +0 -26
  20. package/src/config/load.ts +0 -381
  21. package/src/config/save.ts +0 -95
  22. package/src/discovery/filter.ts +0 -148
  23. package/src/discovery/show.ts +0 -48
  24. package/src/discovery/walk.ts +0 -209
  25. package/src/index.tsx +0 -464
  26. package/src/io/clipboard.ts +0 -53
  27. package/src/io/editor.ts +0 -162
  28. package/src/io/readFile.ts +0 -14
  29. package/src/keymap/browser.ts +0 -335
  30. package/src/keymap/displayKey.ts +0 -17
  31. package/src/keymap/keymap.ts +0 -95
  32. package/src/layout/resolve.ts +0 -52
  33. package/src/layout/sidebarEmptyState.ts +0 -7
  34. package/src/layout/sidebarRow.ts +0 -69
  35. package/src/markdown/frontmatter.ts +0 -62
  36. package/src/serve/css.ts +0 -120
  37. package/src/serve/openBrowser.ts +0 -19
  38. package/src/serve/render.ts +0 -56
  39. package/src/serve/server.ts +0 -166
  40. package/src/theme/atom.ts +0 -23
  41. package/src/theme/colors.ts +0 -86
  42. package/src/theme/loader.ts +0 -110
  43. package/src/theme/registry.ts +0 -12
  44. package/src/theme/resolve.ts +0 -168
  45. package/src/theme/themes/aura.json +0 -58
  46. package/src/theme/themes/ayu.json +0 -69
  47. package/src/theme/themes/carbonfox.json +0 -201
  48. package/src/theme/themes/catppuccin-frappe.json +0 -186
  49. package/src/theme/themes/catppuccin-macchiato.json +0 -186
  50. package/src/theme/themes/catppuccin.json +0 -212
  51. package/src/theme/themes/cobalt2.json +0 -181
  52. package/src/theme/themes/cursor.json +0 -202
  53. package/src/theme/themes/dracula.json +0 -172
  54. package/src/theme/themes/everforest.json +0 -194
  55. package/src/theme/themes/flexoki.json +0 -190
  56. package/src/theme/themes/github.json +0 -186
  57. package/src/theme/themes/gruvbox.json +0 -195
  58. package/src/theme/themes/kanagawa.json +0 -180
  59. package/src/theme/themes/lucent-orng.json +0 -186
  60. package/src/theme/themes/material.json +0 -188
  61. package/src/theme/themes/matrix.json +0 -180
  62. package/src/theme/themes/mercury.json +0 -198
  63. package/src/theme/themes/monokai.json +0 -174
  64. package/src/theme/themes/nightowl.json +0 -174
  65. package/src/theme/themes/nord.json +0 -176
  66. package/src/theme/themes/one-dark.json +0 -184
  67. package/src/theme/themes/opencode.json +0 -198
  68. package/src/theme/themes/orng.json +0 -202
  69. package/src/theme/themes/osaka-jade.json +0 -193
  70. package/src/theme/themes/palenight.json +0 -175
  71. package/src/theme/themes/rosepine.json +0 -187
  72. package/src/theme/themes/solarized.json +0 -180
  73. package/src/theme/themes/synthwave84.json +0 -179
  74. package/src/theme/themes/tokyonight.json +0 -196
  75. package/src/theme/themes/vercel.json +0 -198
  76. package/src/theme/themes/vesper.json +0 -171
  77. package/src/theme/themes/zenburn.json +0 -176
  78. package/src/theme/types.ts +0 -115
  79. package/src/tips.ts +0 -88
  80. package/src/ui/middleTruncate.ts +0 -27
  81. package/src/update/cache.ts +0 -77
  82. package/src/update/check.ts +0 -165
  83. package/src/update/compare.ts +0 -41
  84. package/src/update/notice.ts +0 -29
  85. package/src/update/runtime.ts +0 -48
  86. package/src/update/useUpdateNotice.ts +0 -24
@@ -1,52 +0,0 @@
1
- /**
2
- * Layout primitives for the two-pane shape (sidebar + reader).
3
- *
4
- * Visibility and width are decoupled (see DESIGN.md §7.1):
5
- * visible = shown || focus === "sidebar"
6
- * Width is a pure function of viewport, independent of visibility:
7
- * resolveSidebarWidth(viewport, preferred)
8
- *
9
- * Both render code and key handlers consume these primitives — there is no
10
- * parallel implementation living in JSX.
11
- */
12
-
13
- /** Minimum useful sidebar width; below this, file rows truncate too aggressively. */
14
- export const SIDEBAR_MIN_WIDTH = 28
15
- /** Maximum sidebar width; beyond this, wasted whitespace on wide terminals. */
16
- export const SIDEBAR_MAX_WIDTH = 60
17
- /** Reader pane minimum; below this, prose wraps unpleasantly. */
18
- export const READER_MIN_WIDTH = 40
19
- /** Column gap painted between the two panes when both are inline. */
20
- export const DIVIDER_WIDTH = 1
21
-
22
- /**
23
- * Continuous clamp. `preferred` is the user's desired width (until #13 lands,
24
- * derived from viewport). The reader-min ceiling means the sidebar yields
25
- * space rather than squeezing the reader below readability.
26
- *
27
- * Result is not clamped *up* to SIDEBAR_MIN when the viewport itself is too
28
- * narrow to hold both panes — the caller decides whether to render at all
29
- * (e.g. single-pane stack instead of inline). See `canFitInline`.
30
- */
31
- export const resolveSidebarWidth = (viewport: number, preferred: number): number => {
32
- const ceiling = viewport - DIVIDER_WIDTH - READER_MIN_WIDTH
33
- const lower = Math.min(SIDEBAR_MIN_WIDTH, Math.max(0, ceiling))
34
- const upper = Math.max(lower, ceiling)
35
- return Math.max(lower, Math.min(upper, preferred))
36
- }
37
-
38
- /**
39
- * Default preferred sidebar width, derived from viewport until persistent
40
- * config (#13) provides a user-set value. Matches the previous inline math.
41
- */
42
- export const defaultPreferredWidth = (viewport: number): number =>
43
- Math.max(SIDEBAR_MIN_WIDTH, Math.min(SIDEBAR_MAX_WIDTH, Math.floor(viewport * 0.25)))
44
-
45
- /**
46
- * True when an inline (side-by-side) layout still gives the reader at least
47
- * READER_MIN_WIDTH. When false, the viewport is "narrow" and the UI runs in
48
- * single-pane stack mode — sidebar OR reader fills the pane area, never both.
49
- * See DESIGN.md §7.1.
50
- */
51
- export const canFitInline = (viewport: number): boolean =>
52
- viewport >= SIDEBAR_MIN_WIDTH + DIVIDER_WIDTH + READER_MIN_WIDTH
@@ -1,7 +0,0 @@
1
- const HEAD_ELISION_PREFIX = "…"
2
-
3
- export const fitSidebarEmptyValue = (value: string, width: number): string => {
4
- if (value.length <= width) return value
5
- if (width <= 1) return value.slice(value.length - 1)
6
- return HEAD_ELISION_PREFIX + value.slice(value.length - width + 1)
7
- }
@@ -1,69 +0,0 @@
1
- /**
2
- * Sidebar row layout — basename-first with a dim parent suffix sized to the
3
- * available width.
4
- *
5
- * Pure per-row formatting: a row's rendered shape depends only on its own
6
- * path and the column budget — never on neighboring rows. That keeps the
7
- * sidebar stable as filters change and the file set grows, and keeps the
8
- * function trivially predictable. Disambiguation against same-basename
9
- * siblings is the header's job (it shows the full relative path of the
10
- * selected row); a future auto-scroll on the selected sidebar row can carry
11
- * the same information without altering layout for the rest.
12
- *
13
- * Truncation policy: the basename stays whole when possible; the parent path
14
- * middle-truncates into `a/…/z`-style output once it exceeds the remaining
15
- * width. This preserves both the start and the end of the path, which is
16
- * usually what users need to disambiguate nested docs.
17
- */
18
-
19
- import { middleTruncate } from "../ui/middleTruncate.ts"
20
-
21
- export const SIDEBAR_ROW_SEPARATOR = " "
22
- const MIN_PARENT_BUDGET = 3
23
-
24
- export interface SidebarRowParts {
25
- readonly basename: string
26
- readonly separator: string
27
- readonly parent: string
28
- }
29
-
30
- export const formatSidebarRow = (relativePath: string, totalWidth: number): SidebarRowParts => {
31
- const slash = relativePath.lastIndexOf("/")
32
- if (slash < 0) {
33
- return { basename: fitTail(relativePath, totalWidth), separator: "", parent: "" }
34
- }
35
-
36
- const basename = relativePath.slice(slash + 1)
37
- const parentFull = relativePath.slice(0, slash)
38
-
39
- if (basename.length >= totalWidth) {
40
- return { basename: fitTail(basename, totalWidth), separator: "", parent: "" }
41
- }
42
-
43
- const remaining = totalWidth - basename.length - SIDEBAR_ROW_SEPARATOR.length
44
- if (remaining < MIN_PARENT_BUDGET || parentFull.length === 0) {
45
- return { basename, separator: "", parent: "" }
46
- }
47
-
48
- if (parentFull.length <= remaining) {
49
- return row(basename, parentFull)
50
- }
51
-
52
- return row(basename, middleTruncate(parentFull, remaining))
53
- }
54
-
55
- const row = (basename: string, parent: string): SidebarRowParts => ({
56
- basename,
57
- separator: SIDEBAR_ROW_SEPARATOR,
58
- parent,
59
- })
60
-
61
- const fitTail = (s: string, width: number): string => {
62
- if (s.length <= width) return s
63
- // At width ≤ 1 there's no room for both a character and the ellipsis; emit
64
- // a single char so the result respects the budget. Callers currently clamp
65
- // width to ≥ 4, but the helper carries its own floor so a tighter future
66
- // caller can't silently overflow the column.
67
- if (width <= 1) return s.slice(0, 1)
68
- return s.slice(0, width - 1) + "…"
69
- }
@@ -1,62 +0,0 @@
1
- export interface FrontmatterField {
2
- readonly key: string
3
- readonly value: string
4
- }
5
-
6
- export interface FrontmatterRenderModel {
7
- readonly body: string
8
- readonly fields: readonly FrontmatterField[]
9
- }
10
-
11
- const FRONTMATTER_OPEN = "---"
12
- const FRONTMATTER_CLOSE = "---"
13
-
14
- const normalizeValue = (raw: string): string => {
15
- const value = raw.trim()
16
- if (
17
- (value.startsWith('"') && value.endsWith('"')) ||
18
- (value.startsWith("'") && value.endsWith("'"))
19
- ) {
20
- return value.slice(1, -1)
21
- }
22
- return value
23
- }
24
-
25
- const formatKey = (key: string): string => key.replace(/[-_]+/g, " ")
26
-
27
- const toDisplayField = (key: string, value: string): FrontmatterField => ({
28
- key: formatKey(key),
29
- value: normalizeValue(value),
30
- })
31
-
32
- export const parseFrontmatter = (content: string): FrontmatterRenderModel => {
33
- if (
34
- !content.startsWith(`${FRONTMATTER_OPEN}\n`) &&
35
- !content.startsWith(`${FRONTMATTER_OPEN}\r\n`)
36
- ) {
37
- return { body: content, fields: [] }
38
- }
39
-
40
- const lines = content.split(/\r?\n/)
41
- if (lines[0] !== FRONTMATTER_OPEN) return { body: content, fields: [] }
42
-
43
- const closeIndex = lines.indexOf(FRONTMATTER_CLOSE, 1)
44
- if (closeIndex <= 0) return { body: content, fields: [] }
45
-
46
- const fields: FrontmatterField[] = []
47
- for (const line of lines.slice(1, closeIndex)) {
48
- if (line.trim().length === 0) continue
49
- const colon = line.indexOf(":")
50
- if (colon <= 0) return { body: content, fields: [] }
51
- const key = line.slice(0, colon).trim()
52
- const value = line.slice(colon + 1)
53
- if (key.length === 0) return { body: content, fields: [] }
54
- fields.push(toDisplayField(key, value))
55
- }
56
-
57
- const body = lines
58
- .slice(closeIndex + 1)
59
- .join("\n")
60
- .replace(/^\n+/, "")
61
- return { body, fields }
62
- }
package/src/serve/css.ts DELETED
@@ -1,120 +0,0 @@
1
- /**
2
- * Neutral GitHub-ish CSS for the serve action.
3
- *
4
- * Embedded inline by render.ts so each served page is self-contained — no
5
- * second request, "Save Page As" gives a working file, future --export
6
- * reuses the same renderer.
7
- */
8
- export const css = `
9
- :root {
10
- color-scheme: light dark;
11
- --bg: #ffffff;
12
- --fg: #1f2328;
13
- --muted: #59636e;
14
- --border: #d1d9e0;
15
- --code-bg: #f6f8fa;
16
- --accent: #0969da;
17
- --blockquote: #59636e;
18
- --blockquote-border: #d1d9e0;
19
- }
20
- @media (prefers-color-scheme: dark) {
21
- :root {
22
- --bg: #0d1117;
23
- --fg: #e6edf3;
24
- --muted: #9198a1;
25
- --border: #30363d;
26
- --code-bg: #151b23;
27
- --accent: #4493f8;
28
- --blockquote: #9198a1;
29
- --blockquote-border: #30363d;
30
- }
31
- }
32
- * { box-sizing: border-box; }
33
- html, body {
34
- margin: 0;
35
- padding: 0;
36
- background: var(--bg);
37
- color: var(--fg);
38
- }
39
- body {
40
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans",
41
- Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
42
- font-size: 16px;
43
- line-height: 1.6;
44
- }
45
- main {
46
- max-width: 860px;
47
- margin: 0 auto;
48
- padding: 2.5rem 1.5rem 6rem;
49
- }
50
- h1, h2, h3, h4, h5, h6 {
51
- margin-top: 1.75em;
52
- margin-bottom: 0.6em;
53
- font-weight: 600;
54
- line-height: 1.25;
55
- }
56
- h1 { font-size: 2em; padding-bottom: .3em; border-bottom: 1px solid var(--border); }
57
- h2 { font-size: 1.5em; padding-bottom: .3em; border-bottom: 1px solid var(--border); }
58
- h3 { font-size: 1.25em; }
59
- h4 { font-size: 1em; }
60
- h5 { font-size: .9em; }
61
- h6 { font-size: .85em; color: var(--muted); }
62
- p, ul, ol, blockquote, pre, table { margin: 0 0 1em; }
63
- a { color: var(--accent); text-decoration: none; }
64
- a:hover { text-decoration: underline; }
65
- ul, ol { padding-left: 2em; }
66
- li + li { margin-top: .25em; }
67
- blockquote {
68
- margin: 0 0 1em;
69
- padding: 0 1em;
70
- color: var(--blockquote);
71
- border-left: .25em solid var(--blockquote-border);
72
- }
73
- code {
74
- font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
75
- "Liberation Mono", monospace;
76
- font-size: 85%;
77
- padding: .2em .4em;
78
- background: var(--code-bg);
79
- border-radius: 6px;
80
- }
81
- pre {
82
- background: var(--code-bg);
83
- border: 1px solid var(--border);
84
- padding: 1em;
85
- border-radius: 6px;
86
- overflow: auto;
87
- font-size: 85%;
88
- line-height: 1.45;
89
- }
90
- pre code {
91
- padding: 0;
92
- background: transparent;
93
- border-radius: 0;
94
- font-size: 100%;
95
- }
96
- table {
97
- border-collapse: collapse;
98
- display: block;
99
- overflow: auto;
100
- width: max-content;
101
- max-width: 100%;
102
- }
103
- th, td { padding: .4em .8em; border: 1px solid var(--border); }
104
- th { background: var(--code-bg); font-weight: 600; }
105
- hr { height: 1px; border: 0; background: var(--border); margin: 1.5em 0; }
106
- img { max-width: 100%; }
107
- kbd {
108
- display: inline-block;
109
- padding: 3px 5px;
110
- font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
111
- font-size: 11px;
112
- line-height: 10px;
113
- color: var(--fg);
114
- vertical-align: middle;
115
- background: var(--code-bg);
116
- border: 1px solid var(--border);
117
- border-radius: 6px;
118
- box-shadow: inset 0 -1px 0 var(--border);
119
- }
120
- `.trim()
@@ -1,19 +0,0 @@
1
- /**
2
- * Best-effort "open URL in default browser". Fire and forget — failures are
3
- * silent because the URL has already been printed; user can click it.
4
- */
5
-
6
- import { spawn } from "node:child_process"
7
-
8
- export const openInBrowser = (url: string): void => {
9
- const cmd =
10
- process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open"
11
- const args = process.platform === "win32" ? ["/c", "start", "", url] : [url]
12
- try {
13
- const child = spawn(cmd, args, { stdio: "ignore", detached: true })
14
- child.on("error", () => {})
15
- child.unref()
16
- } catch {
17
- // platform without a launcher; URL was printed already.
18
- }
19
- }
@@ -1,56 +0,0 @@
1
- /**
2
- * renderHtml — wraps marked-rendered markdown in a self-contained HTML page.
3
- *
4
- * Embeds the CSS and a tiny SSE reload script so each response is one round
5
- * trip. The reload script reconnects on drop; the server's `/__reload`
6
- * endpoint pushes a `reload` event whenever the served file changes.
7
- */
8
-
9
- import { marked } from "marked"
10
- import { css } from "./css.ts"
11
-
12
- const escapeHtml = (s: string): string =>
13
- s.replace(/[&<>"']/g, (c) => {
14
- switch (c) {
15
- case "&":
16
- return "&amp;"
17
- case "<":
18
- return "&lt;"
19
- case ">":
20
- return "&gt;"
21
- case '"':
22
- return "&quot;"
23
- default:
24
- return "&#39;"
25
- }
26
- })
27
-
28
- const reloadScript = `
29
- <script>
30
- (function () {
31
- function connect() {
32
- var es = new EventSource("/__reload");
33
- es.addEventListener("reload", function () { location.reload(); });
34
- es.onerror = function () { es.close(); setTimeout(connect, 500); };
35
- }
36
- connect();
37
- })();
38
- </script>
39
- `.trim()
40
-
41
- export const renderHtml = (markdown: string, title: string): string => {
42
- const body = marked.parse(markdown, { async: false }) as string
43
- return `<!DOCTYPE html>
44
- <html lang="en">
45
- <head>
46
- <meta charset="utf-8">
47
- <meta name="viewport" content="width=device-width, initial-scale=1">
48
- <title>${escapeHtml(title)}</title>
49
- <style>${css}</style>
50
- </head>
51
- <body>
52
- <main>${body}</main>
53
- ${reloadScript}
54
- </body>
55
- </html>`
56
- }
@@ -1,166 +0,0 @@
1
- /**
2
- * Local HTML preview server for a single markdown file.
3
- *
4
- * One long-lived `Bun.serve` instance. The served file path is swappable
5
- * via `setTarget(path)` — used by the TUI's `O` binding so pressing it on a
6
- * new file retargets the existing server (live-reload fires) instead of
7
- * spawning a second one.
8
- *
9
- * Live reload: an SSE endpoint at `/__reload` holds connections open and
10
- * pushes a `reload` event whenever the watched file changes. A new watcher
11
- * is created per `setTarget` call; the previous one is closed.
12
- */
13
-
14
- import { basename } from "node:path"
15
- import { watch, type FSWatcher } from "node:fs"
16
- import { readFile } from "node:fs/promises"
17
- import { renderHtml } from "./render.ts"
18
-
19
- export interface ServerHandle {
20
- /** Base URL, e.g. http://localhost:51234 */
21
- readonly url: string
22
- /** Swap which file is served. Pushes a reload to connected clients. */
23
- setTarget(path: string): void
24
- /** Path currently being served. */
25
- currentTarget(): string
26
- stop(): Promise<void>
27
- }
28
-
29
- export interface StartOptions {
30
- readonly path: string
31
- /** 0 = OS-assigned. */
32
- readonly port?: number
33
- }
34
-
35
- type ReloadController = ReadableStreamDefaultController<Uint8Array>
36
-
37
- const encoder = new TextEncoder()
38
- const sseEvent = (event: string, data = ""): Uint8Array =>
39
- encoder.encode(`event: ${event}\ndata: ${data}\n\n`)
40
-
41
- export const startServer = ({ path, port = 0 }: StartOptions): ServerHandle => {
42
- let target = path
43
- let watcher: FSWatcher | null = null
44
- const clients = new Set<ReloadController>()
45
-
46
- const broadcastReload = () => {
47
- for (const c of clients) {
48
- try {
49
- c.enqueue(sseEvent("reload"))
50
- } catch {
51
- clients.delete(c)
52
- }
53
- }
54
- }
55
-
56
- // `fs.watch` watches an inode, not a path. Editors that save via
57
- // write-tmp + rename (vim default, VS Code, JetBrains, …) replace the
58
- // inode, after which our watcher fires nothing. So we re-watch on every
59
- // event, and debounce because a single save often emits 2–3 events.
60
- const startWatching = (p: string) => {
61
- watcher?.close()
62
- watcher = null
63
- let timer: ReturnType<typeof setTimeout> | null = null
64
- try {
65
- watcher = watch(p, () => {
66
- if (timer) clearTimeout(timer)
67
- timer = setTimeout(() => {
68
- broadcastReload()
69
- startWatching(p)
70
- }, 30)
71
- })
72
- watcher.on("error", () => {
73
- // Stale handle after rename; the change event already scheduled
74
- // a re-watch. Swallow so it doesn't crash the process.
75
- })
76
- } catch {
77
- // Path went away between presses. Server still serves the last
78
- // good read; live reload stays off until the path returns.
79
- }
80
- }
81
- startWatching(target)
82
-
83
- const server = Bun.serve({
84
- port,
85
- // Bind to loopback. Default is 0.0.0.0 (LAN-exposed); we render the
86
- // user's local files, so leaking them to the network would be a
87
- // surprise. URL strings are localhost-only by construction below.
88
- hostname: "127.0.0.1",
89
- async fetch(req, server) {
90
- const url = new URL(req.url)
91
- if (url.pathname === "/__reload") {
92
- // SSE stream is silent between file changes; without this Bun
93
- // closes the request at the default 10s idleTimeout and warns.
94
- server.timeout(req, 0)
95
- // `cancel` receives a reason, not the controller — capture
96
- // the controller in `start` so we can remove it from the set
97
- // on disconnect. Without this, dead clients accumulate.
98
- let ctrl: ReloadController | null = null
99
- const stream = new ReadableStream<Uint8Array>({
100
- start(controller) {
101
- ctrl = controller
102
- clients.add(controller)
103
- // Initial comment keeps some proxies from buffering.
104
- controller.enqueue(encoder.encode(": connected\n\n"))
105
- },
106
- cancel() {
107
- if (ctrl) clients.delete(ctrl)
108
- },
109
- })
110
- return new Response(stream, {
111
- headers: {
112
- "content-type": "text/event-stream",
113
- "cache-control": "no-cache",
114
- connection: "keep-alive",
115
- },
116
- })
117
- }
118
- if (url.pathname !== "/") {
119
- return new Response("not found", { status: 404 })
120
- }
121
- try {
122
- const md = await readFile(target, "utf8")
123
- const html = renderHtml(md, basename(target))
124
- return new Response(html, {
125
- headers: {
126
- "content-type": "text/html; charset=utf-8",
127
- "cache-control": "no-store",
128
- },
129
- })
130
- } catch (err) {
131
- return new Response(`cannot read ${target}: ${String(err)}`, {
132
- status: 500,
133
- headers: { "content-type": "text/plain; charset=utf-8" },
134
- })
135
- }
136
- },
137
- })
138
-
139
- const url = `http://localhost:${server.port}`
140
-
141
- return {
142
- url,
143
- currentTarget: () => target,
144
- setTarget: (next) => {
145
- if (next === target) {
146
- broadcastReload()
147
- return
148
- }
149
- target = next
150
- startWatching(next)
151
- broadcastReload()
152
- },
153
- stop: async () => {
154
- watcher?.close()
155
- for (const c of clients) {
156
- try {
157
- c.close()
158
- } catch {
159
- // already closed
160
- }
161
- }
162
- clients.clear()
163
- await server.stop(true)
164
- },
165
- }
166
- }
package/src/theme/atom.ts DELETED
@@ -1,23 +0,0 @@
1
- /**
2
- * themeAtom — signals the active theme identity to React.
3
- *
4
- * Components read this atom only to subscribe to re-renders; they pull the
5
- * actual token values from the `colors` singleton (mutated by
6
- * `setActiveTheme` before the re-render fires).
7
- *
8
- * Shape: `{ id, tone }` so that a future theme picker can write both fields
9
- * atomically and the component sees a single re-render.
10
- */
11
-
12
- import * as Atom from "effect/unstable/reactivity/Atom"
13
- import type { Tone } from "./types.ts"
14
-
15
- export interface ThemeState {
16
- readonly id: string
17
- readonly tone: Tone
18
- }
19
-
20
- export const themeAtom: Atom.Writable<ThemeState, ThemeState> = Atom.make<ThemeState>({
21
- id: "opencode",
22
- tone: "dark",
23
- })
@@ -1,86 +0,0 @@
1
- import type { StyleDefinitionInput } from "@opentui/core"
2
- import { resolveTheme } from "./resolve.ts"
3
- import type { ColorPalette, ResolvedTheme, ThemeDefinition, Tone } from "./types.ts"
4
-
5
- /**
6
- * Adapt a resolved theme (opencode-shaped flat tokens) to the
7
- * {@link ColorPalette} shape consumed by Browser / HelpOverlay / index.
8
- *
9
- * - UI tokens map name-for-name with OpenCode's TUI theme semantics.
10
- * - `syntax` is a fully populated opentui tree-sitter scope map built from
11
- * `markdown*` and `syntax*` tokens.
12
- */
13
- const buildPalette = (r: ResolvedTheme): ColorPalette => {
14
- return {
15
- background: r.background,
16
- backgroundPanel: r.backgroundPanel,
17
- backgroundElement: r.backgroundElement,
18
- text: r.text,
19
- textMuted: r.textMuted,
20
- border: r.border,
21
- borderActive: r.borderActive,
22
- borderSubtle: r.borderSubtle,
23
- selectedListItemText: r.selectedListItemText,
24
- primary: r.primary,
25
- secondary: r.secondary,
26
- accent: r.accent,
27
- error: r.error,
28
- warning: r.warning,
29
- success: r.success,
30
- info: r.info,
31
- syntax: buildSyntaxMap(r),
32
- }
33
- }
34
-
35
- const buildSyntaxMap = (r: ResolvedTheme): Record<string, StyleDefinitionInput> => {
36
- const codeBg = r.backgroundPanel
37
- return {
38
- keyword: { fg: r.syntaxKeyword, bold: true },
39
- string: { fg: r.syntaxString },
40
- comment: { fg: r.syntaxComment, italic: true },
41
- number: { fg: r.syntaxNumber },
42
- function: { fg: r.syntaxFunction },
43
- type: { fg: r.syntaxType },
44
- operator: { fg: r.syntaxOperator },
45
- variable: { fg: r.syntaxVariable },
46
- property: { fg: r.syntaxFunction },
47
- "punctuation.bracket": { fg: r.syntaxPunctuation },
48
- "punctuation.delimiter": { fg: r.syntaxPunctuation },
49
- "punctuation.special": { fg: r.syntaxPunctuation },
50
- "markup.heading": { fg: r.markdownHeading, bold: true },
51
- "markup.heading.1": { fg: r.markdownHeading, bold: true, underline: true },
52
- "markup.heading.2": { fg: r.markdownHeading, bold: true },
53
- "markup.heading.3": { fg: r.markdownHeading },
54
- "markup.bold": { fg: r.markdownStrong, bold: true },
55
- "markup.strong": { fg: r.markdownStrong, bold: true },
56
- "markup.italic": { fg: r.markdownEmph, italic: true },
57
- // opentui's SyntaxStyle has no strikethrough attribute — best-effort:
58
- // dim + muted color so `~~strike~~` is at least visually distinguishable.
59
- "markup.strikethrough": { fg: r.textMuted, dim: true },
60
- "markup.list": { fg: r.markdownListItem },
61
- "markup.quote": { fg: r.markdownBlockQuote, italic: true },
62
- "markup.raw": { fg: r.markdownCode, bg: codeBg },
63
- "markup.raw.block": { fg: r.markdownCodeBlock, bg: codeBg },
64
- "markup.raw.inline": { fg: r.markdownCode, bg: codeBg },
65
- "markup.link": { fg: r.markdownLink, underline: true },
66
- "markup.link.label": { fg: r.markdownLinkText, underline: true },
67
- "markup.link.url": { fg: r.markdownLink, underline: true },
68
- label: { fg: r.markdownListItem },
69
- conceal: { fg: r.borderSubtle },
70
- default: { fg: r.markdownText },
71
- }
72
- }
73
-
74
- /**
75
- * Active flat palette. Components import `colors` and read tokens
76
- * directly. `setActiveTheme` mutates this in place so the reference stays
77
- * stable (no Provider/context).
78
- *
79
- * Initial value uses the all-fallback resolution; `setActiveTheme` is
80
- * called at boot from `index.tsx` before the React tree mounts.
81
- */
82
- export const colors: ColorPalette = buildPalette(resolveTheme({ theme: {} }, "dark"))
83
-
84
- export const setActiveTheme = (definition: ThemeDefinition, tone: Tone): void => {
85
- Object.assign(colors, buildPalette(resolveTheme(definition.source, tone)))
86
- }