@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 +17 -0
- package/LICENSE +15 -0
- package/README.md +200 -2
- package/dist/client.js +234 -0
- package/dist/rsbuild.d.ts +1 -0
- package/dist/rsbuild.js +883 -0
- package/dist/rspack.d.ts +1 -0
- package/dist/rspack.js +892 -0
- package/dist/types/rsbuild.d.ts +4 -0
- package/dist/types/rspack.d.ts +9 -0
- package/dist/types/server/discovery.d.ts +8 -0
- package/dist/types/server/integration.d.ts +6 -0
- package/dist/types/server/project.d.ts +24 -0
- package/dist/types/server/server.d.ts +10 -0
- package/dist/types/shared/types.d.ts +121 -0
- package/dist/types/vite.d.ts +4 -0
- package/dist/vite.d.ts +1 -0
- package/dist/vite.js +887 -0
- package/package.json +103 -4
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
|
-
#
|
|
1
|
+
# Beast Page Builder
|
|
2
2
|
|
|
3
|
-
|
|
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=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[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'
|