actual-css 0.8.0 → 0.9.1

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.
Files changed (91) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/components.json +2 -0
  3. package/dist/actual.css +10 -3
  4. package/dist/actual.full.css +360 -109
  5. package/dist/actual.full.js +1 -1
  6. package/dist/actual.full.min.css +1 -1
  7. package/dist/actual.min.css +1 -1
  8. package/docs/pages/components/alert.md +6 -1
  9. package/docs/pages/components/dialog.md +5 -0
  10. package/docs/pages/components/drawer.md +13 -0
  11. package/docs/pages/components/skeleton.md +3 -1
  12. package/docs/pages/enhancements/flyout.md +16 -2
  13. package/docs/pages/forms/choice-cards.md +5 -4
  14. package/docs/pages/foundations/tokens.md +1 -1
  15. package/docs/pages/guides/progressive-enhancement.md +12 -8
  16. package/package.json +3 -2
  17. package/src/css/components/accordion.css +2 -0
  18. package/src/css/components/actions.css +2 -0
  19. package/src/css/components/alert.css +2 -0
  20. package/src/css/components/app-nav.css +2 -0
  21. package/src/css/components/avatar.css +2 -0
  22. package/src/css/components/badge.css +2 -0
  23. package/src/css/components/breadcrumb.css +2 -0
  24. package/src/css/components/busy.css +2 -0
  25. package/src/css/components/button.css +2 -0
  26. package/src/css/components/card.css +2 -0
  27. package/src/css/components/chat.css +2 -0
  28. package/src/css/components/data-list.css +2 -0
  29. package/src/css/components/drawer.css +55 -27
  30. package/src/css/components/fab.css +2 -0
  31. package/src/css/components/flyout.css +2 -0
  32. package/src/css/components/indicator.css +2 -0
  33. package/src/css/components/join.css +2 -0
  34. package/src/css/components/key.css +2 -0
  35. package/src/css/components/list.css +2 -0
  36. package/src/css/components/meter.css +2 -0
  37. package/src/css/components/modal.css +47 -40
  38. package/src/css/components/navbar.css +2 -0
  39. package/src/css/components/pagination.css +2 -0
  40. package/src/css/components/progress.css +2 -0
  41. package/src/css/components/rating.css +2 -0
  42. package/src/css/components/scrollspy.css +2 -0
  43. package/src/css/components/skeleton.css +17 -4
  44. package/src/css/components/spinner.css +2 -0
  45. package/src/css/components/status-bar.css +2 -0
  46. package/src/css/components/steps.css +2 -0
  47. package/src/css/components/tab.css +2 -0
  48. package/src/css/components/table.css +10 -0
  49. package/src/css/components/tooltip.css +2 -0
  50. package/src/css/core/print.css +3 -0
  51. package/src/css/core/variants.css +7 -3
  52. package/src/css/effects/aura.css +2 -0
  53. package/src/css/forms/choice-card.css +4 -0
  54. package/src/css/forms/choice.css +70 -26
  55. package/src/css/forms/control.css +2 -0
  56. package/src/css/forms/floating-field.css +5 -1
  57. package/src/css/forms/form-actions.css +2 -0
  58. package/src/css/forms/form.css +2 -0
  59. package/src/css/forms/input-icon.css +2 -0
  60. package/src/css/forms/native.css +2 -0
  61. package/src/css/forms/otp.css +10 -0
  62. package/src/css/forms/range.css +2 -0
  63. package/src/css/forms/select.css +2 -0
  64. package/src/css/forms/switch.css +11 -3
  65. package/src/css/forms/validation.css +2 -0
  66. package/src/css/layout/app-layout.css +2 -0
  67. package/src/css/layout/app-shell.css +5 -1
  68. package/src/css/layout/center.css +2 -0
  69. package/src/css/layout/cluster.css +2 -0
  70. package/src/css/layout/column-layout.css +2 -0
  71. package/src/css/layout/frame.css +2 -0
  72. package/src/css/layout/grid.css +2 -0
  73. package/src/css/layout/media.css +2 -1
  74. package/src/css/layout/scroll-snap.css +2 -0
  75. package/src/css/layout/scroll-target.css +2 -0
  76. package/src/css/layout/sidebar-layout.css +2 -0
  77. package/src/css/layout/stack.css +2 -1
  78. package/src/css/layout/switcher.css +2 -0
  79. package/src/css/layout/topbar.css +2 -0
  80. package/src/css/typography/fluid.css +2 -0
  81. package/src/css/typography/lead.css +2 -1
  82. package/src/css/typography/overline.css +2 -0
  83. package/src/css/typography/prose.css +2 -0
  84. package/src/css/utilities/base.css +6 -1
  85. package/src/js/context-menu.js +56 -15
  86. package/src/js/enhancement-loader.js +5 -1
  87. package/src/js/escape.js +11 -1
  88. package/src/js/flyout.js +6 -1
  89. package/src/js/focus.js +8 -3
  90. package/src/js/surface.js +28 -0
  91. package/src/js/tooltip.js +10 -4
@@ -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-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; } }
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, .choice-card, .input-icon, .floating-field, .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, .choice-card, .input-icon, .floating-field, .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, [data-theme][data-theme], body { color-scheme: light; background: #fff; color: #000; } h1, h2, h3, h4, h5, h6 { break-after: avoid; } }
@@ -106,8 +106,13 @@ trailing link or button is content, not anatomy, so it lives in the text column:
106
106
  wrap the row in a `.cluster` and push the two ends apart. Same recipe as any
107
107
  split / spread row — there is no `.alert-action` class.
108
108
 
109
+ The alert aligns its columns to the block start, so a multi-line message keeps
110
+ its icon on the first line. A trailing control makes that row taller than a line
111
+ of text: add `.items-center` to put the icon, the text and the button on one
112
+ center.
113
+
109
114
  ```html demo
110
- <div class="alert surface">
115
+ <div class="alert surface items-center">
111
116
  <i class="ti ti-info-circle alert-icon" aria-hidden="true"></i>
112
117
  <div class="cluster justify-content-space-between">
113
118
  <strong>New message!</strong>
@@ -170,6 +170,11 @@ hidden` (and reserves the scrollbar gutter with `scrollbar-gutter: stable` when
170
170
  one was present). The height cap follows the small viewport, so the panel keeps
171
171
  a stable size while mobile browser chrome expands or collapses.
172
172
 
173
+ The scrolling body is a scroll container, and browsers make one
174
+ keyboard-focusable so it can be scrolled without a pointer — it takes a focus
175
+ ring like any other focusable element, and it can claim initial focus when
176
+ nothing focusable precedes it. Mark the control you want focused `autofocus`.
177
+
173
178
  ```html demo
174
179
  <button class="btn"
175
180
  type="button"
@@ -21,8 +21,21 @@
21
21
  - Use `closedby="none"` when the drawer must not be dismissed by the user at all
22
22
  (e.g. a form with unsaved changes): no backdrop close and no Escape close.
23
23
  - Use `[data-side="end"]` for a right-side drawer.
24
+ - Put the body between a `header` and a `footer`: the panel is a three-row grid
25
+ whose middle region scrolls, so the footer stays reachable however long the
26
+ content gets.
24
27
  - Permanent desktop sidebars belong in layout, not here.
25
28
 
29
+ The panel is `--viewport-block` tall, so it follows the space a mobile browser
30
+ actually leaves on screen as its URL bar collapses. A shell that fakes a
31
+ viewport (an embedded preview) overrides that token rather than the drawer.
32
+
33
+ The body region is a scroll container, and browsers make one keyboard-focusable
34
+ so it can be scrolled without a pointer — it takes a focus ring like any other
35
+ focusable element. `showModal()` focuses the first focusable thing in the
36
+ drawer, so put the close button in the `header` (as both examples do) or mark
37
+ your intended target `autofocus`; otherwise the body claims initial focus.
38
+
26
39
  ```html demo
27
40
  <button class="btn ghost"
28
41
  type="button"
@@ -37,4 +37,6 @@
37
37
  - `--skeleton-highlight` — sweeping highlight color.
38
38
  - `--skeleton-radius` — corner radius; `data-shape` presets override it.
39
39
  - `--skeleton-size` — block size; `data-shape` presets override it.
40
- - `--skeleton-width` — inline size; `data-shape` presets override it.
40
+ - `--skeleton-width` — maximum inline size. A placeholder fills its container
41
+ and stops here, so it never gives an ancestor a width floor. `data-shape`
42
+ presets set it; `avatar` ignores it and stays square.
@@ -506,8 +506,22 @@ delay; a number sets the delay in milliseconds.
506
506
 
507
507
  ## CSS hooks
508
508
 
509
- - `--flyout-inline-size` — panel width.
510
- - `--flyout-max-inline-size` — panel width cap.
509
+ - `--flyout-inline-size` — preferred width. Takes intrinsic values such as
510
+ `max-content`, its default.
511
+ - `--flyout-max-inline-size` — width cap, a `<length-percentage>` the runtime
512
+ also clamps to the viewport. Defaults to `20rem`, independently of the
513
+ preferred width — the same way `width: 42rem; max-width: 20rem` renders at
514
+ 20rem. A panel wider than the cap therefore sets both:
515
+
516
+ ```css
517
+ .mega-panel {
518
+ --flyout-inline-size: 42rem;
519
+ --flyout-max-inline-size: 42rem;
520
+ }
521
+ ```
522
+
523
+ The repetition is the point: the two hooks are different contracts, and only
524
+ the cap may enter the runtime's `min()`.
511
525
  - `--menu-item-size` — minimum row height of `.menu-item`.
512
526
  - `--menu-item-icon-size` — shared leading-column width for icons and checked-state indicators.
513
527
 
@@ -105,7 +105,8 @@ Plain `.check` / `.radio` controls expose two hooks:
105
105
  type. The switch track derives from it too, so a checkbox and a switch on the
106
106
  same line stay proportionate. A `.choice.sm` or `.choice.lg` scales it through
107
107
  the surrounding typography.
108
- - `--choice-control-offset` — top offset that aligns the control with the first
109
- line of a multi-line label. Left unset, each control derives its own offset
110
- from its height and the line box, which keeps a checkbox and a taller switch
111
- level with each other. Setting one constant applies it to both.
108
+ - `--choice-control-nudge` — block correction added to the derived first-line
109
+ offset, for every control in the row. Left unset, each control centres itself
110
+ on the first line's cap height, measured from its own block size — so a
111
+ checkbox and a taller switch stay level with each other, and with the text,
112
+ whatever the font, line-height or control size.
@@ -505,7 +505,7 @@ Because the native select chevron is an SVG background image and cannot inherit
505
505
 
506
506
  Under `prefers-contrast: more`, the user's contrast preference wins over a theme's palette by contract: the core collapses `--text-muted`/`--text-subtle` to `--text` and thickens borders and focus so no theme can defeat a legibility requirement. This is a deliberate decision, not a specificity quirk — the repeated `[data-theme][data-theme]` attribute only outranks a preset theme stylesheet loaded after the core.
507
507
 
508
- Under `forced-colors: active`, the browser automatically remaps `color`, `background`, and `border` properties to system colors. The default theme provides the few necessary system-color overrides for cases the browser cannot handle — `--focus-outline-color`, `--state-selected`/`--state-selected-fg`, `--state-disabled`, `--backdrop-fill`, and decorative shadow removal. Because that rule targets `:root` (which always matches regardless of `data-theme`) and the `[data-theme]` attributes, it reaches every theme. Components should inherit that baseline; keep local forced-color rules only for custom control geometry, native bar parts, disabled states, or selected states that would otherwise be communicated only by background color.
508
+ Under `forced-colors: active`, the browser automatically remaps `color`, `background`, and `border` properties to system colors. The core only adjusts what the remapping cannot express on its own — `--state-disabled` becomes `GrayText` and `--disabled-opacity` becomes `1` — declared on `:root` and `[data-theme]` so every theme boundary follows. Keep local forced-color rules only for custom control geometry, native bar parts, or selected/checked states that would otherwise be communicated only by background color.
509
509
 
510
510
  ## Component mapping
511
511
 
@@ -286,14 +286,14 @@ The runtime dispatches a small set of public `actual:*` events. All are
286
286
  listenable on the document; cancelable events veto the associated action when
287
287
  `preventDefault()` is called.
288
288
 
289
- | Event | Dispatch | `detail` |
290
- | ---------------------- | ------------------------------ | ------------------------------------ |
291
- | `actual:surface-open` | before a surface opens | `{ surface, options }` |
292
- | `actual:context-menu` | before a context menu opens | `{ menu, context, origin, trigger }` |
293
- | `actual:status` | status module, or any code | `{ message, intent, duration }` |
294
- | `actual:invalid` | after a submit was blocked | `{ form, firstInvalid, message }` |
295
- | `actual:dialog-cancel` | before a native cancel close | `{ dialog, sourceEvent }` |
296
- | `actual:dismiss` | after `--dismiss` hid a target | `{ trigger }` |
289
+ | Event | Dispatch | `detail` |
290
+ | ---------------------- | ----------------------------- | ------------------------------------ |
291
+ | `actual:surface-open` | before a surface opens | `{ surface, options }` |
292
+ | `actual:context-menu` | before a context menu opens | `{ menu, context, origin, trigger }` |
293
+ | `actual:status` | status module, or any code | `{ message, intent, duration }` |
294
+ | `actual:invalid` | after a submit was blocked | `{ form, firstInvalid, message }` |
295
+ | `actual:dialog-cancel` | before a native cancel close | `{ dialog, sourceEvent }` |
296
+ | `actual:dismiss` | after a `--dismiss` dismissal | `{ trigger }` |
297
297
 
298
298
  `actual:surface-open`, `actual:context-menu` and `actual:dialog-cancel` are
299
299
  cancelable. `actual:status` both shows and clears the status bar — omit
@@ -343,6 +343,10 @@ applications that unload the module owning the command.
343
343
  The runtime includes a generic `--dismiss` command for hiding any resolved
344
344
  target. It prevents the trigger's default action, sets `hidden`, and emits a
345
345
  bubbling `actual:dismiss` event with the trigger in `event.detail.trigger`.
346
+ On an open surface (flyout, context menu), the surface consumes the event to
347
+ close through its own lifecycle instead of staying hidden — by the time the
348
+ event reaches document listeners, `hidden` has already been lifted again and
349
+ the surface reads as closed, not merely attribute-hidden.
346
350
 
347
351
  ```html demo
348
352
  <section id="notice" class="alert">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "actual-css",
3
- "version": "0.8.0",
3
+ "version": "0.9.1",
4
4
  "description": "Plain CSS component framework with semantic classes, universal variants, small tokens, theme hooks, and progressive enhancements.",
5
5
  "type": "module",
6
6
  "packageManager": "bun@1.4.2",
@@ -124,6 +124,7 @@
124
124
  "report:theme-contrast": "bun scripts/report-theme-contrast.js",
125
125
  "report:neutral-ramp": "bun scripts/report-neutral-ramp.js",
126
126
  "report:dead-css": "bun scripts/report-dead-css.js",
127
+ "report:overflow": "bun scripts/report-overflow.js",
127
128
  "check:architecture": "bun scripts/check-css-architecture.js",
128
129
  "build:dist": "bun scripts/build-dist.js",
129
130
  "build:js": "bun scripts/build-js.js",
@@ -145,7 +146,7 @@
145
146
  "check:sync": "bun scripts/check-sync.js",
146
147
  "test": "bun test",
147
148
  "test:changed": "bun test --changed=master",
148
- "build:all": "bun run lint && bun run build:dist && bun run build:js && bun run format:docs && bun run build:docs && bun run check:docs && bun run check:doc-classes && bun run check:compat && bun run check:css-api && bun run check:color-space && bun run check:architecture && bun run check:enhance && bun run generate:reserved && bun run check:reserved && bun run generate:catalog && bun run check:catalog && bun run check:sync && bun run check:links && bun run check:templates && bun run check:markup && bun run test && bun run build:size",
149
+ "build:all": "bun run lint && bun run build:dist && bun run build:js && bun run build:size && bun run format:docs && bun run build:docs && bun run check:docs && bun run check:doc-classes && bun run check:compat && bun run check:css-api && bun run check:color-space && bun run check:architecture && bun run check:enhance && bun run generate:reserved && bun run check:reserved && bun run generate:catalog && bun run check:catalog && bun run check:sync && bun run check:links && bun run check:templates && bun run check:markup && bun run test",
149
150
  "prepublishOnly": "bun run build:all"
150
151
  },
151
152
  "devDependencies": {
@@ -1,6 +1,8 @@
1
1
  /*
2
2
  * Accordion disclosures built on native details and summary.
3
3
  *
4
+ * Docs: docs/pages/components/accordion.md
5
+ *
4
6
  * Keeps the clickable summary affordance aligned and usable without JavaScript
5
7
  * or demo styles. Decorative palettes and custom markers belong outside the
6
8
  * base component.
@@ -1,6 +1,8 @@
1
1
  /*
2
2
  * Actions — a horizontal action-button cluster.
3
3
  *
4
+ * Docs: docs/pages/patterns/actions.md
5
+ *
4
6
  * Composes with .btn (or any inline element) and feeds layout via the shared
5
7
  * --gap token.
6
8
  *
@@ -1,6 +1,8 @@
1
1
  /*
2
2
  * Alerts for inline feedback and status messages.
3
3
  *
4
+ * Docs: docs/pages/components/alert.md
5
+ *
4
6
  * Keeps alert defaults local while consuming the shared --ui-* variant
5
7
  * plumbing for emphasis.
6
8
  *
@@ -3,6 +3,8 @@
3
3
  * and item states; app-layout owns whether that surface is a bottom bar or
4
4
  * labelled side navigation.
5
5
  *
6
+ * Docs: docs/pages/components/app-navigation.md
7
+ *
6
8
  * Public hooks:
7
9
  * --app-nav-size
8
10
  * --app-nav-gap
@@ -1,6 +1,8 @@
1
1
  /*
2
2
  * Avatars for initials, images, links, and buttons.
3
3
  *
4
+ * Docs: docs/pages/components/avatar.md
5
+ *
4
6
  * The visual box stays the same across semantic elements. Grouping and status
5
7
  * dots (an empty .badge child) compose around the avatar without changing its
6
8
  * meaning.
@@ -1,6 +1,8 @@
1
1
  /*
2
2
  * Badges for compact labels, counts, and state hints.
3
3
  *
4
+ * Docs: docs/pages/components/badge.md
5
+ *
4
6
  * Soft by default. .solid for counters and notification badges. The dot
5
7
  * (.badge:empty) stays solid regardless — a soft dot is pointless.
6
8
  * Shared .outline variant stays available.
@@ -1,6 +1,8 @@
1
1
  /*
2
2
  * Breadcrumb — current-location trail from the site root.
3
3
  *
4
+ * Docs: docs/pages/components/breadcrumb.md
5
+ *
4
6
  * Inline list separated by "/". Mark the current page with `aria-current="page"`
5
7
  * either on the item or on the current link.
6
8
  */
@@ -1,6 +1,8 @@
1
1
  /*
2
2
  * Busy overlay — non-button loading state.
3
3
  *
4
+ * Docs: docs/pages/components/spinner.md
5
+ *
4
6
  * A non-button element with aria-busy="true" and a direct last-child .spinner
5
7
  * shows that spinner as a centered overlay over a faded surface. Buttons keep
6
8
  * their spinners inline (e.g. .btn[aria-busy] > .spinner stays inline-flex).
@@ -1,6 +1,8 @@
1
1
  /*
2
2
  * Buttons for actions and button-like links.
3
3
  *
4
+ * Docs: docs/pages/components/button.md
5
+ *
4
6
  * .btn is primarily for actionable elements: <button>, <a href>,
5
7
  * input[type="button"|"submit"|"reset"].
6
8
  *
@@ -1,6 +1,8 @@
1
1
  /*
2
2
  * Cards provide a neutral raised surface for grouped content.
3
3
  *
4
+ * Docs: docs/pages/components/card.md
5
+ *
4
6
  * Surface, border, radius, padding, density, and elevation stay local here.
5
7
  * Direct header/footer children are structural slots; a direct footer is
6
8
  * anchored to the card's bottom edge when extra block space is available (so
@@ -1,6 +1,8 @@
1
1
  /*
2
2
  * Message rows composed from an optional avatar, metadata, and bubble.
3
3
  *
4
+ * Docs: docs/pages/components/chat.md
5
+ *
4
6
  * Public hooks:
5
7
  * --chat-gap
6
8
  * --chat-max-size
@@ -1,6 +1,8 @@
1
1
  /*
2
2
  * Data list — term/value pairs for the properties of a single object.
3
3
  *
4
+ * Docs: docs/pages/components/data-list.md
5
+ *
4
6
  * Expects alternating <dt>/<dd> children: one term, one value per property.
5
7
  * A plain <dl> handles richer relationships (several terms or descriptions)
6
8
  * and stays untouched by this component.
@@ -1,6 +1,12 @@
1
1
  /*
2
2
  * Drawer — modal side-sheet built on the native dialog element.
3
3
  *
4
+ * Docs: docs/pages/components/drawer.md
5
+ *
6
+ * Open with a native command button:
7
+ * command="show-modal" commandfor="<drawer-id>".
8
+ * For stacked navigation, compose with .nav-list from navbar.css.
9
+ *
4
10
  * Top-layer rendering, ::backdrop, Escape, and modal focus come from the
5
11
  * native dialog. CSS owns progressive enter/exit motion where browsers support
6
12
  * discrete display/overlay transitions. The drawer is a layout shell only —
@@ -13,10 +19,11 @@
13
19
  *
14
20
  * Internal:
15
21
  * --drawer-closed-translate (side/RTL positioning state)
22
+ * --drawer-focus-bleed
16
23
  * --drawer-motion-duration (shared by panel and ::backdrop so exit duration
17
24
  * changes in one place; entry state redefines it longer)
18
25
  */
19
- dialog.drawer {
26
+ :where(dialog).drawer {
20
27
  --drawer-size: 20rem;
21
28
  --drawer-pad: var(--space-40);
22
29
  --drawer-closed-translate: -100%;
@@ -26,15 +33,24 @@ dialog.drawer {
26
33
 
27
34
  /* Keep the surface anchored to the viewport in the top layer. This also
28
35
  provides the containing block for the absolutely positioned close button.
29
- A percentage height resolves against the fixed containing viewport, while
30
- dynamic viewport units can be stale in embedded previews. */
36
+
37
+ The panel reaches both viewport edges, so it has to follow the space that
38
+ is actually on screen: --viewport-block resolves to 100dvh and tracks a
39
+ mobile browser's collapsing URL bar. A percentage resolves against the
40
+ layout viewport instead, which those browsers size as if the bar were
41
+ hidden — that put the footer below the fold for as long as it showed. The
42
+ modal pins the small viewport for the opposite reason: it floats, so a
43
+ stable height beats a panel that breathes while the bar moves.
44
+
45
+ An embedded preview that fakes a viewport overrides --viewport-block on
46
+ its own shell; the component does not degrade to a percentage for it. */
31
47
  position: fixed;
32
48
  inset-block-start: 0;
33
49
 
34
50
  inline-size: min(calc(var(--viewport-inline) * 0.9), var(--drawer-size));
35
- block-size: 100%;
51
+ block-size: var(--viewport-block);
36
52
  max-inline-size: none;
37
- max-block-size: 100%;
53
+ max-block-size: var(--viewport-block);
38
54
  margin: 0;
39
55
  padding: var(--drawer-pad);
40
56
  border: 0;
@@ -50,7 +66,7 @@ dialog.drawer {
50
66
  transform var(--drawer-motion-duration) var(--ease-exit);
51
67
  }
52
68
 
53
- dialog.drawer[open] {
69
+ :where(dialog).drawer[open] {
54
70
  --drawer-motion-duration: var(--duration-slow);
55
71
  display: grid;
56
72
  grid-template-rows: auto minmax(0, 1fr) auto;
@@ -65,7 +81,7 @@ dialog.drawer[open] {
65
81
  /* A form is the drawer's content wrapper when actions need method="dialog".
66
82
  Span the drawer tracks so the form can keep the header at the top, let the
67
83
  body absorb the available height, and keep the footer at the bottom. */
68
- dialog.drawer > form {
84
+ :where(dialog).drawer > form {
69
85
  display: grid;
70
86
  grid-row: 1 / -1;
71
87
  grid-template-rows: auto minmax(0, 1fr) auto;
@@ -74,7 +90,7 @@ dialog.drawer > form {
74
90
  }
75
91
 
76
92
  /* The shared backdrop token keeps drawer and modal scrims consistent. */
77
- dialog.drawer::backdrop {
93
+ :where(dialog).drawer::backdrop {
78
94
  background: var(--backdrop-fill);
79
95
  opacity: 0;
80
96
  /* Scrim curve stays neutral `ease`; only the panel's character curves
@@ -82,7 +98,7 @@ dialog.drawer::backdrop {
82
98
  transition: opacity var(--drawer-motion-duration) ease;
83
99
  }
84
100
 
85
- dialog.drawer[data-side="end"] {
101
+ :where(dialog).drawer[data-side="end"] {
86
102
  --drawer-closed-translate: 100%;
87
103
 
88
104
  margin-inline-start: auto;
@@ -91,29 +107,41 @@ dialog.drawer[data-side="end"] {
91
107
  border-inline-start: var(--border-width) solid var(--border);
92
108
  }
93
109
 
94
- dialog.drawer[open]::backdrop {
110
+ :where(dialog).drawer[open]::backdrop {
95
111
  opacity: 1;
96
112
  }
97
113
 
98
114
  @starting-style {
99
- dialog.drawer[open] {
115
+ :where(dialog).drawer[open] {
100
116
  opacity: 0;
101
117
  transform: translateX(var(--drawer-closed-translate));
102
118
  }
103
119
 
104
- dialog.drawer[open]::backdrop {
120
+ :where(dialog).drawer[open]::backdrop {
105
121
  opacity: 0;
106
122
  }
107
123
  }
108
124
 
109
- /* Scrollable navigation region inside the drawer. */
110
- dialog.drawer nav {
125
+ /* The body region absorbs the panel's height and scrolls. A drawer is a
126
+ full-height shell by definition, so it always scrolls its body — unlike the
127
+ modal, which sizes to its content and opts in with .scrollable. Header and
128
+ footer keep their own tracks; .drawer-close is named because a generic
129
+ exclusion list is easier to trust than its `position: absolute`. */
130
+ :where(dialog).drawer > :not(:is(header, footer, form, .drawer-close)),
131
+ :where(dialog).drawer > form > :not(:is(header, footer, .drawer-close)) {
132
+ --drawer-focus-bleed: calc(var(--focus-outline-offset) + var(--border-width) * 2);
133
+
111
134
  min-block-size: 0;
112
- overflow-y: auto;
135
+ /* Keep focus paint inside the clip without moving the body off the fixed
136
+ header and footer edges. */
137
+ margin-inline: calc(var(--drawer-focus-bleed) * -1);
138
+ padding-inline: var(--drawer-focus-bleed);
139
+ overflow: auto;
140
+ overscroll-behavior: contain;
113
141
  }
114
142
 
115
143
  @supports (transition-behavior: allow-discrete) {
116
- dialog.drawer {
144
+ :where(dialog).drawer {
117
145
  transition:
118
146
  opacity var(--drawer-motion-duration) var(--ease-exit),
119
147
  transform var(--drawer-motion-duration) var(--ease-exit),
@@ -121,7 +149,7 @@ dialog.drawer nav {
121
149
  display var(--drawer-motion-duration) allow-discrete;
122
150
  }
123
151
 
124
- dialog.drawer[open] {
152
+ :where(dialog).drawer[open] {
125
153
  transition:
126
154
  opacity var(--drawer-motion-duration) var(--ease-enter),
127
155
  transform var(--drawer-motion-duration) var(--ease-enter),
@@ -129,7 +157,7 @@ dialog.drawer nav {
129
157
  display var(--drawer-motion-duration) allow-discrete;
130
158
  }
131
159
 
132
- dialog.drawer::backdrop {
160
+ :where(dialog).drawer::backdrop {
133
161
  transition:
134
162
  opacity var(--drawer-motion-duration) ease,
135
163
  overlay var(--drawer-motion-duration) allow-discrete,
@@ -143,40 +171,40 @@ dialog.drawer nav {
143
171
  directionality and handles nested direction changes; the explicit ltr rules
144
172
  neutralize an outer RTL fallback selector. Each specificity group keeps the
145
173
  fallback before the :dir() rules. */
146
- [dir="rtl"] dialog.drawer {
174
+ [dir="rtl"] :where(dialog).drawer {
147
175
  --drawer-closed-translate: 100%;
148
176
  }
149
177
 
150
- dialog.drawer:dir(ltr) {
178
+ :where(dialog).drawer:dir(ltr) {
151
179
  --drawer-closed-translate: -100%;
152
180
  }
153
181
 
154
- dialog.drawer:dir(rtl) {
182
+ :where(dialog).drawer:dir(rtl) {
155
183
  --drawer-closed-translate: 100%;
156
184
  }
157
185
 
158
- [dir="rtl"] dialog.drawer[data-side="end"] {
186
+ [dir="rtl"] :where(dialog).drawer[data-side="end"] {
159
187
  --drawer-closed-translate: -100%;
160
188
  }
161
189
 
162
- dialog.drawer[data-side="end"]:dir(ltr) {
190
+ :where(dialog).drawer[data-side="end"]:dir(ltr) {
163
191
  --drawer-closed-translate: 100%;
164
192
  }
165
193
 
166
- dialog.drawer[data-side="end"]:dir(rtl) {
194
+ :where(dialog).drawer[data-side="end"]:dir(rtl) {
167
195
  --drawer-closed-translate: -100%;
168
196
  }
169
197
 
170
198
  @media (prefers-reduced-motion: no-preference) {
171
- dialog.drawer.is-static {
199
+ :where(dialog).drawer.is-static {
172
200
  animation: actual-dialog-static var(--duration-slow) ease;
173
201
  }
174
202
  }
175
203
 
176
204
  /* The header reserves inline room so the title never runs under the close
177
205
  control. */
178
- dialog.drawer > header,
179
- dialog.drawer > form > header {
206
+ :where(dialog).drawer > header,
207
+ :where(dialog).drawer > form > header {
180
208
  padding-inline-end: calc(var(--control-size) + var(--space-30));
181
209
  }
182
210
 
@@ -3,6 +3,8 @@
3
3
  * Button owns the trigger's size, shape, and intent; FAB owns placement and
4
4
  * the speed dial's structural open/close markers (--icon-plus / --icon-close).
5
5
  *
6
+ * Docs: docs/pages/components/fab.md
7
+ *
6
8
  * Public hooks:
7
9
  * --fab-offset
8
10
  * --fab-gap
@@ -1,6 +1,8 @@
1
1
  /*
2
2
  * Flyout — floating panel for actions and links.
3
3
  *
4
+ * Docs: docs/pages/enhancements/flyout.md
5
+ *
4
6
  * JavaScript promotes the panel to the top layer and chooses viewport or
5
7
  * document coordinates from its anchor. It also owns state (.is-open), focus,
6
8
  * and keyboard behavior. CSS owns the surfaced panel appearance, item styles,
@@ -1,6 +1,8 @@
1
1
  /*
2
2
  * Indicator — generic corner positioning for overlaid component content.
3
3
  *
4
+ * Docs: docs/pages/components/indicator.md
5
+ *
4
6
  * The container only establishes the containing block. The item owns no
5
7
  * paint, size, or ring so badges and other components keep their own visual
6
8
  * contract.
@@ -1,6 +1,8 @@
1
1
  /*
2
2
  * Join — visually groups adjacent controls into a single unit.
3
3
  *
4
+ * Docs: docs/pages/forms/joined-controls.md
5
+ *
4
6
  * This is a cross-cutting control primitive, not a forms-only component:
5
7
  * it applies equally to inputs, selects, buttons, and button-like links.
6
8
  *
@@ -1,6 +1,8 @@
1
1
  /*
2
2
  * Key — keyboard keys and shortcut tokens for UI chrome.
3
3
  *
4
+ * Docs: docs/pages/components/key.md
5
+ *
4
6
  * Use .key outside prose. Plain kbd inside .prose gets the same visual
5
7
  * treatment from prose.css so authored content stays convenient.
6
8
  *
@@ -3,6 +3,8 @@
3
3
  * Content determines the number of lines; the component only owns the row
4
4
  * geometry, dividers, and supporting-text treatment.
5
5
  *
6
+ * Docs: docs/pages/components/list.md
7
+ *
6
8
  * Public hooks:
7
9
  * --list-item-min-size
8
10
  * --list-item-pad-block
@@ -1,6 +1,8 @@
1
1
  /*
2
2
  * Meter — native and segmented gauges for known-range values.
3
3
  *
4
+ * Docs: docs/pages/components/meter.md
5
+ *
4
6
  * Maps the three standard meter zones (optimum / suboptimum / even-less-good)
5
7
  * to the shared success / warning / danger intent colors.
6
8
  *