@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.
@@ -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 - Raw HTML slot — SVG, img, or text
14
- * @param {string} opts.logoHref - Logo link destination (default: '/')
15
- * @param {Array<{label: string, href: string}>} opts.links
16
- * @param {string} opts.action - Raw HTML slot — typically a button()
17
- * @param {boolean} opts.sticky - Position sticky with backdrop blur
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 = links.map(({ label = '', href = '' }) =>
40
- `<a href="${e(href)}" class="ui-nav-link">${e(label)}</a>`
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>${linksHtml}</nav>
98
+ <nav>${mobileLinksHtml}</nav>
52
99
  </div>` : ''
53
100
 
54
101
  return `<header class="${e(classes)}">