diff --git a/.github/workflows/deploy-docs-pages.yml b/.github/workflows/deploy-docs-pages.yml index f043171..e6018be 100644 --- a/.github/workflows/deploy-docs-pages.yml +++ b/.github/workflows/deploy-docs-pages.yml @@ -5,6 +5,7 @@ on: branches: [main] paths: - 'web_examples/**' + - 'packages/axoloth-behavior/src/**' - 'packages/axoloth-style/src/**' - 'packages/axoloth-style/metadata/**' - '.github/workflows/deploy-docs-pages.yml' @@ -53,11 +54,14 @@ jobs: set -euo pipefail rm -rf dist-pages mkdir -p dist-pages/packages/axoloth-style + mkdir -p dist-pages/packages/axoloth-behavior cp -R web_examples/. dist-pages/ + cp -R packages/axoloth-behavior/src dist-pages/packages/axoloth-behavior/src cp -R packages/axoloth-style/src dist-pages/packages/axoloth-style/src sed -i 's#../packages/axoloth-style/src/axoloth.css#./packages/axoloth-style/src/axoloth.css#g' dist-pages/index.html find dist-pages/examples -name index.html -print0 | xargs -0 sed -i 's#../../../packages/axoloth-style/src/axoloth.css#../../packages/axoloth-style/src/axoloth.css#g' find dist-pages/docs -name index.html -print0 | xargs -0 sed -i 's#../../../packages/axoloth-style/src/axoloth.css#../../packages/axoloth-style/src/axoloth.css#g' + find dist-pages/recipes -name index.html -print0 | xargs -0 sed -i 's#../../../packages/axoloth-style/src/axoloth.css#../../packages/axoloth-style/src/axoloth.css#g' - name: Configure GitHub Pages uses: actions/configure-pages@v5 diff --git a/README.md b/README.md index 5feb223..df8aece 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,10 @@ For browser-native Vanilla JavaScript, import a pinned behavior module: ``` Behavior is never initialized by the CSS package. Import and initialize only -the components the page uses. +the components the page uses. See the +[Behavior Guide](https://amilliondriver.github.io/MotionStyleLibrary/docs/behavior/) +for installation, initialize-all and per-component patterns, cleanup, +troubleshooting, and runnable Vanilla examples. ### Bundler And Modular CSS diff --git a/packages/axoloth-behavior/README.md b/packages/axoloth-behavior/README.md index 2e24f22..9ee3010 100644 --- a/packages/axoloth-behavior/README.md +++ b/packages/axoloth-behavior/README.md @@ -4,6 +4,12 @@ Optional zero-dependency JavaScript behaviors for `@quertys/axoloth-style`. Axoloth Behavior keeps interactive state separate from the CSS-first package. Install it only when a layout needs tabs, accordions, dropdowns, toasts, drawers, an off-canvas sidebar, or a dialog. The package is framework-neutral and works with plain HTML, React, Vue, Svelte, Angular, or any DOM-based application. +> Axoloth Style provides layout and presentation. Axoloth Behavior attaches interaction to +> `data-axo-*` attributes. Importing the CSS alone never initializes JavaScript behavior. + +Read the [Behavior Guide and live Vanilla demos](https://amilliondriver.github.io/MotionStyleLibrary/docs/behavior/) +for runnable tabs, accordion, dialog, and drawer examples. + ## API Stability Version `0.6.0` validates package exports, initializers, declarative attributes, @@ -17,7 +23,47 @@ and custom events against the reviewed `0.4.0` baseline. Read npm install @quertys/axoloth-style @quertys/axoloth-behavior ``` -Import the Axoloth CSS once, then initialize only the behavior you use: +## Initialize Everything + +Import the Axoloth CSS once, then initialize the behavior package after the DOM is available: + +```js +import '@quertys/axoloth-style/axoloth.css'; +import { initAxolothBehaviors } from '@quertys/axoloth-behavior'; + +const axoloth = initAxolothBehaviors(); + +// Remove every listener when the page or application is disposed. +window.addEventListener('pagehide', () => axoloth.destroy(), { once: true }); +``` + +`initAxolothBehaviors()` initializes every exported behavior and returns their controllers under +`accordion`, `dialog`, `drawer`, `dropdown`, `offcanvas`, `tabs`, and `toast`. + +## CDN / Native ES Modules + +No bundler is required. Load the CSS with a stylesheet link and import the JavaScript from an ES +module script: + +```html + + + +``` + +Pin both versions in production so a future release cannot change a deployed page unexpectedly. + +## Initialize One Behavior + +Import only the behavior used by the page when you want a smaller, explicit setup: ```js import '@quertys/axoloth-style/axoloth.css'; @@ -42,6 +88,21 @@ All initializers accept an optional root as their first argument and options as their second argument. For example, configure Toast with `initToast(document, { duration: 4500, limit: 3 })`. +Scope an initializer to one part of a page and clean it up independently: + +```js +import { initTabs } from '@quertys/axoloth-behavior/tabs'; + +const accountSection = document.querySelector('#account-section'); +const tabs = initTabs(accountSection); + +// Re-scan after adding matching markup dynamically. +tabs.refresh(); + +// Remove listeners before replacing or unmounting the section. +tabs.destroy(); +``` + ## Tabs ```html @@ -308,18 +369,42 @@ dialogElement.addEventListener('axo:dialog-close', () => { Off-canvas controllers dispatch `axo:offcanvas-open` and `axo:offcanvas-close`. -## Initialize All Available Behaviors +## Troubleshooting -```js -import { initAxolothBehaviors } from '@quertys/axoloth-behavior'; +### `data-axo-*` attributes do nothing -const axoloth = initAxolothBehaviors(); +The package intentionally does not auto-initialize. Confirm that the behavior package is imported +from a ` + + + + + diff --git a/web_examples/index.html b/web_examples/index.html index 69b56ae..8e4373b 100644 --- a/web_examples/index.html +++ b/web_examples/index.html @@ -29,7 +29,7 @@ @@ -197,6 +197,45 @@

CSS + Axoloth Behavior

import { initTabs } from 'https://cdn.jsdelivr.net/npm/@quertys/axoloth-behavior@0.6.0/src/tabs.js'; initTabs(); </script> +
+ Open the complete Behavior Guide +
+ + +
+

Composition Recipes

+
+

Real layouts from existing primitives

+

Loading recipes...

+
+ +

+ Each recipe includes a live preview, complete HTML, the Axoloth classes and variables it + uses, and an explicit boundary for project-owned CSS. No recipe adds a new utility. +

+ +
+ + + + + + + + + + + + + + +
Recipe NamePreviewSourceVersion
Loading recipes...
+
diff --git a/web_examples/package.json b/web_examples/package.json index 458c9e3..a9b4092 100644 --- a/web_examples/package.json +++ b/web_examples/package.json @@ -4,6 +4,6 @@ "private": true, "description": "Static documentation and integration examples for Axoloth Style.", "scripts": { - "check": "node --check scripts/examples.js && node --check scripts/theme.js && node --check scripts/docs-sidebar.js && node --check scripts/component-docs.js && node --check scripts/docs-copy.js && node scripts/verify-docs.mjs" + "check": "node --check scripts/examples.js && node --check scripts/theme.js && node --check scripts/docs-sidebar.js && node --check scripts/component-docs.js && node --check scripts/docs-copy.js && node --check recipes/gallery-dialog/gallery-dialog.js && node scripts/verify-docs.mjs" } } diff --git a/web_examples/recipes/editorial-split/index.html b/web_examples/recipes/editorial-split/index.html new file mode 100644 index 0000000..36d18ee --- /dev/null +++ b/web_examples/recipes/editorial-split/index.html @@ -0,0 +1,228 @@ + + + + + + + + + + Editorial Split Recipe - Axoloth Style Docs + + +
+ Axoloth Style + + + +
+ +
+ + +
+
+

Composition Recipe 02

+

Editorial Split Layout

+

+ Pair a long-form story with portrait media using intrinsic widths rather than a fixed + column count. axo-split wraps only when the content actually needs it. +

+ +
+ +
+

Live Preview

+

Studio profile

+

The media and article negotiate their own width, then become one reading column.

+ +
+
+
+ Studio Notes + + +
+
+

Ink, pressure, and the patience of repetition.

+

+ Printmaker Mara Seno works slowly by design. Each block is cut by hand, proofed in + daylight, and revised until the image carries the grain of the material. +

+
+ “The press records every decision, including the ones I thought nobody would see.” +
+ +
+ +
+
+ Printmaker standing beside a sunlit studio table with paper and carved blocks +
+
+ Mara prepares a small edition beside the north-facing window of her shared studio. +
+
+
+
+ +
+

Complete HTML

+

A standalone editorial feature

+
<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <link rel="stylesheet"
+      href="https://cdn.jsdelivr.net/npm/@quertys/axoloth-style@0.9.0/src/axoloth.css" />
+    <link rel="stylesheet" href="./theme.css" />
+    <title>Studio Notes</title>
+  </head>
+  <body>
+    <main class="axo-page">
+      <article class="recipe-editorial axo-split" aria-labelledby="story-title">
+        <div class="recipe-editorial__copy axo-flow">
+          <div class="recipe-editorial__meta axo-cluster">
+            <span>Studio Notes</span>
+            <time datetime="2026-07-21">21 July 2026</time>
+          </div>
+          <hr class="recipe-editorial__rule" />
+          <h1 id="story-title">Ink, pressure, and the patience of repetition.</h1>
+          <p>Each block is cut by hand, proofed in daylight, and revised slowly.</p>
+          <blockquote class="recipe-editorial__quote">
+            The press records every decision, including the ones nobody sees.
+          </blockquote>
+          <a class="axo-button axo-link recipe-editorial__action" href="#story">
+            Read the full story
+          </a>
+        </div>
+        <figure class="recipe-editorial__figure">
+          <div class="recipe-editorial__image axo-frame">
+            <img src="./printmaker.png" alt="Printmaker working in a sunlit studio" />
+          </div>
+          <figcaption>Preparing a small edition beside the studio window.</figcaption>
+        </figure>
+      </article>
+    </main>
+  </body>
+</html>
+
+ +
+
+
+

Axoloth Classes

+

Structure supplied by the library

+
    +
  • + axo-splitCreates two intrinsic regions that wrap when space runs out. +
  • +
  • + axo-flowMaintains readable vertical rhythm in the article. +
  • +
  • + axo-frameStabilizes and crops the portrait media. +
  • +
  • axo-clusterWraps metadata and actions cleanly.
  • +
  • + axo-button + axo-linkProvide neutral calls to action. +
  • +
+
+
+

Customizable Variables

+

Supported tuning points

+
    +
  • + --axo-split-minMinimum viable width of each region. +
  • +
  • --axo-split-gapSpace between copy and media.
  • +
  • + --axo-split-leading-growRelative growth of the article region. +
  • +
  • + --axo-split-trailing-growRelative growth of the media region. +
  • +
  • --axo-flow-spaceArticle rhythm.
  • +
  • + --axo-frame-ratio + --axo-frame-positionPortrait dimensions and crop. +
  • +
+
+
+
+ +
+
+
+

Project-owned CSS

+

The editorial voice remains yours

+

+ Serif typography, the red rule, quote treatment, paper palette, image radius, and + caption style live in theme.css. They are publication choices rather + than reusable layout API. +

+
+
+

Mobile + Desktop

+

Content determines the switch

+

+ Desktop gives the copy and portrait proportional space. On narrow screens the split + wraps, the image moves first for visual context, and the article remains a normal + document flow. +

+
+
+
+
+
+ + + +
+ + + + + diff --git a/web_examples/recipes/editorial-split/theme.css b/web_examples/recipes/editorial-split/theme.css new file mode 100644 index 0000000..737788c --- /dev/null +++ b/web_examples/recipes/editorial-split/theme.css @@ -0,0 +1,96 @@ +.recipe-editorial { + --axo-flow-space: 1.2rem; + --axo-frame-position: center 35%; + --axo-frame-ratio: 4 / 5; + --axo-split-gap: clamp(1.5rem, 5vw, 5rem); + --axo-split-leading-grow: 6; + --axo-split-min: 20rem; + --axo-split-trailing-grow: 5; + + padding: clamp(1.25rem, 5vw, 4rem); + border: 1px solid #c9c0b2; + border-radius: 0.75rem; + color: #24221e; + background: #f3efe7; +} + +.recipe-editorial__copy { + align-self: center; +} + +.recipe-editorial__copy h2 { + max-inline-size: 12ch; + color: #24221e; + font-family: Georgia, 'Times New Roman', serif; + font-size: clamp(2.5rem, 6vw, 5rem); + font-weight: 500; + line-height: 0.98; +} + +.recipe-editorial__copy > p { + max-inline-size: 58ch; + color: #5c554b; + font-family: Georgia, 'Times New Roman', serif; + font-size: clamp(1rem, 1.8vw, 1.2rem); +} + +.recipe-editorial__meta { + color: #71695f; + font-size: 0.76rem; + font-weight: 800; + letter-spacing: 0.1em; + text-transform: uppercase; +} + +.recipe-editorial__rule { + inline-size: 4rem; + block-size: 2px; + border: 0; + margin-inline: 0; + background: #b14f3d; +} + +.recipe-editorial__quote { + padding-inline-start: 1rem; + border-inline-start: 2px solid #b14f3d; + color: #39342e; + font-family: Georgia, 'Times New Roman', serif; + font-size: clamp(1.15rem, 2.5vw, 1.6rem); + font-style: italic; + line-height: 1.45; +} + +.recipe-editorial__figure { + margin: 0; +} + +.recipe-editorial__image { + border-radius: 0.25rem; + background: #d7d0c4; +} + +.recipe-editorial__figure figcaption { + max-inline-size: 42ch; + margin-block-start: 0.75rem; + color: #71695f; + font-size: 0.78rem; + line-height: 1.5; +} + +.recipe-editorial__action { + border-color: #24221e; + color: #f8f5ee; + background: #24221e; + text-decoration: none; +} + +@media (max-width: 720px) { + .recipe-editorial { + --axo-split-min: 100%; + --axo-split-gap: 2rem; + } + + .recipe-editorial__figure { + order: -1; + } +} diff --git a/web_examples/recipes/gallery-dialog/gallery-dialog.js b/web_examples/recipes/gallery-dialog/gallery-dialog.js new file mode 100644 index 0000000..37ee43c --- /dev/null +++ b/web_examples/recipes/gallery-dialog/gallery-dialog.js @@ -0,0 +1,49 @@ +const galleryRoot = document.querySelector('[data-gallery-dialog-recipe]'); +const galleryStatus = document.querySelector('[data-gallery-status]'); +const dialogTitle = document.querySelector('[data-gallery-dialog-title]'); +const dialogDescription = document.querySelector('[data-gallery-dialog-description]'); +const dialogArtwork = document.querySelector('[data-gallery-dialog-art]'); + +let dialogController = null; + +function syncDialogContent(trigger) { + if (!trigger || !dialogTitle || !dialogDescription || !dialogArtwork) return; + + const title = trigger.dataset.galleryTitle || 'Untitled object'; + const description = trigger.dataset.galleryDescription || ''; + const position = trigger.dataset.galleryPosition || '0% 0%'; + + dialogTitle.textContent = title; + dialogDescription.textContent = description; + dialogArtwork.style.setProperty('--recipe-art-position', position); + dialogArtwork.setAttribute('aria-label', title); +} + +galleryRoot?.querySelectorAll('[data-gallery-title]').forEach((trigger) => { + trigger.addEventListener('click', () => syncDialogContent(trigger)); +}); + +async function initializeGalleryDialog() { + if (!galleryRoot) return; + + const sourcePath = import.meta.url.includes('/web_examples/') + ? '../../../packages/axoloth-behavior/src/dialog.js' + : '../../packages/axoloth-behavior/src/dialog.js'; + const { initDialog } = await import(new URL(sourcePath, import.meta.url)); + + dialogController = initDialog(galleryRoot); + if (galleryStatus) galleryStatus.textContent = 'Dialog behavior initialized.'; +} + +initializeGalleryDialog().catch((error) => { + if (galleryStatus) galleryStatus.textContent = 'Dialog behavior failed to initialize.'; + console.error(error); +}); + +window.addEventListener( + 'pagehide', + () => { + dialogController?.destroy(); + }, + { once: true } +); diff --git a/web_examples/recipes/gallery-dialog/index.html b/web_examples/recipes/gallery-dialog/index.html new file mode 100644 index 0000000..e7f95ac --- /dev/null +++ b/web_examples/recipes/gallery-dialog/index.html @@ -0,0 +1,415 @@ + + + + + + + + + + Gallery with Dialog Recipe - Axoloth Style Docs + + +
+ Axoloth Style + + + +
+ +
+ + +
+
+

Composition Recipe 03

+

Gallery with Dialog

+

+ Let axo-auto-grid choose the column count, then connect every item to one + accessible axo-dialog. Axoloth owns interaction state; project JavaScript + only supplies the selected object's content. +

+ +
+ +
+

Live Preview

+

Material studies

+

+ Open any object, close with Escape or the backdrop, and confirm focus returns to its + trigger. +

+ + +
+ + + + + +
+
+
+

Project-owned CSS + JS

+

Content remains application data

+

+ The image sprite, crop positions, card treatment, typography, and hover styling are + local CSS. The small script copies selected content into the shared dialog; Axoloth + Behavior still owns open state, ARIA, focus trap, Escape, dismissal, and cleanup. +

+
+
+

Mobile + Desktop

+

Columns respond to available width

+

+ Desktop naturally fits several cards. Mobile becomes one column through + --axo-item-min. The dialog uses viewport insets and scroll bounds, so + content remains reachable without a second mobile-only component. +

+
+
+
+
+
+ + + +
+ + + + + + diff --git a/web_examples/recipes/gallery-dialog/theme.css b/web_examples/recipes/gallery-dialog/theme.css new file mode 100644 index 0000000..23466bd --- /dev/null +++ b/web_examples/recipes/gallery-dialog/theme.css @@ -0,0 +1,139 @@ +.recipe-gallery-live { + padding: clamp(1rem, 3vw, 2rem); + border: 1px solid #cfd6dd; + border-radius: 0.75rem; + color: #17202a; + background: #eef1f3; +} + +.recipe-gallery-live > header { + max-inline-size: 42rem; + margin-block-end: 1.5rem; +} + +.recipe-gallery-live > header h2 { + color: #17202a; + font-family: Georgia, 'Times New Roman', serif; + font-size: clamp(2rem, 4vw, 3.5rem); + font-weight: 500; +} + +.recipe-gallery-live > header p { + color: #52606d; +} + +.recipe-gallery-grid { + --axo-grid-gap: 1rem; + --axo-item-min: 13rem; +} + +.recipe-gallery-item { + --axo-card-padding: 0.65rem; + --axo-flow-space: 0.65rem; + --axo-frame-ratio: 4 / 3; + + inline-size: 100%; + color: #17202a; + background: #f9fafb; + cursor: pointer; + text-align: start; + transition: + border-color 160ms ease, + box-shadow 160ms ease, + transform 160ms ease; +} + +.recipe-gallery-item:hover { + border-color: #7a6248; + box-shadow: 0 1rem 2rem rgb(23 32 42 / 0.12); + transform: translateY(-2px); +} + +.recipe-gallery-item:focus-visible { + outline: 3px solid #406f78; + outline-offset: 3px; +} + +.recipe-gallery-art { + display: block; + inline-size: 100%; + border-radius: calc(var(--axo-radius, 0.5rem) - 0.15rem); + background-image: url('../../assets/recipes/gallery-objects.png'); + background-position: var(--recipe-art-position, 0% 0%); + background-repeat: no-repeat; + background-size: 300% 200%; +} + +.recipe-gallery-item strong { + display: block; + color: #17202a; + font-family: Georgia, 'Times New Roman', serif; + font-size: 1.05rem; +} + +.recipe-gallery-item small { + display: block; + color: #68747f; + line-height: 1.45; +} + +.recipe-gallery-dialog { + --axo-dialog-backdrop: rgb(15 23 31 / 0.76); + --axo-dialog-duration: 200ms; + --axo-dialog-padding: clamp(1rem, 3vw, 1.75rem); + --axo-dialog-width: 52rem; +} + +.recipe-gallery-dialog .axo-dialog-backdrop { + border: 0; + padding: 0; +} + +.recipe-gallery-dialog__panel { + color: #17202a; + background: #f9fafb; +} + +.recipe-gallery-dialog__panel h2 { + color: #17202a; + font-family: Georgia, 'Times New Roman', serif; + font-size: clamp(1.7rem, 4vw, 2.7rem); + font-weight: 500; +} + +.recipe-gallery-dialog__art { + --axo-frame-ratio: 16 / 10; + + border-radius: 0.35rem; +} + +.recipe-gallery-dialog__copy { + color: #52606d; +} + +.recipe-gallery-dialog__close { + color: #f8fafc; + background: #17202a; +} + +.docs-section .recipe-gallery-status { + margin-block: 1rem 0; + color: #3f4b56; + font-size: 0.82rem; +} + +@media (max-width: 560px) { + .recipe-gallery-grid { + --axo-item-min: 100%; + } + + .recipe-gallery-dialog { + --axo-dialog-inset: 0.5rem; + } +} + +@media (prefers-reduced-motion: reduce) { + .recipe-gallery-item { + transition: none; + } +} diff --git a/web_examples/recipes/media-hero/index.html b/web_examples/recipes/media-hero/index.html new file mode 100644 index 0000000..11a3a40 --- /dev/null +++ b/web_examples/recipes/media-hero/index.html @@ -0,0 +1,228 @@ + + + + + + + + + + Media Hero Recipe - Axoloth Style Docs + + +
+ Axoloth Style + + + +
+ +
+ + +
+
+

Composition Recipe 01

+

Media Hero

+

+ Layer real media, a project-owned scrim, and readable content without adding a hero + component to Axoloth. The composition comes from axo-pile, + axo-frame, and axo-cover. +

+ +
+ +
+

Live Preview

+

Coastal retreat hero

+

+ Resize the viewport to see the same intrinsic composition adapt without breakpoint + classes. +

+ +
+
+
+ Modern coastal retreat set on dark volcanic rock above the ocean +
+ +
+
+ North Atlantic / Field Notes 04 +

Stay where the land meets weather.

+

+ A quiet coastal retreat shaped around wind, stone, and the long northern light. +

+ +
+
+
+
+
+ +
+

Complete HTML

+

A standalone document

+

+ The theme file only supplies visual identity. The HTML below contains the complete + structural composition used by the live preview. +

+
<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <link rel="stylesheet"
+      href="https://cdn.jsdelivr.net/npm/@quertys/axoloth-style@0.9.0/src/axoloth.css" />
+    <link rel="stylesheet" href="./theme.css" />
+    <title>Coastal Retreat</title>
+  </head>
+  <body>
+    <main>
+      <section class="recipe-media-hero axo-pile" aria-labelledby="hero-title">
+        <div class="recipe-media-hero__media axo-frame">
+          <img src="./media-hero.png" alt="Coastal retreat above the ocean" />
+        </div>
+        <div class="recipe-media-hero__scrim" aria-hidden="true"></div>
+        <div class="recipe-media-hero__content axo-cover">
+          <div class="axo-center axo-stack">
+            <span class="recipe-media-hero__kicker">North Atlantic / Field Notes 04</span>
+            <h1 id="hero-title">Stay where the land meets weather.</h1>
+            <p>A quiet coastal retreat shaped around wind, stone, and northern light.</p>
+            <div class="axo-cluster">
+              <a class="axo-button axo-link recipe-media-hero__primary" href="#stay">
+                Explore the stay
+              </a>
+              <a class="axo-button axo-link recipe-media-hero__secondary" href="#journal">
+                Read the journal
+              </a>
+            </div>
+          </div>
+        </div>
+      </section>
+    </main>
+  </body>
+</html>
+
+ +
+
+
+

Axoloth Classes

+

Structure supplied by the library

+
    +
  • + axo-pilePlaces media, scrim, and content in one grid area. +
  • +
  • + axo-frameOwns media cropping and stable dimensions. +
  • +
  • + axo-coverCreates the vertically centered hero region. +
  • +
  • + axo-centerConstrains readable copy inside the overlay. +
  • +
  • + axo-stackControls vertical rhythm between content elements. +
  • +
  • + axo-clusterWraps the calls to action without fragile widths. +
  • +
  • + axo-button + axo-linkProvide neutral control structure. +
  • +
+
+
+

Customizable Variables

+

Supported tuning points

+
    +
  • --axo-cover-heightHero block size.
  • +
  • --axo-cover-paddingSafe content inset.
  • +
  • --axo-frame-ratioMedia aspect-ratio behavior.
  • +
  • + --axo-frame-positionResponsive image focal point. +
  • +
  • --axo-center-widthReadable copy width.
  • +
  • --axo-stack-gapVertical content rhythm.
  • +
+
+
+
+ +
+
+
+

Project-owned CSS

+

Visual identity stays local

+

+ theme.css chooses the image, scrim gradients, typography, button + colors, and text contrast. These decisions describe this retreat brand, not a + reusable Axoloth layout contract. +

+
+
+

Mobile + Desktop

+

One composition, two crops

+

+ Desktop keeps copy on the image's quiet side. Mobile moves content toward the bottom + and adjusts --axo-frame-position; pile, cover, stack, and cluster + continue to own the layout. +

+
+
+
+
+
+ + + +
+ + + + + diff --git a/web_examples/recipes/media-hero/theme.css b/web_examples/recipes/media-hero/theme.css new file mode 100644 index 0000000..1a2b6f4 --- /dev/null +++ b/web_examples/recipes/media-hero/theme.css @@ -0,0 +1,93 @@ +.recipe-stage { + overflow: hidden; + border: 1px solid var(--docs-line); + border-radius: 0.75rem; + background: #111827; +} + +.recipe-media-hero { + --axo-center-width: 38rem; + --axo-cover-height: clamp(32rem, 78dvh, 46rem); + --axo-cover-padding: clamp(1.25rem, 5vw, 4rem); + --axo-frame-position: 64% center; + --axo-frame-ratio: auto; + --axo-stack-gap: 1.25rem; + + color: #f8fafc; + overflow: hidden; +} + +.recipe-media-hero__media { + min-block-size: var(--axo-cover-height); +} + +.recipe-media-hero__scrim { + background: + linear-gradient(90deg, rgb(3 7 18 / 0.9) 0%, rgb(3 7 18 / 0.62) 42%, transparent 78%), + linear-gradient(0deg, rgb(3 7 18 / 0.48), transparent 55%); +} + +.recipe-media-hero__content { + position: relative; + z-index: 1; + align-self: center; + inline-size: 100%; +} + +.recipe-media-hero__content h2 { + max-inline-size: 12ch; + color: #ffffff; + font-family: Georgia, 'Times New Roman', serif; + font-size: clamp(2.65rem, 8vw, 5.75rem); + font-weight: 500; + line-height: 0.93; +} + +.recipe-media-hero__content p { + max-inline-size: 36rem; + color: rgb(248 250 252 / 0.82); + font-size: clamp(1rem, 2vw, 1.15rem); +} + +.recipe-media-hero__kicker { + color: #f6d68a; + font-size: 0.76rem; + font-weight: 800; + letter-spacing: 0.14em; + text-transform: uppercase; +} + +.recipe-media-hero__primary, +.recipe-media-hero__secondary { + min-inline-size: 9rem; + justify-content: center; + text-decoration: none; +} + +.recipe-media-hero__primary { + border-color: #f6d68a; + color: #111827; + background: #f6d68a; +} + +.recipe-media-hero__secondary { + border-color: rgb(255 255 255 / 0.48); + color: #ffffff; + background: rgb(3 7 18 / 0.28); + backdrop-filter: blur(8px); +} + +@media (max-width: 600px) { + .recipe-media-hero { + --axo-cover-height: 36rem; + --axo-frame-position: 70% center; + } + + .recipe-media-hero__scrim { + background: linear-gradient(0deg, rgb(3 7 18 / 0.94) 0%, rgb(3 7 18 / 0.55) 72%); + } + + .recipe-media-hero__content { + align-self: end; + } +} diff --git a/web_examples/scripts/behavior-guide.js b/web_examples/scripts/behavior-guide.js new file mode 100644 index 0000000..a86a811 --- /dev/null +++ b/web_examples/scripts/behavior-guide.js @@ -0,0 +1,16 @@ +const behaviorModulePath = import.meta.url.includes('/web_examples/') + ? '../../packages/axoloth-behavior/src/index.js' + : '../packages/axoloth-behavior/src/index.js'; + +const { initAxolothBehaviors } = await import(new URL(behaviorModulePath, import.meta.url)); + +const demoRoot = document.querySelector('[data-behavior-demos]'); +const status = document.querySelector('[data-behavior-status]'); +const behaviors = initAxolothBehaviors(demoRoot); + +if (status) { + status.textContent = 'Initialized: tabs, accordion, dialog, and drawer are ready.'; + status.dataset.state = 'ready'; +} + +window.addEventListener('pagehide', () => behaviors.destroy(), { once: true }); diff --git a/web_examples/scripts/docs-sidebar.js b/web_examples/scripts/docs-sidebar.js index fe14016..c87d872 100644 --- a/web_examples/scripts/docs-sidebar.js +++ b/web_examples/scripts/docs-sidebar.js @@ -8,6 +8,7 @@ let isSidebarExpanded = false; const docsIconPaths = { alignment: '', + behavior: '', bento: '', card: '', composition: '', @@ -18,6 +19,9 @@ const docsIconPaths = { navbar: '', overview: '', responsive: '', + 'recipe-editorial-split': '', + 'recipe-gallery-dialog': '', + 'recipe-media-hero': '', row: '', sidebar: '', spacing: '', diff --git a/web_examples/scripts/examples.js b/web_examples/scripts/examples.js index ea41e0c..8e26768 100644 --- a/web_examples/scripts/examples.js +++ b/web_examples/scripts/examples.js @@ -1,11 +1,14 @@ const searchInput = document.querySelector('#example-search'); const examplesTableBody = document.querySelector('#examples-table-body'); +const recipesTableBody = document.querySelector('#recipes-table-body'); const utilitiesTableBody = document.querySelector('#utilities-table-body'); const exampleCount = document.querySelector('#example-count'); +const recipeCount = document.querySelector('#recipe-count'); const utilityCount = document.querySelector('#utility-count'); const catalogs = { examples: { items: [], status: 'loading' }, + recipes: { items: [], status: 'loading' }, utilities: { items: [], status: 'loading' }, }; @@ -78,6 +81,61 @@ function renderExamples(query) { exampleCount.textContent = `${items.length} example${items.length === 1 ? '' : 's'}`; } +function renderRecipes(query) { + if (catalogs.recipes.status === 'error') { + recipesTableBody.innerHTML = ` + + + Failed to load composition recipes. Use Live Server or another static server. + + + `; + recipeCount.textContent = 'Recipes unavailable'; + return; + } + + const items = catalogs.recipes.items.filter((recipe) => + matchesQuery(recipe, query, [ + 'name', + 'category', + 'version', + 'status', + 'description', + 'classes', + 'variables', + 'projectCss', + 'responsive', + ]) + ); + + if (!items.length) { + recipesTableBody.innerHTML = ` + No composition recipes found. + `; + recipeCount.textContent = '0 recipes'; + return; + } + + recipesTableBody.innerHTML = items + .map( + (recipe) => ` + + + ${escapeHtml(recipe.name)} + ${escapeHtml(recipe.category)} / ${escapeHtml(recipe.status)} + ${escapeHtml(recipe.description)} + + Open recipe + Open source + ${escapeHtml(recipe.version)} + + ` + ) + .join(''); + + recipeCount.textContent = `${items.length} recipe${items.length === 1 ? '' : 's'}`; +} + function renderUtilities(query) { if (catalogs.utilities.status === 'error') { utilitiesTableBody.innerHTML = ` @@ -139,6 +197,7 @@ function renderUtilities(query) { function renderCatalogs() { const query = normalize(searchInput.value); renderExamples(query); + renderRecipes(query); renderUtilities(query); } @@ -158,6 +217,14 @@ async function loadCatalogs() { catalogs.examples.status = 'error'; console.error(error); }), + loadJson('./data/recipes.json') + .then((items) => { + catalogs.recipes = { items, status: 'ready' }; + }) + .catch((error) => { + catalogs.recipes.status = 'error'; + console.error(error); + }), loadJson('./data/utilities.json') .then((index) => { catalogs.utilities = { items: index.utilities, status: 'ready' }; diff --git a/web_examples/scripts/verify-docs.mjs b/web_examples/scripts/verify-docs.mjs index 144400e..e91046e 100644 --- a/web_examples/scripts/verify-docs.mjs +++ b/web_examples/scripts/verify-docs.mjs @@ -4,12 +4,31 @@ import { fileURLToPath } from 'node:url'; const docsDirectory = resolve(dirname(fileURLToPath(import.meta.url)), '..'); const examples = JSON.parse(readFileSync(resolve(docsDirectory, 'data/examples.json'), 'utf8')); +const recipes = JSON.parse(readFileSync(resolve(docsDirectory, 'data/recipes.json'), 'utf8')); const docsNav = JSON.parse(readFileSync(resolve(docsDirectory, 'data/docs-nav.json'), 'utf8')); const docsPages = JSON.parse(readFileSync(resolve(docsDirectory, 'data/docs-pages.json'), 'utf8')); const utilityIndex = JSON.parse( readFileSync(resolve(docsDirectory, 'data/utilities.json'), 'utf8') ); const indexHtml = readFileSync(resolve(docsDirectory, 'index.html'), 'utf8'); +const examplesScript = readFileSync(resolve(docsDirectory, 'scripts/examples.js'), 'utf8'); +const behaviorGuideHtml = readFileSync(resolve(docsDirectory, 'docs/behavior/index.html'), 'utf8'); +const behaviorGuideScript = readFileSync( + resolve(docsDirectory, 'scripts/behavior-guide.js'), + 'utf8' +); +const galleryDialogScript = readFileSync( + resolve(docsDirectory, 'recipes/gallery-dialog/gallery-dialog.js'), + 'utf8' +); +const behaviorReadme = readFileSync( + resolve(docsDirectory, '../packages/axoloth-behavior/README.md'), + 'utf8' +); +const deployWorkflow = readFileSync( + resolve(docsDirectory, '../.github/workflows/deploy-docs-pages.yml'), + 'utf8' +); function assert(condition, message) { if (!condition) throw new Error(message); @@ -22,6 +41,11 @@ assert( ); assert(Array.isArray(docsNav.groups), 'Docs nav must expose a groups array.'); assert(Array.isArray(docsPages), 'Docs pages must be an array.'); +assert(recipes.length === 3, `Expected exactly 3 composition recipes, found ${recipes.length}.`); +assert( + new Set(recipes.map((recipe) => recipe.id)).size === recipes.length, + 'Recipe IDs must be unique.' +); examples.forEach((example) => { ['id', 'name', 'category', 'version', 'status', 'previewUrl', 'sourceUrl', 'description'].forEach( @@ -34,8 +58,92 @@ examples.forEach((example) => { const docsPageIds = new Set(docsPages.map((page) => page.id)); const utilityNames = new Set(utilityIndex.utilities.map((utility) => utility.name)); +const utilitiesByName = new Map(utilityIndex.utilities.map((utility) => [utility.name, utility])); assert(docsPageIds.size === docsPages.length, 'Docs page IDs must be unique.'); +const expectedRecipeIds = ['media-hero', 'editorial-split', 'gallery-dialog']; +assert( + expectedRecipeIds.every((id) => recipes.some((recipe) => recipe.id === id)), + 'Composition recipe pack must include Media Hero, Editorial Split, and Gallery with Dialog.' +); + +recipes.forEach((recipe) => { + [ + 'id', + 'name', + 'category', + 'version', + 'status', + 'previewUrl', + 'sourceUrl', + 'description', + 'classes', + 'variables', + 'projectCss', + 'responsive', + ].forEach((field) => + assert(recipe[field], `Recipe ${recipe.id || ''} is missing ${field}.`) + ); + + assert(recipe.status === 'ready', `Recipe ${recipe.id} must be ready.`); + assert( + Array.isArray(recipe.classes) && recipe.classes.length > 0, + `${recipe.id} has no classes.` + ); + assert( + Array.isArray(recipe.variables) && recipe.variables.length > 0, + `${recipe.id} has no customizable variables.` + ); + + [recipe.previewUrl, recipe.sourceUrl].forEach((url) => { + assert(existsSync(resolve(docsDirectory, url)), `Broken recipe path: ${url}`); + }); + + const recipeDirectory = resolve(docsDirectory, `recipes/${recipe.id}`); + const recipeHtml = readFileSync(resolve(recipeDirectory, 'index.html'), 'utf8'); + const recipeTheme = readFileSync(resolve(recipeDirectory, 'theme.css'), 'utf8'); + + [ + 'Live Preview', + 'Complete HTML', + 'Axoloth Classes', + 'Customizable Variables', + 'Project-owned CSS', + 'Mobile + Desktop', + ].forEach((marker) => + assert(recipeHtml.includes(marker), `Recipe ${recipe.id} is missing ${marker}.`) + ); + + assert( + recipeHtml.includes('<!doctype html>'), + `Recipe ${recipe.id} must include a complete HTML document.` + ); + assert( + recipeHtml.includes(`data-docs-active="recipe-${recipe.id}"`), + `Recipe ${recipe.id} must expose its active docs navigation state.` + ); + assert( + recipeHtml.includes('href="./theme.css"') && recipeTheme.trim().length > 0, + `Recipe ${recipe.id} must load non-empty thin theme CSS.` + ); + + recipe.classes.forEach((name) => { + assert( + utilitiesByName.get(name)?.kind === 'class', + `Recipe ${recipe.id} uses unknown class: ${name}` + ); + assert(recipeHtml.includes(name), `Recipe ${recipe.id} does not document class ${name}.`); + }); + + recipe.variables.forEach((name) => { + assert( + utilitiesByName.get(name)?.kind === 'variable', + `Recipe ${recipe.id} uses unknown variable: ${name}` + ); + assert(recipeHtml.includes(name), `Recipe ${recipe.id} does not document variable ${name}.`); + }); +}); + docsPages.forEach((page) => { [ 'id', @@ -86,6 +194,19 @@ docsNavItems.forEach((item) => { assert(docsPageIds.has(routeId), `Docs nav item ${item.id} has no page data.`); assert(existsSync(resolve(docsDirectory, item.path)), `Broken docs nav path: ${item.path}`); } + + if (item.path.startsWith('recipes/')) { + assert(existsSync(resolve(docsDirectory, item.path)), `Broken recipe nav path: ${item.path}`); + } +}); + +recipes.forEach((recipe) => { + assert( + docsNavItems.some( + (item) => item.id === `recipe-${recipe.id}` && item.path === `recipes/${recipe.id}/` + ), + `Recipe ${recipe.id} must be discoverable from the docs navigation.` + ); }); assert( @@ -120,6 +241,12 @@ utilityIndex.utilities ); }); assert(indexHtml.includes('id="examples-table-body"'), 'Docs example table target is missing.'); +assert(indexHtml.includes('id="recipes-table-body"'), 'Docs recipe table target is missing.'); +assert( + examplesScript.includes("loadJson('./data/recipes.json')") && + examplesScript.includes('renderRecipes(query)'), + 'Docs hub must load and render the composition recipe registry.' +); assert(indexHtml.includes('id="utilities-table-body"'), 'Docs utility table target is missing.'); assert(indexHtml.includes('data-docs-sidebar'), 'Docs sidebar target is missing.'); assert(indexHtml.includes('scripts/docs-sidebar.js'), 'Docs sidebar script is missing.'); @@ -137,7 +264,101 @@ assert( indexHtml.includes('remove unused CSS automatically'), 'Full-entry unused CSS behavior must be explained.' ); +assert( + docsNavItems.some((item) => item.id === 'behavior' && item.path === 'docs/behavior/'), + 'Behavior Guide must be discoverable from the docs navigation.' +); +assert( + indexHtml.includes('href="./docs/behavior/"'), + 'The docs quick start must link to the complete Behavior Guide.' +); +assert( + indexHtml.includes('id="composition-recipes"'), + 'The docs hub must expose the composition recipe directory.' +); + +[ + 'id="behavior-install"', + 'id="initialize-all"', + 'id="manual-initialization"', + 'id="behavior-tabs"', + 'id="behavior-accordion"', + 'id="behavior-dialog"', + 'id="behavior-drawer"', + 'id="behavior-cleanup"', + 'id="behavior-troubleshooting"', +].forEach((marker) => + assert(behaviorGuideHtml.includes(marker), `Behavior Guide is missing ${marker}.`) +); + +[ + '@quertys/axoloth-behavior/tabs', + '@quertys/axoloth-behavior/accordion', + '@quertys/axoloth-behavior/dialog', + '@quertys/axoloth-behavior/drawer', +].forEach((entryPoint) => + assert( + behaviorGuideHtml.includes(entryPoint), + `Behavior Guide is missing the per-component import ${entryPoint}.` + ) +); + +['data-axo-tabs', 'data-axo-accordion', 'data-axo-dialog-toggle', 'data-axo-drawer-toggle'].forEach( + (attribute) => + assert( + behaviorGuideHtml.includes(attribute), + `Behavior Guide is missing runnable markup for ${attribute}.` + ) +); + +assert( + behaviorGuideHtml.includes('npm install @quertys/axoloth-style @quertys/axoloth-behavior'), + 'Behavior Guide installation command is missing.' +); +assert( + behaviorGuideHtml.includes('cdn.jsdelivr.net/npm/@quertys/axoloth-behavior@0.6.0/src/index.js'), + 'Behavior Guide CDN ES module setup is missing.' +); +assert( + behaviorGuideHtml.includes('initAxolothBehaviors') && behaviorGuideHtml.includes('destroy()'), + 'Behavior Guide must show aggregate initialization and cleanup.' +); +assert( + behaviorGuideHtml.includes('The package does not auto-initialize'), + 'Behavior Guide troubleshooting must explain explicit initialization.' +); +assert( + behaviorGuideScript.includes('initAxolothBehaviors(demoRoot)'), + 'Behavior Guide live demos must initialize through the framework-free package API.' +); +assert( + behaviorGuideScript.includes('behaviors.destroy()'), + 'Behavior Guide live demos must clean up their controller.' +); +assert( + galleryDialogScript.includes('initDialog(galleryRoot)'), + 'Gallery recipe must initialize the official dialog behavior.' +); +assert( + galleryDialogScript.includes('dialogController?.destroy()'), + 'Gallery recipe must clean up its dialog controller.' +); +assert( + behaviorReadme.includes('CSS alone never initializes JavaScript behavior') && + behaviorReadme.includes('## CDN / Native ES Modules') && + behaviorReadme.includes('## Troubleshooting'), + 'Behavior package README is missing the style/behavior boundary or setup guidance.' +); +assert( + deployWorkflow.includes('packages/axoloth-behavior/src/**') && + deployWorkflow.includes('packages/axoloth-behavior/src'), + 'GitHub Pages must deploy the behavior source used by the live guide.' +); +assert( + deployWorkflow.includes('find dist-pages/recipes'), + 'GitHub Pages must rewrite local style paths for composition recipes.' +); console.log( - `Docs verified: ${examples.length} examples, ${docsPages.length} docs pages, and ${utilityIndex.utilities.length} generated utilities.` + `Docs verified: ${examples.length} examples, ${recipes.length} recipes, ${docsPages.length} docs pages, and ${utilityIndex.utilities.length} generated utilities.` ); diff --git a/web_examples/styles/docs.css b/web_examples/styles/docs.css index b2eb89d..936f793 100644 --- a/web_examples/styles/docs.css +++ b/web_examples/styles/docs.css @@ -308,6 +308,10 @@ body { margin-top: 1rem; } +.code-grid > * { + min-width: 0; +} + .docs-detail-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(260px, 0.55fr); @@ -431,6 +435,60 @@ body { align-items: center; } +.behavior-responsibility-grid article { + min-width: 0; +} + +.behavior-callout, +.behavior-status { + padding: 0.8rem 1rem; + border-inline-start: 3px solid var(--docs-accent); + color: var(--docs-text) !important; + background: var(--docs-accent-soft); +} + +.behavior-status[data-state='ready'] { + border-inline-start-color: #16a34a; +} + +.behavior-example-grid { + display: grid; + grid-template-columns: minmax(0, 1.15fr) minmax(280px, 0.85fr); + gap: 1rem; + align-items: start; + margin-top: 1rem; +} + +.behavior-example-grid > * { + min-width: 0; +} + +.behavior-demo { + --axo-surface-bg: var(--docs-panel); + --axo-surface-color: var(--docs-text); + --axo-surface-border: var(--docs-line); + --axo-surface-shadow: var(--docs-shadow); + --axo-focus-ring: 0 0 0 3px var(--docs-accent-soft); + min-height: 260px; +} + +.behavior-demo :where(.axo-tab-panel, .axo-accordion-panel) { + color: var(--docs-muted); +} + +.behavior-demo :where(.axo-dialog-panel, .axo-drawer) { + text-align: start; +} + +.behavior-troubleshooting-list small code { + display: inline; + margin: 0; +} + +body[data-docs-page='behavior'] { + overflow-x: clip; +} + pre { max-width: 100%; padding: 1rem; @@ -575,6 +633,10 @@ td code { .docs-detail-grid { grid-template-columns: 1fr; } + + .behavior-example-grid { + grid-template-columns: 1fr; + } } @media (max-width: 720px) {