diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6bfc707 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,30 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + test: + name: Node ${{ matrix.node }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node: [20, 22, 24] + + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-node@v6 + with: + node-version: ${{ matrix.node }} + - run: npm test + - run: npm run check diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a366f42 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,32 @@ +# Changelog + +All notable changes to this project are documented here. +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). + +## [2.0.0] - 2026-07-12 + +### Added + +- A two-layer component API that keeps the edge and ground shadow fixed while the cap moves. +- JaydenART green and brown variants with documented CSS custom properties. +- Keyboard focus, disabled, reduced-motion, forced-colors, and touch interaction states. +- A responsive, dependency-free live demo with a strict Content Security Policy. +- Deterministic contract tests and CI on Node.js 20, 22, and 24. +- Contributing, security, and release documentation. + +### Changed + +- The live demo uses semantic HTML, local stylesheets, responsive layout, and stable press geometry. +- The README documents the component structure, customization API, accessibility behavior, and development checks. + +### Fixed + +- Pressing the modern component no longer moves the base or changes layout dimensions. +- Keyboard focus is visible instead of being removed. +- The demo no longer depends on an external background image or inline JavaScript. + +### Compatibility + +- The original `.JayBrown` single-element class remains available for existing markup. + +[2.0.0]: https://github.com/JaydenART/JaydenART_Realistic_3D_CSS_Button/releases/tag/v2.0.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..64a4c88 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,14 @@ +# Contributing + +Focused improvements are welcome. + +## Before Opening a Pull Request + +1. Keep `JayButton.css` dependency-free. +2. Preserve native button semantics and keyboard behavior. +3. Avoid layout shifts in hover, focus, active, and disabled states. +4. Keep reduced-motion and forced-colors behavior working. +5. Run `npm test` and `npm run check`. +6. Update the README and changelog when public behavior changes. + +Please explain the problem, the implementation choice, and how you verified the result. Screenshots are useful for rendering changes, but they should accompany a reproducible description. diff --git a/JayButton.css b/JayButton.css index f197b06..176a0ad 100644 --- a/JayButton.css +++ b/JayButton.css @@ -1,179 +1,176 @@ -/* -- Body Background -- */ -body { - margin: 0; - height: 100vh; - background-image: radial-gradient(ellipse at center, rgba(0,0,0,0) 0%, rgba(19,19,19,0.35) 100%), url("https://www.JaydenYoon.com/images/JaydenYoonZK_Brownish_White_Background_Small.jpg"); -} +/* JaydenART Realistic 3D CSS Button v2.0.0 */ +.jay-button { + --jay-cap: #abcf37; + --jay-cap-hover: #b9dc49; + --jay-edge: #718a23; + --jay-ink: #10110d; + --jay-focus: #ffffff; + --jay-travel: 5px; + --jay-radius: 14px; -/* -- Center Everything -- */ -.Center { - margin: 0; + position: relative; + isolation: isolate; + display: inline-grid; + min-inline-size: 10rem; + padding: 0 0 8px; + border: 0; + border-radius: var(--jay-radius); + color: var(--jay-ink); + background: transparent; + font: 700 1rem/1 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + cursor: pointer; + touch-action: manipulation; + -webkit-tap-highlight-color: transparent; + appearance: none; +} + +.jay-button::before { + content: ""; position: absolute; - top: 50%; - left: 50%; - -ms-transform: translate(-50%, -50%); - transform: translate(-50%, -50%); + z-index: -1; + inset: 5px 0 3px; + border-radius: var(--jay-radius); + background: var(--jay-edge); + box-shadow: inset 0 -1px 0 rgb(0 0 0 / 0.32); } +.jay-button::after { + content: ""; + position: absolute; + z-index: -2; + inset: auto 9% -5px; + height: 16px; + border-radius: 50%; + background: rgb(0 0 0 / 0.34); + filter: blur(7px); +} -/* -- Center Button -- */ -.CenterButton { - display: flex; - align-items: center; - justify-content: center; +.jay-button__cap { + position: relative; + display: block; + padding: 1rem 1.55rem; + border: 1px solid rgb(0 0 0 / 0.2); + border-radius: var(--jay-radius); + background: var(--jay-cap); + box-shadow: + inset 0 1px 0 rgb(255 255 255 / 0.5), + inset 0 -2px 0 rgb(0 0 0 / 0.13), + 0 2px 3px rgb(0 0 0 / 0.16); + transform: translateY(0); + transition: transform 130ms ease, background-color 130ms ease, box-shadow 130ms ease; } -/* -- JaydenART Button : Normal -- */ -.JayBrown { - display: inline-block; - -webkit-box-sizing: content-box; - -moz-box-sizing: content-box; - box-sizing: content-box; - margin: 0 0 15px; - padding: 8px 20px 10px; - border: 1px solid rgba(127,70,34,1); - -webkit-border-radius: 10px; - border-radius: 10px; - font: normal normal bold 14px/1 "Coda", Helvetica, sans-serif; - color: rgb(255, 255, 255); - text-align: center; - -o-text-overflow: clip; - text-overflow: clip; - background: -webkit-linear-gradient(90deg, rgba(255,255,255,0) 0, rgba(255,255,255,0) 1%, rgba(206,133,88,0.99) 100%), rgba(114,71,42,1); - background: -moz-linear-gradient(0deg, rgba(255,255,255,0) 0, rgba(255,255,255,0) 1%, rgba(206,133,88,0.99) 100%), rgba(114,71,42,1); - background: linear-gradient(0deg, rgba(255,255,255,0) 0, rgba(255,255,255,0) 1%, rgba(206,133,88,0.99) 100%), rgba(114,71,42,1); - background-position: 50% 50%; - -webkit-background-origin: padding-box; - background-origin: padding-box; - -webkit-background-clip: border-box; - background-clip: border-box; - -webkit-background-size: auto auto; - background-size: auto auto; - -webkit-box-shadow: 0 0 5px 1px rgba(239,159,105,0.6) inset, 0 1.5px 0 0 rgba(229,151,98,1) inset, 0 6px 0 0 rgba(89,50,24,1) , 0 8px 3px 1px rgba(0,0,0,0.99); - box-shadow: 0 0 5px 1px rgba(239,159,105,0.6) inset, 0 1.5px 0 0 rgba(229,151,98,1) inset, 0 6px 0 0 rgba(89,50,24,1) , 0 8px 3px 1px rgba(0,0,0,0.99); - text-shadow: 0 -1px 1px rgba(45,22,7,1); - outline: none; -} - - -/* -- JaydenART Button : Hover -- */ -.JayBrown:hover { - cursor: pointer; - background: -webkit-linear-gradient(-90deg, rgba(255,255,255,0) 0, rgba(211,140,99,0.7) 100%), rgba(137,89,57,1); - background: -moz-linear-gradient(180deg, rgba(255,255,255,0) 0, rgba(211,140,99,0.7) 100%), rgba(137,89,57,1); - background: linear-gradient(180deg, rgba(255,255,255,0) 0, rgba(211,140,99,0.7) 100%), rgba(137,89,57,1); - background-position: 50% 50%; - -webkit-background-origin: padding-box; - background-origin: padding-box; - -webkit-background-clip: border-box; - background-clip: border-box; - -webkit-background-size: auto auto; - background-size: auto auto; - -webkit-box-shadow: 0 0 5px 1px rgba(239,159,105,0.6) inset, 0 1.5px 0 0 rgba(234,161,112,0.96) inset, 0 6px 0 0 rgba(89,50,24,1) , 0 8px 3px 1px rgba(0,0,0,0.99); - box-shadow: 0 0 5px 1px rgba(239,159,105,0.6) inset, 0 1.5px 0 0 rgba(234,161,112,0.96) inset, 0 6px 0 0 rgba(89,50,24,1) , 0 8px 3px 1px rgba(0,0,0,0.99); - text-shadow: 0 -1px 1px rgb(30,45,77); - outline: none; -} - - -/* -- JaydenART Button : Active -- */ -.JayBrown:active { - margin: 5px 0 10px; - background: -webkit-linear-gradient(90deg, rgba(255,255,255,0) 0, rgba(255,255,255,0) 1%, rgba(206,133,88,0.99) 100%), rgba(114,71,42,1); - background: -moz-linear-gradient(0deg, rgba(255,255,255,0) 0, rgba(255,255,255,0) 1%, rgba(206,133,88,0.99) 100%), rgba(114,71,42,1); - background: linear-gradient(0deg, rgba(255,255,255,0) 0, rgba(255,255,255,0) 1%, rgba(206,133,88,0.99) 100%), rgba(114,71,42,1); - background-position: 50% 50%; - -webkit-background-origin: padding-box; - background-origin: padding-box; - -webkit-background-clip: border-box; - background-clip: border-box; - -webkit-background-size: auto auto; - background-size: auto auto; - -webkit-box-shadow: 0 1px 10px 1px rgba(239,159,105,0.6) inset, 0 1.5px 0 0 rgba(234,171,129,1) inset, 0 2px 0 0 rgba(89,50,24,1) , 0 3px 3px 1px rgba(0,0,0,0.99); - box-shadow: 0 1px 10px 1px rgba(239,159,105,0.6) inset, 0 1.5px 0 0 rgba(234,171,129,1) inset, 0 2px 0 0 rgba(89,50,24,1) , 0 3px 3px 1px rgba(0,0,0,0.99); - outline: none; +@media (hover: hover) and (pointer: fine) { + .jay-button:hover .jay-button__cap { + background: var(--jay-cap-hover); + transform: translateY(-1px); + box-shadow: + inset 0 1px 0 rgb(255 255 255 / 0.58), + inset 0 -2px 0 rgb(0 0 0 / 0.13), + 0 3px 5px rgb(0 0 0 / 0.18); + } } +.jay-button:active .jay-button__cap { + transform: translateY(var(--jay-travel)); + box-shadow: + inset 0 2px 3px rgb(0 0 0 / 0.16), + inset 0 -1px 0 rgb(255 255 255 / 0.28); +} -/* -- Hidden Line Break -- */ -span { - white-space: pre; +.jay-button:focus-visible { + outline: 3px solid var(--jay-focus); + outline-offset: 5px; } -span:after { - content: ' '; +.jay-button:disabled { + cursor: not-allowed; + opacity: 0.48; } -span.line-break { - display: block; +.jay-button:disabled .jay-button__cap { + transform: none; } -span.line-break:after { - content: none; +.jay-button[data-tone="brown"] { + --jay-cap: #6c5b54; + --jay-cap-hover: #7b6961; + --jay-edge: #382f2b; + --jay-ink: #ffffff; } +/* Backward-compatible single-element class from the original release. */ +.JayBrown { + display: inline-block; + min-inline-size: 10rem; + padding: 1rem 1.55rem; + border: 1px solid #3b312d; + border-radius: 14px; + color: #ffffff; + background: #6c5b54; + box-shadow: + inset 0 1px 0 rgb(255 255 255 / 0.22), + inset 0 -2px 0 rgb(0 0 0 / 0.2), + 0 6px 0 #382f2b, + 0 11px 14px rgb(0 0 0 / 0.28); + font: 700 1rem/1 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + cursor: pointer; + transform: translateY(0); + transition: transform 130ms ease, background-color 130ms ease, box-shadow 130ms ease; + touch-action: manipulation; + -webkit-tap-highlight-color: transparent; + appearance: none; +} -/* -- Copyright Texts -- */ -.Copyright a { - text-decoration: none; - color: #C79E5B; +@media (hover: hover) and (pointer: fine) { + .JayBrown:hover { + background: #7b6961; + transform: translateY(-1px); + } } +.JayBrown:active { + transform: translateY(4px); + box-shadow: + inset 0 2px 3px rgb(0 0 0 / 0.18), + 0 2px 0 #382f2b, + 0 6px 9px rgb(0 0 0 / 0.26); +} -/* -- Superscript Registered Trademark Symbol -- */ -sup { - position: relative; - font-size: 40%; - line-height: 0; - vertical-align: baseline; - top: -1.1em; - left: 0.3em; -} - - -/* -- Center Copyright Texts & Word Breaks -- */ -.CenterCopyright { - position: fixed; - left: 0; - bottom: 0; - padding-top: 10px; - padding-bottom: 10px; - width: 100%; - - text-align: center; - align-items: center; - justify-content: center; - text-align: center; - - color: #e0e0e0; - font: bold 15px helvetica; - text-shadow: 0px -1.5px 0px rgba(0, 0, 0, 1),0px 1px 1.5px rgba(255, 255, 255, 0.25); - - background-color: #333742; - -webkit-box-shadow: 0px 10px 3px 11px rgba(0,0,0,0.6); - box-shadow: 0px 10px 3px 11px rgba(0,0,0,0.6); - -/* Word Break */ - word-wrap: break-word; - word-break: break-word; - overflow-wrap: break-word; - -ms-word-break: break-word; - -/* None Standard For Webkit */ - -webkit-hyphens: none; - moz-hyphens: none; - -ms-hyphens: none; - hyphens: none; -} - - -/* -- All Rights Reserved Same Line -- */ -.AllRightsReserved { - display: inline-block; +.JayBrown:focus-visible { + outline: 3px solid #abcf37; + outline-offset: 5px; } +@media (prefers-reduced-motion: reduce) { + .jay-button__cap, + .JayBrown { + transition-duration: 0.01ms; + } + + .jay-button:hover .jay-button__cap, + .jay-button:active .jay-button__cap, + .JayBrown:hover, + .JayBrown:active { + transform: none; + } +} -/* -- Text Highlight + Shadow Removed -- */ -::-moz-selection { - +@media (forced-colors: active) { + .jay-button, + .JayBrown { + forced-color-adjust: none; + } + + .jay-button__cap, + .JayBrown { + border-color: ButtonText; + color: ButtonText; + background: ButtonFace; + } + + .jay-button::before { + background: ButtonText; + } +} diff --git a/README.md b/README.md index 0c9612e..9af8908 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,100 @@ -# JaydenART® - Realistic 3D CSS Button -A button that actually presses. 100% CSS, no JavaScript, no images. +# JaydenART® Realistic 3D CSS Button -The JaydenART 3D button, resting on its edge with a soft ground shadow +

A button that actually presses. Pure CSS, no JavaScript, no images, and no dependencies.

-$~$ + + JaydenART realistic CSS button shown at rest and pressed, with the cap moving down while the base stays anchored + -[![View live](https://img.shields.io/badge/JaydenART%20Button-View%20Live-95613f)](https://jaydenart.github.io/JaydenART_Realistic_3D_CSS_Button) -[![MIT license](https://img.shields.io/badge/license-MIT-97ca00)](LICENSE) +

+ Open the live demo + CI status + MIT License +

-## Why it feels real +## Why It Feels Real -Most "3D" CSS buttons move their shadow together with the cap, so the whole button appears to float. This one is anchored: +Many CSS buttons move the whole element, including the shadow that is supposed to anchor it to the page. This component separates the moving cap from the fixed edge and ground shadow: -- The cap rests on a hard edge, with a soft ambient shadow on the ground beneath it. -- On hover, the cap lifts one step while the edge and ground stay planted. -- On press, the cap travels down onto the edge. The base never moves, so your eye reads it as a real key being pushed, not a div being nudged. +- The outer button owns the edge and shadow. +- The inner cap lifts on hover and travels down on press. +- The component keeps stable dimensions through every state. +- Touch, keyboard, reduced-motion, disabled, and forced-color states are included. -## Use it +## Use It -Copy [`JayButton.css`](JayButton.css) into your project and give any button the class: +Add [`JayButton.css`](JayButton.css) to your project: ```html - + ``` -Colors, sizes, and travel distances are plain CSS values near the top of the file. Tune them freely. +For the brown JaydenART variant: -## Seen in the wild +```html + +``` + +## Customize It + +Override the component variables on the button or in your own class: + +```css +.my-button { + --jay-cap: #abcf37; + --jay-cap-hover: #b9dc49; + --jay-edge: #718a23; + --jay-ink: #10110d; + --jay-focus: #544741; + --jay-travel: 5px; + --jay-radius: 14px; +} +``` + +| Variable | Purpose | +|---|---| +| `--jay-cap` | Resting cap color | +| `--jay-cap-hover` | Hover cap color | +| `--jay-edge` | Fixed edge color | +| `--jay-ink` | Label color | +| `--jay-focus` | Keyboard focus ring | +| `--jay-travel` | Press distance | +| `--jay-radius` | Cap and edge corner radius | + +## Accessibility + +The demo uses native ` +``` + +New work should use `.jay-button` with `.jay-button__cap` because that structure keeps the cap and base physically independent. + +## Development + +```bash +npm test +npm run check +npm run serve +``` + +The test suite checks the public HTML and CSS contracts, accessibility states, local assets, security policy, and documentation hygiene. + +## Contributing -The same anchored physics now drives every button across [Jayden's free browser tools](https://jaydenyoonzk.github.io/projects/). +Focused bug reports and pull requests are welcome. Read [CONTRIBUTING.md](CONTRIBUTING.md) before changing public behavior. Report security issues privately using [SECURITY.md](SECURITY.md). ## License -[MIT](LICENSE). Built by [JaydenART®](https://www.JaydenART.com). +[MIT](LICENSE). Built and maintained by [JaydenART® Inc.](https://www.JaydenART.com). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..86624f9 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,5 @@ +# Security Policy + +Report vulnerabilities privately through [GitHub Security Advisories](https://github.com/JaydenART/JaydenART_Realistic_3D_CSS_Button/security/advisories/new). + +This project is a static CSS and HTML demo. It does not collect data, make API requests, or require credentials. Please include the affected file, reproduction steps, impact, and a minimal proof of concept when relevant. diff --git a/assets/button-preview.svg b/assets/button-preview.svg new file mode 100644 index 0000000..8ccbfa9 --- /dev/null +++ b/assets/button-preview.svg @@ -0,0 +1,30 @@ + + JaydenART realistic 3D CSS button + Two views show the chartreuse button resting above its fixed edge and pressed down onto the same edge. + + + JAYDENART® UI STUDY + A button that actually presses. + Pure CSS. Stable geometry. Keyboard and reduced-motion support. + + + RESTING + + + + + Press me + + + + PRESSED + + + + + Press me + + + + + diff --git a/demo.css b/demo.css new file mode 100644 index 0000000..8c929fe --- /dev/null +++ b/demo.css @@ -0,0 +1,192 @@ +:root { + color-scheme: light dark; + --page: #f5f6f1; + --surface: #ffffff; + --ink: #121310; + --muted: #65685e; + --line: #d9dbd1; + --brand: #abcf37; + --brown: #544741; +} + +* { + box-sizing: border-box; +} + +html { + min-width: 320px; + background: var(--page); +} + +body { + min-height: 100vh; + margin: 0; + color: var(--ink); + background: var(--page); + font: 400 1rem/1.55 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + letter-spacing: 0; +} + +a { + color: inherit; + text-underline-offset: 0.22em; +} + +a:hover { + color: #536a10; +} + +.site-header { + display: flex; + align-items: center; + justify-content: space-between; + width: min(100% - 40px, 1080px); + min-height: 72px; + margin-inline: auto; + border-bottom: 1px solid var(--line); +} + +.brand { + font-weight: 800; + text-decoration: none; +} + +.site-nav { + display: flex; + gap: 22px; + color: var(--muted); + font-size: 0.92rem; +} + +.site-nav a { + text-decoration: none; +} + +main { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(320px, 0.85fr); + align-items: center; + gap: clamp(48px, 8vw, 112px); + width: min(100% - 40px, 1080px); + min-height: calc(100vh - 144px); + margin-inline: auto; + padding-block: 64px; +} + +.eyebrow { + margin: 0 0 16px; + color: #607a12; + font-size: 0.8rem; + font-weight: 800; + text-transform: uppercase; +} + +h1 { + max-width: 720px; + margin: 0; + font-size: 5.4rem; + line-height: 0.98; + letter-spacing: 0; +} + +.lede { + max-width: 620px; + margin: 26px 0 0; + color: var(--muted); + font-size: 1.08rem; +} + +.button-stage { + display: grid; + place-items: center; + min-height: 390px; + padding: 56px 32px; + border: 1px solid var(--line); + border-radius: 8px; + background: var(--surface); + box-shadow: 0 24px 70px rgb(42 44 34 / 0.12); +} + +.button-set { + display: grid; + gap: 38px; + justify-items: center; +} + +.variant-label { + margin: 0 0 12px; + color: var(--muted); + font-size: 0.78rem; + font-weight: 700; + text-align: center; + text-transform: uppercase; +} + +footer { + display: flex; + justify-content: space-between; + width: min(100% - 40px, 1080px); + min-height: 72px; + margin-inline: auto; + padding-block: 22px; + border-top: 1px solid var(--line); + color: var(--muted); + font-size: 0.88rem; +} + +footer p { + margin: 0; +} + +@media (max-width: 760px) { + .site-header { + align-items: flex-start; + gap: 16px; + padding-block: 20px; + } + + .site-nav { + flex-wrap: wrap; + justify-content: flex-end; + gap: 10px 16px; + } + + main { + grid-template-columns: 1fr; + min-height: auto; + padding-block: 52px; + } + + h1 { + font-size: 3.2rem; + } + + .button-stage { + min-height: 340px; + border-radius: 8px; + } + + footer { + display: grid; + gap: 8px; + } +} + +@media (prefers-color-scheme: dark) { + :root { + --page: #0b0c09; + --surface: #151612; + --ink: #ffffff; + --muted: #b7baae; + --line: #303229; + } + + a:hover, + .eyebrow { + color: var(--brand); + } + + .button-stage { + box-shadow: 0 24px 70px rgb(0 0 0 / 0.34); + } +} diff --git a/index.html b/index.html index 4ce5c1b..f0ac023 100644 --- a/index.html +++ b/index.html @@ -1,61 +1,60 @@ - - - - - + + - - - - JaydenART - Realistic 3D CSS Button - - - - - - - - + + + + + Realistic 3D CSS Button by JaydenART + + + + + + + + + + - - - - + +
+
+

Pure CSS interface study

+

A button that actually presses.

+

The cap moves. The edge stays planted. The layout does not jump. No JavaScript, images, frameworks, or dependencies.

+
- -
- - - -
- - - -
- - - - - - - - -
- - +
+
+
+

JaydenART green

+ +
+
+

JaydenART brown

+ +
+
+
+
- - - - - + - - - - diff --git a/package.json b/package.json new file mode 100644 index 0000000..11bce06 --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "jaydenart-realistic-3d-css-button", + "version": "2.0.0", + "description": "A dependency-free CSS button with an anchored edge, stable layout, and accessible interaction states.", + "private": true, + "scripts": { + "check": "node --check test/button.test.mjs", + "test": "node --test test/button.test.mjs", + "serve": "python3 -m http.server 8430" + }, + "engines": { + "node": ">=20" + }, + "license": "MIT", + "author": "JaydenART Inc.", + "homepage": "https://jaydenart.github.io/JaydenART_Realistic_3D_CSS_Button/", + "repository": { + "type": "git", + "url": "https://github.com/JaydenART/JaydenART_Realistic_3D_CSS_Button.git" + } +} diff --git a/test/button.test.mjs b/test/button.test.mjs new file mode 100644 index 0000000..1b2d12f --- /dev/null +++ b/test/button.test.mjs @@ -0,0 +1,48 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; + +const html = readFileSync(new URL("../index.html", import.meta.url), "utf8"); +const component = readFileSync(new URL("../JayButton.css", import.meta.url), "utf8"); +const demo = readFileSync(new URL("../demo.css", import.meta.url), "utf8"); +const readme = readFileSync(new URL("../README.md", import.meta.url), "utf8"); + +test("page has a complete, secure document shell", () => { + assert.match(html, /^/i); + assert.match(html, //); + assert.match(html, /name="viewport"/); + assert.match(html, /Content-Security-Policy/); + assert.match(html, /default-src 'none'/); + assert.doesNotMatch(html, / { + assert.equal((html.match(/class="jay-button"/g) || []).length, 2); + assert.equal((html.match(/class="jay-button__cap"/g) || []).length, 2); + assert.doesNotMatch(html, /]+type="button"/i); + assert.doesNotMatch(html, /]*>\s*<(button|input)\b/is); +}); + +test("component keeps the base separate from the moving cap", () => { + assert.match(component, /\.jay-button::before/); + assert.match(component, /\.jay-button::after/); + assert.match(component, /\.jay-button:active \.jay-button__cap/); + assert.match(component, /transform: translateY\(var\(--jay-travel\)\)/); +}); + +test("component covers keyboard and user preference states", () => { + assert.match(component, /:focus-visible/); + assert.match(component, /prefers-reduced-motion: reduce/); + assert.match(component, /forced-colors: active/); + assert.match(component, /\.jay-button:disabled/); +}); + +test("component has no external asset dependency", () => { + assert.doesNotMatch(component, /https?:\/\//); + assert.doesNotMatch(component, /url\(/); +}); + +test("responsive demo and public writing stay clean", () => { + assert.match(demo, /@media \(max-width: 760px\)/); + assert.doesNotMatch([html, component, demo, readme].join("\n"), /\u2013|\u2014/u); +});