actual-css 0.9.0 → 0.9.2

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 (94) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +2 -1
  3. package/components.json +2 -0
  4. package/dist/actual.css +26 -6
  5. package/dist/actual.full.css +260 -74
  6. package/dist/actual.full.js +1 -1
  7. package/dist/actual.full.min.css +1 -1
  8. package/dist/actual.min.css +1 -1
  9. package/docs/pages/components/table.md +3 -2
  10. package/docs/pages/examples/overview.md +1 -0
  11. package/docs/pages/forms/input-policy.md +59 -0
  12. package/docs/pages/forms/validation.md +37 -1
  13. package/docs/pages/foundations/tokens.md +23 -4
  14. package/docs/pages/foundations/typography.md +10 -0
  15. package/docs/pages/guides/introduction.md +2 -1
  16. package/docs/pages/guides/progressive-enhancement.md +15 -10
  17. package/package.json +160 -159
  18. package/src/css/components/accordion.css +2 -0
  19. package/src/css/components/actions.css +2 -0
  20. package/src/css/components/alert.css +2 -0
  21. package/src/css/components/app-nav.css +2 -0
  22. package/src/css/components/avatar.css +2 -0
  23. package/src/css/components/badge.css +2 -0
  24. package/src/css/components/breadcrumb.css +2 -0
  25. package/src/css/components/busy.css +2 -0
  26. package/src/css/components/button.css +2 -0
  27. package/src/css/components/card.css +2 -0
  28. package/src/css/components/chat.css +2 -0
  29. package/src/css/components/data-list.css +2 -0
  30. package/src/css/components/drawer.css +35 -22
  31. package/src/css/components/fab.css +2 -0
  32. package/src/css/components/flyout.css +2 -0
  33. package/src/css/components/indicator.css +2 -0
  34. package/src/css/components/join.css +2 -0
  35. package/src/css/components/key.css +2 -0
  36. package/src/css/components/list.css +2 -0
  37. package/src/css/components/meter.css +2 -0
  38. package/src/css/components/modal.css +47 -40
  39. package/src/css/components/navbar.css +2 -0
  40. package/src/css/components/pagination.css +2 -0
  41. package/src/css/components/progress.css +2 -0
  42. package/src/css/components/rating.css +2 -0
  43. package/src/css/components/scrollspy.css +2 -0
  44. package/src/css/components/skeleton.css +2 -0
  45. package/src/css/components/spinner.css +2 -0
  46. package/src/css/components/status-bar.css +2 -0
  47. package/src/css/components/steps.css +2 -0
  48. package/src/css/components/tab.css +2 -0
  49. package/src/css/components/table.css +2 -0
  50. package/src/css/components/tooltip.css +2 -0
  51. package/src/css/core/base.css +2 -0
  52. package/src/css/core/print.css +3 -0
  53. package/src/css/core/tokens.css +2 -0
  54. package/src/css/core/variants.css +19 -6
  55. package/src/css/effects/aura.css +2 -0
  56. package/src/css/forms/choice-card.css +4 -0
  57. package/src/css/forms/control.css +2 -0
  58. package/src/css/forms/floating-field.css +5 -1
  59. package/src/css/forms/form-actions.css +2 -0
  60. package/src/css/forms/form.css +8 -0
  61. package/src/css/forms/input-icon.css +2 -0
  62. package/src/css/forms/native.css +2 -0
  63. package/src/css/forms/otp.css +10 -0
  64. package/src/css/forms/range.css +2 -0
  65. package/src/css/forms/select.css +2 -0
  66. package/src/css/forms/switch.css +2 -0
  67. package/src/css/forms/validation.css +2 -0
  68. package/src/css/layout/app-layout.css +2 -0
  69. package/src/css/layout/app-shell.css +5 -1
  70. package/src/css/layout/center.css +2 -0
  71. package/src/css/layout/cluster.css +2 -0
  72. package/src/css/layout/column-layout.css +2 -0
  73. package/src/css/layout/frame.css +2 -0
  74. package/src/css/layout/grid.css +2 -0
  75. package/src/css/layout/media.css +2 -1
  76. package/src/css/layout/scroll-snap.css +2 -0
  77. package/src/css/layout/scroll-target.css +2 -0
  78. package/src/css/layout/sidebar-layout.css +2 -0
  79. package/src/css/layout/stack.css +2 -1
  80. package/src/css/layout/switcher.css +2 -0
  81. package/src/css/layout/topbar.css +2 -0
  82. package/src/css/typography/fluid.css +2 -0
  83. package/src/css/typography/lead.css +2 -1
  84. package/src/css/typography/overline.css +2 -0
  85. package/src/css/typography/prose.css +2 -0
  86. package/src/css/utilities/base.css +6 -1
  87. package/src/js/context-menu.js +56 -15
  88. package/src/js/enhancement-loader.js +5 -1
  89. package/src/js/escape.js +11 -1
  90. package/src/js/flyout.js +6 -1
  91. package/src/js/focus.js +8 -3
  92. package/src/js/surface.js +28 -0
  93. package/src/js/tooltip.js +10 -4
  94. package/src/js/validation.js +31 -0
@@ -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; --font-width: 100%; --font-width-dense: 95%; --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); font-stretch: var(--font-width); 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); font-stretch: var(--font-width-dense); } .spacious { --gap: var(--space-50); --density-space: var(--space-50); --control-size: var(--control-size-lg); font-stretch: var(--font-width); } :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; } }
@@ -16,7 +16,7 @@
16
16
  | ---------------- | --------- | ------------------------------------------------------------ |
17
17
  | `.table-wrap` | Component | Scroll container and outer chrome (surface, border, radius). |
18
18
  | `.table` | Component | The table itself; row and cell rules. |
19
- | `.table.compact` | Density | Tighter cell padding; typography is unchanged. |
19
+ | `.table.compact` | Density | Tighter cell padding; font size and icons unchanged. |
20
20
 
21
21
  ## Basic usage
22
22
 
@@ -101,7 +101,8 @@ table width before the wrapper scrolls horizontally.
101
101
  ## Compact tables
102
102
 
103
103
  `.compact` lowers cell padding and establishes compact density for controls or
104
- spacing helpers inside cells. Typography and icons stay unchanged. It is the
104
+ spacing helpers inside cells. Font size and icons stay unchanged; the framework's
105
+ denser font width may apply when the typeface supports it. It is the
105
106
  same vocabulary as `.card.compact`.
106
107
 
107
108
  ```html demo
@@ -30,6 +30,7 @@ Whole pages, composed the way a real product would.
30
30
  - [Settings modal](../../demo/templates/settings-modal.html) - two-pane preferences dialog, with the local CSS it needs audited rule by rule
31
31
  - [Workspace settings](../../demo/templates/settings-pricing.html) - plans with visible native choices, preferences and a local segmented tab treatment
32
32
  - [Card compositions](../../demo/templates/card-compositions.html) - editorial, team, sign-in and horizontal cards composed from existing primitives
33
+ - [Register form](../../demo/templates/register-form.html) - registration page with choice cards, masks, the password toggle, `tel-prefix` validation, an OTP verification dialog, and status feedback
33
34
 
34
35
  ### Layout references
35
36
 
@@ -2,6 +2,8 @@
2
2
 
3
3
  > Native input intent first. Actual's runtime filters values only when explicitly asked, and fixed-shape masks stay opt-in through `data-mask`.
4
4
 
5
+ **Related terms:** telephone, phone, tel-prefix.
6
+
5
7
  `inputmode` changes the virtual keyboard but does not constrain the value. Actual
6
8
  does not filter plain `inputmode` fields by default because numeric entry often
7
9
  needs temporary, signed, grouped, or locale-specific values while the user types.
@@ -89,3 +91,60 @@ Use `data-mask` when the input has a fixed shape. Tokens are `9` for a digit,
89
91
 
90
92
  Masks enforce shape, not domain validity. Use application validation when a date
91
93
  must reject impossible values such as `2026-13-40`.
94
+
95
+ ### Telephone input
96
+
97
+ Do not mask telephone numbers by default. Phone formatting varies by country,
98
+ device, copied source, and user convention. Preserve the entered value, apply
99
+ only coarse client-side policy, and normalize or validate the actual number on
100
+ the server.
101
+
102
+ `tel-prefix` is that coarse policy: a local number passes, and an explicit
103
+ international number must match one of the allowed prefixes. It analyzes a copy
104
+ of the value and never rewrites it.
105
+
106
+ ```html demo
107
+ <form class="needs-validation stack" data-enhance="validation" novalidate>
108
+ <label class="field">
109
+ <span class="field-label">Phone</span>
110
+ <input
111
+ class="input"
112
+ type="tel"
113
+ name="phone"
114
+ inputmode="tel"
115
+ autocomplete="tel"
116
+ data-validation-rules="tel-prefix +32"
117
+ aria-describedby="phone-help phone-error"
118
+ />
119
+ <span class="field-help" id="phone-help">
120
+ Local number or +32 international number.
121
+ </span>
122
+ <span class="field-error" id="phone-error">
123
+ Use a local number or an allowed international prefix.
124
+ </span>
125
+ </label>
126
+
127
+ <div class="form-actions">
128
+ <button class="btn primary" type="submit">Submit</button>
129
+ <button class="btn neutral outline" type="reset">Reset</button>
130
+ </div>
131
+ </form>
132
+ ```
133
+
134
+ | Input | `tel-prefix +32` |
135
+ | -------------------- | ---------------- |
136
+ | `0470 12 34 56` | passes |
137
+ | `02/123.45.67` | passes |
138
+ | `+32 470 12 34 56` | passes |
139
+ | `0032 470 12 34 56` | passes |
140
+ | `(+32) 470 12 34 56` | passes |
141
+ | `0033 6 12 34 56 78` | fails |
142
+ | `+33 6 12 34 56 78` | fails |
143
+
144
+ A local number passes in any usual notation, but its digits are required: once
145
+ the separators are ignored, letters and stray symbols fail, so a pasted word is
146
+ never read as a local number. An explicit international number is normalized
147
+ for comparison — `0032…`, `(+32)…`, and `+32 (0)…` all read as `+32` — and a
148
+ foreign prefix fails. With no prefix, only local input passes. This is an origin
149
+ policy, not phone validation: region detection, number semantics, and E.164
150
+ normalization stay on the server. See [validation](validation.md).
@@ -2,7 +2,7 @@
2
2
 
3
3
  > Inline error feedback that respects assistive technology and form state.
4
4
 
5
- **Related terms:** inline validation, field error.
5
+ **Related terms:** inline validation, field error, telephone input.
6
6
 
7
7
  ## Class reference
8
8
 
@@ -162,10 +162,46 @@ Custom rules live in `data-validation-rules` as a comma-separated list; each rul
162
162
  - `number` — Value is empty or a valid `Number`.
163
163
  - `digits` — Value is empty or ASCII digits only.
164
164
  - `alnum` — Value is empty or letters and digits only.
165
+ - `starts-with <token>…` — Value must begin with one of the literal tokens (`starts-with INV- CR-`). The value is trimmed before comparison; the match is case-sensitive.
166
+ - `ends-with <token>…` — Value must end with one of the literal tokens (`ends-with .pdf .csv`). Trimmed, case-sensitive.
165
167
  - `date` — Value is a valid date in `yyyy-mm-dd`, `dd/mm/yyyy`, `mm/dd/yyyy`, `dd.mm.yyyy`, `mm.dd.yyyy`, `dd-mm-yyyy`, or `mm-dd-yyyy` shape; rejects non-existent dates like `2026-02-29`. ISO remains the recommended wire format because servers can parse it without guessing.
168
+ - `tel-prefix <prefix>…` — A local number (its digits, with the usual separators `space ( ) . / -` ignored) passes; an explicit international number is normalized (`00` to `+`) and must match one of the literal prefixes (`tel-prefix +32`, `tel-prefix +32 +33 +352`). A value that is not dialing digits fails. With no prefix, only local input passes. The entered value is never modified.
169
+
170
+ `starts-with` and `ends-with` options are literal tokens: no quoting, no escaping, and a token cannot contain whitespace. Without a token they fail rather than passing silently. `tel-prefix` without a prefix is instead a deliberate local-only policy.
166
171
 
167
172
  Add your own with `FormValidator.registerRule(name, (value, el, ...opts) => boolean)`. The built-in rules treat empty values as valid so optional fields stay optional; custom rules should do the same when that behavior is wanted.
168
173
 
174
+ ```html demo
175
+ <form class="needs-validation stack" data-enhance="validation" novalidate>
176
+ <label class="field">
177
+ <span class="field-label">Invoice reference</span>
178
+ <input class="input" name="reference"
179
+ data-validation-rules="starts-with INV- CR-"
180
+ aria-describedby="reference-help reference-error"
181
+ required />
182
+ <span class="field-help" id="reference-help">Start with INV- or CR-.</span>
183
+ <span class="field-error" id="reference-error">Use an INV- or CR- reference.</span>
184
+ </label>
185
+
186
+ <label class="field">
187
+ <span class="field-label">Attachment filename</span>
188
+ <input class="input" name="attachment"
189
+ data-validation-rules="ends-with .pdf .csv"
190
+ aria-describedby="attachment-help attachment-error"
191
+ required />
192
+ <span class="field-help" id="attachment-help">A .pdf or .csv filename.</span>
193
+ <span class="field-error" id="attachment-error">Choose a .pdf or .csv file.</span>
194
+ </label>
195
+
196
+ <div class="form-actions">
197
+ <button class="btn primary" type="submit">Submit</button>
198
+ <button class="btn neutral outline" type="reset">Reset</button>
199
+ </div>
200
+ </form>
201
+ ```
202
+
203
+ More specific telephone policies — determining the actual region, validating number semantics, or producing E.164 — belong in application or server code, registered through `FormValidator.registerRule()`.
204
+
169
205
  The `date` rule pairs naturally with `data-mask` — the mask structures input, the rule validates meaning. Same rule, different formats:
170
206
 
171
207
  ```html demo
@@ -211,6 +211,8 @@ Typography tokens cover the document baseline and common component needs.
211
211
  :root {
212
212
  --font-sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
213
213
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
214
+ --font-width: 100%;
215
+ --font-width-dense: 95%;
214
216
 
215
217
  --line-height: 1.5;
216
218
  --font-weight-normal: 400;
@@ -264,24 +266,41 @@ Disabled controls and disabled-prone components read `--disabled-opacity` for th
264
266
 
265
267
  ### Density
266
268
 
267
- Density contexts swap shared spacing and geometry:
269
+ Density contexts swap shared spacing and geometry, plus a slight typographic
270
+ width bonus:
268
271
 
269
272
  ```css
273
+ :root {
274
+ --font-width: 100%;
275
+ --font-width-dense: 95%;
276
+ }
277
+
270
278
  .compact {
271
279
  --gap: var(--space-20);
272
280
  --density-space: var(--space-20);
273
281
  --control-size: var(--control-size-sm);
282
+ font-stretch: var(--font-width-dense);
274
283
  }
275
284
 
276
285
  .spacious {
277
286
  --gap: var(--space-50);
278
287
  --density-space: var(--space-50);
279
288
  --control-size: var(--control-size-lg);
289
+ font-stretch: var(--font-width);
280
290
  }
281
291
  ```
282
292
 
283
- Density covers spacing and geometry only — it never touches typography or icon
284
- size. Participation is opt-in and selective: controls consume `--control-size`
293
+ Density never changes font size or icon size — it may only use the denser font
294
+ width. `font-stretch` uses a variable `wdth` axis when available, or
295
+ participates in normal width-face matching for families that expose multiple
296
+ widths. Layout must never depend on the narrowing being available. Compact
297
+ density may slightly narrow variable typefaces when they support a width axis.
298
+ This is a density enhancement, not an overflow strategy: never condense a
299
+ component just because its label overflows at some viewport — that hides a
300
+ composition problem. A theme whose typeface has no useful width axis opts out
301
+ with `--font-width-dense: 100%`.
302
+
303
+ Participation is opt-in and selective: controls consume `--control-size`
285
304
  for geometry; spacing helpers and rhythms consume `--density-space` and `--gap`;
286
305
  components whose size is intrinsic (badge, avatar, spinner, rating, key, prose,
287
306
  inline choice/switch) keep their own scales. A local component size overrides
@@ -505,7 +524,7 @@ Because the native select chevron is an SVG background image and cannot inherit
505
524
 
506
525
  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
526
 
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.
527
+ 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
528
 
510
529
  ## Component mapping
511
530
 
@@ -113,6 +113,16 @@ the document) and `--font-mono` (for `code` / `kbd` / `samp` / `pre`). There is
113
113
  theme-level token in your own theme when a project actually needs one; the
114
114
  baseline stays sans so app UI inherits predictably.
115
115
 
116
+ ### Font width
117
+
118
+ Two public hooks carry the typographic side of density: `--font-width` (normal
119
+ width) and `--font-width-dense` (compact width). The document reads
120
+ `--font-width` through `font-stretch`; `.compact` applies
121
+ `--font-width-dense` and `.spacious` restores `--font-width`. A theme whose
122
+ typeface has no useful width axis sets `--font-width-dense: 100%`. Width is a
123
+ density token, not a responsive escape hatch — never condense text to fix an
124
+ overflowing label.
125
+
116
126
  ## Font weights
117
127
 
118
128
  The framework exposes a small set of weights:
@@ -80,7 +80,8 @@ shared by buttons, badges, alerts, and cards; `.inverted` is a shared surface
80
80
  modifier that paints any block with the inverse surface. `.ghost` and `.link`
81
81
  are button-only. Size variants `.sm` and `.lg` scale typography and
82
82
  participating component geometry consistently. Density contexts `.compact` and
83
- `.spacious` change geometry and spacing without changing typography or icons.
83
+ `.spacious` change geometry and spacing without changing font size or icons;
84
+ `.compact` may slightly narrow typefaces that support a width axis.
84
85
  Undocumented `is-*` classes are runtime internals.
85
86
 
86
87
  ```html
@@ -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">
@@ -781,8 +785,9 @@ document.addEventListener("actual:invalid", (event) => {
781
785
 
782
786
  Custom rules go in `data-validation-rules` and resolve through
783
787
  `FormValidator.registerRule`. Built-in rules are `same`, `number`, `digits`,
784
- `alnum`, and `date`. The library never performs network validation; server
785
- errors flow back through `FormValidator.setErrors(form, { name: message })` and
788
+ `alnum`, `starts-with`, `ends-with`, `date`, and `tel-prefix`. The library never
789
+ performs network validation; server errors flow back through
790
+ `FormValidator.setErrors(form, { name: message })` and
786
791
  `FormValidator.clearFieldError(field)`.
787
792
 
788
793
  ```js