@timmo001/groundwork-ui 0.9.19 → 0.9.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3145,7 +3145,7 @@
3145
3145
  "text": "readonly CSSStyleSheet[]"
3146
3146
  },
3147
3147
  "static": true,
3148
- "default": "[ base, controlSizes(\"button\"), css` :host { --gw-button-tone: var(--gw-color-accent); display: inline-block; } :host([variant=\"neutral\"]) { --gw-button-tone: var(--gw-color-text-secondary); --gw-button-ink: var(--gw-color-text-strong); } :host([variant=\"info\"]) { --gw-button-tone: var(--gw-color-info); } :host([variant=\"success\"]) { --gw-button-tone: var(--gw-color-success); } :host([variant=\"warning\"]) { --gw-button-tone: var(--gw-color-warning); } :host([variant=\"danger\"]) { --gw-button-tone: var(--gw-color-danger); } .button { --gw-size: var(--gw-button-scale, 1); --gw-scale: calc(var(--gw-size) * var(--gw-control-scale, 1)); --gw-icon-size: calc(1.125rem * var(--gw-scale)); position: relative; display: inline-flex; gap: calc(var(--gw-space-2) * var(--gw-size)); align-items: center; justify-content: center; min-height: calc(2.5rem * var(--gw-scale)); padding: 0 calc(var(--gw-space-4) * var(--gw-size)); font: inherit; font-size: calc(var(--gw-font-size-s) * var(--gw-size)); font-weight: var(--gw-font-weight-strong); line-height: 1; color: var(--gw-button-ink, var(--gw-button-tone)); text-decoration: none; text-transform: var(--gw-label-transform); letter-spacing: var(--gw-label-tracking); white-space: nowrap; cursor: pointer; background: transparent; border: max(var(--gw-border-width), 1px) solid transparent; border-radius: var(--gw-radius-pill); transition: background-color var(--gw-motion-fast) var(--gw-motion-easing), border-color var(--gw-motion-fast) var(--gw-motion-easing), color var(--gw-motion-fast) var(--gw-motion-easing), scale var(--gw-motion-fast) var(--gw-motion-easing); } /* A slight press, so a click feels physical */ .button:active { scale: 0.97; } .button:hover { background: color-mix(in oklch, var(--gw-button-tone) 16%, transparent); } .button:active { background: color-mix(in oklch, var(--gw-button-tone) 24%, transparent); } /* A solid fill, with text that flips between black and white by the fill's lightness so it reads on every theme's colors */ :host([appearance=\"accent\"]) .button { color: oklch( from var(--gw-button-tone) clamp(0, (0.66 - l) * 1000, 1) 0 0 ); background: var(--gw-button-tone); } :host([appearance=\"accent\"]) .button:hover { background: oklch( from var(--gw-button-tone) calc(l + (0.6 - l) * 0.2) c h ); } :host([appearance=\"accent\"]) .button:active { background: oklch( from var(--gw-button-tone) calc(l + (0.6 - l) * 0.35) c h ); } /* The tint lowers the tone's contrast, so the text leans toward the strongest text color to stay readable on it */ :host([appearance=\"filled\"]) .button { color: var( --gw-button-ink, color-mix( in oklch, var(--gw-button-tone) 70%, var(--gw-color-text-strong) ) ); background: color-mix(in oklch, var(--gw-button-tone) 16%, transparent); } :host([appearance=\"filled\"]) .button:hover { background: color-mix(in oklch, var(--gw-button-tone) 24%, transparent); } :host([appearance=\"filled\"]) .button:active { background: color-mix(in oklch, var(--gw-button-tone) 32%, transparent); } :host([appearance=\"outlined\"]) .button { border-color: var(--gw-button-tone); } /* A slotted link carries the text; the pill draws its focus ring */ ::slotted(a) { color: inherit; text-decoration: none; outline: none; } .button.focused { outline: var(--gw-focus-width) solid var(--gw-color-focus); outline-offset: var(--gw-focus-offset); } :host([appearance=\"plain\"]) .button.current, :host([appearance=\"outlined\"]) .button.current { background: color-mix(in oklch, var(--gw-button-tone) 16%, transparent); } :host([appearance=\"filled\"]) .button.current { background: color-mix(in oklch, var(--gw-button-tone) 32%, transparent); } .button:disabled, .button[aria-disabled=\"true\"] { color: var(--gw-color-text-disabled); pointer-events: none; cursor: not-allowed; background: var(--gw-color-fill-neutral-disabled); border-color: transparent; } :host([appearance=\"plain\"]) .button:disabled, :host([appearance=\"plain\"]) .button[aria-disabled=\"true\"] { background: transparent; } ::slotted([slot=\"icon\"]), .icon { flex: none; width: var(--gw-icon-size); height: var(--gw-icon-size); fill: currentColor; } /* The icon and the ring share one cell and crossfade */ .lead { display: grid; place-items: center; } /* The icon slot is display: contents, so its slotted icon is a grid item of its own and needs the shared cell too */ .lead > *, ::slotted([slot=\"icon\"]) { grid-area: 1 / 1; } /* A color dot in the icon's place */ .swatch { width: calc(var(--gw-icon-size) * 0.6); height: calc(var(--gw-icon-size) * 0.6); border: 1px solid color-mix(in oklab, var(--gw-color-text-primary) 20%, transparent); border-radius: 50%; } .lead > *, .text, .ring { transition: opacity var(--gw-motion-normal) cubic-bezier(0.4, 0, 0.2, 1); } /* Sized to the icon's drawn area, which is inset in its 24px box */ .ring { width: calc(var(--gw-icon-size) * 0.8); height: calc(var(--gw-icon-size) * 0.8); border: calc(2px * var(--gw-scale)) solid color-mix(in oklch, currentColor 25%, transparent); border-top-color: currentColor; border-radius: 50%; opacity: 0; animation: spin 0.9s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } /* Without an icon, the ring sits over the hidden text */ .ring.over { position: absolute; inset: 0; margin: auto; } :host([loading]) .button { cursor: progress; } :host([loading]) .lead > .ring, :host([loading]) .button:not(.has-icon) > .ring.over { opacity: 1; } :host([loading]) .lead > :not(.ring), :host([loading]) .button:not(.has-icon) > .text { opacity: 0; } .button:not(.has-icon) > .lead { display: none; } /* Icon only, to save space: square, with the text kept for assistive technology. auto does this on narrow screens */ :host([labels=\"hide\"]) .button.has-icon { min-width: calc(2.5rem * var(--gw-scale)); padding: 0; } :host([labels=\"hide\"]) .has-icon .text { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } @media (width < 40rem) { :host([labels=\"auto\"]) .button.has-icon { min-width: calc(2.5rem * var(--gw-scale)); padding: 0; } :host([labels=\"auto\"]) .has-icon .text { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } } `, ]",
3148
+ "default": "[ base, controlSizes(\"button\"), css` :host { --gw-button-tone: var(--gw-color-accent); display: inline-block; } :host([variant=\"neutral\"]) { --gw-button-tone: var(--gw-color-text-secondary); --gw-button-ink: var(--gw-color-text-strong); } :host([variant=\"info\"]) { --gw-button-tone: var(--gw-color-info); } :host([variant=\"success\"]) { --gw-button-tone: var(--gw-color-success); } :host([variant=\"warning\"]) { --gw-button-tone: var(--gw-color-warning); } :host([variant=\"danger\"]) { --gw-button-tone: var(--gw-color-danger); } .button { --gw-size: var(--gw-button-scale, 1); --gw-scale: calc(var(--gw-size) * var(--gw-control-scale, 1)); --gw-icon-size: calc(1.125rem * var(--gw-scale)); position: relative; display: inline-flex; gap: calc(var(--gw-space-2) * var(--gw-size)); align-items: center; justify-content: center; min-height: calc(2.5rem * var(--gw-scale)); padding: 0 calc(var(--gw-space-4) * var(--gw-size)); font: inherit; font-size: calc(var(--gw-font-size-s) * var(--gw-size)); font-weight: var(--gw-font-weight-strong); line-height: 1; color: var(--gw-button-ink, var(--gw-button-tone)); text-decoration: none; text-transform: var(--gw-label-transform); letter-spacing: var(--gw-label-tracking); white-space: nowrap; cursor: pointer; background: transparent; border: max(var(--gw-border-width), 1px) solid transparent; border-radius: var(--gw-radius-pill); transition: background-color var(--gw-motion-fast) var(--gw-motion-easing), border-color var(--gw-motion-fast) var(--gw-motion-easing), color var(--gw-motion-fast) var(--gw-motion-easing), scale var(--gw-motion-fast) var(--gw-motion-easing); } /* A slight press, so a click feels physical */ .button:active { scale: 0.97; } .button:hover { background: color-mix(in oklch, var(--gw-button-tone) 16%, transparent); } .button:active { background: color-mix(in oklch, var(--gw-button-tone) 24%, transparent); } /* A solid fill, with text that flips between black and white by the fill's lightness so it reads on every theme's colors */ :host([appearance=\"accent\"]) .button { color: oklch( from var(--gw-button-tone) clamp(0, (0.66 - l) * 1000, 1) 0 0 ); background: var(--gw-button-tone); } :host([appearance=\"accent\"]) .button:hover { background: oklch( from var(--gw-button-tone) calc(l + (0.6 - l) * 0.2) c h ); } :host([appearance=\"accent\"]) .button:active { background: oklch( from var(--gw-button-tone) calc(l + (0.6 - l) * 0.35) c h ); } /* The tint lowers the tone's contrast, so the text leans toward the strongest text color to stay readable on it */ :host([appearance=\"filled\"]) .button { color: var( --gw-button-ink, color-mix( in oklch, var(--gw-button-tone) 70%, var(--gw-color-text-strong) ) ); background: color-mix(in oklch, var(--gw-button-tone) 16%, transparent); } :host([appearance=\"filled\"]) .button:hover { background: color-mix(in oklch, var(--gw-button-tone) 24%, transparent); } :host([appearance=\"filled\"]) .button:active { background: color-mix(in oklch, var(--gw-button-tone) 32%, transparent); } :host([appearance=\"outlined\"]) .button { border-color: var(--gw-button-tone); } /* A slotted link carries the text; the pill draws its focus ring */ ::slotted(a) { color: inherit; text-decoration: none; outline: none; } .button.focused { outline: var(--gw-focus-width) solid var(--gw-color-focus); outline-offset: var(--gw-focus-offset); } :host([appearance=\"plain\"]) .button.current, :host([appearance=\"outlined\"]) .button.current { background: color-mix(in oklch, var(--gw-button-tone) 16%, transparent); } :host([appearance=\"filled\"]) .button.current { background: color-mix(in oklch, var(--gw-button-tone) 32%, transparent); } .button:disabled, .button[aria-disabled=\"true\"] { color: var(--gw-color-text-disabled); pointer-events: none; cursor: not-allowed; background: var(--gw-color-fill-neutral-disabled); border-color: transparent; } :host([appearance=\"plain\"]) .button:disabled, :host([appearance=\"plain\"]) .button[aria-disabled=\"true\"] { background: transparent; } ::slotted([slot=\"icon\"]), .icon { flex: none; width: var(--gw-icon-size); height: var(--gw-icon-size); fill: currentColor; } /* The icon and the ring share one cell and crossfade */ .lead { display: grid; place-items: center; } /* The icon slot is display: contents, so its slotted icon is a grid item of its own and needs the shared cell too */ .lead > *, ::slotted([slot=\"icon\"]) { grid-area: 1 / 1; } /* A color dot in the icon's place */ .swatch { width: calc(var(--gw-icon-size) * 0.6); height: calc(var(--gw-icon-size) * 0.6); border: 1px solid color-mix(in oklab, var(--gw-color-text-primary) 20%, transparent); border-radius: 50%; } .lead > *, .text, .ring { transition: opacity var(--gw-motion-normal) cubic-bezier(0.4, 0, 0.2, 1); } /* Sized to the icon's drawn area, which is inset in its 24px box */ .ring { width: calc(var(--gw-icon-size) * 0.8); height: calc(var(--gw-icon-size) * 0.8); border: calc(2px * var(--gw-scale)) solid color-mix(in oklch, currentColor 25%, transparent); border-top-color: currentColor; border-radius: 50%; opacity: 0; animation: spin 0.9s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } /* Without an icon, the ring sits over the hidden text */ .ring.over { position: absolute; inset: 0; margin: auto; } :host([loading]) .button { cursor: progress; } /* The icon stays for --gw-motion-delay, so a quick action never flashes the ring, then they crossfade; ending is immediate */ :host([loading]) .lead > .ring, :host([loading]) .button:not(.has-icon) > .ring.over { opacity: 1; transition-delay: var(--gw-motion-delay, 500ms); } :host([loading]) .lead > :not(.ring), :host([loading]) .button:not(.has-icon) > .text { opacity: 0; transition-delay: var(--gw-motion-delay, 500ms); } .button:not(.has-icon) > .lead { display: none; } /* Icon only, to save space: square, with the text kept for assistive technology. auto does this on narrow screens */ :host([labels=\"hide\"]) .button.has-icon { min-width: calc(2.5rem * var(--gw-scale)); padding: 0; } :host([labels=\"hide\"]) .has-icon .text { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } @media (width < 40rem) { :host([labels=\"auto\"]) .button.has-icon { min-width: calc(2.5rem * var(--gw-scale)); padding: 0; } :host([labels=\"auto\"]) .has-icon .text { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } } `, ]",
3149
3149
  "inheritedFrom": {
3150
3150
  "name": "GwElement",
3151
3151
  "module": "src/element.ts"
@@ -3722,9 +3722,17 @@
3722
3722
  "description": "The heading.",
3723
3723
  "name": "heading"
3724
3724
  },
3725
+ {
3726
+ "description": "The line under the heading.",
3727
+ "name": "description"
3728
+ },
3725
3729
  {
3726
3730
  "description": "The content.",
3727
3731
  "name": "body"
3732
+ },
3733
+ {
3734
+ "description": "The area under the divider.",
3735
+ "name": "footer"
3728
3736
  }
3729
3737
  ],
3730
3738
  "slots": [
@@ -3739,6 +3747,10 @@
3739
3747
  {
3740
3748
  "description": "Buttons beside the heading.",
3741
3749
  "name": "actions"
3750
+ },
3751
+ {
3752
+ "description": "Content under a divider at the bottom, such as a control for the whole card.",
3753
+ "name": "footer"
3742
3754
  }
3743
3755
  ],
3744
3756
  "members": [
@@ -3749,7 +3761,7 @@
3749
3761
  "text": "Properties"
3750
3762
  },
3751
3763
  "static": true,
3752
- "default": "{ heading: {}, href: {}, flush: { type: \"boolean\", reflect: true }, loading: { type: \"boolean\", reflect: true }, lines: { type: \"number\" }, }",
3764
+ "default": "{ heading: {}, description: {}, level: { type: \"number\" }, href: {}, flush: { type: \"boolean\", reflect: true }, loading: { type: \"boolean\", reflect: true }, lines: { type: \"number\" }, }",
3753
3765
  "inheritedFrom": {
3754
3766
  "name": "GwElement",
3755
3767
  "module": "src/element.ts"
@@ -3762,7 +3774,7 @@
3762
3774
  "text": "readonly CSSStyleSheet[]"
3763
3775
  },
3764
3776
  "static": true,
3765
- "default": "[ base, css` :host { display: block; } .card { display: flex; flex-direction: column; gap: var(--gw-space-3); height: 100%; padding: var(--gw-space-4); color: inherit; text-decoration: none; background: var(--gw-color-surface-raised); border: var(--gw-border-width) solid var(--gw-color-border-normal); border-radius: var(--gw-radius-panel); box-shadow: var(--gw-shadow-s); } :host([flush]) .card { padding: 0; overflow: hidden; } a.card { transition: border-color var(--gw-motion-fast) var(--gw-motion-easing); } a.card:hover, .card.linked:hover { border-color: var(--gw-color-border-accent); } /* A card following a link in its heading slot */ .card.linked { cursor: pointer; transition: border-color var(--gw-motion-fast) var(--gw-motion-easing); } .card.focused { outline: var(--gw-focus-width) solid var(--gw-color-focus); outline-offset: var(--gw-focus-offset); } ::slotted([slot=\"heading\"]) { font: inherit; color: inherit; } ::slotted(a[slot=\"heading\"]) { text-decoration: none; outline: none; } header { display: flex; gap: var(--gw-space-2); align-items: center; } h2 { flex: 1; margin: 0; font-size: var(--gw-font-size-m); font-weight: var(--gw-font-weight-strong); color: var(--gw-color-text-strong); } .body:not([hidden]) { display: flex; flex-direction: column; gap: inherit; } `, ]",
3777
+ "default": "[ base, css` :host { display: block; } .card { display: flex; flex-direction: column; gap: var(--gw-space-3); height: 100%; padding: var(--gw-space-4); color: inherit; text-decoration: none; background: var(--gw-color-surface-raised); border: var(--gw-border-width) solid var(--gw-color-border-normal); border-radius: var(--gw-radius-panel); box-shadow: var(--gw-shadow-s); } :host([flush]) .card { padding: 0; overflow: hidden; } a.card { transition: border-color var(--gw-motion-fast) var(--gw-motion-easing); } a.card:hover, .card.linked:hover { border-color: var(--gw-color-border-accent); } /* A card following a link in its heading slot */ .card.linked { cursor: pointer; transition: border-color var(--gw-motion-fast) var(--gw-motion-easing); } .card.focused { outline: var(--gw-focus-width) solid var(--gw-color-focus); outline-offset: var(--gw-focus-offset); } ::slotted([slot=\"heading\"]) { font: inherit; color: inherit; } ::slotted(a[slot=\"heading\"]) { text-decoration: none; outline: none; } header { display: flex; gap: var(--gw-space-2); align-items: center; } .titles { display: flex; flex: 1; flex-direction: column; gap: var(--gw-space-0-5); min-width: 0; } .title { margin: 0; font-size: var(--gw-font-size-m); font-weight: var(--gw-font-weight-strong); color: var(--gw-color-text-strong); } .description { font-size: var(--gw-font-size-xs); color: var(--gw-color-text-secondary); } /* The divider runs to the card's edges */ footer:not([hidden]) { display: flex; flex-direction: column; gap: inherit; margin-inline: calc(-1 * var(--gw-space-4)); padding: var(--gw-space-3) var(--gw-space-4) 0; border-top: var(--gw-border-width) solid var(--gw-color-border-normal); } :host([flush]) footer:not([hidden]) { margin-inline: 0; padding-bottom: var(--gw-space-3); } .body:not([hidden]) { display: flex; flex-direction: column; gap: inherit; } `, ]",
3766
3778
  "inheritedFrom": {
3767
3779
  "name": "GwElement",
3768
3780
  "module": "src/element.ts"
@@ -3776,6 +3788,23 @@
3776
3788
  },
3777
3789
  "default": "\"\""
3778
3790
  },
3791
+ {
3792
+ "kind": "field",
3793
+ "name": "description",
3794
+ "type": {
3795
+ "text": "string"
3796
+ },
3797
+ "default": "\"\""
3798
+ },
3799
+ {
3800
+ "kind": "field",
3801
+ "name": "level",
3802
+ "type": {
3803
+ "text": "number"
3804
+ },
3805
+ "description": "The heading's level for assistive tech.",
3806
+ "default": "2"
3807
+ },
3779
3808
  {
3780
3809
  "kind": "field",
3781
3810
  "name": "href",
@@ -3827,6 +3856,18 @@
3827
3856
  "privacy": "private",
3828
3857
  "readonly": true
3829
3858
  },
3859
+ {
3860
+ "kind": "field",
3861
+ "name": "#heading",
3862
+ "privacy": "private",
3863
+ "readonly": true
3864
+ },
3865
+ {
3866
+ "kind": "field",
3867
+ "name": "#description",
3868
+ "privacy": "private",
3869
+ "readonly": true
3870
+ },
3830
3871
  {
3831
3872
  "kind": "field",
3832
3873
  "name": "#header",
@@ -3836,6 +3877,21 @@
3836
3877
  },
3837
3878
  "readonly": true
3838
3879
  },
3880
+ {
3881
+ "kind": "field",
3882
+ "name": "#footerSlot",
3883
+ "privacy": "private",
3884
+ "readonly": true
3885
+ },
3886
+ {
3887
+ "kind": "field",
3888
+ "name": "#footer",
3889
+ "privacy": "private",
3890
+ "type": {
3891
+ "text": "HTMLElement"
3892
+ },
3893
+ "readonly": true
3894
+ },
3839
3895
  {
3840
3896
  "kind": "field",
3841
3897
  "name": "#skeleton",
@@ -4208,6 +4264,20 @@
4208
4264
  "description": "The card's heading.",
4209
4265
  "name": "heading"
4210
4266
  },
4267
+ {
4268
+ "type": {
4269
+ "text": "string"
4270
+ },
4271
+ "description": "A line under the heading.",
4272
+ "name": "description"
4273
+ },
4274
+ {
4275
+ "type": {
4276
+ "text": "number"
4277
+ },
4278
+ "description": "The heading's level for assistive tech, 2 by default. Use 3 for a card under a section heading.",
4279
+ "name": "level"
4280
+ },
4211
4281
  {
4212
4282
  "type": {
4213
4283
  "text": "string"
@@ -10441,7 +10511,7 @@
10441
10511
  "text": "readonly CSSStyleSheet[]"
10442
10512
  },
10443
10513
  "static": true,
10444
- "default": "[ base, controlSizes(\"menu-button\"), css` :host { position: relative; display: inline-block; anchor-scope: --gw-menu; } .trigger { --gw-size: var(--gw-menu-button-scale, 1); --gw-scale: calc(var(--gw-size) * var(--gw-control-scale, 1)); --gw-icon-size: calc(1.25rem * var(--gw-scale)); display: inline-flex; gap: calc(var(--gw-space-2) * var(--gw-size)); align-items: center; box-sizing: border-box; height: calc(2.5rem * var(--gw-scale)); padding: 0 calc(var(--gw-space-4) * var(--gw-size)); font: inherit; font-size: calc(var(--gw-font-size-s) * var(--gw-size)); font-weight: var(--gw-font-weight-strong); color: var(--gw-color-text-strong); cursor: pointer; background: var(--gw-color-fill-neutral-resting); border: var(--gw-border-width) solid var(--gw-border-color); border-radius: var(--gw-radius-control); transition: background-color var(--gw-motion-fast) var(--gw-motion-easing); anchor-name: --gw-menu; } .trigger:hover { background: var(--gw-color-fill-neutral-hovered); } /* While a chosen action runs, a ring takes the icon's place */ .ring { display: none; flex: none; width: calc(var(--gw-icon-size) * 0.8); height: calc(var(--gw-icon-size) * 0.8); border: calc(2px * var(--gw-scale)) solid color-mix(in oklch, currentColor 25%, transparent); border-top-color: currentColor; border-radius: 50%; animation: spin 0.9s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } :host([loading]) .trigger { cursor: progress; } :host([loading]) .ring { display: block; } :host([loading]) .trigger > .icon { display: none; } /* The other appearances draw the trigger as gw-button draws a neutral one, a pill, so it sits in a row of buttons without a seam */ :host([appearance=\"accent\"]) .trigger, :host([appearance=\"outlined\"]) .trigger, :host([appearance=\"plain\"]) .trigger { background: transparent; border: max(var(--gw-border-width), 1px) solid transparent; border-radius: var(--gw-radius-pill); transition: background-color var(--gw-motion-fast) var(--gw-motion-easing), border-color var(--gw-motion-fast) var(--gw-motion-easing), color var(--gw-motion-fast) var(--gw-motion-easing); } :host([appearance=\"outlined\"]) .trigger { border-color: var(--gw-color-text-secondary); } :host([appearance=\"outlined\"]) .trigger:hover, :host([appearance=\"plain\"]) .trigger:hover { background: color-mix( in oklch, var(--gw-color-text-secondary) 16%, transparent ); } /* A solid fill, with text that flips between black and white by the fill's lightness, as gw-button's accent does */ :host([appearance=\"accent\"]) .trigger { color: oklch( from var(--gw-color-accent) clamp(0, (0.66 - l) * 1000, 1) 0 0 ); background: var(--gw-color-accent); } :host([appearance=\"accent\"]) .trigger:hover { background: oklch( from var(--gw-color-accent) calc(l + (0.6 - l) * 0.2) c h ); } :host([active]:not([appearance=\"accent\"])) .trigger { color: var(--gw-color-accent); border-color: var(--gw-color-accent); } /* Icon only, square like an icon-only gw-button, such as a ⋮ for more actions, with the text kept for assistive technology */ :host([labels=\"hide\"]) .trigger.has-icon { justify-content: center; min-width: calc(2.5rem * var(--gw-scale)); padding: 0; } :host([labels=\"hide\"]) .has-icon .chevron { display: none; } :host([labels=\"hide\"]) .has-icon .label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } @media (width < 40rem) { :host([labels=\"auto\"]) .trigger.has-icon { padding: 0 calc(var(--gw-space-2) * var(--gw-size)); } :host([labels=\"auto\"]) .has-icon .label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } } .chevron { width: 0.4rem; height: 0.4rem; margin-inline-start: var(--gw-space-1); border-inline-end: 1.5px solid currentColor; border-block-end: 1.5px solid currentColor; rotate: 45deg; translate: 0 -0.125rem; transition: rotate var(--gw-motion-normal) var(--gw-motion-easing), translate var(--gw-motion-normal) var(--gw-motion-easing); } .trigger[aria-expanded=\"true\"] .chevron { rotate: 225deg; translate: 0 0.125rem; } /* Below the button down to the window's edge, or above it when there's more room there, growing to its content and scrolling past that */ [popover] { position-anchor: --gw-menu; position-try-fallbacks: flip-inline; /* The area is the room on that side, so 100% is all of it */ position-area: block-end span-inline-end; inset: auto; box-sizing: border-box; min-width: 15rem; max-width: calc(100vw - 2 * var(--gw-space-2)); max-height: min(24rem, calc(100% - 2 * var(--gw-space-2))); padding: 0; margin: var(--gw-space-1) 0; overflow: hidden; color: var(--gw-color-text-primary); background: var(--gw-color-surface-overlay); border: max(var(--gw-border-width), 1px) solid var(--gw-color-border-normal); border-radius: var(--gw-radius-l); box-shadow: var(--gw-shadow-m); opacity: 0; translate: 0 -0.25rem; transition: opacity var(--gw-motion-fast) var(--gw-motion-easing), translate var(--gw-motion-normal) var(--gw-motion-easing), display var(--gw-motion-normal) allow-discrete, overlay var(--gw-motion-normal) allow-discrete; } [popover]:popover-open { display: flex; flex-direction: column; opacity: 1; translate: 0 0; } @starting-style { [popover]:popover-open { opacity: 0; translate: 0 -0.25rem; } } /* The items scroll; the actions stay put below them, so \"Add\" is always in reach rather than at the end of a long list */ .items, .actions { display: flex; flex-direction: column; gap: var(--gw-space-0-5); padding: var(--gw-space-1); } .items { flex: 1; min-height: 0; overflow-y: auto; } .actions { border-block-start: max(var(--gw-border-width), 1px) solid var(--gw-color-border-normal); } .empty { padding: var(--gw-space-3); margin: 0; font-size: var(--gw-font-size-s); color: var(--gw-color-text-secondary); } .item { display: flex; gap: var(--gw-space-3); align-items: center; box-sizing: border-box; min-height: 2.5rem; padding: var(--gw-space-2) var(--gw-space-3); font: inherit; font-size: var(--gw-font-size-s); color: var(--gw-color-text-primary); text-align: start; cursor: pointer; background: transparent; border: 0; border-radius: var(--gw-radius-m); transition: background-color var(--gw-motion-fast) var(--gw-motion-easing); --gw-icon-size: 1.125rem; } .item:hover, .item:focus-visible { background: var(--gw-color-fill-neutral-hovered); outline: none; } .item:focus-visible { box-shadow: inset 0 0 0 var(--gw-focus-width) var(--gw-color-focus); } .item:disabled { color: var(--gw-color-text-disabled); cursor: not-allowed; background: transparent; } .action { font-weight: var(--gw-font-weight-strong); color: var(--gw-color-accent); } /* Holds the tick's place on every choice, so the labels line up */ .tick { flex: none; width: var(--gw-icon-size); height: var(--gw-icon-size); color: var(--gw-color-accent); } .item[aria-checked=\"false\"] .tick { visibility: hidden; } .item[role=\"menuitemcheckbox\"] .box { flex: none; color: var(--gw-color-text-secondary); } .item[role=\"menuitemcheckbox\"][aria-checked=\"true\"] .box { color: var(--gw-color-accent); } .group { display: flex; flex-direction: column; gap: var(--gw-space-0-5); } .group:not(:first-child) { padding-block-start: var(--gw-space-1); margin-block-start: var(--gw-space-0-5); border-block-start: max(var(--gw-border-width), 1px) solid var(--gw-color-border-normal); } .group-heading { padding: var(--gw-space-1) var(--gw-space-3) 0; margin: 0; font-size: var(--gw-font-size-xs); font-weight: var(--gw-font-weight-strong); color: var(--gw-color-text-secondary); } .separator { flex: none; height: 0; margin: var(--gw-space-0-5) calc(-1 * var(--gw-space-1)); border: 0; border-block-start: max(var(--gw-border-width), 1px) solid var(--gw-color-border-normal); } .text { display: flex; flex-direction: column; gap: var(--gw-space-0-5); } .description { font-size: var(--gw-font-size-xs); color: var(--gw-color-text-secondary); } `, ]",
10514
+ "default": "[ base, controlSizes(\"menu-button\"), css` :host { position: relative; display: inline-block; anchor-scope: --gw-menu; } .trigger { --gw-size: var(--gw-menu-button-scale, 1); --gw-scale: calc(var(--gw-size) * var(--gw-control-scale, 1)); --gw-icon-size: calc(1.25rem * var(--gw-scale)); display: inline-flex; gap: calc(var(--gw-space-2) * var(--gw-size)); align-items: center; box-sizing: border-box; height: calc(2.5rem * var(--gw-scale)); padding: 0 calc(var(--gw-space-4) * var(--gw-size)); font: inherit; font-size: calc(var(--gw-font-size-s) * var(--gw-size)); font-weight: var(--gw-font-weight-strong); color: var(--gw-color-text-strong); cursor: pointer; background: var(--gw-color-fill-neutral-resting); border: var(--gw-border-width) solid var(--gw-border-color); border-radius: var(--gw-radius-control); transition: background-color var(--gw-motion-fast) var(--gw-motion-easing); anchor-name: --gw-menu; } .trigger:hover { background: var(--gw-color-fill-neutral-hovered); } /* While a chosen action runs, a ring takes the icon's place: they share one cell and crossfade after --gw-motion-delay, so a quick action never flashes it and nothing moves. Without an icon, the ring sits over the hidden text. */ .lead { display: grid; flex: none; place-items: center; } .trigger:not(.has-icon) > .lead { display: contents; } .lead > * { grid-area: 1 / 1; } .lead > *, .label { transition: opacity var(--gw-motion-normal) cubic-bezier(0.4, 0, 0.2, 1); } .ring { width: calc(var(--gw-icon-size) * 0.8); height: calc(var(--gw-icon-size) * 0.8); border: calc(2px * var(--gw-scale)) solid color-mix(in oklch, currentColor 25%, transparent); border-top-color: currentColor; border-radius: 50%; opacity: 0; animation: spin 0.9s linear infinite; } .trigger:not(.has-icon) .ring { position: absolute; inset: 0; margin: auto; } .trigger { position: relative; } @keyframes spin { to { transform: rotate(360deg); } } :host([loading]) .trigger { cursor: progress; } :host([loading]) .ring { opacity: 1; transition-delay: var(--gw-motion-delay, 500ms); } :host([loading]) .lead > .icon, :host([loading]) .trigger:not(.has-icon) > .label { opacity: 0; transition-delay: var(--gw-motion-delay, 500ms); } /* The other appearances draw the trigger as gw-button draws a neutral one, a pill, so it sits in a row of buttons without a seam */ :host([appearance=\"accent\"]) .trigger, :host([appearance=\"outlined\"]) .trigger, :host([appearance=\"plain\"]) .trigger { background: transparent; border: max(var(--gw-border-width), 1px) solid transparent; border-radius: var(--gw-radius-pill); transition: background-color var(--gw-motion-fast) var(--gw-motion-easing), border-color var(--gw-motion-fast) var(--gw-motion-easing), color var(--gw-motion-fast) var(--gw-motion-easing); } :host([appearance=\"outlined\"]) .trigger { border-color: var(--gw-color-text-secondary); } :host([appearance=\"outlined\"]) .trigger:hover, :host([appearance=\"plain\"]) .trigger:hover { background: color-mix( in oklch, var(--gw-color-text-secondary) 16%, transparent ); } /* A solid fill, with text that flips between black and white by the fill's lightness, as gw-button's accent does */ :host([appearance=\"accent\"]) .trigger { color: oklch( from var(--gw-color-accent) clamp(0, (0.66 - l) * 1000, 1) 0 0 ); background: var(--gw-color-accent); } :host([appearance=\"accent\"]) .trigger:hover { background: oklch( from var(--gw-color-accent) calc(l + (0.6 - l) * 0.2) c h ); } :host([active]:not([appearance=\"accent\"])) .trigger { color: var(--gw-color-accent); border-color: var(--gw-color-accent); } /* Icon only, square like an icon-only gw-button, such as a ⋮ for more actions, with the text kept for assistive technology */ :host([labels=\"hide\"]) .trigger.has-icon { justify-content: center; min-width: calc(2.5rem * var(--gw-scale)); padding: 0; } :host([labels=\"hide\"]) .has-icon .chevron { display: none; } :host([labels=\"hide\"]) .has-icon .label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } @media (width < 40rem) { :host([labels=\"auto\"]) .trigger.has-icon { padding: 0 calc(var(--gw-space-2) * var(--gw-size)); } :host([labels=\"auto\"]) .has-icon .label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } } .chevron { width: 0.4rem; height: 0.4rem; margin-inline-start: var(--gw-space-1); border-inline-end: 1.5px solid currentColor; border-block-end: 1.5px solid currentColor; rotate: 45deg; translate: 0 -0.125rem; transition: rotate var(--gw-motion-normal) var(--gw-motion-easing), translate var(--gw-motion-normal) var(--gw-motion-easing); } .trigger[aria-expanded=\"true\"] .chevron { rotate: 225deg; translate: 0 0.125rem; } /* Below the button down to the window's edge, or above it when there's more room there, growing to its content and scrolling past that */ [popover] { position-anchor: --gw-menu; position-try-fallbacks: flip-inline; /* The area is the room on that side, so 100% is all of it */ position-area: block-end span-inline-end; inset: auto; box-sizing: border-box; min-width: 15rem; max-width: calc(100vw - 2 * var(--gw-space-2)); max-height: min(24rem, calc(100% - 2 * var(--gw-space-2))); padding: 0; margin: var(--gw-space-1) 0; overflow: hidden; color: var(--gw-color-text-primary); background: var(--gw-color-surface-overlay); border: max(var(--gw-border-width), 1px) solid var(--gw-color-border-normal); border-radius: var(--gw-radius-l); box-shadow: var(--gw-shadow-m); opacity: 0; translate: 0 -0.25rem; transition: opacity var(--gw-motion-fast) var(--gw-motion-easing), translate var(--gw-motion-normal) var(--gw-motion-easing), display var(--gw-motion-normal) allow-discrete, overlay var(--gw-motion-normal) allow-discrete; } [popover]:popover-open { display: flex; flex-direction: column; opacity: 1; translate: 0 0; } @starting-style { [popover]:popover-open { opacity: 0; translate: 0 -0.25rem; } } /* The items scroll; the actions stay put below them, so \"Add\" is always in reach rather than at the end of a long list */ .items, .actions { display: flex; flex-direction: column; gap: var(--gw-space-0-5); padding: var(--gw-space-1); } .items { flex: 1; min-height: 0; overflow-y: auto; } .actions { border-block-start: max(var(--gw-border-width), 1px) solid var(--gw-color-border-normal); } .empty { padding: var(--gw-space-3); margin: 0; font-size: var(--gw-font-size-s); color: var(--gw-color-text-secondary); } .item { display: flex; gap: var(--gw-space-3); align-items: center; box-sizing: border-box; min-height: 2.5rem; padding: var(--gw-space-2) var(--gw-space-3); font: inherit; font-size: var(--gw-font-size-s); color: var(--gw-color-text-primary); text-align: start; cursor: pointer; background: transparent; border: 0; border-radius: var(--gw-radius-m); transition: background-color var(--gw-motion-fast) var(--gw-motion-easing); --gw-icon-size: 1.125rem; } .item:hover, .item:focus-visible { background: var(--gw-color-fill-neutral-hovered); outline: none; } .item:focus-visible { box-shadow: inset 0 0 0 var(--gw-focus-width) var(--gw-color-focus); } .item:disabled { color: var(--gw-color-text-disabled); cursor: not-allowed; background: transparent; } .action { font-weight: var(--gw-font-weight-strong); color: var(--gw-color-accent); } /* Holds the tick's place on every choice, so the labels line up */ .tick { flex: none; width: var(--gw-icon-size); height: var(--gw-icon-size); color: var(--gw-color-accent); } .item[aria-checked=\"false\"] .tick { visibility: hidden; } .item[role=\"menuitemcheckbox\"] .box { flex: none; color: var(--gw-color-text-secondary); } .item[role=\"menuitemcheckbox\"][aria-checked=\"true\"] .box { color: var(--gw-color-accent); } .group { display: flex; flex-direction: column; gap: var(--gw-space-0-5); } .group:not(:first-child) { padding-block-start: var(--gw-space-1); margin-block-start: var(--gw-space-0-5); border-block-start: max(var(--gw-border-width), 1px) solid var(--gw-color-border-normal); } .group-heading { padding: var(--gw-space-1) var(--gw-space-3) 0; margin: 0; font-size: var(--gw-font-size-xs); font-weight: var(--gw-font-weight-strong); color: var(--gw-color-text-secondary); } .separator { flex: none; height: 0; margin: var(--gw-space-0-5) calc(-1 * var(--gw-space-1)); border: 0; border-block-start: max(var(--gw-border-width), 1px) solid var(--gw-color-border-normal); } .text { display: flex; flex-direction: column; gap: var(--gw-space-0-5); } .description { font-size: var(--gw-font-size-xs); color: var(--gw-color-text-secondary); } `, ]",
10445
10515
  "inheritedFrom": {
10446
10516
  "name": "GwElement",
10447
10517
  "module": "src/element.ts"
@@ -11121,6 +11191,12 @@
11121
11191
  "kind": "class",
11122
11192
  "description": "A labeled bar for a value in a range, such as disk use. Past `high` it\nturns warning, and past `critical` danger, unless a `tone` is set. While\n`loading`, the track pulses and a skeleton stands in for the value.",
11123
11193
  "name": "GwMeter",
11194
+ "cssProperties": [
11195
+ {
11196
+ "description": "The bar's height. Defaults to `0.5rem`.",
11197
+ "name": "--gw-meter-height"
11198
+ }
11199
+ ],
11124
11200
  "cssParts": [
11125
11201
  {
11126
11202
  "description": "The row with the label and value.",
@@ -11137,6 +11213,10 @@
11137
11213
  {
11138
11214
  "description": "The filled part of the bar.",
11139
11215
  "name": "fill"
11216
+ },
11217
+ {
11218
+ "description": "The line under the bar.",
11219
+ "name": "caption"
11140
11220
  }
11141
11221
  ],
11142
11222
  "members": [
@@ -11147,7 +11227,7 @@
11147
11227
  "text": "Properties"
11148
11228
  },
11149
11229
  "static": true,
11150
- "default": "{ label: {}, value: { type: \"number\" }, min: { type: \"number\" }, max: { type: \"number\" }, high: { type: \"number\" }, critical: { type: \"number\" }, display: {}, tone: { reflect: true }, loading: { type: \"boolean\", reflect: true }, }",
11230
+ "default": "{ label: {}, value: { type: \"number\" }, min: { type: \"number\" }, max: { type: \"number\" }, high: { type: \"number\" }, critical: { type: \"number\" }, inverted: { type: \"boolean\" }, display: {}, caption: {}, tone: { reflect: true }, loading: { type: \"boolean\", reflect: true }, }",
11151
11231
  "inheritedFrom": {
11152
11232
  "name": "GwElement",
11153
11233
  "module": "src/element.ts"
@@ -11160,7 +11240,7 @@
11160
11240
  "text": "readonly CSSStyleSheet[]"
11161
11241
  },
11162
11242
  "static": true,
11163
- "default": "[ base, css` :host { display: block; } .top { display: flex; gap: var(--gw-space-2); justify-content: space-between; margin-block-end: var(--gw-space-1); font-size: var(--gw-font-size-s); } .value { font-variant-numeric: var(--gw-font-figures); color: var(--gw-color-text-strong); } .track { height: 0.5rem; overflow: hidden; background: var(--gw-color-surface-lower); border: var(--gw-border-width) solid var(--gw-color-border-quiet); border-radius: var(--gw-radius-pill); } .fill { height: 100%; background: var(--gw-meter-color, var(--gw-color-fill-accent-resting)); border-radius: inherit; transition: width var(--gw-motion-slow) var(--gw-motion-easing); } .bar { min-height: 0; height: 0.5rem; --gw-skeleton-radius: var(--gw-radius-pill); } `, ]",
11243
+ "default": "[ base, css` :host { display: block; } .top { display: flex; gap: var(--gw-space-2); justify-content: space-between; margin-block-end: var(--gw-space-1); font-size: var(--gw-font-size-s); } .value { font-variant-numeric: var(--gw-font-figures); color: var(--gw-color-text-strong); } .track { height: var(--gw-meter-height, 0.5rem); overflow: hidden; background: var(--gw-color-surface-lower); border: var(--gw-border-width) solid var(--gw-color-border-quiet); border-radius: var(--gw-radius-pill); } .fill { height: 100%; background: var(--gw-meter-color, var(--gw-color-fill-accent-resting)); border-radius: inherit; transition: width var(--gw-motion-slow) var(--gw-motion-easing); } .bar { min-height: 0; height: var(--gw-meter-height, 0.5rem); --gw-skeleton-radius: var(--gw-radius-pill); } .caption { display: block; margin-block-start: var(--gw-space-1); overflow: hidden; font-size: var(--gw-font-size-xs); color: var(--gw-color-text-secondary); text-overflow: ellipsis; white-space: nowrap; } .caption[hidden] { display: none; } `, ]",
11164
11244
  "inheritedFrom": {
11165
11245
  "name": "GwElement",
11166
11246
  "module": "src/element.ts"
@@ -11178,7 +11258,7 @@
11178
11258
  "kind": "field",
11179
11259
  "name": "value",
11180
11260
  "type": {
11181
- "text": "number"
11261
+ "text": "number | undefined"
11182
11262
  },
11183
11263
  "default": "0"
11184
11264
  },
@@ -11214,6 +11294,14 @@
11214
11294
  },
11215
11295
  "default": "undefined"
11216
11296
  },
11297
+ {
11298
+ "kind": "field",
11299
+ "name": "inverted",
11300
+ "type": {
11301
+ "text": "boolean"
11302
+ },
11303
+ "default": "false"
11304
+ },
11217
11305
  {
11218
11306
  "kind": "field",
11219
11307
  "name": "display",
@@ -11222,6 +11310,14 @@
11222
11310
  },
11223
11311
  "default": "\"\""
11224
11312
  },
11313
+ {
11314
+ "kind": "field",
11315
+ "name": "caption",
11316
+ "type": {
11317
+ "text": "string"
11318
+ },
11319
+ "default": "\"\""
11320
+ },
11225
11321
  {
11226
11322
  "kind": "field",
11227
11323
  "name": "tone",
@@ -11286,6 +11382,12 @@
11286
11382
  "privacy": "private",
11287
11383
  "readonly": true
11288
11384
  },
11385
+ {
11386
+ "kind": "field",
11387
+ "name": "#caption",
11388
+ "privacy": "private",
11389
+ "readonly": true
11390
+ },
11289
11391
  {
11290
11392
  "kind": "field",
11291
11393
  "name": "#rendered",
@@ -11295,6 +11397,25 @@
11295
11397
  },
11296
11398
  "default": "false"
11297
11399
  },
11400
+ {
11401
+ "kind": "field",
11402
+ "name": "#known",
11403
+ "privacy": "private",
11404
+ "readonly": true
11405
+ },
11406
+ {
11407
+ "kind": "method",
11408
+ "name": "#worse",
11409
+ "privacy": "private",
11410
+ "parameters": [
11411
+ {
11412
+ "name": "limit",
11413
+ "type": {
11414
+ "text": "number | undefined"
11415
+ }
11416
+ }
11417
+ ]
11418
+ },
11298
11419
  {
11299
11420
  "kind": "field",
11300
11421
  "name": "#color",
@@ -11625,7 +11746,7 @@
11625
11746
  "type": {
11626
11747
  "text": "number"
11627
11748
  },
11628
- "description": "The value. Defaults to 0.",
11749
+ "description": "The value. Defaults to 0. Leave it unset or `NaN` for an unknown value, shown as `-` with an empty bar.",
11629
11750
  "name": "value"
11630
11751
  },
11631
11752
  {
@@ -11656,6 +11777,13 @@
11656
11777
  "description": "Past this it turns danger.",
11657
11778
  "name": "critical"
11658
11779
  },
11780
+ {
11781
+ "type": {
11782
+ "text": "boolean"
11783
+ },
11784
+ "description": "Lower is worse, such as for a battery: at or below `high` it turns warning, and at or below `critical` danger.",
11785
+ "name": "inverted"
11786
+ },
11659
11787
  {
11660
11788
  "type": {
11661
11789
  "text": "string"
@@ -11663,6 +11791,13 @@
11663
11791
  "description": "The value as shown, such as `42 GB`. Defaults to a percentage.",
11664
11792
  "name": "display"
11665
11793
  },
11794
+ {
11795
+ "type": {
11796
+ "text": "string"
11797
+ },
11798
+ "description": "A quiet line under the bar.",
11799
+ "name": "caption"
11800
+ },
11666
11801
  {
11667
11802
  "type": {
11668
11803
  "text": "\"neutral\"|\"accent\"|\"info\"|\"success\"|\"warning\"|\"danger\"|\"important\""
@@ -17216,7 +17351,7 @@
17216
17351
  "declarations": [
17217
17352
  {
17218
17353
  "kind": "class",
17219
- "description": "A placeholder for what's loading, sized by its container or by `width`\nand `height` in CSS. The shapes below extend it, and components with a\n`loading` attribute draw their own from them, so a spinner is only needed\nfor an action in progress. It's hidden from assistive technology, so mark\nthe region it sits in `aria-busy`.",
17354
+ "description": "A placeholder for what's loading, sized by its container or by `width`\nand `height` in CSS. The shapes below extend it, and components with a\n`loading` attribute draw their own from them, so a spinner is only needed\nfor an action in progress. It stays invisible for `--gw-motion-delay`, then\nfades in and pulses, so a quick load never flashes it; it keeps its size\nthroughout, so nothing moves. It's hidden from assistive technology, so mark\nthe region it sits in `aria-busy`.",
17220
17355
  "name": "GwSkeleton",
17221
17356
  "cssProperties": [
17222
17357
  {
@@ -17226,6 +17361,10 @@
17226
17361
  {
17227
17362
  "description": "The corners. Defaults to the control radius.",
17228
17363
  "name": "--gw-skeleton-radius"
17364
+ },
17365
+ {
17366
+ "description": "How long it waits before fading in. Defaults to `500ms`; set `0ms` to show it at once.",
17367
+ "name": "--gw-motion-delay"
17229
17368
  }
17230
17369
  ],
17231
17370
  "members": [
@@ -17236,7 +17375,7 @@
17236
17375
  "text": "readonly CSSStyleSheet[]"
17237
17376
  },
17238
17377
  "static": true,
17239
- "default": "[ base, css` :host { display: block; min-height: 1rem; background: var( --gw-skeleton-color, var(--gw-color-fill-neutral-resting) ); border-radius: var(--gw-skeleton-radius, var(--gw-radius-control)); animation: pulse 2s ease-in-out infinite; } @keyframes pulse { 50% { opacity: 0.45; } } @media (forced-colors: active) { :host { background: GrayText; } } `, ]",
17378
+ "default": "[ base, css` :host { display: block; min-height: 1rem; background: var( --gw-skeleton-color, var(--gw-color-fill-neutral-resting) ); border-radius: var(--gw-skeleton-radius, var(--gw-radius-control)); /* Waits, fades in, then pulses, so a quick load never flashes it */ animation: gw-appear var(--gw-motion-slow, 350ms) cubic-bezier(0.4, 0, 0.2, 1) var(--gw-motion-delay, 500ms) backwards, pulse 2s ease-in-out calc(var(--gw-motion-delay, 500ms) + var(--gw-motion-slow, 350ms)) infinite; } @keyframes pulse { 50% { opacity: 0.45; } } @media (forced-colors: active) { :host { background: GrayText; } } `, ]",
17240
17379
  "inheritedFrom": {
17241
17380
  "name": "GwElement",
17242
17381
  "module": "src/element.ts"
@@ -17599,6 +17738,14 @@
17599
17738
  "name": "GwSkeleton",
17600
17739
  "module": "src/gw-skeleton.ts"
17601
17740
  }
17741
+ },
17742
+ {
17743
+ "description": "How long it waits before fading in. Defaults to `500ms`; set `0ms` to show it at once.",
17744
+ "name": "--gw-motion-delay",
17745
+ "inheritedFrom": {
17746
+ "name": "GwSkeleton",
17747
+ "module": "src/gw-skeleton.ts"
17748
+ }
17602
17749
  }
17603
17750
  ],
17604
17751
  "members": [
@@ -18346,6 +18493,14 @@
18346
18493
  "name": "GwSkeleton",
18347
18494
  "module": "src/gw-skeleton.ts"
18348
18495
  }
18496
+ },
18497
+ {
18498
+ "description": "How long it waits before fading in. Defaults to `500ms`; set `0ms` to show it at once.",
18499
+ "name": "--gw-motion-delay",
18500
+ "inheritedFrom": {
18501
+ "name": "GwSkeleton",
18502
+ "module": "src/gw-skeleton.ts"
18503
+ }
18349
18504
  }
18350
18505
  ],
18351
18506
  "members": [
@@ -18737,12 +18892,16 @@
18737
18892
  "declarations": [
18738
18893
  {
18739
18894
  "kind": "class",
18740
- "description": "A spinning ring while something loads, announced by its `label`.",
18895
+ "description": "A spinning ring while something loads, announced by its `label`. It keeps\nits space but stays invisible for `--gw-motion-delay`, then fades in, so a\nquick wait never flashes it.",
18741
18896
  "name": "GwSpinner",
18742
18897
  "cssProperties": [
18743
18898
  {
18744
18899
  "description": "The ring's size. Defaults to `1.25rem`.",
18745
18900
  "name": "--gw-spinner-size"
18901
+ },
18902
+ {
18903
+ "description": "How long it waits before fading in. Defaults to `500ms`.",
18904
+ "name": "--gw-motion-delay"
18746
18905
  }
18747
18906
  ],
18748
18907
  "cssParts": [
@@ -18772,7 +18931,7 @@
18772
18931
  "text": "readonly CSSStyleSheet[]"
18773
18932
  },
18774
18933
  "static": true,
18775
- "default": "[ base, css` :host { display: inline-flex; width: var(--gw-spinner-size, 1.25rem); height: var(--gw-spinner-size, 1.25rem); color: var(--gw-color-accent); } .ring { width: 100%; height: 100%; border: 2px solid var(--gw-color-border-quiet); border-top-color: currentColor; border-radius: 50%; animation: spin 0.9s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } .label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); } `, ]",
18934
+ "default": "[ base, css` :host { display: inline-flex; width: var(--gw-spinner-size, 1.25rem); height: var(--gw-spinner-size, 1.25rem); color: var(--gw-color-accent); animation: gw-appear var(--gw-motion-slow, 350ms) cubic-bezier(0.4, 0, 0.2, 1) var(--gw-motion-delay, 500ms) backwards; } .ring { width: 100%; height: 100%; border: 2px solid var(--gw-color-border-quiet); border-top-color: currentColor; border-radius: 50%; animation: spin 0.9s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } .label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); } `, ]",
18776
18935
  "inheritedFrom": {
18777
18936
  "name": "GwElement",
18778
18937
  "module": "src/element.ts"
@@ -22959,7 +23118,7 @@
22959
23118
  {
22960
23119
  "kind": "variable",
22961
23120
  "name": "base",
22962
- "default": "css` :host { font-family: var(--gw-font-sans); color: var(--gw-color-text-primary); } *, *::before, *::after { box-sizing: border-box; } :host([hidden]), [hidden] { display: none !important; } :focus-visible { outline: var(--gw-focus-width) solid var(--gw-color-focus); outline-offset: var(--gw-focus-offset); } .icon { flex: none; width: var(--gw-icon-size, 1.25rem); height: var(--gw-icon-size, 1.25rem); fill: currentColor; } :host([tone=\"accent\"]) { --gw-tone: var(--gw-color-accent); --gw-tone-fill: var(--gw-color-fill-accent-quiet); --gw-tone-border: var(--gw-color-border-accent); } :host([tone=\"info\"]) { --gw-tone: var(--gw-color-info); --gw-tone-fill: var(--gw-color-fill-info-quiet); --gw-tone-border: var(--gw-color-border-info); } :host([tone=\"success\"]) { --gw-tone: var(--gw-color-success); --gw-tone-fill: var(--gw-color-fill-success-quiet); --gw-tone-border: var(--gw-color-border-success); } :host([tone=\"warning\"]) { --gw-tone: var(--gw-color-warning); --gw-tone-fill: var(--gw-color-fill-warning-quiet); --gw-tone-border: var(--gw-color-border-warning); } :host([tone=\"danger\"]) { --gw-tone: var(--gw-color-danger); --gw-tone-fill: var(--gw-color-fill-danger-quiet); --gw-tone-border: var(--gw-color-border-danger); } :host([tone=\"important\"]) { --gw-tone: var(--gw-color-important); --gw-tone-fill: var(--gw-color-fill-important-quiet); --gw-tone-border: var(--gw-color-border-important); } /* :host too, since * in a shadow root doesn't match it, and the skeletons pulse on their host */ @media (prefers-reduced-motion: reduce) { :host, *, *::before, *::after { animation-duration: 0s !important; transition-duration: 0s !important; } } `",
23121
+ "default": "css` :host { font-family: var(--gw-font-sans); color: var(--gw-color-text-primary); } *, *::before, *::after { box-sizing: border-box; } :host([hidden]), [hidden] { display: none !important; } :focus-visible { outline: var(--gw-focus-width) solid var(--gw-color-focus); outline-offset: var(--gw-focus-offset); } .icon { flex: none; width: var(--gw-icon-size, 1.25rem); height: var(--gw-icon-size, 1.25rem); fill: currentColor; } :host([tone=\"accent\"]) { --gw-tone: var(--gw-color-accent); --gw-tone-fill: var(--gw-color-fill-accent-quiet); --gw-tone-border: var(--gw-color-border-accent); } :host([tone=\"info\"]) { --gw-tone: var(--gw-color-info); --gw-tone-fill: var(--gw-color-fill-info-quiet); --gw-tone-border: var(--gw-color-border-info); } :host([tone=\"success\"]) { --gw-tone: var(--gw-color-success); --gw-tone-fill: var(--gw-color-fill-success-quiet); --gw-tone-border: var(--gw-color-border-success); } :host([tone=\"warning\"]) { --gw-tone: var(--gw-color-warning); --gw-tone-fill: var(--gw-color-fill-warning-quiet); --gw-tone-border: var(--gw-color-border-warning); } :host([tone=\"danger\"]) { --gw-tone: var(--gw-color-danger); --gw-tone-fill: var(--gw-color-fill-danger-quiet); --gw-tone-border: var(--gw-color-border-danger); } :host([tone=\"important\"]) { --gw-tone: var(--gw-color-important); --gw-tone-fill: var(--gw-color-fill-important-quiet); --gw-tone-border: var(--gw-color-border-important); } /* A loading placeholder's entrance: hidden for --gw-motion-delay, so a quick load never flashes one, then a fade. Use it with appear below. */ @keyframes gw-appear { from { opacity: 0; } } /* :host too, since * in a shadow root doesn't match it, and the skeletons pulse on their host */ @media (prefers-reduced-motion: reduce) { :host, *, *::before, *::after { animation-duration: 0s !important; transition-duration: 0s !important; } } `",
22963
23122
  "description": "What every component shares: box sizing, the focus ring, icons and the\ntones that set `--gw-tone` from a `tone` attribute."
22964
23123
  },
22965
23124
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timmo001/groundwork-ui",
3
- "version": "0.9.19",
3
+ "version": "0.9.21",
4
4
  "description": "Groundwork's framework-free web components, styled by the --gw-* tokens",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
package/src/gw-button.ts CHANGED
@@ -295,14 +295,18 @@ export class GwButton extends GwElement {
295
295
  cursor: progress;
296
296
  }
297
297
 
298
+ /* The icon stays for --gw-motion-delay, so a quick action never
299
+ flashes the ring, then they crossfade; ending is immediate */
298
300
  :host([loading]) .lead > .ring,
299
301
  :host([loading]) .button:not(.has-icon) > .ring.over {
300
302
  opacity: 1;
303
+ transition-delay: var(--gw-motion-delay, 500ms);
301
304
  }
302
305
 
303
306
  :host([loading]) .lead > :not(.ring),
304
307
  :host([loading]) .button:not(.has-icon) > .text {
305
308
  opacity: 0;
309
+ transition-delay: var(--gw-motion-delay, 500ms);
306
310
  }
307
311
 
308
312
  .button:not(.has-icon) > .lead {
package/src/gw-card.ts CHANGED
@@ -25,6 +25,8 @@ import { base } from "./styles.js";
25
25
  * @summary A surface for one group of content, or a link to more.
26
26
  *
27
27
  * @attr {string} heading - The card's heading.
28
+ * @attr {string} description - A line under the heading.
29
+ * @attr {number} level - The heading's level for assistive tech, 2 by default. Use 3 for a card under a section heading.
28
30
  * @attr {string} href - Makes the whole card a link to this address.
29
31
  * @attr {boolean} flush - Drops the padding, for content that runs to the edges.
30
32
  * @attr {boolean} loading - Shows skeleton lines for the content and any unknown heading.
@@ -33,15 +35,20 @@ import { base } from "./styles.js";
33
35
  * @slot - The content.
34
36
  * @slot heading - The heading, in place of `heading`. An `<a>` here makes the whole card follow it.
35
37
  * @slot actions - Buttons beside the heading.
38
+ * @slot footer - Content under a divider at the bottom, such as a control for the whole card.
36
39
  *
37
40
  * @csspart card - The card's surface.
38
41
  * @csspart header - The row with the heading and actions.
39
42
  * @csspart heading - The heading.
43
+ * @csspart description - The line under the heading.
40
44
  * @csspart body - The content.
45
+ * @csspart footer - The area under the divider.
41
46
  */
42
47
  export class GwCard extends GwElement {
43
48
  static override properties = {
44
49
  heading: {},
50
+ description: {},
51
+ level: { type: "number" },
45
52
  href: {},
46
53
  flush: { type: "boolean", reflect: true },
47
54
  loading: { type: "boolean", reflect: true },
@@ -110,14 +117,41 @@ export class GwCard extends GwElement {
110
117
  align-items: center;
111
118
  }
112
119
 
113
- h2 {
120
+ .titles {
121
+ display: flex;
114
122
  flex: 1;
123
+ flex-direction: column;
124
+ gap: var(--gw-space-0-5);
125
+ min-width: 0;
126
+ }
127
+
128
+ .title {
115
129
  margin: 0;
116
130
  font-size: var(--gw-font-size-m);
117
131
  font-weight: var(--gw-font-weight-strong);
118
132
  color: var(--gw-color-text-strong);
119
133
  }
120
134
 
135
+ .description {
136
+ font-size: var(--gw-font-size-xs);
137
+ color: var(--gw-color-text-secondary);
138
+ }
139
+
140
+ /* The divider runs to the card's edges */
141
+ footer:not([hidden]) {
142
+ display: flex;
143
+ flex-direction: column;
144
+ gap: inherit;
145
+ margin-inline: calc(-1 * var(--gw-space-4));
146
+ padding: var(--gw-space-3) var(--gw-space-4) 0;
147
+ border-top: var(--gw-border-width) solid var(--gw-color-border-normal);
148
+ }
149
+
150
+ :host([flush]) footer:not([hidden]) {
151
+ margin-inline: 0;
152
+ padding-bottom: var(--gw-space-3);
153
+ }
154
+
121
155
  .body:not([hidden]) {
122
156
  display: flex;
123
157
  flex-direction: column;
@@ -127,6 +161,9 @@ export class GwCard extends GwElement {
127
161
  ];
128
162
 
129
163
  declare heading: string;
164
+ declare description: string;
165
+ /** The heading's level for assistive tech. */
166
+ declare level: number;
130
167
  declare href: string;
131
168
  declare flush: boolean;
132
169
  declare loading: boolean;
@@ -139,19 +176,34 @@ export class GwCard extends GwElement {
139
176
 
140
177
  readonly #headingSlot = h("slot", { name: "heading" });
141
178
 
179
+ readonly #heading = h(
180
+ "div",
181
+ { class: "title", part: "heading", role: "heading", "aria-level": "2" },
182
+ this.#title,
183
+ this.#headingSlot,
184
+ this.#titleSkeleton,
185
+ );
186
+
187
+ readonly #description = h("span", {
188
+ class: "description",
189
+ part: "description",
190
+ });
191
+
142
192
  readonly #header: HTMLElement = h(
143
193
  "header",
144
194
  { part: "header" },
145
- h(
146
- "h2",
147
- { part: "heading" },
148
- this.#title,
149
- this.#headingSlot,
150
- this.#titleSkeleton,
151
- ),
195
+ h("div", { class: "titles" }, this.#heading, this.#description),
152
196
  h("slot", { name: "actions" }),
153
197
  );
154
198
 
199
+ readonly #footerSlot = h("slot", { name: "footer" });
200
+
201
+ readonly #footer: HTMLElement = h(
202
+ "footer",
203
+ { part: "footer", hidden: "" },
204
+ this.#footerSlot,
205
+ );
206
+
155
207
  readonly #skeleton = h("gw-skeleton-lines");
156
208
 
157
209
  readonly #body: HTMLDivElement = h(
@@ -169,6 +221,8 @@ export class GwCard extends GwElement {
169
221
  constructor() {
170
222
  super();
171
223
  this.heading = "";
224
+ this.description = "";
225
+ this.level = 2;
172
226
  this.href = "";
173
227
  this.flush = false;
174
228
  this.loading = false;
@@ -215,6 +269,11 @@ export class GwCard extends GwElement {
215
269
  this.#headingSlot.addEventListener("slotchange", () => {
216
270
  this.requestUpdate("heading");
217
271
  });
272
+
273
+ this.#footerSlot.addEventListener("slotchange", () => {
274
+ this.#footer.hidden =
275
+ this.#footerSlot.assignedNodes({ flatten: true }).length === 0;
276
+ });
218
277
  }
219
278
 
220
279
  /** The `<a>` in the heading slot, or holding it, when there is one. */
@@ -227,7 +286,7 @@ export class GwCard extends GwElement {
227
286
  }
228
287
 
229
288
  protected override create(root: ShadowRoot) {
230
- this.#card.append(this.#header, this.#skeleton, this.#body);
289
+ this.#card.append(this.#header, this.#skeleton, this.#body, this.#footer);
231
290
  root.append(this.#card);
232
291
  }
233
292
 
@@ -260,9 +319,14 @@ export class GwCard extends GwElement {
260
319
 
261
320
  this.#card.classList.toggle("linked", this.#link() !== null && !this.href);
262
321
  this.#title.textContent = this.heading;
322
+ this.#heading.setAttribute("aria-level", String(this.level || 2));
323
+ this.#description.textContent = this.description;
324
+ this.#description.hidden = !this.description;
263
325
  this.#titleSkeleton.hidden =
264
326
  this.heading !== "" || slotted || !this.loading;
265
- this.#header.hidden = !this.heading && !slotted && !this.loading;
327
+ this.#header.hidden =
328
+ !this.heading && !this.description && !slotted && !this.loading;
329
+ this.#heading.hidden = !this.heading && !slotted && !this.loading;
266
330
  this.#skeleton.lines = this.lines;
267
331
  this.#skeleton.hidden = !this.loading;
268
332
  this.#body.hidden = this.loading;
@@ -139,19 +139,50 @@ export class GwMenuButton extends GwElement {
139
139
  background: var(--gw-color-fill-neutral-hovered);
140
140
  }
141
141
 
142
- /* While a chosen action runs, a ring takes the icon's place */
143
- .ring {
144
- display: none;
142
+ /* While a chosen action runs, a ring takes the icon's place: they share
143
+ one cell and crossfade after --gw-motion-delay, so a quick action
144
+ never flashes it and nothing moves. Without an icon, the ring sits
145
+ over the hidden text. */
146
+ .lead {
147
+ display: grid;
145
148
  flex: none;
149
+ place-items: center;
150
+ }
151
+
152
+ .trigger:not(.has-icon) > .lead {
153
+ display: contents;
154
+ }
155
+
156
+ .lead > * {
157
+ grid-area: 1 / 1;
158
+ }
159
+
160
+ .lead > *,
161
+ .label {
162
+ transition: opacity var(--gw-motion-normal) cubic-bezier(0.4, 0, 0.2, 1);
163
+ }
164
+
165
+ .ring {
146
166
  width: calc(var(--gw-icon-size) * 0.8);
147
167
  height: calc(var(--gw-icon-size) * 0.8);
148
168
  border: calc(2px * var(--gw-scale)) solid
149
169
  color-mix(in oklch, currentColor 25%, transparent);
150
170
  border-top-color: currentColor;
151
171
  border-radius: 50%;
172
+ opacity: 0;
152
173
  animation: spin 0.9s linear infinite;
153
174
  }
154
175
 
176
+ .trigger:not(.has-icon) .ring {
177
+ position: absolute;
178
+ inset: 0;
179
+ margin: auto;
180
+ }
181
+
182
+ .trigger {
183
+ position: relative;
184
+ }
185
+
155
186
  @keyframes spin {
156
187
  to {
157
188
  transform: rotate(360deg);
@@ -163,11 +194,14 @@ export class GwMenuButton extends GwElement {
163
194
  }
164
195
 
165
196
  :host([loading]) .ring {
166
- display: block;
197
+ opacity: 1;
198
+ transition-delay: var(--gw-motion-delay, 500ms);
167
199
  }
168
200
 
169
- :host([loading]) .trigger > .icon {
170
- display: none;
201
+ :host([loading]) .lead > .icon,
202
+ :host([loading]) .trigger:not(.has-icon) > .label {
203
+ opacity: 0;
204
+ transition-delay: var(--gw-motion-delay, 500ms);
171
205
  }
172
206
 
173
207
  /* The other appearances draw the trigger as gw-button draws a neutral
@@ -689,8 +723,12 @@ export class GwMenuButton extends GwElement {
689
723
 
690
724
  protected override create(root: ShadowRoot) {
691
725
  this.#trigger.append(
692
- this.#triggerIcon,
693
- h("span", { class: "ring", "aria-hidden": "true" }),
726
+ h(
727
+ "span",
728
+ { class: "lead" },
729
+ this.#triggerIcon,
730
+ h("span", { class: "ring", "aria-hidden": "true" }),
731
+ ),
694
732
  this.#triggerText,
695
733
  h("span", { class: "chevron", "aria-hidden": "true" }),
696
734
  );
@@ -12,7 +12,9 @@ import { base } from "./styles.js";
12
12
  * A placeholder for what's loading, sized by its container or by `width`
13
13
  * and `height` in CSS. The shapes below extend it, and components with a
14
14
  * `loading` attribute draw their own from them, so a spinner is only needed
15
- * for an action in progress. It's hidden from assistive technology, so mark
15
+ * for an action in progress. It stays invisible for `--gw-motion-delay`, then
16
+ * fades in and pulses, so a quick load never flashes it; it keeps its size
17
+ * throughout, so nothing moves. It's hidden from assistive technology, so mark
16
18
  * the region it sits in `aria-busy`.
17
19
  *
18
20
  * @element gw-skeleton
@@ -20,6 +22,7 @@ import { base } from "./styles.js";
20
22
  *
21
23
  * @cssprop --gw-skeleton-color - The fill. Defaults to the neutral fill.
22
24
  * @cssprop --gw-skeleton-radius - The corners. Defaults to the control radius.
25
+ * @cssprop --gw-motion-delay - How long it waits before fading in. Defaults to `500ms`; set `0ms` to show it at once.
23
26
  */
24
27
  export class GwSkeleton extends GwElement {
25
28
  static override styles: readonly CSSStyleSheet[] = [
@@ -33,7 +36,13 @@ export class GwSkeleton extends GwElement {
33
36
  var(--gw-color-fill-neutral-resting)
34
37
  );
35
38
  border-radius: var(--gw-skeleton-radius, var(--gw-radius-control));
36
- animation: pulse 2s ease-in-out infinite;
39
+ /* Waits, fades in, then pulses, so a quick load never flashes it */
40
+ animation:
41
+ gw-appear var(--gw-motion-slow, 350ms) cubic-bezier(0.4, 0, 0.2, 1)
42
+ var(--gw-motion-delay, 500ms) backwards,
43
+ pulse 2s ease-in-out
44
+ calc(var(--gw-motion-delay, 500ms) + var(--gw-motion-slow, 350ms))
45
+ infinite;
37
46
  }
38
47
 
39
48
  @keyframes pulse {
package/src/gw-spinner.ts CHANGED
@@ -8,7 +8,9 @@ import { GwElement, css, h } from "./element.js";
8
8
  import { base } from "./styles.js";
9
9
 
10
10
  /**
11
- * A spinning ring while something loads, announced by its `label`.
11
+ * A spinning ring while something loads, announced by its `label`. It keeps
12
+ * its space but stays invisible for `--gw-motion-delay`, then fades in, so a
13
+ * quick wait never flashes it.
12
14
  *
13
15
  * @element gw-spinner
14
16
  * @summary A spinning ring for a wait a skeleton can't stand in for.
@@ -18,6 +20,7 @@ import { base } from "./styles.js";
18
20
  * @csspart ring - The spinning ring.
19
21
  *
20
22
  * @cssprop --gw-spinner-size - The ring's size. Defaults to `1.25rem`.
23
+ * @cssprop --gw-motion-delay - How long it waits before fading in. Defaults to `500ms`.
21
24
  */
22
25
  export class GwSpinner extends GwElement {
23
26
  static override properties = {
@@ -32,6 +35,8 @@ export class GwSpinner extends GwElement {
32
35
  width: var(--gw-spinner-size, 1.25rem);
33
36
  height: var(--gw-spinner-size, 1.25rem);
34
37
  color: var(--gw-color-accent);
38
+ animation: gw-appear var(--gw-motion-slow, 350ms)
39
+ cubic-bezier(0.4, 0, 0.2, 1) var(--gw-motion-delay, 500ms) backwards;
35
40
  }
36
41
 
37
42
  .ring {
package/src/styles.ts CHANGED
@@ -101,6 +101,14 @@ export const base = css`
101
101
  --gw-tone-border: var(--gw-color-border-important);
102
102
  }
103
103
 
104
+ /* A loading placeholder's entrance: hidden for --gw-motion-delay, so a
105
+ quick load never flashes one, then a fade. Use it with appear below. */
106
+ @keyframes gw-appear {
107
+ from {
108
+ opacity: 0;
109
+ }
110
+ }
111
+
104
112
  /* :host too, since * in a shadow root doesn't match it, and the skeletons
105
113
  pulse on their host */
106
114
  @media (prefers-reduced-motion: reduce) {