@jay-framework/jay-stack-cli 0.15.5 → 0.16.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/agent-kit-template/{INSTRUCTIONS.md → designer/INSTRUCTIONS.md} +11 -8
- package/agent-kit-template/{jay-html-syntax.md → designer/jay-html-components.md} +89 -158
- package/agent-kit-template/designer/jay-html-styling.md +97 -0
- package/agent-kit-template/designer/jay-html-syntax.md +44 -0
- package/agent-kit-template/designer/jay-html-template-syntax.md +203 -0
- package/agent-kit-template/developer/INSTRUCTIONS.md +34 -0
- package/agent-kit-template/developer/cli-commands.md +228 -0
- package/agent-kit-template/developer/component-data.md +109 -0
- package/agent-kit-template/developer/component-refs.md +117 -0
- package/agent-kit-template/developer/component-state.md +140 -0
- package/agent-kit-template/developer/configuration.md +76 -0
- package/agent-kit-template/developer/dev-server-service.md +126 -0
- package/agent-kit-template/developer/page-components.md +103 -0
- package/agent-kit-template/developer/page-contracts.md +114 -0
- package/agent-kit-template/developer/project-structure.md +242 -0
- package/agent-kit-template/developer/render-results.md +112 -0
- package/agent-kit-template/developer/routing.md +175 -0
- package/agent-kit-template/developer/seo-guide.md +93 -0
- package/agent-kit-template/plugin/INSTRUCTIONS.md +43 -0
- package/agent-kit-template/plugin/actions-guide.md +184 -0
- package/agent-kit-template/plugin/component-context.md +103 -0
- package/agent-kit-template/plugin/component-data.md +109 -0
- package/agent-kit-template/plugin/component-refs.md +117 -0
- package/agent-kit-template/plugin/component-state.md +140 -0
- package/agent-kit-template/plugin/component-structure.md +174 -0
- package/agent-kit-template/plugin/contracts-guide.md +193 -0
- package/agent-kit-template/plugin/dev-server-service.md +137 -0
- package/agent-kit-template/plugin/plugin-routes.md +146 -0
- package/agent-kit-template/plugin/plugin-structure.md +210 -0
- package/agent-kit-template/plugin/render-results.md +112 -0
- package/agent-kit-template/plugin/seo-guide.md +93 -0
- package/agent-kit-template/plugin/services-guide.md +116 -0
- package/agent-kit-template/plugin/validation.md +101 -0
- package/dist/index.js +791 -60
- package/package.json +10 -10
- /package/agent-kit-template/{cli-commands.md → designer/cli-commands.md} +0 -0
- /package/agent-kit-template/{contracts-and-plugins.md → designer/contracts-and-plugins.md} +0 -0
- /package/agent-kit-template/{project-structure.md → designer/project-structure.md} +0 -0
- /package/agent-kit-template/{routing.md → designer/routing.md} +0 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Dev Server Service API
|
|
2
|
+
|
|
3
|
+
The dev server exposes a `DevServerService` for design board applications, CLI tools, and plugins. It provides route listing, param discovery, and freeze management.
|
|
4
|
+
|
|
5
|
+
## Service Marker
|
|
6
|
+
|
|
7
|
+
Registered as `DEV_SERVER_SERVICE` — injectable in actions and components:
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { DEV_SERVER_SERVICE } from '@jay-framework/dev-server';
|
|
11
|
+
|
|
12
|
+
export const listAllRoutes = makeJayAction('admin.listRoutes')
|
|
13
|
+
.withServices(DEV_SERVER_SERVICE)
|
|
14
|
+
.withHandler(async (_input, devServer) => {
|
|
15
|
+
return devServer.listRoutes();
|
|
16
|
+
});
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Direct Access
|
|
20
|
+
|
|
21
|
+
Also returned from `mkDevServer()` for CLI usage:
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
const { service } = await mkDevServer(options);
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Routes
|
|
28
|
+
|
|
29
|
+
### listRoutes()
|
|
30
|
+
|
|
31
|
+
Returns all page routes in the project:
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
const routes = service.listRoutes();
|
|
35
|
+
// [{ path: '/products/kitan/[[category]]', jayHtmlPath: '...', compPath: '...' }]
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### loadRouteParams(route, onBatch)
|
|
39
|
+
|
|
40
|
+
Async generator that yields param batches from the route's `loadParams`:
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
for await (const batch of service.loadRouteParams('/products/kitan/[[category]]')) {
|
|
44
|
+
console.log(batch); // [{ category: 'shirts' }, { category: 'pants' }]
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Returns empty if the route has no `page.ts` or no `loadParams`. Throws if the route doesn't exist.
|
|
49
|
+
|
|
50
|
+
## Freeze Management
|
|
51
|
+
|
|
52
|
+
### FreezeStore
|
|
53
|
+
|
|
54
|
+
Accessible via `service.freezeStore`:
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
const store = service.freezeStore;
|
|
58
|
+
|
|
59
|
+
// Save a ViewState snapshot
|
|
60
|
+
const entry = await store.save('/products/kitan', viewState);
|
|
61
|
+
|
|
62
|
+
// List freezes for a route
|
|
63
|
+
const freezes = await store.list('/products/kitan');
|
|
64
|
+
|
|
65
|
+
// Get a specific freeze
|
|
66
|
+
const freeze = await store.get('abc123');
|
|
67
|
+
|
|
68
|
+
// Rename
|
|
69
|
+
await store.rename('abc123', 'in-stock state');
|
|
70
|
+
|
|
71
|
+
// Delete
|
|
72
|
+
await store.delete('abc123');
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Editor Protocol
|
|
76
|
+
|
|
77
|
+
These APIs are also exposed via the editor protocol (Socket.IO) for design board applications:
|
|
78
|
+
|
|
79
|
+
| Protocol Message | Service Method |
|
|
80
|
+
| ----------------- | ----------------------------------------- |
|
|
81
|
+
| `listRoutes` | `service.listRoutes()` |
|
|
82
|
+
| `listFreezes` | `service.freezeStore.list(route)` |
|
|
83
|
+
| `renameFreeze` | `service.freezeStore.rename(id, name)` |
|
|
84
|
+
| `deleteFreeze` | `service.freezeStore.delete(id)` |
|
|
85
|
+
| `loadRouteParams` | `service.loadRouteParams(route, onBatch)` |
|
|
86
|
+
|
|
87
|
+
### Streaming Events
|
|
88
|
+
|
|
89
|
+
`loadRouteParams` streams batches via `routeParamsBatch` socket events:
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
// Client sends: { type: 'loadRouteParams', route: '/products/[slug]' }
|
|
93
|
+
// Server responds: { type: 'loadRouteParams', success: true }
|
|
94
|
+
// Server emits: { type: 'routeParamsBatch', route: '...', params: [...], hasMore: true }
|
|
95
|
+
// Server emits: { type: 'routeParamsBatch', route: '...', params: [...], hasMore: true }
|
|
96
|
+
// Server emits: { type: 'routeParamsBatch', route: '...', params: [], hasMore: false }
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Freeze Changed Event
|
|
100
|
+
|
|
101
|
+
The `freezeChanged` socket event is emitted when jay-html or CSS files change. Design board applications should listen for this to refresh their frozen views:
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
socket.on('freezeChanged', () => {
|
|
105
|
+
// Re-fetch frozen page fragments
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Iframe / Embed Mode
|
|
110
|
+
|
|
111
|
+
When a page is loaded inside an iframe with `?_jay_embed=true` (e.g., by the AIditor), the Alt+S shortcut is disabled (parent owns it). Freeze is triggered via `postMessage`:
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
// Parent → iframe: request freeze
|
|
115
|
+
iframe.contentWindow.postMessage({ type: 'jay:requestFreeze' }, '*');
|
|
116
|
+
|
|
117
|
+
// Iframe → parent: freeze done
|
|
118
|
+
// { type: 'jay:freeze', id: string, route: string }
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The parent constructs the frozen page URL:
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
route + '?_jay_freeze=' + id // full page
|
|
125
|
+
route + '?_jay_freeze=' + id + '&format=fragment' // shadow DOM fragment
|
|
126
|
+
```
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Page Components
|
|
2
|
+
|
|
3
|
+
A page component (`page.ts`) uses `makeJayStackComponent` to provide page-level data across rendering phases.
|
|
4
|
+
|
|
5
|
+
## Basic Page Component
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { makeJayStackComponent, phaseOutput } from '@jay-framework/fullstack-component';
|
|
9
|
+
import type { HomePageContract } from './page.jay-contract.generated';
|
|
10
|
+
|
|
11
|
+
export const page = makeJayStackComponent<HomePageContract>()
|
|
12
|
+
.withSlowlyRender(async () => {
|
|
13
|
+
return phaseOutput({ heroTitle: 'Welcome', heroSubtitle: 'Build something great' }, {});
|
|
14
|
+
})
|
|
15
|
+
.withFastRender(async () => {
|
|
16
|
+
return phaseOutput({ featuredCount: 12 }, {});
|
|
17
|
+
});
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The export name must be `page` for page-level components.
|
|
21
|
+
|
|
22
|
+
## Page Component with Params
|
|
23
|
+
|
|
24
|
+
For dynamic routes, use `withLoadParams` and access params via props:
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
export const page = makeJayStackComponent<ProductPageContract>()
|
|
28
|
+
.withServices(PRODUCTS_DB)
|
|
29
|
+
.withLoadParams(async function* (db) {
|
|
30
|
+
const products = await db.getAll();
|
|
31
|
+
yield products.map((p) => ({ slug: p.slug }));
|
|
32
|
+
})
|
|
33
|
+
.withSlowlyRender(async (props, db) => {
|
|
34
|
+
const product = await db.getBySlug(props.slug);
|
|
35
|
+
if (!product) return notFound('Product not found');
|
|
36
|
+
return phaseOutput(
|
|
37
|
+
{ title: product.name, description: product.desc },
|
|
38
|
+
{ productId: product.id },
|
|
39
|
+
);
|
|
40
|
+
})
|
|
41
|
+
.withFastRender(async (props, db) => {
|
|
42
|
+
const price = await db.getPrice(props.carryForward.productId);
|
|
43
|
+
return phaseOutput({ price, inStock: price > 0 }, {});
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Page Component with Interactive Phase
|
|
48
|
+
|
|
49
|
+
Add client-side interactivity:
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
export const page = makeJayStackComponent<ProductPageContract>()
|
|
53
|
+
.withServices(PRODUCTS_DB)
|
|
54
|
+
.withSlowlyRender(async (props, db) => {
|
|
55
|
+
// ... slow render
|
|
56
|
+
})
|
|
57
|
+
.withFastRender(async (props, db) => {
|
|
58
|
+
// ... fast render
|
|
59
|
+
})
|
|
60
|
+
.withInteractive(function ProductPage(props, refs) {
|
|
61
|
+
const [quantity, setQuantity] = createSignal(1);
|
|
62
|
+
|
|
63
|
+
refs.addToCart.onClick(async () => {
|
|
64
|
+
await addToCartAction({
|
|
65
|
+
productId: props.carryForward.productId,
|
|
66
|
+
quantity: quantity(),
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
refs.quantityInput.exec$((input) => {
|
|
71
|
+
input.addEventListener('change', (e) => {
|
|
72
|
+
setQuantity(parseInt((e.target as HTMLInputElement).value));
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
render: () => ({
|
|
78
|
+
quantity: quantity(),
|
|
79
|
+
}),
|
|
80
|
+
};
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Combining with Headless Plugins
|
|
85
|
+
|
|
86
|
+
A page component handles page-level data. Plugin headless components handle their own data independently. Both render into the same page:
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
src/pages/products/[slug]/
|
|
90
|
+
├── page.jay-html # Template: binds to both page + plugin data
|
|
91
|
+
├── page.jay-contract # Page-level contract (title, breadcrumbs, etc.)
|
|
92
|
+
├── page.ts # Page component
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The jay-html template uses unprefixed bindings for page data and key-prefixed bindings for plugin data.
|
|
96
|
+
|
|
97
|
+
## Builder API Reference
|
|
98
|
+
|
|
99
|
+
See the plugin [component-structure.md](../plugin/component-structure.md) for the full builder API: `.withProps()`, `.withServices()`, `.withContexts()`, phase rendering, and render results.
|
|
100
|
+
|
|
101
|
+
## State Hooks Reference
|
|
102
|
+
|
|
103
|
+
See [component-state.md](component-state.md) for `createSignal`, `createMemo`, `createEffect`, and other hooks used in the interactive phase.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Page Contracts
|
|
2
|
+
|
|
3
|
+
A page can have its own contract (`page.jay-contract`) defining page-level data, alongside headless plugin contracts.
|
|
4
|
+
|
|
5
|
+
## When to Use a Page Contract
|
|
6
|
+
|
|
7
|
+
- The page has its own data that isn't provided by a plugin
|
|
8
|
+
- The page component (`page.ts`) renders data into ViewState
|
|
9
|
+
- You want type-safe bindings between the page component and its jay-html template
|
|
10
|
+
|
|
11
|
+
If the page only uses plugin headless components and has no page-level data, a page contract is optional.
|
|
12
|
+
|
|
13
|
+
## Basic Page Contract
|
|
14
|
+
|
|
15
|
+
```yaml
|
|
16
|
+
name: home-page
|
|
17
|
+
tags:
|
|
18
|
+
- tag: heroTitle
|
|
19
|
+
type: data
|
|
20
|
+
dataType: string
|
|
21
|
+
phase: slow
|
|
22
|
+
- tag: heroSubtitle
|
|
23
|
+
type: data
|
|
24
|
+
dataType: string
|
|
25
|
+
phase: slow
|
|
26
|
+
- tag: featuredCount
|
|
27
|
+
type: data
|
|
28
|
+
dataType: number
|
|
29
|
+
phase: fast
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Place as `page.jay-contract` next to `page.jay-html`:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
src/pages/
|
|
36
|
+
├── page.jay-html
|
|
37
|
+
├── page.jay-contract
|
|
38
|
+
└── page.ts
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Page Contract with Params
|
|
42
|
+
|
|
43
|
+
If the page has a dynamic route, declare `params` in the contract:
|
|
44
|
+
|
|
45
|
+
```yaml
|
|
46
|
+
name: product-page
|
|
47
|
+
description: Product detail page
|
|
48
|
+
params:
|
|
49
|
+
slug: string
|
|
50
|
+
tags:
|
|
51
|
+
- tag: title
|
|
52
|
+
type: data
|
|
53
|
+
dataType: string
|
|
54
|
+
phase: slow
|
|
55
|
+
- tag: price
|
|
56
|
+
type: data
|
|
57
|
+
dataType: number
|
|
58
|
+
phase: fast+interactive
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The route must have matching dynamic segments:
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
src/pages/products/[slug]/
|
|
65
|
+
├── page.jay-html
|
|
66
|
+
├── page.jay-contract
|
|
67
|
+
└── page.ts
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Page Contract with Props
|
|
71
|
+
|
|
72
|
+
Pages rarely use props (they're top-level), but page contracts can declare them for reusable page patterns:
|
|
73
|
+
|
|
74
|
+
```yaml
|
|
75
|
+
name: category-page
|
|
76
|
+
props:
|
|
77
|
+
- name: categoryId
|
|
78
|
+
type: string
|
|
79
|
+
required: true
|
|
80
|
+
tags:
|
|
81
|
+
- tag: categoryName
|
|
82
|
+
type: data
|
|
83
|
+
dataType: string
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Combining with Plugin Contracts
|
|
87
|
+
|
|
88
|
+
A page can have both a page contract and headless plugin contracts. The page contract covers page-owned data; plugins cover their own domains:
|
|
89
|
+
|
|
90
|
+
```html
|
|
91
|
+
<!-- page.jay-html -->
|
|
92
|
+
<html>
|
|
93
|
+
<head>
|
|
94
|
+
<script
|
|
95
|
+
type="application/jay-headless"
|
|
96
|
+
plugin="wix-stores"
|
|
97
|
+
contract="product-page"
|
|
98
|
+
key="product"
|
|
99
|
+
></script>
|
|
100
|
+
</head>
|
|
101
|
+
<body>
|
|
102
|
+
<!-- Page contract data -->
|
|
103
|
+
<h1>{heroTitle}</h1>
|
|
104
|
+
|
|
105
|
+
<!-- Plugin contract data (prefixed with key) -->
|
|
106
|
+
<div>{product.name}</div>
|
|
107
|
+
<div>{product.price}</div>
|
|
108
|
+
</body>
|
|
109
|
+
</html>
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Contract Format Reference
|
|
113
|
+
|
|
114
|
+
See the plugin [contracts-guide.md](../plugin/contracts-guide.md) for the full contract format: tag types, phases, sub-contracts, async data, and validation rules.
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# Project Structure
|
|
2
|
+
|
|
3
|
+
## Directory Layout
|
|
4
|
+
|
|
5
|
+
A jay-stack project follows this structure:
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
my-project/
|
|
9
|
+
├── .jay # Dev server configuration (optional)
|
|
10
|
+
├── package.json # Dependencies and scripts
|
|
11
|
+
├── tsconfig.json # TypeScript config
|
|
12
|
+
├── public/ # Static assets (images, fonts, etc.)
|
|
13
|
+
├── config/ # Plugin configuration (generated by jay-stack setup)
|
|
14
|
+
│ ├── project.conf.yaml # Project metadata (name, etc.)
|
|
15
|
+
│ └── <plugin-name>.yaml # Plugin-specific config files
|
|
16
|
+
├── src/
|
|
17
|
+
│ ├── pages/ # Pages (directory-based routing)
|
|
18
|
+
│ │ ├── page.jay-html # Homepage → /
|
|
19
|
+
│ │ ├── page.jay-contract # Homepage contract (optional)
|
|
20
|
+
│ │ ├── products/
|
|
21
|
+
│ │ │ ├── page.jay-html # Products list → /products
|
|
22
|
+
│ │ │ └── [slug]/
|
|
23
|
+
│ │ │ └── page.jay-html # Product detail → /products/:slug
|
|
24
|
+
│ │ └── cart/
|
|
25
|
+
│ │ └── page.jay-html # Cart → /cart
|
|
26
|
+
│ └── styles/
|
|
27
|
+
│ └── theme.css # Global theme stylesheet
|
|
28
|
+
└── agent-kit/ # Generated by jay-stack agent-kit
|
|
29
|
+
├── INSTRUCTIONS.md
|
|
30
|
+
├── materialized-contracts/ # Contracts and indexes
|
|
31
|
+
└── references/ # Plugin reference data (product catalogs, schemas)
|
|
32
|
+
└── <plugin-name>/ # Per-plugin discovery data
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## What You Create
|
|
36
|
+
|
|
37
|
+
As an agent building pages, you typically create:
|
|
38
|
+
|
|
39
|
+
1. **`src/pages/**/\*.jay-html`\*\* — Page templates (the main output)
|
|
40
|
+
2. **`src/pages/**/\*.jay-contract`\*\* — Page-level contracts (when the page has its own data)
|
|
41
|
+
3. **`src/styles/*.css`** — Theme stylesheet (one per project, reused across pages)
|
|
42
|
+
|
|
43
|
+
You do **not** need to create: `package.json`, `tsconfig.json`, `.jay`, `page.conf.yaml`, `src/init.ts`, server actions, or services — these are set up by the project scaffolding, the Figma plugin, or provided by plugins.
|
|
44
|
+
|
|
45
|
+
## Styling
|
|
46
|
+
|
|
47
|
+
### Global Theme (src/styles/)
|
|
48
|
+
|
|
49
|
+
Each project has a theme CSS file in `src/styles/` with CSS custom properties (design tokens):
|
|
50
|
+
|
|
51
|
+
```css
|
|
52
|
+
:root {
|
|
53
|
+
/* Colors */
|
|
54
|
+
--bg-primary: #faf8f5;
|
|
55
|
+
--bg-card: #ffffff;
|
|
56
|
+
--text-primary: #2d2a26;
|
|
57
|
+
--text-secondary: #6b665e;
|
|
58
|
+
--accent: #c45c3e;
|
|
59
|
+
--accent-hover: #d4704f;
|
|
60
|
+
--border: #e8e4dd;
|
|
61
|
+
|
|
62
|
+
/* Typography */
|
|
63
|
+
--font-serif: 'Cormorant Garamond', Georgia, serif;
|
|
64
|
+
--font-sans: 'DM Sans', -apple-system, sans-serif;
|
|
65
|
+
|
|
66
|
+
/* Spacing & Layout */
|
|
67
|
+
--radius-md: 10px;
|
|
68
|
+
--radius-lg: 14px;
|
|
69
|
+
--container-max: 1400px;
|
|
70
|
+
--page-padding: 48px;
|
|
71
|
+
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.04);
|
|
72
|
+
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.06);
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The theme provides reusable CSS classes for common UI patterns:
|
|
77
|
+
|
|
78
|
+
- **Layout**: `.container`, `.container-narrow`
|
|
79
|
+
- **Cards**: `.card`, `.card-elevated`
|
|
80
|
+
- **Buttons**: `.btn`, `.btn-primary`, `.btn-secondary`, `.btn-ghost`, `.btn-sm`, `.btn-lg`
|
|
81
|
+
- **Forms**: `.input`, `.select`, `.checkbox`
|
|
82
|
+
- **Badges**: `.badge`, `.badge-accent`, `.badge-success`, `.badge-error`
|
|
83
|
+
- **Typography**: `.page-title`, `.section-title`, `.label`
|
|
84
|
+
- **Product components**: `.product-card`, `.product-card-image`, `.product-card-content`, `.product-card-price`
|
|
85
|
+
|
|
86
|
+
### Linking the Theme from Pages
|
|
87
|
+
|
|
88
|
+
Use a relative path from the page to the theme file:
|
|
89
|
+
|
|
90
|
+
```html
|
|
91
|
+
<!-- From src/pages/page.jay-html (depth 1) -->
|
|
92
|
+
<link rel="stylesheet" href="../styles/theme.css" />
|
|
93
|
+
|
|
94
|
+
<!-- From src/pages/products/page.jay-html (depth 2) -->
|
|
95
|
+
<link rel="stylesheet" href="../../styles/theme.css" />
|
|
96
|
+
|
|
97
|
+
<!-- From src/pages/products/[slug]/page.jay-html (depth 3) -->
|
|
98
|
+
<link rel="stylesheet" href="../../../styles/theme.css" />
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Page-Specific Styles
|
|
102
|
+
|
|
103
|
+
Add page-specific styles in `<style>` tags within the jay-html `<head>`:
|
|
104
|
+
|
|
105
|
+
```html
|
|
106
|
+
<head>
|
|
107
|
+
<link rel="stylesheet" href="../../styles/theme.css" />
|
|
108
|
+
<style>
|
|
109
|
+
.hero {
|
|
110
|
+
padding: 80px 0;
|
|
111
|
+
text-align: center;
|
|
112
|
+
}
|
|
113
|
+
.featured-grid {
|
|
114
|
+
display: grid;
|
|
115
|
+
grid-template-columns: repeat(3, 1fr);
|
|
116
|
+
gap: 24px;
|
|
117
|
+
}
|
|
118
|
+
</style>
|
|
119
|
+
</head>
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### External Fonts
|
|
123
|
+
|
|
124
|
+
Import fonts in the theme CSS or via `<link>` in jay-html:
|
|
125
|
+
|
|
126
|
+
```css
|
|
127
|
+
/* In theme.css */
|
|
128
|
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Responsive Design
|
|
132
|
+
|
|
133
|
+
Use media queries in the theme CSS or page-specific styles:
|
|
134
|
+
|
|
135
|
+
```css
|
|
136
|
+
@media (max-width: 1024px) {
|
|
137
|
+
.product-page-layout {
|
|
138
|
+
grid-template-columns: 1fr;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
@media (max-width: 768px) {
|
|
142
|
+
.products-grid {
|
|
143
|
+
grid-template-columns: 1fr;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Configuration Files
|
|
149
|
+
|
|
150
|
+
### config/ (Plugin Configuration)
|
|
151
|
+
|
|
152
|
+
The `config/` folder holds plugin configuration files, typically generated by `jay-stack setup`:
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
config/
|
|
156
|
+
├── project.conf.yaml # Project name and metadata
|
|
157
|
+
└── wix-data.yaml # Plugin-specific config (auto-generated by setup)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Plugins create their config files here during setup. The location is configured via `configBase` in the `.jay` file (defaults to `./config`).
|
|
161
|
+
|
|
162
|
+
You generally don't need to create files here manually — plugins handle this via `jay-stack setup`. However, you may need to read these files to understand how a plugin is configured.
|
|
163
|
+
|
|
164
|
+
### .jay (Dev Server Config)
|
|
165
|
+
|
|
166
|
+
Optional YAML file at project root:
|
|
167
|
+
|
|
168
|
+
```yaml
|
|
169
|
+
devServer:
|
|
170
|
+
portRange:
|
|
171
|
+
- 3000
|
|
172
|
+
- 3100
|
|
173
|
+
pagesBase: ./src/pages
|
|
174
|
+
componentsBase: ./src/components
|
|
175
|
+
publicFolder: ./public
|
|
176
|
+
editorServer:
|
|
177
|
+
portRange:
|
|
178
|
+
- 3101
|
|
179
|
+
- 3200
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### package.json (Scripts)
|
|
183
|
+
|
|
184
|
+
Standard scripts for jay-stack projects:
|
|
185
|
+
|
|
186
|
+
```json
|
|
187
|
+
{
|
|
188
|
+
"scripts": {
|
|
189
|
+
"dev": "jay-stack-cli dev",
|
|
190
|
+
"validate": "jay-stack-cli validate",
|
|
191
|
+
"definitions": "jay-cli definitions src",
|
|
192
|
+
"build": "npm run definitions"
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Common Page Patterns
|
|
198
|
+
|
|
199
|
+
### Product List / Search Page
|
|
200
|
+
|
|
201
|
+
Uses a product search contract with filters, sorting, and pagination:
|
|
202
|
+
|
|
203
|
+
```
|
|
204
|
+
src/pages/products/
|
|
205
|
+
└── page.jay-html # Search input, filters sidebar, product grid, load more
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Product Detail Page (Dynamic Route)
|
|
209
|
+
|
|
210
|
+
Uses a product page contract with media gallery, options, and add-to-cart:
|
|
211
|
+
|
|
212
|
+
```
|
|
213
|
+
src/pages/products/[slug]/
|
|
214
|
+
└── page.jay-html # Gallery, options, quantity, actions
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Category Page
|
|
218
|
+
|
|
219
|
+
Uses a category contract with product grid:
|
|
220
|
+
|
|
221
|
+
```
|
|
222
|
+
src/pages/categories/[slug]/
|
|
223
|
+
└── page.jay-html # Category hero, product grid, load more
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Cart Page
|
|
227
|
+
|
|
228
|
+
Uses a cart contract with line items and checkout:
|
|
229
|
+
|
|
230
|
+
```
|
|
231
|
+
src/pages/cart/
|
|
232
|
+
└── page.jay-html # Line items, quantity controls, summary, coupon, checkout
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Homepage
|
|
236
|
+
|
|
237
|
+
Combines multiple headless components:
|
|
238
|
+
|
|
239
|
+
```
|
|
240
|
+
src/pages/
|
|
241
|
+
└── page.jay-html # Hero, featured products, categories, etc.
|
|
242
|
+
```
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Render Results
|
|
2
|
+
|
|
3
|
+
Each rendering phase (slow, fast) returns a render result indicating success, error, or redirect.
|
|
4
|
+
|
|
5
|
+
## phaseOutput — Success
|
|
6
|
+
|
|
7
|
+
Returns ViewState data and optional carry-forward data for the next phase:
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { phaseOutput } from '@jay-framework/fullstack-component';
|
|
11
|
+
|
|
12
|
+
return phaseOutput(
|
|
13
|
+
{ title: 'My Product', price: 29.99 }, // ViewState — sent to template
|
|
14
|
+
{ productId: 'abc123' }, // CarryForward — passed to next phase only
|
|
15
|
+
);
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
CarryForward is available in the next phase via `props.carryForward` but is not part of the ViewState.
|
|
19
|
+
|
|
20
|
+
## Error Results
|
|
21
|
+
|
|
22
|
+
Return errors to stop rendering and show an error page:
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import {
|
|
26
|
+
notFound,
|
|
27
|
+
badRequest,
|
|
28
|
+
unauthorized,
|
|
29
|
+
forbidden,
|
|
30
|
+
serverError5xx,
|
|
31
|
+
clientError4xx,
|
|
32
|
+
} from '@jay-framework/fullstack-component';
|
|
33
|
+
|
|
34
|
+
// 404
|
|
35
|
+
if (!product) return notFound('Product not found');
|
|
36
|
+
|
|
37
|
+
// 400
|
|
38
|
+
if (!input.query) return badRequest('Query is required');
|
|
39
|
+
|
|
40
|
+
// 401
|
|
41
|
+
if (!session) return unauthorized('Please log in');
|
|
42
|
+
|
|
43
|
+
// 403
|
|
44
|
+
if (!canAccess) return forbidden('Access denied');
|
|
45
|
+
|
|
46
|
+
// Custom 4xx
|
|
47
|
+
return clientError4xx(429, 'Rate limit exceeded');
|
|
48
|
+
|
|
49
|
+
// 5xx
|
|
50
|
+
return serverError5xx(500, 'Database connection failed');
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Redirects
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
import { redirect3xx } from '@jay-framework/fullstack-component';
|
|
57
|
+
|
|
58
|
+
return redirect3xx(301, '/new-location');
|
|
59
|
+
return redirect3xx(302, `/products/${product.slug}`);
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## RenderPipeline — Composable Rendering
|
|
63
|
+
|
|
64
|
+
For complex render logic, `RenderPipeline` chains operations with automatic error propagation:
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
import { RenderPipeline } from '@jay-framework/fullstack-component';
|
|
68
|
+
|
|
69
|
+
const Pipeline = RenderPipeline.for<SlowViewState, CarryForward>();
|
|
70
|
+
|
|
71
|
+
return Pipeline.try(() => db.getProduct(props.slug))
|
|
72
|
+
.map((product) => product ?? Pipeline.notFound('Product not found'))
|
|
73
|
+
.map(async (product) => ({
|
|
74
|
+
...product,
|
|
75
|
+
reviews: await db.getReviews(product.id),
|
|
76
|
+
}))
|
|
77
|
+
.toPhaseOutput((product) => ({
|
|
78
|
+
viewState: { title: product.name, price: product.price },
|
|
79
|
+
carryForward: { productId: product.id },
|
|
80
|
+
}));
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Pipeline Factory Methods
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
const P = RenderPipeline.for<VS, CF>();
|
|
87
|
+
|
|
88
|
+
P.ok(value); // Wrap a value
|
|
89
|
+
P.try(() => fetchData()); // Wrap a function (catches errors)
|
|
90
|
+
P.from(previousPhaseResult); // Continue from a prior phase result
|
|
91
|
+
P.notFound('message'); // Error pipeline
|
|
92
|
+
P.badRequest('message'); // Error pipeline
|
|
93
|
+
P.unauthorized('message'); // Error pipeline
|
|
94
|
+
P.forbidden('message'); // Error pipeline
|
|
95
|
+
P.serverError(500, 'message'); // Error pipeline
|
|
96
|
+
P.redirect(301, '/path'); // Redirect pipeline
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Pipeline Chain Methods
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
pipeline
|
|
103
|
+
.map(value => transform(value)) // Transform the value
|
|
104
|
+
.map(async value => await asyncOp(value)) // Async transform
|
|
105
|
+
.recover(error => P.ok(fallbackValue)) // Recover from errors
|
|
106
|
+
.toPhaseOutput(value => ({ // Convert to PhaseOutput
|
|
107
|
+
viewState: { ... },
|
|
108
|
+
carryForward: { ... },
|
|
109
|
+
}));
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Errors short-circuit — if any step returns an error pipeline, subsequent `.map()` calls are skipped.
|