primo-cli 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/README.md +183 -0
- package/dist/commands/build.d.ts +6 -0
- package/dist/commands/build.js +379 -0
- package/dist/commands/deploy.d.ts +6 -0
- package/dist/commands/deploy.js +261 -0
- package/dist/commands/dev.d.ts +6 -0
- package/dist/commands/dev.js +516 -0
- package/dist/commands/export.d.ts +8 -0
- package/dist/commands/export.js +163 -0
- package/dist/commands/import.d.ts +9 -0
- package/dist/commands/import.js +118 -0
- package/dist/commands/init.d.ts +5 -0
- package/dist/commands/init.js +68 -0
- package/dist/commands/login.d.ts +7 -0
- package/dist/commands/login.js +124 -0
- package/dist/commands/new.d.ts +7 -0
- package/dist/commands/new.js +507 -0
- package/dist/commands/publish.d.ts +6 -0
- package/dist/commands/publish.js +239 -0
- package/dist/commands/pull.d.ts +8 -0
- package/dist/commands/pull.js +243 -0
- package/dist/commands/push.d.ts +9 -0
- package/dist/commands/push.js +118 -0
- package/dist/commands/validate.d.ts +7 -0
- package/dist/commands/validate.js +514 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +70 -0
- package/dist/utils/auth.d.ts +2 -0
- package/dist/utils/auth.js +29 -0
- package/dist/utils/binary.d.ts +5 -0
- package/dist/utils/binary.js +129 -0
- package/package.json +53 -0
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import inquirer from 'inquirer';
|
|
6
|
+
import { dev_server } from './dev.js';
|
|
7
|
+
export async function new_site(options) {
|
|
8
|
+
const base_dir = process.cwd();
|
|
9
|
+
const server_config_path = path.join(base_dir, 'server.json');
|
|
10
|
+
// Auto-create server.json if it doesn't exist
|
|
11
|
+
try {
|
|
12
|
+
await fs.access(server_config_path);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
await fs.writeFile(server_config_path, JSON.stringify({ port: 3000 }, null, 2) + '\n');
|
|
16
|
+
}
|
|
17
|
+
let site_name = options.name;
|
|
18
|
+
// Prompt for name if not provided
|
|
19
|
+
if (!site_name) {
|
|
20
|
+
const { name } = await inquirer.prompt([{
|
|
21
|
+
type: 'input',
|
|
22
|
+
name: 'name',
|
|
23
|
+
message: 'Site name:',
|
|
24
|
+
default: 'my-site',
|
|
25
|
+
validate: (input) => {
|
|
26
|
+
if (!input.trim())
|
|
27
|
+
return 'Name is required';
|
|
28
|
+
if (!/^[a-z0-9.-]+$/i.test(input))
|
|
29
|
+
return 'Use only letters, numbers, dots, and hyphens';
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
}]);
|
|
33
|
+
site_name = name;
|
|
34
|
+
}
|
|
35
|
+
// Always create site in subdirectory
|
|
36
|
+
const site_dir = path.join(base_dir, site_name);
|
|
37
|
+
// Check if directory exists
|
|
38
|
+
try {
|
|
39
|
+
await fs.access(site_dir);
|
|
40
|
+
console.log(chalk.red(`Directory "${site_name}" already exists`));
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Directory doesn't exist, good to proceed
|
|
45
|
+
}
|
|
46
|
+
const spinner = ora('Creating site...').start();
|
|
47
|
+
try {
|
|
48
|
+
// Create directory structure
|
|
49
|
+
await fs.mkdir(site_dir, { recursive: true });
|
|
50
|
+
await fs.mkdir(path.join(site_dir, 'blocks'), { recursive: true });
|
|
51
|
+
await fs.mkdir(path.join(site_dir, 'pages'), { recursive: true });
|
|
52
|
+
await fs.mkdir(path.join(site_dir, 'page-types', 'default'), { recursive: true });
|
|
53
|
+
await fs.mkdir(path.join(site_dir, 'site'), { recursive: true });
|
|
54
|
+
await fs.mkdir(path.join(site_dir, 'uploads'), { recursive: true });
|
|
55
|
+
// Create primo.json
|
|
56
|
+
// If name has dots (hostname), use first part capitalized as display name
|
|
57
|
+
const display_name = site_name.includes('.')
|
|
58
|
+
? site_name.split('.')[0].charAt(0).toUpperCase() + site_name.split('.')[0].slice(1)
|
|
59
|
+
: site_name.charAt(0).toUpperCase() + site_name.slice(1).replace(/-/g, ' ');
|
|
60
|
+
const config = {
|
|
61
|
+
name: display_name,
|
|
62
|
+
site_id: generate_id(),
|
|
63
|
+
// Leave host empty for local dev - dev.ts will generate coffee-shop.localhost:3000
|
|
64
|
+
// Only set host if it looks like a real domain (has a dot)
|
|
65
|
+
host: site_name.includes('.') ? site_name : ''
|
|
66
|
+
};
|
|
67
|
+
await fs.writeFile(path.join(site_dir, 'primo.json'), JSON.stringify(config, null, 2) + '\n');
|
|
68
|
+
// Create default page type config
|
|
69
|
+
const page_type_config = {
|
|
70
|
+
id: generate_id(),
|
|
71
|
+
name: 'Default',
|
|
72
|
+
icon: 'mdi:file-document-outline',
|
|
73
|
+
allowed_blocks: ['hero'],
|
|
74
|
+
fields: []
|
|
75
|
+
};
|
|
76
|
+
await fs.writeFile(path.join(site_dir, 'page-types', 'default', 'config.json'), JSON.stringify(page_type_config, null, 2) + '\n');
|
|
77
|
+
// Create site fields (empty array)
|
|
78
|
+
await fs.writeFile(path.join(site_dir, 'site', 'fields.json'), '[]\n');
|
|
79
|
+
// Create site content (empty)
|
|
80
|
+
await fs.writeFile(path.join(site_dir, 'site', 'content.yaml'), '# Site-wide content\n');
|
|
81
|
+
// Create site head with CSS variables
|
|
82
|
+
await fs.writeFile(path.join(site_dir, 'site', 'head.svelte'), `<style>
|
|
83
|
+
:root {
|
|
84
|
+
--theme-primary: #6366f1;
|
|
85
|
+
--theme-primary-dark: #4f46e5;
|
|
86
|
+
--theme-background: #ffffff;
|
|
87
|
+
--theme-background-secondary: #f8fafc;
|
|
88
|
+
--theme-text: #0f172a;
|
|
89
|
+
--theme-text-muted: #64748b;
|
|
90
|
+
--theme-border-color: #e2e8f0;
|
|
91
|
+
--theme-heading-font: system-ui, -apple-system, sans-serif;
|
|
92
|
+
--theme-body-font: system-ui, -apple-system, sans-serif;
|
|
93
|
+
--theme-section-padding: 5rem;
|
|
94
|
+
}
|
|
95
|
+
</style>
|
|
96
|
+
`);
|
|
97
|
+
// Create starter hero block
|
|
98
|
+
await fs.mkdir(path.join(site_dir, 'blocks', 'hero'), { recursive: true });
|
|
99
|
+
const hero_fields = {
|
|
100
|
+
id: generate_id(),
|
|
101
|
+
name: 'Hero',
|
|
102
|
+
fields: [
|
|
103
|
+
{ id: generate_id(), name: 'headline', label: 'Headline', type: 'text', options: null },
|
|
104
|
+
{ id: generate_id(), name: 'subheadline', label: 'Subheadline', type: 'text', options: null },
|
|
105
|
+
{ id: generate_id(), name: 'cta', label: 'Call to Action', type: 'link', options: null }
|
|
106
|
+
]
|
|
107
|
+
};
|
|
108
|
+
await fs.writeFile(path.join(site_dir, 'blocks', 'hero', 'fields.json'), JSON.stringify(hero_fields, null, 2) + '\n');
|
|
109
|
+
await fs.writeFile(path.join(site_dir, 'blocks', 'hero', 'component.svelte'), `<section class="hero">
|
|
110
|
+
<div class="container">
|
|
111
|
+
<h1>{headline}</h1>
|
|
112
|
+
{#if subheadline}
|
|
113
|
+
<p class="subheadline">{subheadline}</p>
|
|
114
|
+
{/if}
|
|
115
|
+
{#if cta?.url}
|
|
116
|
+
<a href={cta.url} class="btn">{cta.label}</a>
|
|
117
|
+
{/if}
|
|
118
|
+
</div>
|
|
119
|
+
</section>
|
|
120
|
+
|
|
121
|
+
<script>
|
|
122
|
+
let { headline = '', subheadline = '', cta = {} } = $props()
|
|
123
|
+
</script>
|
|
124
|
+
|
|
125
|
+
<style>
|
|
126
|
+
.hero {
|
|
127
|
+
padding: var(--theme-section-padding, 5rem) 0;
|
|
128
|
+
background: var(--theme-background-secondary, #f8fafc);
|
|
129
|
+
text-align: center;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.container {
|
|
133
|
+
max-width: 800px;
|
|
134
|
+
margin: 0 auto;
|
|
135
|
+
padding: 0 1.5rem;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
h1 {
|
|
139
|
+
font-family: var(--theme-heading-font, system-ui);
|
|
140
|
+
font-size: clamp(2rem, 5vw, 3.5rem);
|
|
141
|
+
font-weight: 700;
|
|
142
|
+
color: var(--theme-text, #0f172a);
|
|
143
|
+
margin: 0 0 1rem;
|
|
144
|
+
line-height: 1.1;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.subheadline {
|
|
148
|
+
font-size: 1.25rem;
|
|
149
|
+
color: var(--theme-text-muted, #64748b);
|
|
150
|
+
margin: 0 0 2rem;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.btn {
|
|
154
|
+
display: inline-block;
|
|
155
|
+
padding: 0.875rem 2rem;
|
|
156
|
+
background: var(--theme-primary, #6366f1);
|
|
157
|
+
color: white;
|
|
158
|
+
text-decoration: none;
|
|
159
|
+
border-radius: 0.5rem;
|
|
160
|
+
font-weight: 500;
|
|
161
|
+
transition: background 0.2s;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.btn:hover {
|
|
165
|
+
background: var(--theme-primary-dark, #4f46e5);
|
|
166
|
+
}
|
|
167
|
+
</style>
|
|
168
|
+
`);
|
|
169
|
+
await fs.writeFile(path.join(site_dir, 'blocks', 'hero', 'content.yaml'), `headline: Welcome to ${display_name}
|
|
170
|
+
subheadline: Edit this content in your local files or CMS
|
|
171
|
+
cta:
|
|
172
|
+
label: Get Started
|
|
173
|
+
url: "#"
|
|
174
|
+
`);
|
|
175
|
+
// Create index page
|
|
176
|
+
const page_id = generate_id();
|
|
177
|
+
await fs.writeFile(path.join(site_dir, 'pages', 'index.yaml'), `id: ${page_id}
|
|
178
|
+
name: Home
|
|
179
|
+
slug: index
|
|
180
|
+
page_type: default
|
|
181
|
+
fields: {}
|
|
182
|
+
sections:
|
|
183
|
+
- block: hero
|
|
184
|
+
content:
|
|
185
|
+
headline: Welcome to ${display_name}
|
|
186
|
+
subheadline: Edit this content in your local files or CMS
|
|
187
|
+
cta:
|
|
188
|
+
label: Get Started
|
|
189
|
+
url: "#"
|
|
190
|
+
`);
|
|
191
|
+
// Create AGENT.md
|
|
192
|
+
await fs.writeFile(path.join(site_dir, 'AGENT.md'), generate_agent_md(display_name));
|
|
193
|
+
spinner.succeed(`Site created: ${chalk.cyan(site_dir)}`);
|
|
194
|
+
// Check if server is already running
|
|
195
|
+
const port = 3000;
|
|
196
|
+
const server_running = await is_server_running(port);
|
|
197
|
+
if (server_running) {
|
|
198
|
+
// Tell the server to reload and pick up the new site
|
|
199
|
+
try {
|
|
200
|
+
await fetch(`http://127.0.0.1:${port + 1}/reload`, { method: 'POST' });
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
// Reload server might not be running (older version)
|
|
204
|
+
}
|
|
205
|
+
console.log('');
|
|
206
|
+
console.log(chalk.dim(` http://${site_name}.localhost:${port}/`));
|
|
207
|
+
console.log('');
|
|
208
|
+
}
|
|
209
|
+
else if (!options.skipDev) {
|
|
210
|
+
// No server running, start one
|
|
211
|
+
console.log('');
|
|
212
|
+
await dev_server({ dir: base_dir, port: String(port) });
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
console.log('');
|
|
216
|
+
console.log(chalk.dim(' Next steps:'));
|
|
217
|
+
console.log(chalk.dim(' primo dev'));
|
|
218
|
+
console.log('');
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
spinner.fail(`Failed to create site: ${error instanceof Error ? error.message : error}`);
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function generate_id() {
|
|
227
|
+
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
|
228
|
+
let id = '';
|
|
229
|
+
for (let i = 0; i < 15; i++) {
|
|
230
|
+
id += chars[Math.floor(Math.random() * chars.length)];
|
|
231
|
+
}
|
|
232
|
+
return id;
|
|
233
|
+
}
|
|
234
|
+
async function is_server_running(port) {
|
|
235
|
+
try {
|
|
236
|
+
const controller = new AbortController();
|
|
237
|
+
const timeout = setTimeout(() => controller.abort(), 1000);
|
|
238
|
+
const response = await fetch(`http://127.0.0.1:${port}/api/health`, {
|
|
239
|
+
signal: controller.signal
|
|
240
|
+
});
|
|
241
|
+
clearTimeout(timeout);
|
|
242
|
+
return response.ok;
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
function generate_agent_md(site_name) {
|
|
249
|
+
return `# ${site_name}
|
|
250
|
+
|
|
251
|
+
Pala site for local development.
|
|
252
|
+
|
|
253
|
+
## Structure
|
|
254
|
+
|
|
255
|
+
\`\`\`
|
|
256
|
+
blocks/ # Svelte components with content fields
|
|
257
|
+
{name}/
|
|
258
|
+
component.svelte
|
|
259
|
+
fields.json
|
|
260
|
+
content.yaml # Default field values (optional)
|
|
261
|
+
page-types/ # Page templates
|
|
262
|
+
{name}/
|
|
263
|
+
config.json
|
|
264
|
+
pages/ # Page content (YAML)
|
|
265
|
+
index.yaml # Homepage
|
|
266
|
+
contact.yaml # Leaf page (/contact)
|
|
267
|
+
about/ # Section with children
|
|
268
|
+
index.yaml # /about
|
|
269
|
+
team.yaml # /about/team
|
|
270
|
+
site/ # Site-wide settings
|
|
271
|
+
fields.json
|
|
272
|
+
content.yaml
|
|
273
|
+
head.svelte # Injected into <head>
|
|
274
|
+
.primo/ # Internal metadata
|
|
275
|
+
\`\`\`
|
|
276
|
+
|
|
277
|
+
## Creating Blocks
|
|
278
|
+
|
|
279
|
+
Each block needs two files:
|
|
280
|
+
|
|
281
|
+
**component.svelte** - Svelte 5 component:
|
|
282
|
+
\`\`\`svelte
|
|
283
|
+
<h1>{headline}</h1>
|
|
284
|
+
{#if image?.url}
|
|
285
|
+
<img src={image.url} alt={image.alt} />
|
|
286
|
+
{/if}
|
|
287
|
+
|
|
288
|
+
<style>
|
|
289
|
+
h1 { font-size: 2rem; }
|
|
290
|
+
</style>
|
|
291
|
+
\`\`\`
|
|
292
|
+
|
|
293
|
+
**Note:** Props are auto-injected from fields.json. No need to declare \`$props()\` - just use the field names directly in your template.
|
|
294
|
+
|
|
295
|
+
**fields.json** - Field definitions:
|
|
296
|
+
\`\`\`json
|
|
297
|
+
{
|
|
298
|
+
"name": "Hero",
|
|
299
|
+
"fields": [
|
|
300
|
+
{ "name": "headline", "label": "Headline", "type": "text" },
|
|
301
|
+
{ "name": "image", "label": "Image", "type": "image" }
|
|
302
|
+
]
|
|
303
|
+
}
|
|
304
|
+
\`\`\`
|
|
305
|
+
|
|
306
|
+
## Field Types
|
|
307
|
+
|
|
308
|
+
### text
|
|
309
|
+
Single-line text input.
|
|
310
|
+
\`\`\`svelte
|
|
311
|
+
<h1>{headline}</h1>
|
|
312
|
+
\`\`\`
|
|
313
|
+
|
|
314
|
+
### rich-text
|
|
315
|
+
WYSIWYG editor. Outputs HTML.
|
|
316
|
+
\`\`\`svelte
|
|
317
|
+
{@html content}
|
|
318
|
+
\`\`\`
|
|
319
|
+
|
|
320
|
+
### markdown
|
|
321
|
+
Markdown editor. Outputs HTML.
|
|
322
|
+
\`\`\`svelte
|
|
323
|
+
{@html body}
|
|
324
|
+
\`\`\`
|
|
325
|
+
|
|
326
|
+
### image
|
|
327
|
+
Image upload. Returns \`{ url, alt, width, height }\`.
|
|
328
|
+
\`\`\`svelte
|
|
329
|
+
{#if image?.url}
|
|
330
|
+
<img src={image.url} alt={image.alt} />
|
|
331
|
+
{/if}
|
|
332
|
+
\`\`\`
|
|
333
|
+
|
|
334
|
+
### link
|
|
335
|
+
URL with label. Returns \`{ url, label }\`.
|
|
336
|
+
\`\`\`svelte
|
|
337
|
+
{#if cta?.url}
|
|
338
|
+
<a href={cta.url}>{cta.label}</a>
|
|
339
|
+
{/if}
|
|
340
|
+
\`\`\`
|
|
341
|
+
|
|
342
|
+
### url
|
|
343
|
+
Plain URL string.
|
|
344
|
+
\`\`\`svelte
|
|
345
|
+
<a href={website_url}>Visit</a>
|
|
346
|
+
\`\`\`
|
|
347
|
+
|
|
348
|
+
### icon
|
|
349
|
+
Icon picker. Returns SVG string.
|
|
350
|
+
\`\`\`svelte
|
|
351
|
+
{@html icon}
|
|
352
|
+
\`\`\`
|
|
353
|
+
|
|
354
|
+
### number
|
|
355
|
+
Numeric input.
|
|
356
|
+
\`\`\`json
|
|
357
|
+
{ "name": "columns", "type": "number", "options": { "min": 1, "max": 6 } }
|
|
358
|
+
\`\`\`
|
|
359
|
+
|
|
360
|
+
### switch
|
|
361
|
+
Boolean toggle.
|
|
362
|
+
\`\`\`svelte
|
|
363
|
+
{#if show_title}<h1>{title}</h1>{/if}
|
|
364
|
+
\`\`\`
|
|
365
|
+
|
|
366
|
+
### select
|
|
367
|
+
Dropdown selection.
|
|
368
|
+
\`\`\`json
|
|
369
|
+
{ "name": "align", "type": "select", "options": { "choices": ["left", "center", "right"] } }
|
|
370
|
+
\`\`\`
|
|
371
|
+
\`\`\`svelte
|
|
372
|
+
<div class="text-{align}">{content}</div>
|
|
373
|
+
\`\`\`
|
|
374
|
+
|
|
375
|
+
### repeater
|
|
376
|
+
List of items with nested fields.
|
|
377
|
+
\`\`\`json
|
|
378
|
+
{
|
|
379
|
+
"name": "features",
|
|
380
|
+
"type": "repeater",
|
|
381
|
+
"options": {
|
|
382
|
+
"fields": [
|
|
383
|
+
{ "name": "title", "type": "text" },
|
|
384
|
+
{ "name": "description", "type": "text" }
|
|
385
|
+
]
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
\`\`\`
|
|
389
|
+
\`\`\`svelte
|
|
390
|
+
{#each features as feature}
|
|
391
|
+
<div>
|
|
392
|
+
<h3>{feature.title}</h3>
|
|
393
|
+
<p>{feature.description}</p>
|
|
394
|
+
</div>
|
|
395
|
+
{/each}
|
|
396
|
+
\`\`\`
|
|
397
|
+
|
|
398
|
+
### group
|
|
399
|
+
Nested object of fields.
|
|
400
|
+
\`\`\`json
|
|
401
|
+
{
|
|
402
|
+
"name": "author",
|
|
403
|
+
"type": "group",
|
|
404
|
+
"options": {
|
|
405
|
+
"fields": [
|
|
406
|
+
{ "name": "name", "type": "text" },
|
|
407
|
+
{ "name": "avatar", "type": "image" }
|
|
408
|
+
]
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
\`\`\`
|
|
412
|
+
\`\`\`svelte
|
|
413
|
+
<div>{author.name}</div>
|
|
414
|
+
{#if author.avatar?.url}<img src={author.avatar.url} />{/if}
|
|
415
|
+
\`\`\`
|
|
416
|
+
|
|
417
|
+
### page
|
|
418
|
+
Reference to another page. Returns page data with \`_meta.url\`.
|
|
419
|
+
\`\`\`json
|
|
420
|
+
{ "name": "featured_post", "type": "page", "options": { "page_type": "blog-post" } }
|
|
421
|
+
\`\`\`
|
|
422
|
+
|
|
423
|
+
### page-list
|
|
424
|
+
All pages of a type.
|
|
425
|
+
\`\`\`json
|
|
426
|
+
{ "name": "posts", "type": "page-list", "options": { "page_type": "blog-post" } }
|
|
427
|
+
\`\`\`
|
|
428
|
+
|
|
429
|
+
### page-field
|
|
430
|
+
Reference a field from the current page type.
|
|
431
|
+
|
|
432
|
+
### site-field
|
|
433
|
+
Reference a site-wide field.
|
|
434
|
+
|
|
435
|
+
### slider
|
|
436
|
+
Range slider for numeric values.
|
|
437
|
+
\`\`\`json
|
|
438
|
+
{ "name": "opacity", "type": "slider", "options": { "min": 0, "max": 100, "step": 10 } }
|
|
439
|
+
\`\`\`
|
|
440
|
+
|
|
441
|
+
### date
|
|
442
|
+
Date picker.
|
|
443
|
+
|
|
444
|
+
### info
|
|
445
|
+
Display-only text for editors (not rendered in component).
|
|
446
|
+
|
|
447
|
+
## Svelte 5 Syntax
|
|
448
|
+
|
|
449
|
+
Components use Svelte 5:
|
|
450
|
+
- \`$state()\` for reactive variables
|
|
451
|
+
- \`$derived()\` for computed values
|
|
452
|
+
- \`$effect()\` for side effects
|
|
453
|
+
- \`onclick={handler}\` not \`on:click={handler}\`
|
|
454
|
+
|
|
455
|
+
## Editor Context
|
|
456
|
+
|
|
457
|
+
Check if component is running in the CMS editor:
|
|
458
|
+
\`\`\`svelte
|
|
459
|
+
let is_editor = $state(false)
|
|
460
|
+
|
|
461
|
+
if (typeof window !== 'undefined') {
|
|
462
|
+
is_editor = window.__PALA_CONTEXT__?.environment === 'editor'
|
|
463
|
+
}
|
|
464
|
+
\`\`\`
|
|
465
|
+
|
|
466
|
+
Use this for:
|
|
467
|
+
- Disabling fixed/sticky positioning
|
|
468
|
+
- Skipping scroll/resize listeners
|
|
469
|
+
- Showing placeholder content
|
|
470
|
+
|
|
471
|
+
## This Site
|
|
472
|
+
|
|
473
|
+
### Blocks
|
|
474
|
+
|
|
475
|
+
- \`hero\` - Hero
|
|
476
|
+
|
|
477
|
+
### Page Types
|
|
478
|
+
|
|
479
|
+
- \`default\` - Default
|
|
480
|
+
|
|
481
|
+
## Best Practices
|
|
482
|
+
|
|
483
|
+
### Safe Field Access
|
|
484
|
+
|
|
485
|
+
Always handle potentially undefined fields:
|
|
486
|
+
\`\`\`svelte
|
|
487
|
+
<!-- Images -->
|
|
488
|
+
{#if hero_image?.url}
|
|
489
|
+
<img src={hero_image.url} alt={hero_image.alt} />
|
|
490
|
+
{/if}
|
|
491
|
+
|
|
492
|
+
<!-- Links -->
|
|
493
|
+
<a href={cta?.url || '#'}>{cta?.label || 'Learn More'}</a>
|
|
494
|
+
|
|
495
|
+
<!-- Repeaters -->
|
|
496
|
+
{#each features || [] as feature}
|
|
497
|
+
<div>{feature.title}</div>
|
|
498
|
+
{/each}
|
|
499
|
+
\`\`\`
|
|
500
|
+
|
|
501
|
+
## Workflow
|
|
502
|
+
|
|
503
|
+
1. Run \`primo dev\` to start the local preview server
|
|
504
|
+
2. Edit blocks, pages, or site settings - changes auto-sync to dev server
|
|
505
|
+
3. Run \`primo push\` to deploy changes to a live server (if connected)
|
|
506
|
+
`;
|
|
507
|
+
}
|