@beastjs/page-builder 0.0.0-stage → 0.1.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,17 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 — 2026-10-08
4
+
5
+ Initial release of `@beastjs/page-builder`.
6
+
7
+ - Development-only plugins for Vite, Rspack, and Rsbuild with configurable shortcuts, position, theme, scan duration, and launcher.
8
+ - Floating Shadow DOM widget opened with Cmd+B / Ctrl+B, with an entrance transition and hexagonal sonar scan.
9
+ - Searchable inventory of Beast pages and components, navigation menus, and tab groups.
10
+ - Item inspectors resolve TypeScript contracts, imported component props, required and optional fields, literal unions, and discriminated union variants. Inferred shapes are identified separately.
11
+ - Page scaffolding creates `src/pages/<Name>.btsx`, registers a lazy TanStack route in `src/router.ts`, and adds the corresponding navigation link.
12
+ - Blank and landing page starters, section insertion, source editing, Beast/Octane compilation, and a review of all proposed files before saving.
13
+ - Static BTSX menus and shared TypeScript menu arrays, including grouped data and configurable defaults for required fields.
14
+ - Reviewed saves check for stale source, stage all files, preserve permissions, and roll back on write failures. Success survives development-server reloads.
15
+ - Keyboard navigation, focus management, mobile layouts, and reduced-motion support.
16
+
17
+ This version scaffolds source and inspects source semantics. Computed values can remain unresolved; it does not provide visual drag-and-drop editing or whole-project TypeScript validation.
package/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026 phtn
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,201 @@
1
- # Temporary Holding Version
1
+ # Beast Page Builder
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
+ A floating page builder for Beast projects. Press **⌘B** on macOS or **Ctrl+B** elsewhere: the widget transitions in, scans your project with a hexagonal sonar animation, and opens a searchable list of pages, components, navigation menus, tabs, and actions. Creating a page scaffolds its BTSX file, registers its URL in `src/router.ts`, and adds its navigation link in one reviewed change.
4
+
5
+ The plugin runs on development servers only. The widget uses an isolated Shadow DOM and a small prebuilt browser bundle, so it requires no stylesheet rule or extra Octane runtime in the host app.
6
+
7
+ ## Try it locally
8
+
9
+ Requires Bun and Node.js 22.22.2 or newer.
10
+
11
+ ```sh
12
+ bun install
13
+ bun run dev
14
+ ```
15
+
16
+ Open `http://127.0.0.1:5190` and press **⌘B**, or click **Builder** in the bottom-right corner. The playground is a real Beast/Octane app; its `.btsx` files appear in the scan.
17
+
18
+ ## Add it to a Beast project
19
+
20
+ After the release is published to npm, install it in your Beast app:
21
+
22
+ ```sh
23
+ bun add -d @beastjs/page-builder
24
+ ```
25
+
26
+ To try the package locally before publishing, build and link it:
27
+
28
+ ```sh
29
+ # In beast-page-builder
30
+ bun run build
31
+ bun link
32
+
33
+ # In your Beast app
34
+ bun add -d link:@beastjs/page-builder
35
+ ```
36
+
37
+ ### Vite
38
+
39
+ ```ts
40
+ import { defineConfig } from 'vite'
41
+ import { beastOctane } from 'beast-tsrx/vite'
42
+ import { beastPageBuilder } from '@beastjs/page-builder/vite'
43
+
44
+ export default defineConfig({
45
+ plugins: [beastOctane(), beastPageBuilder()],
46
+ })
47
+ ```
48
+
49
+ The package root also exports the Vite plugin. HTML injection respects Vite's `base`.
50
+
51
+ ### Rspack
52
+
53
+ ```ts
54
+ import { beastOctane } from 'beast-tsrx/rspack'
55
+ import { beastPageBuilder } from '@beastjs/page-builder/rspack'
56
+
57
+ export default {
58
+ plugins: [beastOctane(), beastPageBuilder()],
59
+ }
60
+ ```
61
+
62
+ Run with `rspack serve`. When constructing `RspackDevServer` yourself, pass `compiler.options.devServer` so its plugin middleware is used. Only browser compilers receive the widget.
63
+
64
+ ### Rsbuild
65
+
66
+ ```ts
67
+ import { defineConfig } from '@rsbuild/core'
68
+ import { beastOctane } from 'beast-tsrx/rsbuild'
69
+ import { beastPageBuilder } from '@beastjs/page-builder/rsbuild'
70
+
71
+ export default defineConfig({
72
+ plugins: [...beastOctane(), beastPageBuilder()],
73
+ })
74
+ ```
75
+
76
+ The plugin runs with `rsbuild dev`, adding a pre-entry to browser environments.
77
+
78
+ ## Configure it
79
+
80
+ All three adapters accept the same options:
81
+
82
+ ```ts
83
+ beastPageBuilder({
84
+ include: ['src'],
85
+ pagesDir: 'src/pages',
86
+ routerFile: 'src/router.ts',
87
+ // Optional: choose the shared menu for new page links.
88
+ navigation: { file: 'src/lib/navs.ts', array: 'navGroups', group: 'Workspace' },
89
+ shortcut: 'Mod+B',
90
+ position: 'center',
91
+ theme: 'light',
92
+ scanDuration: 1600,
93
+ launcher: true,
94
+ })
95
+ ```
96
+
97
+ | Option | Default | Behavior |
98
+ | --- | --- | --- |
99
+ | `include` | `['src']` | Project-relative directories to scan recursively for `.btsx`, `.ts`, and `.tsx` sources |
100
+ | `pagesDir` | `'src/pages'` | Destination for new files; also included in the scan |
101
+ | `routerFile` | `'src/router.ts'` | Existing `@octanejs/tanstack-router` module with a root `addChildren([...])` route tree |
102
+ | `navigation` | Auto-detect | Optional menu file and selector/array/group; see below |
103
+ | `shortcut` | `'Mod+B'` | `Mod` means Command on macOS and Control elsewhere. Supports `Meta`, `Ctrl`, and optional `+Shift`, followed by a letter. |
104
+ | `position` | `'center'` | `'center'` or `'bottom-right'` |
105
+ | `theme` | `'light'` | `'light'`, `'dark'`, or `'auto'` |
106
+ | `scanDuration` | `1600` | Minimum sonar duration, in milliseconds, from 0 to 10,000. Reduced motion skips this delay. |
107
+ | `launcher` | `true` | Show the floating Builder button |
108
+
109
+ ⌘B/Ctrl+B is intercepted while the plugin is active, including in text fields. Choose a different shortcut if it conflicts with your app. The optional launcher is always another way to open the widget.
110
+
111
+ ## Build a page
112
+
113
+ 1. Open the widget. It requests a fresh inventory from the dev server.
114
+ 2. Search or filter **Pages** and **Components**. Open a result to inspect or copy its source. Use **Navs** and **Tabs** to inspect groups, item types, and data bindings. Compiler diagnostics stay visible for broken files.
115
+ 3. Choose **Create a new page**. Name it in PascalCase, such as `PricingPage`.
116
+ 4. Set the **Page URL** and **Navigation label**, and choose a menu if several are available. These default to `/pricing` and `Pricing` for `PricingPage`.
117
+ 5. Start with a blank page or landing page. Add sections and edit the BTSX source. Choose **Review page** to inspect all three proposed files: the new page, router, and navigation.
118
+ 6. Choose **Create page**. The server saves the reviewed files together. Use **Open page** or the new nav link to visit the URL.
119
+
120
+ Use the arrow keys and Enter to navigate search results. Escape, the close button, the backdrop, or the shortcut closes the widget. Focus returns to the element that opened it. Drafts stay in memory when you close the widget; they reset on reload. Template buttons replace the current draft source.
121
+
122
+ Pages are classified by `pagesDir`, a `pages/` or `routes/` folder, or a component name ending in `Page`. Other Beast files appear as components. Dependency, generated, hidden, and symlinked entries are skipped. Use **Rescan project** after filesystem changes; there is no background file watcher in this version.
123
+
124
+ ## Router and navigation integration
125
+
126
+ The host app must already render a TanStack `RouterProvider` and use an app shell with an `Outlet`. The included playground demonstrates this setup. The builder edits `src/router.ts` by default. It adds a lazy `createRoute`, using the existing root route, and appends it to `rootRoute.addChildren([...])`. It preserves aliased imports, existing routes, comments, and the common `lazyPage` wrapper. It refuses duplicate URLs and computed route trees/paths that cannot be checked safely. If the router file is missing or uses another router, review explains what needs configuring before any files are written.
127
+
128
+ Without a `navigation` option, the scanner offers static `nav` blocks in component BTSX files and exported literal menu arrays with `href`/`to` fields in any scanned TypeScript module. Page-local menus and tab lists remain visible in the inventory but are excluded from automatic page-link insertion. If there is more than one target, choose **Add link to** in the draft.
129
+
130
+ For a static BTSX menu:
131
+
132
+ ```ts
133
+ navigation: { file: 'src/components/Header.btsx', selector: 'nav.workspace-nav' }
134
+ ```
135
+
136
+ The new link uses `Link(to=...)` when the menu uses TanStack links, or `a(href=...)` for a menu using anchors. The selector is the complete `nav` selector, including its ID and classes; omit it when the file contains one editable nav. Dynamic loops are edited through their data file instead.
137
+
138
+ For shared menu data, including menus used by desktop and mobile sidebars:
139
+
140
+ ```ts
141
+ navigation: {
142
+ file: 'src/lib/navs.ts',
143
+ array: 'navGroups',
144
+ group: 'Workspace',
145
+ itemDefaults: { icon: 'folder' },
146
+ }
147
+ ```
148
+
149
+ Supported exported arrays are flat literal items or literal groups with `title`/`label` and `items`. New items follow the acquired contract and existing schema (`href` or `to`, `label` or `title`, and optional `icon`, `value`, `id`, `short`, `description`, `tags`, `disabled`). An icon defaults to the existing first item's icon; override it with a valid icon from your project. Supply `itemDefaults` for additional fields your menu requires. Existing items and other groups are preserved. Required fields from declared types are included even in empty typed arrays. Simple field types and literal unions are checked against the proposed values; optional fields are omitted unless present in the existing template or supplied in defaults. Computed arrays and spreads are inspectable but cannot be edited automatically.
150
+
151
+ ## Navigation, tabs, and item types
152
+
153
+ The scan detects BTSX `nav`, navigation/menu roles, tablist/tab roles, and common `Nav`, `Navigation`, `SidebarNav`, `MenuList`, `TabsList`, and `TabsTrigger` components, including named import aliases. It also discovers shared arrays by link fields, menu/tab naming, or use in an `each` loop or `items`/`tabs`/`links` binding. Loops connect rendered groups to their source arrays, including aliased imports and grouped menus. Custom components with these semantics can expose them using ARIA roles.
154
+
155
+ Open a result in **Navs** or **Tabs** to see:
156
+
157
+ - Each item's kind: internal link, external link, anchor, action, or tab.
158
+ - Its TypeScript type and fields, with required/optional status and literal unions.
159
+ - Distinct item variants in a discriminated union, and the type of each individual item.
160
+ - Imported component prop contracts, shared data bindings, and the files that render them.
161
+
162
+ The TypeScript checker resolves imported types and the host project's `tsconfig.json` aliases. BTSX module/setup declarations participate in type discovery. Explicit contracts are marked **Declared type**; untyped data and static HTML attributes are marked **Inferred shape**. Typed computed collections expose their contracts with unknown runtime values. Scanning parses source without executing host modules.
163
+
164
+ Detection is based on source semantics, not the browser DOM. Runtime-generated labels, destinations, or items may remain unresolved, and arbitrary custom component names need recognizable roles. The `navigation` option restricts where new page links are inserted; it does not hide other menus or tabs from inspection. Tab groups are inspected as controls and are not page-link destinations.
165
+
166
+ ## Scope of this version
167
+
168
+ This is a source-based page scaffolder. Review shows BTSX source, not a rendered page. It does not modify existing pages, arrange arbitrary project components visually, generate content with AI, or generate new router infrastructure. Compilation validates Beast and Octane syntax; it does not type-check imports or props against the whole host project. Run the host app's normal checks after creating a page.
169
+
170
+ The API lives under `/__beast-page-builder`. Reads and writes reject cross-site requests. Writes require same-origin JSON, bounded request bodies, valid page names, and project-contained paths. The review hash includes every original and proposed file. Saving rechecks the router and menu, stages all complete files, then commits them, with rollback on a write failure. Existing page files are never overwritten. A changed draft, router, or menu requires another review; concurrent saves through one project instance are serialized. If an external edit prevents rollback, the error identifies the files to inspect.
171
+
172
+ ## Develop and verify
173
+
174
+ ```sh
175
+ bun run check
176
+ bunx playwright install chromium # First browser-test run
177
+ bun run test:browser
178
+ ```
179
+
180
+ `bun run build` produces the Node plugin adapters, public types, and the standalone widget in `dist/`. Restart the playground after changing the plugin or widget source.
181
+
182
+ ```text
183
+ vite.ts / rspack.ts / rsbuild.ts Bundler adapters
184
+ server/ Project scan, route/menu edits, reviewed saves, JSON API
185
+ shared/ Public configuration and wire types
186
+ client/ Widget, sonar animation, page starters
187
+ playground/ Beast app for trying the plugin
188
+ tests/ Project, API, bundler, and browser checks
189
+ ```
190
+
191
+ ## Release and publish
192
+
193
+ Release artifacts are attached to the corresponding [GitHub release](https://github.com/beastjs/page-builder/releases). The package is published to npm manually.
194
+
195
+ To publish the verified tarball from a release:
196
+
197
+ ```sh
198
+ npm publish ./releases/beastjs-page-builder-0.1.0.tgz --access public
199
+ ```
200
+
201
+ For a future release, update the package version and changelog, run `bun run check` and `bun run test:browser`, then run `npm pack --pack-destination releases`. `prepack` builds the distribution so each tarball includes the adapters, declarations, and browser widget. Publishing from the repository also runs `prepublishOnly` checks. Check the packed file list with `bun run pack:check`.
package/dist/client.js ADDED
@@ -0,0 +1,234 @@
1
+ var le=`:host { all: initial; color-scheme: light; }
2
+ *, *::before, *::after { box-sizing: border-box; }
3
+ .widget { --surface: #fff; --soft: #f5f5f5; --text: #252527; --muted: #939397; --line: #ededee; --hover: #f3f3f4; --blue: #477eea; --blue-soft: #edf3ff; font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: var(--text); letter-spacing: -.02em; }
4
+ .widget[data-theme="dark"] { --surface: #202124; --soft: #27292d; --text: #eeeeef; --muted: #97999f; --line: #33353a; --hover: #2d3036; --blue: #8eafff; --blue-soft: #2b354c; color-scheme: dark; }
5
+ @media (prefers-color-scheme: dark) { .widget[data-theme="auto"] { --surface: #202124; --soft: #27292d; --text: #eeeeef; --muted: #97999f; --line: #33353a; --hover: #2d3036; --blue: #8eafff; --blue-soft: #2b354c; color-scheme: dark; } }
6
+ [hidden] { display: none !important; }
7
+ button, input, textarea, summary { font: inherit; letter-spacing: inherit; }
8
+ select { font: inherit; color: inherit; }
9
+ .route-fields { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 16px; }
10
+ .text-field { width: 100%; min-width: 0; padding: 10px 12px; background: var(--soft); color: var(--text); border: 1px solid var(--line); border-radius: 9px; font: inherit; }
11
+ .planned-file { margin: 0 0 14px; }
12
+ .planned-file > summary { display: flex; align-items: center; gap: 8px; padding: 10px 2px; background: transparent; overflow-wrap: anywhere; }
13
+ .planned-file > summary svg { width: 15px; height: 15px; }
14
+ .planned-file > summary small { margin-left: auto; white-space: nowrap; color: var(--blue); }
15
+ .original-file { margin-top: 8px; }
16
+ .original-file > summary { width: fit-content; margin-bottom: 8px; }
17
+ .success-body .visit { text-decoration: none; margin-bottom: 12px; }
18
+ button { cursor: pointer; color: inherit; border: 0; background: transparent; }
19
+ button:disabled { opacity: .5; cursor: wait; }
20
+ button:focus-visible, input:focus-visible, textarea:focus-visible, summary:focus-visible, pre:focus-visible { outline: 2px solid var(--blue); outline-offset: 3px; }
21
+ svg { width: 20px; height: 20px; flex-shrink: 0; }
22
+ kbd { display: inline-grid; place-items: center; min-width: 23px; padding: 2px 5px; border-radius: 5px; background: var(--soft); font: 12px/1.5 inherit; color: var(--muted); }
23
+ .launcher { position: fixed; z-index: 2147483600; bottom: 24px; right: 24px; background: var(--surface); border: 1px solid var(--line); border-radius: 13px; box-shadow: 0 4px 24px #00000012, 0 1px 3px #0000000a; display: flex; align-items: center; gap: 9px; padding: 11px 13px; transition: transform .2s, box-shadow .2s; }
24
+ .launcher:hover { transform: translateY(-2px); box-shadow: 0 8px 28px #00000018; }
25
+ .launcher > svg { color: var(--blue); width: 19px; }
26
+ .launcher kbd { margin-left: 5px; }
27
+ .backdrop { position: fixed; inset: 0; z-index: 2147483601; background: #15151a19; backdrop-filter: blur(3px); animation: fade-in .18s ease-out; }
28
+ .panel { position: fixed; z-index: 2147483602; top: min(18vh, 140px); left: 50%; width: min(740px, calc(100vw - 40px)); max-height: calc(100dvh - min(18vh, 140px) - 28px); transform: translateX(-50%); display: flex; flex-direction: column; overflow: hidden; background: var(--surface); border: 1px solid #ffffff80; border-radius: 22px; box-shadow: 0 28px 100px #0000001c, 0 8px 28px #0000000f, 0 1px 4px #00000012; animation: panel-in .32s cubic-bezier(.16, 1, .3, 1); }
29
+ .widget[data-position="bottom-right"] .panel { top: auto; bottom: 24px; left: auto; right: 24px; transform: none; max-height: calc(100dvh - 48px); animation-name: corner-in; }
30
+ .panel:focus { outline: none; }
31
+ .icon-button { width: 30px; height: 30px; display: grid; place-items: center; border-radius: 50%; color: var(--muted); flex-shrink: 0; }
32
+ .icon-button:hover { background: var(--hover); color: var(--text); }
33
+ .close { background: var(--soft); }
34
+ .scan-header, .topbar { display: flex; align-items: center; gap: 14px; padding: 22px 24px; border-bottom: 1px solid var(--line); min-height: 78px; }
35
+ .brand { display: flex; gap: 9px; align-items: center; font-weight: 550; flex: 1; }
36
+ .brand > svg { color: var(--blue); width: 22px; height: 22px; }
37
+ .muted { color: var(--muted); font-weight: 400; margin-left: 3px; }
38
+ .scan-body { position: relative; display: flex; flex-direction: column; align-items: center; text-align: center; padding: 16px 20px 40px; }
39
+ .sonar { position: relative; width: 344px; max-width: 100%; height: 180px; margin: 6px 0 20px; mask-image: radial-gradient(ellipse, black 30%, transparent 75%); }
40
+ .sonar > svg { width: 100%; height: 100%; }
41
+ .sonar polygon { fill: #a7c7ff; stroke: var(--surface); stroke-width: 2; opacity: .16; animation: sonar-wave 2.4s ease-in-out infinite; animation-delay: var(--delay); }
42
+ .sonar-core { position: absolute; top: 65px; left: calc(50% - 23px); display: grid; place-items: center; height: 46px; width: 46px; background: var(--surface); border: 1px solid var(--line); border-radius: 13px; color: var(--blue); box-shadow: 0 4px 12px #477eea14; }
43
+ .sonar-core svg { width: 26px; height: 26px; }
44
+ .eyebrow { display: block; font-size: 10px; font-weight: 550; letter-spacing: .15em; color: var(--muted); }
45
+ .scan-body h2, .success-body h2, .error-body h2 { font-size: 23px; font-weight: 500; letter-spacing: -.04em; margin: 11px 0 7px; }
46
+ .scan-body p { color: var(--muted); font-size: 13px; margin: 0; }
47
+ .scan-progress { width: 120px; height: 3px; background: var(--blue-soft); border-radius: 8px; margin: 24px 0 13px; overflow: hidden; }
48
+ .scan-progress i { display: block; width: 45px; height: 100%; background: #91b6fa; border-radius: inherit; animation: scan-progress 1.5s ease-in-out infinite; }
49
+ .scan-note { font-size: 11px; color: var(--muted); }
50
+ .searchbar { display: flex; align-items: center; gap: 13px; padding: 20px 22px; min-height: 83px; border-bottom: 1px solid var(--line); }
51
+ .searchbar > svg { color: var(--muted); }
52
+ .project-chip { display: flex; align-items: center; gap: 6px; background: var(--soft); border: 1px solid var(--line); border-radius: 20px; padding: 6px 11px; font-size: 12px; white-space: nowrap; max-width: 200px; overflow: hidden; text-overflow: ellipsis; }
53
+ .project-chip svg { width: 15px; height: 15px; color: var(--blue); }
54
+ .search { width: 0; min-width: 60px; flex: 1; border: 0; background: transparent; color: var(--text); padding: 5px 0; font-size: 16px; outline: none; }
55
+ .search::placeholder { color: var(--muted); }
56
+ .search:focus-visible { outline: none; }
57
+ .tabs { display: flex; align-items: center; gap: 7px; padding: 13px 22px; border-bottom: 1px solid var(--line); }
58
+ .tab { padding: 7px 12px; color: var(--muted); border-radius: 9px; }
59
+ .tab.active { background: var(--soft); color: var(--text); }
60
+ .tab:hover { color: var(--text); }
61
+ .ready-status { display: flex; align-items: center; gap: 6px; margin-left: auto; color: var(--muted); font-size: 11px; }
62
+ .ready-status i { width: 5px; height: 5px; background: #83ad8a; border-radius: 50%; box-shadow: 0 0 0 3px #83ad8a12; }
63
+ .results { padding: 12px; min-height: 255px; max-height: min(390px, 46vh); overflow-y: auto; overscroll-behavior: contain; }
64
+ .group-label { font-size: 12px; font-weight: 400; color: var(--muted); padding: 0 10px; margin: 12px 0 7px; }
65
+ .group-label:not(:first-child) { margin-top: 21px; }
66
+ .result { display: flex; align-items: center; width: 100%; text-align: left; gap: 13px; padding: 12px 13px; min-height: 48px; border-radius: 11px; }
67
+ .result > svg { color: var(--muted); width: 19px; height: 19px; }
68
+ .result.selected, .result:hover { background: var(--hover); }
69
+ .result-name { font-size: 15px; }
70
+ .result-detail { margin-left: auto; color: var(--muted); font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 45%; }
71
+ .result-enter { color: var(--muted); font-size: 16px; width: 12px; visibility: hidden; }
72
+ .result.selected .result-enter { visibility: visible; }
73
+ .footer { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding: 16px 23px; border-top: 1px solid var(--line); color: var(--muted); font-size: 11px; background: color-mix(in srgb, var(--soft) 45%, var(--surface)); }
74
+ .footer span { display: flex; align-items: center; gap: 6px; }
75
+ .footer kbd:nth-of-type(3) { margin-left: 12px; }
76
+ .header-text { flex: 1; min-width: 0; }
77
+ .header-text h2 { font-size: 17px; font-weight: 500; margin: 0; }
78
+ .header-text p { color: var(--muted); font-size: 11px; margin: 2px 0 0; overflow-wrap: anywhere; }
79
+ .draft-body, .preview-body { padding: 24px; min-height: 0; overflow-y: auto; }
80
+ .field-label { display: block; font-size: 12px; font-weight: 500; margin-bottom: 8px; }
81
+ .name-field { display: flex; align-items: center; background: var(--soft); border: 1px solid var(--line); border-radius: 10px; padding: 10px 13px; }
82
+ .name-field input { flex: 1; min-width: 0; border: 0; background: transparent; color: var(--text); outline: none; }
83
+ .name-field:focus-within { border-color: var(--blue); }
84
+ .name-field input:focus-visible { outline: none; }
85
+ .name-field span { color: var(--muted); font-size: 12px; }
86
+ .hint { font-size: 11px; color: var(--muted); margin: 8px 0 19px; }
87
+ .editor-toolbar { display: flex; align-items: center; gap: 5px; margin: 24px 0 10px; }
88
+ .editor-toolbar .field-label { margin: 0 7px 0 0; }
89
+ .editor-toolbar button, summary { padding: 5px 8px; background: var(--soft); border-radius: 6px; font-size: 11px; cursor: pointer; color: var(--muted); }
90
+ .editor-toolbar button:hover, summary:hover { color: var(--text); }
91
+ .editor-toolbar details { margin-left: auto; position: relative; }
92
+ summary { list-style: none; }
93
+ summary::-webkit-details-marker { display: none; }
94
+ .section-menu { position: absolute; right: 0; top: 30px; background: var(--surface); padding: 6px; border-radius: 9px; border: 1px solid var(--line); box-shadow: 0 8px 24px #00000012; width: 150px; z-index: 1; }
95
+ .section-menu button { display: block; width: 100%; text-align: left; padding: 9px; background: transparent; }
96
+ .section-menu button:hover { background: var(--hover); }
97
+ .source-editor, .source-preview { font: 12px/1.8 "SFMono-Regular", Consolas, monospace; letter-spacing: 0; color: var(--text); background: var(--soft); border: 1px solid var(--line); border-radius: 10px; padding: 15px; width: 100%; tab-size: 2; }
98
+ .source-editor { height: 260px; display: block; resize: vertical; max-height: 44vh; min-height: 110px; }
99
+ .source-preview { max-height: 350px; overflow: auto; margin: 0; white-space: pre; }
100
+ .source-preview code { font: inherit; }
101
+ .action-footer { display: flex; align-items: center; justify-content: space-between; padding: 18px 24px; border-top: 1px solid var(--line); gap: 15px; }
102
+ .action-footer > span { display: flex; align-items: center; gap: 7px; font-size: 11px; color: var(--muted); }
103
+ .action-footer > span svg { width: 15px; }
104
+ .primary, .secondary { display: inline-flex; align-items: center; justify-content: center; gap: 14px; padding: 11px 16px; border-radius: 10px; background: var(--blue-soft); color: var(--blue); font-size: 12px; font-weight: 500; }
105
+ .primary:hover { filter: brightness(.97); }
106
+ .primary svg, .secondary svg { width: 16px; height: 16px; }
107
+ .secondary { background: var(--soft); color: var(--text); }
108
+ .validation { display: flex; align-items: center; gap: 7px; color: #74a27d; font-size: 11px; margin-bottom: 15px; }
109
+ .validation svg { width: 15px; height: 15px; }
110
+ .preview-body > .hint { margin: 16px 0 0; }
111
+ .message { color: var(--muted); font-size: 11px; overflow-wrap: anywhere; white-space: pre-wrap; max-height: 100px; overflow: auto; }
112
+ .message:not(:empty) { padding: 10px 0 0; }
113
+ .panel[data-view="palette"] > .message:not(:empty) { padding: 0 24px 15px; }
114
+ .error-note { color: #c97979; font-size: 12px; overflow-wrap: anywhere; }
115
+ .empty-state, .success-body, .error-body { display: flex; flex-direction: column; align-items: center; text-align: center; padding: 48px 22px; }
116
+ .empty-state > svg { width: 26px; height: 26px; color: var(--muted); margin-bottom: 12px; }
117
+ .empty-state h3 { font-weight: 500; margin: 0; }
118
+ .empty-state p, .success-body p, .error-body p { color: var(--muted); font-size: 13px; margin: 8px 0 23px; }
119
+ .success-body { padding: 44px 20px 56px; }
120
+ .success-icon { display: grid; place-items: center; width: 50px; height: 50px; background: #82ad8912; border-radius: 16px; color: #82ad89; margin-bottom: 25px; }
121
+ .success-icon svg { width: 25px; height: 25px; }
122
+ .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; }
123
+ @keyframes panel-in { from { opacity: 0; transform: translate(-50%, 14px) scale(.97); } to { opacity: 1; transform: translate(-50%, 0) scale(1); } }
124
+ @keyframes corner-in { from { opacity: 0; transform: translateY(14px) scale(.97); } to { opacity: 1; transform: none; } }
125
+ @keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
126
+ @keyframes sonar-wave { 0%, 100% { opacity: .08; } 35% { opacity: var(--cell-opacity); fill: #80aafa; } 65% { opacity: .16; } }
127
+ @keyframes scan-progress { 0% { transform: translateX(-45px); } 100% { transform: translateX(125px); } }
128
+ @media (max-width: 580px) {
129
+ .panel { width: calc(100vw - 24px); top: 7vh; max-height: 86dvh; border-radius: 18px; }
130
+ .widget[data-position="bottom-right"] .panel { right: 12px; bottom: 12px; }
131
+ .searchbar { flex-wrap: wrap; gap: 10px; padding: 15px; }
132
+ .project-chip { max-width: calc(100% - 90px); }
133
+ .searchbar .close { margin-left: auto; }
134
+ .search { order: 4; flex-basis: 100%; width: 100%; font-size: 15px; margin: 3px 0; }
135
+ .tabs { padding: 10px; gap: 2px; }
136
+ .tab { padding: 7px 10px; font-size: 12px; }
137
+ .ready-status { font-size: 10px; margin-right: 4px; }
138
+ .result-detail { max-width: 35%; }
139
+ .footer { padding: 14px; }
140
+ .scan-body h2 { font-size: 20px; }
141
+ .draft-body, .preview-body { padding: 18px; }
142
+ .action-footer { padding: 16px; }
143
+ .action-footer > span { max-width: 40%; }
144
+ .editor-toolbar { flex-wrap: wrap; }
145
+ .editor-toolbar details { margin-left: 0; }
146
+ }
147
+ @media (prefers-reduced-motion: reduce) {
148
+ *, *::before, *::after { animation: none !important; transition: none !important; scroll-behavior: auto !important; }
149
+ .sonar polygon { opacity: .3; }
150
+ }
151
+
152
+ /* Collection contracts share the palette's quiet source-inspection treatment. */
153
+ .collection-meta { display:flex; gap:12px; align-items:center; color:var(--muted); font-size:12px; }
154
+ .collection-body h3 { margin:22px 0 10px; font-size:13px; font-weight:600; }
155
+ .type-contract { border:1px solid var(--line); border-radius:10px; overflow:hidden; margin:10px 0; }
156
+ .type-title { display:flex; align-items:flex-start; justify-content:space-between; gap:12px; padding:12px; font-size:12px; }
157
+ .type-title code { overflow-wrap:anywhere; white-space:pre-wrap; }
158
+ .type-badge { display:inline-block; flex-shrink:0; padding:4px 7px; border-radius:5px; background:var(--hover); color:var(--muted); font-size:10px; white-space:nowrap; }
159
+ .fields-scroll { overflow:auto; max-height:260px; }
160
+ .type-fields { width:100%; border-collapse:collapse; font-size:11px; text-align:left; }
161
+ .type-fields th { color:var(--muted); font-weight:500; }
162
+ .type-fields th, .type-fields td { padding:9px 12px; border-top:1px solid var(--line); vertical-align:top; }
163
+ .type-fields td:nth-child(2) { min-width:160px; overflow-wrap:anywhere; }
164
+ .type-variants { padding:10px 12px; border-top:1px solid var(--line); font-size:12px; }
165
+ .consumer-list { padding-left:18px; color:var(--muted); font-size:12px; }
166
+ .consumer-list li { margin:8px 0; overflow-wrap:anywhere; }
167
+ .detected-item { border-bottom:1px solid var(--line); }
168
+ .detected-item > summary { display:flex; align-items:center; justify-content:space-between; gap:12px; padding:12px 0; cursor:pointer; font-size:13px; }
169
+ .detected-item > summary::before { content:'\u203A'; color:var(--muted); }
170
+ .detected-item[open] > summary::before { content:'\u2304'; }
171
+ .detected-item > summary > span:first-of-type { flex:1; }
172
+ .item-body { padding-bottom:8px; }
173
+ @media (max-width:600px) { .tabs { flex-wrap:wrap; gap:3px; } .tabs .tab { padding:8px 10px; } .tabs .ready-status { margin-left:auto; font-size:10px; } .type-title { flex-direction:column; } }
174
+ `;var R="/__beast-page-builder";var O={blank:`main.page
175
+ h1 A new page
176
+ p Make something worth visiting.
177
+ `,landing:`main.page
178
+ nav.page-nav
179
+ a.brand(href="/") Studio
180
+ a(href="#contact") Let\u2019s talk \u2197
181
+ section.hero
182
+ p.eyebrow MADE WITH BEAST
183
+ h1 Your next big idea starts here.
184
+ p.description A little imagination. A space to build. Everything you need to make something great.
185
+ a.page-button(href="#features") Explore what\u2019s possible \u2192
186
+ section.features#features
187
+ article.feature
188
+ span 01
189
+ h2 Start with a spark
190
+ p Turn a fresh idea into your next favorite project.
191
+ article.feature
192
+ span 02
193
+ h2 Make it yours
194
+ p Build with small pieces that fit beautifully together.
195
+ article.feature
196
+ span 03
197
+ h2 Put it out there
198
+ p Good things are made to be shared with the world.
199
+ footer#contact
200
+ p Have something in mind?
201
+ a(href="mailto:hello@example.com") Let\u2019s make it happen \u2197
202
+
203
+ style
204
+ .page { max-width: 1080px; margin: auto; padding: 40px; font-family: system-ui, sans-serif; color: #222; }
205
+ .page-nav { display: flex; justify-content: space-between; gap: 24px; align-items: center; }
206
+ .page a { color: inherit; text-decoration: none; }
207
+ .brand { font-size: 22px; font-weight: 650; }
208
+ .hero { padding: 100px 0 80px; max-width: 750px; }
209
+ .eyebrow { color: #777; font-size: 11px; letter-spacing: .18em; }
210
+ .hero h1 { font-size: clamp(40px, 6vw, 72px); letter-spacing: -.055em; line-height: 1.05; margin: 28px 0; }
211
+ .description { max-width: 470px; color: #777; line-height: 1.7; }
212
+ .page-button { display: inline-block; margin-top: 24px; background: #edf3ff; padding: 16px 24px; border-radius: 12px; }
213
+ .features { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 36px; padding: 40px 0 80px; border-top: 1px solid #eee; }
214
+ .feature span { color: #999; font-size: 12px; }
215
+ .feature h2 { font-size: 20px; font-weight: 550; letter-spacing: -.03em; }
216
+ .feature p, footer p { color: #777; line-height: 1.7; }
217
+ .page footer { border-top: 1px solid #eee; padding: 32px 0; }
218
+ `},J={Hero:`
219
+ section.hero
220
+ p.eyebrow INTRODUCING
221
+ h1 Build something remarkable.
222
+ p.description Your idea, brought to life.
223
+ `,Features:`
224
+ section.features
225
+ article.feature
226
+ h2 Made for you
227
+ p Thoughtful details. Endless possibilities.
228
+ `,"Call to action":`
229
+ section
230
+ h2 Ready to get started?
231
+ a.page-button(href="/contact") Let\u2019s talk \u2192
232
+ `};var pe="beast-page-builder",de=c=>"/"+(c.replace(/Page$/,"").replace(/([a-z0-9])([A-Z])/g,"$1-$2").replace(/([A-Z])([A-Z][a-z])/g,"$1-$2").toLowerCase()||"page"),ce=c=>c.replace(/Page$/,"").replace(/([a-z0-9])([A-Z])/g,"$1 $2"),we={shortcut:"Mod+B",position:"center",theme:"light",scanDuration:1600,launcher:!0,pagesDir:"src/pages",routerFile:"src/router.ts"},r=c=>c.replace(/[&<>"']/g,u=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[u]),ue={hex:'<path d="m12 2 8.7 5v10L12 22l-8.7-5V7Z"/><path d="m3.3 7 8.7 5 8.7-5M12 12v10"/>',search:'<circle cx="10.5" cy="10.5" r="6.5"/><path d="m16 16 4 4"/>',plus:'<path d="M12 5v14M5 12h14"/>',page:'<path d="M14 3H6v18h12V7Zm0 0v5h5M9 12h6M9 16h6"/>',component:'<rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="3" width="7" height="7" rx="1.5"/><rect x="3" y="14" width="7" height="7" rx="1.5"/><rect x="14" y="14" width="7" height="7" rx="1.5"/>',scan:'<path d="M4 8V4h4m8 0h4v4m0 8v4h-4m-8 0H4v-4M4 12h16"/>',close:'<path d="m6 6 12 12M18 6 6 18"/>',arrow:'<path d="M5 12h14m-6-6 6 6-6 6"/>',back:'<path d="M19 12H5m6-6-6 6 6 6"/>',check:'<path d="m5 12 4 4L19 6"/>'},p=c=>`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${ue[c]??ue.hex}</svg>`;function $e(){let c=[];for(let u=0;u<9;u++)for(let H=0;H<15;H++){let a=12+H*22+u%2*11,b=12+u*19,w=Math.hypot(a-171,b-88),D=Math.max(.12,.85-w/210);c.push(`<polygon points="${a},${b-12} ${a+10},${b-6} ${a+10},${b+6} ${a},${b+12} ${a-10},${b+6} ${a-10},${b-6}" style="--delay:${Math.round(w*5)}ms;--cell-opacity:${D}"/>`)}return`<div class="sonar"> <svg viewBox="0 0 344 180" aria-hidden="true">${c.join("")}</svg><div class="sonar-core">${p("hex")}</div></div>`}function ge(){if(document.getElementById(pe))return;let c=document.createElement("div");c.id=pe,document.body.append(c);let u=c.attachShadow({mode:"open"});u.innerHTML=`<style>${le}</style><div class="widget" data-theme="light" data-position="center"><button class="launcher" aria-label="Open Beast page builder">${p("hex")}<span>Builder</span><kbd>\u2318 B</kbd></button><div class="backdrop" hidden></div><section class="panel" role="dialog" aria-modal="true" aria-label="Beast page builder" hidden></section></div>`;let H=u.querySelector(".widget"),a=u.querySelector(".panel"),b=u.querySelector(".backdrop"),w=u.querySelector(".launcher"),D=new URL(import.meta.url),K=D.pathname.endsWith(`${R}/client.js`)?D.pathname.replace(/\/client\.js$/,""):R,U=`beast-page-builder:save:${K}`;function M(e){try{return e===""?sessionStorage.removeItem(U):e&&sessionStorage.setItem(U,e),sessionStorage.getItem(U)}catch{return null}}let L=we,m=null,h=!1,o="palette",v="All",j="",$=0,Q=null,I=null,f=0,i={name:"UntitledPage",source:O.blank},s=null,ee=null,C=null,Y="",x=!1,g="",Z,te=/Mac|iPhone|iPad/.test(navigator.platform),me=()=>L.shortcut.replace("Mod",te?"\u2318":"Ctrl").replace("Meta","\u2318").replaceAll("+"," ");async function P(e,t,l){let T=await fetch(`${K}${e}`,{signal:l,...t===void 0?{}:{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)}}),y=await T.json();if(!T.ok)throw new Error(y.error??`Request failed (${T.status})`);return y}function G(){H.dataset.theme=L.theme,H.dataset.position=L.position,w.hidden=!L.launcher,w.querySelector("kbd").textContent=me()}let ae=P("/config").then(e=>{L=e,G()});ae.catch(()=>{}),G();let he=()=>r(m?.name??"Your project"),V=()=>`<button class="icon-button close" aria-label="Close builder">${p("close")}</button>`,z=(e,t="")=>`<header class="topbar"><button class="icon-button back" aria-label="Back to builder">${p("back")}</button><div class="header-text"><h2>${e}</h2>${t?`<p>${t}</p>`:""}</div>${V()}</header>`,B=()=>'<footer class="footer"><span><kbd>\u2191</kbd><kbd>\u2193</kbd> Navigate <kbd>\u21B5</kbd> Select</span><span>Close <kbd>Esc</kbd></span></footer>',X=e=>{g=e;let t=a.querySelector(".message");t&&(t.textContent=e)};function d(){if(a.dataset.view=o,o==="scan")a.innerHTML=`<header class="scan-header"><span class="brand">${p("hex")} Beast <span class="muted">/ Page builder</span></span>${V()}</header><div class="scan-body">${$e()}<span class="eyebrow">CONNECTING THE DOTS</span><h2>Getting to know your project</h2><p class="scan-label" role="status">Looking for pages, menus, and tabs\u2026</p><div class="scan-progress"><i></i></div><span class="scan-note">A little scan. A lot of possibilities.</span></div>${B()}`;else if(o==="palette")a.innerHTML=`<header class="searchbar">${p("search")}<span class="project-chip">${p("hex")}${he()}</span><input class="search" aria-label="Search pages, components, navigation, tabs, and actions" placeholder="What would you like to build?" value="${r(j)}" autocomplete="off" spellcheck="false">${V()}</header><nav class="tabs" aria-label="Filter builder results">${["All","Pages","Components","Navs","Tabs"].map(e=>`<button class="tab ${v===e?"active":""}" aria-pressed="${v===e}">${e}</button>`).join("")}<span class="ready-status"><i></i> Builder ready</span></nav><div class="results"></div><div class="message" role="status"></div>${B()}`,_(),a.querySelector(".search").addEventListener("input",e=>{j=e.target.value,$=0,_()}),a.querySelectorAll(".tab").forEach(e=>e.addEventListener("click",()=>{v=e.textContent,$=0,d(),k()}));else if(o==="draft")a.innerHTML=`${z("Make room for a new idea","Create a page in "+r(L.pagesDir))}<div class="draft-body"><label class="field-label" for="page-name">Page name</label><div class="name-field"><input id="page-name" value="${r(i.name)}" spellcheck="false" autocomplete="off" aria-describedby="name-hint"><span>.btsx</span></div><p class="hint" id="name-hint">PascalCase, like AboutPage or PricingPage.</p><div class="route-fields"><div><label class="field-label" for="route-path">Page URL</label><input id="route-path" class="text-field" value="${r(i.routePath??de(i.name))}" placeholder="/pricing" spellcheck="false"></div><div><label class="field-label" for="nav-label">Navigation label</label><input id="nav-label" class="text-field" value="${r(i.navLabel??ce(i.name))}" maxlength="100"></div></div><label class="field-label" for="nav-target">Add link to</label><select id="nav-target" class="text-field"><option value="">${m?.navigationTargets.length===1?r(m.navigationTargets[0].label):"Choose a navigation menu"}</option>${(m?.navigationTargets??[]).map(e=>`<option value="${r(e.id)}" ${i.navigationTarget===e.id?"selected":""}>${r(e.label)}</option>`).join("")}</select><p class="hint nav-contract"></p>${m?.navigationTargets.length?"":'<p class="hint">No editable menu found. Configure navigation.file for this project.</p>'}<div class="editor-toolbar"><span class="field-label">Start with</span><button data-template="blank">Blank page</button><button data-template="landing">Landing page</button><details><summary>Add a section +</summary><div class="section-menu">${Object.keys(J).map(e=>`<button data-section="${e}">${e}</button>`).join("")}</div></details></div><label class="sr-only" for="page-source">BTSX page source</label><textarea id="page-source" class="source-editor" spellcheck="false">${r(i.source)}</textarea><div class="message" role="status">${r(g)}</div></div><footer class="action-footer"><span>${p("page")} Page + route + navigation link.</span><button class="primary review" ${x?"disabled":""}>${x?"Checking\u2026":"Review page"} ${p("arrow")}</button></footer>`,a.querySelector("#page-name").addEventListener("input",e=>{i.name=e.target.value,s=null,i.routePath===void 0&&(a.querySelector("#route-path").value=de(i.name)),i.navLabel===void 0&&(a.querySelector("#nav-label").value=ce(i.name))}),a.querySelector("#route-path").addEventListener("input",e=>{i.routePath=e.target.value,s=null}),a.querySelector("#nav-label").addEventListener("input",e=>{i.navLabel=e.target.value,s=null}),a.querySelector("#nav-target").addEventListener("change",e=>{i.navigationTarget=e.target.value||void 0,s=null,re()}),re(),a.querySelector("#page-source").addEventListener("input",e=>{i.source=e.target.value,s=null}),a.querySelectorAll("[data-template]").forEach(e=>e.addEventListener("click",()=>{i.source=O[e.dataset.template],s=null,g="",d()})),a.querySelectorAll("[data-section]").forEach(e=>e.addEventListener("click",()=>{let t=i.source.indexOf(`
233
+ style
234
+ `),l=J[e.dataset.section];i.source=t<0?i.source.trimEnd()+l:i.source.slice(0,t).trimEnd()+l+i.source.slice(t),s=null,d()})),a.querySelector(".review").addEventListener("click",()=>void xe());else if(o==="preview")a.innerHTML=`${z("One last look",r(s.path))}<div class="preview-body"><div class="validation">${p("check")} Compiles with Beast and Octane</div>${s.files.map(e=>`<details class="planned-file" open><summary>${p(e.action==="create"?"plus":"page")}<span>${r(e.path)}</span><small>${e.action==="create"?"New file":"Update"}</small></summary><pre class="source-preview" tabindex="0" aria-label="Proposed ${r(e.path)}"><code>${r(e.source)}</code></pre>${e.before!==null?`<details class="original-file"><summary>Current source</summary><pre class="source-preview" tabindex="0"><code>${r(e.before)}</code></pre></details>`:""}</details>`).join("")}<div class="message" role="status">${r(g)}</div><p class="hint">Creates the page and updates the route and navigation together.</p></div><footer class="action-footer"><span>${s.files.length} files \xB7 ${r(s.routePath)}</span><button class="primary save" ${x?"disabled":""}>${x?"Creating\u2026":"Create page"} ${p("plus")}</button></footer>`,a.querySelector(".save").addEventListener("click",()=>void be());else if(o==="file")a.innerHTML=`${z(r(C.name),r(C.path))}<div class="preview-body">${C.error?`<p class="error-note">${r(C.error)}</p>`:""}<pre class="source-preview" tabindex="0"><code>${r(Y)}</code></pre><div class="message" role="status"></div></div><footer class="action-footer"><span>${C.lines} lines \xB7 ${C.components.length} component${C.components.length===1?"":"s"}</span><button class="secondary copy">Copy source</button></footer>`,a.querySelector(".copy").addEventListener("click",()=>{navigator.clipboard.writeText(Y).then(()=>X("Source copied.")).catch(()=>X("Could not access the clipboard. Select the source to copy it."))});else if(o==="collection"){let e=ee;a.innerHTML=`${z(r(e.name),r(e.file)+":"+e.line)}<div class="preview-body collection-body"><div class="collection-meta"><span class="type-badge">${e.kind==="tabs"?"Tabs":"Navigation"}</span><span>${e.source==="data"?"Shared data":"Rendered markup"} \xB7 ${e.items.length} items</span></div><h3>Item type</h3>${W(e.itemType)}${e.bindings.length?`<p class="hint">Data binding: <code>${r(e.bindings.join(", "))}</code></p>`:""}${e.reason?`<p class="hint">${r(e.reason)}</p>`:""}${e.consumers.length?`<h3>Used by</h3><ul class="consumer-list">${e.consumers.map(t=>`<li><code>${r(t.file)}:${t.line}</code> \xB7 ${r(t.name)}</li>`).join("")}</ul>`:""}<h3>Items</h3>${e.items.map(t=>`<details class="detected-item"><summary><span>${r(t.label)}</span><span class="type-badge">${r(t.kind.replaceAll("-"," "))}</span></summary><div class="item-body"><p class="hint">${t.component?`Component: <code>${r(t.component)}</code> \xB7 `:""}Line ${t.line}${t.destination?` \xB7 Destination: <code>${r(t.destination)}</code>`:""}${t.value?` \xB7 Value: <code>${r(t.value)}</code>`:""}</p>${W(t.itemType)}</div></details>`).join("")||'<p class="hint">No literal items found. The group may populate its items at runtime.</p>'}</div><footer class="action-footer"><span>${e.editable&&e.kind==="navigation"?"Available for new page links":"Inspect item types and source bindings"}</span></footer>`}else o==="success"?(a.innerHTML=`${z("Your idea has a home",r(s.path))}<div class="success-body"><div class="success-icon">${p("check")}</div><span class="eyebrow">READY FOR WHAT\u2019S NEXT</span><h2>${r(i.name)} is created.</h2><p>Your route and navigation link are ready.<br>Find it at <strong>${r(s.routePath)}</strong>.</p><a class="primary visit" href="${r(K.slice(0,-R.length)+s.routePath)}">Open page ${p("arrow")}</a><button class="secondary done">Back to builder ${p("arrow")}</button></div>${B()}`,a.querySelector(".visit").addEventListener("click",()=>{M("")}),a.querySelector(".done").addEventListener("click",()=>{M(""),i={name:"UntitledPage",source:O.blank},s=null,q()})):(a.innerHTML=`${z("Couldn\u2019t connect to your project")}<div class="error-body">${p("scan")}<h2>Let\u2019s try that again.</h2><p class="error-note" role="alert">${r(g)}</p><button class="primary retry">Scan project ${p("scan")}</button></div>${B()}`,a.querySelector(".retry").addEventListener("click",()=>void q()));a.querySelector(".close")?.addEventListener("click",F),a.querySelector(".back")?.addEventListener("click",()=>{if(g="",o=o==="preview"?"draft":"palette",!m){q();return}d(),k()})}function re(){let e=m?.navigationTargets.find(l=>l.id===i.navigationTarget)??(m?.navigationTargets.length===1?m.navigationTargets[0]:void 0),t=a.querySelector(".nav-contract");t&&(t.textContent=e?.itemType?`Item type: ${e.itemType.name} \xB7 ${e.itemType.origin==="declared"?"Declared type":"Inferred shape"}`:"")}function W(e){return`<div class="type-contract"><div class="type-title"><code>${r(e.name)}</code><span class="type-badge">${e.origin==="declared"?"Declared type":"Inferred shape"}</span></div>${e.fields.length?`<div class="fields-scroll"><table class="type-fields"><thead><tr><th>Field</th><th>Type</th><th>Presence</th></tr></thead><tbody>${e.fields.map(t=>`<tr><td><code>${r(t.name)}</code></td><td><code>${r(t.type)}</code></td><td>${t.optional?"Optional":"Required"}</td></tr>`).join("")}</tbody></table></div>`:""}${e.variants?.length?`<details class="type-variants"><summary>${e.variants.length} item variants</summary>${e.variants.map(t=>W(t)).join("")}</details>`:""}</div>`}let E=[];function _(){let e=j.toLowerCase();E=[...v==="All"?[{name:"Create a new page",detail:"A fresh start",icon:"plus",group:"Actions",run:()=>{o="draft",g="",d(),a.querySelector("#page-name").focus(),a.querySelector("#page-name").select()}},{name:"Rescan project",detail:"Find what\u2019s new",icon:"scan",group:"Actions",run:()=>void q()}]:[],...(m?.files??[]).filter(n=>v==="All"||v==="Pages"&&n.kind==="page"||v==="Components"&&n.kind==="component").map(n=>({name:n.name,detail:n.path,icon:n.kind==="page"?"page":"component",group:n.kind==="page"?"Pages":"Components",run:()=>void fe(n)})),...(m?.collections??[]).filter(n=>v==="All"||v==="Navs"&&n.kind==="navigation"||v==="Tabs"&&n.kind==="tabs").map(n=>({name:n.name,detail:`${n.items.length} items \xB7 ${n.file}`,icon:n.kind==="tabs"?"component":"arrow",group:n.kind==="tabs"?"Tabs":"Navigation",run:()=>{ee=n,o="collection",d(),k()}}))].filter(n=>`${n.name} ${n.detail}`.toLowerCase().includes(e)),$=Math.min($,Math.max(0,E.length-1));let l=a.querySelector(".results"),T="";l.innerHTML=E.length?E.map((n,S)=>{let A=n.group!==T?`<h3 class="group-label">${n.group}</h3>`:"";return T=n.group,`${A}<button class="result ${S===$?"selected":""}" data-index="${S}">${p(n.icon)}<span class="result-name">${r(n.name)}</span><span class="result-detail">${r(n.detail)}</span><span class="result-enter">\u21B5</span></button>`}).join(""):`<div class="empty-state">${p("search")}<h3>${j?"Nothing here just yet":"A clean slate"}</h3><p>${j?"Try another name, or create something new.":`No ${v.toLowerCase()} found in this project.`}</p><button class="secondary empty-create">Create a page ${p("plus")}</button></div>`,l.querySelectorAll(".result").forEach(n=>n.addEventListener("click",()=>E[Number(n.dataset.index)]?.run())),l.querySelector(".empty-create")?.addEventListener("click",()=>{o="draft",g="",d(),a.querySelector("#page-name").focus()});let y=m?.files.filter(n=>n.error).length??0;a.querySelector(".message").textContent=g||m?.warnings.join(" \xB7 ")||(y?`${y} file${y===1?"":"s"} with compiler errors. Open the source to inspect.`:"")}function k(){(a.querySelector(".search")??a.querySelector("button"))?.focus()}async function q(){I?.abort(),I=new AbortController;let e=I.signal,t=++f;o="scan",g="",d();let l=matchMedia("(prefers-reduced-motion: reduce)").matches,T=performance.now(),y=window.setTimeout(()=>{o==="scan"&&t===f&&(a.querySelector(".scan-label").textContent="Resolving menus, tabs, and item types\u2026")},650);Z=y;try{L=await P("/config",void 0,e),G();let n=await P("/scan",void 0,e),S=l?0:L.scanDuration;if(await new Promise(A=>window.setTimeout(A,Math.max(0,S-(performance.now()-T)))),!h||t!==f)return;m=n,o="palette",$=0,d(),k()}catch(n){if(e.aborted||!h||t!==f)return;g=n instanceof Error?n.message:String(n),o="error",d(),k()}finally{window.clearTimeout(y)}}async function fe(e){if(x)return;let t=f;x=!0;try{let l=await P(`/source?path=${encodeURIComponent(e.path)}`);if(!h||t!==f)return;C=e,Y=l.source,o="file",d(),k()}catch(l){X(l instanceof Error?l.message:String(l))}finally{x=!1}}async function xe(){if(x)return;let e=f;x=!0,g="",d();try{let t=await P("/preview",i);if(!h||e!==f)return;s=t,o="preview"}catch(t){g=t instanceof Error?t.message:String(t)}finally{x=!1,h&&e===f&&(d(),k())}}async function be(){if(x||!s)return;let e=f;x=!0,g="",d(),M(s.hash);try{s=await P("/create",{...i,hash:s.hash}),h&&e===f&&(o="success")}catch(t){M(""),g=t instanceof Error?t.message:String(t)}finally{x=!1,h&&e===f&&(d(),k())}}function ne(){if(h)return;let e=u.activeElement??document.activeElement;Q=e instanceof HTMLElement?e:null,h=!0,a.hidden=!1,b.hidden=!1,w.setAttribute("aria-expanded","true"),q(),a.focus()}function F(){o==="success"&&M(""),h=!1,f++,I?.abort(),window.clearTimeout(Z),a.hidden=!0,b.hidden=!0,w.setAttribute("aria-expanded","false"),Q?.focus()}a.tabIndex=-1,w.setAttribute("aria-expanded","false"),w.addEventListener("click",ne),b.addEventListener("click",F);function ie(e){let[t,...l]=L.shortcut.split("+"),T=l.at(-1).toLowerCase(),y=t==="Meta"?e.metaKey&&!e.ctrlKey:t==="Ctrl"?e.ctrlKey&&!e.metaKey:te?e.metaKey&&!e.ctrlKey:e.ctrlKey&&!e.metaKey;if(e.key.toLowerCase()===T&&y&&e.shiftKey===l.includes("Shift")&&!e.altKey){e.preventDefault(),e.stopImmediatePropagation(),e.repeat||(h?F():ne());return}if(h){if(e.key==="Escape"){e.preventDefault(),e.stopImmediatePropagation(),F();return}if(e.key==="Tab"){let n=[...a.querySelectorAll('button:not(:disabled), input, textarea, select, a[href], summary, [tabindex="0"]')].filter(ve=>ve.getClientRects().length),S=u.activeElement,A=n[0],se=n.at(-1);e.shiftKey&&(S===A||S===a)?(e.preventDefault(),se?.focus()):!e.shiftKey&&(S===se||S===a)&&(e.preventDefault(),A?.focus())}if(!(o!=="palette"||!u.activeElement?.matches(".search, .result")))if(e.key==="ArrowDown"||e.key==="ArrowUp"){if(e.preventDefault(),!E.length)return;$=($+(e.key==="ArrowDown"?1:-1)+E.length)%E.length,_(),a.querySelector(`[data-index="${$}"]`)?.scrollIntoView({block:"nearest"}),k()}else e.key==="Enter"&&!e.isComposing&&(e.preventDefault(),E[$]?.run())}}window.addEventListener("keydown",ie,!0);let oe=new MutationObserver(()=>{c.isConnected||(I?.abort(),f++,window.clearTimeout(Z),window.removeEventListener("keydown",ie,!0),oe.disconnect())});oe.observe(document.body,{childList:!0});let N=M();N&&/^[a-f0-9]{64}$/.test(N)&&ae.catch(()=>{}).then(async()=>{try{let e=await P(`/creation?hash=${N}`);if(M()!==N||h)return;s=e,i={name:e.path.split("/").at(-1).replace(/\.btsx$/,""),source:e.source,routePath:e.routePath,navLabel:e.navLabel},h=!0,o="success",a.hidden=!1,b.hidden=!1,w.setAttribute("aria-expanded","true"),d(),k()}catch{M("")}})}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",ge,{once:!0}):ge();
@@ -0,0 +1 @@
1
+ export * from './types/rsbuild.js'