@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 CHANGED
@@ -40,7 +40,6 @@ A spec is a plain JS object. Every property is optional except `route`, `state`,
40
40
  ```js
41
41
  export const mySpec = {
42
42
  route: '/path', // URL pattern, supports :params
43
- hydrate: '/path/to/spec.js',// browser-importable path → enables hydration
44
43
 
45
44
  meta: {
46
45
  title: 'Page Title',
@@ -199,29 +198,41 @@ The server auto-detects the manifest from `staticDir/dist/manifest.json` when `s
199
198
  ```js
200
199
  import { createServer } from './src/server/index.js'
201
200
 
202
- createServer([specA, specB], {
203
- port: 3000,
204
- stream: true, // streaming SSR (default)
205
- staticDir: 'public', // serve static files + auto-load manifest
206
- manifest: null, // explicit manifest path or object (overrides auto-detect)
207
- defaultCache: 3600, // default HTML cache TTL in seconds for all pages (prod only)
208
- // also accepts true (3600s + swr 86400s) or { public, maxAge, staleWhileRevalidate }
209
- // spec.cache overrides per-page; in-process + Cache-Control headers both set
210
- fetcherTimeout: 5000, // ms before any server fetcher times out (null = no limit)
211
- // spec.serverTimeout overrides per-page
212
- shutdownTimeout: 30000, // ms to wait for in-flight requests before force-exit on SIGTERM/SIGINT
213
- healthCheck: '/healthz', // built-in health endpoint path, or false to disable
214
- csp: { // extra sources merged into the framework's default CSP
215
- 'style-src': ['https://fonts.googleapis.com'],
216
- 'font-src': ['https://fonts.gstatic.com'],
217
- },
218
- resolveBrand: async (host) => db.brands.findBySlug(host.split('.')[0]),
219
- // multi-brand: result cached 60s, attached to ctx.brand
220
- onRequest: (req, res) => { /* return false to short-circuit */ },
221
- onError: (err, req, res) => { /* custom error handling */ }
222
- })
201
+ // Pass URL objects — hydrate is auto-derived, no need to set it in specs.
202
+ // Specs with mutations/actions/persist are hydrated automatically.
203
+ // Purely server-rendered specs get zero JavaScript.
204
+ await createServer(
205
+ [
206
+ new URL('./src/pages/home.js', import.meta.url),
207
+ new URL('./src/pages/contact.js', import.meta.url),
208
+ ],
209
+ {
210
+ port: 3000,
211
+ stream: true, // streaming SSR (default)
212
+ staticDir: 'public', // serve static files + auto-load manifest
213
+ root: new URL('.', import.meta.url), // project root for deriving browser paths
214
+ manifest: null, // explicit manifest path or object (overrides auto-detect)
215
+ defaultCache: 3600, // default HTML cache TTL in seconds for all pages (prod only)
216
+ // also accepts true (3600s + swr 86400s) or { public, maxAge, staleWhileRevalidate }
217
+ // spec.cache overrides per-page; in-process + Cache-Control headers both set
218
+ fetcherTimeout: 5000, // ms before any server fetcher times out (null = no limit)
219
+ // spec.serverTimeout overrides per-page
220
+ shutdownTimeout: 30000, // ms to wait for in-flight requests before force-exit on SIGTERM/SIGINT
221
+ healthCheck: '/healthz', // built-in health endpoint path, or false to disable
222
+ csp: { // extra sources merged into the framework's default CSP
223
+ 'style-src': ['https://fonts.googleapis.com'],
224
+ 'font-src': ['https://fonts.gstatic.com'],
225
+ },
226
+ resolveBrand: async (host) => db.brands.findBySlug(host.split('.')[0]),
227
+ // multi-brand: result cached 60s, attached to ctx.brand
228
+ onRequest: (req, res) => { /* return false to short-circuit */ },
229
+ onError: (err, req, res) => { /* custom error handling */ }
230
+ }
231
+ )
223
232
  ```
224
233
 
234
+ `createServer` is async — always `await` it. Spec objects are still accepted alongside URL objects for backwards compatibility.
235
+
225
236
  All specs are validated at startup — bad specs throw before the server accepts connections.
226
237
 
227
238
  ## Multi-brand Sites
@@ -1280,6 +1280,82 @@ hr.ui-divider {
1280
1280
  }
1281
1281
  .ui-nav-mobile .ui-nav-link:last-child { border-bottom: none; }
1282
1282
  .ui-nav-mobile-action { padding-top: .75rem; }
1283
+ .ui-nav-mobile-section {
1284
+ font-size: .7rem;
1285
+ font-weight: 600;
1286
+ text-transform: uppercase;
1287
+ letter-spacing: .08em;
1288
+ color: var(--ui-muted, var(--muted, #6b6b80));
1289
+ margin: .75rem 0 .25rem;
1290
+ padding: 0;
1291
+ }
1292
+
1293
+ /* ─── Mega nav ───────────────────────────────────────────────────────────── */
1294
+ .ui-nav-mega-wrap { position: relative; }
1295
+
1296
+ .ui-nav-mega-trigger {
1297
+ background: none;
1298
+ border: none;
1299
+ cursor: pointer;
1300
+ display: inline-flex;
1301
+ align-items: center;
1302
+ gap: .3rem;
1303
+ padding: 0;
1304
+ font: inherit;
1305
+ font-size: .875rem;
1306
+ }
1307
+
1308
+ .ui-nav-mega-chevron { transition: transform .2s ease; flex-shrink: 0; }
1309
+ .ui-nav-mega-wrap--open .ui-nav-mega-chevron { transform: rotate(180deg); }
1310
+
1311
+ .ui-nav-mega-panel {
1312
+ position: absolute;
1313
+ top: calc(100% + .875rem);
1314
+ left: 50%;
1315
+ transform: translateX(-50%);
1316
+ background: var(--ui-bg, var(--bg, #0d0d10));
1317
+ border: 1px solid var(--ui-border, var(--border, #232329));
1318
+ border-radius: var(--ui-radius, 8px);
1319
+ box-shadow: 0 8px 40px rgba(0,0,0,.35);
1320
+ padding: 1.5rem;
1321
+ z-index: 200;
1322
+ min-width: 360px;
1323
+ }
1324
+ .ui-nav-mega-panel[hidden] { display: none !important; }
1325
+
1326
+ .ui-nav-mega-inner { display: flex; gap: 2rem; }
1327
+
1328
+ .ui-nav-mega-col { flex: 1; min-width: 140px; }
1329
+
1330
+ .ui-nav-mega-heading {
1331
+ font-size: .7rem;
1332
+ font-weight: 600;
1333
+ text-transform: uppercase;
1334
+ letter-spacing: .08em;
1335
+ color: var(--ui-muted, var(--muted, #6b6b80));
1336
+ margin: 0 0 .5rem;
1337
+ padding: 0 .5rem;
1338
+ }
1339
+
1340
+ .ui-nav-mega-item {
1341
+ display: flex;
1342
+ flex-direction: column;
1343
+ gap: .15rem;
1344
+ padding: .5rem;
1345
+ border-radius: calc(var(--ui-radius, 8px) * .65);
1346
+ transition: background .15s;
1347
+ }
1348
+ .ui-nav-mega-item:hover { background: var(--ui-surface, var(--surface, #111116)); }
1349
+ .ui-nav-mega-item-label {
1350
+ font-size: .875rem;
1351
+ font-weight: 500;
1352
+ color: var(--ui-text, var(--text, #e2e2ea));
1353
+ text-decoration: none;
1354
+ }
1355
+ .ui-nav-mega-item-label:hover { color: var(--ui-accent, var(--accent, #9b8dff)); }
1356
+ .ui-nav-mega-item-desc { font-size: .75rem; color: var(--ui-muted, var(--muted, #6b6b80)); }
1357
+
1358
+ [data-theme="light"] .ui-nav-mega-panel { box-shadow: 0 8px 40px rgba(0,0,0,.12); }
1283
1359
 
1284
1360
  /* ─── App Badge ──────────────────────────────────────────────────────────── */
1285
1361
  .ui-app-badge {
@@ -131,19 +131,34 @@ document.addEventListener('keydown', (e) => {
131
131
  function initNav(el) {
132
132
  const burger = el.querySelector('.ui-nav-burger')
133
133
  const mobile = el.querySelector('.ui-nav-mobile')
134
- if (!burger || !mobile) return
135
134
 
136
- const open = () => { el.classList.add('ui-nav--open'); burger.setAttribute('aria-expanded', 'true') }
137
- const close = () => { el.classList.remove('ui-nav--open'); burger.setAttribute('aria-expanded', 'false') }
138
- const toggle = () => el.classList.contains('ui-nav--open') ? close() : open()
135
+ // Mobile burger
136
+ if (burger && mobile) {
137
+ const open = () => { el.classList.add('ui-nav--open'); burger.setAttribute('aria-expanded', 'true') }
138
+ const close = () => { el.classList.remove('ui-nav--open'); burger.setAttribute('aria-expanded', 'false') }
139
+ const toggle = () => el.classList.contains('ui-nav--open') ? close() : open()
139
140
 
140
- burger.addEventListener('click', toggle)
141
+ burger.addEventListener('click', toggle)
142
+ document.addEventListener('keydown', (e) => { if (e.key === 'Escape') close() })
143
+ document.addEventListener('click', (e) => { if (!el.contains(e.target)) close() })
144
+ mobile.querySelectorAll('.ui-nav-link').forEach(a => a.addEventListener('click', close))
145
+ }
146
+
147
+ // Mega nav dropdowns
148
+ el.querySelectorAll('.ui-nav-mega-wrap').forEach(wrap => {
149
+ const trigger = wrap.querySelector('.ui-nav-mega-trigger')
150
+ const panel = wrap.querySelector('.ui-nav-mega-panel')
151
+ if (!trigger || !panel) return
141
152
 
142
- document.addEventListener('keydown', (e) => { if (e.key === 'Escape') close() })
143
- document.addEventListener('click', (e) => { if (!el.contains(e.target)) close() })
153
+ const openMega = () => { panel.hidden = false; wrap.classList.add('ui-nav-mega-wrap--open'); trigger.setAttribute('aria-expanded', 'true') }
154
+ const closeMega = () => { panel.hidden = true; wrap.classList.remove('ui-nav-mega-wrap--open'); trigger.setAttribute('aria-expanded', 'false') }
155
+ const toggleMega = () => panel.hidden ? openMega() : closeMega()
144
156
 
145
- // Close when a mobile link is clicked (navigating away)
146
- mobile.querySelectorAll('.ui-nav-link').forEach(a => a.addEventListener('click', close))
157
+ trigger.addEventListener('click', (e) => { e.stopPropagation(); toggleMega() })
158
+ document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeMega() })
159
+ document.addEventListener('click', (e) => { if (!wrap.contains(e.target)) closeMega() })
160
+ panel.querySelectorAll('.ui-nav-mega-item').forEach(a => a.addEventListener('click', closeMega))
161
+ })
147
162
  }
148
163
 
149
164
  function runNavs() { document.querySelectorAll('.ui-nav').forEach(initNav) }
package/docs/server.js CHANGED
@@ -1,8 +1,8 @@
1
- import fs from 'fs'
2
- import { createServer } from '../src/server/index.js'
3
- import { initLayoutManifest } from './src/lib/layout.js'
4
- import { metrics } from './src/lib/stats.js'
5
- import { metricsStore } from './src/lib/metrics-store.js'
1
+ import fs from 'fs'
2
+ import { createServer } from '../src/server/index.js'
3
+ import { initLayoutManifest } from './src/lib/layout.js'
4
+ import { metrics } from './src/lib/stats.js'
5
+ import { metricsStore } from './src/lib/metrics-store.js'
6
6
  metricsStore.current = metrics
7
7
 
8
8
  // Populate hashed asset paths in layout.js before any page renders
@@ -10,194 +10,113 @@ try {
10
10
  const raw = fs.readFileSync(new URL('./public/dist/manifest.json', import.meta.url), 'utf8')
11
11
  initLayoutManifest(JSON.parse(raw))
12
12
  } catch { /* dev: no manifest yet, layout falls back to unhashed paths */ }
13
- import home from './src/pages/home.js'
14
- import howItWorks from './src/pages/how-it-works.js'
15
- import faq from './src/pages/faq.js'
16
- import config from './src/pages/config.js'
17
- import gettingStarted from './src/pages/getting-started.js'
18
- import slashCommands from './src/pages/slash-commands.js'
19
- import promptExamples from './src/pages/prompt-examples.js'
20
- import components from './src/pages/components.js'
21
- import projectStructure from './src/pages/project-structure.js'
22
- import spec from './src/pages/spec.js'
23
- import state from './src/pages/state.js'
24
- import mutations from './src/pages/mutations.js'
25
- import actions from './src/pages/actions.js'
26
- import validation from './src/pages/validation.js'
27
- import constraints from './src/pages/constraints.js'
28
- import persist from './src/pages/persist.js'
29
- import serverData from './src/pages/server-data.js'
30
- import routing from './src/pages/routing.js'
31
- import streaming from './src/pages/streaming.js'
32
- import caching from './src/pages/caching.js'
33
- import guard from './src/pages/guard.js'
34
- import rawResponses from './src/pages/raw-responses.js'
35
- import serverApi from './src/pages/server-api.js'
36
- import hydration from './src/pages/hydration.js'
37
- import navigation from './src/pages/navigation.js'
38
- import images from './src/pages/images.js'
39
- import extending from './src/pages/extending.js'
40
- import deploy from './src/pages/deploy.js'
41
- import supabase from './src/pages/supabase.js'
42
- import auth from './src/pages/auth.js'
43
- import stripe from './src/pages/stripe.js'
44
- import meta from './src/pages/meta.js'
45
- import performance from './src/pages/performance.js'
46
- import accessibility from './src/pages/accessibility.js'
47
- import testing from './src/pages/testing.js'
48
- import store from './src/pages/store.js'
49
13
 
50
- // Component pages
51
- import compButton from './src/pages/components/button.js'
52
- import compBadge from './src/pages/components/badge.js'
53
- import compCard from './src/pages/components/card.js'
54
- import compInput from './src/pages/components/input.js'
55
- import compSelect from './src/pages/components/select.js'
56
- import compTextarea from './src/pages/components/textarea.js'
57
- import compAlert from './src/pages/components/alert.js'
58
- import compStat from './src/pages/components/stat.js'
59
- import compAvatar from './src/pages/components/avatar.js'
60
- import compEmpty from './src/pages/components/empty.js'
61
- import compTable from './src/pages/components/table.js'
62
- import compNav from './src/pages/components/nav.js'
63
- import compHero from './src/pages/components/hero.js'
64
- import compAppBadge from './src/pages/components/app-badge.js'
65
- import compFeature from './src/pages/components/feature.js'
66
- import compTestimonial from './src/pages/components/testimonial.js'
67
- import compPricing from './src/pages/components/pricing.js'
68
- import compAccordion from './src/pages/components/accordion.js'
69
- import compContainer from './src/pages/components/container.js'
70
- import compSection from './src/pages/components/section.js'
71
- import compGrid from './src/pages/components/grid.js'
72
- import compStack from './src/pages/components/stack.js'
73
- import compCluster from './src/pages/components/cluster.js'
74
- import compDivider from './src/pages/components/divider.js'
75
- import compBanner from './src/pages/components/banner.js'
76
- import compMedia from './src/pages/components/media.js'
77
- import compTooltip from './src/pages/components/tooltip.js'
78
- import compModal from './src/pages/components/modal.js'
79
- import compCarousel from './src/pages/components/carousel.js'
80
- import compCta from './src/pages/components/cta.js'
81
- import compFooter from './src/pages/components/footer.js'
82
- import compCodeWindow from './src/pages/components/code-window.js'
83
- import compFieldset from './src/pages/components/fieldset.js'
84
- import compToggle from './src/pages/components/toggle.js'
85
- import compCheckbox from './src/pages/components/checkbox.js'
86
- import compFileUpload from './src/pages/components/fileupload.js'
87
- import compSlider from './src/pages/components/slider.js'
88
- import compSegmented from './src/pages/components/segmented.js'
89
- import compRadio from './src/pages/components/radio.js'
90
- import compRating from './src/pages/components/rating.js'
91
- import compSearch from './src/pages/components/search.js'
92
- import compSpinner from './src/pages/components/spinner.js'
93
- import compProgress from './src/pages/components/progress.js'
94
- import compBreadcrumbs from './src/pages/components/breadcrumbs.js'
95
- import compStepper from './src/pages/components/stepper.js'
96
- import compImage from './src/pages/components/image.js'
97
- import compPullquote from './src/pages/components/pullquote.js'
98
- import compProse from './src/pages/components/prose.js'
99
- import compHeading from './src/pages/components/heading.js'
100
- import compList from './src/pages/components/list.js'
101
- import compTimeline from './src/pages/components/timeline.js'
102
- import compCharts from './src/pages/components/charts.js'
103
- import compIcons from './src/pages/components/icons.js'
104
-
105
- createServer(
14
+ await createServer(
106
15
  [
107
- home,
108
- howItWorks,
109
- faq,
110
- config,
111
- gettingStarted,
112
- slashCommands,
113
- promptExamples,
114
- components,
115
- projectStructure,
116
- spec,
117
- state,
118
- mutations,
119
- actions,
120
- validation,
121
- constraints,
122
- persist,
123
- serverData,
124
- routing,
125
- streaming,
126
- caching,
127
- guard,
128
- rawResponses,
129
- serverApi,
130
- hydration,
131
- navigation,
132
- images,
133
- extending,
134
- deploy,
135
- supabase,
136
- auth,
137
- stripe,
138
- meta,
139
- performance,
140
- accessibility,
141
- testing,
142
- store,
143
- // Component pages
144
- compButton,
145
- compBadge,
146
- compCard,
147
- compInput,
148
- compSelect,
149
- compTextarea,
150
- compAlert,
151
- compStat,
152
- compAvatar,
153
- compEmpty,
154
- compTable,
155
- compNav,
156
- compHero,
157
- compAppBadge,
158
- compFeature,
159
- compTestimonial,
160
- compPricing,
161
- compAccordion,
162
- compContainer,
163
- compSection,
164
- compGrid,
165
- compStack,
166
- compCluster,
167
- compDivider,
168
- compBanner,
169
- compMedia,
170
- compTooltip,
171
- compModal,
172
- compCarousel,
173
- compCta,
174
- compFooter,
175
- compCodeWindow,
176
- compFieldset,
177
- compToggle,
178
- compCheckbox,
179
- compFileUpload,
180
- compSlider,
181
- compSegmented,
182
- compRadio,
183
- compRating,
184
- compSearch,
185
- compSpinner,
186
- compProgress,
187
- compBreadcrumbs,
188
- compStepper,
189
- compImage,
190
- compPullquote,
191
- compProse,
192
- compHeading,
193
- compList,
194
- compTimeline,
195
- compCharts,
196
- compIcons,
16
+ // Framework
17
+ new URL('./src/pages/home.js', import.meta.url),
18
+ new URL('./src/pages/how-it-works.js', import.meta.url),
19
+ new URL('./src/pages/faq.js', import.meta.url),
20
+ new URL('./src/pages/config.js', import.meta.url),
21
+ new URL('./src/pages/getting-started.js', import.meta.url),
22
+ new URL('./src/pages/slash-commands.js', import.meta.url),
23
+ new URL('./src/pages/prompt-examples.js', import.meta.url),
24
+ new URL('./src/pages/components.js', import.meta.url),
25
+ new URL('./src/pages/project-structure.js', import.meta.url),
26
+ new URL('./src/pages/spec.js', import.meta.url),
27
+ // State & Behaviour
28
+ new URL('./src/pages/state.js', import.meta.url),
29
+ new URL('./src/pages/mutations.js', import.meta.url),
30
+ new URL('./src/pages/actions.js', import.meta.url),
31
+ new URL('./src/pages/validation.js', import.meta.url),
32
+ new URL('./src/pages/constraints.js', import.meta.url),
33
+ new URL('./src/pages/persist.js', import.meta.url),
34
+ // Server
35
+ new URL('./src/pages/server-data.js', import.meta.url),
36
+ new URL('./src/pages/store.js', import.meta.url),
37
+ new URL('./src/pages/routing.js', import.meta.url),
38
+ new URL('./src/pages/streaming.js', import.meta.url),
39
+ new URL('./src/pages/caching.js', import.meta.url),
40
+ new URL('./src/pages/guard.js', import.meta.url),
41
+ new URL('./src/pages/raw-responses.js', import.meta.url),
42
+ new URL('./src/pages/server-api.js', import.meta.url),
43
+ new URL('./src/pages/extending.js', import.meta.url),
44
+ // Client
45
+ new URL('./src/pages/hydration.js', import.meta.url),
46
+ new URL('./src/pages/navigation.js', import.meta.url),
47
+ new URL('./src/pages/images.js', import.meta.url),
48
+ // Deployment
49
+ new URL('./src/pages/deploy.js', import.meta.url),
50
+ // Reference
51
+ new URL('./src/pages/meta.js', import.meta.url),
52
+ new URL('./src/pages/performance.js', import.meta.url),
53
+ new URL('./src/pages/accessibility.js', import.meta.url),
54
+ new URL('./src/pages/testing.js', import.meta.url),
55
+ // Integrations
56
+ new URL('./src/pages/supabase.js', import.meta.url),
57
+ new URL('./src/pages/auth.js', import.meta.url),
58
+ new URL('./src/pages/stripe.js', import.meta.url),
59
+ // UI Components
60
+ new URL('./src/pages/components/alert.js', import.meta.url),
61
+ new URL('./src/pages/components/avatar.js', import.meta.url),
62
+ new URL('./src/pages/components/badge.js', import.meta.url),
63
+ new URL('./src/pages/components/breadcrumbs.js', import.meta.url),
64
+ new URL('./src/pages/components/button.js', import.meta.url),
65
+ new URL('./src/pages/components/card.js', import.meta.url),
66
+ new URL('./src/pages/components/checkbox.js', import.meta.url),
67
+ new URL('./src/pages/components/carousel.js', import.meta.url),
68
+ new URL('./src/pages/components/charts.js', import.meta.url),
69
+ new URL('./src/pages/components/empty.js', import.meta.url),
70
+ new URL('./src/pages/components/fieldset.js', import.meta.url),
71
+ new URL('./src/pages/components/fileupload.js', import.meta.url),
72
+ new URL('./src/pages/components/heading.js', import.meta.url),
73
+ new URL('./src/pages/components/icons.js', import.meta.url),
74
+ new URL('./src/pages/components/image.js', import.meta.url),
75
+ new URL('./src/pages/components/input.js', import.meta.url),
76
+ new URL('./src/pages/components/list.js', import.meta.url),
77
+ new URL('./src/pages/components/modal.js', import.meta.url),
78
+ new URL('./src/pages/components/progress.js', import.meta.url),
79
+ new URL('./src/pages/components/prose.js', import.meta.url),
80
+ new URL('./src/pages/components/pullquote.js', import.meta.url),
81
+ new URL('./src/pages/components/radio.js', import.meta.url),
82
+ new URL('./src/pages/components/rating.js', import.meta.url),
83
+ new URL('./src/pages/components/search.js', import.meta.url),
84
+ new URL('./src/pages/components/segmented.js', import.meta.url),
85
+ new URL('./src/pages/components/select.js', import.meta.url),
86
+ new URL('./src/pages/components/slider.js', import.meta.url),
87
+ new URL('./src/pages/components/spinner.js', import.meta.url),
88
+ new URL('./src/pages/components/stat.js', import.meta.url),
89
+ new URL('./src/pages/components/stepper.js', import.meta.url),
90
+ new URL('./src/pages/components/table.js', import.meta.url),
91
+ new URL('./src/pages/components/textarea.js', import.meta.url),
92
+ new URL('./src/pages/components/timeline.js', import.meta.url),
93
+ new URL('./src/pages/components/toggle.js', import.meta.url),
94
+ new URL('./src/pages/components/tooltip.js', import.meta.url),
95
+ // Landing Components
96
+ new URL('./src/pages/components/accordion.js', import.meta.url),
97
+ new URL('./src/pages/components/app-badge.js', import.meta.url),
98
+ new URL('./src/pages/components/cta.js', import.meta.url),
99
+ new URL('./src/pages/components/feature.js', import.meta.url),
100
+ new URL('./src/pages/components/hero.js', import.meta.url),
101
+ new URL('./src/pages/components/nav.js', import.meta.url),
102
+ new URL('./src/pages/components/pricing.js', import.meta.url),
103
+ new URL('./src/pages/components/testimonial.js', import.meta.url),
104
+ // Layout Components
105
+ new URL('./src/pages/components/banner.js', import.meta.url),
106
+ new URL('./src/pages/components/cluster.js', import.meta.url),
107
+ new URL('./src/pages/components/code-window.js', import.meta.url),
108
+ new URL('./src/pages/components/container.js', import.meta.url),
109
+ new URL('./src/pages/components/divider.js', import.meta.url),
110
+ new URL('./src/pages/components/footer.js', import.meta.url),
111
+ new URL('./src/pages/components/grid.js', import.meta.url),
112
+ new URL('./src/pages/components/media.js', import.meta.url),
113
+ new URL('./src/pages/components/section.js', import.meta.url),
114
+ new URL('./src/pages/components/stack.js', import.meta.url),
197
115
  ],
198
116
  {
199
117
  port: process.env.PORT ? Number(process.env.PORT) : 4000,
200
118
  staticDir: new URL('./public', import.meta.url).pathname,
119
+ root: new URL('.', import.meta.url),
201
120
  defaultCache: true,
202
121
  csp: {
203
122
  'img-src': ['https://picsum.photos', 'https://images.unsplash.com'],
@@ -41,6 +41,47 @@ export default {
41
41
  })`
42
42
  )}
43
43
 
44
+ <h2 class="doc-h2" id="mega">Mega nav</h2>
45
+ <p>Replace a link's <code>href</code> with a <code>mega</code> array to render a dropdown panel. Each entry is a column with an optional heading and a list of items. Items support an optional <code>description</code> line.</p>
46
+ ${demo(
47
+ uiNav({
48
+ logo: '<strong>MyApp</strong>',
49
+ logoHref: '/',
50
+ links: [
51
+ { label: 'Products', mega: [
52
+ { heading: 'Platform', items: [
53
+ { label: 'Analytics', href: '#', description: 'Dashboards and usage metrics' },
54
+ { label: 'Automation', href: '#', description: 'Scheduled jobs and webhooks' },
55
+ { label: 'Storage', href: '#', description: 'Files, blobs, and databases' },
56
+ ]},
57
+ { heading: 'Integrations', items: [
58
+ { label: 'Supabase', href: '#' },
59
+ { label: 'Stripe', href: '#' },
60
+ { label: 'Auth0', href: '#' },
61
+ ]},
62
+ ]},
63
+ { label: 'Pricing', href: '#pricing' },
64
+ { label: 'Docs', href: '#docs' },
65
+ ],
66
+ action: button({ label: 'Get started', size: 'sm' }),
67
+ }),
68
+ `nav({
69
+ links: [
70
+ { label: 'Products', mega: [
71
+ { heading: 'Platform', items: [
72
+ { label: 'Analytics', href: '/analytics', description: 'Dashboards and usage metrics' },
73
+ { label: 'Automation', href: '/automation', description: 'Scheduled jobs and webhooks' },
74
+ ]},
75
+ { heading: 'Integrations', items: [
76
+ { label: 'Supabase', href: '/supabase' },
77
+ { label: 'Stripe', href: '/stripe' },
78
+ ]},
79
+ ]},
80
+ { label: 'Pricing', href: '/pricing' },
81
+ ],
82
+ })`
83
+ )}
84
+
44
85
  <h2 class="doc-h2" id="mobile">Mobile view</h2>
45
86
  <p>At &lt; 640px the links and action are hidden and replaced with a burger button. The panel opens as an overlay — no layout shift.</p>
46
87
 
@@ -73,12 +114,21 @@ export default {
73
114
  ${table(
74
115
  ['Prop', 'Type', 'Default', 'Description'],
75
116
  [
76
- ['<code>logo</code>', 'string (HTML)', '—', 'Raw HTML slot — SVG, img, or text'],
77
- ['<code>logoHref</code>', 'string', "'/'", ''],
78
- ['<code>links</code>', 'array', '[]', '<code>{ label, href }[]</code>'],
79
- ['<code>action</code>', 'string (HTML)', '—', 'Raw HTML slot — shown in desktop bar and mobile menu'],
80
- ['<code>sticky</code>', 'boolean', 'false', 'position: sticky with backdrop blur'],
81
- ['<code>burgerAlign</code>', 'string', "'right'", "'right' or 'left' — mobile burger position"],
117
+ ['<code>logo</code>', 'string (HTML)', '—', 'Raw HTML slot — SVG, img, or text'],
118
+ ['<code>logoHref</code>', 'string', "'/'", ''],
119
+ ['<code>links</code>', 'array', '[]', '<code>{ label, href }[]</code> or <code>{ label, mega: Column[] }</code>'],
120
+ ['<code>action</code>', 'string (HTML)', '—', 'Raw HTML slot — shown in desktop bar and mobile menu'],
121
+ ['<code>sticky</code>', 'boolean', 'false', 'position: sticky with backdrop blur'],
122
+ ['<code>burgerAlign</code>', 'string', "'right'", "'right' or 'left' — mobile burger position"],
123
+ ]
124
+ )}
125
+
126
+ <h3 class="doc-h3" style="margin-top:2rem">Mega nav column</h3>
127
+ ${table(
128
+ ['Prop', 'Type', 'Description'],
129
+ [
130
+ ['<code>heading</code>', 'string', 'Optional column heading (uppercase label)'],
131
+ ['<code>items</code>', 'array', '<code>{ label, href, description? }[]</code>'],
82
132
  ]
83
133
  )}
84
134
  `,