actual-css 0.7.0 → 0.9.0
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/CHANGELOG.md +217 -0
- package/README.md +13 -11
- package/components.json +668 -0
- package/dist/actual.css +166 -37
- package/dist/actual.full.css +1087 -643
- package/dist/actual.full.js +1 -1
- package/dist/actual.full.min.css +1 -1
- package/dist/actual.js +1 -1
- package/dist/actual.min.css +1 -1
- package/docs/design-notes/accessibility-decisions.md +33 -0
- package/docs/design-notes/browser-support.md +62 -30
- package/docs/design-notes/column-layout.md +3 -3
- package/docs/design-notes/css-anchor.md +75 -21
- package/docs/design-notes/enhancement-contract.md +14 -6
- package/docs/design-notes/grid.md +4 -4
- package/docs/design-notes/naming.md +6 -1
- package/docs/design-notes/platform-alignment.md +53 -7
- package/docs/design-notes/popover-manual-poc.md +105 -25
- package/docs/design-notes/surface-reparenting.md +56 -47
- package/docs/design-notes/tooltip-coordinate-space.md +132 -0
- package/docs/design-notes/widget-primitives.md +7 -4
- package/docs/pages/components/accordion.md +41 -2
- package/docs/pages/components/alert.md +85 -30
- package/docs/pages/components/app-navigation.md +6 -0
- package/docs/pages/components/avatar.md +6 -6
- package/docs/pages/components/badge.md +101 -16
- package/docs/pages/components/button.md +16 -8
- package/docs/pages/components/card.md +10 -10
- package/docs/pages/components/dialog.md +21 -6
- package/docs/pages/components/drawer.md +13 -0
- package/docs/pages/components/fab.md +4 -3
- package/docs/pages/components/list.md +42 -4
- package/docs/pages/components/navbar.md +17 -1
- package/docs/pages/components/pagination.md +2 -2
- package/docs/pages/components/skeleton.md +3 -1
- package/docs/pages/components/steps.md +19 -0
- package/docs/pages/components/table.md +3 -2
- package/docs/pages/enhancements/flyout.md +82 -38
- package/docs/pages/enhancements/overview.md +25 -0
- package/docs/pages/enhancements/status-bar.md +1 -1
- package/docs/pages/enhancements/tabs.md +45 -37
- package/docs/pages/enhancements/tooltip.md +36 -10
- package/docs/pages/examples/overview.md +38 -5
- package/docs/pages/forms/choice-cards.md +11 -6
- package/docs/pages/forms/combobox.md +1 -1
- package/docs/pages/forms/floating-field.md +4 -3
- package/docs/pages/forms/joined-controls.md +22 -0
- package/docs/pages/forms/otp.md +2 -1
- package/docs/pages/forms/overview.md +1 -1
- package/docs/pages/forms/states-and-sizes.md +16 -18
- package/docs/pages/forms/sticky-actions.md +2 -2
- package/docs/pages/forms/switch.md +1 -1
- package/docs/pages/foundations/philosophy.md +57 -26
- package/docs/pages/foundations/theming.md +2 -2
- package/docs/pages/foundations/tokens.md +118 -21
- package/docs/pages/foundations/typography.md +35 -14
- package/docs/pages/guides/introduction.md +11 -9
- package/docs/pages/guides/progressive-enhancement.md +115 -30
- package/docs/pages/guides/tailwind.md +3 -2
- package/docs/pages/layout/media.md +1 -1
- package/docs/pages/layout/overview.md +11 -1
- package/docs/pages/layout/scroll-snap.md +3 -3
- package/docs/pages/layout/stack.md +17 -9
- package/docs/pages/patterns/icon-input.md +4 -2
- package/docs/pages/patterns/icon-slot.md +2 -0
- package/docs/pages/patterns/intent-nesting.md +1 -1
- package/docs/pages/utilities/overview.md +23 -2
- package/docs/pages/utilities/sizing-wrapping.md +1 -1
- package/docs/pages/utilities/spacing.md +10 -8
- package/llms.txt +2 -1
- package/package.json +159 -149
- package/{scripts/reserved-classes.json → reserved-classes.json} +4 -6
- package/src/css/components/accordion.css +42 -3
- package/src/css/components/alert.css +25 -13
- package/src/css/components/app-nav.css +18 -8
- package/src/css/components/avatar.css +13 -1
- package/src/css/components/badge.css +78 -12
- package/src/css/components/breadcrumb.css +2 -3
- package/src/css/components/button.css +9 -16
- package/src/css/components/card.css +9 -2
- package/src/css/components/chat.css +1 -0
- package/src/css/components/data-list.css +1 -1
- package/src/css/components/drawer.css +101 -32
- package/src/css/components/fab.css +0 -6
- package/src/css/components/flyout.css +35 -98
- package/src/css/components/index.css +0 -1
- package/src/css/components/indicator.css +21 -0
- package/src/css/components/key.css +10 -0
- package/src/css/components/list.css +40 -4
- package/src/css/components/menu.css +2 -1
- package/src/css/components/modal.css +48 -33
- package/src/css/components/navbar.css +37 -4
- package/src/css/components/pagination.css +1 -0
- package/src/css/components/skeleton.css +15 -4
- package/src/css/components/spinner.css +6 -6
- package/src/css/components/status-bar.css +0 -5
- package/src/css/components/steps.css +45 -59
- package/src/css/components/tab.css +29 -9
- package/src/css/components/table.css +13 -5
- package/src/css/components/tooltip.css +36 -40
- package/src/css/core/base.css +23 -1
- package/src/css/core/intents.css +17 -6
- package/src/css/core/theme.css +27 -0
- package/src/css/core/tokens.css +35 -15
- package/src/css/core/variants.css +64 -15
- package/src/css/forms/choice-card.css +4 -4
- package/src/css/forms/choice.css +76 -29
- package/src/css/forms/control.css +2 -1
- package/src/css/forms/floating-field.css +3 -3
- package/src/css/forms/form.css +4 -3
- package/src/css/forms/input-icon.css +20 -14
- package/src/css/forms/native.css +6 -1
- package/src/css/forms/otp.css +9 -1
- package/src/css/forms/range.css +2 -0
- package/src/css/forms/select.css +29 -9
- package/src/css/forms/switch.css +28 -8
- package/src/css/layout/app-layout.css +1 -1
- package/src/css/layout/grid.css +50 -76
- package/src/css/layout/scroll-snap.css +11 -7
- package/src/css/layout/scroller.css +3 -4
- package/src/css/layout/stack.css +2 -2
- package/src/css/typography/fluid.css +4 -3
- package/src/css/typography/prose.css +6 -5
- package/src/css/utilities/base.css +2 -1
- package/src/css/utilities/extra.css +20 -17
- package/src/js/context-menu.js +27 -26
- package/src/js/dialog.js +2 -1
- package/src/js/dismiss.js +5 -0
- package/src/js/enhance.js +34 -2
- package/src/js/enhancement-loader.js +36 -43
- package/src/js/events.js +0 -3
- package/src/js/filter.js +0 -1
- package/src/js/floating-space.js +42 -0
- package/src/js/flyout.js +9 -44
- package/src/js/focus-group.js +7 -0
- package/src/js/focus.js +6 -0
- package/src/js/menu.js +10 -1
- package/src/js/selectors.js +5 -9
- package/src/js/status.js +6 -5
- package/src/js/surface.js +97 -139
- package/src/js/tab.js +6 -1
- package/src/js/tooltip.js +150 -51
- package/src/js/validation.js +1 -1
- package/src/css/components/surface.css +0 -49
package/dist/actual.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var g=new WeakMap,w=new WeakMap;function b(){return{refresh(){},disconnect(){}}}function N(e){return e?.nodeType===1||e?.nodeType===9||e?.nodeType===11}function A(e){let n=new Set,r="";function t(o){try{if(e.querySelector?.(o),e.nodeType===1)e.matches(o);return!0}catch(s){return console.error(`Invalid enhancer selector "${o}"`,s),!1}}function c(){r=[...n].flatMap((o)=>o.selectors).join(",")}function a(o,s){if(s?.nodeType!==1)return;let f=o.instances.get(s);for(let u of o.selectors){if(!s.matches(u))continue;if(f?.has(u))continue;let p;try{p=o.enhancers[u](s)}catch(v){console.error(`Enhancer failed for selector "${u}"`,v);continue}if(!f)f=new Map,o.instances.set(s,f);f.set(u,typeof p==="function"?p:null)}}function l(o,s){let f=o.instances.get(s);if(!f)return;for(let u of f.values())try{u?.()}catch(p){console.error("Enhancer cleanup failed",p)}o.instances.delete(s)}function i(o,s){if(!N(s))return;if(s.nodeType===1)a(o,s);if(o.selectorString)s.querySelectorAll?.(o.selectorString).forEach((f)=>{a(o,f)})}function d(o){if(!N(o)||!r)return;if(o.nodeType===1)for(let s of n)a(s,o);o.querySelectorAll?.(r).forEach((s)=>{for(let f of n)a(f,s)})}function y(){for(let o of n)for(let s of Array.from(o.instances.keys()))if(!s.isConnected||!e.contains(s))l(o,s)}let h=new MutationObserver((o)=>{let s=!1;for(let f of o){for(let u of f.addedNodes)d(u);if(f.removedNodes.length>0)s=!0}if(s)y()});return{add(o){let s=Object.keys(o).filter(t);if(!s.length)return null;let u={disconnected:!1,enhancers:Object.fromEntries(s.map((p)=>[p,o[p]])),instances:new Map,selectors:s,selectorString:s.join(",")};if(n.add(u),n.size===1)h.observe(e,{childList:!0,subtree:!0});return c(),i(u,e),u},refresh(o,s){if(o.disconnected)return;i(o,s)},remove(o){if(o.disconnected)return;o.disconnected=!0;for(let s of Array.from(o.instances.keys()))l(o,s);if(n.delete(o),c(),n.size===0)h.disconnect(),g.delete(e)}}}var _=/^[a-z][a-z0-9-]*$/;function M(e){if(!_.test(e))throw TypeError(`Invalid enhancement name: ${e}`);return`[data-enhance~="${e}"]`}function T(e,n,r){if(typeof document>"u")return b();if(M(e),typeof n!=="function")throw TypeError("registerEnhancement() requires a function init.");r??=document.documentElement;let t=w.get(r);if(!t)t=new Map,w.set(r,t);if(t.has(e))throw Error(`Enhancement "${e}" is already registered on this root.`);let c=E({[M(e)]:n},r);return t.set(e,c),{refresh:(a)=>c.refresh(a),disconnect:()=>{if(c.disconnect(),t.get(e)===c)t.delete(e);if(t.size===0)w.delete(r)}}}function U(e){let n=g.get(e);if(!n)n=A(e),g.set(e,n);return n}function E(e,n){if(typeof document>"u")return b();n??=document.documentElement;let r=U(n),t=r.add(e);if(!t)return b();return{refresh:(c)=>r.refresh(t,c),disconnect:()=>{r.remove(t)}}}function D(e){return e.replace(/([a-zA-Z_$][\w$-]*)\s*:|'((?:\\'|[^'])*)'/g,(n,r,t)=>{if(r)return`"${r}":`;return`"${t.replace(/\\'/g,"'").replace(/"/g,"\\\"")}"`})}function O(e){if(e==null||typeof e!=="string")return{};let n=e.trim();if(n==="")return{};let r=n,t;try{t=JSON.parse(r)}catch{let c=D(r);r=c.startsWith("{")?c:`{${c}}`,t=JSON.parse(r)}if(t===null||typeof t!=="object"||Array.isArray(t))throw TypeError("Enhancement config must be an object");return t}var R='script[type="application/json"][data-enhance-modules]',m=new Map,L=new Map,j=new Map,C=(e)=>import(e);function S(e,n,r){let t=new URL(e,document.baseURI).href;if(!n.has(t)){let c=new Promise((a,l)=>{let i=document.createElement(r==="script"?"script":"link");if(r==="script")i.src=t;else i.rel="stylesheet",i.href=t;i.onload=a,i.onerror=()=>{i.remove(),l(Error(`Unable to load ${r}: ${t}`))},document.head.append(i)});n.set(t,c),c.catch(()=>{if(n.get(t)===c)n.delete(t)})}return n.get(t)}function k(e){return S(e,L,"script")}function I(e){return S(e,j,"style")}function J(e,n){return(r)=>{let t=new AbortController,c=r.getAttribute("data-enhance-config"),a=O(c??""),l=(d,y,h={})=>r.dispatchEvent(new CustomEvent(`actual:${e}:${d}`,{bubbles:!0,detail:y,...h})),i;try{i=n(r,{config:a,signal:t.signal,emit:l})}catch(d){throw t.abort(),d}if(i?.then)throw t.abort(),TypeError(`Enhancement "${e}" returned a Promise. connect() must be synchronous; use prepare() for asynchronous setup.`);if(i!==void 0&&typeof i!=="function")throw t.abort(),TypeError(`Enhancement "${e}" must return a cleanup function or undefined.`);return()=>{t.abort(),i?.()}}}function q(e,n){if(typeof n!=="string"||n.trim()==="")throw TypeError(`Enhancement "${e}" must declare a non-empty module URL.`)}function z(e,n){q(e,n);let r=new URL(n,document.baseURI).href,t=m.get(e);if(t){if(t.url!==r)throw Error(`Enhancement "${e}" is already declared from ${t.url} and cannot be replaced by ${r}.`);return t.promise.then(()=>({name:e,status:"skipped"}))}let c=C(r).then(async(a)=>{if(typeof a.default!=="function")throw TypeError(`Enhancement "${e}" must export a default function.`);if(a.prepare!==void 0){if(typeof a.prepare!=="function")throw TypeError(`Enhancement "${e}" exports an invalid prepare().`);await a.prepare({resolve:(l)=>new URL(l,r).href,loadScript:k,loadStyle:I})}return T(e,J(e,a.default)),{name:e,status:"loaded"}}).catch((a)=>{if(m.get(e)?.promise===c)m.delete(e);throw a});return m.set(e,{url:r,promise:c}),c}async function F(e){let n=await Promise.allSettled(e.map(([t,c])=>z(t,c))),r={names:[],failed:[],skipped:[]};return n.forEach((t,c)=>{let a=e[c][0];if(t.status==="rejected")r.failed.push({name:a,error:t.reason});else if(t.value.status==="skipped")r.skipped.push(a);else r.names.push(a)}),r}function P(e){let n=JSON.parse(e);if(typeof n!=="object"||n===null||Array.isArray(n))throw TypeError("Manifest must be a JSON object.");return Object.entries(n)}async function W(e){let n=P(e.textContent),r=await F(n);for(let t of r.failed)console.error(`Enhancement "${t.name}" failed to load:`,t.error);if(r.failed.length===0)e.remove();return r}function x(){return E({[R]:(e)=>{W(e).catch((n)=>{console.error("Failed to parse enhancement manifest:",n)})}})}x();
|
package/dist/actual.min.css
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
*, *::before, *::after { box-sizing: border-box; } html { -webkit-text-size-adjust: 100%; } body { margin: 0; } button, input, textarea, select { font: inherit; } [hidden]:not([hidden="until-found"]) { display: none !important; } :where(img, svg, video) { max-inline-size: 100%; block-size: auto; } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto !important; } *, *::before, *::after { animation-duration: 0.01ms !important; animation-delay: 0s !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; transition-delay: 0s !important; } } :root { --font-sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; --line-height: 1.5; --line-height-tight: 1.25; --line-height-relaxed: 1.75; --font-weight: 400; --font-weight-medium: 500; --font-weight-strong: 650; --font-weight-bold: 700; --font-size-xs: 0.8125rem; --font-size-sm: 0.875rem; --font-size-md: 1rem; --font-size-lg: 1.125rem; --gap: 0.75rem; --density-space: var(--space-40); --density-compact-size: 1.5rem; --space-10: 0.25rem; --space-20: 0.5rem; --space-30: 0.75rem; --space-40: 1rem; --space-50: 1.5rem; --space-60: 2rem; --border-width: 1px; --radius-full: 9999px; --shadow: 0 1px 3px rgb(0 0 0 / 0.05), 0 4px 12px rgb(0 0 0 / 0.04); --shadow-popout: 0 0.25rem 0.75rem rgb(0 0 0 / 0.15); --duration: 150ms; --duration-fast: 100ms; --duration-slow: 200ms; --duration-spin: 0.75s; --duration-shimmer: 1.2s; --ease-enter: cubic-bezier(0.2, 0, 0, 1); --ease-exit: cubic-bezier(0.4, 0, 1, 1); --focus-outline-color: currentColor; --focus-outline: calc(var(--border-width) * 2) solid var(--focus-outline-color); --focus-outline-offset: 2px; --focus-ring-width: 3px; --indicator-offset: 14.65%; --backdrop-color: rgb(0 0 0); --backdrop-opacity: 0.45; --backdrop-fill: rgb(0 0 0 / var(--backdrop-opacity)); --bar-height: 0.65rem; --control-size-sm: 2rem; --control-size-md: 2.375rem; --control-size-lg: 2.75rem; --control-size: var(--control-size-md); --control-pad-x: 1em; --control-font-size: var(--font-size-md); --choice-control-size: 1.125em; --disabled-opacity: 0.65; } :root, [data-theme] { --state-selected: var(--primary); --state-selected-fg: var(--primary-fg); --state-disabled: var(--text-subtle); --indicator-ring: var(--surface); } :root { --soft-bg-mix: 88%; --soft-border-mix: 65%; --soft-hover-alpha: 12%; --soft-fg-mix: 100%; --icon-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%238a8594' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E"); --icon-check: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'/%3E%3C/svg%3E"); --icon-close: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='6' y1='6' x2='18' y2='18'/%3E%3Cline x1='18' y1='6' x2='6' y2='18'/%3E%3C/svg%3E"); --icon-plus: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='12' y1='5' x2='12' y2='19'/%3E%3Cline x1='5' y1='12' x2='19' y2='12'/%3E%3C/svg%3E"); --viewport-block: 100vh; --viewport-inline: 100vw; --z-sticky: 10; --z-menu: 20; --z-tooltip: 50; --z-status: 60; } @media (forced-colors: active) { :root, [data-theme] { --state-disabled: GrayText; --disabled-opacity: 1; } } @supports (block-size: 100dvh) { :root { --viewport-block: 100dvh; } } @supports (inline-size: 100vi) { :root { --viewport-inline: 100vi; } } @supports (color: color-mix(in oklch, red, white)) { :root, [data-theme] { --shadow: 0 1px 3px color-mix(in oklch, var(--shadow-color) 5%, transparent), 0 4px 12px color-mix(in oklch, var(--shadow-color) 4%, transparent); --shadow-popout: 0 0.25rem 0.75rem color-mix(in oklch, var(--shadow-color) 15%, transparent); } :root { --backdrop-fill: color-mix( in oklch, var(--backdrop-color) calc(var(--backdrop-opacity) * 100%), transparent ); } } :root { color-scheme: light dark; } [data-theme="light"] { color-scheme: light; } [data-theme="dark"] { color-scheme: dark; } @supports not (color: light-dark(red, blue)) { :root, [data-theme="light"], [data-theme="dark"] { color-scheme: light; } } :root, [data-theme="light"] { --primary: hsl(268 32% 33%); --primary-fg: hsl(0 0% 98%); --secondary: hsl(16 38% 38%); --secondary-fg: hsl(0 0% 98%); --success: hsl(150 40% 31%); --success-fg: hsl(0 0% 98%); --warning: hsl(36 68% 34%); --warning-fg: hsl(0 0% 98%); --danger: hsl(5 58% 43%); --danger-fg: hsl(0 0% 98%); --neutral: hsl(258 7% 43%); --neutral-fg: hsl(0 0% 98%); --surface: hsl(0 0% 100%); --surface-raised: hsl(0 0% 100%); --surface-subtle: hsl(262 24% 96%); --surface-solid: hsl(261 16% 13%); --text: hsl(261 20% 13%); --text-muted: hsl(259 9% 42%); --text-subtle: hsl(258 7% 57%); --border: hsl(260 15% 88%); --focus: var(--neutral); --focus-ring: hsl(258 7% 43% / 0.3); --hover-overlay: hsl(260 20% 10% / 0.04); --radius: 0.5rem; --radius-sm: 0.25rem; --radius-lg: 0.75rem; --shadow-color: hsl(260 25% 10%); --hgroup-gap: 0.25em; } :root, [data-theme] { --heading: var(--text); --selection-bg: var(--primary); --selection-fg: var(--primary-fg); --focus-ring-shadow: 0 0 0 var(--focus-ring-width) var(--focus-ring); } :root, [data-theme] { accent-color: var(--primary); } [data-tone="dark"] { --tone-fg: white; } [data-tone="light"] { --tone-fg: var(--text); } @supports (color: color-mix(in oklch, red, white)) { :root, [data-theme] { --focus-ring: color-mix(in oklch, var(--focus) 30%, transparent); } } @supports (color: light-dark(red, blue)) { :root, [data-theme="light"] { --primary: light-dark(hsl(268 32% 33%), hsl(268 42% 76%)); --primary-fg: light-dark(hsl(0 0% 98%), hsl(268 30% 12%)); --secondary: light-dark(hsl(16 38% 38%), hsl(18 45% 72%)); --secondary-fg: light-dark(hsl(0 0% 98%), hsl(18 32% 11%)); --success: light-dark(hsl(150 40% 31%), hsl(150 38% 62%)); --success-fg: light-dark(hsl(0 0% 98%), hsl(150 35% 8%)); --warning: light-dark(hsl(36 68% 34%), hsl(40 62% 64%)); --warning-fg: light-dark(hsl(0 0% 98%), hsl(38 35% 10%)); --danger: light-dark(hsl(5 58% 43%), hsl(4 60% 70%)); --danger-fg: light-dark(hsl(0 0% 98%), hsl(4 35% 10%)); --neutral: light-dark(hsl(258 7% 43%), hsl(258 10% 72%)); --neutral-fg: light-dark(hsl(0 0% 98%), hsl(260 18% 10%)); --surface: light-dark(hsl(0 0% 100%), hsl(263 17% 9%)); --surface-raised: light-dark(hsl(0 0% 100%), hsl(263 15% 12%)); --surface-subtle: light-dark(hsl(262 24% 96%), hsl(263 13% 16%)); --surface-solid: light-dark(hsl(261 16% 13%), hsl(0 0% 96%)); --text: light-dark(hsl(261 20% 13%), hsl(260 12% 95%)); --text-muted: light-dark(hsl(259 9% 42%), hsl(259 9% 67%)); --text-subtle: light-dark(hsl(258 7% 57%), hsl(258 7% 54%)); --border: light-dark(hsl(260 15% 88%), hsl(262 12% 24%)); --focus-ring: light-dark(hsl(258 7% 43% / 0.3), hsl(258 10% 72% / 0.28)); --hover-overlay: light-dark(hsl(260 20% 10% / 0.04), hsl(0 0% 100% / 0.06)); --shadow-color: light-dark(hsl(260 25% 10%), hsl(262 40% 2%)); } } @supports (color: color-mix(in oklch, red, white)) and (color: light-dark(red, blue)) { :root, [data-theme] { --focus-ring: light-dark( color-mix(in oklch, var(--focus) 30%, transparent), color-mix(in oklch, var(--focus) 28%, transparent) ); } } @media (prefers-contrast: more) { :root, [data-theme][data-theme] { --border-width: 2px; --focus-outline-offset: 3px; --focus-ring-width: 4px; --text-muted: var(--text); --text-subtle: var(--text); } } body { background: var(--surface); color: var(--text); font-family: var(--font-sans); line-height: var(--line-height); } a { color: var(--link, inherit); text-underline-offset: 0.18em; } a:hover { text-decoration-thickness: 0.12em; } :where(h1, h2, h3, h4, h5, h6) { color: var(--heading, inherit); line-height: var(--line-height-tight); text-wrap: balance; } hgroup { display: grid; gap: var(--hgroup-gap); } hgroup > * { margin-block: 0; } hr { border: 0; border-block-start: var(--border-width) solid var(--border); margin-block: var(--space-60); } code, kbd, samp, pre { font-family: var(--font-mono); } small { font-size: 0.875em; } ::selection { background: var(--selection-bg, var(--primary)); color: var(--selection-fg, var(--primary-fg)); } .primary { --intent: var(--primary); --intent-fg: var(--primary-fg); } .secondary { --intent: var(--secondary); --intent-fg: var(--secondary-fg); } .success { --intent: var(--success); --intent-fg: var(--success-fg); } .warning { --intent: var(--warning); --intent-fg: var(--warning-fg); } .danger { --intent: var(--danger); --intent-fg: var(--danger-fg); } .neutral { --intent: var(--neutral); --intent-fg: var(--neutral-fg); } .intent-color { color: var(--intent); } .solid { --ui-bg: var(--intent, var(--neutral)); --ui-fg: var(--intent-fg, var(--neutral-fg)); --ui-border: transparent; } .soft { --ui-bg: var(--surface-subtle); --ui-border: var(--border); --ui-fg: var(--intent, var(--text)); } .outline { --ui-bg: transparent; --ui-fg: var(--intent, currentColor); --ui-border: var(--intent, currentColor); } .surface { --ui-bg: var(--surface); --ui-fg: var(--intent, var(--text)); --ui-border: var(--border); } .inverted { --ui-bg: var(--surface-solid); --ui-fg: var(--surface); --ui-border: transparent; --ui-hover-bg: var(--surface-solid); --heading: var(--surface); --busy-overlay-bg: var(--surface-solid); background: var(--ui-bg); color: var(--ui-fg); border-color: var(--ui-border); } .sm { --gap: var(--space-20); --density-space: var(--space-20); --control-size: var(--control-size-sm); --density-compact-size: 1.25rem; } .lg { --gap: var(--space-50); --density-space: var(--space-50); --control-size: var(--control-size-lg); --density-compact-size: 1.875rem; } @supports (color: color-mix(in oklch, red, white)) { .soft { --ui-bg: color-mix(in oklab, var(--surface) var(--soft-bg-mix), var(--intent, var(--neutral))); --ui-border: color-mix( in oklab, var(--surface) var(--soft-border-mix), var(--intent, var(--neutral)) ); --ui-fg: color-mix(in oklab, var(--text), var(--intent, var(--text)) var(--soft-fg-mix)); } .inverted { --ui-hover-bg: color-mix(in oklch, var(--surface-solid) 88%, black); } } :focus { outline: var(--focus-outline); outline-offset: var(--focus-outline-offset); } @supports selector(:focus-visible) { :focus:not(:focus-visible) { outline: none; } } @media print { :root, body { color-scheme: light; background: #fff; color: #000; } h1, h2, h3, h4, h5, h6 { break-after: avoid; } }
|
|
1
|
+
*, *::before, *::after { box-sizing: border-box; } html { -webkit-text-size-adjust: 100%; } body { margin: 0; } button, input, textarea, select { font: inherit; } [hidden]:not([hidden="until-found"]) { display: none !important; } :where(img, svg, video) { max-inline-size: 100%; block-size: auto; } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto !important; } *, *::before, *::after { animation-duration: 0.01ms !important; animation-delay: 0s !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; transition-delay: 0s !important; } } :root { --font-sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; --line-height: 1.5; --line-height-tight: 1.25; --line-height-relaxed: 1.75; --font-weight-normal: 400; --font-weight-medium: 500; --font-weight-semibold: 600; --font-weight-bold: 700; --font-size-xs: 0.8125rem; --font-size-sm: 0.875rem; --font-size-md: 1rem; --font-size-lg: 1.125rem; --gap: 0.75rem; --density-space: var(--space-40); --space-10: 0.25rem; --space-20: 0.5rem; --space-30: 0.75rem; --space-40: 1rem; --space-50: 1.5rem; --space-60: 2rem; --border-width: 1px; --radius-full: 9999px; --shadow: 0 1px 3px rgb(0 0 0 / 0.05), 0 4px 12px rgb(0 0 0 / 0.04); --shadow-popout: 0 0.25rem 0.75rem rgb(0 0 0 / 0.15); --duration: 150ms; --duration-fast: 100ms; --duration-slow: 200ms; --duration-spin: 0.75s; --duration-shimmer: 1.2s; --ease-enter: cubic-bezier(0.2, 0, 0, 1); --ease-exit: cubic-bezier(0.4, 0, 1, 1); --focus-outline-color: currentColor; --focus-outline: calc(var(--border-width) * 2) solid var(--focus-outline-color); --focus-outline-offset: 2px; --focus-ring-width: 3px; --indicator-offset: 14.65%; --backdrop-color: rgb(0 0 0); --backdrop-opacity: 0.45; --backdrop-fill: rgb(0 0 0 / var(--backdrop-opacity)); --bar-height: 0.65rem; --control-size-sm: 2rem; --control-size-md: 2.375rem; --control-size-lg: 2.75rem; --control-size: var(--control-size-md); --control-pad-x: var(--font-size-md); --control-font-size: var(--font-size-md); --choice-control-size: 1.125em; --disabled-opacity: 0.65; } :root, [data-theme] { --state-selected: var(--primary); --state-selected-fg: var(--primary-fg); --state-disabled: var(--text-subtle); --indicator-ring: var(--surface); } :root { --soft-bg-mix: 88%; --soft-border-mix: 65%; --soft-hover-alpha: 12%; --soft-fg-mix: 100%; --icon-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%238a8594' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E"); --icon-check: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'/%3E%3C/svg%3E"); --icon-close: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='6' y1='6' x2='18' y2='18'/%3E%3Cline x1='18' y1='6' x2='6' y2='18'/%3E%3C/svg%3E"); --icon-plus: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='12' y1='5' x2='12' y2='19'/%3E%3Cline x1='5' y1='12' x2='19' y2='12'/%3E%3C/svg%3E"); --viewport-block: 100vh; --viewport-inline: 100vw; --z-sticky: 10; --z-menu: 20; --z-status: 60; } @media (forced-colors: active) { :root, [data-theme] { --state-disabled: GrayText; --disabled-opacity: 1; } } @supports (block-size: 100dvh) { :root { --viewport-block: 100dvh; } } @supports (inline-size: 100vi) { :root { --viewport-inline: 100vi; } } @supports (color: color-mix(in oklch, red, white)) { :root, [data-theme] { --shadow: 0 1px 3px color-mix(in oklch, var(--shadow-color) 5%, transparent), 0 4px 12px color-mix(in oklch, var(--shadow-color) 4%, transparent); --shadow-popout: 0 0.25rem 0.75rem color-mix(in oklch, var(--shadow-color) 15%, transparent); } :root, [data-theme] { --backdrop-fill: color-mix( in oklch, var(--backdrop-color) calc(var(--backdrop-opacity) * 100%), transparent ); } } :root { color-scheme: light dark; } [data-theme="light"] { color-scheme: light; } [data-theme="dark"] { color-scheme: dark; } @supports not (color: light-dark(red, blue)) { :root, [data-theme="light"], [data-theme="dark"] { color-scheme: light; } } :root, [data-theme="light"] { --primary: hsl(268 32% 33%); --primary-fg: hsl(0 0% 98%); --secondary: hsl(16 38% 38%); --secondary-fg: hsl(0 0% 98%); --success: hsl(150 40% 31%); --success-fg: hsl(0 0% 98%); --warning: hsl(36 68% 34%); --warning-fg: hsl(0 0% 98%); --danger: hsl(5 58% 43%); --danger-fg: hsl(0 0% 98%); --neutral: hsl(258 7% 43%); --neutral-fg: hsl(0 0% 98%); --surface: hsl(0 0% 100%); --surface-raised: hsl(0 0% 100%); --surface-subtle: hsl(262 24% 96%); --surface-solid: hsl(261 16% 13%); --text: hsl(261 20% 13%); --text-muted: hsl(259 9% 42%); --text-subtle: hsl(258 7% 57%); --border: hsl(260 15% 88%); --focus: var(--neutral); --focus-ring: hsl(258 7% 43% / 0.3); --hover-overlay: hsl(260 20% 10% / 0.04); --radius: 0.5rem; --radius-sm: 0.25rem; --radius-lg: 0.75rem; --shadow-color: hsl(260 25% 10%); --hgroup-gap: 0.25em; } :root, [data-theme] { --heading: var(--text); --selection-bg: var(--primary); --selection-fg: var(--primary-fg); --focus-ring-shadow: 0 0 0 var(--focus-ring-width) var(--focus-ring); } :where([data-theme]:not([data-theme="light"]):not([data-theme="dark"])) { --primary-soft-fg: initial; --secondary-soft-fg: initial; --success-soft-fg: initial; --warning-soft-fg: initial; --danger-soft-fg: initial; --neutral-soft-fg: initial; } :root, [data-theme="light"], [data-theme="dark"] { --secondary-soft-fg: color-mix(in oklab, var(--text) 10%, var(--secondary) 90%); --success-soft-fg: color-mix(in oklab, var(--text) 15%, var(--success) 85%); --warning-soft-fg: color-mix(in oklab, var(--text) 25%, var(--warning) 75%); --danger-soft-fg: color-mix(in oklab, var(--text) 15%, var(--danger) 85%); --neutral-soft-fg: color-mix(in oklab, var(--text) 20%, var(--neutral) 80%); } :root, [data-theme] { accent-color: var(--primary); } [data-tone="dark"] { --tone-fg: white; } [data-tone="light"] { --tone-fg: var(--text); } @supports (color: color-mix(in oklch, red, white)) { :root, [data-theme] { --focus-ring: color-mix(in oklch, var(--focus) 30%, transparent); } } @supports (color: light-dark(red, blue)) { :root, [data-theme="light"] { --primary: light-dark(hsl(268 32% 33%), hsl(268 42% 76%)); --primary-fg: light-dark(hsl(0 0% 98%), hsl(268 30% 12%)); --secondary: light-dark(hsl(16 38% 38%), hsl(18 45% 72%)); --secondary-fg: light-dark(hsl(0 0% 98%), hsl(18 32% 11%)); --success: light-dark(hsl(150 40% 31%), hsl(150 38% 62%)); --success-fg: light-dark(hsl(0 0% 98%), hsl(150 35% 8%)); --warning: light-dark(hsl(36 68% 34%), hsl(40 62% 64%)); --warning-fg: light-dark(hsl(0 0% 98%), hsl(38 35% 10%)); --danger: light-dark(hsl(5 58% 43%), hsl(4 60% 70%)); --danger-fg: light-dark(hsl(0 0% 98%), hsl(4 35% 10%)); --neutral: light-dark(hsl(258 7% 43%), hsl(258 10% 72%)); --neutral-fg: light-dark(hsl(0 0% 98%), hsl(260 18% 10%)); --surface: light-dark(hsl(0 0% 100%), hsl(263 17% 9%)); --surface-raised: light-dark(hsl(0 0% 100%), hsl(263 15% 12%)); --surface-subtle: light-dark(hsl(262 24% 96%), hsl(263 13% 16%)); --surface-solid: light-dark(hsl(261 16% 13%), hsl(0 0% 96%)); --text: light-dark(hsl(261 20% 13%), hsl(260 12% 95%)); --text-muted: light-dark(hsl(259 9% 42%), hsl(259 9% 67%)); --text-subtle: light-dark(hsl(258 7% 57%), hsl(258 7% 54%)); --border: light-dark(hsl(260 15% 88%), hsl(262 12% 24%)); --focus-ring: light-dark(hsl(258 7% 43% / 0.3), hsl(258 10% 72% / 0.28)); --hover-overlay: light-dark(hsl(260 20% 10% / 0.04), hsl(0 0% 100% / 0.06)); --shadow-color: light-dark(hsl(260 25% 10%), hsl(262 40% 2%)); } } @supports (color: color-mix(in oklch, red, white)) and (color: light-dark(red, blue)) { :root, [data-theme] { --focus-ring: light-dark( color-mix(in oklch, var(--focus) 30%, transparent), color-mix(in oklch, var(--focus) 28%, transparent) ); } } @media (prefers-contrast: more) { :root, [data-theme][data-theme] { --border-width: 2px; --focus-outline-offset: 3px; --focus-ring-width: 4px; --text-muted: var(--text); --text-subtle: var(--text); } } body { background: var(--surface); color: var(--text); font-family: var(--font-sans); line-height: var(--line-height); } a { color: var(--link, inherit); text-underline-offset: 0.18em; } a:hover { text-decoration-thickness: 0.12em; } :where(h1, h2, h3, h4, h5, h6) { color: var(--heading, inherit); line-height: var(--line-height-tight); text-wrap: balance; } hgroup { display: grid; gap: var(--hgroup-gap); } hgroup > * { margin-block: 0; } hr { border: 0; border-block-start: var(--border-width) solid var(--border); margin-block: var(--hr-space, var(--space-60)); margin-inline: 0; } code, kbd, samp, pre { font-family: var(--font-mono); } pre { overflow-x: auto; } small { font-size: 0.875em; } ::selection { background: var(--selection-bg, var(--primary)); color: var(--selection-fg, var(--primary-fg)); } .primary { --intent: var(--primary); --intent-fg: var(--primary-fg); --intent-soft-fg: var(--primary-soft-fg); } .secondary { --intent: var(--secondary); --intent-fg: var(--secondary-fg); --intent-soft-fg: var(--secondary-soft-fg); } .success { --intent: var(--success); --intent-fg: var(--success-fg); --intent-soft-fg: var(--success-soft-fg); } .warning { --intent: var(--warning); --intent-fg: var(--warning-fg); --intent-soft-fg: var(--warning-soft-fg); } .danger { --intent: var(--danger); --intent-fg: var(--danger-fg); --intent-soft-fg: var(--danger-soft-fg); } .neutral { --intent: var(--neutral); --intent-fg: var(--neutral-fg); --intent-soft-fg: var(--neutral-soft-fg); } .intent-color { color: var(--intent); } .solid { --ui-bg: var(--intent, var(--neutral)); --ui-fg: var(--intent-fg, var(--neutral-fg)); --ui-border: transparent; } .soft { --ui-bg: var(--surface-subtle); --ui-border: var(--border); --ui-fg: var(--intent, var(--text)); } .outline { --ui-bg: transparent; --ui-fg: var(--intent, currentColor); --ui-border: var(--intent, currentColor); } .surface { --ui-bg: var(--surface); --ui-fg: var(--intent, var(--text)); --ui-border: var(--border); } .inverted { --ui-bg: var(--surface-solid); --ui-fg: var(--surface); --ui-border: transparent; --ui-hover-bg: var(--surface-solid); --heading: var(--surface); --busy-overlay-bg: var(--surface-solid); background: var(--ui-bg); color: var(--ui-fg); border-color: var(--ui-border); } .compact { --gap: var(--space-20); --density-space: var(--space-20); --control-size: var(--control-size-sm); } .spacious { --gap: var(--space-50); --density-space: var(--space-50); --control-size: var(--control-size-lg); } :where( .btn, .input, .textarea, .select, .color, .file, .field, .choice, .input-icon, .otp, .join, .pagination, .tabs, .tab, .flyout ).sm { --control-size: var(--control-size-sm); --control-font-size: var(--font-size-sm); --control-pad-x: var(--font-size-sm); } :where( .btn, .input, .textarea, .select, .color, .file, .field, .choice, .input-icon, .otp, .join, .pagination, .tabs, .tab, .flyout ).lg { --control-size: var(--control-size-lg); --control-font-size: var(--font-size-lg); --control-pad-x: var(--font-size-lg); } @supports (color: color-mix(in oklch, red, white)) { .soft { --ui-bg: color-mix(in oklab, var(--surface) var(--soft-bg-mix), var(--intent, var(--neutral))); --ui-border: color-mix( in oklab, var(--surface) var(--soft-border-mix), var(--intent, var(--neutral)) ); --ui-fg: var( --intent-soft-fg, color-mix(in oklab, var(--text), var(--intent, var(--text)) var(--soft-fg-mix)) ); } .inverted { --ui-hover-bg: color-mix(in oklch, var(--surface-solid) 88%, black); } } :focus { outline: var(--focus-outline); outline-offset: var(--focus-outline-offset); } @supports selector(:focus-visible) { :focus:not(:focus-visible) { outline: none; } } @media print { :root, body { color-scheme: light; background: #fff; color: #000; } h1, h2, h3, h4, h5, h6 { break-after: avoid; } }
|
|
@@ -10,6 +10,39 @@ Actual CSS makes the accessible path the default where CSS and small runtime enh
|
|
|
10
10
|
- Forced-colors support on controls and visual state indicators.
|
|
11
11
|
- DOM tests for enhancer lifecycle, focus, attributes, and keyboard behavior.
|
|
12
12
|
|
|
13
|
+
## Linters that reject ARIA patterns
|
|
14
|
+
|
|
15
|
+
Measured on Biome 2.5.6, in HTML and JSX alike.
|
|
16
|
+
|
|
17
|
+
`a11y/noNoninteractiveElementToInteractiveRole` reports `<menu role="menu">`,
|
|
18
|
+
and its fix removes the role, which dismantles the composite. It is a port of
|
|
19
|
+
`jsx-a11y/no-noninteractive-element-to-interactive-role` that dropped the source
|
|
20
|
+
rule's default allowlist, so every ARIA composite built on a list is reported —
|
|
21
|
+
`menu`, `listbox`, `tablist`, `menubar`, `tree`, `grid` — even though the source
|
|
22
|
+
documents `<ul role="menu">` as valid. Switch the rule off; a per-element
|
|
23
|
+
suppression would sit above every menu in the markup.
|
|
24
|
+
|
|
25
|
+
`a11y/useSemanticElements` reports `role="group"` on `.join` and
|
|
26
|
+
`.avatar-stack`, suggesting `<fieldset>`. `<fieldset>` groups form controls
|
|
27
|
+
under a `<legend>`; ARIA's `group` is any labelled set of related objects. This
|
|
28
|
+
rule stays on — it correctly catches `<div role="navigation">`,
|
|
29
|
+
`<div role="button">`, `<div role="separator">` — so suppress it on the wrapper
|
|
30
|
+
instead. A wrapper with no label to give should drop the role rather than
|
|
31
|
+
suppress the rule.
|
|
32
|
+
|
|
33
|
+
Two decisions these findings settled:
|
|
34
|
+
|
|
35
|
+
- `.join` keeps `role="group"` rather than `role="toolbar"`, which the rule does
|
|
36
|
+
not report. Toolbar owes arrow-key navigation with a roving tabindex, and
|
|
37
|
+
claiming the role without the behaviour is worse than the false positive.
|
|
38
|
+
- `.menu-separator` is a plain `<hr>`. The `role="separator"` these docs used to
|
|
39
|
+
show is implied by the element, and `a11y/noRedundantRoles` was right to
|
|
40
|
+
reject it. Nothing in the CSS or the runtime read it.
|
|
41
|
+
|
|
42
|
+
Only the menu container is ever reported: `role="menuitem"` sits on a `<button>`
|
|
43
|
+
or `<a>`, never on the `<li>`, and an interactive element is a legitimate role
|
|
44
|
+
host. Tabs put `role="tablist"` on a `<div>`, so they are unaffected.
|
|
45
|
+
|
|
13
46
|
## Author Responsibilities
|
|
14
47
|
|
|
15
48
|
- Name icon-only buttons.
|
|
@@ -5,12 +5,11 @@
|
|
|
5
5
|
The compatibility contract is expressed in browser versions, not ECMAScript
|
|
6
6
|
edition labels. Each tier is defined by what it guarantees.
|
|
7
7
|
|
|
8
|
-
| Actual tier
|
|
9
|
-
|
|
|
10
|
-
| Degraded
|
|
11
|
-
| **Minimal**
|
|
12
|
-
|
|
|
13
|
-
| Recommended | 129+ | 17.5+ | 123+ | Target experience |
|
|
8
|
+
| Actual tier | Firefox | Safari | Chromium | Contract |
|
|
9
|
+
| ----------- | -------: | ------: | -------: | ---------------------------------------------- |
|
|
10
|
+
| Degraded | 78+ | 14+ | 88+ | Semantic HTML + core CSS; JS unsupported |
|
|
11
|
+
| **Minimal** | **125+** | **17+** | **116+** | Full supported Actual experience, including JS |
|
|
12
|
+
| Recommended | 129+ | 17.5+ | 123+ | Target experience |
|
|
14
13
|
|
|
15
14
|
- **Degraded** keeps useful HTML and CSS on older browsers. JavaScript is
|
|
16
15
|
explicitly outside the supported contract there.
|
|
@@ -19,33 +18,43 @@ edition labels. Each tier is defined by what it guarantees.
|
|
|
19
18
|
modern platform primitives available across the whole tier and ships no
|
|
20
19
|
legacy compatibility layer or polyfill.
|
|
21
20
|
|
|
21
|
+
Only the JavaScript floor is expressed by Minimal. CSS degradation is owed to
|
|
22
|
+
Degraded, so a capability being available across Minimal does not make it CSS
|
|
23
|
+
baseline — `check:compat` audits stylesheets against the Degraded floor.
|
|
24
|
+
|
|
22
25
|
Degraded browsers may still run some JavaScript successfully, but that
|
|
23
26
|
behavior is not tested, documented as supported, or preserved when the
|
|
24
27
|
runtime evolves.
|
|
25
28
|
|
|
26
29
|
### Native Popover and the tiers
|
|
27
30
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
Minimal sits exactly where the manual Popover transport lands: Chromium 116,
|
|
32
|
+
Firefox 125, Safari 17. That is what the floor buys, and the whole of what the
|
|
33
|
+
runtime requires:
|
|
34
|
+
|
|
35
|
+
```text
|
|
36
|
+
popover="manual"
|
|
37
|
+
showPopover()
|
|
38
|
+
hidePopover()
|
|
39
|
+
top-layer promotion + closed-state display: none
|
|
40
|
+
```
|
|
32
41
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
| Intermediate | no | `surface.js` |
|
|
38
|
-
| Recommended | yes, core lifecycle | native Popover, or `surface.js` |
|
|
42
|
+
`popovertarget`, `popover="auto"` and `:popover-open` arrive on the same
|
|
43
|
+
engines and are guaranteed too, but the runtime uses none of them — it keeps
|
|
44
|
+
`aria-controls` and drives the lifecycle itself. Newer parts of the API
|
|
45
|
+
(`popover="hint"`, interest invokers) are covered by no tier.
|
|
39
46
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
application targeting iOS should verify the specific pieces it relies on.
|
|
47
|
+
| Actual tier | Manual Popover transport | Lifecycle owner |
|
|
48
|
+
| ----------- | ------------------------ | -------------------------------------------- |
|
|
49
|
+
| Degraded | no | HTML/CSS fallback only |
|
|
50
|
+
| Minimal | yes | `surface.js`, `tooltip.js` |
|
|
51
|
+
| Recommended | yes | native Popover, or `surface.js`/`tooltip.js` |
|
|
46
52
|
|
|
47
|
-
Actual's presentation classes compose with either lifecycle
|
|
48
|
-
|
|
53
|
+
Actual's presentation classes compose with either lifecycle. Both runtimes use
|
|
54
|
+
the transport and never the native lifecycle: `popover="auto"` cannot express
|
|
55
|
+
`data-flyout-auto-close`, and it would put the browser's light dismiss in
|
|
56
|
+
competition with the tooltip's own hover and focus policy. See
|
|
57
|
+
[platform-alignment](platform-alignment.md).
|
|
49
58
|
|
|
50
59
|
## JavaScript philosophy
|
|
51
60
|
|
|
@@ -60,9 +69,9 @@ JavaScript support below Minimal.
|
|
|
60
69
|
|
|
61
70
|
### Abortable listeners
|
|
62
71
|
|
|
63
|
-
Abortable event listeners predate the Minimal floor
|
|
64
|
-
and WebKit (early 2021)
|
|
65
|
-
|
|
72
|
+
Abortable event listeners predate the Minimal floor by a wide margin: Firefox
|
|
73
|
+
86, Chrome 88, and WebKit (early 2021). ([Bugzilla][1], [Chrome][2],
|
|
74
|
+
[WebKit][3])
|
|
66
75
|
|
|
67
76
|
> `addEventListener({ signal })` is a baseline runtime primitive.
|
|
68
77
|
|
|
@@ -72,11 +81,25 @@ that is already native throughout Minimal.
|
|
|
72
81
|
|
|
73
82
|
### Native `<dialog>`
|
|
74
83
|
|
|
75
|
-
|
|
76
|
-
|
|
84
|
+
Native `<dialog>` predates the floor on every engine: Safari 15.4, Firefox 98,
|
|
85
|
+
and Chromium earlier still. ([WebKit][4])
|
|
77
86
|
|
|
78
87
|
> Native dialog is assumed by the runtime; no dialog polyfill is shipped.
|
|
79
88
|
|
|
89
|
+
### The manual Popover transport
|
|
90
|
+
|
|
91
|
+
The floor is set by this capability. `surface.js` and `tooltip.js` both promote
|
|
92
|
+
their element with `popover="manual"` instead of moving it in the DOM, which is
|
|
93
|
+
what keeps a surface — and an explicit tooltip — inside every scope that reaches
|
|
94
|
+
it by inheritance. See [surface-reparenting](surface-reparenting.md).
|
|
95
|
+
|
|
96
|
+
> The manual Popover transport is assumed by the runtime; there is no
|
|
97
|
+
> reparenting fallback.
|
|
98
|
+
|
|
99
|
+
A capability branch was rejected rather than overlooked: the fallback path is
|
|
100
|
+
the defect, so keeping it would leave the bug in place for exactly the browsers
|
|
101
|
+
the branch would have served.
|
|
102
|
+
|
|
80
103
|
### Future-direction invariants
|
|
81
104
|
|
|
82
105
|
1. **Minimal drives JavaScript compatibility; Degraded does not.**
|
|
@@ -120,7 +143,7 @@ different notion from an Actual tier.
|
|
|
120
143
|
| `appearance: base-select` | 2024+ | Native `<select>` |
|
|
121
144
|
| `@starting-style` | 2024+ | Instant appearance |
|
|
122
145
|
| View Transition API | 2024+ | Dialog open/close transition |
|
|
123
|
-
| Popover
|
|
146
|
+
| Popover transport (`manual`) | Baseline 2025 | none — runtime requirement |
|
|
124
147
|
| CSS Anchor Positioning | Future | JS positioning (`@lekoala/floating`) |
|
|
125
148
|
|
|
126
149
|
Core layout and visual tokens work on all Baseline 2023 browsers. Features marked
|
|
@@ -155,6 +178,15 @@ background: color-mix(in oklch, var(--surface) 88%, transparent);
|
|
|
155
178
|
backdrop-filter: blur(1rem);
|
|
156
179
|
```
|
|
157
180
|
|
|
181
|
+
`@supports` is the simplest machine-readable boundary for a structural
|
|
182
|
+
enhancement, not a preferred shape in itself. Where an unsupported selector
|
|
183
|
+
already drops the enhancement cleanly and the baseline stays functional, a
|
|
184
|
+
documented why-comment (the one the guard audit recognises) or a `PROGRESSIVE`
|
|
185
|
+
ledger entry is just as valid. A wrapper that is behaviorally redundant is a
|
|
186
|
+
simplification candidate: improve `check:compat` — for example, let one
|
|
187
|
+
justification cover a contiguous group of rules — rather than let the wrapper
|
|
188
|
+
become a framework convention.
|
|
189
|
+
|
|
158
190
|
### CSS nesting inside @supports
|
|
159
191
|
|
|
160
192
|
The no-nesting rule protects browsers that predate CSS nesting (Chrome 112, Firefox 117, Safari 16.5). A block already gated behind a later feature can't be reached by any browser that lacks nesting, so the inspector-friendliness argument no longer applies — nest freely inside it. `appearance: base-select` (Chrome 133+) is the current example: everything inside that `@supports` block in `custom-select.css` uses `&`-nesting instead of repeating the `.select:not(...)` selector.
|
|
@@ -153,11 +153,11 @@ It does **not** guarantee that arbitrary content will remain usable inside a ver
|
|
|
153
153
|
|
|
154
154
|
A two-column span in a narrow container may still be too small for its content. That is an application composition problem, not something the primitive should guess how to solve.
|
|
155
155
|
|
|
156
|
-
## Gap
|
|
156
|
+
## Gap is not coupled to the track count
|
|
157
157
|
|
|
158
|
-
|
|
158
|
+
A grid that sizes its tracks from `--gap` couples spacing and density: setting the `gap` property on its own can make the two disagree. Actual removed its `.gap-sm` / `.gap-md` / `.gap-lg` utilities before 0.8 for exactly that reason — a local gap is now a direct `gap` declaration, and `--gap` is reserved for rebinding an inherited rhythm.
|
|
159
159
|
|
|
160
|
-
`.column-layout`
|
|
160
|
+
`.column-layout` never had that coupling. Its track template is `repeat(12, minmax(0, 1fr))` and never reads `--gap` for sizing, so `gap` and the track count are independent at every width.
|
|
161
161
|
|
|
162
162
|
This is worth stating explicitly because an author who has read the grid page will reasonably assume the opposite.
|
|
163
163
|
|
|
@@ -1,33 +1,87 @@
|
|
|
1
|
-
#
|
|
1
|
+
# CSS anchor positioning
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Placement stays in JavaScript. This note records why, so the question is not
|
|
4
|
+
reopened on browser-support figures alone.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
## What owns placement today
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
`@lekoala/floating` positions every anchored surface. `surface.js` calls
|
|
9
|
+
`reposition()` for an element anchor and `repositionAt()` for a pointer
|
|
10
|
+
coordinate, both inside an `autoUpdate()` loop.
|
|
8
11
|
|
|
9
|
-
|
|
12
|
+
The positioner is not only a placement engine. It writes outputs the components
|
|
13
|
+
read as ordinary CSS:
|
|
10
14
|
|
|
11
|
-
|
|
15
|
+
```text
|
|
16
|
+
left / top the placement itself
|
|
17
|
+
data-placement resolved side, after flip
|
|
18
|
+
--arrow-x / --arrow-y arrow offset along the panel edge
|
|
19
|
+
--available-height space left for the panel
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`tooltip.css` draws its arrow from `data-placement` and `--arrow-x/-y`;
|
|
23
|
+
`flyout.css` caps its panel with `--available-height`. Replacing the engine
|
|
24
|
+
means replacing those, not only the coordinates.
|
|
12
25
|
|
|
13
|
-
|
|
26
|
+
## The blocker is not support
|
|
14
27
|
|
|
15
|
-
|
|
28
|
+
Support is the obvious gate and the wrong one. The blocking output is the one
|
|
29
|
+
that is not CSS at all — the positioner's return value:
|
|
16
30
|
|
|
17
|
-
```
|
|
18
|
-
.
|
|
19
|
-
position: absolute;
|
|
20
|
-
position-anchor: --trigger-element;
|
|
21
|
-
top: anchor(bottom);
|
|
22
|
-
left: anchor(start);
|
|
23
|
-
|
|
24
|
-
/* Replaces the JS flip/shift logic natively */
|
|
25
|
-
position-try-fallbacks: flip-block, flip-inline;
|
|
26
|
-
}
|
|
31
|
+
```js
|
|
32
|
+
if (!positionSurface(menu)) closeSurface(menu); // surface.js
|
|
27
33
|
```
|
|
28
34
|
|
|
29
|
-
|
|
35
|
+
`reposition()` returns `false` when positioning cannot be performed: a hidden
|
|
36
|
+
floating element, a missing reference rect, an anchor outside the boundary, or
|
|
37
|
+
a document with no browsing context. `surface.js` runs that check on every
|
|
38
|
+
`autoUpdate` tick, so an anchor that leaves its boundary **closes** the surface.
|
|
39
|
+
|
|
40
|
+
CSS anchor positioning offers no equivalent signal. `position-try-fallbacks`
|
|
41
|
+
re-places the box and `position-visibility` can hide it, but hiding is not
|
|
42
|
+
closing: no event fires, `.is-open` stays set, focus is never restored, and the
|
|
43
|
+
Escape stack keeps an entry for a panel the user can no longer see.
|
|
44
|
+
|
|
45
|
+
This is the boundary the manual Popover transport was adopted on, in the other
|
|
46
|
+
direction — a capability takes one responsibility without being handed the
|
|
47
|
+
neighbouring one. See [platform-alignment](platform-alignment.md).
|
|
48
|
+
|
|
49
|
+
## The pointer-coordinate case
|
|
50
|
+
|
|
51
|
+
Context menus open at `clientX`/`clientY`, not at a DOM element
|
|
52
|
+
(`context-menu.js` stores the point, `surface.js` passes it to
|
|
53
|
+
`repositionAt()`). Anchor positioning anchors to elements, so this path needs a
|
|
54
|
+
script layer whatever happens to the element-anchored one. Adoption would split
|
|
55
|
+
the positioner in two rather than delete it.
|
|
56
|
+
|
|
57
|
+
## The one output with a declarative equivalent
|
|
58
|
+
|
|
59
|
+
`--surface-anchor-width` is measured from the trigger's rect on every
|
|
60
|
+
reposition, solely so a flyout can match its trigger's width. `anchor-size()`
|
|
61
|
+
expresses that relationship directly and would remove both the measurement and
|
|
62
|
+
the property.
|
|
63
|
+
|
|
64
|
+
It is not free either: the panel would need an `anchor-name` on its trigger and
|
|
65
|
+
a `position-anchor` written at runtime, which is the same plumbing the
|
|
66
|
+
positioner already does. Worth doing only alongside a wider move, not as a
|
|
67
|
+
standalone saving.
|
|
68
|
+
|
|
69
|
+
## What would unblock the move
|
|
70
|
+
|
|
71
|
+
One of:
|
|
72
|
+
|
|
73
|
+
- a script-observable anchor-visibility signal, so the lifecycle keeps its
|
|
74
|
+
close decision while CSS takes the geometry; or
|
|
75
|
+
- a deliberate contract change — an out-of-view surface hides instead of
|
|
76
|
+
closing. That is a component decision, not a transport swap, and it has the
|
|
77
|
+
same shape as the `popover="auto"` rejection in
|
|
78
|
+
[popover-manual-poc](popover-manual-poc.md). It has been taken for tooltips,
|
|
79
|
+
which have no focus restoration, event or Escape entry to lose — see
|
|
80
|
+
[tooltip-coordinate-space](tooltip-coordinate-space.md).
|
|
81
|
+
|
|
82
|
+
Browser support is a precondition for either, never the trigger on its own.
|
|
30
83
|
|
|
31
|
-
|
|
84
|
+
## Review trigger
|
|
32
85
|
|
|
33
|
-
|
|
86
|
+
Revisit when a script-observable anchor-visibility signal appears, or when a
|
|
87
|
+
project need makes hide-instead-of-close the wanted behavior.
|
|
@@ -47,6 +47,12 @@ and must match `[a-z][a-z0-9-]*`. Order is irrelevant; duplicates are harmless.
|
|
|
47
47
|
<form data-enhance="validation autosubmit"> <!-- two behaviors -->
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
+
`applyEnhancement()` is an application-owned authoring convenience: it writes
|
|
51
|
+
the same tokens and refreshes their containing registrations. It does not watch
|
|
52
|
+
presentation classes or establish a second discovery contract. Walking the
|
|
53
|
+
scope's ancestors finds lifecycle owners even when the query scope is narrower
|
|
54
|
+
than the registration root, without crossing shadow boundaries.
|
|
55
|
+
|
|
50
56
|
## When a token, when a `data-*`
|
|
51
57
|
|
|
52
58
|
> `data-enhance` is a generic opt-in for when HTML does not already provide an unambiguous one.
|
|
@@ -105,10 +111,10 @@ defeating the split.
|
|
|
105
111
|
|
|
106
112
|
## Design decisions recorded
|
|
107
113
|
|
|
108
|
-
**D2 — `selectors.js` drops its 5 discovery entries and keeps
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
114
|
+
**D2 — `selectors.js` drops its 5 discovery entries and keeps 9 state
|
|
115
|
+
entries.** The file's job changes from *CSS↔init bridge* to *state vocabulary
|
|
116
|
+
the runtime writes*. It stays the single file to alias for another CSS
|
|
117
|
+
framework.
|
|
112
118
|
|
|
113
119
|
**D4 — `status.js` uses `[data-status][role="status"]`, with no class and no
|
|
114
120
|
token.** The markup already carries `role="status"` everywhere, so semantics
|
|
@@ -128,8 +134,10 @@ working.
|
|
|
128
134
|
`menuFor()` validates semantically (`menu, [role="menu"]`). A context menu may
|
|
129
135
|
legitimately want the flyout *look* without the flyout *behavior*.
|
|
130
136
|
|
|
131
|
-
**D9 —
|
|
132
|
-
|
|
137
|
+
**D9 — An orphaned surface is torn down by a reaper observing
|
|
138
|
+
`data-actual-surface`.** `prepareSurface()` writes the marker; the reaper
|
|
139
|
+
closes and forgets a surface once it leaves the document. A `fix:`, not part
|
|
140
|
+
of the contract itself.
|
|
133
141
|
|
|
134
142
|
## Token vocabulary
|
|
135
143
|
|
|
@@ -407,7 +407,8 @@ meaning, rather than a stand-in for a coordinate system.
|
|
|
407
407
|
|
|
408
408
|
## Progressive enhancement
|
|
409
409
|
|
|
410
|
-
Container queries are
|
|
410
|
+
Container queries are guaranteed across Minimal, but not across Degraded — and
|
|
411
|
+
Degraded browsers still receive every stylesheet.
|
|
411
412
|
|
|
412
413
|
Structural grids must therefore have a usable fallback without them.
|
|
413
414
|
|
|
@@ -780,6 +781,5 @@ A modifier introducing a second density ladder becomes defensible only when
|
|
|
780
781
|
too conservative while wanting identical balanced-collapse behavior. One
|
|
781
782
|
component in one miscalibrated container is not that evidence. A per-component
|
|
782
783
|
threshold would also collide with the established meaning of `.compact` in
|
|
783
|
-
Actual,
|
|
784
|
-
`.table.compact
|
|
785
|
-
|
|
784
|
+
Actual: UI density, including tighter local padding on `.card.compact` and
|
|
785
|
+
`.table.compact`, never a layout threshold.
|
|
@@ -47,12 +47,17 @@ never about opting in.
|
|
|
47
47
|
- **Base** (`utilities/base.css`) is a curated set of common ergonomic
|
|
48
48
|
shortcuts (`.overflow-auto`, `.gap-none`, `.text-nowrap`, `.px`, `.py`).
|
|
49
49
|
- **Extra** (`utilities/extra.css`) uses explicit property/value names
|
|
50
|
-
(`.overflow-hidden`, `.
|
|
50
|
+
(`.overflow-hidden`, `.padding-context`, `.white-space-normal`).
|
|
51
51
|
|
|
52
52
|
Call them base and extra, after the files. "Optional" is the wrong word for
|
|
53
53
|
either: it reads as a layer an author has to opt into, which is true of every
|
|
54
54
|
module outside `core/` and therefore says nothing about these two.
|
|
55
55
|
|
|
56
|
+
The `.gap-sm` / `.gap-md` / `.gap-lg` scale was removed before 0.8: it set `gap`
|
|
57
|
+
without `--gap`, which can diverge from `.grid-N` track sizing. A local gap is
|
|
58
|
+
now a direct `gap` declaration; `--gap` is reserved for rebinding an inherited
|
|
59
|
+
rhythm.
|
|
60
|
+
|
|
56
61
|
Base may use compact names for a small set of frequent operations (`.px`,
|
|
57
62
|
`.py`, `.mbs`, `.mbe`). Extra uses explicit property/value names and must not
|
|
58
63
|
duplicate an operation already provided by base. The two sets may cover the
|
|
@@ -157,7 +157,44 @@ Popover can take over the top layer without touching placement. Anchor
|
|
|
157
157
|
positioning could later take over placement without touching lifecycle — though
|
|
158
158
|
not yet, because the current positioner also reports *placement validity*, which
|
|
159
159
|
the lifecycle uses to close a surface whose anchor has left the viewport, and
|
|
160
|
-
declarative CSS positioning offers no equivalent signal.
|
|
160
|
+
declarative CSS positioning offers no equivalent signal. See
|
|
161
|
+
[css-anchor](css-anchor.md) for what else the positioner writes and what would
|
|
162
|
+
unblock the move.
|
|
163
|
+
|
|
164
|
+
### Top-layer transport must not imply lifecycle ownership
|
|
165
|
+
|
|
166
|
+
A capability that solves one problem is not an invitation to hand it the
|
|
167
|
+
neighbouring ones. Popover bundles a transport — promotion to the top layer —
|
|
168
|
+
with a lifecycle, and the two are separable: `popover="manual"` supplies the
|
|
169
|
+
transport and nothing else.
|
|
170
|
+
|
|
171
|
+
That separation is what lets Actual use the native top layer while remaining
|
|
172
|
+
the owner of:
|
|
173
|
+
|
|
174
|
+
```text
|
|
175
|
+
.is-open
|
|
176
|
+
Escape ordering
|
|
177
|
+
inside / outside dismissal
|
|
178
|
+
focus restoration
|
|
179
|
+
sheet policy
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
The slide to guard against is "we are using Popover, so we should use its
|
|
183
|
+
lifecycle too". Those are two decisions, taken on different evidence. The
|
|
184
|
+
transport is judged on what it removes; the lifecycle is judged on whether the
|
|
185
|
+
component contract survives it — and for Flyout it does not, because
|
|
186
|
+
`data-flyout-auto-close` defaults to closing on an inside click, which native
|
|
187
|
+
light-dismiss cannot express.
|
|
188
|
+
|
|
189
|
+
The rule cuts the other way too, and that is where the boundary actually falls:
|
|
190
|
+
a capability is not obliged to take on a responsibility just because it looks
|
|
191
|
+
adjacent. A native `::backdrop` is painted for a manual popover and even
|
|
192
|
+
inherits custom properties from its originating element, but its UA
|
|
193
|
+
`pointer-events: none` is not overridable from author CSS — not with
|
|
194
|
+
`!important` either, since a UA `!important` outranks an author one. So it
|
|
195
|
+
cannot do the scrim's second job, absorbing the pointer, and the scrim stays an
|
|
196
|
+
Actual element. Adopting the transport did not make the platform responsible
|
|
197
|
+
for everything the old transport happened to carry.
|
|
161
198
|
|
|
162
199
|
### Ceding the properties a capability is expressed through
|
|
163
200
|
|
|
@@ -184,10 +221,13 @@ This is checkable rather than aspirational: any Actual class that carries a
|
|
|
184
221
|
|
|
185
222
|
The corresponding trap is adopting a capability in a way that adds an axis
|
|
186
223
|
instead of replacing one. A second surface transport living alongside the
|
|
187
|
-
existing one would
|
|
188
|
-
behavior matrix, which is the opposite of the deletion this note asks for.
|
|
189
|
-
|
|
190
|
-
|
|
224
|
+
existing one would have left the DOM-reparenting code in place while doubling
|
|
225
|
+
the behavior matrix, which is the opposite of the deletion this note asks for.
|
|
226
|
+
That is why the top-layer transport was adopted by raising the floor rather
|
|
227
|
+
than behind a capability branch: the axis was replaced, not added. Counting
|
|
228
|
+
lines was the weaker half of the criterion — the transport removed one concept
|
|
229
|
+
and a defect while the file grew by five lines. See
|
|
230
|
+
[popover-manual-poc](popover-manual-poc.md).
|
|
191
231
|
|
|
192
232
|
## Prefer capabilities over browser generations
|
|
193
233
|
|
|
@@ -237,7 +277,13 @@ Actual already has most of the visual building blocks through buttons, actions,
|
|
|
237
277
|
|
|
238
278
|
A toolbar may be a useful small pattern, but its keyboard contract should preferably be built on a shared focus primitive rather than implemented independently.
|
|
239
279
|
|
|
240
|
-
Do not add `.toolbar` merely as another horizontal flex utility.
|
|
280
|
+
Do not add `.toolbar` merely as another horizontal flex utility. If Toolbar is ever added, it is a semantic and keyboard enhancement over existing layout, with no CSS class of its own:
|
|
281
|
+
|
|
282
|
+
```html
|
|
283
|
+
<div class="cluster" role="toolbar" aria-label="Formatting" data-enhance="toolbar">
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
`.cluster` owns the layout, the controls keep their own presentation, `role="toolbar"` carries the semantics, and the enhancement supplies only the roving tab stop — which is `connectFocusGroup` almost exactly as it already exists. APG reserves the pattern for groups of at least three controls; below that the grouping earns nothing.
|
|
241
287
|
|
|
242
288
|
### Menu elements
|
|
243
289
|
|
|
@@ -483,7 +529,7 @@ If several widgets need the same roving-focus behavior, extract one reusable pri
|
|
|
483
529
|
|
|
484
530
|
Consider a small Toolbar pattern only after the shared focus behavior is clear.
|
|
485
531
|
|
|
486
|
-
Its value should come from semantics and interaction, not from duplicating `.cluster`.
|
|
532
|
+
Its value should come from semantics and interaction, not from duplicating `.cluster`. It is also not a reason to consolidate the focus primitive: `focus-group` is a documented public export with a real internal consumer in `menu`, so it does not need a second one to justify itself. Tabs in particular are not the missing consumer — they navigate *and* activate, sync `aria-selected`, toggle panels, and treat a valid panel as a candidacy requirement, so folding them in would trade little code for a subtle coupling between the focused item and the selected one. Wait for a third consumer to reveal the shared abstraction instead of designing it from two.
|
|
487
533
|
|
|
488
534
|
### Switch
|
|
489
535
|
|