@invisibleloop/pulse 0.20.58 → 0.20.59

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@invisibleloop/pulse",
3
- "version": "0.20.58",
3
+ "version": "0.20.59",
4
4
  "type": "module",
5
5
  "description": "AI-first frontend framework. The spec is the source of truth.",
6
6
  "license": "MIT",
@@ -1 +1 @@
1
- 0.20.58
1
+ 0.20.59
@@ -3026,3 +3026,9 @@ fieldset[disabled] .ui-rating-stars .ui-rating-star:hover ~ .ui-rating-star { co
3026
3026
  line-height: 0;
3027
3027
  }
3028
3028
 
3029
+ /* ─── Framework skip link ───────────────────────────────────────────────────
3030
+ * Injected by the server's buffered render path (404s, POST re-renders, stream: false).
3031
+ * Hidden until keyboard focus, like the one in pulse.css — pages that load only
3032
+ * pulse-ui.css must not see it as a plain link at the top of the page. */
3033
+ .pulse-skip-link{position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden}
3034
+ .pulse-skip-link:focus{position:fixed;left:1rem;top:1rem;width:auto;height:auto;padding:.5rem 1rem;background:#000;color:#fff;font-size:1rem;font-weight:600;border-radius:4px;text-decoration:none;z-index:9999;outline:3px solid #fff;outline-offset:2px}
@@ -9,8 +9,13 @@
9
9
  // CSS class extractor
10
10
  // ---------------------------------------------------------------------------
11
11
 
12
+ // Classes the framework itself writes into the document (src/runtime/ssr.js wrapDocument),
13
+ // outside any page's view, so nothing the build scans ever contains them. Always kept:
14
+ // without this a built app loses the rules that hide the injected skip link (#161).
15
+ export const FRAMEWORK_INJECTED_CLASSES = ['pulse-skip-link']
16
+
12
17
  export function extractUsedClasses(htmlContents, jsContents, safelist = []) {
13
- const used = new Set()
18
+ const used = new Set(FRAMEWORK_INJECTED_CLASSES)
14
19
 
15
20
  // Extract from class="..." in rendered HTML (no template expressions to worry about)
16
21
  for (const content of htmlContents) {
@@ -605,7 +605,7 @@ Rules:
605
605
  Every page must be fully navigable by keyboard alone.
606
606
 
607
607
  **Structure**
608
- - Wrap page content in \`<main id="main-content">\` — the skip link injected by the framework targets this id
608
+ - Wrap page content in \`<main id="main-content">\` — a skip link targets this id (add your own to the layout; the framework only injects a fallback on 404s and form re-renders)
609
609
  - Use semantic elements: \`<nav>\`, \`<main>\`, \`<header>\`, \`<footer>\`, \`<section>\`, \`<article>\`, \`<aside>\`
610
610
  - One \`<h1>\` per page — the primary heading that describes the current page
611
611
 
@@ -896,7 +896,7 @@ Threshold merge order: **global config → environment override → per-route ov
896
896
  - **Redirect loops** — if \`/dashboard\` guards redirect to \`/login\`, and \`/login\` guards redirect authenticated users to \`/dashboard\`, test both directions to confirm there is no loop.
897
897
 
898
898
  ### Performance & Lighthouse
899
- - **Missing \`<main id="main-content">\`** — the framework injects a skip link targeting \`#main-content\` on every page. A missing element breaks the skip link and fails Lighthouse Accessibility.
899
+ - **Missing \`<main id="main-content">\`** — skip links target \`#main-content\`. A missing element breaks the skip link and fails Lighthouse Accessibility.
900
900
  - **Multiple \`<h1>\` elements** — one \`<h1>\` per page. Multiple at the same level fail Lighthouse Accessibility.
901
901
  - **Images without \`width\` and \`height\`** — omitting dimensions prevents the browser from reserving space, causing layout shift and a non-zero CLS. Always provide both.
902
902
  - **Colour contrast** — \`#888\` on \`#111\` is the practical minimum for muted text that passes WCAG AA. Lighter greys will fail. Check new colour tokens before committing.
@@ -909,7 +909,7 @@ Threshold merge order: **global config → environment override → per-route ov
909
909
  ## Rules the agent must follow
910
910
 
911
911
  - **Never set \`hydrate\`** — the framework sets it automatically.
912
- - **Always wrap page content in \`<main id="main-content">\`** — the framework injects a skip link targeting this id on every page.
912
+ - **Always wrap page content in \`<main id="main-content">\`** — skip links target this id.
913
913
  - **Never use \`data-event\` on text inputs** to mirror value into state. It destroys focus on every keystroke. Use uncontrolled inputs and read values from \`FormData\` in \`action.onStart\` instead. For client-side filtering/search, render all items in the HTML and use an inline \`<script>\` to show/hide elements — no state or re-render needed.
914
914
  - **Never add \`<script>\` tags manually** — hydration is handled by the framework.
915
915
  - **Never add external npm packages** for client-side behaviour — express it in the spec.
@@ -343,6 +343,10 @@ export function wrapDocument({ content, spec = {}, serverState = {}, storeState
343
343
  const meta = spec.meta || {}
344
344
  const title = meta.title || 'Pulse'
345
345
 
346
+ // The fallback skip link is only for pages that have none: a page (or its layout) that
347
+ // already links to #main-content supplies its own, and two would be a redundant tab stop.
348
+ const ownSkipLink = /href=["']#main-content["']/.test(content)
349
+
346
350
  const bodyAttr = [
347
351
  meta.theme ? `data-theme="${esc(meta.theme)}"` : '',
348
352
  meta.vibe ? `data-vibe="${esc(meta.vibe)}"` : '',
@@ -419,7 +423,7 @@ export function wrapDocument({ content, spec = {}, serverState = {}, storeState
419
423
  ${schemaScript}
420
424
  </head>
421
425
  <body${bodyAttr}>
422
- <a href="#main-content" class="pulse-skip-link">Skip to main content</a>
426
+ ${ownSkipLink ? '' : '<a href="#main-content" class="pulse-skip-link">Skip to main content</a>'}
423
427
  <div id="pulse-root">
424
428
  ${content}
425
429
  </div>