create-obsidian-arrow 0.2.2 → 0.3.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 (34) hide show
  1. package/README.md +5 -4
  2. package/package.json +1 -1
  3. package/template/AGENTS.md +27 -3
  4. package/template/README.md +40 -1
  5. package/template/_gitignore +3 -0
  6. package/template/docs/prompts/agent-setup.md +22 -11
  7. package/template/docs/prompts/update-existing.md +1 -1
  8. package/template/docs/workflow.md +18 -6
  9. package/template/package.json +1 -1
  10. package/template/src/components/SettingsPanel.stories.ts +11 -0
  11. package/template/src/components/SettingsPanel.ts +1 -1
  12. package/template/src/components/Toggle.stories.ts +28 -0
  13. package/template/src/main.ts +1 -0
  14. package/template/src/router/client.ts +15 -2
  15. package/template/src/router/routeToPage.ts +74 -27
  16. package/template/src/sandbox/home.ts +55 -26
  17. package/template/src/sandbox/sandbox.css +302 -0
  18. package/template/src/utilities.css +205 -0
  19. package/template/src/viewer/ClassesPage.ts +37 -0
  20. package/template/src/viewer/ComponentsIndex.ts +56 -0
  21. package/template/src/viewer/StoryPage.ts +73 -0
  22. package/template/src/viewer/TokensPage.ts +82 -0
  23. package/template/src/viewer/derive.ts +81 -0
  24. package/template/src/viewer/discovery.ts +63 -0
  25. package/template/src/viewer/obsidian-classes.ts +269 -0
  26. package/template/src/viewer/sidebar.ts +55 -0
  27. package/template/src/viewer/stories.ts +83 -0
  28. package/template/src/viewer/token-utils.ts +84 -0
  29. package/template/src/viewer/tokens.ts +30 -0
  30. package/template/test/token-utils.test.mjs +65 -0
  31. package/template/test/viewer-derive.test.mjs +65 -0
  32. package/template/test/viewer-stories.test.mjs +44 -0
  33. package/template/src/examples/ExamplesIndex.ts +0 -36
  34. package/template/src/examples/registry.ts +0 -26
package/README.md CHANGED
@@ -68,10 +68,11 @@ node create-obsidian-arrow/index.mjs update ../my-app # update
68
68
  ## What you get
69
69
 
70
70
  A full sandbox: client-only Vite + TS, `@arrow-js/core` + `@arrow-js/framework`
71
- (no SSR), `routeToPage` + Navigation-API router with an `/example` demo, Biome +
72
- husky pre-commit + `node:test` + GitHub Actions CI, a `skills:install` that pulls
73
- the agent skills from the published repo, and the `pull-css` script that extracts
74
- Obsidian's `app.css`.
71
+ (no SSR), `routeToPage` + Navigation-API router, a Storybook-style component
72
+ viewer at `/components` (co-locate `*.stories.ts` to add stories), a live token
73
+ and class reference at `/reference`, Biome + husky pre-commit + `node:test` +
74
+ GitHub Actions CI, a `skills:install` that pulls the agent skills from the
75
+ published repo, and the `pull-css` script that extracts Obsidian's `app.css`.
75
76
 
76
77
  ## Maintaining the template
77
78
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-obsidian-arrow",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "Scaffold an Obsidian-styled Arrow.js UI sandbox (pnpm create obsidian-arrow <dir>).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,9 +10,13 @@ This file is the hub — everything else is linked from here:
10
10
 
11
11
  - [`docs/workflow.md`](docs/workflow.md) — fresh-machine → running workflow.
12
12
  - [`skills/`](skills/) — installable domain skills (`pnpm skills:install`):
13
- obsidian-arrow-sandbox, arrow-js-obsidian-templates, arrow-js-obsidian-patterns,
14
- arrow-js-obsidian-porting (sandbox→plugin parity check), obsidian-arrow-maintenance
15
- (updating an existing project).
13
+ - `obsidian-arrow-sandbox` — running the sandbox, CSS scoping, porting basics.
14
+ - `obsidian-arrow-stories` **component + story authoring workflow**: `defineStories` API, variants, children, status flag, DRY patterns, utilities.
15
+ - `obsidian-arrow-css` **CSS decision hierarchy**: Obsidian classes → oas-* utilities → custom CSS; token reference; specificity scoping; overrides via variables; auditing for excess CSS.
16
+ - `arrow-js-obsidian-templates` — Arrow v1.0.6 template syntax + footguns.
17
+ - `arrow-js-obsidian-patterns` — icons, CSS scoping, lifecycle, reactive state.
18
+ - `arrow-js-obsidian-porting` — sandbox→plugin parity check (`component-hash`).
19
+ - `obsidian-arrow-maintenance` — updating an existing project.
16
20
  - [`docs/prompts/`](docs/prompts/) — copy-paste agent prompts: `agent-setup.md`
17
21
  (scaffold + orient) and `update-existing.md` (update tooling + skills, keep src).
18
22
 
@@ -78,6 +82,20 @@ events via `@event` (`@click="${fn}"`), keyed lists via
78
82
  `html\`…\`.key(id)`, async sections via `component(asyncFn, { fallback })` wrapped
79
83
  in `boundary()`.
80
84
 
85
+ ## Conventions
86
+
87
+ - **Check `/reference/classes` first** — Obsidian has semantic classes for most
88
+ common patterns. Use them before writing any custom CSS.
89
+ - **Use `oas-*` utilities second** (`src/utilities.css`) — flex, gap, padding,
90
+ margin, typography, border, overflow helpers built on Obsidian's token scale.
91
+ Prefer `class="oas-flex oas-gap-2"` over `style="display:flex;gap:8px"`.
92
+ - **Custom CSS last** — only when Obsidian has no class and utilities don't cover
93
+ it. Scope under container + element type, use `var(--…)` tokens for values.
94
+ - Add a story by creating a co-located `*.stories.ts` next to the component —
95
+ it appears at `/components/<slug>` automatically. See the `obsidian-arrow-stories`
96
+ skill for the full `defineStories` API (variants, children, status, notes).
97
+ Browse live tokens at `/reference`, curated classes at `/reference/classes`.
98
+
81
99
  ## CSS scoping
82
100
 
83
101
  - Use Obsidian's own classes (`.setting-item`, `.clickable-icon`,
@@ -109,3 +127,9 @@ mount it from `ItemView.onOpen()` via `template(this.contentEl)`. If it uses
109
127
  `boundary()`/async components, add `@arrow-js/framework` to the plugin and the
110
128
  `import '@arrow-js/framework'` side-effect import. Strip any sandbox chrome
111
129
  (`src/sandbox/*`) — that stays here.
130
+
131
+ **Bring `src/utilities.css` along.** Components may use `oas-`-prefixed utility
132
+ classes (flex, gap, padding, typography, border — all built on Obsidian's token
133
+ scale). Copy `src/utilities.css` once into the plugin's styles directory and
134
+ import it there. The `oas-` prefix guarantees no conflict with Obsidian selectors.
135
+ Re-copy when the sandbox file changes (the porting-parity skill covers this).
@@ -91,7 +91,9 @@ skills under [`skills/`](skills/) — it's a skill marketplace. Scaffolds **don'
91
91
  vendor copies**; they pull from this published repo, so installs are always
92
92
  current.
93
93
 
94
- - `obsidian-arrow-sandbox` — running and using this sandbox, and porting to a plugin.
94
+ - `obsidian-arrow-sandbox` — running and using this sandbox, CSS scoping, porting basics.
95
+ - `obsidian-arrow-stories` — **component + story authoring workflow**: the complete `defineStories` API (variants, children, status flag, notes), DRY patterns, utility classes, story viewing, and how to structure sub-components.
96
+ - `obsidian-arrow-css` — **CSS decision hierarchy**: Obsidian classes first, `oas-*` utilities second, custom CSS last; the live token reference (`/reference`), class catalog (`/reference/classes`), specificity scoping, overrides via CSS variables, and auditing components to minimize hand-written CSS.
95
97
  - `arrow-js-obsidian-templates` — Arrow v1.0.6 template rules + footguns.
96
98
  - `arrow-js-obsidian-patterns` — integration patterns: icons (Lucide / data-icon
97
99
  sweep), CSS scoping vs Obsidian globals, mount/unmount lifecycle, reactive state.
@@ -125,6 +127,38 @@ different project root, e.g. an outer repo a scaffold is nested in), `--global`
125
127
  forms `SKILLS_AGENT` / `SKILLS_PROJECT_DIR` / `SKILLS_GLOBAL` (and
126
128
  `SKIP_SKILLS_INSTALL=1`) influence *that* path.
127
129
 
130
+ ## Component viewer & reference
131
+
132
+ **Component viewer (`/components`):** a Storybook-style browser for sandbox
133
+ components. Co-locate a `*.stories.ts` file next to any component and it appears
134
+ in the sidebar and on Home automatically. Stories support named variants,
135
+ drill-in via `children` slugs, and a derived src path shown in the viewer — all
136
+ discovered at build time via `import.meta.glob`.
137
+
138
+ **Reference index (`/reference`):** all `var(--)` tokens parsed live from
139
+ `app.css`, grouped by category (Size & spacing, Radius, Colors, …) with color
140
+ swatches, size bars, a filter input, theme-aware resolved values, and a copy
141
+ button. `/reference/classes` is a curated catalog of Obsidian pattern classes
142
+ with live previews.
143
+
144
+ ### Add a story
145
+
146
+ Create `src/components/MyThing.stories.ts` next to the component:
147
+
148
+ ```ts
149
+ import { defineStories } from "../viewer/stories";
150
+ import { MyThing } from "./MyThing";
151
+
152
+ export default defineStories({
153
+ description: "What it demonstrates.",
154
+ variants: { default: () => MyThing() },
155
+ });
156
+ ```
157
+
158
+ It appears in the sidebar and on Home automatically; the src path shown in the
159
+ viewer is derived from the file location. Stories are sandbox-only — they never
160
+ port to the plugin.
161
+
128
162
  ## Porting a component into the plugin
129
163
 
130
164
  Components use only Obsidian classes + `var(--…)` tokens and mount via
@@ -133,6 +167,11 @@ copy the component file into the plugin's view directory and mount it from
133
167
  `onOpen()`. If it uses `boundary()`/async components, add `@arrow-js/framework`
134
168
  to the plugin and the `import '@arrow-js/framework'` side-effect import.
135
169
 
170
+ Also copy `src/utilities.css` into the plugin once — components may use `oas-`
171
+ utility classes (flex, gap, padding, typography, border helpers built on
172
+ Obsidian's token scale). The `oas-` prefix means no conflicts with Obsidian's
173
+ own selectors. All ported components in a plugin share one copy of this file.
174
+
136
175
  ## Arrow v1.0.6 footguns (learned the hard way)
137
176
 
138
177
  These are enforced/encoded so they don't regress:
@@ -10,3 +10,6 @@ dist
10
10
  # Obsidian's app.css is proprietary — extract it locally with `pnpm pull-css`,
11
11
  # don't commit/redistribute it.
12
12
  public/app.css
13
+
14
+ # subagent-driven-development scratch (briefs, reports, ledger)
15
+ .superpowers/
@@ -30,7 +30,8 @@ Then:
30
30
  # committed; it's Obsidian's proprietary CSS). Auto-detect is
31
31
  # macOS-only; on Windows/WSL pass --path <obsidian.asar|app.css>
32
32
  # or set OBSIDIAN_ASAR=<path>.
33
- pnpm dev # open the printed URL: / is the examples index, /example the demo.
33
+ pnpm dev # open the printed URL: / is home, /components the story viewer,
34
+ # /reference the Obsidian token/class index.
34
35
  # The toolbar slider/presets + edge drag handle resize the panel.
35
36
  pnpm skills:install --yes # install all agent skills non-interactively, pulled
36
37
  # from the published repo (not vendored) — this loads
@@ -42,11 +43,17 @@ Then:
42
43
  READ FIRST
43
44
  - AGENTS.md (root) — operating guide + docs map (links everything below).
44
45
  - docs/workflow.md — fresh-machine → running workflow.
45
- - skills/*/SKILL.md — obsidian-arrow-sandbox (workflow), arrow-js-obsidian-
46
- templates (template syntax + footguns), arrow-js-obsidian-patterns (icons via
47
- Lucide/data-icon sweep, CSS scoping, mount/unmount lifecycle, reactive state),
48
- arrow-js-obsidian-porting (sandbox→plugin parity check),
49
- obsidian-arrow-maintenance (updating an existing project).
46
+ - skills/*/SKILL.md — seven installable skills:
47
+ obsidian-arrow-sandbox running the sandbox, CSS scoping, porting basics
48
+ obsidian-arrow-stories component + story authoring: defineStories API,
49
+ variants, children, status, DRY patterns, utilities
50
+ obsidian-arrow-css CSS decision hierarchy: Obsidian classes → oas-*
51
+ utilities → custom CSS; token/class reference;
52
+ scoping rules; overrides via variables; CSS audit
53
+ arrow-js-obsidian-templates template syntax + hard footguns
54
+ arrow-js-obsidian-patterns icons, CSS scoping, lifecycle, reactive state
55
+ arrow-js-obsidian-porting sandbox→plugin parity check
56
+ obsidian-arrow-maintenance updating an existing project
50
57
 
51
58
  ARROW v1.0.6 FOOTGUNS — do not relearn these the hard way:
52
59
  1. NO literal HTML comments inside html`` templates — Arrow treats HTML comments
@@ -68,8 +75,9 @@ CONVENTIONS
68
75
  only when Obsidian has no class, scoped under a container class + element type
69
76
  (e.g. `.my-panel button.my-action`) so it beats Obsidian's global button rule.
70
77
  - Sandbox-only chrome lives in src/sandbox/* — it does NOT port to a plugin.
71
- - Add a demo by exporting an Arrow component and registering it in
72
- src/examples/registry.ts (it shows on the index and at its own route).
78
+ - Add a demo by creating a co-located `*.stories.ts` next to the component (see
79
+ README "Add a story"); it appears at `/components/<slug>` automatically.
80
+ Browse Obsidian tokens/classes at `/reference`.
73
81
 
74
82
  VERIFY BEFORE CLAIMING DONE
75
83
  - `pnpm typecheck && pnpm test && pnpm lint` (or `pnpm run ci` for the full chain).
@@ -81,15 +89,18 @@ PORTING TO A PLUGIN
81
89
  Copy the component file into the plugin's view dir and mount from
82
90
  ItemView.onOpen() via `template(this.contentEl)`. If it uses boundary()/async
83
91
  components, add @arrow-js/framework to the plugin and the side-effect
84
- `import '@arrow-js/framework'`. Leave src/sandbox/* behind. Guard against drift
85
- with the porting-parity check (see the arrow-js-obsidian-porting skill).
92
+ `import '@arrow-js/framework'`. Also copy src/utilities.css into the plugin once
93
+ (all ported components share it) components may use oas-* utility classes
94
+ (flex, gap, padding, text, border helpers built on Obsidian's token scale).
95
+ Leave src/sandbox/* behind. Guard against drift with the porting-parity check
96
+ (see the arrow-js-obsidian-porting skill).
86
97
 
87
98
  MAINTENANCE (existing project)
88
99
  Refresh tooling later with `npx create-obsidian-arrow update` (preserves src/),
89
100
  update skills with `pnpm skills:update`. See the obsidian-arrow-maintenance skill.
90
101
 
91
102
  Start by scaffolding, running setup steps, then read AGENTS.md and confirm
92
- `pnpm dev` renders /example correctly. Report what you see.
103
+ `pnpm dev` renders correctly at /components and /reference. Report what you see.
93
104
  ```
94
105
 
95
106
  ---
@@ -60,7 +60,7 @@ STEPS (in order)
60
60
 
61
61
  7. Verify
62
62
  - pnpm run ci (biome + typecheck + tests + build)
63
- - pnpm dev and confirm /example renders with a clean console
63
+ - pnpm dev and confirm /components and /reference render with a clean console
64
64
 
65
65
  REPORT
66
66
  - What `update` changed, which skills are now installed and where, the pull-css
@@ -25,7 +25,8 @@ pnpm pull-css # macOS: auto-detects /Applications/Obs
25
25
  # or OBSIDIAN_ASAR=<path> pnpm pull-css
26
26
 
27
27
  # 4. Run it
28
- pnpm dev # open the printed URL — / is the index, /example the demo
28
+ pnpm dev # open the printed URL — / is home, /components the story
29
+ # viewer, /reference the token/class index
29
30
  ```
30
31
 
31
32
  `public/app.css` is **git-ignored and never shipped** (it's Obsidian's proprietary
@@ -54,8 +55,15 @@ Then point the agent at [`AGENTS.md`](../AGENTS.md), or brief a fresh agent with
54
55
  ## Build → verify → port loop
55
56
 
56
57
  ```sh
57
- # add a component in src/components/, register it in src/examples/registry.ts
58
+ # 1. Check /reference/classes in the running sandbox does Obsidian have a class
59
+ # for your pattern? Use it before writing custom CSS.
60
+ # 2. Add src/components/MyThing.ts (Arrow component)
61
+ # 3. Add src/components/MyThing.stories.ts (co-located story file)
58
62
  pnpm dev # iterate with HMR
63
+ # /components → index of all discovered stories
64
+ # /components/my-thing → story for MyThing
65
+ # /reference → live Obsidian token reference
66
+ # /reference/classes → curated class catalog with live previews
59
67
  pnpm run ci # biome + typecheck + tests + build before trusting it
60
68
  ```
61
69
 
@@ -63,10 +71,14 @@ Always confirm the actual render in the browser — Arrow's footguns surface onl
63
71
  at render, so a passing `tsc` is not proof a component works. See the footguns in
64
72
  [`AGENTS.md`](../AGENTS.md) and the `arrow-js-obsidian-templates` skill.
65
73
 
66
- **Port a component into a plugin:** copy the file into the plugin's view directory
67
- and mount it from `ItemView.onOpen()` via `template(this.contentEl)`. If it uses
68
- `boundary()`/async components, add `@arrow-js/framework` to the plugin and the
69
- side-effect `import '@arrow-js/framework'`. Leave `src/sandbox/*` behind.
74
+ For the complete story authoring workflow (`defineStories` API, variants, children,
75
+ status flag, DRY patterns) see the `obsidian-arrow-stories` skill.
76
+
77
+ **Port a component into a plugin:** copy the component file and `src/utilities.css`
78
+ (once, shared by all ports) into the plugin. Mount from `ItemView.onOpen()` via
79
+ `template(this.contentEl)`. If it uses `boundary()`/async, add
80
+ `@arrow-js/framework` and `import '@arrow-js/framework'`. Leave `src/sandbox/*`
81
+ behind.
70
82
 
71
83
  ## Scaffold vs. clone
72
84
 
@@ -14,7 +14,7 @@
14
14
  "create:sync": "node create-obsidian-arrow/scripts/sync-template.mjs",
15
15
  "lint": "biome check .",
16
16
  "format": "biome check --write .",
17
- "test": "node --test test/*.test.mjs",
17
+ "test": "node --experimental-strip-types --test test/*.test.mjs",
18
18
  "check": "biome check --write . && pnpm typecheck && pnpm test",
19
19
  "ci": "biome ci . && pnpm typecheck && pnpm test && pnpm build",
20
20
  "skills:install": "node scripts/install-skills.mjs --force",
@@ -0,0 +1,11 @@
1
+ import { defineStories } from "../viewer/stories";
2
+ import { SettingsPanel } from "./SettingsPanel";
3
+
4
+ export default defineStories({
5
+ description: "Vertical tabs, toggles, a keyed list, and an async boundary() section.",
6
+ variants: {
7
+ default: () => SettingsPanel(),
8
+ },
9
+ children: ["toggle"],
10
+ status: "live",
11
+ });
@@ -70,7 +70,7 @@ function rebuildIndex(): void {
70
70
  * live state; clicking flips it in place (deep reactivity re-runs only the
71
71
  * tracked expressions below — no list re-render).
72
72
  */
73
- const Toggle = (enabled: () => boolean, onToggle: () => void): ArrowTemplate => html`<div
73
+ export const Toggle = (enabled: () => boolean, onToggle: () => void): ArrowTemplate => html`<div
74
74
  class="${() => `checkbox-container${enabled() ? " is-enabled" : ""}`}"
75
75
  @click="${onToggle}"
76
76
  >
@@ -0,0 +1,28 @@
1
+ import { reactive } from "@arrow-js/core";
2
+ import { defineStories } from "../viewer/stories";
3
+ import { Toggle } from "./SettingsPanel";
4
+
5
+ export default defineStories({
6
+ description: "Obsidian checkbox-container toggle used by SettingsPanel.",
7
+ componentPath: "src/components/SettingsPanel.ts",
8
+ variants: {
9
+ interactive: () => {
10
+ const state = reactive({ on: true });
11
+ return Toggle(
12
+ () => state.on,
13
+ () => {
14
+ state.on = !state.on;
15
+ }
16
+ );
17
+ },
18
+ off: {
19
+ render: () =>
20
+ Toggle(
21
+ () => false,
22
+ () => {}
23
+ ),
24
+ notes: "Static off state (click does nothing).",
25
+ },
26
+ },
27
+ status: "live",
28
+ });
@@ -4,6 +4,7 @@ import "@arrow-js/framework";
4
4
 
5
5
  import { startRouter } from "./router/client";
6
6
  import { applyTheme } from "./sandbox/theme";
7
+ import "./utilities.css";
7
8
  import "./sandbox/sandbox.css";
8
9
 
9
10
  applyTheme();
@@ -1,5 +1,7 @@
1
+ import { html } from "@arrow-js/core";
1
2
  import { Frame } from "../sandbox/frame";
2
3
  import { Shell } from "../sandbox/shell";
4
+ import type { Page } from "./routeToPage";
3
5
  import { routeToPage } from "./routeToPage";
4
6
 
5
7
  /**
@@ -29,10 +31,21 @@ function getNavigation(): NavigationLike | undefined {
29
31
 
30
32
  export function startRouter(root: HTMLElement): void {
31
33
  const render = (url: string): void => {
32
- const page = routeToPage(url);
34
+ let resolved = routeToPage(url);
35
+ for (let hops = 0; "redirect" in resolved && hops < 3; hops++) {
36
+ window.history.replaceState({}, "", resolved.redirect);
37
+ resolved = routeToPage(resolved.redirect);
38
+ }
39
+ if ("redirect" in resolved) {
40
+ return;
41
+ }
42
+ const page: Page = resolved;
33
43
  document.title = page.title;
34
44
  root.replaceChildren();
35
- Shell(Frame(page.title, page.view))(root);
45
+ const content = page.sidebar
46
+ ? html`${page.sidebar}${Frame(page.title, page.view)}`
47
+ : Frame(page.title, page.view);
48
+ Shell(content)(root);
36
49
  };
37
50
 
38
51
  render(window.location.href);
@@ -1,43 +1,33 @@
1
1
  import { html } from "@arrow-js/core";
2
2
  import type { ArrowExpression } from "@arrow-js/core";
3
- import { findExample } from "../examples/registry";
4
3
  import { Home } from "../sandbox/home";
4
+ import { ClassesPage } from "../viewer/ClassesPage";
5
+ import { ComponentsIndex } from "../viewer/ComponentsIndex";
6
+ import { StoryPage } from "../viewer/StoryPage";
7
+ import { TokensPage } from "../viewer/TokensPage";
8
+ import { findStory } from "../viewer/discovery";
9
+ import { ViewerSidebar } from "../viewer/sidebar";
5
10
 
6
11
  /**
7
- * Single route resolver, shared by every entry point. Returns the page status,
8
- * title (metadata), and Arrow view together the same shape the Arrow Vite
9
- * scaffold uses, so a future SSR/hydration lane can call this identically on
10
- * both server and client. The client router (./client.ts) wraps the view in the
11
- * sandbox Frame and sets document.title from this.
12
+ * Single route resolver, shared by every entry point (Arrow scaffold shape, so
13
+ * a future SSR lane could call it identically). Pages may carry a sidebar
14
+ * (rendered outside the pane) and routes may resolve to a redirect, which the
15
+ * client router applies via history.replaceState.
12
16
  */
13
17
  export interface Page {
14
18
  status: number;
15
19
  title: string;
16
20
  view: ArrowExpression;
21
+ sidebar?: ArrowExpression;
17
22
  }
18
23
 
19
- const APP_NAME = "Arrow Sandbox";
20
-
21
- export function routeToPage(url: string): Page {
22
- const { pathname } = new URL(url, window.location.origin);
23
-
24
- if (pathname === "/" || pathname === "") {
25
- return {
26
- status: 200,
27
- title: APP_NAME,
28
- view: Home(),
29
- };
30
- }
24
+ export interface Redirect {
25
+ redirect: string;
26
+ }
31
27
 
32
- const match = findExample(pathname);
33
- if (match) {
34
- return {
35
- status: 200,
36
- title: `${match.label} · ${APP_NAME}`,
37
- view: match.view(),
38
- };
39
- }
28
+ const APP_NAME = "Arrow Sandbox";
40
29
 
30
+ function notFound(pathname: string): Page {
41
31
  return {
42
32
  status: 404,
43
33
  title: `Not found · ${APP_NAME}`,
@@ -47,7 +37,7 @@ export function routeToPage(url: string): Page {
47
37
  <div class="setting-item-info">
48
38
  <div class="setting-item-name">Not found</div>
49
39
  <div class="setting-item-description">
50
- No route for <code>${pathname}</code>. <a href="/">Back to examples</a>.
40
+ No route for <code>${pathname}</code>. <a href="/">Back home</a>.
51
41
  </div>
52
42
  </div>
53
43
  </div>
@@ -55,3 +45,60 @@ export function routeToPage(url: string): Page {
55
45
  `,
56
46
  };
57
47
  }
48
+
49
+ export function routeToPage(url: string): Page | Redirect {
50
+ const { pathname, searchParams } = new URL(url, window.location.origin);
51
+
52
+ if (pathname === "/" || pathname === "") {
53
+ return { status: 200, title: APP_NAME, view: Home() };
54
+ }
55
+
56
+ if (pathname === "/example") {
57
+ return { redirect: "/components/settings-panel" };
58
+ }
59
+
60
+ if (pathname === "/components" || pathname === "/components/") {
61
+ return {
62
+ status: 200,
63
+ title: `Components · ${APP_NAME}`,
64
+ view: ComponentsIndex(),
65
+ sidebar: ViewerSidebar(pathname),
66
+ };
67
+ }
68
+
69
+ const storyMatch = pathname.match(/^\/components\/([^/]+)$/);
70
+ if (storyMatch) {
71
+ const story = findStory(storyMatch[1]);
72
+ if (!story) {
73
+ return { ...notFound(pathname), sidebar: ViewerSidebar(pathname) };
74
+ }
75
+ const requested = searchParams.get("variant");
76
+ const variantName = requested ?? Object.keys(story.variants)[0];
77
+ return {
78
+ status: story.variants[variantName] ? 200 : 404,
79
+ title: `${story.title} · ${APP_NAME}`,
80
+ view: StoryPage(story, variantName),
81
+ sidebar: ViewerSidebar(`/components/${story.slug}`),
82
+ };
83
+ }
84
+
85
+ if (pathname === "/reference") {
86
+ return {
87
+ status: 200,
88
+ title: `Tokens · ${APP_NAME}`,
89
+ view: TokensPage(),
90
+ sidebar: ViewerSidebar(pathname),
91
+ };
92
+ }
93
+
94
+ if (pathname === "/reference/classes") {
95
+ return {
96
+ status: 200,
97
+ title: `Classes · ${APP_NAME}`,
98
+ view: ClassesPage(),
99
+ sidebar: ViewerSidebar(pathname),
100
+ };
101
+ }
102
+
103
+ return notFound(pathname);
104
+ }
@@ -1,22 +1,12 @@
1
1
  import { component, html, reactive } from "@arrow-js/core";
2
2
  import type { ArrowTemplate } from "@arrow-js/core";
3
- import { ExamplesIndex } from "../examples/ExamplesIndex";
4
- import { examples } from "../examples/registry";
5
3
  import { layoutState } from "./layout";
6
4
  import { themeState } from "./theme";
7
5
 
8
- /**
9
- * Sandbox landing page at "/": a readiness check + getting-started commands +
10
- * the examples list. Sandbox chrome — does not port to a plugin.
11
- *
12
- * The readiness probe catches the #1 fresh-machine gotcha: running `pnpm dev`
13
- * before `pnpm pull-css` leaves app.css unloaded, so every `var(--…)` token is
14
- * empty. We detect that by reading the computed value of a known token.
15
- */
16
6
  const probe = reactive({ tick: 0 });
17
7
 
18
8
  function stylingLoaded(): boolean {
19
- const generation = probe.tick; // reactive dependency; Re-check bumps it
9
+ const generation = probe.tick;
20
10
  const style = getComputedStyle(document.body);
21
11
  return (
22
12
  generation >= 0 &&
@@ -29,6 +19,8 @@ function recheck(): void {
29
19
  probe.tick++;
30
20
  }
31
21
 
22
+ const gettingStarted = reactive({ expanded: false });
23
+
32
24
  const GETTING_STARTED = [
33
25
  { cmd: "pnpm pull-css", note: "extract Obsidian's app.css — run once (macOS auto-detect)" },
34
26
  { cmd: "pnpm dev", note: "this dev server (Vite + HMR)" },
@@ -36,9 +28,13 @@ const GETTING_STARTED = [
36
28
  { cmd: "pnpm run ci", note: "biome + typecheck + tests + build" },
37
29
  ];
38
30
 
31
+ const VIEWS = [
32
+ { label: "Components", path: "/components", note: "Component story viewer" },
33
+ { label: "Tokens", path: "/reference", note: "CSS custom property reference" },
34
+ { label: "Classes", path: "/reference/classes", note: "Obsidian class catalog" },
35
+ ];
36
+
39
37
  export const Home = component((): ArrowTemplate => {
40
- // Re-probe shortly after mount, in case app.css finished loading after the
41
- // first paint (stylesheets load async).
42
38
  setTimeout(recheck, 250);
43
39
 
44
40
  return html`
@@ -79,28 +75,61 @@ export const Home = component((): ArrowTemplate => {
79
75
  </div>
80
76
  </div>
81
77
  </div>
78
+ </div>
82
79
 
80
+ <div class="${() => (gettingStarted.expanded ? "oas-card is-expanded" : "oas-card")}">
81
+ <div
82
+ class="oas-card-header"
83
+ @click="${() => {
84
+ gettingStarted.expanded = !gettingStarted.expanded;
85
+ }}"
86
+ >
87
+ <span class="oas-card-title">Getting started</span>
88
+ <span class="oas-card-chevron">›</span>
89
+ </div>
90
+ <div class="oas-card-body">
91
+ <div class="oas-settings">
92
+ ${GETTING_STARTED.map((step) =>
93
+ html`
94
+ <div class="setting-item">
95
+ <div class="setting-item-info">
96
+ <div class="setting-item-name" style="font-family: var(--font-monospace);">
97
+ ${step.cmd}
98
+ </div>
99
+ <div class="setting-item-description">${step.note}</div>
100
+ </div>
101
+ </div>
102
+ `.key(step.cmd)
103
+ )}
104
+ </div>
105
+ <p class="oas-card-note">
106
+ See AGENTS.md + docs/ for the full flow; agent prompts in docs/prompts/.
107
+ </p>
108
+ </div>
109
+ </div>
110
+
111
+ <div class="oas-settings">
83
112
  <div class="setting-item setting-item-heading">
84
113
  <div class="setting-item-info">
85
- <div class="setting-item-name">Getting started</div>
86
- <div class="setting-item-description">
87
- See AGENTS.md + docs/ for the full flow; agent prompts in docs/prompts/.
88
- </div>
114
+ <div class="setting-item-name">Views</div>
115
+ <div class="setting-item-description">Main pages in this sandbox.</div>
89
116
  </div>
90
117
  </div>
91
- ${GETTING_STARTED.map((step) =>
118
+ ${VIEWS.map((view) =>
92
119
  html`
93
- <div class="setting-item">
94
- <div class="setting-item-info">
95
- <div class="setting-item-name" style="font-family: var(--font-monospace);">
96
- ${step.cmd}
97
- </div>
98
- <div class="setting-item-description">${step.note}</div>
120
+ <div class="setting-item">
121
+ <div class="setting-item-info">
122
+ <div class="setting-item-name">
123
+ <a href="${view.path}">${view.label}</a>
99
124
  </div>
125
+ <div class="setting-item-description">${view.note}</div>
100
126
  </div>
101
- `.key(step.cmd)
127
+ <div class="setting-item-control">
128
+ <a class="mod-cta oas-open-link" href="${view.path}">Open</a>
129
+ </div>
130
+ </div>
131
+ `.key(view.label)
102
132
  )}
103
133
  </div>
104
- ${ExamplesIndex(examples)}
105
134
  `;
106
135
  });