diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/AccentColorSwitcher/BitAccentColorSwitcher.razor.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/AccentColorSwitcher/BitAccentColorSwitcher.razor.cs index 04b16c776b..be73ee0c5d 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/AccentColorSwitcher/BitAccentColorSwitcher.razor.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/AccentColorSwitcher/BitAccentColorSwitcher.razor.cs @@ -17,10 +17,10 @@ public partial class BitAccentColorSwitcher : BitComponentBase /// The accent colors offered when is not set (on the /// parameter nor on the configuration registered in DI): the six /// hues, starting with the packaged palette's own primary. - /// That first item is named "Default" rather than after a hue: its token is the Fluent blue the - /// service treats as "no override", but the swatch paints the primary of whichever packaged - /// preset is active (purple under Material, iOS blue under Cupertino - see the --bit-acs-ntr - /// custom property in BitAccentColorSwitcher.scss), so naming it "Blue" would be wrong there. + /// That first item is named "Default" rather than after a hue: its token is the blue the service + /// treats as "no override", but the swatch paints the primary of whichever packaged preset is + /// active (iOS blue under Cupertino - see the --bit-acs-ntr custom property in + /// BitAccentColorSwitcher.scss), so naming it "Blue" would be wrong there. /// public static readonly IReadOnlyList DefaultAccents = [ @@ -190,12 +190,12 @@ private static string GetSwatchColor(string? color) // The neutral accent is "the packaged palette's own primary" (see BitAccentColorService): // picking it clears the overrides, so what it yields is whatever primary the active preset - // ships - Fluent's blue, Material's purple, Cupertino's system blue. Its swatch therefore - // paints through --bit-acs-ntr, which the stylesheet sets per packaged preset (the Fluent - // blue by default; the Material and Cupertino bundles retune it), with the token's own hex - // as the fallback for a page that loads none of the bundles. Read from a custom property - // rather than from the theme's --bit-clr-pri, which an applied accent overrides - the point - // of this swatch is to show the primary underneath that override. + // ships - this blue under Fluent, Fluent 2 and Material, Cupertino's system blue. Its swatch + // therefore paints through --bit-acs-ntr, which the stylesheet sets per packaged preset (this + // blue by default; the Cupertino bundle retunes it), with the token's own hex as the fallback + // for a page that loads none of the bundles. Read from a custom property rather than from the + // theme's --bit-clr-pri, which an applied accent overrides - the point of this swatch is to + // show the primary underneath that override. if (BitAccentColorSsr.NormalizeToken(value) == BitAccentColorSsr.NormalizeToken(BitAccentColorPresets.Blue)) { return $"var(--bit-acs-ntr, {BitAccentColorPresets.Blue})"; diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/AccentColorSwitcher/BitAccentColorSwitcher.scss b/src/BlazorUI/Bit.BlazorUI.Extras/Components/AccentColorSwitcher/BitAccentColorSwitcher.scss index 22cbeef071..bcf62b6074 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/AccentColorSwitcher/BitAccentColorSwitcher.scss +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/AccentColorSwitcher/BitAccentColorSwitcher.scss @@ -3,21 +3,20 @@ .bit-acs { --bit-acs-size: #{rem2(24px)}; --bit-acs-gap: #{rem2(10px)}; - // What the neutral ("Default") swatch paints: the primary the active packaged preset ships, - // since picking that swatch clears every accent override and leaves exactly that primary. The - // Fluent light primary here (BitAccentColorPresets.Blue - a literal, not $clr-pri, which an - // applied accent rewrites while this swatch has to keep showing the color underneath it); the - // two rules below retune it to the Material and Cupertino seeds. A custom preset sets it on - // .bit-acs to its own primary. + // What the neutral ("Default") swatch paints: the seed blue of the active packaged preset - the + // color picking that swatch returns the palette to, since it clears every accent override. A + // literal (BitAccentColorPresets.Blue) rather than $clr-pri, because an applied accent rewrites + // $clr-pri while this swatch has to keep showing the color underneath it. It is the primary the + // Fluent and Material light schemes ship, and Fluent 2's #0F6CBD is the same blue a shade off; + // their dark schemes lighten that same hue (#4FA3F4), so the swatch names the palette family's + // blue rather than the exact primary of the scheme in use. The rule below retunes it to + // Cupertino's seed, the one packaged preset far enough away for the difference to show. A custom + // preset sets it on .bit-acs to its own seed. --bit-acs-ntr: #1276C6; - // The seeds the packaged Material and Cupertino palettes are generated from (M3 baseline - // primary, iOS system blue). Matched on the prefix so the scheme suffix and a scoped preset - // (the attribute on an ancestor rather than on :root) both count. - [bit-theme^="material"] & { - --bit-acs-ntr: #6750A4; - } - + // The seed the packaged Cupertino palette is generated from (iOS system blue). Matched on the + // prefix so the scheme suffix and a scoped preset (the attribute on an ancestor rather than on + // :root) both count. [bit-theme^="cupertino"] & { --bit-acs-ntr: #0071E3; } diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Styles/Material/colors.material-dark.scss b/src/BlazorUI/Bit.BlazorUI.Extras/Styles/Material/colors.material-dark.scss index be5f6b5684..6aacbe2191 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Styles/Material/colors.material-dark.scss +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Styles/Material/colors.material-dark.scss @@ -2,19 +2,19 @@ :root[bit-theme="material-dark"], :root [bit-theme="material-dark"] { color-scheme: dark; - --bit-clr-pri: #917BD1; - --bit-clr-pri-hover: #9B87D8; - --bit-clr-pri-active: #A290DD; - --bit-clr-pri-dark: #8572BD; - --bit-clr-pri-dark-hover: #8975C5; - --bit-clr-pri-dark-active: #8C79CA; - --bit-clr-pri-light: #AEA0DF; - --bit-clr-pri-light-hover: #B8ADE2; - --bit-clr-pri-light-active: #BFB6E3; + --bit-clr-pri: #4FA3F4; + --bit-clr-pri-hover: #61AEFB; + --bit-clr-pri-active: #6EB6FF; + --bit-clr-pri-dark: #3E87CE; + --bit-clr-pri-dark-hover: #4492DD; + --bit-clr-pri-dark-active: #499AE7; + --bit-clr-pri-light: #89C3FF; + --bit-clr-pri-light-hover: #9FCEFF; + --bit-clr-pri-light-active: #AED5FF; --bit-clr-pri-text: #141414; - --bit-clr-pri-dis: #2F2B3C; - --bit-clr-pri-dis-text: #7B7690; - --bit-clr-pri-focus: #917BD1; + --bit-clr-pri-dis: #222F3C; + --bit-clr-pri-dis-text: #697D92; + --bit-clr-pri-focus: #4FA3F4; --bit-clr-sec: #8C859B; --bit-clr-sec-hover: #968FA5; --bit-clr-sec-active: #9E97AC; @@ -41,19 +41,19 @@ --bit-clr-ter-dis: #3B282E; --bit-clr-ter-dis-text: #8F717A; --bit-clr-ter-focus: #A97C8A; - --bit-clr-inf: #ABA9B4; - --bit-clr-inf-hover: #B6B5BE; - --bit-clr-inf-active: #BEBCC5; - --bit-clr-inf-dark: #908E96; - --bit-clr-inf-dark-hover: #9A99A2; - --bit-clr-inf-dark-active: #A2A0A9; - --bit-clr-inf-light: #CBC9D1; - --bit-clr-inf-light-hover: #D6D4DB; - --bit-clr-inf-light-active: #DDDDE4; + --bit-clr-inf: #A4ACB4; + --bit-clr-inf-hover: #B0B7BE; + --bit-clr-inf-active: #B8BEC6; + --bit-clr-inf-dark: #8A9097; + --bit-clr-inf-dark-hover: #949BA2; + --bit-clr-inf-dark-active: #9BA2AA; + --bit-clr-inf-light: #C5CBD2; + --bit-clr-inf-light-hover: #D1D6DC; + --bit-clr-inf-light-active: #D9DFE4; --bit-clr-inf-text: #141414; - --bit-clr-inf-dis: #2E2C35; - --bit-clr-inf-dis-text: #7B7983; - --bit-clr-inf-focus: #ABA9B4; + --bit-clr-inf-dis: #282F35; + --bit-clr-inf-dis-text: #747B83; + --bit-clr-inf-focus: #A4ACB4; --bit-clr-suc: #78B774; --bit-clr-suc-hover: #86C182; --bit-clr-suc-active: #90C98C; @@ -106,141 +106,141 @@ --bit-clr-err-dis: #3C2825; --bit-clr-err-dis-text: #91726D; --bit-clr-err-focus: #E35B4C; - --bit-clr-fg-pri: #EBE9F7; - --bit-clr-fg-pri-hover: #F2EFFC; - --bit-clr-fg-pri-active: #F7F5FF; - --bit-clr-fg-pri-dark: #D8D5E2; - --bit-clr-fg-pri-dark-hover: #E0DDEA; - --bit-clr-fg-pri-dark-active: #E6E4F2; - --bit-clr-fg-pri-light: #F9F7FF; - --bit-clr-fg-pri-light-hover: #FCFBFF; - --bit-clr-fg-pri-light-active: #FFFEFF; - --bit-clr-fg-pri-dis: #2E292D; - --bit-clr-fg-pri-dis-text: #77737A; - --bit-clr-fg-pri-focus: #EBE9F7; - --bit-clr-fg-sec: #BEBBC8; - --bit-clr-fg-sec-hover: #C8C5D2; - --bit-clr-fg-sec-active: #D0CDDA; - --bit-clr-fg-sec-dark: #A8A5B2; - --bit-clr-fg-sec-dark-hover: #B0B0BC; - --bit-clr-fg-sec-dark-active: #B8B8C4; - --bit-clr-fg-sec-light: #D4D2E0; - --bit-clr-fg-sec-light-hover: #DEDCEA; - --bit-clr-fg-sec-light-active: #E6E4F2; - --bit-clr-fg-sec-dis: #2E292D; - --bit-clr-fg-sec-dis-text: #77737A; - --bit-clr-fg-sec-focus: #BEBBC8; - --bit-clr-fg-ter: #92919D; - --bit-clr-fg-ter-hover: #9B9AA6; - --bit-clr-fg-ter-active: #A2A2AE; - --bit-clr-fg-ter-dark: #7D7C87; - --bit-clr-fg-ter-dark-hover: #868590; - --bit-clr-fg-ter-dark-active: #8E8B98; - --bit-clr-fg-ter-light: #A8A5B2; - --bit-clr-fg-ter-light-hover: #B0B0BC; - --bit-clr-fg-ter-light-active: #B8B8C4; - --bit-clr-fg-ter-dis: #2E292D; - --bit-clr-fg-ter-dis-text: #77737A; - --bit-clr-fg-ter-focus: #92919D; - --bit-clr-fg-dis: #63626D; - --bit-clr-bg-pri: #14111A; - --bit-clr-bg-pri-hover: #1A1822; - --bit-clr-bg-pri-active: #1E1D27; - --bit-clr-bg-pri-dark: #0A0912; - --bit-clr-bg-pri-dark-hover: #120F18; - --bit-clr-bg-pri-dark-active: #16131D; - --bit-clr-bg-pri-light: #1D1A24; - --bit-clr-bg-pri-light-hover: #25202B; - --bit-clr-bg-pri-light-active: #272630; - --bit-clr-bg-pri-dis: #0F0B0E; - --bit-clr-bg-pri-dis-text: #5F5E64; - --bit-clr-bg-pri-focus: #14111A; - --bit-clr-bg-sec: #1F1E28; - --bit-clr-bg-sec-hover: #26252F; - --bit-clr-bg-sec-active: #2B2A35; - --bit-clr-bg-sec-dark: #15151F; - --bit-clr-bg-sec-dark-hover: #1E1B25; - --bit-clr-bg-sec-dark-active: #21202A; - --bit-clr-bg-sec-light: #2B2631; - --bit-clr-bg-sec-light-hover: #302F3A; - --bit-clr-bg-sec-light-active: #35343F; - --bit-clr-bg-sec-dis: #1B171A; - --bit-clr-bg-sec-dis-text: #66656B; - --bit-clr-bg-sec-focus: #1F1E28; - --bit-clr-bg-ter: #2B2A35; - --bit-clr-bg-ter-hover: #33303C; - --bit-clr-bg-ter-active: #373742; - --bit-clr-bg-ter-dark: #21202A; - --bit-clr-bg-ter-dark-hover: #2B2631; - --bit-clr-bg-ter-dark-active: #2E2B37; - --bit-clr-bg-ter-light: #35343F; - --bit-clr-bg-ter-light-hover: #3C3C47; - --bit-clr-bg-ter-light-active: #43404C; - --bit-clr-bg-ter-dis: #242329; - --bit-clr-bg-ter-dis-text: #6C6E75; - --bit-clr-bg-ter-focus: #2B2A35; - --bit-clr-bg-dis: #1B1923; + --bit-clr-fg-pri: #E2EDF7; + --bit-clr-fg-pri-hover: #E9F3FD; + --bit-clr-fg-pri-active: #F1F7FF; + --bit-clr-fg-pri-dark: #CFD9E3; + --bit-clr-fg-pri-dark-hover: #D7E1EB; + --bit-clr-fg-pri-dark-active: #DDE8F2; + --bit-clr-fg-pri-light: #F4F9FF; + --bit-clr-fg-pri-light-hover: #F9FCFF; + --bit-clr-fg-pri-light-active: #FEFEFF; + --bit-clr-fg-pri-dis: #2B2B30; + --bit-clr-fg-pri-dis-text: #72757C; + --bit-clr-fg-pri-focus: #E2EDF7; + --bit-clr-fg-sec: #B6BEC9; + --bit-clr-fg-sec-hover: #C0C9D3; + --bit-clr-fg-sec-active: #C7D1DB; + --bit-clr-fg-sec-dark: #A0A8B3; + --bit-clr-fg-sec-dark-hover: #A8B3BC; + --bit-clr-fg-sec-dark-active: #B0BBC4; + --bit-clr-fg-sec-light: #CBD6E0; + --bit-clr-fg-sec-light-hover: #D5E0EA; + --bit-clr-fg-sec-light-active: #DDE8F2; + --bit-clr-fg-sec-dis: #2B2B30; + --bit-clr-fg-sec-dis-text: #72757C; + --bit-clr-fg-sec-focus: #B6BEC9; + --bit-clr-fg-ter: #8A949D; + --bit-clr-fg-ter-hover: #939DA6; + --bit-clr-fg-ter-active: #9AA5AE; + --bit-clr-fg-ter-dark: #757F88; + --bit-clr-fg-ter-dark-hover: #7E8891; + --bit-clr-fg-ter-dark-active: #868E99; + --bit-clr-fg-ter-light: #A0A8B3; + --bit-clr-fg-ter-light-hover: #A8B3BC; + --bit-clr-fg-ter-light-active: #B0BBC4; + --bit-clr-fg-ter-dis: #2B2B30; + --bit-clr-fg-ter-dis-text: #72757C; + --bit-clr-fg-ter-focus: #8A949D; + --bit-clr-fg-dis: #5C656D; + --bit-clr-bg-pri: #0C131B; + --bit-clr-bg-pri-hover: #121A23; + --bit-clr-bg-pri-active: #171F28; + --bit-clr-bg-pri-dark: #050B12; + --bit-clr-bg-pri-dark-hover: #0A1119; + --bit-clr-bg-pri-dark-active: #0E151E; + --bit-clr-bg-pri-light: #151C25; + --bit-clr-bg-pri-light-hover: #1D232D; + --bit-clr-bg-pri-light-active: #202831; + --bit-clr-bg-pri-dis: #0C0C10; + --bit-clr-bg-pri-dis-text: #5B6065; + --bit-clr-bg-pri-focus: #0C131B; + --bit-clr-bg-sec: #182029; + --bit-clr-bg-sec-hover: #1F2730; + --bit-clr-bg-sec-active: #242C35; + --bit-clr-bg-sec-dark: #0F171E; + --bit-clr-bg-sec-dark-hover: #161D26; + --bit-clr-bg-sec-dark-active: #1A222B; + --bit-clr-bg-sec-light: #232933; + --bit-clr-bg-sec-light-hover: #29313A; + --bit-clr-bg-sec-light-active: #2E363F; + --bit-clr-bg-sec-dis: #18181D; + --bit-clr-bg-sec-dis-text: #62676C; + --bit-clr-bg-sec-focus: #182029; + --bit-clr-bg-ter: #242C35; + --bit-clr-bg-ter-hover: #2B333D; + --bit-clr-bg-ter-active: #2F3A42; + --bit-clr-bg-ter-dark: #1A222B; + --bit-clr-bg-ter-dark-hover: #232933; + --bit-clr-bg-ter-dark-active: #262E38; + --bit-clr-bg-ter-light: #2E363F; + --bit-clr-bg-ter-light-hover: #343F47; + --bit-clr-bg-ter-light-active: #3B434D; + --bit-clr-bg-ter-dis: #202429; + --bit-clr-bg-ter-dis-text: #697074; + --bit-clr-bg-ter-focus: #242C35; + --bit-clr-bg-dis: #131B24; --bit-clr-bg-overlay: rgba(0, 0, 0, 0.6); - --bit-clr-brd-pri: #7A7885; - --bit-clr-brd-pri-hover: #898794; - --bit-clr-brd-pri-active: #9593A0; - --bit-clr-brd-pri-dark: #696774; - --bit-clr-brd-pri-dark-hover: #777582; - --bit-clr-brd-pri-dark-active: #83818E; - --bit-clr-brd-pri-light: #8C8A97; - --bit-clr-brd-pri-light-hover: #9C99A7; - --bit-clr-brd-pri-light-active: #A8A5B3; - --bit-clr-brd-pri-dis: #2E292D; - --bit-clr-brd-pri-dis-text: #77737A; - --bit-clr-brd-pri-focus: #7A7885; - --bit-clr-brd-sec: #585663; - --bit-clr-brd-sec-hover: #666471; - --bit-clr-brd-sec-active: #716F7C; - --bit-clr-brd-sec-dark: #484551; - --bit-clr-brd-sec-dark-hover: #555360; - --bit-clr-brd-sec-dark-active: #605E6B; - --bit-clr-brd-sec-light: #696774; - --bit-clr-brd-sec-light-hover: #777582; - --bit-clr-brd-sec-light-active: #83818E; - --bit-clr-brd-sec-dis: #2E292D; - --bit-clr-brd-sec-dis-text: #77737A; - --bit-clr-brd-sec-focus: #585663; - --bit-clr-brd-ter: #3C3C47; - --bit-clr-brd-ter-hover: #484551; - --bit-clr-brd-ter-active: #504D59; - --bit-clr-brd-ter-dark: #2E2B37; - --bit-clr-brd-ter-dark-hover: #373742; - --bit-clr-brd-ter-dark-active: #403D49; - --bit-clr-brd-ter-light: #585663; - --bit-clr-brd-ter-light-hover: #63616E; - --bit-clr-brd-ter-light-active: #6D6976; - --bit-clr-brd-ter-dis: #2E292D; - --bit-clr-brd-ter-dis-text: #77737A; - --bit-clr-brd-ter-focus: #3C3C47; - --bit-clr-brd-dis: #302F3A; + --bit-clr-brd-pri: #717C86; + --bit-clr-brd-pri-hover: #808B95; + --bit-clr-brd-pri-active: #8C97A1; + --bit-clr-brd-pri-dark: #606B75; + --bit-clr-brd-pri-dark-hover: #6E7983; + --bit-clr-brd-pri-dark-active: #7A858F; + --bit-clr-brd-pri-light: #838E98; + --bit-clr-brd-pri-light-hover: #939CA8; + --bit-clr-brd-pri-light-active: #9FA8B4; + --bit-clr-brd-pri-dis: #2B2B30; + --bit-clr-brd-pri-dis-text: #72757C; + --bit-clr-brd-pri-focus: #717C86; + --bit-clr-brd-sec: #505A63; + --bit-clr-brd-sec-hover: #5E6872; + --bit-clr-brd-sec-active: #68737D; + --bit-clr-brd-sec-dark: #404852; + --bit-clr-brd-sec-dark-hover: #4D5660; + --bit-clr-brd-sec-dark-active: #58626B; + --bit-clr-brd-sec-light: #606B75; + --bit-clr-brd-sec-light-hover: #6E7983; + --bit-clr-brd-sec-light-active: #7A858F; + --bit-clr-brd-sec-dis: #2B2B30; + --bit-clr-brd-sec-dis-text: #72757C; + --bit-clr-brd-sec-focus: #505A63; + --bit-clr-brd-ter: #343F47; + --bit-clr-brd-ter-hover: #404852; + --bit-clr-brd-ter-active: #48505A; + --bit-clr-brd-ter-dark: #262E38; + --bit-clr-brd-ter-dark-hover: #2F3A42; + --bit-clr-brd-ter-dark-active: #38404A; + --bit-clr-brd-ter-light: #505A63; + --bit-clr-brd-ter-light-hover: #5B656E; + --bit-clr-brd-ter-light-active: #646C78; + --bit-clr-brd-ter-dis: #2B2B30; + --bit-clr-brd-ter-dis-text: #72757C; + --bit-clr-brd-ter-focus: #343F47; + --bit-clr-brd-dis: #29313A; --bit-clr-req: #E56167; --bit-clr-ntr-white: #FFFFFF; --bit-clr-ntr-black: #000000; - --bit-clr-ntr-gray10: #F9FAF6; - --bit-clr-ntr-gray20: #F2F3EF; - --bit-clr-ntr-gray30: #EBECE7; - --bit-clr-ntr-gray40: #DFE0DB; - --bit-clr-ntr-gray50: #D0D1CC; - --bit-clr-ntr-gray60: #C6C7C2; - --bit-clr-ntr-gray70: #BCBCB6; - --bit-clr-ntr-gray80: #B1B1AB; - --bit-clr-ntr-gray90: #9FA09B; - --bit-clr-ntr-gray100: #959691; - --bit-clr-ntr-gray110: #888984; - --bit-clr-ntr-gray120: #777873; - --bit-clr-ntr-gray130: #5E5F5A; - --bit-clr-ntr-gray140: #464742; - --bit-clr-ntr-gray150: #3A3B37; - --bit-clr-ntr-gray160: #31322E; - --bit-clr-ntr-gray170: #282925; - --bit-clr-ntr-gray180: #242521; - --bit-clr-ntr-gray190: #1F201C; - --bit-clr-ntr-gray200: #1A1B17; - --bit-clr-ntr-gray210: #151612; - --bit-clr-ntr-gray220: #10110D; + --bit-clr-ntr-gray10: #FCF9F6; + --bit-clr-ntr-gray20: #F5F2EF; + --bit-clr-ntr-gray30: #EFEBE7; + --bit-clr-ntr-gray40: #E3DFDB; + --bit-clr-ntr-gray50: #D4D0CC; + --bit-clr-ntr-gray60: #CAC6C2; + --bit-clr-ntr-gray70: #C0BBB6; + --bit-clr-ntr-gray80: #B5B0AB; + --bit-clr-ntr-gray90: #A39F9B; + --bit-clr-ntr-gray100: #999591; + --bit-clr-ntr-gray110: #8C8884; + --bit-clr-ntr-gray120: #7B7773; + --bit-clr-ntr-gray130: #615E5A; + --bit-clr-ntr-gray140: #494642; + --bit-clr-ntr-gray150: #3C3A37; + --bit-clr-ntr-gray160: #33312E; + --bit-clr-ntr-gray170: #2A2825; + --bit-clr-ntr-gray180: #262421; + --bit-clr-ntr-gray190: #211F1C; + --bit-clr-ntr-gray200: #1C1A17; + --bit-clr-ntr-gray210: #171512; + --bit-clr-ntr-gray220: #12100D; } diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Styles/Material/colors.material-light.scss b/src/BlazorUI/Bit.BlazorUI.Extras/Styles/Material/colors.material-light.scss index bd86e6eb33..7a04e58434 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Styles/Material/colors.material-light.scss +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Styles/Material/colors.material-light.scss @@ -1,7 +1,21 @@ -// Material preset - light palette. GENERATED by the theme seed pipeline (BitThemeFactory -// .CreateLightThemeFromSeed("#6750A4", NeutralTintChroma 0.008) with the M3 secondary/tertiary/ -// error mains refilled via BitThemeColorDerivation), so the ramps satisfy the same WCAG contract -// as the packaged Fluent palettes. Regenerate with the same pipeline rather than editing by hand. +// Material preset - light palette. GENERATED by the theme seed pipeline, so the ramps satisfy the +// same WCAG contract as the packaged Fluent palettes. Regenerate with the pipeline, not by hand: +// 1. BitThemeFactory.CreateLightThemeFromSeed("#1276C6", NeutralTintChroma 0.008) - the packaged +// palettes' own primary rather than M3's baseline #6750A4, deliberately against what Material +// names: every packaged preset ships the same brand blue, so the accent switcher's "Default" +// swatch means one color whichever preset is active, and M3's purple is offered next to it as +// an accent instead (BitAccentColorPresets.Purple). The neutral tint is what keeps the greys +// reading as Material's tinted surfaces rather than Fluent's untinted ones - it now carries +// the seed's blue where it used to carry the purple's. +// 2. Secondary, tertiary and error refilled from M3's own mains - #625B71, #7D5260, #B3261E - +// which M3 names independently of the seed, so they stay M3's. Each is refilled by running +// the main back through the pipeline and taking its primary ladder +// (CreateLightThemeFromSeed(main).Color.Primary, CreateDarkThemeFromSeed(main) for the dark +// palette, which applies the packaged light-to-dark relationship to it): primary is the one +// role the pipeline lands exactly on its seed, so a refilled role carries the same hand-solved +// ramp as the roles the seed itself produced. +// 3. Every role's disabled pair overwritten with BitThemeColorDerivation.FillColorRoleFromMain's, +// which is the pair BitThemeColorDerivationTests holds every packaged palette to. // The ":root [bit-theme=…]" descendant twins scope the palette to any element carrying the // attribute (see colors.fluent-light.scss). :root[bit-theme="material"], @@ -9,19 +23,19 @@ :root[bit-theme="material-light"], :root [bit-theme="material-light"] { color-scheme: light; - --bit-clr-pri: #6750A4; - --bit-clr-pri-hover: #5B4693; - --bit-clr-pri-active: #523D86; - --bit-clr-pri-dark: #453373; - --bit-clr-pri-dark-hover: #3A2A63; - --bit-clr-pri-dark-active: #312255; - --bit-clr-pri-light: #AC9DDF; - --bit-clr-pri-light-hover: #A090D5; - --bit-clr-pri-light-active: #9684CD; + --bit-clr-pri: #1276C6; + --bit-clr-pri-hover: #0B6AB4; + --bit-clr-pri-active: #0460A5; + --bit-clr-pri-dark: #005391; + --bit-clr-pri-dark-hover: #00487F; + --bit-clr-pri-dark-active: #003E70; + --bit-clr-pri-light: #85C1FF; + --bit-clr-pri-light-hover: #74B4F6; + --bit-clr-pri-light-active: #66A9EE; --bit-clr-pri-text: #FFFFFF; - --bit-clr-pri-dis: #ECE8FE; - --bit-clr-pri-dis-text: #847F99; - --bit-clr-pri-focus: #6750A4; + --bit-clr-pri-dis: #DDEDFF; + --bit-clr-pri-dis-text: #71869B; + --bit-clr-pri-focus: #1276C6; --bit-clr-sec: #625B71; --bit-clr-sec-hover: #575064; --bit-clr-sec-active: #4D475A; @@ -48,19 +62,19 @@ --bit-clr-ter-dis: #FDE3EB; --bit-clr-ter-dis-text: #987A83; --bit-clr-ter-focus: #7D5260; - --bit-clr-inf: #73707B; - --bit-clr-inf-hover: #66666F; - --bit-clr-inf-active: #5D5C65; - --bit-clr-inf-dark: #514F57; - --bit-clr-inf-dark-hover: #46444B; - --bit-clr-inf-dark-active: #3C3B42; - --bit-clr-inf-light: #BCBAC2; - --bit-clr-inf-light-hover: #AFAEB7; - --bit-clr-inf-light-active: #A4A3AC; + --bit-clr-inf: #6B737C; + --bit-clr-inf-hover: #60686F; + --bit-clr-inf-active: #575E65; + --bit-clr-inf-dark: #4B5158; + --bit-clr-inf-dark-hover: #41464C; + --bit-clr-inf-dark-active: #383D42; + --bit-clr-inf-light: #B6BCC3; + --bit-clr-inf-light-hover: #A9B0B7; + --bit-clr-inf-light-active: #9EA5AC; --bit-clr-inf-text: #FFFFFF; - --bit-clr-inf-dis: #ECE9F5; - --bit-clr-inf-dis-text: #84818C; - --bit-clr-inf-focus: #73707B; + --bit-clr-inf-dis: #E3ECF7; + --bit-clr-inf-dis-text: #7C848D; + --bit-clr-inf-focus: #6B737C; --bit-clr-suc: #228422; --bit-clr-suc-hover: #1B771C; --bit-clr-suc-active: #156B16; @@ -113,141 +127,141 @@ --bit-clr-err-dis: #FFE4E0; --bit-clr-err-dis-text: #9A7B76; --bit-clr-err-focus: #B3261E; - --bit-clr-fg-pri: #1A191E; - --bit-clr-fg-pri-hover: #121115; - --bit-clr-fg-pri-active: #0A090D; - --bit-clr-fg-pri-dark: #0D0C10; - --bit-clr-fg-pri-dark-hover: #08080B; + --bit-clr-fg-pri: #171A1E; + --bit-clr-fg-pri-hover: #0F1216; + --bit-clr-fg-pri-active: #080A0D; + --bit-clr-fg-pri-dark: #0B0D10; + --bit-clr-fg-pri-dark-hover: #06080B; --bit-clr-fg-pri-dark-active: #000000; - --bit-clr-fg-pri-light: #2E2D32; - --bit-clr-fg-pri-light-hover: #26252A; - --bit-clr-fg-pri-light-active: #1F1E23; - --bit-clr-fg-pri-dis: #EBEAF0; - --bit-clr-fg-pri-dis-text: #85848A; - --bit-clr-fg-pri-focus: #1A191E; - --bit-clr-fg-sec: #525156; - --bit-clr-fg-sec-hover: #454449; - --bit-clr-fg-sec-active: #38373C; - --bit-clr-fg-sec-dark: #3D3C41; - --bit-clr-fg-sec-dark-hover: #302F34; - --bit-clr-fg-sec-dark-active: #242328; - --bit-clr-fg-sec-light: #6B6A6F; - --bit-clr-fg-sec-light-hover: #5E5D62; - --bit-clr-fg-sec-light-active: #4D4C51; - --bit-clr-fg-sec-dis: #EBEAF0; - --bit-clr-fg-sec-dis-text: #85848A; - --bit-clr-fg-sec-focus: #525156; - --bit-clr-fg-ter: #706F74; - --bit-clr-fg-ter-hover: #636267; - --bit-clr-fg-ter-active: #57565B; - --bit-clr-fg-ter-dark: #5C5B60; - --bit-clr-fg-ter-dark-hover: #4F4E53; - --bit-clr-fg-ter-dark-active: #424146; - --bit-clr-fg-ter-light: #8A898F; - --bit-clr-fg-ter-light-hover: #7D7C82; - --bit-clr-fg-ter-light-active: #69686D; - --bit-clr-fg-ter-dis: #EBEAF0; - --bit-clr-fg-ter-dis-text: #85848A; - --bit-clr-fg-ter-focus: #706F74; - --bit-clr-fg-dis: #8F8E94; + --bit-clr-fg-pri-light: #2B2E32; + --bit-clr-fg-pri-light-hover: #23262A; + --bit-clr-fg-pri-light-active: #1C1F23; + --bit-clr-fg-pri-dis: #E7ECF0; + --bit-clr-fg-pri-dis-text: #82868A; + --bit-clr-fg-pri-focus: #171A1E; + --bit-clr-fg-sec: #4F5356; + --bit-clr-fg-sec-hover: #424649; + --bit-clr-fg-sec-active: #35383C; + --bit-clr-fg-sec-dark: #3A3E41; + --bit-clr-fg-sec-dark-hover: #2D3034; + --bit-clr-fg-sec-dark-active: #212428; + --bit-clr-fg-sec-light: #686C70; + --bit-clr-fg-sec-light-hover: #5B5F63; + --bit-clr-fg-sec-light-active: #4A4E51; + --bit-clr-fg-sec-dis: #E7ECF0; + --bit-clr-fg-sec-dis-text: #82868A; + --bit-clr-fg-sec-focus: #4F5356; + --bit-clr-fg-ter: #6D7175; + --bit-clr-fg-ter-hover: #606468; + --bit-clr-fg-ter-active: #54585C; + --bit-clr-fg-ter-dark: #595D61; + --bit-clr-fg-ter-dark-hover: #4C5053; + --bit-clr-fg-ter-dark-active: #3F4346; + --bit-clr-fg-ter-light: #878B8F; + --bit-clr-fg-ter-light-hover: #7A7E82; + --bit-clr-fg-ter-light-active: #666A6E; + --bit-clr-fg-ter-dis: #E7ECF0; + --bit-clr-fg-ter-dis-text: #82868A; + --bit-clr-fg-ter-focus: #6D7175; + --bit-clr-fg-dis: #8C9094; --bit-clr-bg-pri: #FFFFFF; - --bit-clr-bg-pri-hover: #F5F4FA; - --bit-clr-bg-pri-active: #EBEAF0; - --bit-clr-bg-pri-dark: #EDECF2; - --bit-clr-bg-pri-dark-hover: #E3E2E8; - --bit-clr-bg-pri-dark-active: #D9D8DE; + --bit-clr-bg-pri-hover: #F1F6FA; + --bit-clr-bg-pri-active: #E7ECF0; + --bit-clr-bg-pri-dark: #E9EEF2; + --bit-clr-bg-pri-dark-hover: #DFE4E8; + --bit-clr-bg-pri-dark-active: #D5DADE; --bit-clr-bg-pri-light: #FFFFFF; - --bit-clr-bg-pri-light-hover: #F5F4FA; - --bit-clr-bg-pri-light-active: #EBEAF0; - --bit-clr-bg-pri-dis: #F7F6FC; - --bit-clr-bg-pri-dis-text: #8C8B91; + --bit-clr-bg-pri-light-hover: #F1F6FA; + --bit-clr-bg-pri-light-active: #E7ECF0; + --bit-clr-bg-pri-dis: #F3F8FC; + --bit-clr-bg-pri-dis-text: #898D91; --bit-clr-bg-pri-focus: #FFFFFF; - --bit-clr-bg-sec: #F5F4FA; - --bit-clr-bg-sec-hover: #EDECF2; - --bit-clr-bg-sec-active: #E6E5EB; - --bit-clr-bg-sec-dark: #E8E7ED; - --bit-clr-bg-sec-dark-hover: #E0DFE5; - --bit-clr-bg-sec-dark-active: #D9D8DE; - --bit-clr-bg-sec-light: #FCFCFF; - --bit-clr-bg-sec-light-hover: #F7F6FC; - --bit-clr-bg-sec-light-active: #F0EFF5; - --bit-clr-bg-sec-dis: #EDECF2; - --bit-clr-bg-sec-dis-text: #87868C; - --bit-clr-bg-sec-focus: #F5F4FA; - --bit-clr-bg-ter: #EBEAF0; - --bit-clr-bg-ter-hover: #E3E2E8; - --bit-clr-bg-ter-active: #DBDAE0; - --bit-clr-bg-ter-dark: #DEDDE3; - --bit-clr-bg-ter-dark-hover: #D6D5DB; - --bit-clr-bg-ter-dark-active: #CFCED4; - --bit-clr-bg-ter-light: #F5F4FA; - --bit-clr-bg-ter-light-hover: #F0EFF5; - --bit-clr-bg-ter-light-active: #E6E5EB; - --bit-clr-bg-ter-dis: #E3E2E8; - --bit-clr-bg-ter-dis-text: #807F85; - --bit-clr-bg-ter-focus: #EBEAF0; - --bit-clr-bg-dis: #F0EFF5; + --bit-clr-bg-sec: #F1F6FA; + --bit-clr-bg-sec-hover: #E9EEF2; + --bit-clr-bg-sec-active: #E2E7EB; + --bit-clr-bg-sec-dark: #E4E9ED; + --bit-clr-bg-sec-dark-hover: #DCE1E5; + --bit-clr-bg-sec-dark-active: #D5DADE; + --bit-clr-bg-sec-light: #FAFCFF; + --bit-clr-bg-sec-light-hover: #F3F8FC; + --bit-clr-bg-sec-light-active: #ECF1F5; + --bit-clr-bg-sec-dis: #E9EEF2; + --bit-clr-bg-sec-dis-text: #84888C; + --bit-clr-bg-sec-focus: #F1F6FA; + --bit-clr-bg-ter: #E7ECF0; + --bit-clr-bg-ter-hover: #DFE4E8; + --bit-clr-bg-ter-active: #D7DCE0; + --bit-clr-bg-ter-dark: #DADFE3; + --bit-clr-bg-ter-dark-hover: #D2D7DB; + --bit-clr-bg-ter-dark-active: #CBD0D4; + --bit-clr-bg-ter-light: #F1F6FA; + --bit-clr-bg-ter-light-hover: #ECF1F5; + --bit-clr-bg-ter-light-active: #E2E7EB; + --bit-clr-bg-ter-dis: #DFE4E8; + --bit-clr-bg-ter-dis-text: #7D8185; + --bit-clr-bg-ter-focus: #E7ECF0; + --bit-clr-bg-dis: #ECF1F5; --bit-clr-bg-overlay: rgba(0, 0, 0, 0.4); - --bit-clr-brd-pri: #706F74; - --bit-clr-brd-pri-hover: #616065; - --bit-clr-brd-pri-active: #525156; - --bit-clr-brd-pri-dark: #59585D; - --bit-clr-brd-pri-dark-hover: #4A494E; - --bit-clr-brd-pri-dark-active: #3B3A3F; - --bit-clr-brd-pri-light: #8C8B91; - --bit-clr-brd-pri-light-hover: #7D7C82; - --bit-clr-brd-pri-light-active: #69686D; - --bit-clr-brd-pri-dis: #EBEAF0; - --bit-clr-brd-pri-dis-text: #85848A; - --bit-clr-brd-pri-focus: #706F74; - --bit-clr-brd-sec: #ADACB2; - --bit-clr-brd-sec-hover: #9E9DA3; - --bit-clr-brd-sec-active: #8F8E94; - --bit-clr-brd-sec-dark: #96959B; - --bit-clr-brd-sec-dark-hover: #87868C; - --bit-clr-brd-sec-dark-active: #78777D; - --bit-clr-brd-sec-light: #C7C6CC; - --bit-clr-brd-sec-light-hover: #B8B7BD; - --bit-clr-brd-sec-light-active: #A6A5AB; - --bit-clr-brd-sec-dis: #EBEAF0; - --bit-clr-brd-sec-dis-text: #85848A; - --bit-clr-brd-sec-focus: #ADACB2; - --bit-clr-brd-ter: #E0DFE5; - --bit-clr-brd-ter-hover: #D1D0D6; - --bit-clr-brd-ter-active: #C2C1C7; - --bit-clr-brd-ter-dark: #C9C8CE; - --bit-clr-brd-ter-dark-hover: #BAB9BF; - --bit-clr-brd-ter-dark-active: #ABAAB0; - --bit-clr-brd-ter-light: #F2F1F7; - --bit-clr-brd-ter-light-hover: #E8E7ED; - --bit-clr-brd-ter-light-active: #D9D8DE; - --bit-clr-brd-ter-dis: #EBEAF0; - --bit-clr-brd-ter-dis-text: #85848A; - --bit-clr-brd-ter-focus: #E0DFE5; - --bit-clr-brd-dis: #E0DFE5; + --bit-clr-brd-pri: #6D7175; + --bit-clr-brd-pri-hover: #5E6266; + --bit-clr-brd-pri-active: #4F5356; + --bit-clr-brd-pri-dark: #565A5E; + --bit-clr-brd-pri-dark-hover: #474B4E; + --bit-clr-brd-pri-dark-active: #383C3F; + --bit-clr-brd-pri-light: #898D91; + --bit-clr-brd-pri-light-hover: #7A7E82; + --bit-clr-brd-pri-light-active: #666A6E; + --bit-clr-brd-pri-dis: #E7ECF0; + --bit-clr-brd-pri-dis-text: #82868A; + --bit-clr-brd-pri-focus: #6D7175; + --bit-clr-brd-sec: #A9AEB2; + --bit-clr-brd-sec-hover: #9A9FA3; + --bit-clr-brd-sec-active: #8C9094; + --bit-clr-brd-sec-dark: #93979B; + --bit-clr-brd-sec-dark-hover: #84888C; + --bit-clr-brd-sec-dark-active: #75797D; + --bit-clr-brd-sec-light: #C3C8CC; + --bit-clr-brd-sec-light-hover: #B4B9BD; + --bit-clr-brd-sec-light-active: #A2A7AB; + --bit-clr-brd-sec-dis: #E7ECF0; + --bit-clr-brd-sec-dis-text: #82868A; + --bit-clr-brd-sec-focus: #A9AEB2; + --bit-clr-brd-ter: #DCE1E5; + --bit-clr-brd-ter-hover: #CDD2D6; + --bit-clr-brd-ter-active: #BEC3C7; + --bit-clr-brd-ter-dark: #C5CACE; + --bit-clr-brd-ter-dark-hover: #B6BBBF; + --bit-clr-brd-ter-dark-active: #A7ACB0; + --bit-clr-brd-ter-light: #EEF3F7; + --bit-clr-brd-ter-light-hover: #E4E9ED; + --bit-clr-brd-ter-light-active: #D5DADE; + --bit-clr-brd-ter-dis: #E7ECF0; + --bit-clr-brd-ter-dis-text: #82868A; + --bit-clr-brd-ter-focus: #DCE1E5; + --bit-clr-brd-dis: #DCE1E5; --bit-clr-req: #A4262C; --bit-clr-ntr-white: #FFFFFF; --bit-clr-ntr-black: #000000; - --bit-clr-ntr-gray10: #F9FAF6; - --bit-clr-ntr-gray20: #F2F3EF; - --bit-clr-ntr-gray30: #EBECE7; - --bit-clr-ntr-gray40: #DFE0DB; - --bit-clr-ntr-gray50: #D0D1CC; - --bit-clr-ntr-gray60: #C6C7C2; - --bit-clr-ntr-gray70: #BCBCB6; - --bit-clr-ntr-gray80: #B1B1AB; - --bit-clr-ntr-gray90: #9FA09B; - --bit-clr-ntr-gray100: #959691; - --bit-clr-ntr-gray110: #888984; - --bit-clr-ntr-gray120: #777873; - --bit-clr-ntr-gray130: #5E5F5A; - --bit-clr-ntr-gray140: #464742; - --bit-clr-ntr-gray150: #3A3B37; - --bit-clr-ntr-gray160: #31322E; - --bit-clr-ntr-gray170: #282925; - --bit-clr-ntr-gray180: #242521; - --bit-clr-ntr-gray190: #1F201C; - --bit-clr-ntr-gray200: #1A1B17; - --bit-clr-ntr-gray210: #151612; - --bit-clr-ntr-gray220: #10110D; + --bit-clr-ntr-gray10: #FCF9F6; + --bit-clr-ntr-gray20: #F5F2EF; + --bit-clr-ntr-gray30: #EFEBE7; + --bit-clr-ntr-gray40: #E3DFDB; + --bit-clr-ntr-gray50: #D4D0CC; + --bit-clr-ntr-gray60: #CAC6C2; + --bit-clr-ntr-gray70: #C0BBB6; + --bit-clr-ntr-gray80: #B5B0AB; + --bit-clr-ntr-gray90: #A39F9B; + --bit-clr-ntr-gray100: #999591; + --bit-clr-ntr-gray110: #8C8884; + --bit-clr-ntr-gray120: #7B7773; + --bit-clr-ntr-gray130: #615E5A; + --bit-clr-ntr-gray140: #494642; + --bit-clr-ntr-gray150: #3C3A37; + --bit-clr-ntr-gray160: #33312E; + --bit-clr-ntr-gray170: #2A2825; + --bit-clr-ntr-gray180: #262421; + --bit-clr-ntr-gray190: #211F1C; + --bit-clr-ntr-gray200: #1C1A17; + --bit-clr-ntr-gray210: #171512; + --bit-clr-ntr-gray220: #12100D; } diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/Dropdown/BitDropdown.scss b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/Dropdown/BitDropdown.scss index 8fbad2c685..2a029e40d0 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/Dropdown/BitDropdown.scss +++ b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/Dropdown/BitDropdown.scss @@ -3,13 +3,11 @@ // The size of the control and of its callout is driven by these variables so a single size class, // applied to the root element and repeated on the callout, resizes every part consistently. -// The trigger text is a line box stretched over the control, so its line-height is derived from the -// control height minus the two borders instead of being a fixed length: a preset that raises the -// control height (Material 40px, Cupertino 36px) then keeps the text centered next to the chevron. +// A preset that raises the control height (Material 40px, Cupertino 36px) then resizes the trigger, +// the callout and the rows inside it together. @mixin drp-size-medium { --bit-drp-fs: #{$tg-fs-sm}; --bit-drp-h: #{$siz-ctrl-md}; - --bit-drp-lh: calc(#{$siz-ctrl-md} - 2 * #{$shp-border-width}); --bit-drp-lbl-lh: #{spacing(2.75)}; --bit-drp-itm-h: #{$siz-item-md}; --bit-drp-itm-fs: #{$tg-fs-sm}; @@ -262,20 +260,33 @@ overflow: hidden; font-weight: $tg-fw-regular; position: relative; - align-self: stretch; + align-self: center; white-space: nowrap; box-sizing: border-box; text-overflow: ellipsis; font-size: var(--bit-drp-fs); - line-height: var(--bit-drp-lh); + line-height: normal; border-radius: $shp-radius-control; padding-inline-start: spacing(1); + + // Centered as a line box of its own rather than as a line box stretched over the control: where + // the line box is stretched, the glyphs sit on the half-leading the font's ascent and descent + // produce, and no two families put the baseline in the same place - Roboto's ascent minus its + // descent falls a pixel short of its cap height at the control font size where Segoe UI's does + // not, so the Material trigger read a pixel high while the Fluent one read centered. Trimming the + // box down to the cap and the baseline takes the metrics out of it and centers the glyphs + // themselves; the symmetric padding is what the overflow clips against, so descenders survive it. + @supports (text-box: trim-both cap alphabetic) { + text-box: trim-both cap alphabetic; + padding-block: 0.35em; + } } .bit-drp-sch { .bit-drp-tcn { height: unset; display: flex; + align-self: stretch; flex-wrap: wrap; gap: spacing(0.5); min-height: var(--bit-drp-h); @@ -1134,7 +1145,6 @@ .bit-drp-sm { --bit-drp-fs: #{$tg-fs-xs}; --bit-drp-h: #{$siz-ctrl-sm}; - --bit-drp-lh: calc(#{$siz-ctrl-sm} - 2 * #{$shp-border-width}); --bit-drp-lbl-lh: #{spacing(2.5)}; --bit-drp-itm-h: #{$siz-item-sm}; --bit-drp-itm-fs: #{$tg-fs-xs}; @@ -1147,7 +1157,6 @@ .bit-drp-lg { --bit-drp-fs: #{$tg-fs-md}; --bit-drp-h: #{$siz-ctrl-lg}; - --bit-drp-lh: calc(#{$siz-ctrl-lg} - 2 * #{$shp-border-width}); --bit-drp-lbl-lh: #{spacing(3)}; --bit-drp-itm-h: #{$siz-item-lg}; --bit-drp-itm-fs: #{$tg-fs-md}; diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/SearchBox/BitSearchBox.razor.cs b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/SearchBox/BitSearchBox.razor.cs index 356bce53d0..e7af294954 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/SearchBox/BitSearchBox.razor.cs +++ b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/SearchBox/BitSearchBox.razor.cs @@ -1419,7 +1419,7 @@ await _js.BitCalloutToggleCallout( scrollOffset: 0, headerId: string.Empty, footerId: string.Empty, - setCalloutWidth: false, + setCalloutWidth: true, fixedCalloutWidth: FixedCalloutWidth, maxWindowWidth: 0); } diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Notifications/Tag/BitTag.scss b/src/BlazorUI/Bit.BlazorUI/Components/Notifications/Tag/BitTag.scss index 6fb01a98e8..6d63dd67f3 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Notifications/Tag/BitTag.scss +++ b/src/BlazorUI/Bit.BlazorUI/Components/Notifications/Tag/BitTag.scss @@ -11,7 +11,7 @@ border-style: $shp-border-style; border-width: $shp-border-width; border-radius: $shp-radius-chip; - padding: var(--bit-tag-padding); + padding: var(--bit-tag-pad-y) var(--bit-tag-pad-x); font-size: var(--bit-tag-fontsize); min-height: var(--bit-tag-minheight); letter-spacing: $tg-ctrl-letter-spacing; @@ -92,20 +92,28 @@ } +// The inset of a chip is not square: the height is set by --bit-tag-minheight, so the block pad only has +// to keep the label off the rule, while the inline pad is what makes the chip read as a chip. It is taken +// from the control padding of the theme rather than from a literal, since the corner is $shp-radius-chip - +// a pill in Cupertino, 8px in Material - and a rounder corner eats into the inline inset. Half the control +// padding is the chip's share of it: a tag is a label, not a button. .bit-tag-sm { --bit-tag-fontsize: #{$tg-fs-xs}; - --bit-tag-padding: #{spacing(0.250)}; + --bit-tag-pad-y: #{spacing(0.250)}; + --bit-tag-pad-x: calc(#{$siz-ctrl-pad-x-sm} / 2); --bit-tag-minheight: #{spacing(2.25)}; } .bit-tag-md { --bit-tag-fontsize: #{$tg-fs-sm}; - --bit-tag-padding: #{spacing(0.375)}; + --bit-tag-pad-y: #{spacing(0.375)}; + --bit-tag-pad-x: calc(#{$siz-ctrl-pad-x-md} / 2); --bit-tag-minheight: #{spacing(2.75)}; } .bit-tag-lg { --bit-tag-fontsize: #{$tg-fs-md}; - --bit-tag-padding: #{spacing(0.500)}; + --bit-tag-pad-y: #{spacing(0.500)}; + --bit-tag-pad-x: calc(#{$siz-ctrl-pad-x-lg} / 2); --bit-tag-minheight: #{spacing(3.5)}; } diff --git a/src/BlazorUI/Bit.BlazorUI/Utils/Theme/BitAccentColorPresets.cs b/src/BlazorUI/Bit.BlazorUI/Utils/Theme/BitAccentColorPresets.cs index 5a5434852b..5762a3726e 100644 --- a/src/BlazorUI/Bit.BlazorUI/Utils/Theme/BitAccentColorPresets.cs +++ b/src/BlazorUI/Bit.BlazorUI/Utils/Theme/BitAccentColorPresets.cs @@ -7,9 +7,10 @@ namespace Bit.BlazorUI; /// public static class BitAccentColorPresets { - // Blue is the packaged Fluent light palette's own primary; the rest are Fluent accent hues. + // Blue is the packaged Fluent and Material palettes' own primary - the accent that changes + // nothing. Purple is Material 3's baseline primary; the rest are Fluent accent hues. public const string Blue = "#1276C6"; - public const string Purple = "#8764B8"; + public const string Purple = "#6750A4"; public const string Green = "#107C10"; public const string Orange = "#CA5010"; public const string Teal = "#038387"; diff --git a/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Server/Components/App.razor b/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Server/Components/App.razor index e51a9643af..d0448518e6 100644 --- a/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Server/Components/App.razor +++ b/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Server/Components/App.razor @@ -52,8 +52,14 @@ @* bit-theme-persist / bit-theme-persist-cookie: the client keeps the chosen theme in localStorage AND in the cookie read above, which is what lets the server render the right theme in the first place. bit-theme-view-transition: theme toggles cross-fade via the View Transitions API (skipped - automatically on unsupported browsers / prefers-reduced-motion). *@ - + automatically on unsupported browsers / prefers-reduced-motion). + bit-theme-light / bit-theme-dark: the site dresses in Fluent 2 by default, so the two names the + OS-following resolution (bit-theme-system, emitted above whenever nothing is persisted) picks + between are the Fluent 2 pair rather than the core light / dark Fluent ones. A visitor who has + already picked a design system in the header is painted from their cookie instead and never + reaches this. *@ + @* First thing in the document: re-resolves bit-theme from localStorage / the preference cookie diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Components/ComponentGallery.razor b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Components/ComponentGallery.razor index f296d81135..e10bba55f4 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Components/ComponentGallery.razor +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Components/ComponentGallery.razor @@ -3,86 +3,64 @@ @* The library, browsable. Two ways in - type what you are after, or pick the category it belongs to - over one grid of cards that says what each component is for. It reads ComponentCatalog, so it can never fall behind the nav the way the hand-written list it replaces did. *@ - + } - - +} diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Components/DemoExample.razor.scss b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Components/DemoExample.razor.scss index b9aa68dadd..cab2e1de20 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Components/DemoExample.razor.scss +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Components/DemoExample.razor.scss @@ -55,7 +55,7 @@ // the page behind it. .example-container { overflow: auto; - // padding: rem2(24px); + padding: 1rem; // background-color: $bit-color-background-primary; @include lt-lg { diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Components/DemoPage.razor.scss b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Components/DemoPage.razor.scss index 7b30a7dd1f..712a2bd4c4 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Components/DemoPage.razor.scss +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Components/DemoPage.razor.scss @@ -19,15 +19,28 @@ align-items: center; margin-bottom: $doc-space-md; padding: $doc-space-2xs $doc-space-sm; - top: calc(var(--demo-header-height) + 0.35rem + #{$bit-env-inset-top}); + + // Both halves of the bar's box come from app.scss rather than from the toggle inside it: the + // pivot bar of a multi-API page pins itself directly under this one, and it can only do that + // if the height it offsets by is the height this bar actually takes. A floor rather than a + // fixed height, so a preset that draws its switch track shorter than the label beside it + // still leaves the row tall enough to read. + top: calc(var(--demo-motion-bar-top) + #{$bit-env-inset-top}); + min-height: var(--demo-motion-bar-height); + + // The label is held to one line for the same reason: that height is derived from the switch + // track rather than measured, so a label wrapping on a narrow screen would make this bar + // taller than the value the pivot bar offsets by - and pin that bar inside this one. + ::deep .bit-tgl-stx { + white-space: nowrap; + } // Drawn as one more surface in the column under it - same corner family, stroked all round - // so it does not read as a stray rule across the page. @include doc-surface; - // background-color: $bit-color-background-secondary; .bit-ios & { - top: calc(var(--demo-header-height) + 0.35rem + env(safe-area-inset-top)); + top: calc(var(--demo-motion-bar-top) + env(safe-area-inset-top)); } } } diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Extensions/IJSRuntimeExtensions.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Extensions/IJSRuntimeExtensions.cs index 95e37243b1..70f8a512a8 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Extensions/IJSRuntimeExtensions.cs +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Extensions/IJSRuntimeExtensions.cs @@ -57,6 +57,21 @@ public static async Task UnregisterWindowResizeListener(this IJSRuntime jsRuntim await jsRuntime.InvokeVoid("unregisterWindowResizeListener", id); } + /// + /// Watches the usable width of the element with the given id and reports it - immediately, then on + /// every change - to the named method. The iconography grid needs it to know how many icons fit on + /// a row, which is what lets it virtualize by row. + /// + public static async Task ObserveElementWidth(this IJSRuntime jsRuntime, string id, DotNetObjectReference dotnetObj, string methodName) where T : class + { + await jsRuntime.InvokeVoid("observeElementWidth", id, dotnetObj, methodName); + } + + public static async Task UnobserveElementWidth(this IJSRuntime jsRuntime, string id) + { + await jsRuntime.InvokeVoid("unobserveElementWidth", id); + } + /// /// Claims Ctrl/Cmd+K (and a bare "/") for the search input inside the element with the given id. /// Registered from JS rather than through a Blazor key handler so the shortcut works no matter diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Iconography/IconographyPage.razor b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Iconography/IconographyPage.razor index b352540975..fb622cf3c9 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Iconography/IconographyPage.razor +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Iconography/IconographyPage.razor @@ -27,39 +27,52 @@ descend from - the page's own root markup is all components. *@
- - - - @if (filteredIcons.Count == 0) - { - No icon matches that name. - } - else - { - foreach (var icon in filteredIcons) + DebounceTime="300" + OnClear="HandleClear" + OnChange="HandleChange" + Placeholder="Search icons" + IconName="@BitIconName.Search" + Class="iconography-search-box" /> + + @* The icon font carries well over two thousand glyphs, which is more markup than a phone + can lay out in one go - so the grid is windowed. BitVirtualize windows a *vertical* + list, so the unit of virtualization is a row of icons rather than an icon: the cells + are chunked into rows of columnCount (see OnIconGridResized, which recomputes it from + the measured width of the scroller), and only the rows in view get rendered. + + The cell is plain markup rather than a BitButton hosting a BitIcon and a BitText, + because at this count the per-component cost is the cost: everything the components + would have contributed here is a handful of rules in the stylesheet. *@ + + +
+ @foreach (var icon in context) + { + + } +
+
+ + @* Empty also means "not measured yet", one frame before the observer first + reports a width - and that frame must not claim the search found nothing. *@ + @if (filteredIcons.Count == 0) { - - - - - @icon.Value - - - + No icon matches that name. } - } -
-
+ +
diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Iconography/IconographyPage.razor.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Iconography/IconographyPage.razor.cs index 167e9d237a..74987e3fa4 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Iconography/IconographyPage.razor.cs +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Iconography/IconographyPage.razor.cs @@ -5,7 +5,7 @@ namespace Bit.BlazorUI.Demo.Client.Core.Pages.Iconography; -public partial class IconographyPage +public partial class IconographyPage : IAsyncDisposable { private sealed record IconInfo(string FieldName, string Value) { @@ -24,6 +24,17 @@ private sealed record IconInfo(string FieldName, string Value) private const string COPY_KEY_NAME = "name"; + private const string GRID_ELEMENT_ID = "iconography-grid"; + + // The geometry of one cell of the icon grid, in px, duplicating what the stylesheet lays out - + // and it has to stay a duplicate. The virtualizer places its rows at absolute pixel offsets, so + // the row height it is handed must be exactly the height a row occupies, and the column count + // derived from these must be exactly how many cells a row fits, or the rows drift apart or + // overlap. The matching $icon-cell-* values live in IconographyPage.razor.scss. + private const int ICON_CELL_WIDTH = 88; + private const int ICON_CELL_GAP = 8; + private const int ICON_ROW_HEIGHT = 76; + private static readonly (string Label, BitColor Value)[] previewColors = [ ("Primary", BitColor.Primary), @@ -37,10 +48,13 @@ private static readonly (string Label, BitColor Value)[] previewColors = private List allIcons = default!; private List filteredIcons = default!; + private List iconRows = []; + private int columnCount; private IconInfo? selectedIcon; private bool isIconPanelOpen; private Dictionary? iconGlyphs; private string? copyFeedbackKey; + private DotNetObjectReference? dotnetObj; @@ -66,12 +80,16 @@ protected override void OnInitialized() /// /// The install snippets are constant, so one Prism pass on the first render is all they need. /// The panel's own snippets are highlighted when it opens (see ). + /// The grid starts being watched here too - it only exists in the DOM once it has rendered. /// protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { await _js.InvokeVoid("highlightSnippet"); + + dotnetObj = DotNetObjectReference.Create(this); + await _js.ObserveElementWidth(GRID_ELEMENT_ID, dotnetObj, nameof(OnIconGridResized)); } await base.OnAfterRenderAsync(firstRender); @@ -79,19 +97,66 @@ protected override async Task OnAfterRenderAsync(bool firstRender) + /// + /// The grid changed width, so the number of icons that fit across it may have changed. Rebuilding + /// the rows is only worth a render when it actually did - a scrollbar appearing, or any resize + /// smaller than a whole column, leaves the layout exactly as it was. + /// + [JSInvokable] + public Task OnIconGridResized(double width) + { + if (width <= 0) return Task.CompletedTask; + + var columns = Math.Max(1, (int)((width + ICON_CELL_GAP) / (ICON_CELL_WIDTH + ICON_CELL_GAP))); + + if (columns == columnCount) return Task.CompletedTask; + + columnCount = columns; + BuildIconRows(); + + return InvokeAsync(StateHasChanged); + } + + + private void HandleClear() { filteredIcons = allIcons; + BuildIconRows(); } private void HandleChange(string text) { - HandleClear(); - if (string.IsNullOrEmpty(text)) return; + filteredIcons = string.IsNullOrEmpty(text) + ? allIcons + : allIcons.FindAll(icon => + icon.Value.Contains(text, StringComparison.InvariantCultureIgnoreCase) || + icon.FieldName.Contains(text, StringComparison.InvariantCultureIgnoreCase)); - filteredIcons = allIcons.FindAll(icon => - icon.Value.Contains(text, StringComparison.InvariantCultureIgnoreCase) || - icon.FieldName.Contains(text, StringComparison.InvariantCultureIgnoreCase)); + BuildIconRows(); + } + + /// + /// Chunks the icons currently on show into the fixed-width rows the virtualizer renders one at a + /// time. Nothing to chunk until the grid has been measured, which is the columnCount 0 case; the + /// empty list it leaves behind is what the EmptyTemplate reads as "not measured yet". + /// + private void BuildIconRows() + { + if (columnCount == 0) + { + iconRows = []; + return; + } + + var rows = new List((filteredIcons.Count + columnCount - 1) / columnCount); + + for (var i = 0; i < filteredIcons.Count; i += columnCount) + { + rows.Add([.. filteredIcons.GetRange(i, Math.Min(columnCount, filteredIcons.Count - i))]); + } + + iconRows = rows; } private async Task OpenIconPanel(IconInfo icon) @@ -174,4 +239,20 @@ private async Task CopyToClipboard(string text, string feedbackKey) StateHasChanged(); } } + + + + public async ValueTask DisposeAsync() + { + try + { + await _js.UnobserveElementWidth(GRID_ELEMENT_ID); + } + catch (JSDisconnectedException) { } // the circuit is already gone, nothing left to unobserve + + dotnetObj?.Dispose(); + dotnetObj = null; + + GC.SuppressFinalize(this); + } } diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Iconography/IconographyPage.razor.scss b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Iconography/IconographyPage.razor.scss index f9dda0eefe..90698159c2 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Iconography/IconographyPage.razor.scss +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Iconography/IconographyPage.razor.scss @@ -1,5 +1,15 @@ @import '../../Styles/abstracts/_docs.scss'; +// The height of one cell of the grid and the gap that follows it. In px, and duplicated by the +// ICON_CELL_* constants in the code-behind, because the grid is virtualized by row: the virtualizer +// places rows at absolute pixel offsets, so the row height it is handed - cell plus gap - has to be +// exactly the height a row occupies here. rem would let a text-zoom move one side of that agreement +// and not the other. How WIDE a cell is is not settled here: the code-behind divides the measured +// width by a minimum cell width to get a column count, and the row shares its width out between +// that many equal columns. +$icon-cell-height: 68px; +$icon-cell-gap: 8px; + .icon-browser { display: flex; gap: $doc-space-sm; @@ -9,30 +19,64 @@ max-width: rem2(320px); } - // A fixed-height scroller rather than a thousand icons in the page flow: the grid is the one - // place on this site where the number of nodes, not the layout, is what costs. + // A fixed-height scroller, and the viewport the virtualizer windows against: the grid is the + // one place on this site where the number of nodes, not the layout, is what costs. ::deep .icon-scroller { + height: rem2(480px); padding: $doc-space-sm; @include doc-surface; + @include doc-scrollbar; + } + + // One virtualized item. A grid of exactly the column count the code-behind measured, rather + // than a row of fixed-width cells, so the last row stays aligned with the ones above it and no + // slack is left over on the right - which on a phone, three columns wide, is most of a cell. + // Its height is the cell plus the gap that follows it, so the trailing gap sits inside the row + // rather than between two absolutely positioned rows, where it would have nothing to push against. + ::deep .icon-row { + display: grid; + gap: $icon-cell-gap; + padding-bottom: $icon-cell-gap; + grid-template-columns: repeat(var(--icon-cols, 1), minmax(0, 1fr)); } // The label is revealed on hover and focus rather than always shown: at rest the grid should - // read as a sheet of glyphs, which is how an icon is actually recognised. + // read as a sheet of glyphs, which is how an icon is actually recognised. Its box is reserved + // either way, so revealing it cannot move anything. ::deep .icon-box { - margin: rem2(1px); - height: auto; + gap: rem2(6px); + border: none; + display: flex; cursor: pointer; - min-height: unset; - width: rem2(88px); - padding: $doc-space-2xs; + color: inherit; + overflow: hidden; + text-align: center; + align-items: center; + font-family: inherit; + justify-content: center; + flex-flow: column nowrap; + background-color: transparent; + height: $icon-cell-height; + padding: rem2(8px) rem2(4px); border-radius: $bit-shape-radius-surface; transition: background-color $bit-motion-duration-short $bit-motion-easing; + > i { + flex: none; + font-size: rem2(20px); + color: $bit-color-foreground-secondary; + } + .icon-name { opacity: 0; - text-align: center; + display: block; + overflow: hidden; + max-height: rem2(26px); + line-height: rem2(13px); overflow-wrap: anywhere; + font-size: $bit-typography-caption2-font-size; + font-weight: $bit-typography-caption2-font-weight; transition: opacity $bit-motion-duration-short $bit-motion-easing; } @@ -40,7 +84,6 @@ &:focus-visible, &:active, &.selected { - color: inherit; background-color: $bit-color-background-secondary; .icon-name { @@ -48,6 +91,13 @@ } } + // Inset rather than the docs' offset ring: the cells sit a gap apart, so an outward ring + // would draw over its neighbours. + &:focus-visible { + outline: rem2(2px) solid $bit-color-primary; + outline-offset: rem2(-2px); + } + &.selected { outline: rem2(1px) solid $bit-color-primary; } diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/OverviewPage.razor b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/OverviewPage.razor index fef1ae5525..871d9e1dde 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/OverviewPage.razor +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/OverviewPage.razor @@ -118,6 +118,8 @@ <BitDatePicker Label="Select a date" /> +
+ The full getting started guide - optional packages, cache busting, MCP diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Theming/ThemingPage.razor b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Theming/ThemingPage.razor index 984d6ef209..7d59f9c7ce 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Theming/ThemingPage.razor +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Theming/ThemingPage.razor @@ -1,4 +1,4 @@ -@page "/theming" +@page "/theming" @inherits AppComponentBase @code { @@ -592,7 +592,7 @@ await _bitThemeManager.SetThemeAsync(corporate); // Sample accents (hex) BitAccentColorPresets.Blue; // #1276C6 - the packaged palette's own primary -BitAccentColorPresets.Purple; // #8764B8 +BitAccentColorPresets.Purple; // #6750A4 - Material 3's baseline primary BitAccentColorPresets.Green; // #107C10 BitAccentColorPresets.Orange; // #CA5010 BitAccentColorPresets.Teal; // #038387 diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Scripts/app.ts b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Scripts/app.ts index d638cf8f6a..bb185d56d6 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Scripts/app.ts +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Scripts/app.ts @@ -263,6 +263,39 @@ function unregisterWindowResizeListener(id: string) { delete windowResizeListeners[id]; } +const elementWidthObservers: { [key: string]: ResizeObserver } = {}; + +// Reports the usable (padding-excluded) width of an element to .NET whenever it changes. The +// iconography grid virtualizes by row, so it has to know how many cells fit across before it can +// chunk the icons - and a window resize listener would not see the width change when it is the +// side rail collapsing or a scrollbar appearing rather than the window that moved. +function observeElementWidth(id: string, dotnetObj: any, methodName: string) { + unobserveElementWidth(id); + + const element = document.getElementById(id); + if (element == null) return; + + const report = () => { + const style = getComputedStyle(element); + const width = element.clientWidth - parseFloat(style.paddingInlineStart) - parseFloat(style.paddingInlineEnd); + dotnetObj.invokeMethodAsync(methodName, Math.max(0, width)); + }; + + const observer = new ResizeObserver(report); + observer.observe(element); + elementWidthObservers[id] = observer; + + report(); +} + +function unobserveElementWidth(id: string) { + const observer = elementWidthObservers[id]; + if (observer == null) return; + + observer.disconnect(); + delete elementWidthObservers[id]; +} + declare namespace BitBlazorUI { class Theme { static init(options: any): void; } } diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Shared/AppHeader.razor b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Shared/AppHeader.razor index b541edd5d6..7fcb886893 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Shared/AppHeader.razor +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Shared/AppHeader.razor @@ -60,19 +60,21 @@ } + @* The design system picker and the light/dark toggle are both this component's; all that is + left here is the three things it cannot know about - the design systems this site offers + (see _designSystems), the theme to show before JS interop is reachable (see OnInitAsync) + and the native shell that follows the applied scheme. *@ + + - - @* The design system picker and the light/dark toggle are both this component's; all that is - left here is the two things it cannot know about - the theme to show before JS interop is - reachable (see OnInitAsync) and the native shell that follows the applied scheme. *@ - diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Shared/AppHeader.razor.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Shared/AppHeader.razor.cs index a811b74ed3..de94dd74b3 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Shared/AppHeader.razor.cs +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Shared/AppHeader.razor.cs @@ -10,6 +10,31 @@ public partial class AppHeader ///
private string? _initialTheme; + /// + /// The design systems the picker offers: the library's four, in the library's own order, differing from + /// only in how Fluent's two schemes are spelled. + /// + /// + /// The default list names Fluent's pair light / dark, which is the pair the switcher hands + /// to BitThemeManager.ToggleDarkLightAsync rather than setting outright, so that a host page stays in + /// charge of what those two names mean. This host points them at Fluent 2 (see App.razor), which is what + /// makes Fluent 2 the site's default - so left as they are, toggling the scheme while on Fluent would + /// switch design system. fluent-light / fluent-dark name the same two palettes outright, + /// leaving nothing to defer to. + /// + /// Fluent stays first, and is therefore still the fallback for a theme none of the items claims: after + /// the respelling those are exactly the bare light / dark names, which a visitor from + /// before the Fluent 2 default may still have persisted - and they are core Fluent. + /// + /// + private static readonly BitThemeSwitcherItem[] _designSystems = + [ + new() { Text = "Fluent", Value = BitThemePresets.Fluent, LightTheme = BitThemePresets.FluentLight, DarkTheme = BitThemePresets.FluentDark }, + new() { Text = "Fluent 2", Value = BitExtraThemePresets.Fluent2, LightTheme = BitExtraThemePresets.Fluent2Light, DarkTheme = BitExtraThemePresets.Fluent2Dark }, + new() { Text = "Material", Value = BitExtraThemePresets.Material, LightTheme = BitExtraThemePresets.MaterialLight, DarkTheme = BitExtraThemePresets.MaterialDark }, + new() { Text = "Cupertino", Value = BitExtraThemePresets.Cupertino, LightTheme = BitExtraThemePresets.CupertinoLight, DarkTheme = BitExtraThemePresets.CupertinoDark }, + ]; + /// /// The current path, lower-cased and without the base URI, kept for the active-link check. /// @@ -51,8 +76,19 @@ protected override async Task OnInitAsync() // Prerendering has no JS runtime to ask, so the server reads the persisted theme from its // cookie and cascades it here; the value is then persisted into the prerendered state so the - // interactive client comes up with the same selection instead of flashing "Fluent" first. - _initialTheme = await PrerenderStateService.GetValue("AppHeader.Theme", () => Task.FromResult(PersistedTheme)); + // interactive client comes up with the same selection instead of flashing another one first. + // Nothing persisted (a first visit, or a visitor following the OS) means the site default, + // which is Fluent 2 - without it the picker would show the first offered item until hydration. + // Resolved through the same helper the host page splats onto (see App.razor) rather + // than compared as a raw string: it trims, lower-cases and validates the token, so a cookie + // reading " System " or a tampered value lands on whatever the document is painting instead + // of leaving the picker on a second, different selection. No bit-theme attribute comes back + // exactly when the OS is being followed or the stored value was unusable. + var persisted = await PrerenderStateService.GetValue("AppHeader.Theme", () => Task.FromResult(PersistedTheme)); + var rootTheme = BitThemeSsr.BuildRootThemeAttributeMap(persisted); + _initialTheme = rootTheme.TryGetValue(BitThemeAttributeNames.Theme, out var theme) && theme is string themeName + ? themeName + : BitExtraThemePresets.Fluent2; } private void OnLocationChanged(object? sender, LocationChangedEventArgs args) diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Styles/abstracts/_bit-css-variables.scss b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Styles/abstracts/_bit-css-variables.scss index 9d4bf9c5e4..41c1e0fd54 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Styles/abstracts/_bit-css-variables.scss +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Styles/abstracts/_bit-css-variables.scss @@ -356,6 +356,8 @@ $bit-size-control-lg: var(--bit-siz-ctrl-lg); $bit-size-icon-sm: var(--bit-siz-icon-sm); $bit-size-icon-md: var(--bit-siz-icon-md); $bit-size-icon-lg: var(--bit-siz-icon-lg); +$bit-size-tab: var(--bit-siz-tab); +$bit-size-switch-h-md: var(--bit-siz-switch-h-md); $bit-size-selection-sm: var(--bit-siz-sel-sm); $bit-size-selection-md: var(--bit-siz-sel-md); $bit-size-selection-lg: var(--bit-siz-sel-lg); diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Styles/app.scss b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Styles/app.scss index 462b721717..2c0c0cad0c 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Styles/app.scss +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Styles/app.scss @@ -29,14 +29,19 @@ // examples still use the full column. --demo-header-height: #{$doc-header-height}; - // The height of DemoPage's reduced-motion toggle bar. Two unrelated stylesheets need to agree - // on it: the bar sizes itself from this value, and every other sticky element on a demo page - // offsets its own top by it so they stack instead of overlapping. It stays 0 while the bar is - // hidden, which is every state except reduced motion. + // Where DemoPage's reduced-motion toggle bar pins itself, and how tall it is. Two unrelated + // stylesheets need to agree on both: the bar sizes and places itself from these values, and + // every other sticky element on a demo page stacks under them instead of overlapping. The + // height stays 0 while the bar is hidden, which is every state except reduced motion. + --demo-motion-bar-top: calc(var(--demo-header-height) + #{rem2(2px)}); --demo-motion-bar-height: 0rem; + // Where the multi-API pages' sticky pivot bar pins itself: directly under the motion bar, with + // a hairline of air between them. Derived rather than written out, because the motion bar's + // height is a theme decision (see the reduced-motion block below). + --demo-pivot-sticky-top: calc(var(--demo-motion-bar-top) + var(--demo-motion-bar-height) + #{rem2(1px)}); // How far an anchored heading has to stop short of the sticky chrome above it. The default // clears the app header; a page that pins a second bar (the multi-API pivot) raises it. - --demo-section-scroll-margin: #{rem2(90px)}; + --demo-section-scroll-margin: #{rem2(120px)}; } // The site ignores prefers-reduced-motion everywhere except the component demo pages. @@ -62,8 +67,16 @@ --bit-mot-easing-spinner: var(--bit-mot-easing-spinner-full, cubic-bezier(0.53, 0.21, 0.29, 0.67)); --bit-mot-loop-factor: var(--bit-mot-loop-factor-full, 1); - // The bar's own height, which the sticky pivot bar under it offsets itself by. - --demo-motion-bar-height: #{rem2(32px)}; + // The bar's own height, which the sticky pivot bar under it offsets itself by. It holds a + // single BitToggle on a doc surface, so it is that toggle's switch track plus the surface's + // vertical padding and its stroke on both edges. Derived rather than written out as one + // number, because the track is a design-system decision - Material draws it 32px and + // Cupertino 31px where Fluent draws it 20px - and a literal tuned to Fluent leaves the pivot + // bar pinned *inside* this one on the other two presets. The floor keeps the row tall enough + // for the label beside a switch a preset draws shorter than its own text - and stands in for + // the track entirely under a custom theme that ships no --bit-siz-switch-h-md, which would + // otherwise leave the max() invalid at computed-value time and this whole variable unusable. + --demo-motion-bar-height: calc(max(var(--bit-siz-switch-h-md, #{rem2(24px)}), #{rem2(24px)}) + #{$doc-space-2xs} * 2 + #{rem2(2px)}); } // A component demo page with its ForceAnimation toggle off: exactly the library's own reduced @@ -273,6 +286,13 @@ html, body, .site-content, pre, .table-view, .side-rail { margin-top: $doc-space-sm; } + // The rule above stops at an accordion, and what a section folds away is the same mix of prose + // and code boxes as what it leaves open, so the accordion's body takes the same rhythm rather + // than the
tags that rule was written to make unnecessary. + .bit-acd-con > * + * { + margin-top: $doc-space-sm; + } + > p { line-height: 1.65; } @@ -442,19 +462,30 @@ code { position: sticky; padding: 0.25rem 0.5rem; box-shadow: $bit-box-shadow-callout; - background-color: $bit-color-background-secondary; - top: calc(rem2(75px) + #{$bit-env-inset-top} + var(--demo-motion-bar-height)); + background-color: $bit-color-background-tertiary; + top: calc(var(--demo-pivot-sticky-top) + #{$bit-env-inset-top}); .bit-ios & { - top: calc(rem2(75px) + env(safe-area-inset-top) + var(--demo-motion-bar-height)); + top: calc(var(--demo-pivot-sticky-top) + env(safe-area-inset-top)); } } // A page hosting this bar has one more sticky layer over its examples, so DemoExample's anchored -// section titles need more clearance than the default. Published as a variable on the page rather -// than a selector into the pivot's markup, so it holds for any pivot that adopts the class. +// section titles need more clearance than the default: everything the bar pins itself past, plus +// the bar itself - a tab row and the padding around it - and a line of air under it. Published as a +// variable on the page rather than a selector into the pivot's markup, so it holds for any pivot +// that adopts the class. +// +// The first term is the bar's own `top` verbatim, safe-area inset included, so the clearance tracks +// where the bar actually lands rather than where it would land on a device with no notch; the +// default's 90px stays the floor, since with the motion bar hidden the derived stack can come out +// under it on a short header. .doc-main:has(.pivot-sticky-header) { - --demo-section-scroll-margin: 11rem; + --demo-section-scroll-margin: calc(max(#{rem2(120px)}, var(--demo-pivot-sticky-top) + #{$bit-env-inset-top}) + #{$bit-size-tab} + #{rem2(8px)} + #{$doc-space-2xs}); + + .bit-ios & { + --demo-section-scroll-margin: calc(max(#{rem2(120px)}, var(--demo-pivot-sticky-top) + env(safe-area-inset-top)) + #{$bit-size-tab} + #{rem2(8px)} + #{$doc-space-2xs}); + } } // The Color sections of the Slider, Toggle, NavBar and Pivot demos put the *Background roles on a diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Maui/wwwroot/index.html b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Maui/wwwroot/index.html index c2c6feb1c3..78872b7684 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Maui/wwwroot/index.html +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Maui/wwwroot/index.html @@ -1,7 +1,9 @@ - + (skipped automatically on unsupported WebViews / prefers-reduced-motion). + bit-theme: the app dresses in Fluent 2 by default, same as the web host, and opens on its dark + scheme; the header's theme switcher takes it from there. --> + diff --git a/src/BlazorUI/Tests/Bit.BlazorUI.Tests/Components/Extras/AccentColorSwitcher/BitAccentColorHeadTests.cs b/src/BlazorUI/Tests/Bit.BlazorUI.Tests/Components/Extras/AccentColorSwitcher/BitAccentColorHeadTests.cs index 309f0d82e1..8b95ffdbe0 100644 --- a/src/BlazorUI/Tests/Bit.BlazorUI.Tests/Components/Extras/AccentColorSwitcher/BitAccentColorHeadTests.cs +++ b/src/BlazorUI/Tests/Bit.BlazorUI.Tests/Components/Extras/AccentColorSwitcher/BitAccentColorHeadTests.cs @@ -8,6 +8,11 @@ namespace Bit.BlazorUI.Tests.Components.Extras.AccentColorSwitcher; [TestClass] public class BitAccentColorHeadTests : BunitTestContext { + // The second of the DefaultAccents, standing in here for "a configured accent that is not the + // neutral one". Read off the preset rather than spelled out, so a retune of the preset cannot + // leave these tests asserting on a hex nothing offers any more. + private static readonly string PurpleToken = BitAccentColorPresets.Purple.TrimStart('#').ToLowerInvariant(); + [TestMethod] public void BitAccentColorHeadShouldEmitNothingByDefault() { @@ -64,7 +69,7 @@ public void BitAccentColorHeadInStaticCssModeShouldEmitTheInlineScriptAndInlineT StringAssert.Contains(component.Markup, BitAccentColorSsr.InlineHeadScriptBody, StringComparison.Ordinal, "The inline script is what personalizes a cached response pre-paint; without it the component solves nothing."); - StringAssert.Contains(component.Find("style").TextContent, $"[{BitAccentColorNames.Attribute}=\"8764b8\"]", StringComparison.Ordinal, + StringAssert.Contains(component.Find("style").TextContent, $"[{BitAccentColorNames.Attribute}=\"{PurpleToken}\"]", StringComparison.Ordinal, "With no StylesheetHref, the all-accents stylesheet must be inlined so no endpoint is required."); Assert.AreEqual(0, component.FindAll("link").Count, "Inlined and linked stylesheets are alternatives, not companions."); @@ -137,7 +142,7 @@ public void BitAccentColorHeadShouldEmitTheNonceOnThePrerenderStyle() { FirstPaintStrategy = BitAccentColorFirstPaintStrategy.StoredCss, }); - parameters.Add(p => p.PersistedAccent, "8764b8"); + parameters.Add(p => p.PersistedAccent, PurpleToken); parameters.Add(p => p.Nonce, "abc123"); }); @@ -168,7 +173,7 @@ public void BitAccentColorHeadInStoredCssModeShouldEmitThePersistedAccentsPreren { FirstPaintStrategy = BitAccentColorFirstPaintStrategy.StoredCss, }); - parameters.Add(p => p.PersistedAccent, "8764b8"); + parameters.Add(p => p.PersistedAccent, PurpleToken); }); var style = component.Find($"style[id=\"{BitAccentColorNames.StyleElementId}\"]"); @@ -188,11 +193,11 @@ public void BitAccentColorHeadInStoredCssModeShouldGuardThePrerenderStyleAgainst FirstPaintStrategy = BitAccentColorFirstPaintStrategy.StoredCss, Persistence = BitAccentColorPersistence.All, }); - parameters.Add(p => p.PersistedAccent, "#8764B8"); + parameters.Add(p => p.PersistedAccent, BitAccentColorPresets.Purple); }); var style = component.Find($"style[id=\"{BitAccentColorNames.StyleElementId}\"]"); - Assert.AreEqual("8764b8", style.GetAttribute(BitAccentColorNames.StyleAccentAttribute), + Assert.AreEqual(PurpleToken, style.GetAttribute(BitAccentColorNames.StyleAccentAttribute), "The per-request style has to say which accent it was built for, or the guard cannot tell it from the visitor's own snapshot."); StringAssert.Contains(component.Markup, BitAccentColorSsr.PrerenderCssGuardScript, StringComparison.Ordinal, @@ -211,7 +216,7 @@ public void BitAccentColorHeadInStoredCssModeShouldNotGuardThePrerenderStyleWith { FirstPaintStrategy = BitAccentColorFirstPaintStrategy.StoredCss, }); - parameters.Add(p => p.PersistedAccent, "8764b8"); + parameters.Add(p => p.PersistedAccent, PurpleToken); }); Assert.IsNotNull(component.Find($"style[id=\"{BitAccentColorNames.StyleElementId}\"]")); @@ -277,7 +282,7 @@ public void BitAccentColorHeadShouldMarkTheSwatchesOfItsOwnAccentList() var marker = component.FindAll("style").Last().TextContent; StringAssert.Contains(marker, $"[{BitAccentColorNames.SwatchAttribute}=\"dc143c\"]", StringComparison.Ordinal); - Assert.IsFalse(marker.Contains("8764b8", StringComparison.Ordinal), + Assert.IsFalse(marker.Contains(PurpleToken, StringComparison.Ordinal), "A custom accent list replaces the defaults here as everywhere else."); } @@ -295,7 +300,7 @@ public void BitAccentColorHeadShouldHonorACustomAccentList() var css = component.Find("style").TextContent; StringAssert.Contains(css, $"[{BitAccentColorNames.Attribute}=\"dc143c\"]", StringComparison.Ordinal); - Assert.IsFalse(css.Contains("8764b8", StringComparison.Ordinal), + Assert.IsFalse(css.Contains(PurpleToken, StringComparison.Ordinal), "A custom accent list replaces the defaults; the default palettes must not leak in."); } } diff --git a/src/BlazorUI/Tests/Bit.BlazorUI.Tests/Components/Extras/AccentColorSwitcher/BitAccentColorSsrTests.cs b/src/BlazorUI/Tests/Bit.BlazorUI.Tests/Components/Extras/AccentColorSwitcher/BitAccentColorSsrTests.cs index ed00241e6c..813d44fcc1 100644 --- a/src/BlazorUI/Tests/Bit.BlazorUI.Tests/Components/Extras/AccentColorSwitcher/BitAccentColorSsrTests.cs +++ b/src/BlazorUI/Tests/Bit.BlazorUI.Tests/Components/Extras/AccentColorSwitcher/BitAccentColorSsrTests.cs @@ -7,6 +7,11 @@ namespace Bit.BlazorUI.Tests.Components.Extras.AccentColorSwitcher; [TestClass] public sealed class BitAccentColorSsrTests { + // The second of the DefaultAccents, standing in here for "a configured accent that is not the + // neutral one". Read off the preset rather than spelled out, so a retune of the preset cannot + // leave these tests asserting on a hex nothing offers any more. + private static readonly string PurpleToken = BitAccentColorPresets.Purple.TrimStart('#').ToLowerInvariant(); + [TestMethod] public void InlineHeadScriptWrapsTheBodyInAScriptElement() { @@ -126,7 +131,7 @@ public void BuildStaticCssHonorsACustomAccentList() var css = BitAccentColorSsr.BuildStaticCss([new BitAccentColorItem { Name = "Crimson", Color = "#DC143C" }]); StringAssert.Contains(css, $"[{BitAccentColorNames.Attribute}=\"dc143c\"]", StringComparison.Ordinal); - Assert.IsFalse(css.Contains("8764b8", StringComparison.Ordinal), + Assert.IsFalse(css.Contains(PurpleToken, StringComparison.Ordinal), "A custom accent list replaces the defaults; the default palettes must not leak in."); } diff --git a/src/BlazorUI/Tests/Bit.BlazorUI.Tests/Components/Extras/AccentColorSwitcher/BitAccentColorSwitcherTests.cs b/src/BlazorUI/Tests/Bit.BlazorUI.Tests/Components/Extras/AccentColorSwitcher/BitAccentColorSwitcherTests.cs index e01841468b..961979acc2 100644 --- a/src/BlazorUI/Tests/Bit.BlazorUI.Tests/Components/Extras/AccentColorSwitcher/BitAccentColorSwitcherTests.cs +++ b/src/BlazorUI/Tests/Bit.BlazorUI.Tests/Components/Extras/AccentColorSwitcher/BitAccentColorSwitcherTests.cs @@ -8,6 +8,11 @@ namespace Bit.BlazorUI.Tests.Components.Extras.AccentColorSwitcher; [TestClass] public class BitAccentColorSwitcherTests : BunitTestContext { + // The second of the DefaultAccents, standing in here for "a configured accent that is not the + // neutral one". Read off the preset rather than spelled out, so a retune of the preset cannot + // leave these tests asserting on a hex nothing offers any more. + private static readonly string PurpleToken = BitAccentColorPresets.Purple.TrimStart('#').ToLowerInvariant(); + private void RegisterServices(System.Action? accentColor = null) { // The switcher resolves the scoped BitAccentColorService, which itself needs the core @@ -214,7 +219,7 @@ public void BitAccentColorSwitcherShouldApplyWithTheConfiguredStrategyAndPersist component.FindAll(".bit-acs-swt").First(s => s.GetAttribute("title") == "Purple").Click(); var apply = Context.JSInterop.Invocations.Single(i => i.Identifier == "BitBlazorUI.AccentColor.apply"); - Assert.AreEqual("8764b8", apply.Arguments[0]); + Assert.AreEqual(PurpleToken, apply.Arguments[0]); Assert.IsNull(apply.Arguments[1], "The default None strategy keeps no palette snapshot."); Assert.AreEqual(false, apply.Arguments[3], "The default None strategy sets no bit-accent attribute."); Assert.AreEqual((int)BitAccentColorPersistence.LocalStorage, apply.Arguments[4],