@invisibleloop/pulse 0.2.15 → 0.4.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/CLAUDE.md +33 -22
- package/docs/public/pulse-ui.css +76 -0
- package/docs/public/pulse-ui.js +24 -9
- package/docs/server.js +106 -187
- package/docs/src/pages/components/nav.js +56 -6
- package/docs/src/pages/hydration.js +19 -10
- package/docs/src/pages/server-api.js +40 -34
- package/docs/src/pages/spec.js +3 -5
- package/examples/contact.js +0 -1
- package/examples/counter.js +0 -1
- package/examples/dev.server.js +10 -8
- package/examples/pricing.js +0 -1
- package/examples/products.js +0 -1
- package/examples/quiz.js +0 -1
- package/examples/todos.js +0 -1
- package/package.json +1 -1
- package/public/.pulse-ui-version +1 -1
- package/public/pulse-ui.css +76 -0
- package/public/pulse-ui.js +24 -9
- package/src/agent/checklist.md +1 -5
- package/src/server/index.js +63 -3
- package/src/server/server.test.js +6 -6
- package/src/spec/schema.js +0 -7
- package/src/ui/nav.js +57 -10
package/src/spec/schema.js
CHANGED
|
@@ -371,13 +371,6 @@ export function validateSpec(spec) {
|
|
|
371
371
|
}
|
|
372
372
|
}
|
|
373
373
|
|
|
374
|
-
// hydrate — required whenever there is any client interactivity
|
|
375
|
-
if (!spec.hydrate && !spec.contentType) {
|
|
376
|
-
if (spec.mutations || spec.actions || spec.persist) {
|
|
377
|
-
warnings.push('spec.hydrate is missing — pages with mutations, actions, or persist need hydrate set to their browser-importable path, or client interactivity will silently do nothing')
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
374
|
// meta quality
|
|
382
375
|
if (!spec.meta) {
|
|
383
376
|
warnings.push('spec.meta is missing — add at minimum title and description for SEO')
|
package/src/ui/nav.js
CHANGED
|
@@ -9,21 +9,69 @@
|
|
|
9
9
|
* reveals an absolutely-positioned dropdown that overlays page content.
|
|
10
10
|
* Wired automatically by pulse-ui.js — no extra JS needed.
|
|
11
11
|
*
|
|
12
|
+
* Links may be plain links or mega-nav dropdowns:
|
|
13
|
+
* { label: 'Docs', href: '/docs' }
|
|
14
|
+
* { label: 'Products', mega: [
|
|
15
|
+
* { heading: 'Solutions', items: [
|
|
16
|
+
* { label: 'Analytics', href: '/analytics', description: 'Track metrics' },
|
|
17
|
+
* ]},
|
|
18
|
+
* ]}
|
|
19
|
+
*
|
|
12
20
|
* @param {object} opts
|
|
13
|
-
* @param {string} opts.logo
|
|
14
|
-
* @param {string} opts.logoHref
|
|
15
|
-
* @param {Array<{label:
|
|
16
|
-
* @param {string} opts.action
|
|
17
|
-
* @param {boolean} opts.sticky
|
|
21
|
+
* @param {string} opts.logo - Raw HTML slot — SVG, img, or text
|
|
22
|
+
* @param {string} opts.logoHref - Logo link destination (default: '/')
|
|
23
|
+
* @param {Array<{label:string, href?:string, mega?:Array}>} opts.links
|
|
24
|
+
* @param {string} opts.action - Raw HTML slot — typically a button()
|
|
25
|
+
* @param {boolean} opts.sticky - Position sticky with backdrop blur
|
|
18
26
|
* @param {'right'|'left'} opts.burgerAlign - Mobile burger position (default: 'right')
|
|
19
27
|
* @param {string} opts.class
|
|
20
28
|
*/
|
|
21
29
|
|
|
22
30
|
import { escHtml as e } from '../html.js'
|
|
23
|
-
import { iconMenu, iconX } from './icons.js'
|
|
31
|
+
import { iconMenu, iconX, iconChevronDown } from './icons.js'
|
|
24
32
|
|
|
25
33
|
let _navId = 0
|
|
26
34
|
|
|
35
|
+
function megaItem({ label = '', href = '', description = '' }) {
|
|
36
|
+
return `<div class="ui-nav-mega-item">
|
|
37
|
+
<a href="${e(href)}" class="ui-nav-mega-item-label">${e(label)}</a>
|
|
38
|
+
${description ? `<span class="ui-nav-mega-item-desc">${e(description)}</span>` : ''}
|
|
39
|
+
</div>`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function megaPanel(columns, panelId) {
|
|
43
|
+
const cols = columns.map(col => `
|
|
44
|
+
<div class="ui-nav-mega-col">
|
|
45
|
+
${col.heading ? `<p class="ui-nav-mega-heading">${e(col.heading)}</p>` : ''}
|
|
46
|
+
${(col.items || []).map(megaItem).join('')}
|
|
47
|
+
</div>`).join('')
|
|
48
|
+
return `<div class="ui-nav-mega-panel" id="${panelId}" hidden>
|
|
49
|
+
<div class="ui-nav-mega-inner">${cols}
|
|
50
|
+
</div>
|
|
51
|
+
</div>`
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function renderLink(link, navId, idx) {
|
|
55
|
+
if (link.mega) {
|
|
56
|
+
const panelId = `${navId}-mega-${idx}`
|
|
57
|
+
return `<div class="ui-nav-mega-wrap">
|
|
58
|
+
<button type="button" class="ui-nav-link ui-nav-mega-trigger" aria-expanded="false" aria-haspopup="true" aria-controls="${panelId}">
|
|
59
|
+
${e(link.label || '')}${iconChevronDown({ size: 13, class: 'ui-nav-mega-chevron' })}
|
|
60
|
+
</button>
|
|
61
|
+
${megaPanel(link.mega, panelId)}
|
|
62
|
+
</div>`
|
|
63
|
+
}
|
|
64
|
+
return `<a href="${e(link.href || '')}" class="ui-nav-link">${e(link.label || '')}</a>`
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function renderMobileLink(link) {
|
|
68
|
+
if (link.mega) {
|
|
69
|
+
const items = link.mega.flatMap(col => col.items || [])
|
|
70
|
+
return `<p class="ui-nav-mobile-section">${e(link.label || '')}</p>` + items.map(item => megaItem(item)).join('')
|
|
71
|
+
}
|
|
72
|
+
return `<a href="${e(link.href || '')}" class="ui-nav-link">${e(link.label || '')}</a>`
|
|
73
|
+
}
|
|
74
|
+
|
|
27
75
|
export function nav({
|
|
28
76
|
logo = '',
|
|
29
77
|
logoHref = '/',
|
|
@@ -36,9 +84,8 @@ export function nav({
|
|
|
36
84
|
const id = `ui-nav-${++_navId}`
|
|
37
85
|
const classes = ['ui-nav', sticky && 'ui-nav--sticky', burgerAlign === 'left' && 'ui-nav--burger-left', cls].filter(Boolean).join(' ')
|
|
38
86
|
|
|
39
|
-
const linksHtml
|
|
40
|
-
|
|
41
|
-
).join('')
|
|
87
|
+
const linksHtml = links.map((l, i) => renderLink(l, id, i)).join('')
|
|
88
|
+
const mobileLinksHtml = links.map(renderMobileLink).join('')
|
|
42
89
|
|
|
43
90
|
const burgerHtml = links.length ? `
|
|
44
91
|
<button class="ui-nav-burger" type="button" aria-label="Toggle menu" aria-expanded="false" aria-controls="${id}-mobile">
|
|
@@ -48,7 +95,7 @@ export function nav({
|
|
|
48
95
|
|
|
49
96
|
const mobileMenuHtml = links.length ? `
|
|
50
97
|
<div class="ui-nav-mobile" id="${id}-mobile" aria-label="Mobile navigation">
|
|
51
|
-
<nav>${
|
|
98
|
+
<nav>${mobileLinksHtml}</nav>
|
|
52
99
|
</div>` : ''
|
|
53
100
|
|
|
54
101
|
return `<header class="${e(classes)}">
|