howone 0.1.19 → 0.1.22
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 +1 -1
- package/templates/vite/.howone/skills/hallmark/LICENSE +21 -0
- package/templates/vite/.howone/skills/hallmark/README.md +147 -0
- package/templates/vite/.howone/skills/hallmark/ROADMAP.md +201 -0
- package/templates/vite/.howone/skills/hallmark/SKILL.md +551 -0
- package/templates/vite/.howone/skills/hallmark/docs/recipes.md +186 -0
- package/templates/vite/.howone/skills/hallmark/docs/screenshots/hero-anya.jpg +0 -0
- package/templates/vite/.howone/skills/hallmark/docs/screenshots/hero-bananastudio.jpg +0 -0
- package/templates/vite/.howone/skills/hallmark/docs/screenshots/hero-hyperlane.jpg +0 -0
- package/templates/vite/.howone/skills/hallmark/docs/screenshots/hero-najm.jpg +0 -0
- package/templates/vite/.howone/skills/hallmark/docs/screenshots/hero-slow-pour.jpg +0 -0
- package/templates/vite/.howone/skills/hallmark/docs/screenshots/hero-soroe.jpg +0 -0
- package/templates/vite/.howone/skills/hallmark/docs/screenshots/hero-tally.jpg +0 -0
- package/templates/vite/.howone/skills/hallmark/docs/screenshots/hero-wayfare.jpg +0 -0
- package/templates/vite/.howone/skills/hallmark/docs/study-examples.md +176 -0
- package/templates/vite/.howone/skills/hallmark/docs/talk-slides.md +364 -0
- package/templates/vite/.howone/skills/hallmark/package.json +36 -0
- package/templates/vite/.howone/skills/hallmark/references/anti-patterns.md +412 -0
- package/templates/vite/.howone/skills/hallmark/references/assets.md +399 -0
- package/templates/vite/.howone/skills/hallmark/references/color.md +95 -0
- package/templates/vite/.howone/skills/hallmark/references/component-cookbook.md +256 -0
- package/templates/vite/.howone/skills/hallmark/references/components/c1-outlined-chip.md +12 -0
- package/templates/vite/.howone/skills/hallmark/references/components/c2-inline-form-as-cta.md +16 -0
- package/templates/vite/.howone/skills/hallmark/references/components/c3-typographic-link.md +8 -0
- package/templates/vite/.howone/skills/hallmark/references/components/c4-sticky-bottom-bar.md +16 -0
- package/templates/vite/.howone/skills/hallmark/references/components/f1-bento-grid.md +20 -0
- package/templates/vite/.howone/skills/hallmark/references/components/f2-sticky-scroll-stack.md +20 -0
- package/templates/vite/.howone/skills/hallmark/references/components/f3-tabular-spec-sheet.md +11 -0
- package/templates/vite/.howone/skills/hallmark/references/components/f4-step-sequence.md +11 -0
- package/templates/vite/.howone/skills/hallmark/references/components/f5-annotated-screenshot.md +11 -0
- package/templates/vite/.howone/skills/hallmark/references/components/f6-product-card-grid.md +41 -0
- package/templates/vite/.howone/skills/hallmark/references/components/ft1-mast-headed.md +13 -0
- package/templates/vite/.howone/skills/hallmark/references/components/ft2-inline-rule-single-line.md +10 -0
- package/templates/vite/.howone/skills/hallmark/references/components/ft3-index-style-category-list.md +12 -0
- package/templates/vite/.howone/skills/hallmark/references/components/ft4-dense-typographic.md +10 -0
- package/templates/vite/.howone/skills/hallmark/references/components/ft5-statement.md +21 -0
- package/templates/vite/.howone/skills/hallmark/references/components/ft6-letter-close.md +19 -0
- package/templates/vite/.howone/skills/hallmark/references/components/ft7-newsletter-first.md +27 -0
- package/templates/vite/.howone/skills/hallmark/references/components/ft8-marquee-scroll.md +25 -0
- package/templates/vite/.howone/skills/hallmark/references/components/h1-marquee.md +15 -0
- package/templates/vite/.howone/skills/hallmark/references/components/h2-split-diptych.md +15 -0
- package/templates/vite/.howone/skills/hallmark/references/components/h3-quote-led.md +11 -0
- package/templates/vite/.howone/skills/hallmark/references/components/h4-stat-led.md +14 -0
- package/templates/vite/.howone/skills/hallmark/references/components/h5-letter-hero.md +11 -0
- package/templates/vite/.howone/skills/hallmark/references/components/h6-photographic-fold.md +16 -0
- package/templates/vite/.howone/skills/hallmark/references/components/h7-demo-video-clipped-by-viewport-edge.md +27 -0
- package/templates/vite/.howone/skills/hallmark/references/components/h8-mockup-split-browser-framed.md +23 -0
- package/templates/vite/.howone/skills/hallmark/references/components/h9-custom-illustration-centerpiece.md +27 -0
- package/templates/vite/.howone/skills/hallmark/references/components/n1-wordmark-2-links.md +12 -0
- package/templates/vite/.howone/skills/hallmark/references/components/n10-floating-on-scroll-morph.md +19 -0
- package/templates/vite/.howone/skills/hallmark/references/components/n2-floating-chip.md +14 -0
- package/templates/vite/.howone/skills/hallmark/references/components/n3-side-rail.md +14 -0
- package/templates/vite/.howone/skills/hallmark/references/components/n4-hidden-behind-k.md +9 -0
- package/templates/vite/.howone/skills/hallmark/references/components/n5-floating-pill.md +28 -0
- package/templates/vite/.howone/skills/hallmark/references/components/n6-newspaper-masthead.md +24 -0
- package/templates/vite/.howone/skills/hallmark/references/components/n7-brutal-slab.md +22 -0
- package/templates/vite/.howone/skills/hallmark/references/components/n8-terminal-command.md +21 -0
- package/templates/vite/.howone/skills/hallmark/references/components/n9-edge-aligned-minimal.md +17 -0
- package/templates/vite/.howone/skills/hallmark/references/components/s1-left-margin-numbered.md +15 -0
- package/templates/vite/.howone/skills/hallmark/references/components/s2-hanging.md +13 -0
- package/templates/vite/.howone/skills/hallmark/references/components/s3-sticky-pinned.md +19 -0
- package/templates/vite/.howone/skills/hallmark/references/components/s4-inline-no-break.md +11 -0
- package/templates/vite/.howone/skills/hallmark/references/components/s5-bottom-anchored.md +13 -0
- package/templates/vite/.howone/skills/hallmark/references/components/t1-pull-quote-with-marginalia.md +12 -0
- package/templates/vite/.howone/skills/hallmark/references/components/t2-logo-wall-hairline.md +19 -0
- package/templates/vite/.howone/skills/hallmark/references/components/t3-single-huge-quote.md +11 -0
- package/templates/vite/.howone/skills/hallmark/references/components/t4-numbered-stat-strip.md +14 -0
- package/templates/vite/.howone/skills/hallmark/references/contract.md +24 -0
- package/templates/vite/.howone/skills/hallmark/references/copy.md +182 -0
- package/templates/vite/.howone/skills/hallmark/references/custom-craft.md +626 -0
- package/templates/vite/.howone/skills/hallmark/references/custom-theme.md +329 -0
- package/templates/vite/.howone/skills/hallmark/references/design-md.md +116 -0
- package/templates/vite/.howone/skills/hallmark/references/export-formats.md +328 -0
- package/templates/vite/.howone/skills/hallmark/references/floating-nav.md +89 -0
- package/templates/vite/.howone/skills/hallmark/references/genres/atmospheric.md +65 -0
- package/templates/vite/.howone/skills/hallmark/references/genres/editorial.md +70 -0
- package/templates/vite/.howone/skills/hallmark/references/genres/modern-minimal.md +67 -0
- package/templates/vite/.howone/skills/hallmark/references/genres/playful.md +65 -0
- package/templates/vite/.howone/skills/hallmark/references/hero-enrichment.md +474 -0
- package/templates/vite/.howone/skills/hallmark/references/imagery-kit.md +170 -0
- package/templates/vite/.howone/skills/hallmark/references/interaction-and-states.md +207 -0
- package/templates/vite/.howone/skills/hallmark/references/layout-and-space.md +111 -0
- package/templates/vite/.howone/skills/hallmark/references/macrostructures/01-bento-grid.md +35 -0
- package/templates/vite/.howone/skills/hallmark/references/macrostructures/02-long-document.md +34 -0
- package/templates/vite/.howone/skills/hallmark/references/macrostructures/03-marquee-hero.md +31 -0
- package/templates/vite/.howone/skills/hallmark/references/macrostructures/04-stat-led.md +32 -0
- package/templates/vite/.howone/skills/hallmark/references/macrostructures/05-workbench.md +32 -0
- package/templates/vite/.howone/skills/hallmark/references/macrostructures/06-conversational-faq.md +33 -0
- package/templates/vite/.howone/skills/hallmark/references/macrostructures/07-manifesto.md +32 -0
- package/templates/vite/.howone/skills/hallmark/references/macrostructures/08-photographic.md +34 -0
- package/templates/vite/.howone/skills/hallmark/references/macrostructures/09-quote-led.md +32 -0
- package/templates/vite/.howone/skills/hallmark/references/macrostructures/10-specimen.md +32 -0
- package/templates/vite/.howone/skills/hallmark/references/macrostructures/11-catalogue.md +23 -0
- package/templates/vite/.howone/skills/hallmark/references/macrostructures/12-letter.md +23 -0
- package/templates/vite/.howone/skills/hallmark/references/macrostructures/13-index-first.md +23 -0
- package/templates/vite/.howone/skills/hallmark/references/macrostructures/14-narrative-workflow.md +23 -0
- package/templates/vite/.howone/skills/hallmark/references/macrostructures/15-split-studio.md +23 -0
- package/templates/vite/.howone/skills/hallmark/references/macrostructures/16-feature-stack.md +23 -0
- package/templates/vite/.howone/skills/hallmark/references/macrostructures/17-type-specimen.md +23 -0
- package/templates/vite/.howone/skills/hallmark/references/macrostructures/18-portfolio-grid.md +23 -0
- package/templates/vite/.howone/skills/hallmark/references/macrostructures/19-map-diagram.md +23 -0
- package/templates/vite/.howone/skills/hallmark/references/macrostructures/20-ecosystem-index.md +23 -0
- package/templates/vite/.howone/skills/hallmark/references/macrostructures/21-component-playground.md +23 -0
- package/templates/vite/.howone/skills/hallmark/references/macrostructures.md +89 -0
- package/templates/vite/.howone/skills/hallmark/references/microinteractions.md +260 -0
- package/templates/vite/.howone/skills/hallmark/references/motion.md +109 -0
- package/templates/vite/.howone/skills/hallmark/references/preview-examples.md +49 -0
- package/templates/vite/.howone/skills/hallmark/references/responsive.md +138 -0
- package/templates/vite/.howone/skills/hallmark/references/slop-test.md +205 -0
- package/templates/vite/.howone/skills/hallmark/references/structure.md +164 -0
- package/templates/vite/.howone/skills/hallmark/references/study.md +486 -0
- package/templates/vite/.howone/skills/hallmark/references/typography.md +243 -0
- package/templates/vite/.howone/skills/hallmark/references/verbs/audit.md +25 -0
- package/templates/vite/.howone/skills/hallmark/references/verbs/redesign.md +269 -0
- package/templates/vite/.howone/skills/hallmark/site/OG-hallmark.png +0 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/01-tide-podcast/brief.md +71 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/01-tide-podcast/index.html +64 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/01-tide-podcast/style.css +240 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/02-streampipe-cli/brief.md +65 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/02-streampipe-cli/index.html +105 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/02-streampipe-cli/style.css +250 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/03-maple-bakery/brief.md +64 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/03-maple-bakery/index.html +131 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/03-maple-bakery/style.css +240 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/04-meridian-manifesto/brief.md +67 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/04-meridian-manifesto/index.html +86 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/04-meridian-manifesto/style.css +262 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/05-tracejam-saas/brief.md +63 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/05-tracejam-saas/index.html +167 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/05-tracejam-saas/style.css +457 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/06-anya-portfolio/brief.md +65 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/06-anya-portfolio/index.html +159 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/06-anya-portfolio/style.css +288 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/07-foundry-compliance/brief.md +64 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/07-foundry-compliance/index.html +146 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/07-foundry-compliance/style.css +484 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/08-cohort-courses/brief.md +64 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/08-cohort-courses/index.html +116 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/08-cohort-courses/style.css +354 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/09-slow-pour/index.html +638 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/10-owl-hours/index.html +515 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/11-soroe-ceramics/index.html +515 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/12-loafer/index.html +608 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/13-alma/index.html +587 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/README.md +157 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/_thumbs/BananaStudio-loop.mp4 +0 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/_thumbs/BananaStudio-still.jpg +0 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/_thumbs/Hyperlane-example.mp4 +0 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/_thumbs/Hyperlane-still.jpg +0 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/_thumbs/Najm-loop.mp4 +0 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/_thumbs/Najm-still.jpg +0 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/_thumbs/Podcast-loop.mp4 +0 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/_thumbs/SaaS-loop.mp4 +0 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/_thumbs/SaaS-still.jpg +0 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/_thumbs/Soroe-loop.mp4 +0 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/_thumbs/Soroe-still.jpg +0 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/_thumbs/after-quiet-hour.png +0 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/_thumbs/anya-loop.mp4 +0 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/_thumbs/anya-still.jpg +0 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/_thumbs/audit-example.png +0 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/_thumbs/before-quiet-hour.png +0 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/_thumbs/example-redesign-uractivation.png +0 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/_thumbs/slow-pour-loop.mp4 +0 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/_thumbs/slow-pour-still.jpg +0 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/_thumbs/study-example.png +0 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/_thumbs/uractivation-after-loop.mp4 +0 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/_thumbs/wayfare-loop.mp4 +0 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/_thumbs/wayfare-still.jpg +0 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/custom/01-coffeebox/index.html +77 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/custom/01-coffeebox/style.css +238 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/custom/02-loop/index.html +110 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/custom/02-loop/style.css +326 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/custom/03-mossroot/index.html +134 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/custom/03-mossroot/style.css +262 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/custom/README.md +30 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/verbs/README.md +17 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/verbs/audit/audit-report.md +56 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/verbs/audit/input.html +160 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/verbs/audit/notes.md +29 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/verbs/redesign/input.html +63 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/verbs/redesign/notes.md +72 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/verbs/redesign/output.html +374 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/verbs/study/diagnosis.md +52 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/verbs/study/input-description.md +29 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/verbs/study/notes.md +61 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/verbs/study/output.css +193 -0
- package/templates/vite/.howone/skills/hallmark/site/_tests/verbs/study/output.html +66 -0
- package/templates/vite/.howone/skills/hallmark/site/css/base.css +194 -0
- package/templates/vite/.howone/skills/hallmark/site/css/components.css +4886 -0
- package/templates/vite/.howone/skills/hallmark/site/css/sections.css +2072 -0
- package/templates/vite/.howone/skills/hallmark/site/css/tokens.css +1129 -0
- package/templates/vite/.howone/skills/hallmark/site/examples/bananastudio/index.html +475 -0
- package/templates/vite/.howone/skills/hallmark/site/examples/bananastudio/styles.css +1584 -0
- package/templates/vite/.howone/skills/hallmark/site/examples/bananastudio/tokens.css +96 -0
- package/templates/vite/.howone/skills/hallmark/site/examples/hyperlane/index.html +344 -0
- package/templates/vite/.howone/skills/hallmark/site/examples/hyperlane/script.js +103 -0
- package/templates/vite/.howone/skills/hallmark/site/examples/hyperlane/styles.css +1103 -0
- package/templates/vite/.howone/skills/hallmark/site/examples/hyperlane/tokens.css +83 -0
- package/templates/vite/.howone/skills/hallmark/site/examples/najm/index.html +368 -0
- package/templates/vite/.howone/skills/hallmark/site/examples/najm/script.js +133 -0
- package/templates/vite/.howone/skills/hallmark/site/examples/najm/styles.css +1062 -0
- package/templates/vite/.howone/skills/hallmark/site/examples/najm/tokens.css +97 -0
- package/templates/vite/.howone/skills/hallmark/site/examples/tally/app.js +84 -0
- package/templates/vite/.howone/skills/hallmark/site/examples/tally/index.html +446 -0
- package/templates/vite/.howone/skills/hallmark/site/examples/tally/styles.css +1087 -0
- package/templates/vite/.howone/skills/hallmark/site/examples/tally/tokens.css +101 -0
- package/templates/vite/.howone/skills/hallmark/site/examples/wayfare/index.html +359 -0
- package/templates/vite/.howone/skills/hallmark/site/examples/wayfare/style.css +1168 -0
- package/templates/vite/.howone/skills/hallmark/site/examples/wayfare/tokens.css +81 -0
- package/templates/vite/.howone/skills/hallmark/site/favicon-dark.svg +5 -0
- package/templates/vite/.howone/skills/hallmark/site/favicon-light.svg +5 -0
- package/templates/vite/.howone/skills/hallmark/site/index.html +1043 -0
- package/templates/vite/.howone/skills/hallmark/site/js/main.js +1175 -0
- package/templates/vite/.howone/skills/hallmark/vercel.json +6 -0
- package/templates/vite/.howone/skills/howone-sdk/01-architect/01-app-generation.md +101 -0
- package/templates/vite/.howone/skills/howone-sdk/02-database/01-schema-design.md +147 -0
- package/templates/vite/.howone/skills/howone-sdk/02-database/02-schema-operations.md +96 -0
- package/templates/vite/.howone/skills/howone-sdk/02-database/03-data-access-patterns.md +172 -0
- package/templates/vite/.howone/skills/howone-sdk/{references → 03-sdk}/01-client-setup.md +3 -3
- package/templates/vite/.howone/skills/howone-sdk/{references/04-auth.md → 03-sdk/03-auth.md} +120 -3
- package/templates/vite/.howone/skills/howone-sdk/04-ai/.gitkeep +1 -0
- package/templates/vite/.howone/skills/howone-sdk/SKILL.md +67 -93
- package/templates/vite/.howone/skills/howone-sdk/agents/openai.yaml +3 -3
- package/templates/vite/.howone/skills/impeccable/SKILL.md +168 -0
- package/templates/vite/.howone/skills/impeccable/agents/impeccable-asset-producer.md +101 -0
- package/templates/vite/.howone/skills/impeccable/reference/adapt.md +190 -0
- package/templates/vite/.howone/skills/impeccable/reference/animate.md +175 -0
- package/templates/vite/.howone/skills/impeccable/reference/audit.md +133 -0
- package/templates/vite/.howone/skills/impeccable/reference/bolder.md +113 -0
- package/templates/vite/.howone/skills/impeccable/reference/brand.md +118 -0
- package/templates/vite/.howone/skills/impeccable/reference/clarify.md +174 -0
- package/templates/vite/.howone/skills/impeccable/reference/codex.md +105 -0
- package/templates/vite/.howone/skills/impeccable/reference/cognitive-load.md +106 -0
- package/templates/vite/.howone/skills/impeccable/reference/color-and-contrast.md +105 -0
- package/templates/vite/.howone/skills/impeccable/reference/colorize.md +154 -0
- package/templates/vite/.howone/skills/impeccable/reference/craft.md +123 -0
- package/templates/vite/.howone/skills/impeccable/reference/critique.md +273 -0
- package/templates/vite/.howone/skills/impeccable/reference/delight.md +302 -0
- package/templates/vite/.howone/skills/impeccable/reference/distill.md +111 -0
- package/templates/vite/.howone/skills/impeccable/reference/document.md +427 -0
- package/templates/vite/.howone/skills/impeccable/reference/extract.md +69 -0
- package/templates/vite/.howone/skills/impeccable/reference/harden.md +347 -0
- package/templates/vite/.howone/skills/impeccable/reference/heuristics-scoring.md +234 -0
- package/templates/vite/.howone/skills/impeccable/reference/interaction-design.md +195 -0
- package/templates/vite/.howone/skills/impeccable/reference/layout.md +141 -0
- package/templates/vite/.howone/skills/impeccable/reference/live.md +622 -0
- package/templates/vite/.howone/skills/impeccable/reference/motion-design.md +109 -0
- package/templates/vite/.howone/skills/impeccable/reference/onboard.md +234 -0
- package/templates/vite/.howone/skills/impeccable/reference/optimize.md +258 -0
- package/templates/vite/.howone/skills/impeccable/reference/overdrive.md +130 -0
- package/templates/vite/.howone/skills/impeccable/reference/personas.md +179 -0
- package/templates/vite/.howone/skills/impeccable/reference/polish.md +242 -0
- package/templates/vite/.howone/skills/impeccable/reference/product.md +62 -0
- package/templates/vite/.howone/skills/impeccable/reference/quieter.md +99 -0
- package/templates/vite/.howone/skills/impeccable/reference/responsive-design.md +114 -0
- package/templates/vite/.howone/skills/impeccable/reference/shape.md +165 -0
- package/templates/vite/.howone/skills/impeccable/reference/spatial-design.md +100 -0
- package/templates/vite/.howone/skills/impeccable/reference/teach.md +156 -0
- package/templates/vite/.howone/skills/impeccable/reference/typeset.md +124 -0
- package/templates/vite/.howone/skills/impeccable/reference/typography.md +159 -0
- package/templates/vite/.howone/skills/impeccable/reference/ux-writing.md +107 -0
- package/templates/vite/.howone/skills/impeccable/scripts/cleanup-deprecated.mjs +284 -0
- package/templates/vite/.howone/skills/impeccable/scripts/command-metadata.json +94 -0
- package/templates/vite/.howone/skills/impeccable/scripts/critique-storage.mjs +242 -0
- package/templates/vite/.howone/skills/impeccable/scripts/design-parser.mjs +820 -0
- package/templates/vite/.howone/skills/impeccable/scripts/detect-csp.mjs +198 -0
- package/templates/vite/.howone/skills/impeccable/scripts/detect.mjs +21 -0
- package/templates/vite/.howone/skills/impeccable/scripts/impeccable-paths.mjs +110 -0
- package/templates/vite/.howone/skills/impeccable/scripts/is-generated.mjs +69 -0
- package/templates/vite/.howone/skills/impeccable/scripts/live-accept.mjs +595 -0
- package/templates/vite/.howone/skills/impeccable/scripts/live-browser-session.js +123 -0
- package/templates/vite/.howone/skills/impeccable/scripts/live-browser.js +4860 -0
- package/templates/vite/.howone/skills/impeccable/scripts/live-complete.mjs +75 -0
- package/templates/vite/.howone/skills/impeccable/scripts/live-completion.mjs +18 -0
- package/templates/vite/.howone/skills/impeccable/scripts/live-inject.mjs +446 -0
- package/templates/vite/.howone/skills/impeccable/scripts/live-poll.mjs +200 -0
- package/templates/vite/.howone/skills/impeccable/scripts/live-resume.mjs +48 -0
- package/templates/vite/.howone/skills/impeccable/scripts/live-server.mjs +838 -0
- package/templates/vite/.howone/skills/impeccable/scripts/live-session-store.mjs +254 -0
- package/templates/vite/.howone/skills/impeccable/scripts/live-status.mjs +47 -0
- package/templates/vite/.howone/skills/impeccable/scripts/live-wrap.mjs +632 -0
- package/templates/vite/.howone/skills/impeccable/scripts/live.mjs +247 -0
- package/templates/vite/.howone/skills/impeccable/scripts/load-context.mjs +141 -0
- package/templates/vite/.howone/skills/impeccable/scripts/modern-screenshot.umd.js +14 -0
- package/templates/vite/.howone/skills/impeccable/scripts/pin.mjs +214 -0
- package/templates/vite/AGENTS.md +2 -3
- package/templates/vite/package.json +1 -1
- /package/templates/vite/.howone/skills/howone-sdk/{references/08-manifest-codegen.md → 01-architect/02-manifest-codegen.md} +0 -0
- /package/templates/vite/.howone/skills/howone-sdk/{references → 03-sdk}/02-entity-operations.md +0 -0
- /package/templates/vite/.howone/skills/howone-sdk/{references/06-react-integration.md → 03-sdk/04-react-integration.md} +0 -0
- /package/templates/vite/.howone/skills/howone-sdk/{references → 03-sdk}/05-file-upload.md +0 -0
- /package/templates/vite/.howone/skills/howone-sdk/{references/07-raw-http.md → 03-sdk/06-raw-http.md} +0 -0
- /package/templates/vite/.howone/skills/howone-sdk/{references/03-ai-actions.md → 03-sdk/07-ai-action-calls.md} +0 -0
|
@@ -0,0 +1,1175 @@
|
|
|
1
|
+
// Hallmark — landing-page interactions.
|
|
2
|
+
// Sticky banner theme picker + per-theme component archetype swap.
|
|
3
|
+
// Dogfoods the patterns in references/microinteractions.md.
|
|
4
|
+
|
|
5
|
+
const reduced = matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
6
|
+
|
|
7
|
+
/* Reveal/scroll-in animations are disabled by design — every .reveal
|
|
8
|
+
element renders in its final state on load so scrolling reads clean. */
|
|
9
|
+
document.querySelectorAll(".reveal").forEach((el) => el.classList.add("is-in"));
|
|
10
|
+
|
|
11
|
+
/* — Hover-to-play videos —————————————————————————————————
|
|
12
|
+
Videos with data-hover-play render as static first-frame on desktop
|
|
13
|
+
(hover-capable devices) and only play while the pointer is over the
|
|
14
|
+
card. On touch devices (no hover), they autoplay continuously so the
|
|
15
|
+
page reads as a moving showcase on mobile. */
|
|
16
|
+
{
|
|
17
|
+
const supportsHover = matchMedia("(hover: hover) and (pointer: fine)").matches;
|
|
18
|
+
const videos = document.querySelectorAll("video[data-hover-play]");
|
|
19
|
+
|
|
20
|
+
if (supportsHover) {
|
|
21
|
+
videos.forEach((video) => {
|
|
22
|
+
// Pause + reset to first frame on load (desktop static preview).
|
|
23
|
+
video.removeAttribute("autoplay");
|
|
24
|
+
try { video.pause(); video.currentTime = 0; } catch (_) {}
|
|
25
|
+
|
|
26
|
+
const card = video.closest(".ex-card, .diptych__half") || video.parentElement;
|
|
27
|
+
if (!card) return;
|
|
28
|
+
|
|
29
|
+
const onEnter = () => { video.play().catch(() => {}); };
|
|
30
|
+
const onLeave = () => { video.pause(); try { video.currentTime = 0; } catch (_) {} };
|
|
31
|
+
|
|
32
|
+
card.addEventListener("mouseenter", onEnter);
|
|
33
|
+
card.addEventListener("mouseleave", onLeave);
|
|
34
|
+
card.addEventListener("focusin", onEnter);
|
|
35
|
+
card.addEventListener("focusout", onLeave);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
// On touch devices, the autoplay attribute already runs the loop.
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* — Theme registry ————————————————————————————————————— */
|
|
42
|
+
const THEMES = {
|
|
43
|
+
specimen: "Specimen",
|
|
44
|
+
midnight: "Midnight",
|
|
45
|
+
brutal: "Brutal",
|
|
46
|
+
garden: "Garden",
|
|
47
|
+
atelier: "Atelier",
|
|
48
|
+
newsprint: "Newsprint",
|
|
49
|
+
terminal: "Terminal",
|
|
50
|
+
manifesto: "Manifesto",
|
|
51
|
+
salon: "Salon",
|
|
52
|
+
linen: "Linen",
|
|
53
|
+
almanac: "Almanac",
|
|
54
|
+
sport: "Sport",
|
|
55
|
+
studio: "Studio",
|
|
56
|
+
riso: "Riso",
|
|
57
|
+
quiet: "Quiet",
|
|
58
|
+
bloom: "Bloom",
|
|
59
|
+
coral: "Coral",
|
|
60
|
+
violet: "Violet",
|
|
61
|
+
aurora: "Aurora",
|
|
62
|
+
halo: "Halo",
|
|
63
|
+
plume: "Plume",
|
|
64
|
+
editorial: "Editorial",
|
|
65
|
+
};
|
|
66
|
+
const STORAGE_KEY = "hallmark-theme";
|
|
67
|
+
|
|
68
|
+
/* — Theme → archetype tuple map ——————————————————————————
|
|
69
|
+
Each theme picks one cookbook entry per slot. The point is structural
|
|
70
|
+
variety: switching themes literally rebuilds the page, not just
|
|
71
|
+
recolours it. See references/component-cookbook.md. */
|
|
72
|
+
const ARCHETYPES = {
|
|
73
|
+
specimen: { hero: "marquee", footer: "colophon" },
|
|
74
|
+
newsprint: { hero: "split", footer: "colophon" },
|
|
75
|
+
atelier: { hero: "quote-led", footer: "colophon" },
|
|
76
|
+
garden: { hero: "letter", footer: "colophon" },
|
|
77
|
+
salon: { hero: "quote-led", footer: "colophon" },
|
|
78
|
+
linen: { hero: "letter", footer: "colophon" },
|
|
79
|
+
almanac: { hero: "stat-led", footer: "colophon" },
|
|
80
|
+
midnight: { hero: "stat-led", footer: "colophon" },
|
|
81
|
+
terminal: { hero: "marquee", footer: "colophon" },
|
|
82
|
+
brutal: { hero: "marquee", footer: "colophon" },
|
|
83
|
+
manifesto: { hero: "marquee", footer: "colophon" },
|
|
84
|
+
sport: { hero: "stat-led", footer: "colophon" },
|
|
85
|
+
studio: { hero: "letter", footer: "colophon" },
|
|
86
|
+
riso: { hero: "quote-led", footer: "colophon" },
|
|
87
|
+
quiet: { hero: "split", footer: "colophon" },
|
|
88
|
+
bloom: { hero: "marquee", footer: "colophon" },
|
|
89
|
+
coral: { hero: "split", footer: "colophon" },
|
|
90
|
+
violet: { hero: "split", footer: "colophon" },
|
|
91
|
+
aurora: { hero: "marquee", footer: "colophon" },
|
|
92
|
+
halo: { hero: "orbit", footer: "colophon" },
|
|
93
|
+
plume: { hero: "bloom", footer: "colophon" },
|
|
94
|
+
editorial: { hero: "split", footer: "colophon" },
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/* — Theme → genre map ——————————————————————————————————
|
|
98
|
+
Each theme belongs to one of four genres — a rule-set overlay that
|
|
99
|
+
scopes which slop-test gates apply and which voice fixtures the
|
|
100
|
+
skill picks from. See references/genres/. */
|
|
101
|
+
const THEME_GENRES = {
|
|
102
|
+
// editorial — the canonical Hallmark voice (12 themes)
|
|
103
|
+
specimen: "editorial",
|
|
104
|
+
newsprint: "editorial",
|
|
105
|
+
atelier: "editorial",
|
|
106
|
+
garden: "editorial",
|
|
107
|
+
salon: "editorial",
|
|
108
|
+
linen: "editorial",
|
|
109
|
+
almanac: "editorial",
|
|
110
|
+
studio: "editorial",
|
|
111
|
+
riso: "editorial",
|
|
112
|
+
sport: "editorial",
|
|
113
|
+
brutal: "editorial",
|
|
114
|
+
manifesto: "editorial",
|
|
115
|
+
// modern-minimal — Stripe / Linear / ElevenLabs school (3 themes)
|
|
116
|
+
quiet: "modern-minimal",
|
|
117
|
+
coral: "modern-minimal",
|
|
118
|
+
violet: "modern-minimal",
|
|
119
|
+
// atmospheric — Suno / Runway / dark-AI-tool school (5 themes)
|
|
120
|
+
bloom: "atmospheric",
|
|
121
|
+
midnight: "atmospheric",
|
|
122
|
+
terminal: "atmospheric",
|
|
123
|
+
aurora: "atmospheric",
|
|
124
|
+
halo: "atmospheric",
|
|
125
|
+
// playful — post-Linear soft school (1 theme)
|
|
126
|
+
plume: "playful",
|
|
127
|
+
// editorial — open-design-inspired premium (added v1.0.0)
|
|
128
|
+
editorial: "editorial",
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/* — Locked hero title —————————————————————————————————
|
|
132
|
+
The H1 string is the same across every theme. Only the visual
|
|
133
|
+
treatment swaps — italic vs roman, serif vs sans, all-caps vs not.
|
|
134
|
+
The page is the demo: one sentence, twenty-two distinct designs. */
|
|
135
|
+
const HERO_TITLE = "A design skill that refuses to look AI-generated.";
|
|
136
|
+
|
|
137
|
+
/* — Per-theme copy fixtures —————————————————————————————
|
|
138
|
+
Distinct voice per theme so the page doesn't read like the same
|
|
139
|
+
page in different fonts. The `title` is locked across themes;
|
|
140
|
+
eyebrow, lede, quote, stat, salutation, etc. still vary. */
|
|
141
|
+
const COPY = {
|
|
142
|
+
specimen: {
|
|
143
|
+
eyebrow: "A design skill",
|
|
144
|
+
title: HERO_TITLE,
|
|
145
|
+
lede: "Hallmark is a skill for Claude Code, Cursor, and Codex. It encodes the anti-slop consensus — typography, colour, layout, motion, interaction — into one holistic ruleset your AI assistant will actually follow.",
|
|
146
|
+
ctaLabel: "01 ⁄ Install",
|
|
147
|
+
proofLabel: "Proof",
|
|
148
|
+
proofA: "21 macrostructures · 40 archetypes",
|
|
149
|
+
proofB: "9 navs · 8 footers · 4 hero polish",
|
|
150
|
+
proofC: "55-gate slop test",
|
|
151
|
+
cta: "Read the rules",
|
|
152
|
+
stat: "21",
|
|
153
|
+
qualifier: "macrostructures",
|
|
154
|
+
quote: "Two pages from two briefs feel like different sites — not colour-swaps of one template.",
|
|
155
|
+
attrib: "The rule Hallmark is built around",
|
|
156
|
+
salutation: "Dear designer,",
|
|
157
|
+
letterBody: "Every LLM has been trained on the same templates. Without a firm hand, they all emit the same page. Hallmark is the firm hand. It refuses the defaults, asks the questions that matter, and stamps the page so the next run produces something genuinely different.",
|
|
158
|
+
signoff: "With care,",
|
|
159
|
+
captionA: "Plate 01",
|
|
160
|
+
captionB: "From the working archive",
|
|
161
|
+
},
|
|
162
|
+
newsprint: {
|
|
163
|
+
eyebrow: "Volume I · Issue 02 · 28 April 2026",
|
|
164
|
+
title: HERO_TITLE,
|
|
165
|
+
lede: "Twenty-three themes. Twenty-one named page shapes. Forty component archetypes — nine navs, eight footers, four hero polish patterns. A 55-gate slop test that gates every output before it ships. Hallmark is the rulebook the LLM never read.",
|
|
166
|
+
ctaLabel: "Distribution",
|
|
167
|
+
proofLabel: "From the rule sheet",
|
|
168
|
+
proofA: "Multi-column body, justified",
|
|
169
|
+
proofB: "Hairline rules, not boxes",
|
|
170
|
+
proofC: "Outlined CTAs, never pills",
|
|
171
|
+
cta: "Read in full",
|
|
172
|
+
stat: "29",
|
|
173
|
+
qualifier: "gates",
|
|
174
|
+
quote: "We compose the page like a broadsheet. Hairlines, columns, restraint.",
|
|
175
|
+
attrib: "From the Hallmark rule sheet",
|
|
176
|
+
salutation: "Letter from the editor.",
|
|
177
|
+
letterBody: "There was a time when a printed page implied that a hand had been on it. We have tried, in this skill, to put a hand on every screen.",
|
|
178
|
+
signoff: "Yours,",
|
|
179
|
+
captionA: "Issue 02",
|
|
180
|
+
captionB: "Press run",
|
|
181
|
+
},
|
|
182
|
+
atelier: {
|
|
183
|
+
eyebrow: "An atelier note",
|
|
184
|
+
title: HERO_TITLE,
|
|
185
|
+
lede: "A small, opinionated craftsmanship engine that argues with your AI assistant on your behalf — and wins.",
|
|
186
|
+
ctaLabel: "By appointment",
|
|
187
|
+
proofLabel: "Marks of the house",
|
|
188
|
+
proofA: "OKLCH-anchored palettes",
|
|
189
|
+
proofB: "Italic display, weighted rest",
|
|
190
|
+
proofC: "Negative space as divider",
|
|
191
|
+
cta: "Request the manual",
|
|
192
|
+
stat: "12",
|
|
193
|
+
qualifier: "themes",
|
|
194
|
+
quote: "A workshop, not a template.",
|
|
195
|
+
attrib: "Studio note",
|
|
196
|
+
salutation: "A note from the studio.",
|
|
197
|
+
letterBody: "We do not believe in defaults. The default is the average; we are looking for the specific. Every page Hallmark touches is a small refusal of the average, in favour of a page that knows what it is.",
|
|
198
|
+
signoff: "— the studio",
|
|
199
|
+
captionA: "Workbench",
|
|
200
|
+
captionB: "12 April",
|
|
201
|
+
},
|
|
202
|
+
garden: {
|
|
203
|
+
eyebrow: "A small dispatch",
|
|
204
|
+
title: HERO_TITLE,
|
|
205
|
+
lede: "Twelve themes that disagree with each other on purpose. Pick one and the whole page changes — not the colours, the bones.",
|
|
206
|
+
ctaLabel: "Plant it",
|
|
207
|
+
proofLabel: "What grows here",
|
|
208
|
+
proofA: "Hairline rules · negative space",
|
|
209
|
+
proofB: "Italic body · serif emphasis",
|
|
210
|
+
proofC: "One accent, used sparingly",
|
|
211
|
+
cta: "Begin",
|
|
212
|
+
stat: "12",
|
|
213
|
+
qualifier: "themes in the garden",
|
|
214
|
+
quote: "A garden is not the absence of weeds. It is the presence of a plan.",
|
|
215
|
+
attrib: "Hallmark, on design",
|
|
216
|
+
salutation: "Hello,",
|
|
217
|
+
letterBody: "This skill is small. It is opinionated about a few things and quiet about the rest. We have tended it like a garden: pulling out the loud, leaving the considered. We hope it produces, for you, pages that feel grown rather than generated.",
|
|
218
|
+
signoff: "Yours,",
|
|
219
|
+
captionA: "Plot 04",
|
|
220
|
+
captionB: "Late spring",
|
|
221
|
+
},
|
|
222
|
+
salon: {
|
|
223
|
+
eyebrow: "A salon",
|
|
224
|
+
title: HERO_TITLE,
|
|
225
|
+
lede: "Hallmark is composed, not generated. Twelve themes, twenty-one page shapes, thirty-two archetypes, all chosen with intent.",
|
|
226
|
+
ctaLabel: "By invitation",
|
|
227
|
+
proofLabel: "Of note",
|
|
228
|
+
proofA: "Centred display · ornamental",
|
|
229
|
+
proofB: "Fleuron dividers, tightly cropped",
|
|
230
|
+
proofC: "One typographic CTA per fold",
|
|
231
|
+
cta: "Be received",
|
|
232
|
+
stat: "21",
|
|
233
|
+
qualifier: "page shapes",
|
|
234
|
+
quote: "A page should arrive like a person — composed, deliberate, in good clothes.",
|
|
235
|
+
attrib: "From the salon",
|
|
236
|
+
salutation: "With pleasure,",
|
|
237
|
+
letterBody: "You are most welcome. We have arranged the room with care. Each theme is a different room, and you are invited to walk through all twelve. Take your time — they are all furnished with the same intention.",
|
|
238
|
+
signoff: "À bientôt,",
|
|
239
|
+
captionA: "Salon No. 7",
|
|
240
|
+
captionB: "April",
|
|
241
|
+
},
|
|
242
|
+
linen: {
|
|
243
|
+
eyebrow: "A note",
|
|
244
|
+
title: HERO_TITLE,
|
|
245
|
+
lede: "A skill that prefers the obvious thing done right. Hairline rules. Margin notes. Generous space. The page reads first, designs second.",
|
|
246
|
+
ctaLabel: "Begin reading",
|
|
247
|
+
proofLabel: "Plain rules",
|
|
248
|
+
proofA: "Two-column asymmetric body",
|
|
249
|
+
proofB: "Margin-aligned imagery",
|
|
250
|
+
proofC: "Unstyled-link CTAs",
|
|
251
|
+
cta: "Read on",
|
|
252
|
+
stat: "32",
|
|
253
|
+
qualifier: "archetypes",
|
|
254
|
+
quote: "If you can leave it out and the page still works, leave it out.",
|
|
255
|
+
attrib: "Linen rule",
|
|
256
|
+
salutation: "Dear reader,",
|
|
257
|
+
letterBody: "This is a longer letter than the other themes. It is a deliberate choice. Hallmark believes that prose-led pages still have a place — that not every product needs a hero, a stat, and a CTA stack. Sometimes, a paragraph is the page.",
|
|
258
|
+
signoff: "With patience,",
|
|
259
|
+
captionA: "Folio II",
|
|
260
|
+
captionB: "Quiet hours",
|
|
261
|
+
},
|
|
262
|
+
almanac: {
|
|
263
|
+
eyebrow: "Almanac · 2026 edition",
|
|
264
|
+
title: HERO_TITLE,
|
|
265
|
+
lede: "A reference book of structural choices, indexed and cross-referenced. Hallmark looks up the right page-shape for the brief and refuses to use the same one twice.",
|
|
266
|
+
ctaLabel: "Open the index",
|
|
267
|
+
proofLabel: "Catalogued",
|
|
268
|
+
proofA: "21 macrostructures · 40 archetypes",
|
|
269
|
+
proofB: "9 navs · 8 footers · 4 polish patterns",
|
|
270
|
+
proofC: "55 slop-test gates",
|
|
271
|
+
cta: "Open the index",
|
|
272
|
+
stat: "462",
|
|
273
|
+
qualifier: "theme × shape combinations",
|
|
274
|
+
quote: "An almanac is a book that knows where to look.",
|
|
275
|
+
attrib: "Almanac, frontispiece",
|
|
276
|
+
salutation: "Reference note,",
|
|
277
|
+
letterBody: "This page is a reference, not an argument. The numbers are the point: 21 macrostructures, 40 archetypes, 22 themes, 65 slop-test gates. Cross-referenced so the next page Hallmark builds is genuinely different from the last.",
|
|
278
|
+
signoff: "— editor",
|
|
279
|
+
captionA: "Vol. III",
|
|
280
|
+
captionB: "Plate 12",
|
|
281
|
+
},
|
|
282
|
+
midnight: {
|
|
283
|
+
eyebrow: "Built for the dark",
|
|
284
|
+
title: HERO_TITLE,
|
|
285
|
+
lede: "A dark theme that uses lightness for elevation, not shadow. Numbered display labels. Typewriter reveals. Indigo accent at low chroma.",
|
|
286
|
+
ctaLabel: "Run it",
|
|
287
|
+
proofLabel: "Console",
|
|
288
|
+
proofA: "OKLCH dark palette · perceptual",
|
|
289
|
+
proofB: "Lightness elevation, no shadow",
|
|
290
|
+
proofC: "Numbered display headers",
|
|
291
|
+
cta: "$ open",
|
|
292
|
+
stat: "23",
|
|
293
|
+
qualifier: "themes, dark-set first",
|
|
294
|
+
quote: "On dark surfaces, elevation is lightness — never glow.",
|
|
295
|
+
attrib: "Midnight rule",
|
|
296
|
+
salutation: "01 — HELLO.",
|
|
297
|
+
letterBody: "This is a dark page that tries not to be a tinted-light page. The neutrals are mixed at low chroma in OKLCH so the steps feel even at the eye, not just at the value. Elevation is brighter surface, not heavier shadow.",
|
|
298
|
+
signoff: "— Midnight",
|
|
299
|
+
captionA: "Frame 03",
|
|
300
|
+
captionB: "0240h",
|
|
301
|
+
},
|
|
302
|
+
terminal: {
|
|
303
|
+
eyebrow: "$ hallmark",
|
|
304
|
+
title: HERO_TITLE,
|
|
305
|
+
lede: "Honest about its medium. Monospace top to bottom. No animations. The page is what it is.",
|
|
306
|
+
ctaLabel: "Run",
|
|
307
|
+
proofLabel: "Process",
|
|
308
|
+
proofA: "Monospace, single column",
|
|
309
|
+
proofB: "Underlined links · no hover scale",
|
|
310
|
+
proofC: "No reveal animation",
|
|
311
|
+
cta: "$ run",
|
|
312
|
+
stat: "0",
|
|
313
|
+
qualifier: "animations",
|
|
314
|
+
quote: "$ tput sgr0",
|
|
315
|
+
attrib: "End of file",
|
|
316
|
+
salutation: "> hello",
|
|
317
|
+
letterBody: "> a terminal page is not a page that pretends. it does not transition. it does not hover-scale. it is monospace because the work that made it was monospace. the rest of the page can read what it likes.",
|
|
318
|
+
signoff: "> bye",
|
|
319
|
+
captionA: "frame_07",
|
|
320
|
+
captionB: "0241",
|
|
321
|
+
},
|
|
322
|
+
brutal: {
|
|
323
|
+
eyebrow: "Brutal — uncompromised",
|
|
324
|
+
title: HERO_TITLE,
|
|
325
|
+
lede: "Heavy display. Hard edges. One accent that means it. The grid does not flex; the grid is the point.",
|
|
326
|
+
ctaLabel: "Take it",
|
|
327
|
+
proofLabel: "Stack",
|
|
328
|
+
proofA: "Photographic full-bleed",
|
|
329
|
+
proofB: "Bleed-colour dividers",
|
|
330
|
+
proofC: "Oversized solid CTAs",
|
|
331
|
+
cta: "GO",
|
|
332
|
+
stat: "100",
|
|
333
|
+
qualifier: "PERCENT.",
|
|
334
|
+
quote: "A page that hedges is a page that fails.",
|
|
335
|
+
attrib: "BRUTAL",
|
|
336
|
+
salutation: "DEAR READER.",
|
|
337
|
+
letterBody: "WE WILL NOT HEDGE. THE GRID DOES NOT FLEX. THE TYPE IS HEAVY. THE ACCENT IS RED. EVERY DECISION ON THIS PAGE IS A DECISION; NONE OF THEM ARE DEFAULTS. TAKE IT OR LEAVE IT.",
|
|
338
|
+
signoff: "— BRUTAL",
|
|
339
|
+
captionA: "BLOCK A",
|
|
340
|
+
captionB: "PRINT 01",
|
|
341
|
+
},
|
|
342
|
+
manifesto: {
|
|
343
|
+
eyebrow: "Manifesto",
|
|
344
|
+
title: "The Design Skill.",
|
|
345
|
+
lede: "The page is a statement. We don't soften it. The accent is a colour with a position. The headline is a belief.",
|
|
346
|
+
ctaLabel: "Sign it",
|
|
347
|
+
proofLabel: "Beliefs",
|
|
348
|
+
proofA: "Bleed-colour dividers",
|
|
349
|
+
proofB: "Oversized solid buttons",
|
|
350
|
+
proofC: "Declarative large type",
|
|
351
|
+
cta: "SIGN ON",
|
|
352
|
+
stat: "23",
|
|
353
|
+
qualifier: "tells, named.",
|
|
354
|
+
quote: "WE BELIEVE A LANDING PAGE IS NOT A TEMPLATE.",
|
|
355
|
+
attrib: "MANIFESTO",
|
|
356
|
+
salutation: "TO WHOM IT CONCERNS.",
|
|
357
|
+
letterBody: "WE BELIEVE THE PAGE IS A POSITION. WE BELIEVE THE TEMPLATE IS THE ENEMY. WE BELIEVE THAT EVERY DECISION SHOULD BE VISIBLE FROM ACROSS THE ROOM. WE BELIEVE THE ACCENT COLOUR IS A POLITICS. WE BELIEVE — AND THE PAGE BELIEVES WITH US.",
|
|
358
|
+
signoff: "— THE UNDERSIGNED",
|
|
359
|
+
captionA: "PLATE I",
|
|
360
|
+
captionB: "POSTER",
|
|
361
|
+
},
|
|
362
|
+
sport: {
|
|
363
|
+
eyebrow: "Sport · 2026",
|
|
364
|
+
title: HERO_TITLE,
|
|
365
|
+
lede: "Italic display. Tabular nums. Numbered display headers. The page moves like a scoreboard — fast, decisive, in motion.",
|
|
366
|
+
ctaLabel: "Kick off",
|
|
367
|
+
proofLabel: "Stats",
|
|
368
|
+
proofA: "Italic display, oversized",
|
|
369
|
+
proofB: "Tabular numbers everywhere",
|
|
370
|
+
proofC: "Horizontal-sweep reveals",
|
|
371
|
+
cta: "GO",
|
|
372
|
+
stat: "23",
|
|
373
|
+
qualifier: "THEMES · TWO GENRES DEEP.",
|
|
374
|
+
quote: "A FAST PAGE IS A FAST DECISION.",
|
|
375
|
+
attrib: "SPORT",
|
|
376
|
+
salutation: "READY?",
|
|
377
|
+
letterBody: "YOU ARE TWO MINUTES FROM SHIPPING. THE PAGE IS WAITING. EVERY DECISION ON IT IS NUMBERED, TABULATED, INDEXED. THE ACCENT IS A STARTING GUN. RUN IT.",
|
|
378
|
+
signoff: "— SPORT",
|
|
379
|
+
captionA: "RACE 03",
|
|
380
|
+
captionB: "LAP 12",
|
|
381
|
+
},
|
|
382
|
+
studio: {
|
|
383
|
+
eyebrow: "Studio · 2026",
|
|
384
|
+
title: HERO_TITLE,
|
|
385
|
+
lede: "We design and build distinctive products for ambitious teams. Hallmark is our typography opinion, codified — fifteen themes, twenty-one shapes, no defaults.",
|
|
386
|
+
ctaLabel: "Engage",
|
|
387
|
+
proofLabel: "Selected work",
|
|
388
|
+
proofA: "01 — A foundry rebrand",
|
|
389
|
+
proofB: "02 — A reading app",
|
|
390
|
+
proofC: "03 — A type specimen",
|
|
391
|
+
cta: "See the work",
|
|
392
|
+
stat: "21",
|
|
393
|
+
qualifier: "named page shapes.",
|
|
394
|
+
quote: "We don't believe in defaults. The default is the average; we are looking for the specific.",
|
|
395
|
+
attrib: "Studio note · 2026",
|
|
396
|
+
salutation: "Hello, friend.",
|
|
397
|
+
letterBody: "We started this studio because we kept being asked to build the same page, twelve different ways. Hallmark is our argument: one brief, one shape — chosen, not defaulted to. Take a look.",
|
|
398
|
+
signoff: "Yours,",
|
|
399
|
+
captionA: "Studio · 2026",
|
|
400
|
+
captionB: "Selected work",
|
|
401
|
+
},
|
|
402
|
+
riso: {
|
|
403
|
+
eyebrow: "ed. 12 · printed today",
|
|
404
|
+
title: HERO_TITLE,
|
|
405
|
+
lede: "warm paper, off-register accents, one bold lowercase headline. a page that feels printed, not generated.",
|
|
406
|
+
ctaLabel: "press →",
|
|
407
|
+
proofLabel: "colophon",
|
|
408
|
+
proofA: "bricolage display, 800 weight",
|
|
409
|
+
proofB: "newsreader italic body",
|
|
410
|
+
proofC: "riso cyan + yellow accent pair",
|
|
411
|
+
cta: "print one →",
|
|
412
|
+
stat: "12",
|
|
413
|
+
qualifier: "editions, hand-set.",
|
|
414
|
+
quote: "design like print: warm, off-register, intentional.",
|
|
415
|
+
attrib: "RISO · ed. 12",
|
|
416
|
+
salutation: "from the press,",
|
|
417
|
+
letterBody: "this is not a page that pretends to be paper. it is a page that remembers paper. the colors mis-register on purpose. the headline sits low. the body is a serif italic that wants to be read in a chair, not on a phone in a queue. take a seat.",
|
|
418
|
+
signoff: "— the press",
|
|
419
|
+
captionA: "ed. 12",
|
|
420
|
+
captionB: "press · 04",
|
|
421
|
+
},
|
|
422
|
+
quiet: {
|
|
423
|
+
eyebrow: "Polished minimal",
|
|
424
|
+
title: HERO_TITLE,
|
|
425
|
+
lede: "Geist sans. Pure white. One bold display. Generous space. The design decides what to leave out — and stands behind those choices.",
|
|
426
|
+
ctaLabel: "Get started",
|
|
427
|
+
proofLabel: "Decisions",
|
|
428
|
+
proofA: "Pure-white paper, dark ink",
|
|
429
|
+
proofB: "Geist sans, single weight",
|
|
430
|
+
proofC: "Pill CTAs · monochrome accent",
|
|
431
|
+
cta: "Get started",
|
|
432
|
+
stat: "1",
|
|
433
|
+
qualifier: "decision, made everywhere.",
|
|
434
|
+
mockStat: "1",
|
|
435
|
+
quote: "The work that looks effortless is the work where the choices were made.",
|
|
436
|
+
attrib: "Quiet",
|
|
437
|
+
salutation: "Hello.",
|
|
438
|
+
letterBody: "A theme for the modern enterprise page — the Stripe / Linear / ElevenLabs school of restraint. Clean white, confident typography, pill CTAs. Minimalism with conviction, not absence.",
|
|
439
|
+
signoff: "Yours,",
|
|
440
|
+
captionA: "Quiet",
|
|
441
|
+
captionB: "v1.0",
|
|
442
|
+
},
|
|
443
|
+
bloom: {
|
|
444
|
+
eyebrow: "Atmospheric · 2026",
|
|
445
|
+
title: HERO_TITLE,
|
|
446
|
+
lede: "For the AI-creative product page. Dark canvas, warm bloom, declarative type. The aesthetic of a tool you'd actually want to use after dark.",
|
|
447
|
+
ctaLabel: "Try it now",
|
|
448
|
+
proofLabel: "Atmosphere",
|
|
449
|
+
proofA: "Dark canvas with two warm blooms",
|
|
450
|
+
proofB: "Geist sans, one weight, plain English",
|
|
451
|
+
proofC: "Single warm accent — never gradient text",
|
|
452
|
+
cta: "Try it now",
|
|
453
|
+
stat: "1",
|
|
454
|
+
qualifier: "warm canvas — many uses.",
|
|
455
|
+
mockStat: "1",
|
|
456
|
+
quote: "The page should feel like a place you could sit in.",
|
|
457
|
+
attrib: "Bloom note",
|
|
458
|
+
salutation: "Welcome,",
|
|
459
|
+
letterBody: "A dark theme for the AI-creative tool page — Suno, Runway, the late-night software where atmosphere matters. Two soft colour blooms, plain confident type, a single warm accent. Restraint of a different kind.",
|
|
460
|
+
signoff: "— Bloom",
|
|
461
|
+
captionA: "Bloom",
|
|
462
|
+
captionB: "Late-night",
|
|
463
|
+
},
|
|
464
|
+
coral: {
|
|
465
|
+
eyebrow: "Modern minimal · warm-grey",
|
|
466
|
+
title: HERO_TITLE,
|
|
467
|
+
lede: "For the polished SaaS that wants warmth without losing discipline. Warm-grey paper, Geist throughout, a single coral accent kept under three percent of the view.",
|
|
468
|
+
ctaLabel: "Get started",
|
|
469
|
+
proofLabel: "Decisions",
|
|
470
|
+
proofA: "Warm-grey paper, not pure white",
|
|
471
|
+
proofB: "Geist + General Sans, one weight",
|
|
472
|
+
proofC: "Single coral accent · pill CTAs",
|
|
473
|
+
cta: "Get started",
|
|
474
|
+
stat: "1",
|
|
475
|
+
qualifier: "warm minimal · pill CTAs.",
|
|
476
|
+
mockStat: "1",
|
|
477
|
+
quote: "Restraint with warmth is harder than restraint without it.",
|
|
478
|
+
attrib: "Coral note",
|
|
479
|
+
salutation: "Hello.",
|
|
480
|
+
letterBody: "The polished-SaaS school of restraint, but warmer. Warm-grey paper instead of pure white; coral accent on focus rings and small marks. Pill CTAs, two-column heroes, generous space.",
|
|
481
|
+
signoff: "Yours,",
|
|
482
|
+
captionA: "Coral",
|
|
483
|
+
captionB: "v1.0",
|
|
484
|
+
},
|
|
485
|
+
violet: {
|
|
486
|
+
eyebrow: "Modern minimal · quiet violet",
|
|
487
|
+
title: HERO_TITLE,
|
|
488
|
+
lede: "Restrained near-black on near-white. Tight Geist throughout. A single quiet violet accent on focus rings — the rest is type, space, and rhythm.",
|
|
489
|
+
ctaLabel: "Get started",
|
|
490
|
+
proofLabel: "Decisions",
|
|
491
|
+
proofA: "Near-white paper · near-black ink",
|
|
492
|
+
proofB: "Geist tight tracking, single weight",
|
|
493
|
+
proofC: "Quiet violet · focus rings + small marks",
|
|
494
|
+
cta: "Get started",
|
|
495
|
+
stat: "1",
|
|
496
|
+
qualifier: "the Linear voice, not the brand.",
|
|
497
|
+
mockStat: "1",
|
|
498
|
+
quote: "The work that looks effortless is the work where the choices were made.",
|
|
499
|
+
attrib: "Violet note",
|
|
500
|
+
salutation: "Hello.",
|
|
501
|
+
letterBody: "A modern minimal theme tuned for dev tools and platforms. Near-white paper, near-black ink, single quiet violet accent on focus rings. Tight Geist throughout — letterspacing pulled in, type-led hierarchy, no ornament.",
|
|
502
|
+
signoff: "Yours,",
|
|
503
|
+
captionA: "Violet",
|
|
504
|
+
captionB: "v1.0",
|
|
505
|
+
},
|
|
506
|
+
aurora: {
|
|
507
|
+
eyebrow: "Atmospheric · cool",
|
|
508
|
+
title: HERO_TITLE,
|
|
509
|
+
lede: "For the after-dark dev tool. Cool blue-green canvas, two atmospheric blooms behind the content, single cyan accent. Sentient body for warmth on the cool ground.",
|
|
510
|
+
ctaLabel: "Open it",
|
|
511
|
+
proofLabel: "Atmosphere",
|
|
512
|
+
proofA: "Dark cool canvas · two cool blooms",
|
|
513
|
+
proofB: "Geist display + Sentient body",
|
|
514
|
+
proofC: "Cyan accent, never gradient text",
|
|
515
|
+
cta: "Open it",
|
|
516
|
+
stat: "2",
|
|
517
|
+
qualifier: "atmospheric blooms · cool canvas.",
|
|
518
|
+
mockStat: "2",
|
|
519
|
+
quote: "The page should feel like the moment after a deploy goes green.",
|
|
520
|
+
attrib: "Aurora note",
|
|
521
|
+
salutation: "Online,",
|
|
522
|
+
letterBody: "A dark cool atmospheric theme — the dev-tool-after-dark register. Two cool blooms (cyan top-right, teal-green bottom-left). Geist display for confidence; Sentient body to keep the cool ground from feeling clinical.",
|
|
523
|
+
signoff: "— Aurora",
|
|
524
|
+
captionA: "Aurora",
|
|
525
|
+
captionB: "Late-shift",
|
|
526
|
+
},
|
|
527
|
+
halo: {
|
|
528
|
+
eyebrow: "Atmospheric · charcoal",
|
|
529
|
+
title: HERO_TITLE,
|
|
530
|
+
lede: "Less canvas, more content. Neutral charcoal page with a single warm-amber bloom around the hero — below that, the rest of the page is content-led on dark paper. The tool you actually work in.",
|
|
531
|
+
ctaLabel: "Get to work",
|
|
532
|
+
proofLabel: "Discipline",
|
|
533
|
+
proofA: "Charcoal canvas · one warm bloom up top",
|
|
534
|
+
proofB: "Geist throughout · single weight",
|
|
535
|
+
proofC: "Hero is the only atmospheric moment",
|
|
536
|
+
cta: "Get to work",
|
|
537
|
+
stat: "1",
|
|
538
|
+
qualifier: "moment of atmosphere · then content.",
|
|
539
|
+
mockStat: "1",
|
|
540
|
+
quote: "The atmosphere does its job at the top, then steps aside.",
|
|
541
|
+
attrib: "Halo note",
|
|
542
|
+
salutation: "Online,",
|
|
543
|
+
letterBody: "A dark theme for the working tool — the bloom lives only at the top of the page, around the hero. Below it the canvas is plain charcoal, content-led, no atmospheric distractions. Less Suno, more the IDE you actually open every day.",
|
|
544
|
+
signoff: "— Halo",
|
|
545
|
+
captionA: "Halo",
|
|
546
|
+
captionB: "Workshop",
|
|
547
|
+
},
|
|
548
|
+
plume: {
|
|
549
|
+
eyebrow: "Playful · warm cream",
|
|
550
|
+
title: HERO_TITLE,
|
|
551
|
+
lede: "Warm cream throughout. Alternating tinted bands on sections. Hover-lift on cards. A soft rose accent. Friendly without being twee — the page wants to feel approachable.",
|
|
552
|
+
ctaLabel: "Try it",
|
|
553
|
+
proofLabel: "Marks of the house",
|
|
554
|
+
proofA: "Warm cream paper · tinted bands on sections",
|
|
555
|
+
proofB: "Bricolage display + Geist body",
|
|
556
|
+
proofC: "Soft rose accent · hover-lift cards",
|
|
557
|
+
cta: "Try it",
|
|
558
|
+
stat: "1",
|
|
559
|
+
qualifier: "soft accent · friendly motion.",
|
|
560
|
+
mockStat: "1",
|
|
561
|
+
quote: "Soft is harder than serious; it has nowhere to hide.",
|
|
562
|
+
attrib: "Plume note",
|
|
563
|
+
salutation: "Hello,",
|
|
564
|
+
letterBody: "A playful theme that earns the word. Warm cream paper, alternating tinted bands so each section has its own register, soft drop shadows, hover-lift on cards. Friendly motion that responds to the user instead of performing for them.",
|
|
565
|
+
signoff: "Yours,",
|
|
566
|
+
captionA: "Plume",
|
|
567
|
+
captionB: "Late spring",
|
|
568
|
+
},
|
|
569
|
+
editorial: {
|
|
570
|
+
eyebrow: "No XXIII · Editorial",
|
|
571
|
+
title: HERO_TITLE,
|
|
572
|
+
lede: "An editorial-premium voice — warm cream paper, coral accent, mixed sans + serif italic. Magazine-shaped, hairline rules, asymmetric grids. Inspired by open-design.",
|
|
573
|
+
ctaLabel: "I · Install",
|
|
574
|
+
proofLabel: "From the rule sheet",
|
|
575
|
+
proofA: "Inter Tight 800 + Playfair italic, mixed in display",
|
|
576
|
+
proofB: "Hairlines, generous whitespace, Roman-numeral marginalia",
|
|
577
|
+
proofC: "Coral accent · ≤ 5% of viewport",
|
|
578
|
+
cta: "Read the issue",
|
|
579
|
+
stat: "23",
|
|
580
|
+
qualifier: "the twenty-third theme. open-design-inspired.",
|
|
581
|
+
mockStat: "23",
|
|
582
|
+
quote: "A magazine page knows how much to leave out.",
|
|
583
|
+
attrib: "Editorial, frontispiece",
|
|
584
|
+
salutation: "Dear reader,",
|
|
585
|
+
letterBody: "An editorial premium that takes its cue from the print magazines that still feel right — warm cream paper, hairline rules, Roman-numeral marginalia, an italic display word slipped inside a sans-serif headline. Coral the only colour besides ink. Asymmetric without being clever about it.",
|
|
586
|
+
signoff: "Yours,",
|
|
587
|
+
captionA: "Issue 23",
|
|
588
|
+
captionB: "Spring 2026",
|
|
589
|
+
},
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
/* — Slot population ———————————————————————————————————— */
|
|
593
|
+
const root = document.documentElement;
|
|
594
|
+
const banner = document.querySelector(".banner");
|
|
595
|
+
const currentLabel = document.querySelector("[data-theme-current]");
|
|
596
|
+
const dots = document.querySelectorAll("[data-theme-btn]");
|
|
597
|
+
const slotEls = {
|
|
598
|
+
hero: document.querySelector('[data-slot="hero"]'),
|
|
599
|
+
footer: document.querySelector('[data-slot="footer"]'),
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
function interpolate(node, copy) {
|
|
603
|
+
// Walk text nodes and attribute values, replace {{key}} with copy[key].
|
|
604
|
+
const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, null);
|
|
605
|
+
const textNodes = [];
|
|
606
|
+
let n; while ((n = walker.nextNode())) textNodes.push(n);
|
|
607
|
+
for (const t of textNodes) {
|
|
608
|
+
if (t.nodeValue.includes("{{")) {
|
|
609
|
+
t.nodeValue = t.nodeValue.replace(/\{\{(\w+)\}\}/g, (_, k) => copy[k] ?? "");
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
// Attributes
|
|
613
|
+
const all = node.querySelectorAll("*");
|
|
614
|
+
for (const el of all) {
|
|
615
|
+
for (const attr of el.attributes) {
|
|
616
|
+
if (attr.value.includes("{{")) {
|
|
617
|
+
attr.value = attr.value.replace(/\{\{(\w+)\}\}/g, (_, k) => copy[k] ?? "");
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function swapArchetypes(theme) {
|
|
624
|
+
const tuple = ARCHETYPES[theme] || ARCHETYPES.specimen;
|
|
625
|
+
const copy = COPY[theme];
|
|
626
|
+
|
|
627
|
+
for (const slot of ["hero", "footer"]) {
|
|
628
|
+
const region = slotEls[slot];
|
|
629
|
+
if (!region) continue;
|
|
630
|
+
const tplId = `${slot}-${tuple[slot]}`;
|
|
631
|
+
const tpl = document.getElementById(tplId);
|
|
632
|
+
if (!tpl) continue;
|
|
633
|
+
|
|
634
|
+
const fragment = tpl.content.cloneNode(true);
|
|
635
|
+
interpolate(fragment, copy);
|
|
636
|
+
|
|
637
|
+
region.replaceChildren(fragment);
|
|
638
|
+
region.dataset.archetype = tuple[slot];
|
|
639
|
+
|
|
640
|
+
// Trigger a one-shot fade-in for the freshly-populated children.
|
|
641
|
+
region.removeAttribute("data-populating");
|
|
642
|
+
void region.offsetWidth; // restart animation
|
|
643
|
+
region.setAttribute("data-populating", "");
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Re-attach copy buttons inside the new hero, since clone doesn't carry handlers.
|
|
647
|
+
attachCopyButtons(slotEls.hero);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/* — Copy-to-clipboard (silent success, label swap pattern) —————
|
|
651
|
+
Two click surfaces:
|
|
652
|
+
- The Copy button (visible on desktop) — explicit affordance.
|
|
653
|
+
- The whole pre[data-copy-source] — falls back to a tappable area
|
|
654
|
+
on mobile where the button is hidden by CSS.
|
|
655
|
+
Both call the same async copy + state-flash routine. We bind once
|
|
656
|
+
per element via `data-copy-bound` so re-attached templates don't
|
|
657
|
+
double-bind. */
|
|
658
|
+
async function copyFromSource(source) {
|
|
659
|
+
if (!source) return;
|
|
660
|
+
const textNode = source.querySelector("[data-copy-text]");
|
|
661
|
+
const text = textNode ? textNode.textContent.trim() : "";
|
|
662
|
+
if (!text) return;
|
|
663
|
+
|
|
664
|
+
try {
|
|
665
|
+
await navigator.clipboard.writeText(text);
|
|
666
|
+
} catch (err) {
|
|
667
|
+
const ta = document.createElement("textarea");
|
|
668
|
+
ta.value = text;
|
|
669
|
+
ta.setAttribute("readonly", "");
|
|
670
|
+
ta.style.position = "fixed";
|
|
671
|
+
ta.style.left = "-9999px";
|
|
672
|
+
document.body.appendChild(ta);
|
|
673
|
+
ta.select();
|
|
674
|
+
try { document.execCommand("copy"); } catch (e) { }
|
|
675
|
+
document.body.removeChild(ta);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Flash both the source and any visible button so the right
|
|
679
|
+
// surface (mobile pseudo-element vs desktop button label) animates.
|
|
680
|
+
source.dataset.state = "copied";
|
|
681
|
+
source.setAttribute("aria-live", "polite");
|
|
682
|
+
const btn = source.querySelector("[data-copy-btn]");
|
|
683
|
+
if (btn) btn.dataset.state = "copied";
|
|
684
|
+
|
|
685
|
+
clearTimeout(source._copyTimer);
|
|
686
|
+
source._copyTimer = setTimeout(() => {
|
|
687
|
+
delete source.dataset.state;
|
|
688
|
+
if (btn) delete btn.dataset.state;
|
|
689
|
+
}, 2200);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function attachCopyButtons(scope = document) {
|
|
693
|
+
// Bind the whole pre — works on mobile where the button is hidden.
|
|
694
|
+
const sources = scope.querySelectorAll("[data-copy-source]:not([data-copy-bound])");
|
|
695
|
+
sources.forEach((source) => {
|
|
696
|
+
source.dataset.copyBound = "true";
|
|
697
|
+
source.addEventListener("click", () => copyFromSource(source));
|
|
698
|
+
});
|
|
699
|
+
// Button click is also handled — stop propagation so the source
|
|
700
|
+
// listener doesn't double-fire (single copy per click).
|
|
701
|
+
const btns = scope.querySelectorAll("[data-copy-btn]:not([data-copy-btn-bound])");
|
|
702
|
+
btns.forEach((btn) => {
|
|
703
|
+
btn.dataset.copyBtnBound = "true";
|
|
704
|
+
btn.addEventListener("click", (e) => {
|
|
705
|
+
e.stopPropagation();
|
|
706
|
+
copyFromSource(btn.closest("[data-copy-source]"));
|
|
707
|
+
});
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
attachCopyButtons();
|
|
711
|
+
|
|
712
|
+
/* — Release :hover / :focus on the sticky Install pill after click.
|
|
713
|
+
The banner is position: sticky, so when smooth-scroll lands the
|
|
714
|
+
page on #install the cursor is still over the pill and Safari /
|
|
715
|
+
Chrome can keep the inverted hover stuck. Blur + a one-frame
|
|
716
|
+
pointer-events nudge forces the browser to release both states.
|
|
717
|
+
No layout side-effects; the pill is back to normal as soon as the
|
|
718
|
+
pointer next moves. */
|
|
719
|
+
document.querySelectorAll('.banner__install').forEach((el) => {
|
|
720
|
+
el.addEventListener('click', () => {
|
|
721
|
+
requestAnimationFrame(() => {
|
|
722
|
+
el.blur();
|
|
723
|
+
el.style.pointerEvents = 'none';
|
|
724
|
+
setTimeout(() => { el.style.pointerEvents = ''; }, 60);
|
|
725
|
+
});
|
|
726
|
+
});
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
/* — GitHub star count — cached in localStorage for 1h ——————————
|
|
730
|
+
Hits the public GitHub API on first paint, caches the count keyed
|
|
731
|
+
by REPO so renames auto-bust old caches. Stale-while-revalidate:
|
|
732
|
+
shows a cached value (even if past TTL) instantly, then fetches a
|
|
733
|
+
fresh count in the background and updates the DOM if it changed.
|
|
734
|
+
Falls back to whatever's currently on screen if the request fails. */
|
|
735
|
+
(() => {
|
|
736
|
+
const starEl = document.querySelector("[data-star-count]");
|
|
737
|
+
if (!starEl) return;
|
|
738
|
+
|
|
739
|
+
const REPO = "nutlope/hallmark";
|
|
740
|
+
const CACHE_KEY = "hallmark-star-count:" + REPO; // key by repo — renames auto-invalidate
|
|
741
|
+
const TTL = 60 * 60 * 1000; // 1h
|
|
742
|
+
|
|
743
|
+
const format = (n) => (n >= 1000 ? (n / 1000).toFixed(1) + "k" : String(n));
|
|
744
|
+
|
|
745
|
+
// Read cache first — show whatever's there instantly (even if stale).
|
|
746
|
+
let cachedFresh = false;
|
|
747
|
+
try {
|
|
748
|
+
const raw = localStorage.getItem(CACHE_KEY);
|
|
749
|
+
if (raw) {
|
|
750
|
+
const cached = JSON.parse(raw);
|
|
751
|
+
if (cached && typeof cached.n === "number") {
|
|
752
|
+
starEl.textContent = format(cached.n);
|
|
753
|
+
cachedFresh = Date.now() - cached.t < TTL;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
} catch (e) { /* localStorage may throw on private mode */ }
|
|
757
|
+
|
|
758
|
+
// Fresh cache → skip network. Stale or absent → fetch and update.
|
|
759
|
+
if (cachedFresh) return;
|
|
760
|
+
|
|
761
|
+
fetch(`https://api.github.com/repos/${REPO}`, { headers: { Accept: "application/vnd.github+json" } })
|
|
762
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
763
|
+
.then((d) => {
|
|
764
|
+
if (!d || typeof d.stargazers_count !== "number") return;
|
|
765
|
+
const n = d.stargazers_count;
|
|
766
|
+
starEl.textContent = format(n);
|
|
767
|
+
try { localStorage.setItem(CACHE_KEY, JSON.stringify({ n, t: Date.now() })); } catch (e) { }
|
|
768
|
+
})
|
|
769
|
+
.catch(() => { /* leave the cached / placeholder value as-is */ });
|
|
770
|
+
})();
|
|
771
|
+
|
|
772
|
+
/* — Theme application ————————————————————————————————— */
|
|
773
|
+
/* Cached banner subnodes — populated once at startup. */
|
|
774
|
+
const themeLabelEl = document.querySelector(".banner__theme");
|
|
775
|
+
const themeGenreEl = document.querySelector("[data-theme-genre]");
|
|
776
|
+
const ordinalEl = document.querySelector("[data-theme-ordinal]");
|
|
777
|
+
const themeKeys = Object.keys(THEMES);
|
|
778
|
+
const totalThemes = themeKeys.length;
|
|
779
|
+
|
|
780
|
+
function setPressed(theme) {
|
|
781
|
+
dots.forEach((btn) => {
|
|
782
|
+
const active = btn.dataset.themeBtn === theme;
|
|
783
|
+
btn.setAttribute("aria-pressed", active ? "true" : "false");
|
|
784
|
+
});
|
|
785
|
+
const themeName = THEMES[theme] || "Specimen";
|
|
786
|
+
const genre = THEME_GENRES[theme] || "editorial";
|
|
787
|
+
const idx = themeKeys.indexOf(theme);
|
|
788
|
+
const ordinal = idx >= 0 ? String(idx + 1).padStart(2, "0") : "01";
|
|
789
|
+
|
|
790
|
+
if (themeLabelEl) themeLabelEl.textContent = themeName;
|
|
791
|
+
if (themeGenreEl) themeGenreEl.textContent = genre;
|
|
792
|
+
if (ordinalEl) ordinalEl.textContent = `${ordinal} / ${totalThemes}`;
|
|
793
|
+
|
|
794
|
+
// Colophon footer — update the "Currently rendered in <theme>" line.
|
|
795
|
+
const footThemeEl = document.querySelector("[data-theme-current-foot]");
|
|
796
|
+
if (footThemeEl) footThemeEl.textContent = themeName;
|
|
797
|
+
|
|
798
|
+
// Fallback for older callers — keep the public theme-current span up to date.
|
|
799
|
+
if (currentLabel && !themeLabelEl) currentLabel.textContent = themeName;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function applyTheme(theme) {
|
|
803
|
+
if (!THEMES[theme]) return;
|
|
804
|
+
const apply = () => {
|
|
805
|
+
root.dataset.theme = theme;
|
|
806
|
+
swapArchetypes(theme);
|
|
807
|
+
setPressed(theme);
|
|
808
|
+
try { localStorage.setItem(STORAGE_KEY, theme); } catch (e) { }
|
|
809
|
+
};
|
|
810
|
+
if (!reduced && document.startViewTransition) {
|
|
811
|
+
document.startViewTransition(apply);
|
|
812
|
+
} else {
|
|
813
|
+
apply();
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
const queried = (() => {
|
|
818
|
+
try { return new URL(window.location.href).searchParams.get("theme"); } catch (e) { return null; }
|
|
819
|
+
})();
|
|
820
|
+
const stored = (() => {
|
|
821
|
+
try { return localStorage.getItem(STORAGE_KEY); } catch (e) { return null; }
|
|
822
|
+
})();
|
|
823
|
+
const initial = THEMES[queried] ? queried
|
|
824
|
+
: THEMES[stored] ? stored
|
|
825
|
+
: (root.dataset.theme || "specimen");
|
|
826
|
+
|
|
827
|
+
// First paint — populate slots without a transition (no flash).
|
|
828
|
+
// Run swapArchetypes BEFORE setPressed so the footer template is materialised
|
|
829
|
+
// before setPressed writes the current-theme name into it.
|
|
830
|
+
root.dataset.theme = initial;
|
|
831
|
+
swapArchetypes(initial);
|
|
832
|
+
setPressed(initial);
|
|
833
|
+
try { localStorage.setItem(STORAGE_KEY, initial); } catch (e) { }
|
|
834
|
+
|
|
835
|
+
dots.forEach((btn) => {
|
|
836
|
+
btn.addEventListener("click", () => {
|
|
837
|
+
applyTheme(btn.dataset.themeBtn);
|
|
838
|
+
closeThemeDropdown();
|
|
839
|
+
});
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
/* — Theme dropdown — open/close + outside-dismiss ————————————
|
|
843
|
+
The indicator button toggles the panel. Outside clicks, Escape,
|
|
844
|
+
and theme selection all close it. The dropdown is hidden via the
|
|
845
|
+
`hidden` attribute (CSS handles `[hidden] { display: none }`). */
|
|
846
|
+
const themeTrigger = document.querySelector("[data-theme-trigger]");
|
|
847
|
+
const themeDropdown = document.getElementById("theme-dropdown");
|
|
848
|
+
const themeWrap = document.querySelector("[data-theme-wrap]");
|
|
849
|
+
|
|
850
|
+
function openThemeDropdown() {
|
|
851
|
+
if (!themeDropdown || !themeTrigger) return;
|
|
852
|
+
themeDropdown.hidden = false;
|
|
853
|
+
themeTrigger.setAttribute("aria-expanded", "true");
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
function closeThemeDropdown() {
|
|
857
|
+
if (!themeDropdown || !themeTrigger) return;
|
|
858
|
+
themeDropdown.hidden = true;
|
|
859
|
+
themeTrigger.setAttribute("aria-expanded", "false");
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
if (themeTrigger && themeDropdown) {
|
|
863
|
+
themeTrigger.addEventListener("click", (e) => {
|
|
864
|
+
e.stopPropagation();
|
|
865
|
+
if (themeDropdown.hidden) openThemeDropdown(); else closeThemeDropdown();
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
// Click outside the wrap closes the dropdown.
|
|
869
|
+
document.addEventListener("click", (e) => {
|
|
870
|
+
if (themeDropdown.hidden) return;
|
|
871
|
+
if (themeWrap && themeWrap.contains(e.target)) return;
|
|
872
|
+
closeThemeDropdown();
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
// Escape closes too.
|
|
876
|
+
document.addEventListener("keydown", (e) => {
|
|
877
|
+
if (e.key === "Escape" && !themeDropdown.hidden) {
|
|
878
|
+
closeThemeDropdown();
|
|
879
|
+
themeTrigger.focus();
|
|
880
|
+
}
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/* — Shuffle button + R shortcut ——————————————————————————— */
|
|
885
|
+
const shuffleBtn = document.querySelector(".banner__shuffle, .banner__random");
|
|
886
|
+
function pickRandomTheme() {
|
|
887
|
+
const keys = Object.keys(THEMES).filter((k) => k !== root.dataset.theme);
|
|
888
|
+
return keys[Math.floor(Math.random() * keys.length)];
|
|
889
|
+
}
|
|
890
|
+
if (shuffleBtn) {
|
|
891
|
+
shuffleBtn.addEventListener("click", () => applyTheme(pickRandomTheme()));
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/* — T-key onboarding tooltip ————————————————————————————————
|
|
895
|
+
First-time visitors don't know T cycles themes. After ~5s of no T
|
|
896
|
+
presses (and only if they haven't seen the tooltip before), fade
|
|
897
|
+
it in near the shuffle button. Dismisses on first T press, on
|
|
898
|
+
click, or after 8s of being shown. localStorage flag is set on
|
|
899
|
+
dismiss so it never returns. */
|
|
900
|
+
const T_TOOLTIP_KEY = "hallmark-t-tooltip-seen";
|
|
901
|
+
const T_TOOLTIP_DELAY_MS = 5000;
|
|
902
|
+
const T_TOOLTIP_AUTO_HIDE_MS = 8000;
|
|
903
|
+
const T_TOOLTIP_FADE_MS = 240;
|
|
904
|
+
const tTooltipEl = document.querySelector("[data-t-tooltip]");
|
|
905
|
+
let tTooltipShown = false;
|
|
906
|
+
let tTooltipTimer = null;
|
|
907
|
+
let tTooltipAutoHideTimer = null;
|
|
908
|
+
|
|
909
|
+
function tTooltipSeen() {
|
|
910
|
+
try { return localStorage.getItem(T_TOOLTIP_KEY) === "1"; } catch (e) { return false; }
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function markTTooltipSeen() {
|
|
914
|
+
try { localStorage.setItem(T_TOOLTIP_KEY, "1"); } catch (e) { }
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function showTTooltip() {
|
|
918
|
+
if (!tTooltipEl || tTooltipShown || tTooltipSeen()) return;
|
|
919
|
+
tTooltipShown = true;
|
|
920
|
+
tTooltipEl.hidden = false;
|
|
921
|
+
delete tTooltipEl.dataset.state;
|
|
922
|
+
clearTimeout(tTooltipAutoHideTimer);
|
|
923
|
+
tTooltipAutoHideTimer = setTimeout(hideTTooltip, T_TOOLTIP_AUTO_HIDE_MS);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
function hideTTooltip() {
|
|
927
|
+
if (!tTooltipEl || !tTooltipShown) return;
|
|
928
|
+
clearTimeout(tTooltipAutoHideTimer);
|
|
929
|
+
tTooltipEl.dataset.state = "closing";
|
|
930
|
+
setTimeout(() => {
|
|
931
|
+
tTooltipEl.hidden = true;
|
|
932
|
+
delete tTooltipEl.dataset.state;
|
|
933
|
+
tTooltipShown = false;
|
|
934
|
+
markTTooltipSeen();
|
|
935
|
+
}, T_TOOLTIP_FADE_MS);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
if (tTooltipEl && !tTooltipSeen()) {
|
|
939
|
+
tTooltipTimer = setTimeout(showTTooltip, T_TOOLTIP_DELAY_MS);
|
|
940
|
+
tTooltipEl.addEventListener("click", hideTTooltip);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/* — Easter egg — "chill, designer." ————————————————————————
|
|
944
|
+
Spam T fast enough and the page intervenes. We track timestamps in
|
|
945
|
+
a rolling 3.2s window; if the user crosses the threshold (≈ a full
|
|
946
|
+
catalog cycle inside the window), the overlay takes over. While
|
|
947
|
+
it's visible, every keystroke is swallowed — no theme cycling, no
|
|
948
|
+
shortcuts. After ~4s the overlay fades and the page returns. A 30s
|
|
949
|
+
cooldown stops it from firing back-to-back. */
|
|
950
|
+
const easterEl = document.querySelector("[data-easter-egg]");
|
|
951
|
+
const EASTER_WINDOW_MS = 3200;
|
|
952
|
+
const EASTER_THRESHOLD = 12; // ≈ 3.8 presses/sec
|
|
953
|
+
const EASTER_VISIBLE_MS = 3400; // total time the overlay stays up
|
|
954
|
+
const EASTER_FADE_MS = 360; // matches the fade-out animation
|
|
955
|
+
const EASTER_COOLDOWN_MS = 15000;
|
|
956
|
+
const EASTER_PUNCHLINES = [
|
|
957
|
+
"theme connoisseur.",
|
|
958
|
+
"easy on the keys.",
|
|
959
|
+
"speed-run noted.",
|
|
960
|
+
"calm down, designer.",
|
|
961
|
+
"showing off, are we?",
|
|
962
|
+
"you've seen them all.",
|
|
963
|
+
"pick one. ship.",
|
|
964
|
+
"one theme will do.",
|
|
965
|
+
];
|
|
966
|
+
const tStamps = [];
|
|
967
|
+
let easterLastFired = 0;
|
|
968
|
+
let easterOpen = false;
|
|
969
|
+
let easterDismissTimer = null;
|
|
970
|
+
let easterFadeTimer = null;
|
|
971
|
+
|
|
972
|
+
function showEasterEgg() {
|
|
973
|
+
if (!easterEl || easterOpen) return;
|
|
974
|
+
|
|
975
|
+
// Re-mount the lines so the staggered fade-in animation re-runs each
|
|
976
|
+
// time. Populate the punchline after replacement so we don't write
|
|
977
|
+
// into a detached node.
|
|
978
|
+
const lines = easterEl.querySelector(".easter__lines");
|
|
979
|
+
if (lines) {
|
|
980
|
+
const fresh = lines.cloneNode(true);
|
|
981
|
+
lines.parentNode.replaceChild(fresh, lines);
|
|
982
|
+
}
|
|
983
|
+
const lineEl = easterEl.querySelector("[data-easter-line]");
|
|
984
|
+
if (lineEl) lineEl.textContent = EASTER_PUNCHLINES[Math.floor(Math.random() * EASTER_PUNCHLINES.length)];
|
|
985
|
+
|
|
986
|
+
delete easterEl.dataset.state;
|
|
987
|
+
easterEl.hidden = false;
|
|
988
|
+
easterOpen = true;
|
|
989
|
+
document.body.style.overflow = "hidden";
|
|
990
|
+
// Toggle body class so the page-shrink + blur animation runs in sync
|
|
991
|
+
// with the easter overlay's arrival. CSS handles the rest.
|
|
992
|
+
document.body.classList.add("easter-open");
|
|
993
|
+
|
|
994
|
+
clearTimeout(easterDismissTimer);
|
|
995
|
+
clearTimeout(easterFadeTimer);
|
|
996
|
+
easterDismissTimer = setTimeout(hideEasterEgg, EASTER_VISIBLE_MS);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
function hideEasterEgg() {
|
|
1000
|
+
if (!easterEl || !easterOpen) return;
|
|
1001
|
+
clearTimeout(easterDismissTimer);
|
|
1002
|
+
clearTimeout(easterFadeTimer);
|
|
1003
|
+
easterEl.dataset.state = "closing";
|
|
1004
|
+
// Drop the body class first so the page un-blurs / scales back in
|
|
1005
|
+
// tandem with the overlay leaving.
|
|
1006
|
+
document.body.classList.remove("easter-open");
|
|
1007
|
+
easterFadeTimer = setTimeout(() => {
|
|
1008
|
+
easterEl.hidden = true;
|
|
1009
|
+
delete easterEl.dataset.state;
|
|
1010
|
+
easterOpen = false;
|
|
1011
|
+
document.body.style.overflow = "";
|
|
1012
|
+
}, EASTER_FADE_MS);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/* — Keyboard shortcuts ————————————————————————————————— */
|
|
1016
|
+
// T cycles forward, Shift+T cycles back, R picks random.
|
|
1017
|
+
document.addEventListener("keydown", (e) => {
|
|
1018
|
+
// While the easter egg is up, swallow every key — including T —
|
|
1019
|
+
// so the user can't trigger anything until it auto-dismisses.
|
|
1020
|
+
if (easterOpen) {
|
|
1021
|
+
e.preventDefault();
|
|
1022
|
+
e.stopPropagation();
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
const tag = (e.target.tagName || "").toLowerCase();
|
|
1027
|
+
if (tag === "input" || tag === "textarea" || e.target.isContentEditable) return;
|
|
1028
|
+
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
|
1029
|
+
|
|
1030
|
+
if (e.key === "t" || e.key === "T") {
|
|
1031
|
+
e.preventDefault();
|
|
1032
|
+
// Dismiss the onboarding tooltip on first T press.
|
|
1033
|
+
clearTimeout(tTooltipTimer);
|
|
1034
|
+
if (tTooltipShown) hideTTooltip();
|
|
1035
|
+
else markTTooltipSeen();
|
|
1036
|
+
// Easter-egg counter — track press cadence in a rolling window.
|
|
1037
|
+
// Push BEFORE applying the theme so the trigger fires on this same
|
|
1038
|
+
// keystroke if we've crossed the threshold.
|
|
1039
|
+
const now = performance.now();
|
|
1040
|
+
while (tStamps.length && now - tStamps[0] > EASTER_WINDOW_MS) tStamps.shift();
|
|
1041
|
+
tStamps.push(now);
|
|
1042
|
+
if (tStamps.length >= EASTER_THRESHOLD && now - easterLastFired > EASTER_COOLDOWN_MS) {
|
|
1043
|
+
easterLastFired = now;
|
|
1044
|
+
tStamps.length = 0;
|
|
1045
|
+
showEasterEgg();
|
|
1046
|
+
return; // skip the theme swap on the trigger keystroke
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
const order = Object.keys(THEMES);
|
|
1050
|
+
const i = order.indexOf(root.dataset.theme);
|
|
1051
|
+
const dir = e.shiftKey ? -1 : 1;
|
|
1052
|
+
const next = order[(i + dir + order.length) % order.length];
|
|
1053
|
+
applyTheme(next);
|
|
1054
|
+
} else if (e.key === "r" || e.key === "R") {
|
|
1055
|
+
e.preventDefault();
|
|
1056
|
+
applyTheme(pickRandomTheme());
|
|
1057
|
+
}
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
/* — Hover preview on dots — reads theme name in the centre ————— */
|
|
1061
|
+
let previewTimer = null;
|
|
1062
|
+
let lastConfirmed = root.dataset.theme;
|
|
1063
|
+
dots.forEach((btn) => {
|
|
1064
|
+
btn.addEventListener("mouseenter", () => {
|
|
1065
|
+
lastConfirmed = root.dataset.theme;
|
|
1066
|
+
clearTimeout(previewTimer);
|
|
1067
|
+
previewTimer = setTimeout(() => {
|
|
1068
|
+
if (currentLabel) currentLabel.textContent = THEMES[btn.dataset.themeBtn];
|
|
1069
|
+
}, 80);
|
|
1070
|
+
});
|
|
1071
|
+
btn.addEventListener("mouseleave", () => {
|
|
1072
|
+
clearTimeout(previewTimer);
|
|
1073
|
+
if (currentLabel && root.dataset.theme === lastConfirmed) {
|
|
1074
|
+
currentLabel.textContent = THEMES[lastConfirmed];
|
|
1075
|
+
}
|
|
1076
|
+
});
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1079
|
+
/* — Foundations · F/04 Motion demo —————————————————————
|
|
1080
|
+
Click "Play" to run the entrance once. The keyframe is in
|
|
1081
|
+
components.css; we just toggle the class so it can replay. */
|
|
1082
|
+
const motionBtn = document.querySelector("[data-motion-demo]");
|
|
1083
|
+
const motionBlock = document.querySelector("[data-motion-block]");
|
|
1084
|
+
if (motionBtn && motionBlock) {
|
|
1085
|
+
motionBtn.addEventListener("click", () => {
|
|
1086
|
+
motionBlock.classList.remove("is-running");
|
|
1087
|
+
// force reflow so the class re-add triggers the animation again
|
|
1088
|
+
void motionBlock.offsetWidth;
|
|
1089
|
+
motionBlock.classList.add("is-running");
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
/* — Foundations · F/05 States demo ———————————————————————
|
|
1094
|
+
Real button. The readout reflects whatever state the button is in:
|
|
1095
|
+
default, hover, focus, active, loading (for ~1s after click). */
|
|
1096
|
+
const statesBtn = document.querySelector("[data-states-demo]");
|
|
1097
|
+
const statesReadout = document.querySelector("[data-states-readout]");
|
|
1098
|
+
if (statesBtn && statesReadout) {
|
|
1099
|
+
let loadingTimer = null;
|
|
1100
|
+
|
|
1101
|
+
function setState(state) {
|
|
1102
|
+
statesReadout.textContent = state;
|
|
1103
|
+
if (state === "loading") {
|
|
1104
|
+
statesBtn.dataset.state = "loading";
|
|
1105
|
+
} else {
|
|
1106
|
+
delete statesBtn.dataset.state;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
statesBtn.addEventListener("mouseenter", () => {
|
|
1111
|
+
if (statesBtn.dataset.state !== "loading") setState("hover");
|
|
1112
|
+
});
|
|
1113
|
+
statesBtn.addEventListener("mouseleave", () => {
|
|
1114
|
+
if (statesBtn.dataset.state !== "loading" && document.activeElement !== statesBtn) {
|
|
1115
|
+
setState("default");
|
|
1116
|
+
}
|
|
1117
|
+
});
|
|
1118
|
+
statesBtn.addEventListener("focus", () => {
|
|
1119
|
+
if (statesBtn.dataset.state !== "loading") setState("focus");
|
|
1120
|
+
});
|
|
1121
|
+
statesBtn.addEventListener("blur", () => {
|
|
1122
|
+
if (statesBtn.dataset.state !== "loading") setState("default");
|
|
1123
|
+
});
|
|
1124
|
+
statesBtn.addEventListener("mousedown", () => {
|
|
1125
|
+
if (statesBtn.dataset.state !== "loading") setState("active");
|
|
1126
|
+
});
|
|
1127
|
+
statesBtn.addEventListener("click", () => {
|
|
1128
|
+
setState("loading");
|
|
1129
|
+
clearTimeout(loadingTimer);
|
|
1130
|
+
loadingTimer = setTimeout(() => {
|
|
1131
|
+
setState("success");
|
|
1132
|
+
setTimeout(() => setState("default"), 900);
|
|
1133
|
+
}, 900);
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
/* — Tab-click scroll-jump fix —————————————————————————————
|
|
1138
|
+
The CSS-only radio tab pattern in Section 04 (Without/With) and
|
|
1139
|
+
Section 05 (Foundations) places the radio inputs at top:0 of their
|
|
1140
|
+
section. When the user clicks a label, the browser focuses the
|
|
1141
|
+
associated input — which scrolls the section's top into view, even
|
|
1142
|
+
if the user clicked from the middle of the page. The result is the
|
|
1143
|
+
page jumping upward on every tab click.
|
|
1144
|
+
|
|
1145
|
+
Fix: intercept label clicks, prevent the default chain, manually
|
|
1146
|
+
toggle the radio's checked state, and focus with preventScroll so
|
|
1147
|
+
keyboard navigation still works without the unwanted scroll. */
|
|
1148
|
+
const tabLabels = document.querySelectorAll(
|
|
1149
|
+
".vs-toggle__btn, .found-nav__btn"
|
|
1150
|
+
);
|
|
1151
|
+
tabLabels.forEach((label) => {
|
|
1152
|
+
label.addEventListener("click", (e) => {
|
|
1153
|
+
const id = label.getAttribute("for");
|
|
1154
|
+
if (!id) return;
|
|
1155
|
+
const radio = document.getElementById(id);
|
|
1156
|
+
if (!radio) return;
|
|
1157
|
+
|
|
1158
|
+
e.preventDefault();
|
|
1159
|
+
if (!radio.checked) {
|
|
1160
|
+
radio.checked = true;
|
|
1161
|
+
radio.dispatchEvent(new Event("change", { bubbles: true }));
|
|
1162
|
+
}
|
|
1163
|
+
// Keep keyboard nav working — focus the input but don't scroll.
|
|
1164
|
+
try {
|
|
1165
|
+
radio.focus({ preventScroll: true });
|
|
1166
|
+
} catch (_) {
|
|
1167
|
+
// Older browsers without preventScroll option — fall back to
|
|
1168
|
+
// saving and restoring scroll position.
|
|
1169
|
+
const x = window.scrollX;
|
|
1170
|
+
const y = window.scrollY;
|
|
1171
|
+
radio.focus();
|
|
1172
|
+
window.scrollTo(x, y);
|
|
1173
|
+
}
|
|
1174
|
+
});
|
|
1175
|
+
});
|