diff --git a/CHANGELOG.md b/CHANGELOG.md index bb56375..21e0333 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,19 @@ All notable changes to this package are documented in this file. ## Unreleased +## 1.3.0 - 2026-07-08 + +### Added + +- Added first-class `data-surface-variant` support for `primary`, `secondary`, `accent`, `subtle`, `warning`, and `danger` so runtime-assigned surface hooks match the existing `.variant-*` class API. +- Added standalone `data-surface-level="1"`, `"2"`, and `"3"` depth defaults with matching hover, active, and focus state-opacity hooks. +- Added Playwright coverage for data-variant parity and data-level depth defaults. + +### Changed + +- Updated README, wiki, and demo guidance to document standalone interaction hooks, UI Style Kit 2.x bridge usage, and the ecosystem boundary between layout, visual paint, and interaction states. +- Updated embedded demo docs to avoid stale version-pinned CDN examples. + ## 1.2.4 - 2026-06-22 ### Added diff --git a/README.md b/README.md index 9d32952..874f026 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,12 @@ CDN: Continue ``` +```html + + Continue + +``` + ```html ... @@ -144,6 +150,13 @@ Visual variants: - `.variant-warning` - `.variant-danger` +Data attribute aliases: + +- `data-surface-variant="primary|secondary|accent|subtle|warning|danger"` +- `data-surface-level="1|2|3"` + +Use the class API when you prefer compact standalone markup. Use the data attributes when another library or runtime assigns semantic surface hooks. + Icon pattern: - `.icon-only` @@ -156,13 +169,23 @@ Preferred token namespace: The package also supports legacy fallback tokens and semantic fallback tokens. Full details and examples are in [Token Reference](./wiki/Token-Reference.md). +## Ecosystem Integration + +The three CSS libraries have separate ownership boundaries: + +- `layout-style-css` owns structural layout recipes and page geometry. +- `ui-style-kit-css` owns visual themes, style systems, and mode-aware paint. +- `interactive-surface-css` owns interaction behavior on `.interactive-surface`. + +Use one, two, or all three packages based on the layer you need. Interactive Surface does not require either companion library. + ## Integration with UI Style Kit CSS `interactive-surface-css` and `ui-style-kit-css` are designed to work separately and complementarily. UI Style Kit owns visual theme tokens. Interactive Surface owns interaction behavior on `.interactive-surface`. -With `ui-style-kit-css@2.0.1`, the default full bundle does not include the bridge. Use the opt-in bridge bundle when you want UI Style Kit's runtime style switcher and Interactive Surface token mapping in one import: +With `ui-style-kit-css` 2.x, the default full bundle does not include the bridge. Use the opt-in bridge bundle when you want UI Style Kit's runtime style switcher and Interactive Surface token mapping in one import: ```js import "ui-style-kit-css/with-bridge.css"; @@ -185,7 +208,7 @@ import "ui-style-kit-css/styles/minimal-saas.css"; import "ui-style-kit-css/interactive-surface-bridge"; ``` -The bridge maps active `data-ui`, `data-theme`, and `data-mode` tokens to Interactive Surface's public token contract, including variant, icon-role, state-layer, and optional surface-level hooks. UI Style Kit owns the bridge token mapping; Interactive Surface keeps ownership of `.interactive-surface` interaction behavior. +The bridge maps active `data-ui`, `data-theme`, and `data-mode` tokens to Interactive Surface's public token contract, including variant, icon-role, state-layer, and optional surface-level hooks. Interactive Surface now also treats `data-surface-variant` and `data-surface-level` as standalone public hooks, so bridge-generated markup keeps useful behavior even without style-specific class names. UI Style Kit owns the bridge token mapping; Interactive Surface keeps ownership of `.interactive-surface` interaction behavior. ## Accessibility @@ -221,7 +244,7 @@ Release checklist: 2. Bump `version` in `package.json`. 3. Update `CHANGELOG.md`. 4. Push to `main`. -5. Create and publish a GitHub Release tag (for example `v1.2.4`). +5. Create and publish a GitHub Release tag (for example `v1.3.0`). 6. Verify the `Publish to npm` workflow succeeds. 7. Verify CDN availability: - `https://cdn.jsdelivr.net/npm/interactive-surface-css@/interactive-surface.css` @@ -241,4 +264,3 @@ The `prepublishOnly` guard intentionally avoids browser downloads so `npm publis `interactive-surface` should be the only transform-based motion owner on its host element. Avoid applying additional `transform`, `translate`, `scale`, or `rotate` rules to the same node. If you need extra animation, apply it to a child element. -](https://github.com/Foscat/Interactive-Surface-CSS) diff --git a/docs/superpowers/specs/2026-07-08-interactive-surface-data-hooks.md b/docs/superpowers/specs/2026-07-08-interactive-surface-data-hooks.md new file mode 100644 index 0000000..3f4c30a --- /dev/null +++ b/docs/superpowers/specs/2026-07-08-interactive-surface-data-hooks.md @@ -0,0 +1,36 @@ +# Interactive Surface Data Hooks Follow-Up + +## Summary + +This follow-up implements the `interactive-surface-css` portion of the three-library ecosystem plan. The package remains an interaction primitive, while adding data-attribute hooks that can be assigned by apps, renderers, or companion CSS libraries without requiring style-specific class names. + +## Public API + +- Existing class variants remain supported: + - `.variant-primary` + - `.variant-secondary` + - `.variant-accent` + - `.variant-subtle` + - `.variant-warning` + - `.variant-danger` +- New equivalent data variant hook: + - `data-surface-variant="primary|secondary|accent|subtle|warning|danger"` +- Existing `data-surface-level` support now has standalone defaults for: + - `data-surface-level="1"` + - `data-surface-level="2"` + - `data-surface-level="3"` + +## Ownership Boundaries + +- `layout-style-css` owns structure and layout recipes. +- `ui-style-kit-css` owns visual theme identity, color roles, and style systems. +- `interactive-surface-css` owns hover, focus, active, selected, pressed, disabled, reduced-motion, and forced-colors behavior on `.interactive-surface`. + +Interactive Surface does not define global theme tokens and does not depend on either companion package. + +## Verification Targets + +- Data variant styles match the existing class variant styles. +- Surface levels have distinct standalone depth defaults. +- UI Style Kit bridge imports can continue to provide stronger token values through the public `--interactive-surface-*` contract. +- Existing accessibility behavior remains unchanged for focus, pressed, disabled, reduced motion, high contrast, and forced colors. diff --git a/index.html b/index.html index 8fe1d9a..3a16f66 100644 --- a/index.html +++ b/index.html @@ -1164,6 +1164,9 @@ Buttons and states Primary CTA Secondary + + Data hook CTA + Selected (.is-active) Warning Danger @@ -1190,7 +1193,7 @@ Card surfaces ARIA-aware state Pressed style can come from semantic attributes instead of custom state classes. - + Content-rich cards Compose text, metadata, and icons while preserving a single interaction model. @@ -1338,9 +1341,9 @@ README and wiki links - + - + Edit token @@ -1447,11 +1450,11 @@ README and wiki links import "interactive-surface-css"; ``` -CDN: +CDN (replace `` with the desired release, e.g. `1.3.0`): ```html - - + + ``` ## Quick Start @@ -1464,6 +1467,12 @@ README and wiki links Continue ``` +```html + + Continue + +``` + ```html ... @@ -1504,6 +1513,13 @@ README and wiki links - `.variant-warning` - `.variant-danger` +Data attribute aliases: + +- `data-surface-variant="primary|secondary|accent|subtle|warning|danger"` +- `data-surface-level="1|2|3"` + +Use the class API for compact standalone markup and the data attributes when another library or runtime assigns semantic surface hooks. + Icon pattern: - `.icon-only` @@ -1548,11 +1564,11 @@ README and wiki links 2. Bump `version` in `package.json`. 3. Update `CHANGELOG.md`. 4. Push to `main`. -5. Create and publish a GitHub Release tag (for example `v1.2.0`). +5. Create and publish a GitHub Release tag (for example `v1.3.0`). 6. Verify the `Publish to npm` workflow succeeds. 7. Verify CDN availability: - - `https://cdn.jsdelivr.net/npm/interactive-surface-css@1.2.0/interactive-surface.css` - - `https://unpkg.com/interactive-surface-css@1.2.0/interactive-surface.css` + - `https://cdn.jsdelivr.net/npm/interactive-surface-css@/interactive-surface.css` + - `https://unpkg.com/interactive-surface-css@/interactive-surface.css` Manual fallback: @@ -1589,6 +1605,7 @@ README and wiki links const tokenCards = Array.from(document.querySelectorAll(".token-card[data-token]")); const tokenGrid = document.querySelector(".token-grid"); const tokenModal = document.getElementById("tokenModal"); + const tokenModalPanel = tokenModal?.querySelector(".token-modal-panel"); const tokenModalBackdrop = document.getElementById("tokenModalBackdrop"); const tokenEditor = document.getElementById("tokenEditor"); const tokenEditorTitle = document.getElementById("tokenEditorTitle"); @@ -1716,6 +1733,30 @@ README and wiki links userTokenOverrides.textContent = blocks.join("\n\n"); } + /* + * Dialog semantics are applied only while visible so hidden demo markup + * is not exposed as an active modal to assistive technology or tests. + */ + function setTokenEditorModalState(isOpen) { + if (!tokenModal || !tokenModalPanel) { + return; + } + + tokenModal.hidden = !isOpen; + tokenModal.setAttribute("aria-hidden", String(!isOpen)); + + if (isOpen) { + tokenModalPanel.setAttribute("role", "dialog"); + tokenModalPanel.setAttribute("aria-modal", "true"); + tokenModalPanel.setAttribute("aria-labelledby", "tokenEditorTitle"); + return; + } + + tokenModalPanel.removeAttribute("role"); + tokenModalPanel.removeAttribute("aria-modal"); + tokenModalPanel.removeAttribute("aria-labelledby"); + } + function openTokenEditor(tokenName) { if (!tokenEditor || !tokenEditorName || !tokenEditorValue || !tokenModal) { return; @@ -1724,7 +1765,7 @@ README and wiki links tokenEditor.dataset.token = tokenName; tokenEditorName.value = tokenName; tokenEditorValue.value = readCssVariable(tokenName); - tokenModal.hidden = false; + setTokenEditorModalState(true); if (tokenEditorTitle) { tokenEditorTitle.textContent = `Edit ${tokenName}`; @@ -1739,7 +1780,7 @@ README and wiki links return; } - tokenModal.hidden = true; + setTokenEditorModalState(false); tokenEditor.dataset.token = ""; } diff --git a/interactive-surface.css b/interactive-surface.css index dfc3a78..0133725 100644 --- a/interactive-surface.css +++ b/interactive-surface.css @@ -125,7 +125,8 @@ /* * UI Style Kit 2.x exposes surface depth through bridge level tokens. - * The attribute gate keeps default surfaces on the existing base contract. + * The attribute gates keep default surfaces on the existing base contract while + * letting bridge or standalone markup opt into semantic depth. */ .interactive-surface[data-surface-level] { --_is-bg: var(--interactive-surface-level-bg, var(--_is-base-bg)); @@ -133,6 +134,39 @@ --_is-shadow-base: var(--interactive-surface-level-shadow, var(--_is-base-shadow)); } +.interactive-surface[data-surface-level="1"] { + --_is-bg: var(--interactive-surface-level-bg, var(--interactive-surface-level-1-bg, var(--_is-base-bg))); + --_is-border-color: var(--interactive-surface-level-border-color, var(--interactive-surface-level-1-border-color, var(--_is-base-border-color))); + --_is-shadow-base: var(--interactive-surface-level-shadow, var(--interactive-surface-level-1-shadow, var(--_is-base-shadow))); + --_is-state-layer-hover-opacity: var(--interactive-surface-state-layer-hover-opacity, var(--interactive-surface-level-1-hover-opacity, 0.08)); + --_is-state-layer-active-opacity: var(--interactive-surface-state-layer-active-opacity, var(--interactive-surface-level-1-active-opacity, 0.14)); + --_is-state-layer-focus-opacity: var(--interactive-surface-state-layer-focus-opacity, var(--interactive-surface-level-1-focus-opacity, 0.18)); +} + +.interactive-surface[data-surface-level="2"] { + --_is-bg: var(--interactive-surface-level-bg, var(--interactive-surface-level-2-bg, var(--_is-base-bg))); + --_is-border-color: var(--interactive-surface-level-border-color, var(--interactive-surface-level-2-border-color, var(--_is-base-border-color))); + --_is-shadow-base: var(--interactive-surface-level-shadow, var( + --interactive-surface-level-2-shadow, + 0 8px 20px rgb(0 0 0 / 0.22) + )); + --_is-state-layer-hover-opacity: var(--interactive-surface-state-layer-hover-opacity, var(--interactive-surface-level-2-hover-opacity, 0.11)); + --_is-state-layer-active-opacity: var(--interactive-surface-state-layer-active-opacity, var(--interactive-surface-level-2-active-opacity, 0.18)); + --_is-state-layer-focus-opacity: var(--interactive-surface-state-layer-focus-opacity, var(--interactive-surface-level-2-focus-opacity, 0.22)); +} + +.interactive-surface[data-surface-level="3"] { + --_is-bg: var(--interactive-surface-level-bg, var(--interactive-surface-level-3-bg, var(--_is-base-bg))); + --_is-border-color: var(--interactive-surface-level-border-color, var(--interactive-surface-level-3-border-color, var(--_is-base-border-color))); + --_is-shadow-base: var(--interactive-surface-level-shadow, var( + --interactive-surface-level-3-shadow, + 0 14px 34px rgb(0 0 0 / 0.3) + )); + --_is-state-layer-hover-opacity: var(--interactive-surface-state-layer-hover-opacity, var(--interactive-surface-level-3-hover-opacity, 0.14)); + --_is-state-layer-active-opacity: var(--interactive-surface-state-layer-active-opacity, var(--interactive-surface-level-3-active-opacity, 0.24)); + --_is-state-layer-focus-opacity: var(--interactive-surface-state-layer-focus-opacity, var(--interactive-surface-level-3-focus-opacity, 0.28)); +} + /* 2a) State-layer pseudo-element */ .interactive-surface::before { @@ -324,7 +358,8 @@ /* 8) Visual variant token modifiers */ -.interactive-surface.variant-primary { +.interactive-surface.variant-primary, +.interactive-surface[data-surface-variant="primary"] { --interactive-surface-bg: var( --interactive-surface-variant-primary-bg, var(--variant-primary-bg, rgb(15 79 127)) @@ -341,7 +376,8 @@ --interactive-surface-darken-active: 0.97; } -.interactive-surface.variant-secondary { +.interactive-surface.variant-secondary, +.interactive-surface[data-surface-variant="secondary"] { --interactive-surface-bg: var( --interactive-surface-variant-secondary-bg, var(--variant-secondary-bg, rgb(198 220 237)) @@ -358,7 +394,8 @@ --interactive-surface-darken-active: 0.99; } -.interactive-surface.variant-accent { +.interactive-surface.variant-accent, +.interactive-surface[data-surface-variant="accent"] { --interactive-surface-bg: var( --interactive-surface-variant-accent-bg, var(--variant-accent-bg, rgb(201 240 237)) @@ -375,7 +412,8 @@ --interactive-surface-darken-active: 0.96; } -.interactive-surface.variant-subtle { +.interactive-surface.variant-subtle, +.interactive-surface[data-surface-variant="subtle"] { --interactive-surface-bg: var( --interactive-surface-variant-subtle-bg, var(--variant-subtle-bg, rgb(219 231 243)) @@ -392,7 +430,8 @@ --interactive-surface-darken-active: 1; } -.interactive-surface.variant-warning { +.interactive-surface.variant-warning, +.interactive-surface[data-surface-variant="warning"] { --interactive-surface-bg: var( --interactive-surface-variant-warning-bg, var(--variant-warning-bg, rgb(255 227 186)) @@ -409,7 +448,8 @@ --interactive-surface-darken-active: 0.95; } -.interactive-surface.variant-danger { +.interactive-surface.variant-danger, +.interactive-surface[data-surface-variant="danger"] { --interactive-surface-bg: var( --interactive-surface-variant-danger-bg, var(--variant-danger-bg, rgb(255 211 213)) diff --git a/package-lock.json b/package-lock.json index fc12d21..ba58fcf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,17 @@ { "name": "interactive-surface-css", - "version": "1.2.5", + "version": "1.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "interactive-surface-css", - "version": "1.2.5", + "version": "1.3.0", "license": "MIT", "devDependencies": { "@playwright/test": "^1.57.0", "clean-css": "~5.3.3", - "stylelint": "^17.1.0", + "stylelint": "^17.14.0", "stylelint-config-standard": "^40.0.0", "ui-style-kit-css": "2.0.1" }, @@ -20,13 +20,13 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -35,9 +35,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", "engines": { @@ -69,9 +69,9 @@ } }, "node_modules/@csstools/css-calc": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", - "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", + "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", "dev": true, "funding": [ { @@ -116,9 +116,9 @@ } }, "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.2.tgz", - "integrity": "sha512-5GkLzz4prTIpoyeUiIu3iV6CSG3Plo7xRVOFPKI7FVEJ3mZ0A8SwK0XU3Gl7xAkiQ+mDyam+NNp875/C5y+jSA==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.6.tgz", + "integrity": "sha512-TcJCWFbXLPpJYq6z7bfOyjWYJDiDg2/I4gyUC9pqPNqHFRIey0EB0q0L5cSnQDfWJg8Jd6VadakxdIez/3zkqQ==", "dev": true, "funding": [ { @@ -462,9 +462,9 @@ "license": "MIT" }, "node_modules/cosmiconfig": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz", - "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", + "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", "dev": true, "license": "MIT", "dependencies": { @@ -689,9 +689,9 @@ } }, "node_modules/get-east-asian-width": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", - "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", "dev": true, "license": "MIT", "engines": { @@ -924,16 +924,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-plain-object": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -949,9 +939,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", "dev": true, "funding": [ { @@ -1251,9 +1241,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz", + "integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==", "dev": true, "license": "MIT", "dependencies": { @@ -1432,9 +1422,9 @@ } }, "node_modules/string-width": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.0.tgz", - "integrity": "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz", + "integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==", "dev": true, "license": "MIT", "dependencies": { @@ -1465,9 +1455,9 @@ } }, "node_modules/stylelint": { - "version": "17.6.0", - "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.6.0.tgz", - "integrity": "sha512-tokrsMIVAR9vAQ/q3UVEr7S0dGXCi7zkCezPRnS2kqPUulvUh5Vgfwngrk4EoAoW7wnrThqTdnTFN5Ra7CaxIg==", + "version": "17.14.0", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.14.0.tgz", + "integrity": "sha512-8xkHPpdqYryeIsOgfsYTmr6cIeC4nLYWk5S8BPxpodq8mIuepggkMljsHewWfuAjj/+qpRKou2QerhjMH3iasg==", "dev": true, "funding": [ { @@ -1481,38 +1471,37 @@ ], "license": "MIT", "dependencies": { - "@csstools/css-calc": "^3.1.1", + "@csstools/css-calc": "^3.2.1", "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-syntax-patches-for-csstree": "^1.1.1", + "@csstools/css-syntax-patches-for-csstree": "^1.1.5", "@csstools/css-tokenizer": "^4.0.0", "@csstools/media-query-list-parser": "^5.0.0", "@csstools/selector-resolve-nested": "^4.0.0", "@csstools/selector-specificity": "^6.0.0", "colord": "^2.9.3", - "cosmiconfig": "^9.0.1", + "cosmiconfig": "^9.0.2", "css-functions-list": "^3.3.3", "css-tree": "^3.2.1", "debug": "^4.4.3", "fast-glob": "^3.3.3", "fastest-levenshtein": "^1.0.16", - "file-entry-cache": "^11.1.2", + "file-entry-cache": "^11.1.3", "global-modules": "^2.0.0", - "globby": "^16.1.1", + "globby": "^16.2.0", "globjoin": "^0.1.4", "html-tags": "^5.1.0", "ignore": "^7.0.5", "import-meta-resolve": "^4.2.0", - "is-plain-object": "^5.0.0", "mathml-tag-names": "^4.0.0", "meow": "^14.1.0", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "picocolors": "^1.1.1", - "postcss": "^8.5.8", + "postcss": "^8.5.15", "postcss-safe-parser": "^7.0.1", - "postcss-selector-parser": "^7.1.1", + "postcss-selector-parser": "^7.1.4", "postcss-value-parser": "^4.2.0", - "string-width": "^8.2.0", + "string-width": "^8.2.1", "supports-hyperlinks": "^4.4.0", "svg-tags": "^1.0.0", "table": "^6.9.0", diff --git a/package.json b/package.json index 2153180..e59db05 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "interactive-surface-css", - "version": "1.2.5", + "version": "1.3.0", "description": "A framework-agnostic, accessible CSS interaction primitive for buttons, cards, and icon controls with token-driven theming.", "keywords": [ "css", @@ -64,30 +64,25 @@ "scripts": { "check:no-hex-colors": "node ./scripts/check-no-hex-colors.mjs", "lint:css": "stylelint interactive-surface.css", - "pack:dry": "npm pack --dry-run --cache ./.npm-cache", - "bundle": "node ./scripts/build.mjs bundle", "minify": "node ./scripts/build.mjs minify", "build": "node ./scripts/build.mjs", - "test": "playwright test --config=./playwright.config.mjs", "test:chromium": "playwright test --config=./playwright.config.mjs --project=chromium", "test:install": "playwright install --with-deps chromium firefox webkit", "test:install:chromium": "playwright install chromium --with-deps --only-shell", - "validate:publish": "npm run check:no-hex-colors && npm run lint:css && npm run build && npm run pack:dry", "validate:ci": "npm run validate:publish", "validate:browsers": "npm run validate:publish && npm run test:install:chromium && npm run test:chromium", "validate:full": "npm run validate:publish && npm run test:install && npm test", "validate": "npm run validate:ci", - "prepublishOnly": "npm run validate:publish" }, "devDependencies": { "@playwright/test": "^1.57.0", "clean-css": "~5.3.3", - "stylelint": "^17.1.0", + "stylelint": "^17.14.0", "stylelint-config-standard": "^40.0.0", "ui-style-kit-css": "2.0.1" }, diff --git a/tests/example.spec.ts b/tests/example.spec.ts index b6c46af..f5d6ab3 100644 --- a/tests/example.spec.ts +++ b/tests/example.spec.ts @@ -29,4 +29,18 @@ test.describe("example page", () => { const controls = page.locator(".interactive-surface"); await expect(await controls.count()).toBeGreaterThanOrEqual(27); }); + + test("token editor dialog semantics are active only while open", async ({ page }) => { + const modalDialog = page.locator('[role="dialog"][aria-modal="true"]'); + const editTriggers = page.locator("[data-token-edit]"); + + await expect(modalDialog).toHaveCount(0); + expect(await editTriggers.count()).toBeGreaterThan(0); + + await editTriggers.nth(0).click(); + await expect(modalDialog).toHaveCount(1); + + await page.keyboard.press("Escape"); + await expect(modalDialog).toHaveCount(0); + }); }); diff --git a/tests/interactive-surface.spec.ts b/tests/interactive-surface.spec.ts index 0524fab..ca34083 100644 --- a/tests/interactive-surface.spec.ts +++ b/tests/interactive-surface.spec.ts @@ -32,6 +32,11 @@ const html = ` Target Pressed Disabled + Class Primary + Data Primary + Level 1 + Level 2 + Level 3 ☀ ☾ @@ -108,6 +113,82 @@ test.describe("interactive surface package behavior", () => { expect(pointerEvents).toBe("none"); }); + test("data-surface-variant aliases class-based visual variants", async ({ page }) => { + const variantNames = ["primary", "secondary", "accent", "subtle", "warning", "danger"]; + + for (const variantName of variantNames) { + const { classStyles, dataStyles, dataVariant } = await page.evaluate((value) => { + const classButton = document.createElement("button"); + classButton.className = `interactive-surface variant-${value}`; + classButton.textContent = "Class variant"; + + const dataButton = document.createElement("button"); + dataButton.className = "interactive-surface"; + dataButton.setAttribute("data-surface-variant", value); + dataButton.textContent = "Data variant"; + + document.body.append(classButton, dataButton); + + const getSurfaceStyles = (el: HTMLElement) => { + const computed = window.getComputedStyle(el); + + return { + background: computed.backgroundColor, + borderColor: computed.borderColor, + color: computed.color + }; + }; + + const comparison = { + classStyles: getSurfaceStyles(classButton), + dataStyles: getSurfaceStyles(dataButton), + dataVariant: dataButton.getAttribute("data-surface-variant") + }; + + classButton.remove(); + dataButton.remove(); + + return comparison; + }, variantName); + + expect(dataVariant).toBe(variantName); + expect(dataStyles, `data-surface-variant="${variantName}"`).toEqual(classStyles); + } + }); + + test("data-surface-level exposes distinct standalone depth and state defaults", async ({ page }) => { + const levelStyles = await page.locator("[data-surface-level]").evaluateAll((elements) => + elements.map((el) => { + const computed = window.getComputedStyle(el); + + const resolveOpacity = (varName: string) => { + const probe = document.createElement("span"); + el.appendChild(probe); + (probe as HTMLSpanElement).style.opacity = `var(${varName})`; + const val = parseFloat(window.getComputedStyle(probe).opacity).toFixed(2); + probe.remove(); + return val; + }; + + return { + level: el.getAttribute("data-surface-level"), + boxShadow: computed.boxShadow, + hoverOpacity: resolveOpacity("--_is-state-layer-hover-opacity"), + activeOpacity: resolveOpacity("--_is-state-layer-active-opacity"), + focusOpacity: resolveOpacity("--_is-state-layer-focus-opacity") + }; + }) + ); + + expect(levelStyles.map(({ level }) => level)).toEqual(["1", "2", "3"]); + expect(new Set(levelStyles.map(({ boxShadow }) => boxShadow)).size).toBe(3); + expect(levelStyles).toEqual([ + expect.objectContaining({ hoverOpacity: "0.08", activeOpacity: "0.14", focusOpacity: "0.18" }), + expect.objectContaining({ hoverOpacity: "0.11", activeOpacity: "0.18", focusOpacity: "0.22" }), + expect.objectContaining({ hoverOpacity: "0.14", activeOpacity: "0.24", focusOpacity: "0.28" }) + ]); + }); + test("reduced motion disables movement transform", async ({ page }) => { await page.emulateMedia({ reducedMotion: "reduce" }); await page.setContent(html); diff --git a/wiki/API-Reference.md b/wiki/API-Reference.md index 10550e7..77621d9 100644 --- a/wiki/API-Reference.md +++ b/wiki/API-Reference.md @@ -57,7 +57,7 @@ Native disabled controls also receive disabled styling. ## Visual variants -These classes only tune brightness behavior. They do not define complete color themes by themselves. +These classes and matching data attributes tune variant color and state behavior. They do not define complete color themes by themselves. - `.variant-primary` - `.variant-secondary` @@ -66,8 +66,24 @@ These classes only tune brightness behavior. They do not define complete color t - `.variant-warning` - `.variant-danger` +The equivalent data attribute API is available for bridge-generated or runtime-assigned markup: + +```html +Save +``` + +Supported values are `primary`, `secondary`, `accent`, `subtle`, `warning`, and `danger`. + These are best understood as behavioral modifiers layered on top of your own theme tokens. +## Surface levels + +### `data-surface-level="1|2|3"` + +Applies semantic depth without requiring a layout or theme library. Level `1` keeps a quiet default surface, level `2` adds a raised state, and level `3` adds the strongest default depth. + +Bridge styles can override these defaults through the public level tokens while Interactive Surface keeps the interaction behavior. + ## Icon micro pattern ### `.icon-only` diff --git a/wiki/Getting-Started.md b/wiki/Getting-Started.md index 969c039..25e4105 100644 --- a/wiki/Getting-Started.md +++ b/wiki/Getting-Started.md @@ -73,6 +73,14 @@ That one class gives the element: Continue ``` +### Runtime-assigned surface intent + +```html + + Continue + +``` + ### Icon-only control ```html diff --git a/wiki/Home.md b/wiki/Home.md index 2b573a6..b026d95 100644 --- a/wiki/Home.md +++ b/wiki/Home.md @@ -33,8 +33,8 @@ Interactive Surface CSS centralizes that logic into a single reusable primitive: - **JS entry:** `index.js` (imports the stylesheet) - **Demo/customization page:** `index.html` - **License:** MIT -- **Current version in repo:** `1.2.3` -- **Release tag:** `v1.2.3` +- **Current version in repo:** `1.3.0` +- **Release tag:** `v1.3.0` ## Documentation map @@ -61,6 +61,12 @@ Interactive Surface CSS centralizes that logic into a single reusable primitive: ``` +```html + + Continue + +``` + ```html ... @@ -72,3 +78,5 @@ Interactive Surface CSS centralizes that logic into a single reusable primitive: This library is not trying to be a full component system. It is a lower-level interaction primitive intended to sit underneath a design system or application UI layer. That makes it useful when you want consistency and accessibility without taking on the API surface of a large framework-specific component library. + +In the companion CSS ecosystem, `layout-style-css` owns structure, `ui-style-kit-css` owns visual paint and themes, and Interactive Surface CSS owns interaction states. diff --git a/wiki/Installation-and-Usage.md b/wiki/Installation-and-Usage.md index 3927620..a4c3fc1 100644 --- a/wiki/Installation-and-Usage.md +++ b/wiki/Installation-and-Usage.md @@ -80,6 +80,16 @@ import "interactive-surface-css"; Learn more ``` +### Data attribute surface hooks + +```html + + Learn more + +``` + +Use data attributes when another library, renderer, or design-system adapter assigns semantic surface intent. + ### Large surface card ```html @@ -155,7 +165,7 @@ import "interactive-surface-css"; UI Style Kit owns visual theme tokens. Interactive Surface owns interaction behavior on `.interactive-surface`. -With `ui-style-kit-css@2.0.1`, the default full bundle does not include the bridge. Use the opt-in bridge bundle when you want runtime UI switching and Interactive Surface token mapping in one import: +With `ui-style-kit-css` 2.x, the default full bundle does not include the bridge. Use the opt-in bridge bundle when you want runtime UI switching and Interactive Surface token mapping in one import: ```js import "ui-style-kit-css/with-bridge.css"; @@ -178,7 +188,7 @@ import "ui-style-kit-css/styles/minimal-saas.css"; import "ui-style-kit-css/interactive-surface-bridge"; ``` -The bridge maps UI Style Kit theme roles into Interactive Surface tokens. UI Style Kit owns those visual token values; Interactive Surface owns the hover, focus, pressed, selected, disabled, and reduced-motion behavior on `.interactive-surface`. +The bridge maps UI Style Kit theme roles into Interactive Surface tokens. UI Style Kit owns those visual token values; Interactive Surface owns the hover, focus, pressed, selected, disabled, and reduced-motion behavior on `.interactive-surface`. `data-surface-variant` and `data-surface-level` are safe standalone hooks, so bridge-generated markup keeps meaningful interaction behavior without requiring companion class names. ## Motion guardrail diff --git a/wiki/Testing-and-Quality.md b/wiki/Testing-and-Quality.md index 9c649c3..39d7b2b 100644 --- a/wiki/Testing-and-Quality.md +++ b/wiki/Testing-and-Quality.md @@ -52,6 +52,8 @@ Current Playwright tests validate: - keyboard focus visibility - `aria-pressed` active styling - `aria-disabled` non-interactivity +- `data-surface-variant` parity with `.variant-*` classes +- `data-surface-level` standalone depth and state defaults - reduced-motion transform removal - icon-only minimum target size - index demo page rendering and control count diff --git a/wiki/Token-Reference.md b/wiki/Token-Reference.md index 6b9fdbf..e717311 100644 --- a/wiki/Token-Reference.md +++ b/wiki/Token-Reference.md @@ -60,7 +60,7 @@ The stylesheet also recognizes these semantic fallbacks: ## UI Style Kit CSS bridge -When used with `ui-style-kit-css@2.0.1`, the UI Style Kit bridge maps active `data-ui`, `data-theme`, and `data-mode` roles into this package's public `--interactive-surface-*` contract. +When used with `ui-style-kit-css` 2.x, the UI Style Kit bridge maps active `data-ui`, `data-theme`, and `data-mode` roles into this package's public `--interactive-surface-*` contract. Interactive Surface does not depend on UI Style Kit token names directly. It continues to resolve values through its own token contract, so the package remains usable without UI Style Kit. @@ -70,12 +70,26 @@ The bridge can also provide state-layer aliases for visible hover, focus, and ac - `--interactive-surface-state-layer-opacity-focus` - `--interactive-surface-state-layer-opacity-active` -When a bridged surface includes `data-surface-level`, Interactive Surface also reads these optional depth tokens: +When a surface includes `data-surface-level`, Interactive Surface reads these optional depth tokens: - `--interactive-surface-level-bg` - `--interactive-surface-level-border-color` - `--interactive-surface-level-shadow` +The package also exposes per-level defaults that bridge styles or app themes can override: + +- `--interactive-surface-level-1-bg` +- `--interactive-surface-level-1-border-color` +- `--interactive-surface-level-1-shadow` +- `--interactive-surface-level-2-bg` +- `--interactive-surface-level-2-border-color` +- `--interactive-surface-level-2-shadow` +- `--interactive-surface-level-3-bg` +- `--interactive-surface-level-3-border-color` +- `--interactive-surface-level-3-shadow` + +The matching state-opacity hooks are `--interactive-surface-level--hover-opacity`, `--interactive-surface-level--active-opacity`, and `--interactive-surface-level--focus-opacity`. + ## Important implementation detail The package does **not** define global `:root` tokens.
Pressed style can come from semantic attributes instead of custom state classes.
Compose text, metadata, and icons while preserving a single interaction model.