From 5f899c7287a94d2d00531d0c4f324406b420a620 Mon Sep 17 00:00:00 2001 From: Andreas Reinhold / reini Date: Mon, 14 Sep 2026 13:42:43 +0200 Subject: [PATCH 1/7] Layer the token files and keep their utilities apart Plan step 33. Each token file now opens with the material layer statement and wraps its declarations in material.tokens (the font face in material.base), so an application's unlayered scheme outranks the package default whatever the selectors, and the foundation can import the files with no layer() on the import. The type-* and state utilities move to tokens/utilities.css, still imported by material.css, so no file the foundation imports carries a Tailwind directive. [x-cloak] moves beside the [hidden] rule in foundation/hidden.css, outside every layer; material.css imports it. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9 --- resources/css/foundation/hidden.css | 23 + resources/css/material.css | 5 +- resources/css/tokens/elevation.css | 21 +- resources/css/tokens/font.css | 22 +- resources/css/tokens/motion.css | 121 ++-- resources/css/tokens/scheme.css | 836 ++++++++++++++-------------- resources/css/tokens/shape.css | 27 +- resources/css/tokens/state.css | 112 +--- resources/css/tokens/type.css | 327 +++-------- resources/css/tokens/utilities.css | 286 ++++++++++ tests/Feature/TokensTest.php | 5 +- 11 files changed, 932 insertions(+), 853 deletions(-) create mode 100644 resources/css/foundation/hidden.css create mode 100644 resources/css/tokens/utilities.css diff --git a/resources/css/foundation/hidden.css b/resources/css/foundation/hidden.css new file mode 100644 index 00000000..addb0aaa --- /dev/null +++ b/resources/css/foundation/hidden.css @@ -0,0 +1,23 @@ +/* + * The two rules that hide, outside every layer. + * + * A layered rule always loses to an unlayered one, and no rule without `!important` beats one with + * it, so these two sit outside the layers and carry `!important`: whatever `display` a component + * gives its root, `hidden` on it hides it, and an application can pass `hidden` to any component. + * `hidden="until-found"` is left to the browser, which keeps such content searchable. + * `[x-cloak]` hides what Alpine has not started yet; Alpine removes the attribute when it has. + * + * Only a layered `!important` could outrank these, and the package writes none. The file is + * apart from reset.css because material.css, which stays for applications importing it after + * Tailwind, needs `[x-cloak]` without the reset. + */ + +@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility; + +[hidden]:where(:not([hidden='until-found'])) { + display: none !important; +} + +[x-cloak] { + display: none !important; +} diff --git a/resources/css/material.css b/resources/css/material.css index e5fbcdd9..3524a01f 100644 --- a/resources/css/material.css +++ b/resources/css/material.css @@ -9,7 +9,8 @@ * @source '../../vendor/nonameweb/livewire-material/src'; * * tokens/scheme.css is the package's own default (M3's baseline purple), so an application - * renders before it has a scheme; the application's file declares the same roles later and wins. + * renders before it has a scheme; the application's file declares the same roles, unlayered, and + * wins over the default in `material.tokens`. */ @import './tokens/scheme.css'; @@ -20,6 +21,8 @@ @import './tokens/font.css'; @import './tokens/theme.css'; @import './tokens/state.css'; +@import './tokens/utilities.css'; +@import './foundation/hidden.css'; @import './components/actions.css'; @import './components/groups.css'; @import './components/list.css'; diff --git a/resources/css/tokens/elevation.css b/resources/css/tokens/elevation.css index 849783be..65abc857 100644 --- a/resources/css/tokens/elevation.css +++ b/resources/css/tokens/elevation.css @@ -9,13 +9,18 @@ * sheet. On a dark surface a shadow is nearly invisible, so a panel separates from the page * by its container tone instead. As utilities: shadow-elevation-1 … shadow-elevation-5. */ -:root { - --md-sys-color-shadow: #000000; - --md-sys-color-scrim: #000000; - --md-sys-elevation-1: 0 1px 2px 0 color-mix(in srgb, var(--md-sys-color-shadow) 30%, transparent), 0 1px 3px 1px color-mix(in srgb, var(--md-sys-color-shadow) 15%, transparent); - --md-sys-elevation-2: 0 1px 2px 0 color-mix(in srgb, var(--md-sys-color-shadow) 30%, transparent), 0 2px 6px 2px color-mix(in srgb, var(--md-sys-color-shadow) 15%, transparent); - --md-sys-elevation-3: 0 1px 3px 0 color-mix(in srgb, var(--md-sys-color-shadow) 30%, transparent), 0 4px 8px 3px color-mix(in srgb, var(--md-sys-color-shadow) 15%, transparent); - --md-sys-elevation-4: 0 2px 3px 0 color-mix(in srgb, var(--md-sys-color-shadow) 30%, transparent), 0 6px 10px 4px color-mix(in srgb, var(--md-sys-color-shadow) 15%, transparent); - --md-sys-elevation-5: 0 4px 4px 0 color-mix(in srgb, var(--md-sys-color-shadow) 30%, transparent), 0 8px 12px 6px color-mix(in srgb, var(--md-sys-color-shadow) 15%, transparent); +@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility; + +@layer material.tokens { + :root { + --md-sys-color-shadow: #000000; + --md-sys-color-scrim: #000000; + + --md-sys-elevation-1: 0 1px 2px 0 color-mix(in srgb, var(--md-sys-color-shadow) 30%, transparent), 0 1px 3px 1px color-mix(in srgb, var(--md-sys-color-shadow) 15%, transparent); + --md-sys-elevation-2: 0 1px 2px 0 color-mix(in srgb, var(--md-sys-color-shadow) 30%, transparent), 0 2px 6px 2px color-mix(in srgb, var(--md-sys-color-shadow) 15%, transparent); + --md-sys-elevation-3: 0 1px 3px 0 color-mix(in srgb, var(--md-sys-color-shadow) 30%, transparent), 0 4px 8px 3px color-mix(in srgb, var(--md-sys-color-shadow) 15%, transparent); + --md-sys-elevation-4: 0 2px 3px 0 color-mix(in srgb, var(--md-sys-color-shadow) 30%, transparent), 0 6px 10px 4px color-mix(in srgb, var(--md-sys-color-shadow) 15%, transparent); + --md-sys-elevation-5: 0 4px 4px 0 color-mix(in srgb, var(--md-sys-color-shadow) 30%, transparent), 0 8px 12px 6px color-mix(in srgb, var(--md-sys-color-shadow) 15%, transparent); + } } diff --git a/resources/css/tokens/font.css b/resources/css/tokens/font.css index ca18d2df..2a5fe5c4 100644 --- a/resources/css/tokens/font.css +++ b/resources/css/tokens/font.css @@ -6,13 +6,19 @@ * size pinned — 63 KB where the full font is 4 MB. Vite copies it into the application's * build from this relative url. * - * An application that wants another typeface overrides `--md-ref-typeface-brand` (and so - * `font-sans`) after importing material.css. + * The face sits in `material.base` (foundation/base.css imports it). An application that wants + * another typeface overrides `--md-ref-typeface-brand` in its own stylesheet, which outranks + * every package layer. */ -@font-face { - font-family: 'Google Sans Flex'; - src: url('../../fonts/google-sans-flex/GoogleSansFlex-Latin.woff2') format('woff2'); - font-weight: 400 700; - font-style: normal; - font-display: swap; + +@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility; + +@layer material.base { + @font-face { + font-family: 'Google Sans Flex'; + src: url('../../fonts/google-sans-flex/GoogleSansFlex-Latin.woff2') format('woff2'); + font-weight: 400 700; + font-style: normal; + font-display: swap; + } } diff --git a/resources/css/tokens/motion.css b/resources/css/tokens/motion.css index bc98b741..25d9306f 100644 --- a/resources/css/tokens/motion.css +++ b/resources/css/tokens/motion.css @@ -40,73 +40,78 @@ * these tokens turns instant with no per-component check; anything that animates without them * is a bug. */ -:root { - /* damping 0.6, stiffness 800; settles in 360ms; peaks at 1.095 at 139ms; sampled over 350ms */ - --md-sys-motion-spatial-fast: linear(0, 0.0195, 0.0716, 0.1471, 0.2381, 0.3378, 0.4404, 0.5416, 0.6378, 0.7265, 0.806, 0.8754, 0.9342, 0.9826, 1.0211, 1.0503, 1.0713, 1.0849, 1.0924, 1.0948, 1.0931, 1.0882, 1.0812, 1.0726, 1.0632, 1.0534, 1.0438, 1.0347, 1.0263, 1.0187, 1.0121, 1.0064, 1.0018, 0.9981, 0.9953, 0.9933, 0.992, 0.9913, 0.991, 0.9912, 0.9916, 0.9923, 0.9931, 0.994, 0.9949, 0.9958, 0.9967, 0.9975, 1); - --md-sys-motion-spatial-fast-duration: 350ms; - /* damping 0.8, stiffness 380; settles in 440ms; peaks at 1.015 at 269ms; sampled over 500ms */ - --md-sys-motion-spatial-default: linear(0, 0.0185, 0.0663, 0.1336, 0.2126, 0.2973, 0.3832, 0.4669, 0.5461, 0.6192, 0.6854, 0.7443, 0.7958, 0.8402, 0.8779, 0.9094, 0.9354, 0.9564, 0.9731, 0.9861, 0.996, 1.0033, 1.0085, 1.0119, 1.014, 1.015, 1.0152, 1.0148, 1.014, 1.0129, 1.0117, 1.0104, 1.0091, 1.0078, 1.0066, 1.0055, 1.0046, 1.0037, 1.0029, 1.0023, 1.0017, 1.0013, 1.0009, 1.0006, 1.0004, 1.0002, 1, 0.9999, 1); - --md-sys-motion-spatial-default-duration: 500ms; +@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility; - /* damping 0.8, stiffness 200; settles in 600ms; peaks at 1.015 at 370ms; sampled over 650ms */ - --md-sys-motion-spatial-slow: linear(0, 0.0166, 0.0597, 0.1211, 0.1939, 0.273, 0.354, 0.434, 0.5107, 0.5826, 0.6487, 0.7084, 0.7615, 0.8081, 0.8484, 0.8829, 0.912, 0.9361, 0.9559, 0.9719, 0.9845, 0.9943, 1.0017, 1.0071, 1.0108, 1.0132, 1.0146, 1.0151, 1.0151, 1.0145, 1.0137, 1.0126, 1.0114, 1.0102, 1.009, 1.0078, 1.0067, 1.0057, 1.0047, 1.0039, 1.0031, 1.0025, 1.0019, 1.0015, 1.0011, 1.0008, 1.0005, 1.0003, 1); - --md-sys-motion-spatial-slow-duration: 650ms; +@layer material.tokens { + :root { + /* damping 0.6, stiffness 800; settles in 360ms; peaks at 1.095 at 139ms; sampled over 350ms */ + --md-sys-motion-spatial-fast: linear(0, 0.0195, 0.0716, 0.1471, 0.2381, 0.3378, 0.4404, 0.5416, 0.6378, 0.7265, 0.806, 0.8754, 0.9342, 0.9826, 1.0211, 1.0503, 1.0713, 1.0849, 1.0924, 1.0948, 1.0931, 1.0882, 1.0812, 1.0726, 1.0632, 1.0534, 1.0438, 1.0347, 1.0263, 1.0187, 1.0121, 1.0064, 1.0018, 0.9981, 0.9953, 0.9933, 0.992, 0.9913, 0.991, 0.9912, 0.9916, 0.9923, 0.9931, 0.994, 0.9949, 0.9958, 0.9967, 0.9975, 1); + --md-sys-motion-spatial-fast-duration: 350ms; - /* damping 1.0, stiffness 3800; settles in 150ms; sampled over 150ms */ - --md-sys-motion-effects-fast: linear(0, 0.0163, 0.0576, 0.1147, 0.1807, 0.2507, 0.3214, 0.3902, 0.4558, 0.5172, 0.5737, 0.6253, 0.6718, 0.7136, 0.7508, 0.7837, 0.8128, 0.8383, 0.8606, 0.8801, 0.897, 0.9117, 0.9244, 0.9353, 0.9448, 0.9529, 0.9599, 0.9658, 0.9709, 0.9753, 0.979, 0.9822, 0.9849, 0.9872, 0.9892, 0.9909, 0.9923, 0.9935, 0.9945, 0.9954, 0.9961, 0.9967, 0.9972, 0.9977, 0.998, 0.9983, 0.9986, 0.9988, 1); - --md-sys-motion-effects-fast-duration: 150ms; + /* damping 0.8, stiffness 380; settles in 440ms; peaks at 1.015 at 269ms; sampled over 500ms */ + --md-sys-motion-spatial-default: linear(0, 0.0185, 0.0663, 0.1336, 0.2126, 0.2973, 0.3832, 0.4669, 0.5461, 0.6192, 0.6854, 0.7443, 0.7958, 0.8402, 0.8779, 0.9094, 0.9354, 0.9564, 0.9731, 0.9861, 0.996, 1.0033, 1.0085, 1.0119, 1.014, 1.015, 1.0152, 1.0148, 1.014, 1.0129, 1.0117, 1.0104, 1.0091, 1.0078, 1.0066, 1.0055, 1.0046, 1.0037, 1.0029, 1.0023, 1.0017, 1.0013, 1.0009, 1.0006, 1.0004, 1.0002, 1, 0.9999, 1); + --md-sys-motion-spatial-default-duration: 500ms; - /* damping 1.0, stiffness 1600; settles in 240ms; sampled over 200ms */ - --md-sys-motion-effects-default: linear(0, 0.0124, 0.0446, 0.0902, 0.1443, 0.2032, 0.2642, 0.3253, 0.3849, 0.4422, 0.4963, 0.547, 0.594, 0.6372, 0.6768, 0.7127, 0.7452, 0.7745, 0.8009, 0.8244, 0.8454, 0.8641, 0.8807, 0.8954, 0.9084, 0.9199, 0.93, 0.9389, 0.9467, 0.9536, 0.9596, 0.9648, 0.9694, 0.9734, 0.9769, 0.98, 0.9826, 0.985, 0.987, 0.9887, 0.9902, 0.9916, 0.9927, 0.9937, 0.9946, 0.9953, 0.9959, 0.9965, 1); - --md-sys-motion-effects-default-duration: 200ms; + /* damping 0.8, stiffness 200; settles in 600ms; peaks at 1.015 at 370ms; sampled over 650ms */ + --md-sys-motion-spatial-slow: linear(0, 0.0166, 0.0597, 0.1211, 0.1939, 0.273, 0.354, 0.434, 0.5107, 0.5826, 0.6487, 0.7084, 0.7615, 0.8081, 0.8484, 0.8829, 0.912, 0.9361, 0.9559, 0.9719, 0.9845, 0.9943, 1.0017, 1.0071, 1.0108, 1.0132, 1.0146, 1.0151, 1.0151, 1.0145, 1.0137, 1.0126, 1.0114, 1.0102, 1.009, 1.0078, 1.0067, 1.0057, 1.0047, 1.0039, 1.0031, 1.0025, 1.0019, 1.0015, 1.0011, 1.0008, 1.0005, 1.0003, 1); + --md-sys-motion-spatial-slow-duration: 650ms; - /* damping 1.0, stiffness 800; settles in 330ms; sampled over 300ms */ - --md-sys-motion-effects-slow: linear(0, 0.0139, 0.0496, 0.0995, 0.1583, 0.2216, 0.2865, 0.3509, 0.4131, 0.4722, 0.5275, 0.5788, 0.6258, 0.6687, 0.7075, 0.7424, 0.7737, 0.8016, 0.8264, 0.8484, 0.8678, 0.8849, 0.8999, 0.9131, 0.9247, 0.9347, 0.9435, 0.9512, 0.9578, 0.9636, 0.9686, 0.973, 0.9767, 0.98, 0.9828, 0.9852, 0.9873, 0.9891, 0.9907, 0.992, 0.9931, 0.9941, 0.995, 0.9957, 0.9963, 0.9969, 0.9973, 0.9977, 1); - --md-sys-motion-effects-slow-duration: 300ms; + /* damping 1.0, stiffness 3800; settles in 150ms; sampled over 150ms */ + --md-sys-motion-effects-fast: linear(0, 0.0163, 0.0576, 0.1147, 0.1807, 0.2507, 0.3214, 0.3902, 0.4558, 0.5172, 0.5737, 0.6253, 0.6718, 0.7136, 0.7508, 0.7837, 0.8128, 0.8383, 0.8606, 0.8801, 0.897, 0.9117, 0.9244, 0.9353, 0.9448, 0.9529, 0.9599, 0.9658, 0.9709, 0.9753, 0.979, 0.9822, 0.9849, 0.9872, 0.9892, 0.9909, 0.9923, 0.9935, 0.9945, 0.9954, 0.9961, 0.9967, 0.9972, 0.9977, 0.998, 0.9983, 0.9986, 0.9988, 1); + --md-sys-motion-effects-fast-duration: 150ms; - --md-sys-motion-easing-standard: cubic-bezier(0.2, 0, 0, 1); - --md-sys-motion-easing-standard-accelerate: cubic-bezier(0.3, 0, 1, 1); - --md-sys-motion-easing-standard-decelerate: cubic-bezier(0, 0, 0, 1); - --md-sys-motion-easing-emphasized: cubic-bezier(0.2, 0, 0, 1); - --md-sys-motion-easing-emphasized-accelerate: cubic-bezier(0.3, 0, 0.8, 0.15); - --md-sys-motion-easing-emphasized-decelerate: cubic-bezier(0.05, 0.7, 0.1, 1); - --md-sys-motion-duration-short: 150ms; - --md-sys-motion-duration-medium: 300ms; - --md-sys-motion-duration-long: 500ms; -} + /* damping 1.0, stiffness 1600; settles in 240ms; sampled over 200ms */ + --md-sys-motion-effects-default: linear(0, 0.0124, 0.0446, 0.0902, 0.1443, 0.2032, 0.2642, 0.3253, 0.3849, 0.4422, 0.4963, 0.547, 0.594, 0.6372, 0.6768, 0.7127, 0.7452, 0.7745, 0.8009, 0.8244, 0.8454, 0.8641, 0.8807, 0.8954, 0.9084, 0.9199, 0.93, 0.9389, 0.9467, 0.9536, 0.9596, 0.9648, 0.9694, 0.9734, 0.9769, 0.98, 0.9826, 0.985, 0.987, 0.9887, 0.9902, 0.9916, 0.9927, 0.9937, 0.9946, 0.9953, 0.9959, 0.9965, 1); + --md-sys-motion-effects-default-duration: 200ms; -/* - * The Standard scheme: the same three spatial springs at a damping ratio of 0.9, which leaves - * a 0.2% overshoot instead of Expressive's 9% — motion that eases into place rather than - * bouncing. Google's web durations for them are 350/500/750 ms; the effects springs, the - * legacy easings and every other token are the ones above. - */ -[data-motion="standard"] { - /* damping 0.9, stiffness 1400; settles in 230ms; peaks at 1.002 at 193ms; sampled over 350ms */ - --md-sys-motion-spatial-fast: linear(0, 0.0316, 0.1076, 0.2062, 0.313, 0.4185, 0.517, 0.6056, 0.6828, 0.7486, 0.8036, 0.8487, 0.8852, 0.9142, 0.937, 0.9546, 0.968, 0.9781, 0.9856, 0.991, 0.9948, 0.9975, 0.9992, 1.0004, 1.001, 1.0014, 1.0015, 1.0015, 1.0014, 1.0013, 1.0011, 1.001, 1.0008, 1.0007, 1.0005, 1.0004, 1.0003, 1.0003, 1.0002, 1.0001, 1.0001, 1.0001, 1.0001, 1, 1, 1, 1, 1, 1); - --md-sys-motion-spatial-fast-duration: 350ms; + /* damping 1.0, stiffness 800; settles in 330ms; sampled over 300ms */ + --md-sys-motion-effects-slow: linear(0, 0.0139, 0.0496, 0.0995, 0.1583, 0.2216, 0.2865, 0.3509, 0.4131, 0.4722, 0.5275, 0.5788, 0.6258, 0.6687, 0.7075, 0.7424, 0.7737, 0.8016, 0.8264, 0.8484, 0.8678, 0.8849, 0.8999, 0.9131, 0.9247, 0.9347, 0.9435, 0.9512, 0.9578, 0.9636, 0.9686, 0.973, 0.9767, 0.98, 0.9828, 0.9852, 0.9873, 0.9891, 0.9907, 0.992, 0.9931, 0.9941, 0.995, 0.9957, 0.9963, 0.9969, 0.9973, 0.9977, 1); + --md-sys-motion-effects-slow-duration: 300ms; - /* damping 0.9, stiffness 700; settles in 320ms; peaks at 1.002 at 272ms; sampled over 500ms */ - --md-sys-motion-spatial-default: linear(0, 0.0322, 0.1094, 0.2094, 0.3173, 0.4237, 0.5227, 0.6114, 0.6886, 0.7541, 0.8086, 0.8532, 0.8891, 0.9175, 0.9397, 0.9569, 0.9699, 0.9796, 0.9867, 0.9918, 0.9954, 0.9979, 0.9995, 1.0006, 1.0011, 1.0014, 1.0015, 1.0015, 1.0014, 1.0012, 1.0011, 1.0009, 1.0007, 1.0006, 1.0005, 1.0004, 1.0003, 1.0002, 1.0002, 1.0001, 1.0001, 1.0001, 1, 1, 1, 1, 1, 1, 1); - --md-sys-motion-spatial-default-duration: 500ms; + --md-sys-motion-easing-standard: cubic-bezier(0.2, 0, 0, 1); + --md-sys-motion-easing-standard-accelerate: cubic-bezier(0.3, 0, 1, 1); + --md-sys-motion-easing-standard-decelerate: cubic-bezier(0, 0, 0, 1); + --md-sys-motion-easing-emphasized: cubic-bezier(0.2, 0, 0, 1); + --md-sys-motion-easing-emphasized-accelerate: cubic-bezier(0.3, 0, 0.8, 0.15); + --md-sys-motion-easing-emphasized-decelerate: cubic-bezier(0.05, 0.7, 0.1, 1); + --md-sys-motion-duration-short: 150ms; + --md-sys-motion-duration-medium: 300ms; + --md-sys-motion-duration-long: 500ms; + } - /* damping 0.9, stiffness 300; settles in 490ms; peaks at 1.002 at 416ms; sampled over 750ms */ - --md-sys-motion-spatial-slow: linear(0, 0.0311, 0.1061, 0.2037, 0.3095, 0.4143, 0.5125, 0.6009, 0.6782, 0.7442, 0.7995, 0.8451, 0.882, 0.9115, 0.9347, 0.9527, 0.9665, 0.9769, 0.9846, 0.9903, 0.9943, 0.9971, 0.999, 1.0002, 1.0009, 1.0013, 1.0015, 1.0015, 1.0014, 1.0013, 1.0012, 1.001, 1.0008, 1.0007, 1.0006, 1.0005, 1.0004, 1.0003, 1.0002, 1.0002, 1.0001, 1.0001, 1.0001, 1, 1, 1, 1, 1, 1); - --md-sys-motion-spatial-slow-duration: 750ms; -} - -@media (prefers-reduced-motion: reduce) { - :root, + /* + * The Standard scheme: the same three spatial springs at a damping ratio of 0.9, which leaves + * a 0.2% overshoot instead of Expressive's 9% — motion that eases into place rather than + * bouncing. Google's web durations for them are 350/500/750 ms; the effects springs, the + * legacy easings and every other token are the ones above. + */ [data-motion="standard"] { - --md-sys-motion-spatial-fast-duration: 0ms; - --md-sys-motion-spatial-default-duration: 0ms; - --md-sys-motion-spatial-slow-duration: 0ms; - --md-sys-motion-effects-fast-duration: 0ms; - --md-sys-motion-effects-default-duration: 0ms; - --md-sys-motion-effects-slow-duration: 0ms; - --md-sys-motion-duration-short: 0ms; - --md-sys-motion-duration-medium: 0ms; - --md-sys-motion-duration-long: 0ms; + /* damping 0.9, stiffness 1400; settles in 230ms; peaks at 1.002 at 193ms; sampled over 350ms */ + --md-sys-motion-spatial-fast: linear(0, 0.0316, 0.1076, 0.2062, 0.313, 0.4185, 0.517, 0.6056, 0.6828, 0.7486, 0.8036, 0.8487, 0.8852, 0.9142, 0.937, 0.9546, 0.968, 0.9781, 0.9856, 0.991, 0.9948, 0.9975, 0.9992, 1.0004, 1.001, 1.0014, 1.0015, 1.0015, 1.0014, 1.0013, 1.0011, 1.001, 1.0008, 1.0007, 1.0005, 1.0004, 1.0003, 1.0003, 1.0002, 1.0001, 1.0001, 1.0001, 1.0001, 1, 1, 1, 1, 1, 1); + --md-sys-motion-spatial-fast-duration: 350ms; + + /* damping 0.9, stiffness 700; settles in 320ms; peaks at 1.002 at 272ms; sampled over 500ms */ + --md-sys-motion-spatial-default: linear(0, 0.0322, 0.1094, 0.2094, 0.3173, 0.4237, 0.5227, 0.6114, 0.6886, 0.7541, 0.8086, 0.8532, 0.8891, 0.9175, 0.9397, 0.9569, 0.9699, 0.9796, 0.9867, 0.9918, 0.9954, 0.9979, 0.9995, 1.0006, 1.0011, 1.0014, 1.0015, 1.0015, 1.0014, 1.0012, 1.0011, 1.0009, 1.0007, 1.0006, 1.0005, 1.0004, 1.0003, 1.0002, 1.0002, 1.0001, 1.0001, 1.0001, 1, 1, 1, 1, 1, 1, 1); + --md-sys-motion-spatial-default-duration: 500ms; + + /* damping 0.9, stiffness 300; settles in 490ms; peaks at 1.002 at 416ms; sampled over 750ms */ + --md-sys-motion-spatial-slow: linear(0, 0.0311, 0.1061, 0.2037, 0.3095, 0.4143, 0.5125, 0.6009, 0.6782, 0.7442, 0.7995, 0.8451, 0.882, 0.9115, 0.9347, 0.9527, 0.9665, 0.9769, 0.9846, 0.9903, 0.9943, 0.9971, 0.999, 1.0002, 1.0009, 1.0013, 1.0015, 1.0015, 1.0014, 1.0013, 1.0012, 1.001, 1.0008, 1.0007, 1.0006, 1.0005, 1.0004, 1.0003, 1.0002, 1.0002, 1.0001, 1.0001, 1.0001, 1, 1, 1, 1, 1, 1); + --md-sys-motion-spatial-slow-duration: 750ms; + } + + @media (prefers-reduced-motion: reduce) { + :root, + [data-motion="standard"] { + --md-sys-motion-spatial-fast-duration: 0ms; + --md-sys-motion-spatial-default-duration: 0ms; + --md-sys-motion-spatial-slow-duration: 0ms; + --md-sys-motion-effects-fast-duration: 0ms; + --md-sys-motion-effects-default-duration: 0ms; + --md-sys-motion-effects-slow-duration: 0ms; + --md-sys-motion-duration-short: 0ms; + --md-sys-motion-duration-medium: 0ms; + --md-sys-motion-duration-long: 0ms; + } } } diff --git a/resources/css/tokens/scheme.css b/resources/css/tokens/scheme.css index 1a4735b1..1afe20e2 100644 --- a/resources/css/tokens/scheme.css +++ b/resources/css/tokens/scheme.css @@ -11,431 +11,439 @@ * data-contrast — which the head script writes from the visitor's choice, or from the * operating system's contrast setting while that choice is `system`. No media query * decides here, as none decides the theme. + * + * This is the package's default scheme, so the generated blocks are wrapped in + * `material.tokens` behind the layer statement (keep both when regenerating into this file): an + * application's own scheme, written unlayered, outranks it whatever the selectors. */ -:root, -[data-theme='light'] { - color-scheme: light; +@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility; - --md-sys-color-background: #fdf7fe; - --md-sys-color-on-background: #34313a; - --md-sys-color-surface: #fdf7fe; - --md-sys-color-surface-dim: #ded8e4; - --md-sys-color-surface-bright: #fdf7fe; - --md-sys-color-surface-container-lowest: #ffffff; - --md-sys-color-surface-container-low: #f8f1fa; - --md-sys-color-surface-container: #f2ecf5; - --md-sys-color-surface-container-high: #ece6f0; - --md-sys-color-surface-container-highest: #e7e0ec; - --md-sys-color-on-surface: #34313a; - --md-sys-color-on-surface-variant: #615d68; - --md-sys-color-outline: #7d7983; - --md-sys-color-outline-variant: #b5b0bb; - --md-sys-color-inverse-surface: #0f0d12; - --md-sys-color-inverse-on-surface: #a09ba1; - --md-sys-color-primary: #655789; - --md-sys-color-primary-dim: #594b7c; - --md-sys-color-on-primary: #fdf7ff; - --md-sys-color-primary-container: #d4c3fd; - --md-sys-color-on-primary-container: #493c6c; - --md-sys-color-primary-fixed: #d4c3fd; - --md-sys-color-primary-fixed-dim: #c6b6ee; - --md-sys-color-on-primary-fixed: #352857; - --md-sys-color-on-primary-fixed-variant: #524576; - --md-sys-color-inverse-primary: #d4c3fd; - --md-sys-color-secondary: #625c71; - --md-sys-color-secondary-dim: #565065; - --md-sys-color-on-secondary: #fdf7ff; - --md-sys-color-secondary-container: #e8def8; - --md-sys-color-on-secondary-container: #554f63; - --md-sys-color-secondary-fixed: #e8def8; - --md-sys-color-secondary-fixed-dim: #dad0ea; - --md-sys-color-on-secondary-fixed: #423c50; - --md-sys-color-on-secondary-fixed-variant: #5f586e; - --md-sys-color-tertiary: #7b5270; - --md-sys-color-tertiary-dim: #6e4664; - --md-sys-color-on-tertiary: #fff7f9; - --md-sys-color-tertiary-container: #f4bfe3; - --md-sys-color-on-tertiary-container: #5f3956; - --md-sys-color-tertiary-fixed: #f4bfe3; - --md-sys-color-tertiary-fixed-dim: #e5b2d5; - --md-sys-color-on-tertiary-fixed: #4a2642; - --md-sys-color-on-tertiary-fixed-variant: #694260; - --md-sys-color-error: #a8364b; - --md-sys-color-error-dim: #6b0221; - --md-sys-color-on-error: #fff7f7; - --md-sys-color-error-container: #f97386; - --md-sys-color-on-error-container: #6e0523; - --md-sys-color-success: #006d46; - --md-sys-color-on-success: #e7ffed; - --md-sys-color-success-container: #86f9bc; - --md-sys-color-on-success-container: #00734a; - --md-sys-color-warning: #7c5900; - --md-sys-color-on-warning: #fff8f1; - --md-sys-color-warning-container: #fab925; - --md-sys-color-on-warning-container: #6a4b00; - --md-sys-color-info: #005bc5; - --md-sys-color-on-info: #f9f8ff; - --md-sys-color-info-container: #1c7afc; - --md-sys-color-on-info-container: #001435; - --md-sys-color-inverse-error: #f97386; - --md-sys-color-inverse-success: #3bb27b; - --md-sys-color-inverse-warning: #f3b31e; - --md-sys-color-inverse-info: #699cff; -} +@layer material.tokens { + :root, + [data-theme='light'] { + color-scheme: light; -[data-theme='dark'] { - color-scheme: dark; + --md-sys-color-background: #fdf7fe; + --md-sys-color-on-background: #34313a; + --md-sys-color-surface: #fdf7fe; + --md-sys-color-surface-dim: #ded8e4; + --md-sys-color-surface-bright: #fdf7fe; + --md-sys-color-surface-container-lowest: #ffffff; + --md-sys-color-surface-container-low: #f8f1fa; + --md-sys-color-surface-container: #f2ecf5; + --md-sys-color-surface-container-high: #ece6f0; + --md-sys-color-surface-container-highest: #e7e0ec; + --md-sys-color-on-surface: #34313a; + --md-sys-color-on-surface-variant: #615d68; + --md-sys-color-outline: #7d7983; + --md-sys-color-outline-variant: #b5b0bb; + --md-sys-color-inverse-surface: #0f0d12; + --md-sys-color-inverse-on-surface: #a09ba1; + --md-sys-color-primary: #655789; + --md-sys-color-primary-dim: #594b7c; + --md-sys-color-on-primary: #fdf7ff; + --md-sys-color-primary-container: #d4c3fd; + --md-sys-color-on-primary-container: #493c6c; + --md-sys-color-primary-fixed: #d4c3fd; + --md-sys-color-primary-fixed-dim: #c6b6ee; + --md-sys-color-on-primary-fixed: #352857; + --md-sys-color-on-primary-fixed-variant: #524576; + --md-sys-color-inverse-primary: #d4c3fd; + --md-sys-color-secondary: #625c71; + --md-sys-color-secondary-dim: #565065; + --md-sys-color-on-secondary: #fdf7ff; + --md-sys-color-secondary-container: #e8def8; + --md-sys-color-on-secondary-container: #554f63; + --md-sys-color-secondary-fixed: #e8def8; + --md-sys-color-secondary-fixed-dim: #dad0ea; + --md-sys-color-on-secondary-fixed: #423c50; + --md-sys-color-on-secondary-fixed-variant: #5f586e; + --md-sys-color-tertiary: #7b5270; + --md-sys-color-tertiary-dim: #6e4664; + --md-sys-color-on-tertiary: #fff7f9; + --md-sys-color-tertiary-container: #f4bfe3; + --md-sys-color-on-tertiary-container: #5f3956; + --md-sys-color-tertiary-fixed: #f4bfe3; + --md-sys-color-tertiary-fixed-dim: #e5b2d5; + --md-sys-color-on-tertiary-fixed: #4a2642; + --md-sys-color-on-tertiary-fixed-variant: #694260; + --md-sys-color-error: #a8364b; + --md-sys-color-error-dim: #6b0221; + --md-sys-color-on-error: #fff7f7; + --md-sys-color-error-container: #f97386; + --md-sys-color-on-error-container: #6e0523; + --md-sys-color-success: #006d46; + --md-sys-color-on-success: #e7ffed; + --md-sys-color-success-container: #86f9bc; + --md-sys-color-on-success-container: #00734a; + --md-sys-color-warning: #7c5900; + --md-sys-color-on-warning: #fff8f1; + --md-sys-color-warning-container: #fab925; + --md-sys-color-on-warning-container: #6a4b00; + --md-sys-color-info: #005bc5; + --md-sys-color-on-info: #f9f8ff; + --md-sys-color-info-container: #1c7afc; + --md-sys-color-on-info-container: #001435; + --md-sys-color-inverse-error: #f97386; + --md-sys-color-inverse-success: #3bb27b; + --md-sys-color-inverse-warning: #f3b31e; + --md-sys-color-inverse-info: #699cff; + } - --md-sys-color-background: #0f0d12; - --md-sys-color-on-background: #eae3ef; - --md-sys-color-surface: #0f0d12; - --md-sys-color-surface-dim: #0f0d12; - --md-sys-color-surface-bright: #2e2b34; - --md-sys-color-surface-container-lowest: #000000; - --md-sys-color-surface-container-low: #141218; - --md-sys-color-surface-container: #1b181f; - --md-sys-color-surface-container-high: #211e26; - --md-sys-color-surface-container-highest: #27242d; - --md-sys-color-on-surface: #eae3ef; - --md-sys-color-on-surface-variant: #aea9b4; - --md-sys-color-outline: #78737e; - --md-sys-color-outline-variant: #4a4650; - --md-sys-color-inverse-surface: #fdf7fe; - --md-sys-color-inverse-on-surface: #575459; - --md-sys-color-primary: #cdc0ec; - --md-sys-color-primary-dim: #bfb2de; - --md-sys-color-on-primary: #443a5f; - --md-sys-color-primary-container: #574d72; - --md-sys-color-on-primary-container: #e9deff; - --md-sys-color-primary-fixed: #ded0fe; - --md-sys-color-primary-fixed-dim: #d0c3ef; - --md-sys-color-on-primary-fixed: #3c3256; - --md-sys-color-on-primary-fixed-variant: #594e74; - --md-sys-color-inverse-primary: #645980; - --md-sys-color-secondary: #cbc2db; - --md-sys-color-secondary-dim: #beb5cd; - --md-sys-color-on-secondary: #433d51; - --md-sys-color-secondary-container: #3e384c; - --md-sys-color-on-secondary-container: #c4bbd4; - --md-sys-color-secondary-fixed: #e8def8; - --md-sys-color-secondary-fixed-dim: #dad0ea; - --md-sys-color-on-secondary-fixed: #423c50; - --md-sys-color-on-secondary-fixed-variant: #5f586e; - --md-sys-color-tertiary: #ffcfef; - --md-sys-color-tertiary-dim: #f4bfe3; - --md-sys-color-on-tertiary: #69415f; - --md-sys-color-tertiary-container: #f4bfe3; - --md-sys-color-on-tertiary-container: #5f3956; - --md-sys-color-tertiary-fixed: #f4bfe3; - --md-sys-color-tertiary-fixed-dim: #e5b2d5; - --md-sys-color-on-tertiary-fixed: #4a2642; - --md-sys-color-on-tertiary-fixed-variant: #694260; - --md-sys-color-error: #f97386; - --md-sys-color-error-dim: #c44b5f; - --md-sys-color-on-error: #490013; - --md-sys-color-error-container: #871c34; - --md-sys-color-on-error-container: #ff97a3; - --md-sys-color-success: #3bb27b; - --md-sys-color-on-success: #002615; - --md-sys-color-success-container: #059460; - --md-sys-color-on-success-container: #001e10; - --md-sys-color-warning: #f3b31e; - --md-sys-color-on-warning: #4e3600; - --md-sys-color-warning-container: #e4a604; - --md-sys-color-on-warning-container: #593f00; - --md-sys-color-info: #699cff; - --md-sys-color-on-info: #001e4b; - --md-sys-color-info-container: #0c74f6; - --md-sys-color-on-info-container: #000a23; - --md-sys-color-inverse-error: #a8364b; - --md-sys-color-inverse-success: #006d46; - --md-sys-color-inverse-warning: #7c5900; - --md-sys-color-inverse-info: #005bc5; -} + [data-theme='dark'] { + color-scheme: dark; -[data-contrast='medium'], -[data-contrast='medium'][data-theme='light'], -[data-contrast='medium'] [data-theme='light'] { - color-scheme: light; + --md-sys-color-background: #0f0d12; + --md-sys-color-on-background: #eae3ef; + --md-sys-color-surface: #0f0d12; + --md-sys-color-surface-dim: #0f0d12; + --md-sys-color-surface-bright: #2e2b34; + --md-sys-color-surface-container-lowest: #000000; + --md-sys-color-surface-container-low: #141218; + --md-sys-color-surface-container: #1b181f; + --md-sys-color-surface-container-high: #211e26; + --md-sys-color-surface-container-highest: #27242d; + --md-sys-color-on-surface: #eae3ef; + --md-sys-color-on-surface-variant: #aea9b4; + --md-sys-color-outline: #78737e; + --md-sys-color-outline-variant: #4a4650; + --md-sys-color-inverse-surface: #fdf7fe; + --md-sys-color-inverse-on-surface: #575459; + --md-sys-color-primary: #cdc0ec; + --md-sys-color-primary-dim: #bfb2de; + --md-sys-color-on-primary: #443a5f; + --md-sys-color-primary-container: #574d72; + --md-sys-color-on-primary-container: #e9deff; + --md-sys-color-primary-fixed: #ded0fe; + --md-sys-color-primary-fixed-dim: #d0c3ef; + --md-sys-color-on-primary-fixed: #3c3256; + --md-sys-color-on-primary-fixed-variant: #594e74; + --md-sys-color-inverse-primary: #645980; + --md-sys-color-secondary: #cbc2db; + --md-sys-color-secondary-dim: #beb5cd; + --md-sys-color-on-secondary: #433d51; + --md-sys-color-secondary-container: #3e384c; + --md-sys-color-on-secondary-container: #c4bbd4; + --md-sys-color-secondary-fixed: #e8def8; + --md-sys-color-secondary-fixed-dim: #dad0ea; + --md-sys-color-on-secondary-fixed: #423c50; + --md-sys-color-on-secondary-fixed-variant: #5f586e; + --md-sys-color-tertiary: #ffcfef; + --md-sys-color-tertiary-dim: #f4bfe3; + --md-sys-color-on-tertiary: #69415f; + --md-sys-color-tertiary-container: #f4bfe3; + --md-sys-color-on-tertiary-container: #5f3956; + --md-sys-color-tertiary-fixed: #f4bfe3; + --md-sys-color-tertiary-fixed-dim: #e5b2d5; + --md-sys-color-on-tertiary-fixed: #4a2642; + --md-sys-color-on-tertiary-fixed-variant: #694260; + --md-sys-color-error: #f97386; + --md-sys-color-error-dim: #c44b5f; + --md-sys-color-on-error: #490013; + --md-sys-color-error-container: #871c34; + --md-sys-color-on-error-container: #ff97a3; + --md-sys-color-success: #3bb27b; + --md-sys-color-on-success: #002615; + --md-sys-color-success-container: #059460; + --md-sys-color-on-success-container: #001e10; + --md-sys-color-warning: #f3b31e; + --md-sys-color-on-warning: #4e3600; + --md-sys-color-warning-container: #e4a604; + --md-sys-color-on-warning-container: #593f00; + --md-sys-color-info: #699cff; + --md-sys-color-on-info: #001e4b; + --md-sys-color-info-container: #0c74f6; + --md-sys-color-on-info-container: #000a23; + --md-sys-color-inverse-error: #a8364b; + --md-sys-color-inverse-success: #006d46; + --md-sys-color-inverse-warning: #7c5900; + --md-sys-color-inverse-info: #005bc5; + } - --md-sys-color-background: #fdf7fe; - --md-sys-color-on-background: #25232b; - --md-sys-color-surface: #fdf7fe; - --md-sys-color-surface-dim: #ded8e4; - --md-sys-color-surface-bright: #fdf7fe; - --md-sys-color-surface-container-lowest: #ffffff; - --md-sys-color-surface-container-low: #f8f1fa; - --md-sys-color-surface-container: #f2ecf5; - --md-sys-color-surface-container-high: #ece6f0; - --md-sys-color-surface-container-highest: #e7e0ec; - --md-sys-color-on-surface: #25232b; - --md-sys-color-on-surface-variant: #45414b; - --md-sys-color-outline: #615d68; - --md-sys-color-outline-variant: #7d7983; - --md-sys-color-inverse-surface: #0f0d12; - --md-sys-color-inverse-on-surface: #c8c3c9; - --md-sys-color-primary: #483b6b; - --md-sys-color-primary-dim: #3c305f; - --md-sys-color-on-primary: #e1d3ff; - --md-sys-color-primary-container: #7b6da0; - --md-sys-color-on-primary-container: #ffffff; - --md-sys-color-primary-fixed: #7b6da0; - --md-sys-color-primary-fixed-dim: #6e6093; - --md-sys-color-on-primary-fixed: #ffffff; - --md-sys-color-on-primary-fixed-variant: #ffffff; - --md-sys-color-inverse-primary: #d4c3fd; - --md-sys-color-secondary: #464054; - --md-sys-color-secondary-dim: #3a3548; - --md-sys-color-on-secondary: #dfd5ef; - --md-sys-color-secondary-container: #787187; - --md-sys-color-on-secondary-container: #ffffff; - --md-sys-color-secondary-fixed: #787187; - --md-sys-color-secondary-fixed-dim: #6c657b; - --md-sys-color-on-secondary-fixed: #ffffff; - --md-sys-color-on-secondary-fixed-variant: #ffffff; - --md-sys-color-tertiary: #5c3653; - --md-sys-color-tertiary-dim: #502b47; - --md-sys-color-on-tertiary: #ffcaee; - --md-sys-color-tertiary-container: #936787; - --md-sys-color-on-tertiary-container: #ffffff; - --md-sys-color-tertiary-fixed: #936787; - --md-sys-color-tertiary-fixed-dim: #855b7a; - --md-sys-color-on-tertiary-fixed: #ffffff; - --md-sys-color-on-tertiary-fixed-variant: #ffffff; - --md-sys-color-error: #821830; - --md-sys-color-error-dim: #6b0221; - --md-sys-color-on-error: #ffcdd1; - --md-sys-color-error-container: #c44b5f; - --md-sys-color-on-error-container: #ffffff; - --md-sys-color-success: #004d30; - --md-sys-color-on-success: #7df0b3; - --md-sys-color-success-container: #008656; - --md-sys-color-on-success-container: #ffffff; - --md-sys-color-warning: #593e00; - --md-sys-color-on-warning: #ffd385; - --md-sys-color-warning-container: #986d00; - --md-sys-color-on-warning-container: #ffffff; - --md-sys-color-info: #003f8e; - --md-sys-color-on-info: #cbd9ff; - --md-sys-color-info-container: #006fef; - --md-sys-color-on-info-container: #ffffff; - --md-sys-color-inverse-error: #ff9da8; - --md-sys-color-inverse-success: #5ace94; - --md-sys-color-inverse-warning: #f3b31e; - --md-sys-color-inverse-info: #98b8ff; -} + [data-contrast='medium'], + [data-contrast='medium'][data-theme='light'], + [data-contrast='medium'] [data-theme='light'] { + color-scheme: light; -[data-contrast='medium'][data-theme='dark'], -[data-contrast='medium'] [data-theme='dark'] { - color-scheme: dark; + --md-sys-color-background: #fdf7fe; + --md-sys-color-on-background: #25232b; + --md-sys-color-surface: #fdf7fe; + --md-sys-color-surface-dim: #ded8e4; + --md-sys-color-surface-bright: #fdf7fe; + --md-sys-color-surface-container-lowest: #ffffff; + --md-sys-color-surface-container-low: #f8f1fa; + --md-sys-color-surface-container: #f2ecf5; + --md-sys-color-surface-container-high: #ece6f0; + --md-sys-color-surface-container-highest: #e7e0ec; + --md-sys-color-on-surface: #25232b; + --md-sys-color-on-surface-variant: #45414b; + --md-sys-color-outline: #615d68; + --md-sys-color-outline-variant: #7d7983; + --md-sys-color-inverse-surface: #0f0d12; + --md-sys-color-inverse-on-surface: #c8c3c9; + --md-sys-color-primary: #483b6b; + --md-sys-color-primary-dim: #3c305f; + --md-sys-color-on-primary: #e1d3ff; + --md-sys-color-primary-container: #7b6da0; + --md-sys-color-on-primary-container: #ffffff; + --md-sys-color-primary-fixed: #7b6da0; + --md-sys-color-primary-fixed-dim: #6e6093; + --md-sys-color-on-primary-fixed: #ffffff; + --md-sys-color-on-primary-fixed-variant: #ffffff; + --md-sys-color-inverse-primary: #d4c3fd; + --md-sys-color-secondary: #464054; + --md-sys-color-secondary-dim: #3a3548; + --md-sys-color-on-secondary: #dfd5ef; + --md-sys-color-secondary-container: #787187; + --md-sys-color-on-secondary-container: #ffffff; + --md-sys-color-secondary-fixed: #787187; + --md-sys-color-secondary-fixed-dim: #6c657b; + --md-sys-color-on-secondary-fixed: #ffffff; + --md-sys-color-on-secondary-fixed-variant: #ffffff; + --md-sys-color-tertiary: #5c3653; + --md-sys-color-tertiary-dim: #502b47; + --md-sys-color-on-tertiary: #ffcaee; + --md-sys-color-tertiary-container: #936787; + --md-sys-color-on-tertiary-container: #ffffff; + --md-sys-color-tertiary-fixed: #936787; + --md-sys-color-tertiary-fixed-dim: #855b7a; + --md-sys-color-on-tertiary-fixed: #ffffff; + --md-sys-color-on-tertiary-fixed-variant: #ffffff; + --md-sys-color-error: #821830; + --md-sys-color-error-dim: #6b0221; + --md-sys-color-on-error: #ffcdd1; + --md-sys-color-error-container: #c44b5f; + --md-sys-color-on-error-container: #ffffff; + --md-sys-color-success: #004d30; + --md-sys-color-on-success: #7df0b3; + --md-sys-color-success-container: #008656; + --md-sys-color-on-success-container: #ffffff; + --md-sys-color-warning: #593e00; + --md-sys-color-on-warning: #ffd385; + --md-sys-color-warning-container: #986d00; + --md-sys-color-on-warning-container: #ffffff; + --md-sys-color-info: #003f8e; + --md-sys-color-on-info: #cbd9ff; + --md-sys-color-info-container: #006fef; + --md-sys-color-on-info-container: #ffffff; + --md-sys-color-inverse-error: #ff9da8; + --md-sys-color-inverse-success: #5ace94; + --md-sys-color-inverse-warning: #f3b31e; + --md-sys-color-inverse-info: #98b8ff; + } - --md-sys-color-background: #0f0d12; - --md-sys-color-on-background: #ffffff; - --md-sys-color-surface: #0f0d12; - --md-sys-color-surface-dim: #0f0d12; - --md-sys-color-surface-bright: #2e2b34; - --md-sys-color-surface-container-lowest: #000000; - --md-sys-color-surface-container-low: #141218; - --md-sys-color-surface-container: #1b181f; - --md-sys-color-surface-container-high: #211e26; - --md-sys-color-surface-container-highest: #27242d; - --md-sys-color-on-surface: #ffffff; - --md-sys-color-on-surface-variant: #bcb6c2; - --md-sys-color-outline: #96919c; - --md-sys-color-outline-variant: #78737e; - --md-sys-color-inverse-surface: #fdf7fe; - --md-sys-color-inverse-on-surface: #39373c; - --md-sys-color-primary: #cdc0ec; - --md-sys-color-primary-dim: #bfb2de; - --md-sys-color-on-primary: #3a3054; - --md-sys-color-primary-container: #7a6f96; - --md-sys-color-on-primary-container: #ffffff; - --md-sys-color-primary-fixed: #ded0fe; - --md-sys-color-primary-fixed-dim: #d0c3ef; - --md-sys-color-on-primary-fixed: #180e30; - --md-sys-color-on-primary-fixed-variant: #3c3256; - --md-sys-color-inverse-primary: #5a4f75; - --md-sys-color-secondary: #cbc2db; - --md-sys-color-secondary-dim: #beb5cd; - --md-sys-color-on-secondary: #393347; - --md-sys-color-secondary-container: #787187; - --md-sys-color-on-secondary-container: #ffffff; - --md-sys-color-secondary-fixed: #e8def8; - --md-sys-color-secondary-fixed-dim: #dad0ea; - --md-sys-color-on-secondary-fixed: #221d2f; - --md-sys-color-on-secondary-fixed-variant: #423c50; - --md-sys-color-tertiary: #ffcfef; - --md-sys-color-tertiary-dim: #f4bfe3; - --md-sys-color-on-tertiary: #5e3855; - --md-sys-color-tertiary-container: #f4bfe3; - --md-sys-color-on-tertiary-container: #542f4c; - --md-sys-color-tertiary-fixed: #f4bfe3; - --md-sys-color-tertiary-fixed-dim: #e5b2d5; - --md-sys-color-on-tertiary-fixed: #180015; - --md-sys-color-on-tertiary-fixed-variant: #4a2642; - --md-sys-color-error: #ff9da8; - --md-sys-color-error-dim: #ff8695; - --md-sys-color-on-error: #5f001c; - --md-sys-color-error-container: #c44b5f; - --md-sys-color-on-error-container: #ffffff; - --md-sys-color-success: #5ace94; - --md-sys-color-on-success: #00341e; - --md-sys-color-success-container: #008656; - --md-sys-color-on-success-container: #ffffff; - --md-sys-color-warning: #f3b31e; - --md-sys-color-on-warning: #412d00; - --md-sys-color-warning-container: #e4a604; - --md-sys-color-on-warning-container: #332200; - --md-sys-color-info: #98b8ff; - --md-sys-color-on-info: #002a62; - --md-sys-color-info-container: #006fef; - --md-sys-color-on-info-container: #ffffff; - --md-sys-color-inverse-error: #821830; - --md-sys-color-inverse-success: #004d30; - --md-sys-color-inverse-warning: #593e00; - --md-sys-color-inverse-info: #003f8e; -} + [data-contrast='medium'][data-theme='dark'], + [data-contrast='medium'] [data-theme='dark'] { + color-scheme: dark; -[data-contrast='high'], -[data-contrast='high'][data-theme='light'], -[data-contrast='high'] [data-theme='light'] { - color-scheme: light; + --md-sys-color-background: #0f0d12; + --md-sys-color-on-background: #ffffff; + --md-sys-color-surface: #0f0d12; + --md-sys-color-surface-dim: #0f0d12; + --md-sys-color-surface-bright: #2e2b34; + --md-sys-color-surface-container-lowest: #000000; + --md-sys-color-surface-container-low: #141218; + --md-sys-color-surface-container: #1b181f; + --md-sys-color-surface-container-high: #211e26; + --md-sys-color-surface-container-highest: #27242d; + --md-sys-color-on-surface: #ffffff; + --md-sys-color-on-surface-variant: #bcb6c2; + --md-sys-color-outline: #96919c; + --md-sys-color-outline-variant: #78737e; + --md-sys-color-inverse-surface: #fdf7fe; + --md-sys-color-inverse-on-surface: #39373c; + --md-sys-color-primary: #cdc0ec; + --md-sys-color-primary-dim: #bfb2de; + --md-sys-color-on-primary: #3a3054; + --md-sys-color-primary-container: #7a6f96; + --md-sys-color-on-primary-container: #ffffff; + --md-sys-color-primary-fixed: #ded0fe; + --md-sys-color-primary-fixed-dim: #d0c3ef; + --md-sys-color-on-primary-fixed: #180e30; + --md-sys-color-on-primary-fixed-variant: #3c3256; + --md-sys-color-inverse-primary: #5a4f75; + --md-sys-color-secondary: #cbc2db; + --md-sys-color-secondary-dim: #beb5cd; + --md-sys-color-on-secondary: #393347; + --md-sys-color-secondary-container: #787187; + --md-sys-color-on-secondary-container: #ffffff; + --md-sys-color-secondary-fixed: #e8def8; + --md-sys-color-secondary-fixed-dim: #dad0ea; + --md-sys-color-on-secondary-fixed: #221d2f; + --md-sys-color-on-secondary-fixed-variant: #423c50; + --md-sys-color-tertiary: #ffcfef; + --md-sys-color-tertiary-dim: #f4bfe3; + --md-sys-color-on-tertiary: #5e3855; + --md-sys-color-tertiary-container: #f4bfe3; + --md-sys-color-on-tertiary-container: #542f4c; + --md-sys-color-tertiary-fixed: #f4bfe3; + --md-sys-color-tertiary-fixed-dim: #e5b2d5; + --md-sys-color-on-tertiary-fixed: #180015; + --md-sys-color-on-tertiary-fixed-variant: #4a2642; + --md-sys-color-error: #ff9da8; + --md-sys-color-error-dim: #ff8695; + --md-sys-color-on-error: #5f001c; + --md-sys-color-error-container: #c44b5f; + --md-sys-color-on-error-container: #ffffff; + --md-sys-color-success: #5ace94; + --md-sys-color-on-success: #00341e; + --md-sys-color-success-container: #008656; + --md-sys-color-on-success-container: #ffffff; + --md-sys-color-warning: #f3b31e; + --md-sys-color-on-warning: #412d00; + --md-sys-color-warning-container: #e4a604; + --md-sys-color-on-warning-container: #332200; + --md-sys-color-info: #98b8ff; + --md-sys-color-on-info: #002a62; + --md-sys-color-info-container: #006fef; + --md-sys-color-on-info-container: #ffffff; + --md-sys-color-inverse-error: #821830; + --md-sys-color-inverse-success: #004d30; + --md-sys-color-inverse-warning: #593e00; + --md-sys-color-inverse-info: #003f8e; + } - --md-sys-color-background: #fdf7fe; - --md-sys-color-on-background: #000000; - --md-sys-color-surface: #fdf7fe; - --md-sys-color-surface-dim: #ded8e4; - --md-sys-color-surface-bright: #fdf7fe; - --md-sys-color-surface-container-lowest: #ffffff; - --md-sys-color-surface-container-low: #f8f1fa; - --md-sys-color-surface-container: #f2ecf5; - --md-sys-color-surface-container-high: #ece6f0; - --md-sys-color-surface-container-highest: #e7e0ec; - --md-sys-color-on-surface: #000000; - --md-sys-color-on-surface-variant: #25232b; - --md-sys-color-outline: #45414b; - --md-sys-color-outline-variant: #54505a; - --md-sys-color-inverse-surface: #0f0d12; - --md-sys-color-inverse-on-surface: #ffffff; - --md-sys-color-primary: #281b49; - --md-sys-color-primary-dim: #1e103f; - --md-sys-color-on-primary: #e1d3ff; - --md-sys-color-primary-container: #584a7b; - --md-sys-color-on-primary-container: #ffffff; - --md-sys-color-primary-fixed: #584a7b; - --md-sys-color-primary-fixed-dim: #4c3f6f; - --md-sys-color-on-primary-fixed: #ffffff; - --md-sys-color-on-primary-fixed-variant: #ffffff; - --md-sys-color-inverse-primary: #d4c3fd; - --md-sys-color-secondary: #262134; - --md-sys-color-secondary-dim: #1c1729; - --md-sys-color-on-secondary: #dfd6ef; - --md-sys-color-secondary-container: #554f64; - --md-sys-color-on-secondary-container: #ffffff; - --md-sys-color-secondary-fixed: #554f64; - --md-sys-color-secondary-fixed-dim: #494358; - --md-sys-color-on-secondary-fixed: #ffffff; - --md-sys-color-on-secondary-fixed-variant: #ffffff; - --md-sys-color-tertiary: #391733; - --md-sys-color-tertiary-dim: #2e0d28; - --md-sys-color-on-tertiary: #ffcbee; - --md-sys-color-tertiary-container: #6d4563; - --md-sys-color-on-tertiary-container: #ffffff; - --md-sys-color-tertiary-fixed: #6d4563; - --md-sys-color-tertiary-fixed-dim: #603a57; - --md-sys-color-on-tertiary-fixed: #ffffff; - --md-sys-color-on-tertiary-fixed-variant: #ffffff; - --md-sys-color-error: #500016; - --md-sys-color-error-dim: #3d000f; - --md-sys-color-on-error: #ffced2; - --md-sys-color-error-container: #97283e; - --md-sys-color-on-error-container: #ffffff; - --md-sys-color-success: #002a18; - --md-sys-color-on-success: #7df0b4; - --md-sys-color-success-container: #005f3c; - --md-sys-color-on-success-container: #ffffff; - --md-sys-color-warning: #312100; - --md-sys-color-on-warning: #ffd486; - --md-sys-color-warning-container: #6c4d00; - --md-sys-color-on-warning-container: #ffffff; - --md-sys-color-info: #002252; - --md-sys-color-on-info: #ccdaff; - --md-sys-color-info-container: #004eac; - --md-sys-color-on-info-container: #ffffff; - --md-sys-color-inverse-error: #ffdddf; - --md-sys-color-inverse-success: #89fcbf; - --md-sys-color-inverse-warning: #ffe2b1; - --md-sys-color-inverse-info: #dde5ff; -} + [data-contrast='high'], + [data-contrast='high'][data-theme='light'], + [data-contrast='high'] [data-theme='light'] { + color-scheme: light; -[data-contrast='high'][data-theme='dark'], -[data-contrast='high'] [data-theme='dark'] { - color-scheme: dark; + --md-sys-color-background: #fdf7fe; + --md-sys-color-on-background: #000000; + --md-sys-color-surface: #fdf7fe; + --md-sys-color-surface-dim: #ded8e4; + --md-sys-color-surface-bright: #fdf7fe; + --md-sys-color-surface-container-lowest: #ffffff; + --md-sys-color-surface-container-low: #f8f1fa; + --md-sys-color-surface-container: #f2ecf5; + --md-sys-color-surface-container-high: #ece6f0; + --md-sys-color-surface-container-highest: #e7e0ec; + --md-sys-color-on-surface: #000000; + --md-sys-color-on-surface-variant: #25232b; + --md-sys-color-outline: #45414b; + --md-sys-color-outline-variant: #54505a; + --md-sys-color-inverse-surface: #0f0d12; + --md-sys-color-inverse-on-surface: #ffffff; + --md-sys-color-primary: #281b49; + --md-sys-color-primary-dim: #1e103f; + --md-sys-color-on-primary: #e1d3ff; + --md-sys-color-primary-container: #584a7b; + --md-sys-color-on-primary-container: #ffffff; + --md-sys-color-primary-fixed: #584a7b; + --md-sys-color-primary-fixed-dim: #4c3f6f; + --md-sys-color-on-primary-fixed: #ffffff; + --md-sys-color-on-primary-fixed-variant: #ffffff; + --md-sys-color-inverse-primary: #d4c3fd; + --md-sys-color-secondary: #262134; + --md-sys-color-secondary-dim: #1c1729; + --md-sys-color-on-secondary: #dfd6ef; + --md-sys-color-secondary-container: #554f64; + --md-sys-color-on-secondary-container: #ffffff; + --md-sys-color-secondary-fixed: #554f64; + --md-sys-color-secondary-fixed-dim: #494358; + --md-sys-color-on-secondary-fixed: #ffffff; + --md-sys-color-on-secondary-fixed-variant: #ffffff; + --md-sys-color-tertiary: #391733; + --md-sys-color-tertiary-dim: #2e0d28; + --md-sys-color-on-tertiary: #ffcbee; + --md-sys-color-tertiary-container: #6d4563; + --md-sys-color-on-tertiary-container: #ffffff; + --md-sys-color-tertiary-fixed: #6d4563; + --md-sys-color-tertiary-fixed-dim: #603a57; + --md-sys-color-on-tertiary-fixed: #ffffff; + --md-sys-color-on-tertiary-fixed-variant: #ffffff; + --md-sys-color-error: #500016; + --md-sys-color-error-dim: #3d000f; + --md-sys-color-on-error: #ffced2; + --md-sys-color-error-container: #97283e; + --md-sys-color-on-error-container: #ffffff; + --md-sys-color-success: #002a18; + --md-sys-color-on-success: #7df0b4; + --md-sys-color-success-container: #005f3c; + --md-sys-color-on-success-container: #ffffff; + --md-sys-color-warning: #312100; + --md-sys-color-on-warning: #ffd486; + --md-sys-color-warning-container: #6c4d00; + --md-sys-color-on-warning-container: #ffffff; + --md-sys-color-info: #002252; + --md-sys-color-on-info: #ccdaff; + --md-sys-color-info-container: #004eac; + --md-sys-color-on-info-container: #ffffff; + --md-sys-color-inverse-error: #ffdddf; + --md-sys-color-inverse-success: #89fcbf; + --md-sys-color-inverse-warning: #ffe2b1; + --md-sys-color-inverse-info: #dde5ff; + } - --md-sys-color-background: #0f0d12; - --md-sys-color-on-background: #ffffff; - --md-sys-color-surface: #0f0d12; - --md-sys-color-surface-dim: #0f0d12; - --md-sys-color-surface-bright: #2e2b34; - --md-sys-color-surface-container-lowest: #000000; - --md-sys-color-surface-container-low: #141218; - --md-sys-color-surface-container: #1b181f; - --md-sys-color-surface-container-high: #211e26; - --md-sys-color-surface-container-highest: #27242d; - --md-sys-color-on-surface: #ffffff; - --md-sys-color-on-surface-variant: #eae3ef; - --md-sys-color-outline: #bcb6c2; - --md-sys-color-outline-variant: #a7a1ad; - --md-sys-color-inverse-surface: #fdf7fe; - --md-sys-color-inverse-on-surface: #000000; - --md-sys-color-primary: #ebe1ff; - --md-sys-color-primary-dim: #ded0fe; - --md-sys-color-on-primary: #302649; - --md-sys-color-primary-container: #aa9dc8; - --md-sys-color-on-primary-container: #000000; - --md-sys-color-primary-fixed: #ded0fe; - --md-sys-color-primary-fixed-dim: #d0c3ef; - --md-sys-color-on-primary-fixed: #000000; - --md-sys-color-on-primary-fixed-variant: #180e30; - --md-sys-color-inverse-primary: #3c3256; - --md-sys-color-secondary: #ebe1fb; - --md-sys-color-secondary-dim: #ddd3ed; - --md-sys-color-on-secondary: #2e293c; - --md-sys-color-secondary-container: #a8a0b8; - --md-sys-color-on-secondary-container: #000000; - --md-sys-color-secondary-fixed: #e8def8; - --md-sys-color-secondary-fixed-dim: #dad0ea; - --md-sys-color-on-secondary-fixed: #000000; - --md-sys-color-on-secondary-fixed-variant: #221d2f; - --md-sys-color-tertiary: #ffdbf2; - --md-sys-color-tertiary-dim: #fac5e9; - --md-sys-color-on-tertiary: #43203b; - --md-sys-color-tertiary-container: #f4bfe3; - --md-sys-color-on-tertiary-container: #2e0e29; - --md-sys-color-tertiary-fixed: #f4bfe3; - --md-sys-color-tertiary-fixed-dim: #e5b2d5; - --md-sys-color-on-tertiary-fixed: #000000; - --md-sys-color-on-tertiary-fixed-variant: #180015; - --md-sys-color-error: #ffdddf; - --md-sys-color-error-dim: #ffc7cb; - --md-sys-color-on-error: #5f001c; - --md-sys-color-error-container: #ff798c; - --md-sys-color-on-error-container: #000000; - --md-sys-color-success: #89fcbf; - --md-sys-color-on-success: #00341e; - --md-sys-color-success-container: #42b880; - --md-sys-color-on-success-container: #000000; - --md-sys-color-warning: #ffe2b1; - --md-sys-color-on-warning: #3c2900; - --md-sys-color-warning-container: #e4a604; - --md-sys-color-on-warning-container: #000000; - --md-sys-color-info: #dde5ff; - --md-sys-color-on-info: #002a62; - --md-sys-color-info-container: #74a2ff; - --md-sys-color-on-info-container: #000000; - --md-sys-color-inverse-error: #500016; - --md-sys-color-inverse-success: #002a18; - --md-sys-color-inverse-warning: #312100; - --md-sys-color-inverse-info: #002252; + [data-contrast='high'][data-theme='dark'], + [data-contrast='high'] [data-theme='dark'] { + color-scheme: dark; + + --md-sys-color-background: #0f0d12; + --md-sys-color-on-background: #ffffff; + --md-sys-color-surface: #0f0d12; + --md-sys-color-surface-dim: #0f0d12; + --md-sys-color-surface-bright: #2e2b34; + --md-sys-color-surface-container-lowest: #000000; + --md-sys-color-surface-container-low: #141218; + --md-sys-color-surface-container: #1b181f; + --md-sys-color-surface-container-high: #211e26; + --md-sys-color-surface-container-highest: #27242d; + --md-sys-color-on-surface: #ffffff; + --md-sys-color-on-surface-variant: #eae3ef; + --md-sys-color-outline: #bcb6c2; + --md-sys-color-outline-variant: #a7a1ad; + --md-sys-color-inverse-surface: #fdf7fe; + --md-sys-color-inverse-on-surface: #000000; + --md-sys-color-primary: #ebe1ff; + --md-sys-color-primary-dim: #ded0fe; + --md-sys-color-on-primary: #302649; + --md-sys-color-primary-container: #aa9dc8; + --md-sys-color-on-primary-container: #000000; + --md-sys-color-primary-fixed: #ded0fe; + --md-sys-color-primary-fixed-dim: #d0c3ef; + --md-sys-color-on-primary-fixed: #000000; + --md-sys-color-on-primary-fixed-variant: #180e30; + --md-sys-color-inverse-primary: #3c3256; + --md-sys-color-secondary: #ebe1fb; + --md-sys-color-secondary-dim: #ddd3ed; + --md-sys-color-on-secondary: #2e293c; + --md-sys-color-secondary-container: #a8a0b8; + --md-sys-color-on-secondary-container: #000000; + --md-sys-color-secondary-fixed: #e8def8; + --md-sys-color-secondary-fixed-dim: #dad0ea; + --md-sys-color-on-secondary-fixed: #000000; + --md-sys-color-on-secondary-fixed-variant: #221d2f; + --md-sys-color-tertiary: #ffdbf2; + --md-sys-color-tertiary-dim: #fac5e9; + --md-sys-color-on-tertiary: #43203b; + --md-sys-color-tertiary-container: #f4bfe3; + --md-sys-color-on-tertiary-container: #2e0e29; + --md-sys-color-tertiary-fixed: #f4bfe3; + --md-sys-color-tertiary-fixed-dim: #e5b2d5; + --md-sys-color-on-tertiary-fixed: #000000; + --md-sys-color-on-tertiary-fixed-variant: #180015; + --md-sys-color-error: #ffdddf; + --md-sys-color-error-dim: #ffc7cb; + --md-sys-color-on-error: #5f001c; + --md-sys-color-error-container: #ff798c; + --md-sys-color-on-error-container: #000000; + --md-sys-color-success: #89fcbf; + --md-sys-color-on-success: #00341e; + --md-sys-color-success-container: #42b880; + --md-sys-color-on-success-container: #000000; + --md-sys-color-warning: #ffe2b1; + --md-sys-color-on-warning: #3c2900; + --md-sys-color-warning-container: #e4a604; + --md-sys-color-on-warning-container: #000000; + --md-sys-color-info: #dde5ff; + --md-sys-color-on-info: #002a62; + --md-sys-color-info-container: #74a2ff; + --md-sys-color-on-info-container: #000000; + --md-sys-color-inverse-error: #500016; + --md-sys-color-inverse-success: #002a18; + --md-sys-color-inverse-warning: #312100; + --md-sys-color-inverse-info: #002252; + } } diff --git a/resources/css/tokens/shape.css b/resources/css/tokens/shape.css index af637c6b..e0636801 100644 --- a/resources/css/tokens/shape.css +++ b/resources/css/tokens/shape.css @@ -7,15 +7,20 @@ * * As utilities: rounded-corner-xs … rounded-corner-full. */ -:root { - --md-sys-shape-corner-none: 0px; - --md-sys-shape-corner-xs: 4px; - --md-sys-shape-corner-sm: 8px; - --md-sys-shape-corner-md: 12px; - --md-sys-shape-corner-lg: 16px; - --md-sys-shape-corner-lg-increased: 20px; - --md-sys-shape-corner-xl: 28px; - --md-sys-shape-corner-xl-increased: 32px; - --md-sys-shape-corner-xxl: 48px; - --md-sys-shape-corner-full: 9999px; + +@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility; + +@layer material.tokens { + :root { + --md-sys-shape-corner-none: 0px; + --md-sys-shape-corner-xs: 4px; + --md-sys-shape-corner-sm: 8px; + --md-sys-shape-corner-md: 12px; + --md-sys-shape-corner-lg: 16px; + --md-sys-shape-corner-lg-increased: 20px; + --md-sys-shape-corner-xl: 28px; + --md-sys-shape-corner-xl-increased: 32px; + --md-sys-shape-corner-xxl: 48px; + --md-sys-shape-corner-full: 9999px; + } } diff --git a/resources/css/tokens/state.css b/resources/css/tokens/state.css index 39f33e9b..3b1f0a5a 100644 --- a/resources/css/tokens/state.css +++ b/resources/css/tokens/state.css @@ -5,108 +5,20 @@ * element at 8% on hover, 10% on focus and press, 16% while dragged; disabled content at 38% * and a disabled container at 12%. * - * `state-layer` puts M3's state layer on an element. Hover only where the pointer can hover, - * because a touch screen keeps the last hover after a tap; `data-dragged` on the element draws - * the dragged layer. The layer is a ::before, so the element becomes `position: relative` and - * `isolation: isolate`; never pass `static` or a z-index to it. - * - * `touch-target` extends an element's pointer target to M3's 48×48px minimum without changing - * what is drawn: a ::after centred on it, at least 48px each way. It is `::after`-based, so a - * component that already draws with ::after wraps its control in an element carrying it. - * - * `focus-ring` is M3's focus indicator — 3px in secondary, 2px out, drawn only for keyboard - * focus. The site gives no thickness or offset; the values are @material/web's focus ring - * tokens (Apache-2.0, © Google LLC). `link` is a link in running text: underlined in the - * colour of the words around it. + * The classes that draw them are foundation/interaction.css (`md-state-layer`, `md-focus-ring`, + * `md-touch-target`, `md-link`), and utilities.css for the components still written in Tailwind. + * The tokens sit in `material.tokens`, so an application's own unlayered declaration wins. */ -:root { - --md-sys-state-hover-state-layer-opacity: 0.08; - --md-sys-state-focus-state-layer-opacity: 0.1; - --md-sys-state-pressed-state-layer-opacity: 0.1; - --md-sys-state-dragged-state-layer-opacity: 0.16; - --md-sys-state-disabled-content-opacity: 0.38; - --md-sys-state-disabled-container-opacity: 0.12; -} +@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility; -@utility state-layer { - position: relative; - isolation: isolate; - - &::before { - content: ''; - position: absolute; - inset: 0; - z-index: -1; - border-radius: inherit; - background-color: currentColor; - opacity: 0; - pointer-events: none; - transition: opacity var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast); - } - - @media (hover: hover) { - &:hover::before { - opacity: var(--md-sys-state-hover-state-layer-opacity); - } - } - - &:focus-visible::before { - opacity: var(--md-sys-state-focus-state-layer-opacity); - } - - &:active::before { - opacity: var(--md-sys-state-pressed-state-layer-opacity); - } - - &[data-dragged]::before { - opacity: var(--md-sys-state-dragged-state-layer-opacity); - } - - &:disabled::before, - &[aria-disabled='true']::before { - display: none; +@layer material.tokens { + :root { + --md-sys-state-hover-state-layer-opacity: 0.08; + --md-sys-state-focus-state-layer-opacity: 0.1; + --md-sys-state-pressed-state-layer-opacity: 0.1; + --md-sys-state-dragged-state-layer-opacity: 0.16; + --md-sys-state-disabled-content-opacity: 0.38; + --md-sys-state-disabled-container-opacity: 0.12; } } - -@utility touch-target { - position: relative; - - &::after { - content: ''; - position: absolute; - top: 50%; - left: 50%; - width: 100%; - height: 100%; - min-width: 3rem; - min-height: 3rem; - transform: translate(-50%, -50%); - } -} - -@utility focus-ring { - outline: none; - - &:focus-visible { - outline: 3px solid var(--md-sys-color-secondary); - outline-offset: 2px; - } -} - -@utility link { - cursor: pointer; - text-decoration-line: underline; - text-decoration-thickness: from-font; - text-underline-offset: 0.15em; - border-radius: var(--md-sys-shape-corner-xs); - - &:focus-visible { - outline: 3px solid var(--md-sys-color-secondary); - outline-offset: 2px; - } -} - -[x-cloak] { - display: none !important; -} diff --git a/resources/css/tokens/type.css b/resources/css/tokens/type.css index cfbd0264..f49b5f35 100644 --- a/resources/css/tokens/type.css +++ b/resources/css/tokens/type.css @@ -4,265 +4,90 @@ * Sizes, line heights, weights and tracking are androidx Compose Material 3's own token values * (tokens/TypeScaleTokens.kt, Apache-2.0, © Google LLC, androidx-main @ 1608250), converted as * M3's units table says: rem = sp / 16, tracking in rem as well (docs/reference/m3/styles.md - * § Typography). A style is used through its utility, never by hand-assembling `text-*`, - * `leading-*` and `tracking-*`: + * § Typography). A style is used whole, through its class, never by hand-assembling a size, a + * line height and a tracking: * - *

- *

+ *

+ *

+ * + * The `md-type-*` classes are text.css; the `type-*` utilities of the components still written in + * Tailwind are utilities.css, with the same declarations. * * The brand typeface is Google Sans Flex (font.css). Emphasized styles are one weight step * heavier and fully rounded ("ROND" 100) — the axis that typeface exists for, verified by * `npm run check:font` — and they carry their own tracking, because M3 widens four of them and * takes Display Large's negative tracking back to zero. - * - * M3 asks for emphasis one element at a time, and `font-variation-settings` inherits, so every - * regular utility resets it: body copy inside an emphasized heading reads as itself again. */ -:root { - --md-ref-typeface-brand: 'Google Sans Flex', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; - --md-ref-typeface-plain: var(--md-ref-typeface-brand); - --md-ref-typeface-weight-regular: 400; - --md-ref-typeface-weight-medium: 500; - --md-ref-typeface-weight-bold: 700; - --md-sys-typescale-display-lg: var(--md-ref-typeface-weight-regular) 3.5625rem/4rem var(--md-ref-typeface-brand); - --md-sys-typescale-display-lg-tracking: -0.0125rem; - --md-sys-typescale-emphasized-display-lg: var(--md-ref-typeface-weight-medium) 3.5625rem/4rem var(--md-ref-typeface-brand); - --md-sys-typescale-emphasized-display-lg-tracking: 0rem; - --md-sys-typescale-display-md: var(--md-ref-typeface-weight-regular) 2.8125rem/3.25rem var(--md-ref-typeface-brand); - --md-sys-typescale-display-md-tracking: 0rem; - --md-sys-typescale-emphasized-display-md: var(--md-ref-typeface-weight-medium) 2.8125rem/3.25rem var(--md-ref-typeface-brand); - --md-sys-typescale-emphasized-display-md-tracking: 0rem; - --md-sys-typescale-display-sm: var(--md-ref-typeface-weight-regular) 2.25rem/2.75rem var(--md-ref-typeface-brand); - --md-sys-typescale-display-sm-tracking: 0rem; - --md-sys-typescale-emphasized-display-sm: var(--md-ref-typeface-weight-medium) 2.25rem/2.75rem var(--md-ref-typeface-brand); - --md-sys-typescale-emphasized-display-sm-tracking: 0rem; - --md-sys-typescale-headline-lg: var(--md-ref-typeface-weight-regular) 2rem/2.5rem var(--md-ref-typeface-brand); - --md-sys-typescale-headline-lg-tracking: 0rem; - --md-sys-typescale-emphasized-headline-lg: var(--md-ref-typeface-weight-medium) 2rem/2.5rem var(--md-ref-typeface-brand); - --md-sys-typescale-emphasized-headline-lg-tracking: 0rem; - --md-sys-typescale-headline-md: var(--md-ref-typeface-weight-regular) 1.75rem/2.25rem var(--md-ref-typeface-brand); - --md-sys-typescale-headline-md-tracking: 0rem; - --md-sys-typescale-emphasized-headline-md: var(--md-ref-typeface-weight-medium) 1.75rem/2.25rem var(--md-ref-typeface-brand); - --md-sys-typescale-emphasized-headline-md-tracking: 0rem; - --md-sys-typescale-headline-sm: var(--md-ref-typeface-weight-regular) 1.5rem/2rem var(--md-ref-typeface-brand); - --md-sys-typescale-headline-sm-tracking: 0rem; - --md-sys-typescale-emphasized-headline-sm: var(--md-ref-typeface-weight-medium) 1.5rem/2rem var(--md-ref-typeface-brand); - --md-sys-typescale-emphasized-headline-sm-tracking: 0rem; - --md-sys-typescale-title-lg: var(--md-ref-typeface-weight-regular) 1.375rem/1.75rem var(--md-ref-typeface-brand); - --md-sys-typescale-title-lg-tracking: 0rem; - --md-sys-typescale-emphasized-title-lg: var(--md-ref-typeface-weight-medium) 1.375rem/1.75rem var(--md-ref-typeface-brand); - --md-sys-typescale-emphasized-title-lg-tracking: 0rem; - --md-sys-typescale-title-md: var(--md-ref-typeface-weight-medium) 1rem/1.5rem var(--md-ref-typeface-plain); - --md-sys-typescale-title-md-tracking: 0.0125rem; - --md-sys-typescale-emphasized-title-md: var(--md-ref-typeface-weight-bold) 1rem/1.5rem var(--md-ref-typeface-plain); - --md-sys-typescale-emphasized-title-md-tracking: 0.009375rem; - --md-sys-typescale-title-sm: var(--md-ref-typeface-weight-medium) 0.875rem/1.25rem var(--md-ref-typeface-plain); - --md-sys-typescale-title-sm-tracking: 0.00625rem; - --md-sys-typescale-emphasized-title-sm: var(--md-ref-typeface-weight-bold) 0.875rem/1.25rem var(--md-ref-typeface-plain); - --md-sys-typescale-emphasized-title-sm-tracking: 0.00625rem; - --md-sys-typescale-body-lg: var(--md-ref-typeface-weight-regular) 1rem/1.5rem var(--md-ref-typeface-plain); - --md-sys-typescale-body-lg-tracking: 0.03125rem; - --md-sys-typescale-emphasized-body-lg: var(--md-ref-typeface-weight-medium) 1rem/1.5rem var(--md-ref-typeface-plain); - --md-sys-typescale-emphasized-body-lg-tracking: 0.009375rem; - --md-sys-typescale-body-md: var(--md-ref-typeface-weight-regular) 0.875rem/1.25rem var(--md-ref-typeface-plain); - --md-sys-typescale-body-md-tracking: 0.0125rem; - --md-sys-typescale-emphasized-body-md: var(--md-ref-typeface-weight-medium) 0.875rem/1.25rem var(--md-ref-typeface-plain); - --md-sys-typescale-emphasized-body-md-tracking: 0.015625rem; - --md-sys-typescale-body-sm: var(--md-ref-typeface-weight-regular) 0.75rem/1rem var(--md-ref-typeface-plain); - --md-sys-typescale-body-sm-tracking: 0.025rem; - --md-sys-typescale-emphasized-body-sm: var(--md-ref-typeface-weight-medium) 0.75rem/1rem var(--md-ref-typeface-plain); - --md-sys-typescale-emphasized-body-sm-tracking: 0.025rem; - --md-sys-typescale-label-lg: var(--md-ref-typeface-weight-medium) 0.875rem/1.25rem var(--md-ref-typeface-plain); - --md-sys-typescale-label-lg-tracking: 0.00625rem; - --md-sys-typescale-emphasized-label-lg: var(--md-ref-typeface-weight-bold) 0.875rem/1.25rem var(--md-ref-typeface-plain); - --md-sys-typescale-emphasized-label-lg-tracking: 0.00625rem; - --md-sys-typescale-label-md: var(--md-ref-typeface-weight-medium) 0.75rem/1rem var(--md-ref-typeface-plain); - --md-sys-typescale-label-md-tracking: 0.03125rem; - --md-sys-typescale-emphasized-label-md: var(--md-ref-typeface-weight-bold) 0.75rem/1rem var(--md-ref-typeface-plain); - --md-sys-typescale-emphasized-label-md-tracking: 0.03125rem; - --md-sys-typescale-label-sm: var(--md-ref-typeface-weight-medium) 0.6875rem/1rem var(--md-ref-typeface-plain); - --md-sys-typescale-label-sm-tracking: 0.03125rem; - --md-sys-typescale-emphasized-label-sm: var(--md-ref-typeface-weight-bold) 0.6875rem/1rem var(--md-ref-typeface-plain); - --md-sys-typescale-emphasized-label-sm-tracking: 0.03125rem; -} +@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility; -@utility type-display-lg { - font: var(--md-sys-typescale-display-lg); - letter-spacing: var(--md-sys-typescale-display-lg-tracking); - font-variation-settings: normal; -} +@layer material.tokens { + :root { + --md-ref-typeface-brand: 'Google Sans Flex', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; + --md-ref-typeface-plain: var(--md-ref-typeface-brand); + --md-ref-typeface-weight-regular: 400; + --md-ref-typeface-weight-medium: 500; + --md-ref-typeface-weight-bold: 700; -@utility type-emphasized-display-lg { - font: var(--md-sys-typescale-emphasized-display-lg); - letter-spacing: var(--md-sys-typescale-emphasized-display-lg-tracking); - font-variation-settings: "ROND" 100; -} - -@utility type-display-md { - font: var(--md-sys-typescale-display-md); - letter-spacing: var(--md-sys-typescale-display-md-tracking); - font-variation-settings: normal; -} - -@utility type-emphasized-display-md { - font: var(--md-sys-typescale-emphasized-display-md); - letter-spacing: var(--md-sys-typescale-emphasized-display-md-tracking); - font-variation-settings: "ROND" 100; -} - -@utility type-display-sm { - font: var(--md-sys-typescale-display-sm); - letter-spacing: var(--md-sys-typescale-display-sm-tracking); - font-variation-settings: normal; -} - -@utility type-emphasized-display-sm { - font: var(--md-sys-typescale-emphasized-display-sm); - letter-spacing: var(--md-sys-typescale-emphasized-display-sm-tracking); - font-variation-settings: "ROND" 100; -} - -@utility type-headline-lg { - font: var(--md-sys-typescale-headline-lg); - letter-spacing: var(--md-sys-typescale-headline-lg-tracking); - font-variation-settings: normal; -} - -@utility type-emphasized-headline-lg { - font: var(--md-sys-typescale-emphasized-headline-lg); - letter-spacing: var(--md-sys-typescale-emphasized-headline-lg-tracking); - font-variation-settings: "ROND" 100; -} - -@utility type-headline-md { - font: var(--md-sys-typescale-headline-md); - letter-spacing: var(--md-sys-typescale-headline-md-tracking); - font-variation-settings: normal; -} - -@utility type-emphasized-headline-md { - font: var(--md-sys-typescale-emphasized-headline-md); - letter-spacing: var(--md-sys-typescale-emphasized-headline-md-tracking); - font-variation-settings: "ROND" 100; -} - -@utility type-headline-sm { - font: var(--md-sys-typescale-headline-sm); - letter-spacing: var(--md-sys-typescale-headline-sm-tracking); - font-variation-settings: normal; -} - -@utility type-emphasized-headline-sm { - font: var(--md-sys-typescale-emphasized-headline-sm); - letter-spacing: var(--md-sys-typescale-emphasized-headline-sm-tracking); - font-variation-settings: "ROND" 100; -} - -@utility type-title-lg { - font: var(--md-sys-typescale-title-lg); - letter-spacing: var(--md-sys-typescale-title-lg-tracking); - font-variation-settings: normal; -} - -@utility type-emphasized-title-lg { - font: var(--md-sys-typescale-emphasized-title-lg); - letter-spacing: var(--md-sys-typescale-emphasized-title-lg-tracking); - font-variation-settings: "ROND" 100; -} - -@utility type-title-md { - font: var(--md-sys-typescale-title-md); - letter-spacing: var(--md-sys-typescale-title-md-tracking); - font-variation-settings: normal; -} - -@utility type-emphasized-title-md { - font: var(--md-sys-typescale-emphasized-title-md); - letter-spacing: var(--md-sys-typescale-emphasized-title-md-tracking); - font-variation-settings: "ROND" 100; -} - -@utility type-title-sm { - font: var(--md-sys-typescale-title-sm); - letter-spacing: var(--md-sys-typescale-title-sm-tracking); - font-variation-settings: normal; -} - -@utility type-emphasized-title-sm { - font: var(--md-sys-typescale-emphasized-title-sm); - letter-spacing: var(--md-sys-typescale-emphasized-title-sm-tracking); - font-variation-settings: "ROND" 100; -} - -@utility type-body-lg { - font: var(--md-sys-typescale-body-lg); - letter-spacing: var(--md-sys-typescale-body-lg-tracking); - font-variation-settings: normal; -} - -@utility type-emphasized-body-lg { - font: var(--md-sys-typescale-emphasized-body-lg); - letter-spacing: var(--md-sys-typescale-emphasized-body-lg-tracking); - font-variation-settings: "ROND" 100; -} - -@utility type-body-md { - font: var(--md-sys-typescale-body-md); - letter-spacing: var(--md-sys-typescale-body-md-tracking); - font-variation-settings: normal; -} - -@utility type-emphasized-body-md { - font: var(--md-sys-typescale-emphasized-body-md); - letter-spacing: var(--md-sys-typescale-emphasized-body-md-tracking); - font-variation-settings: "ROND" 100; -} - -@utility type-body-sm { - font: var(--md-sys-typescale-body-sm); - letter-spacing: var(--md-sys-typescale-body-sm-tracking); - font-variation-settings: normal; -} - -@utility type-emphasized-body-sm { - font: var(--md-sys-typescale-emphasized-body-sm); - letter-spacing: var(--md-sys-typescale-emphasized-body-sm-tracking); - font-variation-settings: "ROND" 100; -} - -@utility type-label-lg { - font: var(--md-sys-typescale-label-lg); - letter-spacing: var(--md-sys-typescale-label-lg-tracking); - font-variation-settings: normal; -} - -@utility type-emphasized-label-lg { - font: var(--md-sys-typescale-emphasized-label-lg); - letter-spacing: var(--md-sys-typescale-emphasized-label-lg-tracking); - font-variation-settings: "ROND" 100; -} - -@utility type-label-md { - font: var(--md-sys-typescale-label-md); - letter-spacing: var(--md-sys-typescale-label-md-tracking); - font-variation-settings: normal; -} - -@utility type-emphasized-label-md { - font: var(--md-sys-typescale-emphasized-label-md); - letter-spacing: var(--md-sys-typescale-emphasized-label-md-tracking); - font-variation-settings: "ROND" 100; -} - -@utility type-label-sm { - font: var(--md-sys-typescale-label-sm); - letter-spacing: var(--md-sys-typescale-label-sm-tracking); - font-variation-settings: normal; -} - -@utility type-emphasized-label-sm { - font: var(--md-sys-typescale-emphasized-label-sm); - letter-spacing: var(--md-sys-typescale-emphasized-label-sm-tracking); - font-variation-settings: "ROND" 100; + --md-sys-typescale-display-lg: var(--md-ref-typeface-weight-regular) 3.5625rem/4rem var(--md-ref-typeface-brand); + --md-sys-typescale-display-lg-tracking: -0.0125rem; + --md-sys-typescale-emphasized-display-lg: var(--md-ref-typeface-weight-medium) 3.5625rem/4rem var(--md-ref-typeface-brand); + --md-sys-typescale-emphasized-display-lg-tracking: 0rem; + --md-sys-typescale-display-md: var(--md-ref-typeface-weight-regular) 2.8125rem/3.25rem var(--md-ref-typeface-brand); + --md-sys-typescale-display-md-tracking: 0rem; + --md-sys-typescale-emphasized-display-md: var(--md-ref-typeface-weight-medium) 2.8125rem/3.25rem var(--md-ref-typeface-brand); + --md-sys-typescale-emphasized-display-md-tracking: 0rem; + --md-sys-typescale-display-sm: var(--md-ref-typeface-weight-regular) 2.25rem/2.75rem var(--md-ref-typeface-brand); + --md-sys-typescale-display-sm-tracking: 0rem; + --md-sys-typescale-emphasized-display-sm: var(--md-ref-typeface-weight-medium) 2.25rem/2.75rem var(--md-ref-typeface-brand); + --md-sys-typescale-emphasized-display-sm-tracking: 0rem; + --md-sys-typescale-headline-lg: var(--md-ref-typeface-weight-regular) 2rem/2.5rem var(--md-ref-typeface-brand); + --md-sys-typescale-headline-lg-tracking: 0rem; + --md-sys-typescale-emphasized-headline-lg: var(--md-ref-typeface-weight-medium) 2rem/2.5rem var(--md-ref-typeface-brand); + --md-sys-typescale-emphasized-headline-lg-tracking: 0rem; + --md-sys-typescale-headline-md: var(--md-ref-typeface-weight-regular) 1.75rem/2.25rem var(--md-ref-typeface-brand); + --md-sys-typescale-headline-md-tracking: 0rem; + --md-sys-typescale-emphasized-headline-md: var(--md-ref-typeface-weight-medium) 1.75rem/2.25rem var(--md-ref-typeface-brand); + --md-sys-typescale-emphasized-headline-md-tracking: 0rem; + --md-sys-typescale-headline-sm: var(--md-ref-typeface-weight-regular) 1.5rem/2rem var(--md-ref-typeface-brand); + --md-sys-typescale-headline-sm-tracking: 0rem; + --md-sys-typescale-emphasized-headline-sm: var(--md-ref-typeface-weight-medium) 1.5rem/2rem var(--md-ref-typeface-brand); + --md-sys-typescale-emphasized-headline-sm-tracking: 0rem; + --md-sys-typescale-title-lg: var(--md-ref-typeface-weight-regular) 1.375rem/1.75rem var(--md-ref-typeface-brand); + --md-sys-typescale-title-lg-tracking: 0rem; + --md-sys-typescale-emphasized-title-lg: var(--md-ref-typeface-weight-medium) 1.375rem/1.75rem var(--md-ref-typeface-brand); + --md-sys-typescale-emphasized-title-lg-tracking: 0rem; + --md-sys-typescale-title-md: var(--md-ref-typeface-weight-medium) 1rem/1.5rem var(--md-ref-typeface-plain); + --md-sys-typescale-title-md-tracking: 0.0125rem; + --md-sys-typescale-emphasized-title-md: var(--md-ref-typeface-weight-bold) 1rem/1.5rem var(--md-ref-typeface-plain); + --md-sys-typescale-emphasized-title-md-tracking: 0.009375rem; + --md-sys-typescale-title-sm: var(--md-ref-typeface-weight-medium) 0.875rem/1.25rem var(--md-ref-typeface-plain); + --md-sys-typescale-title-sm-tracking: 0.00625rem; + --md-sys-typescale-emphasized-title-sm: var(--md-ref-typeface-weight-bold) 0.875rem/1.25rem var(--md-ref-typeface-plain); + --md-sys-typescale-emphasized-title-sm-tracking: 0.00625rem; + --md-sys-typescale-body-lg: var(--md-ref-typeface-weight-regular) 1rem/1.5rem var(--md-ref-typeface-plain); + --md-sys-typescale-body-lg-tracking: 0.03125rem; + --md-sys-typescale-emphasized-body-lg: var(--md-ref-typeface-weight-medium) 1rem/1.5rem var(--md-ref-typeface-plain); + --md-sys-typescale-emphasized-body-lg-tracking: 0.009375rem; + --md-sys-typescale-body-md: var(--md-ref-typeface-weight-regular) 0.875rem/1.25rem var(--md-ref-typeface-plain); + --md-sys-typescale-body-md-tracking: 0.0125rem; + --md-sys-typescale-emphasized-body-md: var(--md-ref-typeface-weight-medium) 0.875rem/1.25rem var(--md-ref-typeface-plain); + --md-sys-typescale-emphasized-body-md-tracking: 0.015625rem; + --md-sys-typescale-body-sm: var(--md-ref-typeface-weight-regular) 0.75rem/1rem var(--md-ref-typeface-plain); + --md-sys-typescale-body-sm-tracking: 0.025rem; + --md-sys-typescale-emphasized-body-sm: var(--md-ref-typeface-weight-medium) 0.75rem/1rem var(--md-ref-typeface-plain); + --md-sys-typescale-emphasized-body-sm-tracking: 0.025rem; + --md-sys-typescale-label-lg: var(--md-ref-typeface-weight-medium) 0.875rem/1.25rem var(--md-ref-typeface-plain); + --md-sys-typescale-label-lg-tracking: 0.00625rem; + --md-sys-typescale-emphasized-label-lg: var(--md-ref-typeface-weight-bold) 0.875rem/1.25rem var(--md-ref-typeface-plain); + --md-sys-typescale-emphasized-label-lg-tracking: 0.00625rem; + --md-sys-typescale-label-md: var(--md-ref-typeface-weight-medium) 0.75rem/1rem var(--md-ref-typeface-plain); + --md-sys-typescale-label-md-tracking: 0.03125rem; + --md-sys-typescale-emphasized-label-md: var(--md-ref-typeface-weight-bold) 0.75rem/1rem var(--md-ref-typeface-plain); + --md-sys-typescale-emphasized-label-md-tracking: 0.03125rem; + --md-sys-typescale-label-sm: var(--md-ref-typeface-weight-medium) 0.6875rem/1rem var(--md-ref-typeface-plain); + --md-sys-typescale-label-sm-tracking: 0.03125rem; + --md-sys-typescale-emphasized-label-sm: var(--md-ref-typeface-weight-bold) 0.6875rem/1rem var(--md-ref-typeface-plain); + --md-sys-typescale-emphasized-label-sm-tracking: 0.03125rem; + } } diff --git a/resources/css/tokens/utilities.css b/resources/css/tokens/utilities.css new file mode 100644 index 00000000..55f77aa3 --- /dev/null +++ b/resources/css/tokens/utilities.css @@ -0,0 +1,286 @@ +/* + * The token utilities, for the components still written in Tailwind. + * + * Tailwind's side of tokens/state.css and tokens/type.css, kept apart so that the token files the + * foundation imports carry no Tailwind directive. material.css imports this file after Tailwind; + * it goes when the last component leaves Tailwind. New markup uses the plain-CSS classes instead, + * which carry the same declarations: `md-state-layer`, `md-focus-ring`, `md-touch-target` and + * `md-link` (foundation/interaction.css), and `md-type-*` (text.css). + * + * `state-layer` puts M3's state layer on an element. Hover only where the pointer can hover, + * because a touch screen keeps the last hover after a tap; `data-dragged` on the element draws + * the dragged layer. The layer is a ::before, so the element becomes `position: relative` and + * `isolation: isolate`; never pass `static` or a z-index to it. + * + * `touch-target` extends an element's pointer target to M3's 48×48px minimum without changing + * what is drawn: a ::after centred on it, at least 48px each way. It is `::after`-based, so a + * component that already draws with ::after wraps its control in an element carrying it. + * + * `focus-ring` is M3's focus indicator — 3px in secondary, 2px out, drawn only for keyboard + * focus. The site gives no thickness or offset; the values are @material/web's focus ring + * tokens (Apache-2.0, © Google LLC). `link` is a link in running text: underlined in the + * colour of the words around it. + * + * `type-*` is one of M3's 30 type styles (tokens/type.css): size, line height, weight and + * tracking together. M3 asks for emphasis one element at a time, and `font-variation-settings` + * inherits, so every regular utility resets it: body copy inside an emphasized heading reads as + * itself again. + */ + +@utility state-layer { + position: relative; + isolation: isolate; + + &::before { + content: ''; + position: absolute; + inset: 0; + z-index: -1; + border-radius: inherit; + background-color: currentColor; + opacity: 0; + pointer-events: none; + transition: opacity var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast); + } + + @media (hover: hover) { + &:hover::before { + opacity: var(--md-sys-state-hover-state-layer-opacity); + } + } + + &:focus-visible::before { + opacity: var(--md-sys-state-focus-state-layer-opacity); + } + + &:active::before { + opacity: var(--md-sys-state-pressed-state-layer-opacity); + } + + &[data-dragged]::before { + opacity: var(--md-sys-state-dragged-state-layer-opacity); + } + + &:disabled::before, + &[aria-disabled='true']::before { + display: none; + } +} + +@utility touch-target { + position: relative; + + &::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 100%; + height: 100%; + min-width: 3rem; + min-height: 3rem; + transform: translate(-50%, -50%); + } +} + +@utility focus-ring { + outline: none; + + &:focus-visible { + outline: 3px solid var(--md-sys-color-secondary); + outline-offset: 2px; + } +} + +@utility link { + cursor: pointer; + text-decoration-line: underline; + text-decoration-thickness: from-font; + text-underline-offset: 0.15em; + border-radius: var(--md-sys-shape-corner-xs); + + &:focus-visible { + outline: 3px solid var(--md-sys-color-secondary); + outline-offset: 2px; + } +} + +@utility type-display-lg { + font: var(--md-sys-typescale-display-lg); + letter-spacing: var(--md-sys-typescale-display-lg-tracking); + font-variation-settings: normal; +} + +@utility type-emphasized-display-lg { + font: var(--md-sys-typescale-emphasized-display-lg); + letter-spacing: var(--md-sys-typescale-emphasized-display-lg-tracking); + font-variation-settings: "ROND" 100; +} + +@utility type-display-md { + font: var(--md-sys-typescale-display-md); + letter-spacing: var(--md-sys-typescale-display-md-tracking); + font-variation-settings: normal; +} + +@utility type-emphasized-display-md { + font: var(--md-sys-typescale-emphasized-display-md); + letter-spacing: var(--md-sys-typescale-emphasized-display-md-tracking); + font-variation-settings: "ROND" 100; +} + +@utility type-display-sm { + font: var(--md-sys-typescale-display-sm); + letter-spacing: var(--md-sys-typescale-display-sm-tracking); + font-variation-settings: normal; +} + +@utility type-emphasized-display-sm { + font: var(--md-sys-typescale-emphasized-display-sm); + letter-spacing: var(--md-sys-typescale-emphasized-display-sm-tracking); + font-variation-settings: "ROND" 100; +} + +@utility type-headline-lg { + font: var(--md-sys-typescale-headline-lg); + letter-spacing: var(--md-sys-typescale-headline-lg-tracking); + font-variation-settings: normal; +} + +@utility type-emphasized-headline-lg { + font: var(--md-sys-typescale-emphasized-headline-lg); + letter-spacing: var(--md-sys-typescale-emphasized-headline-lg-tracking); + font-variation-settings: "ROND" 100; +} + +@utility type-headline-md { + font: var(--md-sys-typescale-headline-md); + letter-spacing: var(--md-sys-typescale-headline-md-tracking); + font-variation-settings: normal; +} + +@utility type-emphasized-headline-md { + font: var(--md-sys-typescale-emphasized-headline-md); + letter-spacing: var(--md-sys-typescale-emphasized-headline-md-tracking); + font-variation-settings: "ROND" 100; +} + +@utility type-headline-sm { + font: var(--md-sys-typescale-headline-sm); + letter-spacing: var(--md-sys-typescale-headline-sm-tracking); + font-variation-settings: normal; +} + +@utility type-emphasized-headline-sm { + font: var(--md-sys-typescale-emphasized-headline-sm); + letter-spacing: var(--md-sys-typescale-emphasized-headline-sm-tracking); + font-variation-settings: "ROND" 100; +} + +@utility type-title-lg { + font: var(--md-sys-typescale-title-lg); + letter-spacing: var(--md-sys-typescale-title-lg-tracking); + font-variation-settings: normal; +} + +@utility type-emphasized-title-lg { + font: var(--md-sys-typescale-emphasized-title-lg); + letter-spacing: var(--md-sys-typescale-emphasized-title-lg-tracking); + font-variation-settings: "ROND" 100; +} + +@utility type-title-md { + font: var(--md-sys-typescale-title-md); + letter-spacing: var(--md-sys-typescale-title-md-tracking); + font-variation-settings: normal; +} + +@utility type-emphasized-title-md { + font: var(--md-sys-typescale-emphasized-title-md); + letter-spacing: var(--md-sys-typescale-emphasized-title-md-tracking); + font-variation-settings: "ROND" 100; +} + +@utility type-title-sm { + font: var(--md-sys-typescale-title-sm); + letter-spacing: var(--md-sys-typescale-title-sm-tracking); + font-variation-settings: normal; +} + +@utility type-emphasized-title-sm { + font: var(--md-sys-typescale-emphasized-title-sm); + letter-spacing: var(--md-sys-typescale-emphasized-title-sm-tracking); + font-variation-settings: "ROND" 100; +} + +@utility type-body-lg { + font: var(--md-sys-typescale-body-lg); + letter-spacing: var(--md-sys-typescale-body-lg-tracking); + font-variation-settings: normal; +} + +@utility type-emphasized-body-lg { + font: var(--md-sys-typescale-emphasized-body-lg); + letter-spacing: var(--md-sys-typescale-emphasized-body-lg-tracking); + font-variation-settings: "ROND" 100; +} + +@utility type-body-md { + font: var(--md-sys-typescale-body-md); + letter-spacing: var(--md-sys-typescale-body-md-tracking); + font-variation-settings: normal; +} + +@utility type-emphasized-body-md { + font: var(--md-sys-typescale-emphasized-body-md); + letter-spacing: var(--md-sys-typescale-emphasized-body-md-tracking); + font-variation-settings: "ROND" 100; +} + +@utility type-body-sm { + font: var(--md-sys-typescale-body-sm); + letter-spacing: var(--md-sys-typescale-body-sm-tracking); + font-variation-settings: normal; +} + +@utility type-emphasized-body-sm { + font: var(--md-sys-typescale-emphasized-body-sm); + letter-spacing: var(--md-sys-typescale-emphasized-body-sm-tracking); + font-variation-settings: "ROND" 100; +} + +@utility type-label-lg { + font: var(--md-sys-typescale-label-lg); + letter-spacing: var(--md-sys-typescale-label-lg-tracking); + font-variation-settings: normal; +} + +@utility type-emphasized-label-lg { + font: var(--md-sys-typescale-emphasized-label-lg); + letter-spacing: var(--md-sys-typescale-emphasized-label-lg-tracking); + font-variation-settings: "ROND" 100; +} + +@utility type-label-md { + font: var(--md-sys-typescale-label-md); + letter-spacing: var(--md-sys-typescale-label-md-tracking); + font-variation-settings: normal; +} + +@utility type-emphasized-label-md { + font: var(--md-sys-typescale-emphasized-label-md); + letter-spacing: var(--md-sys-typescale-emphasized-label-md-tracking); + font-variation-settings: "ROND" 100; +} + +@utility type-label-sm { + font: var(--md-sys-typescale-label-sm); + letter-spacing: var(--md-sys-typescale-label-sm-tracking); + font-variation-settings: normal; +} + +@utility type-emphasized-label-sm { + font: var(--md-sys-typescale-emphasized-label-sm); + letter-spacing: var(--md-sys-typescale-emphasized-label-sm-tracking); + font-variation-settings: "ROND" 100; +} diff --git a/tests/Feature/TokensTest.php b/tests/Feature/TokensTest.php index 0593bb17..84a8caf1 100644 --- a/tests/Feature/TokensTest.php +++ b/tests/Feature/TokensTest.php @@ -37,13 +37,14 @@ function typeStyles(): array } /** - * The body of every `@utility type-…` block of type.css, keyed by utility name. + * The body of every `@utility type-…` block, keyed by utility name. The utilities live in + * tokens/utilities.css, apart from the tokens, so the foundation imports no Tailwind directive. * * @return array */ function typeUtilities(): array { - preg_match_all('/@utility (type-[\w-]+) \{\n(.*?)\n\}/s', File::get(packageCss('tokens/type.css')), $matches, PREG_SET_ORDER); + preg_match_all('/@utility (type-[\w-]+) \{\n(.*?)\n\}/s', File::get(packageCss('tokens/utilities.css')), $matches, PREG_SET_ORDER); return array_combine(array_column($matches, 1), array_column($matches, 2)); } From d36f50bc3ceadf97ddb9d9d330173c07e7245931 Mon Sep 17 00:00:00 2001 From: Andreas Reinhold / reini Date: Mon, 14 Sep 2026 13:42:49 +0200 Subject: [PATCH 2/7] Add M3's spacing tokens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plan step 33. tokens/spacing.css declares --md-sys-measurement-space25 to space900 (2 to 72px) in material.tokens, from docs/reference/m3/styles-supplement.md § Spacing, in px because a dp is a CSS pixel and spacing does not scale with text. The design skill names the new token family, as its drift test requires. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9 --- .../boost/skills/material-3-design/SKILL.md | 2 +- resources/css/tokens/spacing.css | 32 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 resources/css/tokens/spacing.css diff --git a/resources/boost/skills/material-3-design/SKILL.md b/resources/boost/skills/material-3-design/SKILL.md index a9d9ae77..e583ee72 100644 --- a/resources/boost/skills/material-3-design/SKILL.md +++ b/resources/boost/skills/material-3-design/SKILL.md @@ -9,7 +9,7 @@ description: Material 3 Expressive's design system as Livewire Material implemen Use this skill when deciding how a screen, panel or control should look or behave — which colour, container, corner, type style, motion, breakpoint or spacing — in an application that requires `nonameweb/livewire-material`, and when reviewing a view against Material 3. The props and slots of each component are in the `livewire-material-development` skill; this one is the design language they implement. The rules an agent must always follow are in the `material-3` guideline; the tables here are what those rules compress. -Every table pairs the M3 name with the library's utility or attribute. The tokens are CSS variables (`--md-sys-color-*`, `--md-sys-typescale-*`, `--md-sys-shape-*`, `--md-sys-elevation-*`, `--md-sys-motion-*`, `--md-sys-state-*`, `--md-ref-typeface-*`) and the utilities read them, so a template names a utility and never a variable or a value. +Every table pairs the M3 name with the library's utility or attribute. The tokens are CSS variables (`--md-sys-color-*`, `--md-sys-typescale-*`, `--md-sys-shape-*`, `--md-sys-elevation-*`, `--md-sys-motion-*`, `--md-sys-state-*`, `--md-sys-measurement-*`, `--md-ref-typeface-*`) and the utilities read them, so a template names a utility and never a variable or a value. ## Colour diff --git a/resources/css/tokens/spacing.css b/resources/css/tokens/spacing.css new file mode 100644 index 00000000..b4ba5b60 --- /dev/null +++ b/resources/css/tokens/spacing.css @@ -0,0 +1,32 @@ +/* + * Spacing, as M3's measurement scale. + * + * An 8dp grid where space100 is the base unit, with the nested steps M3 uses below and just above + * it: 0.25×, 0.5×, 0.75× and 1.25× (docs/reference/m3/styles-supplement.md § Spacing, "Spacing + * scale"). Names are Compose's `md.sys.measurement.space*`. A dp is a CSS pixel, so the values are + * px and a larger text size leaves them alone — M3 keeps spacing when text scales to 200%. + * + * Padding and gaps on the parent, margins only between layout regions. A value between two steps + * is a new token in the application's own stylesheet, named by the same multiplier convention + * (space225 = 18px), never a literal. + */ + +@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility; + +@layer material.tokens { + :root { + --md-sys-measurement-space25: 2px; + --md-sys-measurement-space50: 4px; + --md-sys-measurement-space75: 6px; + --md-sys-measurement-space100: 8px; + --md-sys-measurement-space125: 10px; + --md-sys-measurement-space200: 16px; + --md-sys-measurement-space300: 24px; + --md-sys-measurement-space400: 32px; + --md-sys-measurement-space500: 40px; + --md-sys-measurement-space600: 48px; + --md-sys-measurement-space700: 56px; + --md-sys-measurement-space800: 64px; + --md-sys-measurement-space900: 72px; + } +} From c545fb69efd33a76849764bf76ba964a088c3954 Mon Sep 17 00:00:00 2001 From: Andreas Reinhold / reini Date: Mon, 14 Sep 2026 13:43:04 +0200 Subject: [PATCH 3/7] Add the foundation stylesheet, written without Tailwind Plan step 33. foundation.css is the one required import: the full material layer statement, then plain imports of its parts, each opening with the same statement. - foundation/reset.css (material.reset): Tailwind 4's preflight rule for rule, with M3's on-surface-variant placeholder and no typeface. - foundation/hidden.css: [hidden] and [x-cloak], outside every layer. - foundation/tokens.css (material.tokens): the token files. - foundation/base.css (material.base): the page's surface, ink and brand typeface, and the font face. - foundation/interaction.css (material.base): md-state-layer, md-focus-ring, md-touch-target and md-link, the plain-CSS forms of the utilities; md-state-layer also takes data-md-dragged, and md-touch-target is 48px (space600) where the utility reads 3rem. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9 --- resources/css/foundation.css | 37 ++++ resources/css/foundation/base.css | 24 +++ resources/css/foundation/interaction.css | 111 +++++++++++ resources/css/foundation/reset.css | 236 +++++++++++++++++++++++ resources/css/foundation/tokens.css | 27 +++ 5 files changed, 435 insertions(+) create mode 100644 resources/css/foundation.css create mode 100644 resources/css/foundation/base.css create mode 100644 resources/css/foundation/interaction.css create mode 100644 resources/css/foundation/reset.css create mode 100644 resources/css/foundation/tokens.css diff --git a/resources/css/foundation.css b/resources/css/foundation.css new file mode 100644 index 00000000..ff936aaf --- /dev/null +++ b/resources/css/foundation.css @@ -0,0 +1,37 @@ +/* + * Livewire Material — the foundation, the one stylesheet every application imports, first: + * + * @import '../../vendor/nonameweb/livewire-material/resources/css/foundation.css'; + * @import './material-scheme.css'; + * + * Plain CSS, no build step of its own: an application's bundler (Vite) inlines the imports, and a + * browser could read the files as they are. + * + * Every rule of the package sits in a sub-layer of `material`, in this order, lowest first: + * + * material.reset foundation/reset.css — the browser's defaults taken back + * material.tokens foundation/tokens.css — every `--md-sys-*` and `--md-ref-*` property + * material.base foundation/base.css, foundation/interaction.css — the page, the font + * face, and md-state-layer, md-focus-ring, md-touch-target, md-link + * material.layout the layout components: scaffold, panes, the canonical layouts + * material.components the components + * material.text text.css — md-type-*, md-ink-*, and the few classes for text + * material.visibility hiding an element below or from a breakpoint, over a component's display + * + * Every package stylesheet opens with the same layer statement, before its imports, so the order + * holds whichever file a bundler reaches first. An application's own CSS is unlayered and + * therefore outranks every package rule, whatever the selector — an application's broad selector + * (`button { … }`) restyles the components too. foundation/hidden.css keeps its two `!important` + * rules outside the layers, so `hidden` and `x-cloak` hide an element whatever sets its display. + * + * Until the components leave Tailwind, an application still building it imports this file before + * `@import 'tailwindcss'`, so the `material` layers are declared first and sit below Tailwind's. + */ + +@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility; + +@import './foundation/reset.css'; +@import './foundation/hidden.css'; +@import './foundation/tokens.css'; +@import './foundation/base.css'; +@import './foundation/interaction.css'; diff --git a/resources/css/foundation/base.css b/resources/css/foundation/base.css new file mode 100644 index 00000000..bda12bcc --- /dev/null +++ b/resources/css/foundation/base.css @@ -0,0 +1,24 @@ +/* + * The page itself: M3's background and default ink, in the brand typeface. + * + * The page is `surface` and its text `on-surface`, the default typography colour + * (docs/reference/m3/styles.md § Typography, "Accessibility requirements"). `color-scheme` is + * declared with the roles in tokens/scheme.css, so native controls and scrollbars change with + * `data-theme` in the same block as the colours. The typeface is + * `--md-ref-typeface-brand` (tokens/type.css), Google Sans Flex from tokens/font.css, whose + * `@font-face` this file imports into the same layer. + * + * In `material.base`, above the reset and below every component and text class. + */ + +@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility; + +@import '../tokens/font.css'; + +@layer material.base { + html { + background-color: var(--md-sys-color-surface); + color: var(--md-sys-color-on-surface); + font-family: var(--md-ref-typeface-brand); + } +} diff --git a/resources/css/foundation/interaction.css b/resources/css/foundation/interaction.css new file mode 100644 index 00000000..de484196 --- /dev/null +++ b/resources/css/foundation/interaction.css @@ -0,0 +1,111 @@ +/* + * The interaction classes every interactive component shares, and an application's own control + * can use: M3's state layer, its focus indicator, its minimum target, and a link in running text. + * + * `md-state-layer` lays the content colour over an element at M3's state opacities + * (tokens/state.css): hover 8%, focus and press 10%, dragged 16%. Hover only where the pointer + * can hover, because a touch screen keeps the last hover after a tap; `data-md-dragged` (or the + * older `data-dragged`) on the element draws the dragged layer; a disabled element has none. The + * layer is a ::before, so the element becomes `position: relative` and `isolation: isolate`. + * + * `md-focus-ring` is M3's focus indicator — 3px in secondary, 2px out, drawn only for keyboard + * focus. The site gives no thickness or offset; the values are @material/web's focus ring tokens + * (Apache-2.0, © Google LLC). + * + * `md-touch-target` extends an element's pointer target to M3's 48×48 minimum without changing + * what is drawn: a ::after centred on it, at least `space600` (48px) each way. M3 gives the web + * target as "48 × 48 CSS pixels", so the size is px and does not grow with the text. It is + * `::after`-based, so a component that already draws with ::after wraps its control in an element + * carrying it. + * + * `md-link` is a link in running text: underlined in the colour of the words around it, because + * M3 never lets colour alone mark a link (docs/reference/m3/styles.md § Typography); give it + * `md-ink-primary` where it should also read as primary. + * + * The same declarations as the `state-layer`, `focus-ring`, `touch-target` and `link` utilities + * (tokens/utilities.css) the components still written in Tailwind use; `md-touch-target` alone + * differs, in px where the utility reads 3rem. In `material.base`, so a component's own rules, + * the text classes and an application's rules all outrank them. + */ + +@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility; + +@layer material.base { + .md-state-layer { + position: relative; + isolation: isolate; + + &::before { + content: ''; + position: absolute; + inset: 0; + z-index: -1; + border-radius: inherit; + background-color: currentColor; + opacity: 0; + pointer-events: none; + transition: opacity var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast); + } + + @media (hover: hover) { + &:hover::before { + opacity: var(--md-sys-state-hover-state-layer-opacity); + } + } + + &:focus-visible::before { + opacity: var(--md-sys-state-focus-state-layer-opacity); + } + + &:active::before { + opacity: var(--md-sys-state-pressed-state-layer-opacity); + } + + &:is([data-md-dragged], [data-dragged])::before { + opacity: var(--md-sys-state-dragged-state-layer-opacity); + } + + &:disabled::before, + &[aria-disabled='true']::before { + display: none; + } + } + + .md-focus-ring { + outline: none; + + &:focus-visible { + outline: 3px solid var(--md-sys-color-secondary); + outline-offset: 2px; + } + } + + .md-touch-target { + position: relative; + + &::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 100%; + height: 100%; + min-width: var(--md-sys-measurement-space600); + min-height: var(--md-sys-measurement-space600); + transform: translate(-50%, -50%); + } + } + + .md-link { + cursor: pointer; + text-decoration-line: underline; + text-decoration-thickness: from-font; + text-underline-offset: 0.15em; + border-radius: var(--md-sys-shape-corner-xs); + + &:focus-visible { + outline: 3px solid var(--md-sys-color-secondary); + outline-offset: 2px; + } + } +} diff --git a/resources/css/foundation/reset.css b/resources/css/foundation/reset.css new file mode 100644 index 00000000..815af0ab --- /dev/null +++ b/resources/css/foundation/reset.css @@ -0,0 +1,236 @@ +/* + * The reset: what a browser draws before any stylesheet, taken back to a clean start. + * + * Rule for rule the reset the components were built on, Tailwind CSS 4's preflight (MIT, + * © Tailwind Labs, after modern-normalize), so a component looks the same whether an application + * still builds Tailwind or not: every box `border-box` with a `0 solid` border and no margin or + * padding, headings and lists unstyled, form controls inheriting font, colour and tracking with no + * background or radius, media as blocks that never overflow their parent. Three differences: + * + * - no typeface here: foundation/base.css sets `--md-ref-typeface-brand` on ; + * - a placeholder is `on-surface-variant`, the colour of M3's text field placeholder (androidx + * FilledTextFieldTokens.kt, InputPlaceholderColor), not half the text colour; + * - it sits in `material.reset`, the lowest layer, so every package rule and every application + * rule outranks it without a specificity fight. + * + * The two rules that must win everything — `[hidden]` and `[x-cloak]` — are foundation/hidden.css. + */ + +@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility; + +@layer material.reset { + /* Padding and border inside the width; no default margin, padding or border. */ + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + box-sizing: border-box; + margin: 0; + padding: 0; + border: 0 solid; + } + + /* A readable line height and tab size, no font resizing on rotation (iOS), no tap flash. */ + html, + :host { + line-height: 1.5; + -webkit-text-size-adjust: 100%; + tab-size: 4; + font-feature-settings: normal; + font-variation-settings: normal; + -webkit-tap-highlight-color: transparent; + } + + hr { + height: 0; + color: inherit; + border-top-width: 1px; + } + + abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; + } + + h1, + h2, + h3, + h4, + h5, + h6 { + font-size: inherit; + font-weight: inherit; + } + + a { + color: inherit; + -webkit-text-decoration: inherit; + text-decoration: inherit; + } + + b, + strong { + font-weight: bolder; + } + + code, + kbd, + samp, + pre { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; + font-feature-settings: normal; + font-variation-settings: normal; + font-size: 1em; + } + + small { + font-size: 80%; + } + + sub, + sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; + } + + sub { + bottom: -0.25em; + } + + sup { + top: -0.5em; + } + + table { + text-indent: 0; + border-color: inherit; + border-collapse: collapse; + } + + :-moz-focusring:where(:not(iframe)) { + outline: auto; + } + + progress { + vertical-align: baseline; + } + + summary { + display: list-item; + } + + ol, + ul, + menu { + list-style: none; + } + + /* Replaced elements are blocks, aligned to the middle when made inline again. */ + img, + svg, + video, + canvas, + audio, + iframe, + embed, + object { + display: block; + vertical-align: middle; + } + + img, + video { + max-width: 100%; + height: auto; + } + + /* Form controls take the text around them, with no background, radius or disabled fade. */ + button, + input, + select, + optgroup, + textarea, + ::file-selector-button { + font: inherit; + font-feature-settings: inherit; + font-variation-settings: inherit; + letter-spacing: inherit; + color: inherit; + border-radius: 0; + background-color: transparent; + opacity: 1; + } + + :where(select:is([multiple], [size])) optgroup { + font-weight: bolder; + } + + :where(select:is([multiple], [size])) optgroup option { + padding-inline-start: 20px; + } + + ::file-selector-button { + margin-inline-end: 4px; + } + + ::placeholder { + opacity: 1; + color: var(--md-sys-color-on-surface-variant); + } + + textarea { + resize: vertical; + } + + ::-webkit-search-decoration { + -webkit-appearance: none; + } + + /* Date and time inputs keep one height and follow text-align (Safari). */ + ::-webkit-date-and-time-value { + min-height: 1lh; + text-align: inherit; + } + + ::-webkit-datetime-edit { + display: inline-flex; + } + + ::-webkit-datetime-edit-fields-wrapper { + padding: 0; + } + + ::-webkit-datetime-edit, + ::-webkit-datetime-edit-year-field, + ::-webkit-datetime-edit-month-field, + ::-webkit-datetime-edit-day-field, + ::-webkit-datetime-edit-hour-field, + ::-webkit-datetime-edit-minute-field, + ::-webkit-datetime-edit-second-field, + ::-webkit-datetime-edit-millisecond-field, + ::-webkit-datetime-edit-meridiem-field { + padding-block: 0; + } + + ::-webkit-calendar-picker-indicator { + line-height: 1; + } + + :-moz-ui-invalid { + box-shadow: none; + } + + button, + input:where([type='button'], [type='reset'], [type='submit']), + ::file-selector-button { + appearance: button; + } + + ::-webkit-inner-spin-button, + ::-webkit-outer-spin-button { + height: auto; + } +} diff --git a/resources/css/foundation/tokens.css b/resources/css/foundation/tokens.css new file mode 100644 index 00000000..9c062e80 --- /dev/null +++ b/resources/css/foundation/tokens.css @@ -0,0 +1,27 @@ +/* + * The tokens: every `--md-sys-*` and `--md-ref-*` custom property the package reads. + * + * Each token file wraps its own declarations in `material.tokens`, because an `@import` cannot sit + * inside a layer block and `layer()` on the import would nest the layer a second time. Being the + * lowest layer but one, a token here loses to the same property declared anywhere an application + * writes it: its generated `material-scheme.css`, unlayered, replaces the default scheme below + * whatever its selectors, and a typeface or a spacing step is overridden the same way. + * + * scheme the colour roles of M3's baseline scheme, light and dark, three contrast levels + * shape the corner scale + * elevation the five shadow levels + * motion the springs of both motion schemes and M3's legacy easings + * type the 30 type styles and the brand typeface + * state the state-layer and disabled opacities + * spacing the measurement scale, space25 … space900 + */ + +@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility; + +@import '../tokens/scheme.css'; +@import '../tokens/shape.css'; +@import '../tokens/elevation.css'; +@import '../tokens/motion.css'; +@import '../tokens/type.css'; +@import '../tokens/state.css'; +@import '../tokens/spacing.css'; From 0ab3bd861d65548894ee37427c819e4103a19df3 Mon Sep 17 00:00:00 2001 From: Andreas Reinhold / reini Date: Mon, 14 Sep 2026 13:43:11 +0200 Subject: [PATCH 4/7] Add the md text classes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plan step 34. text.css (material.text, imported by the foundation) is the fixed set for text on plain elements: md-type-* and md-type-emphasized-* with the type utilities' declarations, the role inks md-ink to md-ink-inverse, md-text-start|center|end, md-truncate, md-line-clamp-2|3, md-nowrap, md-tabular and md-visually-hidden. Its header says when to use each, from the material-3 guideline and docs/reference/m3/styles.md § Typography. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9 --- resources/css/foundation.css | 1 + resources/css/text.css | 323 +++++++++++++++++++++++++++++++++++ 2 files changed, 324 insertions(+) create mode 100644 resources/css/text.css diff --git a/resources/css/foundation.css b/resources/css/foundation.css index ff936aaf..6409a60c 100644 --- a/resources/css/foundation.css +++ b/resources/css/foundation.css @@ -35,3 +35,4 @@ @import './foundation/tokens.css'; @import './foundation/base.css'; @import './foundation/interaction.css'; +@import './text.css'; diff --git a/resources/css/text.css b/resources/css/text.css new file mode 100644 index 00000000..ce14f112 --- /dev/null +++ b/resources/css/text.css @@ -0,0 +1,323 @@ +/* + * The text classes: a fixed, documented set for text on plain elements. A component sets its own + * type and ink; these are for the headings, paragraphs and captions an application writes around + * them, and the set the package's own views use for text once they leave Tailwind. + * + * Type — one per text element, the whole style at once (size, line height, weight, tracking; + * tokens/type.css), never assembled by hand. Roles from docs/reference/m3/styles.md § Typography: + * + * md-type-display-{lg|md|sm} the largest text on a screen: short, important text or numerals, + * best on large screens + * md-type-headline-{lg|md|sm} short, high-emphasis text: page and section titles + * md-type-title-{lg|md|sm} medium-emphasis, relatively short text: card, dialog and + * list-section titles + * md-type-body-{lg|md|sm} longer passages and paragraphs; body-lg for reading + * md-type-label-{lg|md|sm} small, utilitarian text inside components, and captions + * + * `md-type-emphasized-*` is the same style one weight step heavier and fully rounded, with its own + * tracking. M3's components do not use it by default: it is a deliberate choice for a selected + * item, a primary action, a headline or a badge, not decoration. `font-variation-settings` + * inherits, so every regular class resets it and body copy inside an emphasized heading reads as + * itself again. The heading level is structure; the class is appearance. + * + * Ink — an M3 role, never an opacity (38% means disabled): + * + * md-ink on-surface: default text + * md-ink-variant on-surface-variant: lower emphasis — secondary text, metadata + * md-ink-quiet outline: decoration only, never text that must be read + * md-ink-primary primary: a link, together with `md-link` + * md-ink-error error: the text of an error + * md-ink-success, md-ink-warning, md-ink-info: the text of that state, and nothing else + * md-ink-inverse inverse-on-surface: text on an inverse surface (a snackbar) + * + * On a container, the ink is the container's `on-` role, which the component sets; these inks are + * for the surface roles. + * + * Layout of text: + * + * md-text-start|center|end logical alignment, so it turns with the writing direction + * md-truncate one line with an ellipsis — only where the whole text can be read + * elsewhere, because M3's text must scale to 200% without loss + * md-line-clamp-2|3 at most two or three lines, the same caveat + * md-nowrap never wraps: a figure with its unit, a short label + * md-tabular tabular figures, where digits change often (tables, clocks, + * counters), so the layout does not shift + * md-visually-hidden read by assistive technology, not drawn: a label a picture already + * gives the eye + * + * In `material.text`, above every component: a text class on a component's root or on an element + * inside it wins over the component's own type and ink. The `md-type-*` declarations are the + * `type-*` utilities' (tokens/utilities.css), which the components still written in Tailwind use. + */ + +@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility; + +@layer material.text { + .md-type-display-lg { + font: var(--md-sys-typescale-display-lg); + letter-spacing: var(--md-sys-typescale-display-lg-tracking); + font-variation-settings: normal; + } + + .md-type-emphasized-display-lg { + font: var(--md-sys-typescale-emphasized-display-lg); + letter-spacing: var(--md-sys-typescale-emphasized-display-lg-tracking); + font-variation-settings: "ROND" 100; + } + + .md-type-display-md { + font: var(--md-sys-typescale-display-md); + letter-spacing: var(--md-sys-typescale-display-md-tracking); + font-variation-settings: normal; + } + + .md-type-emphasized-display-md { + font: var(--md-sys-typescale-emphasized-display-md); + letter-spacing: var(--md-sys-typescale-emphasized-display-md-tracking); + font-variation-settings: "ROND" 100; + } + + .md-type-display-sm { + font: var(--md-sys-typescale-display-sm); + letter-spacing: var(--md-sys-typescale-display-sm-tracking); + font-variation-settings: normal; + } + + .md-type-emphasized-display-sm { + font: var(--md-sys-typescale-emphasized-display-sm); + letter-spacing: var(--md-sys-typescale-emphasized-display-sm-tracking); + font-variation-settings: "ROND" 100; + } + + .md-type-headline-lg { + font: var(--md-sys-typescale-headline-lg); + letter-spacing: var(--md-sys-typescale-headline-lg-tracking); + font-variation-settings: normal; + } + + .md-type-emphasized-headline-lg { + font: var(--md-sys-typescale-emphasized-headline-lg); + letter-spacing: var(--md-sys-typescale-emphasized-headline-lg-tracking); + font-variation-settings: "ROND" 100; + } + + .md-type-headline-md { + font: var(--md-sys-typescale-headline-md); + letter-spacing: var(--md-sys-typescale-headline-md-tracking); + font-variation-settings: normal; + } + + .md-type-emphasized-headline-md { + font: var(--md-sys-typescale-emphasized-headline-md); + letter-spacing: var(--md-sys-typescale-emphasized-headline-md-tracking); + font-variation-settings: "ROND" 100; + } + + .md-type-headline-sm { + font: var(--md-sys-typescale-headline-sm); + letter-spacing: var(--md-sys-typescale-headline-sm-tracking); + font-variation-settings: normal; + } + + .md-type-emphasized-headline-sm { + font: var(--md-sys-typescale-emphasized-headline-sm); + letter-spacing: var(--md-sys-typescale-emphasized-headline-sm-tracking); + font-variation-settings: "ROND" 100; + } + + .md-type-title-lg { + font: var(--md-sys-typescale-title-lg); + letter-spacing: var(--md-sys-typescale-title-lg-tracking); + font-variation-settings: normal; + } + + .md-type-emphasized-title-lg { + font: var(--md-sys-typescale-emphasized-title-lg); + letter-spacing: var(--md-sys-typescale-emphasized-title-lg-tracking); + font-variation-settings: "ROND" 100; + } + + .md-type-title-md { + font: var(--md-sys-typescale-title-md); + letter-spacing: var(--md-sys-typescale-title-md-tracking); + font-variation-settings: normal; + } + + .md-type-emphasized-title-md { + font: var(--md-sys-typescale-emphasized-title-md); + letter-spacing: var(--md-sys-typescale-emphasized-title-md-tracking); + font-variation-settings: "ROND" 100; + } + + .md-type-title-sm { + font: var(--md-sys-typescale-title-sm); + letter-spacing: var(--md-sys-typescale-title-sm-tracking); + font-variation-settings: normal; + } + + .md-type-emphasized-title-sm { + font: var(--md-sys-typescale-emphasized-title-sm); + letter-spacing: var(--md-sys-typescale-emphasized-title-sm-tracking); + font-variation-settings: "ROND" 100; + } + + .md-type-body-lg { + font: var(--md-sys-typescale-body-lg); + letter-spacing: var(--md-sys-typescale-body-lg-tracking); + font-variation-settings: normal; + } + + .md-type-emphasized-body-lg { + font: var(--md-sys-typescale-emphasized-body-lg); + letter-spacing: var(--md-sys-typescale-emphasized-body-lg-tracking); + font-variation-settings: "ROND" 100; + } + + .md-type-body-md { + font: var(--md-sys-typescale-body-md); + letter-spacing: var(--md-sys-typescale-body-md-tracking); + font-variation-settings: normal; + } + + .md-type-emphasized-body-md { + font: var(--md-sys-typescale-emphasized-body-md); + letter-spacing: var(--md-sys-typescale-emphasized-body-md-tracking); + font-variation-settings: "ROND" 100; + } + + .md-type-body-sm { + font: var(--md-sys-typescale-body-sm); + letter-spacing: var(--md-sys-typescale-body-sm-tracking); + font-variation-settings: normal; + } + + .md-type-emphasized-body-sm { + font: var(--md-sys-typescale-emphasized-body-sm); + letter-spacing: var(--md-sys-typescale-emphasized-body-sm-tracking); + font-variation-settings: "ROND" 100; + } + + .md-type-label-lg { + font: var(--md-sys-typescale-label-lg); + letter-spacing: var(--md-sys-typescale-label-lg-tracking); + font-variation-settings: normal; + } + + .md-type-emphasized-label-lg { + font: var(--md-sys-typescale-emphasized-label-lg); + letter-spacing: var(--md-sys-typescale-emphasized-label-lg-tracking); + font-variation-settings: "ROND" 100; + } + + .md-type-label-md { + font: var(--md-sys-typescale-label-md); + letter-spacing: var(--md-sys-typescale-label-md-tracking); + font-variation-settings: normal; + } + + .md-type-emphasized-label-md { + font: var(--md-sys-typescale-emphasized-label-md); + letter-spacing: var(--md-sys-typescale-emphasized-label-md-tracking); + font-variation-settings: "ROND" 100; + } + + .md-type-label-sm { + font: var(--md-sys-typescale-label-sm); + letter-spacing: var(--md-sys-typescale-label-sm-tracking); + font-variation-settings: normal; + } + + .md-type-emphasized-label-sm { + font: var(--md-sys-typescale-emphasized-label-sm); + letter-spacing: var(--md-sys-typescale-emphasized-label-sm-tracking); + font-variation-settings: "ROND" 100; + } + + .md-ink { + color: var(--md-sys-color-on-surface); + } + + .md-ink-variant { + color: var(--md-sys-color-on-surface-variant); + } + + .md-ink-quiet { + color: var(--md-sys-color-outline); + } + + .md-ink-primary { + color: var(--md-sys-color-primary); + } + + .md-ink-error { + color: var(--md-sys-color-error); + } + + .md-ink-success { + color: var(--md-sys-color-success); + } + + .md-ink-warning { + color: var(--md-sys-color-warning); + } + + .md-ink-info { + color: var(--md-sys-color-info); + } + + .md-ink-inverse { + color: var(--md-sys-color-inverse-on-surface); + } + + .md-text-start { + text-align: start; + } + + .md-text-center { + text-align: center; + } + + .md-text-end { + text-align: end; + } + + .md-truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .md-line-clamp-2 { + overflow: hidden; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + } + + .md-line-clamp-3 { + overflow: hidden; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; + } + + .md-nowrap { + white-space: nowrap; + } + + .md-tabular { + font-variant-numeric: tabular-nums; + } + + .md-visually-hidden { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; + border-width: 0; + } +} From 1075305a9cdd3191b1c9a72bc8236f44fc2a4bb9 Mon Sep 17 00:00:00 2001 From: Andreas Reinhold / reini Date: Mon, 14 Sep 2026 13:43:23 +0200 Subject: [PATCH 5/7] Import the foundation before Tailwind in the Workbench Plan step 33. The Workbench imports foundation.css ahead of @import 'tailwindcss', so the material layers are declared first and sit below Tailwind's, then tailwind.css: the theme, the token utilities and the component stylesheets still written for Tailwind, with no tokens, so nothing is declared twice. material.css keeps working for 1.x-style imports after Tailwind by bringing the tokens, the hiding rules and tailwind.css, and never the reset, which would outrank Tailwind's utilities from there. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9 --- resources/css/material.css | 28 ++++++------------- resources/css/tailwind.css | 34 ++++++++++++++++++++++++ tests/Feature/Components/OverlayTest.php | 2 +- workbench/resources/css/app.css | 9 ++++--- 4 files changed, 49 insertions(+), 24 deletions(-) create mode 100644 resources/css/tailwind.css diff --git a/resources/css/material.css b/resources/css/material.css index 3524a01f..49a822a3 100644 --- a/resources/css/material.css +++ b/resources/css/material.css @@ -1,6 +1,6 @@ /* - * Livewire Material — the one stylesheet an application imports, after Tailwind, followed by - * the scheme `php artisan material:scheme` wrote for it: + * Livewire Material on 1.x-style imports — the one stylesheet an application imports, after + * Tailwind, followed by the scheme `php artisan material:scheme` wrote for it: * * @import 'tailwindcss'; * @import '../../vendor/nonameweb/livewire-material/resources/css/material.css'; @@ -11,6 +11,11 @@ * tokens/scheme.css is the package's own default (M3's baseline purple), so an application * renders before it has a scheme; the application's file declares the same roles, unlayered, and * wins over the default in `material.tokens`. + * + * It brings the tokens, the two hiding rules and tailwind.css, but not foundation.css: imported + * after Tailwind, the foundation's reset would land in a layer above Tailwind's utilities and + * undo them. An application moving to 2.0.0's vocabulary imports foundation.css before + * `@import 'tailwindcss'` and tailwind.css after it, instead of this file. */ @import './tokens/scheme.css'; @@ -19,26 +24,9 @@ @import './tokens/motion.css'; @import './tokens/type.css'; @import './tokens/font.css'; -@import './tokens/theme.css'; @import './tokens/state.css'; -@import './tokens/utilities.css'; @import './foundation/hidden.css'; -@import './components/actions.css'; -@import './components/groups.css'; -@import './components/list.css'; -@import './components/collapse.css'; -@import './components/dialog.css'; -@import './components/field.css'; -@import './components/menu.css'; -@import './components/selection.css'; -@import './components/search.css'; -@import './components/datepicker.css'; -@import './components/timepicker.css'; -@import './components/tabs.css'; -@import './components/app-bar.css'; -@import './components/navigation.css'; -@import './components/toolbar.css'; -@import './components/table.css'; +@import './tailwind.css'; @layer base { html { diff --git a/resources/css/tailwind.css b/resources/css/tailwind.css new file mode 100644 index 00000000..5d76a8c3 --- /dev/null +++ b/resources/css/tailwind.css @@ -0,0 +1,34 @@ +/* + * Livewire Material's Tailwind half: the theme that turns the tokens into utilities, the token + * utilities, and the component stylesheets still written for Tailwind (`@utility`, `@variant`, + * `@layer components`). It goes when the last component leaves Tailwind. + * + * It carries no tokens, so it can sit behind the foundation without declaring them twice — the + * Workbench imports it that way, after Tailwind: + * + * @import '../../../resources/css/foundation.css'; + * @import 'tailwindcss' source(none); + * @import '../../../resources/css/tailwind.css'; + * + * An application on 1.x-style imports keeps importing material.css, which brings the tokens and + * this file together. + */ + +@import './tokens/theme.css'; +@import './tokens/utilities.css'; +@import './components/actions.css'; +@import './components/groups.css'; +@import './components/list.css'; +@import './components/collapse.css'; +@import './components/dialog.css'; +@import './components/field.css'; +@import './components/menu.css'; +@import './components/selection.css'; +@import './components/search.css'; +@import './components/datepicker.css'; +@import './components/timepicker.css'; +@import './components/tabs.css'; +@import './components/app-bar.css'; +@import './components/navigation.css'; +@import './components/toolbar.css'; +@import './components/table.css'; diff --git a/tests/Feature/Components/OverlayTest.php b/tests/Feature/Components/OverlayTest.php index 3f78f736..9ef4396b 100644 --- a/tests/Feature/Components/OverlayTest.php +++ b/tests/Feature/Components/OverlayTest.php @@ -88,7 +88,7 @@ it('divides a scrolling body from its header and actions only while content is h ->toContain('position: absolute;') // A token that reduced motion sets to zero, so then the rule appears without a fade. ->toContain('transition: opacity var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);') - ->and(File::get(__DIR__.'/../../../resources/css/material.css'))->toContain("@import './components/dialog.css';") + ->and(File::get(__DIR__.'/../../../resources/css/tailwind.css'))->toContain("@import './components/dialog.css';") ->and(File::get(__DIR__.'/../../../resources/js/material.js'))->toContain("import './dialog.js'") ->and(File::get(__DIR__.'/../../../resources/js/dialog.js')) ->toContain("directive('dialog-dividers'") diff --git a/workbench/resources/css/app.css b/workbench/resources/css/app.css index 60797de7..1072fdfe 100644 --- a/workbench/resources/css/app.css +++ b/workbench/resources/css/app.css @@ -1,7 +1,10 @@ -/* The Workbench builds the package exactly as an application would, with explicit - sources so the plans in docs/ are not scanned for class names. */ +/* The Workbench builds the package as an application would, with explicit sources so the plans + in docs/ are not scanned for class names. The foundation comes before Tailwind, so the + `material` layers are declared first and sit below Tailwind's; tailwind.css brings the + components still written for Tailwind, without declaring the tokens a second time. */ +@import '../../../resources/css/foundation.css'; @import 'tailwindcss' source(none); -@import '../../../resources/css/material.css'; +@import '../../../resources/css/tailwind.css'; @import './material-scheme.css'; @source '../../../resources/views'; From 66e219ae6387adeba21a73c16c1fc450c62be25e Mon Sep 17 00:00:00 2001 From: Andreas Reinhold / reini Date: Mon, 14 Sep 2026 13:43:33 +0200 Subject: [PATCH 6/7] Check the foundation's stylesheets Plan steps 33, 34 and 42. StylesheetsTest follows foundation.css through its imports and checks each file: a header, the exact layer statement, plain imports only before its first block, every rule in its own material layer but the two hiding rules, no Tailwind directive, media queries only at 600/840/1200/1600px. It also pins the spacing tokens to the reference table, the md-type classes to the type utilities' declarations, the whole text class set, the interaction classes against their utilities, one [x-cloak], material.css never reaching the reset, and the Workbench importing the foundation before Tailwind. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9 --- tests/Feature/StylesheetsTest.php | 427 ++++++++++++++++++++++++++++++ 1 file changed, 427 insertions(+) create mode 100644 tests/Feature/StylesheetsTest.php diff --git a/tests/Feature/StylesheetsTest.php b/tests/Feature/StylesheetsTest.php new file mode 100644 index 00000000..2d6df214 --- /dev/null +++ b/tests/Feature/StylesheetsTest.php @@ -0,0 +1,427 @@ +` blocks. The only rules outside + * a layer are foundation/hidden.css's two `!important` ones. + * + * Until the components leave Tailwind, the checks cover the foundation — foundation.css and every + * file it imports, text.css included — and material.css and tailwind.css carry the rest. + */ +const MATERIAL_LAYER_STATEMENT = '@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;'; + +function stylesheetPath(string $path = ''): string +{ + return (string) realpath(__DIR__.'/../../resources/css'.($path === '' ? '' : '/'.$path)); +} + +/** + * A stylesheet without its comments; a quoted string keeps whatever it holds. + */ +function stylesheetWithoutComments(string $css): string +{ + return (string) preg_replace('~("(?:\\\\.|[^"\\\\])*"|\'(?:\\\\.|[^\'\\\\])*\')|/\*.*?\*/~s', '$1', $css); +} + +/** + * The top-level statements (`@layer …;`, `@import …;`) and blocks of a stylesheet, in order, with + * whitespace collapsed in statements and preludes. + * + * @return list + */ +function stylesheetItems(string $css): array +{ + $css = stylesheetWithoutComments($css); + $items = []; + $start = 0; + $depth = 0; + $quote = null; + $opening = 0; + + for ($i = 0, $length = strlen($css); $i < $length; $i++) { + $char = $css[$i]; + + if ($quote !== null) { + if ($char === '\\') { + $i++; + } elseif ($char === $quote) { + $quote = null; + } + + continue; + } + + if ($char === '"' || $char === "'") { + $quote = $char; + } elseif ($char === '{') { + if ($depth++ === 0) { + $opening = $i; + } + } elseif ($char === '}' && --$depth === 0) { + $items[] = [ + 'prelude' => (string) preg_replace('/\s+/', ' ', trim(substr($css, $start, $opening - $start))), + 'body' => substr($css, $opening + 1, $i - $opening - 1), + ]; + $start = $i + 1; + } elseif ($char === ';' && $depth === 0) { + $items[] = ['statement' => preg_replace('/\s+/', ' ', trim(substr($css, $start, $i - $start))).';']; + $start = $i + 1; + } + } + + expect(trim(substr($css, $start)))->toBe('', 'The stylesheet ends inside an unclosed rule.'); + + return $items; +} + +/** + * The declarations of a flat block body, by property. + * + * @return array + */ +function stylesheetDeclarations(string $body): array +{ + return collect(explode(';', $body)) + ->map(fn (string $declaration): string => trim($declaration)) + ->filter() + ->mapWithKeys(fn (string $declaration): array => [trim(strstr($declaration, ':', true)) => trim(substr(strstr($declaration, ':'), 1))]) + ->all(); +} + +/** + * The body of the first block with this prelude, at the top level or one layer block down. + */ +function stylesheetBlock(string $css, string $prelude): string +{ + foreach (stylesheetItems($css) as $item) { + if (($item['prelude'] ?? null) === $prelude) { + return $item['body']; + } + + if (str_starts_with($item['prelude'] ?? '', '@layer ')) { + foreach (stylesheetItems($item['body']) as $inner) { + if (($inner['prelude'] ?? null) === $prelude) { + return $inner['body']; + } + } + } + } + + throw new RuntimeException("No block `{$prelude}`."); +} + +/** + * The paths a stylesheet imports, as written. + * + * @return list + */ +function stylesheetImports(string $file): array +{ + return collect(stylesheetItems(File::get($file))) + ->map(fn (array $item): ?string => preg_match('/^@import\s+([\'"])(.+?)\1/', $item['statement'] ?? '', $match) === 1 ? $match[2] : null) + ->filter() + ->values() + ->all(); +} + +/** + * A stylesheet and every stylesheet it reaches through `@import`, each once, in the order a + * bundler meets them. `tailwindcss` itself is a package, not a file, and is left out. + * + * @return list + */ +function stylesheetTree(string $entry): array +{ + $files = []; + + $visit = function (string $file) use (&$visit, &$files): void { + $file = (string) realpath($file); + + if (in_array($file, $files, true)) { + return; + } + + $files[] = $file; + + foreach (stylesheetImports($file) as $import) { + if (str_starts_with($import, '.')) { + $visit(dirname($file).'/'.$import); + } + } + }; + + $visit($entry); + + return $files; +} + +/** + * The foundation: foundation.css and everything it imports. + * + * @return list + */ +function foundationStylesheets(): array +{ + return stylesheetTree(stylesheetPath('foundation.css')); +} + +function stylesheetName(string $file): string +{ + return substr($file, strlen(stylesheetPath()) + 1); +} + +it('brings every foundation file and text.css into the foundation, and nothing of Tailwind', function () { + $foundation = array_map(stylesheetName(...), foundationStylesheets()); + + $parts = collect(File::files(stylesheetPath('foundation'))) + ->map(fn (SplFileInfo $file): string => 'foundation/'.$file->getFilename()) + ->push('foundation.css', 'text.css'); + + expect($parts->diff($foundation)->values()->all())->toBe([]) + ->and(array_map(fn (string $import): string => basename($import), stylesheetImports(stylesheetPath('foundation.css')))) + // In the order of their layers, the rules outside every layer beside the reset. + ->toBe(['reset.css', 'hidden.css', 'tokens.css', 'base.css', 'interaction.css', 'text.css']) + ->and($foundation) + ->toContain('tokens/scheme.css', 'tokens/shape.css', 'tokens/elevation.css', 'tokens/motion.css', 'tokens/type.css', 'tokens/state.css', 'tokens/spacing.css', 'tokens/font.css') + // The Tailwind half: the theme, the token utilities and the components still written for it. + ->and(array_values(array_intersect($foundation, ['tokens/theme.css', 'tokens/utilities.css', 'tailwind.css', 'material.css'])))->toBe([]) + ->and(array_values(preg_grep('/^components\//', $foundation)))->toBe([]); +}); + +it('opens every foundation stylesheet with a header, the layer statement and plain imports', function () { + foreach (foundationStylesheets() as $file) { + $name = stylesheetName($file); + $items = stylesheetItems(File::get($file)); + + expect(File::get($file))->toStartWith('/*', "{$name} has no header comment") + ->and($items[0]['statement'] ?? null)->toBe(MATERIAL_LAYER_STATEMENT, "{$name} does not open with the layer statement"); + + $blocks = false; + + foreach (array_slice($items, 1) as $item) { + if (isset($item['prelude'])) { + $blocks = true; + + continue; + } + + expect($blocks)->toBeFalse("{$name}: `{$item['statement']}` comes after a block") + // A plain import: a quoted path and nothing after it — no layer(), supports() or media. + ->and($item['statement'])->toMatch('/^@import ([\'"])\.{1,2}\/[\w.\/-]+\.css\1;$/', "{$name}: `{$item['statement']}` is not a plain import"); + } + } +}); + +it('keeps every foundation rule inside its material layer, but the two that hide', function () { + // Which layer each file writes into; tokens.css only imports. + $layers = [ + 'foundation.css' => null, + 'foundation/reset.css' => 'material.reset', + 'foundation/hidden.css' => null, + 'foundation/tokens.css' => null, + 'foundation/base.css' => 'material.base', + 'foundation/interaction.css' => 'material.base', + 'text.css' => 'material.text', + 'tokens/font.css' => 'material.base', + ]; + + $unlayered = []; + $important = 0; + + foreach (foundationStylesheets() as $file) { + $name = stylesheetName($file); + $layer = array_key_exists($name, $layers) ? $layers[$name] : 'material.tokens'; + $important += substr_count(stylesheetWithoutComments(File::get($file)), '!important'); + + foreach (stylesheetItems(File::get($file)) as $item) { + if (! isset($item['prelude'])) { + continue; + } + + if (! str_starts_with($item['prelude'], '@layer ')) { + $unlayered["{$name}: {$item['prelude']}"] = stylesheetDeclarations($item['body']); + + continue; + } + + expect($item['prelude'])->toBe("@layer {$layer}", "{$name} writes into `{$item['prelude']}`") + // A layer inside the block would become material.., below the rest of . + ->and($item['body'])->not->toContain('@layer'); + } + } + + expect($unlayered)->toBe([ + "foundation/hidden.css: [hidden]:where(:not([hidden='until-found']))" => ['display' => 'none !important'], + 'foundation/hidden.css: [x-cloak]' => ['display' => 'none !important'], + ])->and($important)->toBe(2); +}); + +it('defines x-cloak once, where both the foundation and material.css reach it', function () { + $definitions = collect(File::allFiles(stylesheetPath())) + ->filter(fn (SplFileInfo $file): bool => $file->getExtension() === 'css') + ->filter(fn (SplFileInfo $file): bool => str_contains(stylesheetWithoutComments($file->getContents()), '[x-cloak]')) + ->map(fn (SplFileInfo $file): string => stylesheetName((string) $file->getRealPath())) + ->values() + ->all(); + + $material = array_map(stylesheetName(...), stylesheetTree(stylesheetPath('material.css'))); + + expect($definitions)->toBe(['foundation/hidden.css']) + ->and($material)->toContain('foundation/hidden.css', 'tailwind.css') + // Imported after Tailwind, the reset would sit in a layer above Tailwind's utilities. + ->and(array_values(array_intersect($material, ['foundation.css', 'foundation/reset.css', 'foundation/base.css', 'foundation/interaction.css', 'text.css'])))->toBe([]); +}); + +it('writes no Tailwind directive in the foundation', function () { + foreach (foundationStylesheets() as $file) { + expect(stylesheetWithoutComments(File::get($file))) + ->not->toMatch('/@(?:tailwind|theme|utility|variant|custom-variant|apply|source|config|plugin|reference)\b/', stylesheetName($file)) + ->not->toMatch('/--(?:theme|spacing|alpha)\(|\btheme\(|[\'"]tailwindcss[\'"]/', stylesheetName($file)); + } +}); + +it('writes a media query in the foundation only at M3\'s breakpoints, in px', function () { + $queries = collect(foundationStylesheets()) + ->flatMap(function (string $file): array { + preg_match_all('/@media\b([^{]*)\{/', stylesheetWithoutComments(File::get($file)), $matches); + + return array_map(fn (string $query): string => stylesheetName($file).': '.trim($query), $matches[1]); + }); + + // The foundation's own queries (hover, reduced motion) hold no width; the check is for what + // lands here later. + expect($queries)->not->toBeEmpty(); + + foreach ($queries as $query) { + preg_match_all('/(\d*\.?\d+)([a-z%]*)/i', substr($query, strpos($query, ': ') + 2), $lengths, PREG_SET_ORDER); + + foreach ($lengths as [, $number, $unit]) { + expect($unit)->toBe('px', $query) + ->and(in_array($number, ['600', '840', '1200', '1600'], true))->toBeTrue("{$query} is not at an M3 breakpoint"); + } + } +}); + +it('declares M3\'s spacing scale as the reference gives it', function () { + preg_match_all( + '/^\|\s*\**space(\d+)\**\s*\|\s*\**([\d.]+)×\**\s*\|\s*\**(\d+)dp/m', + File::get(__DIR__.'/../../docs/reference/m3/styles-supplement.md'), + $rows, + PREG_SET_ORDER, + ); + + $reference = collect($rows)->mapWithKeys(fn (array $row): array => ["--md-sys-measurement-space{$row[1]}" => "{$row[3]}px"])->all(); + $tokens = stylesheetDeclarations(stylesheetBlock(File::get(stylesheetPath('tokens/spacing.css')), ':root')); + + expect($reference)->toHaveCount(13) + ->and($tokens)->toBe($reference); + + // space100 is the 8dp base unit and every name is its multiplier: space125 = 1.25 × 8. + foreach ($rows as [, $name, $multiplier, $value]) { + expect((float) $name / 100 * 8)->toBe((float) $value) + ->and((float) $multiplier * 8)->toBe((float) $value); + } +}); + +it('gives every md-type class the declarations of its type utility', function () { + preg_match_all('/@utility (type-[\w-]+) \{\n(.*?)\n\}/s', File::get(stylesheetPath('tokens/utilities.css')), $matches, PREG_SET_ORDER); + + $utilities = collect($matches)->mapWithKeys(fn (array $match): array => ["md-{$match[1]}" => stylesheetDeclarations($match[2])]); + $text = File::get(stylesheetPath('text.css')); + + expect($utilities)->toHaveCount(30); + + foreach ($utilities as $class => $declarations) { + expect(stylesheetDeclarations(stylesheetBlock($text, ".{$class}")))->toBe($declarations, $class) + ->and(array_keys($declarations))->toBe(['font', 'letter-spacing', 'font-variation-settings']); + } +}); + +it('ships exactly the documented text classes, each ink an M3 role', function () { + $text = File::get(stylesheetPath('text.css')); + + $styles = collect(['display', 'headline', 'title', 'body', 'label']) + ->crossJoin(['lg', 'md', 'sm']) + ->flatMap(fn (array $style): array => ["md-type-{$style[0]}-{$style[1]}", "md-type-emphasized-{$style[0]}-{$style[1]}"]); + + $inks = [ + 'md-ink' => 'on-surface', + 'md-ink-variant' => 'on-surface-variant', + 'md-ink-quiet' => 'outline', + 'md-ink-primary' => 'primary', + 'md-ink-error' => 'error', + 'md-ink-success' => 'success', + 'md-ink-warning' => 'warning', + 'md-ink-info' => 'info', + 'md-ink-inverse' => 'inverse-on-surface', + ]; + + $rest = [ + 'md-text-start' => ['text-align' => 'start'], + 'md-text-center' => ['text-align' => 'center'], + 'md-text-end' => ['text-align' => 'end'], + 'md-truncate' => ['overflow' => 'hidden', 'text-overflow' => 'ellipsis', 'white-space' => 'nowrap'], + 'md-line-clamp-2' => ['overflow' => 'hidden', 'display' => '-webkit-box', '-webkit-box-orient' => 'vertical', '-webkit-line-clamp' => '2'], + 'md-line-clamp-3' => ['overflow' => 'hidden', 'display' => '-webkit-box', '-webkit-box-orient' => 'vertical', '-webkit-line-clamp' => '3'], + 'md-nowrap' => ['white-space' => 'nowrap'], + 'md-tabular' => ['font-variant-numeric' => 'tabular-nums'], + 'md-visually-hidden' => [ + 'position' => 'absolute', 'width' => '1px', 'height' => '1px', 'padding' => '0', 'margin' => '-1px', + 'overflow' => 'hidden', 'clip-path' => 'inset(50%)', 'white-space' => 'nowrap', 'border-width' => '0', + ], + ]; + + $classes = collect(stylesheetItems(stylesheetBlock($text, '@layer material.text'))) + ->map(fn (array $item): string => $item['prelude'] ?? $item['statement']) + ->all(); + + expect($classes)->toBe($styles->merge(array_keys($inks))->merge(array_keys($rest))->map(fn (string $class): string => ".{$class}")->all()); + + foreach ($inks as $class => $role) { + expect(stylesheetDeclarations(stylesheetBlock($text, ".{$class}")))->toBe(['color' => "var(--md-sys-color-{$role})"]); + } + + foreach ($rest as $class => $declarations) { + expect(stylesheetDeclarations(stylesheetBlock($text, ".{$class}")))->toBe($declarations); + } +}); + +it('draws the interaction classes as the utilities the Tailwind components still use', function () { + $utilities = File::get(stylesheetPath('tokens/utilities.css')); + $interaction = File::get(stylesheetPath('foundation/interaction.css')); + + $normalise = fn (string $body): string => trim((string) preg_replace('/\s+/', ' ', $body)); + + foreach (['state-layer', 'focus-ring', 'touch-target', 'link'] as $name) { + $class = stylesheetBlock($interaction, ".md-{$name}"); + + expect($normalise(str_replace( + // The two deliberate differences: the data-md-* hook beside the old one, and M3's 48 + // CSS pixels for the target where the utility reads 3rem. + ['&:is([data-md-dragged], [data-dragged])', 'var(--md-sys-measurement-space600)'], + ['&[data-dragged]', '3rem'], + $class, + )))->toBe($normalise(stylesheetBlock($utilities, "@utility {$name}")), $name); + } + + expect(stylesheetBlock($interaction, '.md-state-layer'))->toContain('&:is([data-md-dragged], [data-dragged])::before') + ->and(stylesheetBlock($interaction, '.md-touch-target'))->toContain('min-width: var(--md-sys-measurement-space600);'); +}); + +it('declares the material layers in the Workbench before Tailwind\'s', function () { + $app = stylesheetWithoutComments(File::get(__DIR__.'/../../workbench/resources/css/app.css')); + + $foundation = strpos($app, "@import '../../../resources/css/foundation.css';"); + $tailwind = strpos($app, "@import 'tailwindcss'"); + $components = strpos($app, "@import '../../../resources/css/tailwind.css';"); + + expect($foundation)->toBeInt() + ->and($tailwind)->toBeGreaterThan($foundation) + ->and($components)->toBeGreaterThan($tailwind) + // material.css would declare the tokens a second time. + ->and($app)->not->toContain('material.css'); +}); From 7fb7fb7bf29d54f3be98349fdc2dbd1c97968c5a Mon Sep 17 00:00:00 2001 From: Andreas Reinhold / reini Date: Mon, 14 Sep 2026 13:43:33 +0200 Subject: [PATCH 7/7] Name the foundation and the text classes in the development skill Plan steps 33 and 34. The Tokens section gains a paragraph naming foundation.css as the one required import, its layer order, and the md-* text classes as 2.0.0's vocabulary; the full rewrite of the docs is step 47. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9 --- resources/boost/skills/livewire-material-development/SKILL.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/resources/boost/skills/livewire-material-development/SKILL.md b/resources/boost/skills/livewire-material-development/SKILL.md index ecf1e992..56713dd2 100644 --- a/resources/boost/skills/livewire-material-development/SKILL.md +++ b/resources/boost/skills/livewire-material-development/SKILL.md @@ -88,6 +88,8 @@ Scheme::resolveProfileUsing(fn (): ?string => Setting::get('color_profile')); ## Tokens +2.0.0's vocabulary is plain CSS, without Tailwind. `resources/css/foundation.css` is the one required import, first in the entry (while an application still builds Tailwind: before `@import 'tailwindcss'`, with `resources/css/tailwind.css` after it in place of `material.css`). It declares the layer order `material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility`, so an application's unlayered CSS beats every package rule, and `hidden` or `x-cloak` hides any element. It brings the reset, every `--md-sys-*` token (spacing included: `--md-sys-measurement-space25` … `space900`, 2–72px), and `md-state-layer`, `md-focus-ring`, `md-touch-target`, `md-link`. Text on plain elements takes the fixed text classes of `resources/css/text.css` and nothing else: `md-type-{display|headline|title|body|label}-{lg|md|sm}` and `md-type-emphasized-…`; `md-ink` (on-surface), `md-ink-variant`, `md-ink-quiet`, `md-ink-primary`, `md-ink-error`, `md-ink-success`, `md-ink-warning`, `md-ink-info`, `md-ink-inverse`; `md-text-start|center|end`, `md-truncate`, `md-line-clamp-2|3`, `md-nowrap`, `md-tabular`, `md-visually-hidden`. The Tailwind names below stay for the components not yet rewritten. + Tailwind's default palette is cleared: every colour class names an M3 role. `text-red-600`, `bg-base-200` or `text-gray-500` compile to nothing. The rules behind the names below — which role, surface container, corner, type style, elevation level, motion spring, state and window size class to use, and M3's don'ts — are the `material-3` guideline (always on) and the `material-3-design` skill (the tables and Google's source pages); activate that skill before designing a screen. - Colour roles (`bg-*`, `text-*`, `border-*`, …): `primary`, `on-primary`, `primary-container`, `on-primary-container`, `inverse-primary`, `primary-fixed`, `primary-fixed-dim`, `on-primary-fixed`, `on-primary-fixed-variant`; the same for `secondary` and `tertiary`; `error`, `on-error`, `error-container`, `on-error-container`; `success`, `warning` and `info` with their `on-`, `-container` and `on-…-container`; `inverse-error|success|warning|info`; `surface`, `surface-dim`, `surface-bright`, `surface-container-lowest|low||high|highest`, `on-surface`, `on-surface-variant`, `inverse-surface`, `inverse-on-surface`, `outline`, `outline-variant`, `scrim`, `shadow`; plus `white` and `black`.