@timmo001/groundwork-ui 0.9.24 → 0.9.26

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.
@@ -629,7 +629,7 @@
629
629
  {
630
630
  "kind": "variable",
631
631
  "name": "fieldStyles",
632
- "default": "css` :where(gw-input, gw-select, gw-switch, gw-checkbox) { display: flex; flex-direction: column; gap: var(--gw-space-2); font-family: var(--gw-font-sans); color: var(--gw-color-text-primary); } :where(gw-input, gw-select) .gw-field-heading { display: flex; gap: var(--gw-space-1); align-items: center; } :where(gw-input, gw-select) .gw-field-heading > label { font-size: var(--gw-font-size-s); font-weight: var(--gw-font-weight-strong); color: var(--gw-color-text-strong); } /* The (i) button by the label that opens a field's longer help */ :where(gw-input, gw-select) .gw-info { display: inline-grid; place-items: center; width: 1.5rem; height: 1.5rem; padding: 0; color: var(--gw-color-text-secondary); cursor: pointer; background: none; border: 0; border-radius: var(--gw-radius-pill); transition: color var(--gw-motion-fast) var(--gw-motion-easing), background-color var(--gw-motion-fast) var(--gw-motion-easing); } :where(gw-input, gw-select) .gw-info:hover { color: var(--gw-color-text-strong); background: var(--gw-color-fill-neutral-hovered); } :where(gw-input, gw-select) .gw-info:focus-visible { outline: var(--gw-focus-width) solid var(--gw-color-focus); outline-offset: var(--gw-focus-offset); } :where(gw-input, gw-select) .gw-info[hidden] { display: none; } :where(gw-input, gw-select) .gw-info > .icon { width: 1rem; height: 1rem; fill: currentColor; } /* Sizes run xs to xl through --gw-field-scale, around 1 for medium, and the control's height follows the theme's density too */ :where(gw-input, gw-select)[size=\"xs\"] { --gw-field-scale: 0.6; } :where(gw-input, gw-select)[size=\"small\"] { --gw-field-scale: 0.8; } :where(gw-input, gw-select)[size=\"large\"] { --gw-field-scale: 1.2; } :where(gw-input, gw-select)[size=\"xl\"] { --gw-field-scale: 1.4; } :where(gw-input, gw-select) :where(input, textarea, .gw-select-trigger) { --gw-field-size: var(--gw-field-scale, 1); --gw-icon-size: calc( 1.25rem * var(--gw-field-size) * var(--gw-control-scale, 1) ); width: 100%; min-height: calc( 2.5rem * var(--gw-field-size) * var(--gw-control-scale, 1) ); padding: 0 calc(var(--gw-space-3) * var(--gw-field-size)); font: inherit; font-size: calc(var(--gw-font-size-s) * var(--gw-field-size)); color: var(--gw-color-text-strong); background: var(--gw-color-surface-lower); border: var(--gw-border-width) solid var(--gw-color-border-normal); border-radius: var(--gw-radius-control); transition: border-color var(--gw-motion-fast) var(--gw-motion-easing); } /* An icon inside the field, before the text, with the text moved clear of it */ gw-input .gw-input-control { position: relative; display: flex; align-items: center; } gw-input .gw-input-control > .icon { position: absolute; inset-inline-start: calc(var(--gw-space-3) * var(--gw-field-scale, 1)); width: calc(1.125rem * var(--gw-field-scale, 1)); height: calc(1.125rem * var(--gw-field-scale, 1)); color: var(--gw-color-text-secondary); pointer-events: none; fill: currentColor; } gw-input .gw-input-control > .icon[hidden] { display: none; } gw-input .gw-input-control > .icon:not([hidden]) + input { padding-inline-start: calc( (var(--gw-space-3) * 2 + 1.125rem) * var(--gw-field-scale, 1) ); } /* A textarea keeps the field's padding on every side and grows by its corner, never sideways out of the layout */ gw-input textarea { display: block; padding-block: calc(var(--gw-space-2) * var(--gw-field-size)); line-height: 1.5; resize: vertical; } gw-input[monospace] :where(input, textarea) { font-family: var(--gw-font-mono); } gw-input .gw-optional { font-weight: var(--gw-font-weight-regular); color: var(--gw-color-text-secondary); } gw-input .gw-optional[hidden] { display: none; } /* The show and hide button sits inside the end of a password field, with the text kept clear of it */ gw-input .gw-reveal { position: absolute; inset-inline-end: calc(var(--gw-space-1) * var(--gw-field-scale, 1)); display: grid; place-items: center; width: calc(2rem * var(--gw-field-scale, 1)); height: calc(2rem * var(--gw-field-scale, 1)); padding: 0; color: var(--gw-color-text-secondary); cursor: pointer; background: none; border: 0; border-radius: var(--gw-radius-control); transition: color var(--gw-motion-fast) var(--gw-motion-easing), background-color var(--gw-motion-fast) var(--gw-motion-easing); } gw-input .gw-reveal:hover:not(:disabled) { color: var(--gw-color-text-strong); background: var(--gw-color-fill-neutral-hovered); } gw-input .gw-reveal:focus-visible { outline: var(--gw-focus-width) solid var(--gw-color-focus); outline-offset: calc(var(--gw-focus-width) * -1); } gw-input .gw-reveal:disabled { cursor: not-allowed; opacity: 0.5; } gw-input .gw-reveal[hidden] { display: none; } gw-input .gw-reveal > .icon { width: calc(1.125rem * var(--gw-field-scale, 1)); height: calc(1.125rem * var(--gw-field-scale, 1)); fill: currentColor; } gw-input .gw-input-control:has(> .gw-reveal:not([hidden])) > input { padding-inline-end: calc( (var(--gw-space-1) + 2rem + var(--gw-space-1)) * var(--gw-field-scale, 1) ); } /* The trigger shows the chosen option, with its icon, and a chevron with room around it that turns over while the list is open */ gw-select .gw-select-trigger { display: flex; gap: calc(var(--gw-space-2) * var(--gw-field-size)); align-items: center; padding-inline-end: calc(var(--gw-space-3) * var(--gw-field-size)); text-align: start; cursor: pointer; } gw-select .gw-select-value { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } /* With an inline label the select sits among toolbar controls, so it's a pill that fits its content, like the toggles and buttons beside it */ gw-select[inline-label] { display: inline-flex; } gw-select[inline-label] .gw-select-trigger { width: auto; padding-inline: calc(var(--gw-space-4) * var(--gw-field-size)) calc(var(--gw-space-3) * var(--gw-field-size)); border-radius: var(--gw-radius-pill); } /* An inline label reads as a quiet prefix to the value */ gw-select .gw-select-inline-label { flex: none; color: var(--gw-color-text-secondary); } gw-select .gw-select-inline-label[hidden] { display: none; } gw-select .gw-select-value[data-placeholder] { color: var(--gw-color-text-secondary); } gw-select .gw-select-trigger > .icon { flex: none; width: var(--gw-icon-size, 1.25rem); height: var(--gw-icon-size, 1.25rem); fill: currentColor; } gw-select .gw-select-trigger > .icon:last-child { color: var(--gw-color-text-secondary); transition: rotate var(--gw-motion-normal) var(--gw-motion-easing); } gw-select[open] .gw-select-trigger > .icon:last-child { rotate: 180deg; } gw-select .gw-select-trigger > .icon[hidden] { display: none; } /* The list opens under the trigger, growing from it, or as a bottom sheet sliding up on phones */ gw-select .gw-select-popup { position: fixed; inset: unset; box-sizing: border-box; max-width: calc(100vw - 12px); padding: 0; margin: 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-panel); box-shadow: var(--gw-shadow-l); opacity: 0; scale: 1 0.96; transform-origin: top; transition: opacity var(--gw-motion-fast) var(--gw-motion-easing), scale var(--gw-motion-normal) 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; } gw-select .gw-select-popup[data-placement=\"above\"] { transform-origin: bottom; } gw-select .gw-select-popup:popover-open { opacity: 1; scale: 1; } @starting-style { gw-select .gw-select-popup:popover-open { opacity: 0; scale: 1 0.96; } } gw-select .gw-select-popup[data-placement=\"sheet\"] { inset: auto 0 0; width: 100%; max-width: none; padding-block-end: env(safe-area-inset-bottom); border-end-start-radius: 0; border-end-end-radius: 0; scale: 1; translate: 0 100%; --gw-option-list-height: 60vh; } gw-select .gw-select-popup[data-placement=\"sheet\"]:popover-open { translate: 0 0; } @starting-style { gw-select .gw-select-popup[data-placement=\"sheet\"]:popover-open { translate: 0 100%; } } gw-select .gw-select-popup[data-placement=\"sheet\"]::backdrop { background: rgb(0 0 0 / 40%); } /* With multiple, a full-width Clear under the list empties the choice */ gw-select .gw-select-clear { display: block; width: 100%; min-height: 2.5rem; padding: 0 var(--gw-space-3); font: inherit; font-size: var(--gw-font-size-s); font-weight: var(--gw-font-weight-strong); color: var(--gw-color-text-secondary); cursor: pointer; background: none; border: 0; border-block-start: var(--gw-border-width) solid var(--gw-color-border-quiet); transition: color var(--gw-motion-fast) var(--gw-motion-easing), background-color var(--gw-motion-fast) var(--gw-motion-easing); } gw-select .gw-select-clear:hover { color: var(--gw-color-text-strong); background: var(--gw-color-fill-neutral-hovered); } gw-select .gw-select-clear:focus-visible { outline: var(--gw-focus-width) solid var(--gw-color-focus); outline-offset: calc(var(--gw-focus-width) * -1); } gw-select .gw-select-clear[hidden] { display: none; } :where(gw-input) .gw-input-control:focus-within > .icon { color: var(--gw-color-text-strong); } :where(gw-input, gw-select) :where(input, textarea, .gw-select-trigger):hover { border-color: var(--gw-color-border-loud); } :where(gw-input, gw-select, gw-switch, gw-checkbox) :where(input, textarea, .gw-select-trigger):focus-visible { outline: var(--gw-focus-width) solid var(--gw-color-focus); outline-offset: var(--gw-focus-offset); } :where(gw-input, gw-select) [aria-invalid=\"true\"] { border-color: var(--gw-color-border-danger); } :where(gw-input, gw-select, gw-switch, gw-checkbox) :disabled { color: var(--gw-color-text-disabled); cursor: not-allowed; } :where(gw-input, gw-select, gw-switch, gw-checkbox) .gw-hint { margin: 0; font-size: var(--gw-font-size-xs); color: var(--gw-color-text-secondary); } :where(gw-input, gw-select, gw-switch, gw-checkbox) .gw-error { margin: 0; font-size: var(--gw-font-size-xs); color: var(--gw-color-danger); } :where(gw-switch, gw-checkbox) > label { display: inline-flex; gap: var(--gw-space-3); align-items: center; font-size: var(--gw-font-size-s); cursor: pointer; } :where(gw-input, gw-switch, gw-checkbox, gw-select) .gw-visually-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } gw-checkbox input { width: 1.125rem; height: 1.125rem; margin: 0; accent-color: var(--gw-color-fill-accent-resting); } gw-switch input { position: relative; flex: none; width: 2.5rem; height: 1.5rem; margin: 0; cursor: pointer; appearance: none; background: var(--gw-color-surface-lower); border: var(--gw-border-width) solid var(--gw-color-border-loud); border-radius: var(--gw-radius-pill); transition: background-color var(--gw-motion-normal) var(--gw-motion-easing), border-color var(--gw-motion-normal) var(--gw-motion-easing); } gw-switch input::after { position: absolute; inset-block-start: 0.1875rem; inset-inline-start: 0.1875rem; width: 1rem; height: 1rem; content: \"\"; background: var(--gw-color-text-secondary); border-radius: 50%; transition: translate var(--gw-motion-normal) var(--gw-motion-easing), background-color var(--gw-motion-normal) var(--gw-motion-easing); } gw-switch input:checked { background: var(--gw-color-fill-accent-resting); border-color: transparent; } gw-switch input:checked::after { translate: 1rem 0; background: var(--gw-color-on-accent); } gw-switch input:disabled { opacity: 0.5; } gw-range { display: flex; flex-direction: column; gap: var(--gw-space-2); min-width: 0; font-family: var(--gw-font-sans); color: var(--gw-color-text-primary); } gw-range > label { font-size: var(--gw-font-size-s); font-weight: var(--gw-font-weight-strong); color: var(--gw-color-text-strong); } gw-range .gw-range-control { position: relative; display: block; } gw-range input { display: block; width: 100%; margin: 0; accent-color: var(--gw-color-fill-accent-resting); cursor: pointer; } gw-range input:focus-visible { outline: var(--gw-focus-width) solid var(--gw-color-focus); outline-offset: var(--gw-focus-offset); } gw-range input:disabled { cursor: not-allowed; opacity: 0.5; } /* The value above the thumb while it's held */ gw-range .gw-range-bubble { position: absolute; bottom: calc(100% + var(--gw-space-1)); padding: var(--gw-space-1) var(--gw-space-2); font-size: var(--gw-font-size-xs); font-variant-numeric: tabular-nums; color: var(--gw-color-on-accent); white-space: nowrap; pointer-events: none; background: var(--gw-color-fill-accent-resting); border-radius: var(--gw-radius-control); box-shadow: var(--gw-shadow-s); translate: -50% 0; } gw-range .gw-range-bubble[hidden] { display: none; } gw-range .gw-hint { margin: 0; font-size: var(--gw-font-size-xs); color: var(--gw-color-text-secondary); } gw-range .gw-visually-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } `",
632
+ "default": "css` :where(gw-input, gw-select, gw-switch, gw-checkbox) { display: flex; flex-direction: column; gap: var(--gw-space-2); font-family: var(--gw-font-sans); color: var(--gw-color-text-primary); } :where(gw-input, gw-select) .gw-field-heading { display: flex; gap: var(--gw-space-1); align-items: center; } :where(gw-input, gw-select) .gw-field-heading > label { font-size: var(--gw-font-size-s); font-weight: var(--gw-font-weight-strong); color: var(--gw-color-text-strong); } /* The (i) button by the label that opens a field's longer help */ :where(gw-input, gw-select) .gw-info { display: inline-grid; place-items: center; width: 1.5rem; height: 1.5rem; padding: 0; color: var(--gw-color-text-secondary); cursor: pointer; background: none; border: 0; border-radius: var(--gw-radius-pill); transition: color var(--gw-motion-fast) var(--gw-motion-easing), background-color var(--gw-motion-fast) var(--gw-motion-easing); } :where(gw-input, gw-select) .gw-info:hover { color: var(--gw-color-text-strong); background: var(--gw-color-fill-neutral-hovered); } :where(gw-input, gw-select) .gw-info:focus-visible { outline: var(--gw-focus-width) solid var(--gw-color-focus); outline-offset: var(--gw-focus-offset); } :where(gw-input, gw-select) .gw-info[hidden] { display: none; } :where(gw-input, gw-select) .gw-info > .icon { width: 1rem; height: 1rem; fill: currentColor; } /* Sizes run xs to xl through --gw-field-scale, around 1 for medium, and the control's height follows the theme's density too */ :where(gw-input, gw-select)[size=\"xs\"] { --gw-field-scale: 0.6; } :where(gw-input, gw-select)[size=\"small\"] { --gw-field-scale: 0.8; } :where(gw-input, gw-select)[size=\"large\"] { --gw-field-scale: 1.2; } :where(gw-input, gw-select)[size=\"xl\"] { --gw-field-scale: 1.4; } :where(gw-input, gw-select) :where(input, textarea, .gw-select-trigger) { --gw-field-size: var(--gw-field-scale, 1); --gw-icon-size: calc( 1.25rem * var(--gw-field-size) * var(--gw-control-scale, 1) ); width: 100%; min-height: calc( 2.5rem * var(--gw-field-size) * var(--gw-control-scale, 1) ); padding: 0 calc(var(--gw-space-3) * var(--gw-field-size)); font: inherit; font-size: calc(var(--gw-font-size-s) * var(--gw-field-size)); color: var(--gw-color-text-strong); background: var(--gw-color-surface-lower); border: var(--gw-border-width) solid var(--gw-color-border-normal); border-radius: var(--gw-radius-control); transition: border-color var(--gw-motion-fast) var(--gw-motion-easing); } /* An icon inside the field, before the text, with the text moved clear of it */ gw-input .gw-input-control { position: relative; display: flex; align-items: center; } gw-input .gw-input-control > .icon { position: absolute; inset-inline-start: calc(var(--gw-space-3) * var(--gw-field-scale, 1)); width: calc(1.125rem * var(--gw-field-scale, 1)); height: calc(1.125rem * var(--gw-field-scale, 1)); color: var(--gw-color-text-secondary); pointer-events: none; fill: currentColor; } gw-input .gw-input-control > .icon[hidden] { display: none; } gw-input .gw-input-control > .icon:not([hidden]) + input { padding-inline-start: calc( (var(--gw-space-3) * 2 + 1.125rem) * var(--gw-field-scale, 1) ); } /* A textarea keeps the field's padding on every side and grows by its corner, never sideways out of the layout */ gw-input textarea { display: block; padding-block: calc(var(--gw-space-2) * var(--gw-field-size)); line-height: 1.5; resize: vertical; } gw-input[monospace] :where(input, textarea) { font-family: var(--gw-font-mono); } gw-input .gw-optional { font-weight: var(--gw-font-weight-regular); color: var(--gw-color-text-secondary); } gw-input .gw-optional[hidden] { display: none; } /* The show and hide button sits inside the end of a password field, with the text kept clear of it */ gw-input .gw-reveal { position: absolute; inset-inline-end: calc(var(--gw-space-1) * var(--gw-field-scale, 1)); display: grid; place-items: center; width: calc(2rem * var(--gw-field-scale, 1)); height: calc(2rem * var(--gw-field-scale, 1)); padding: 0; color: var(--gw-color-text-secondary); cursor: pointer; background: none; border: 0; border-radius: var(--gw-radius-control); transition: color var(--gw-motion-fast) var(--gw-motion-easing), background-color var(--gw-motion-fast) var(--gw-motion-easing); } gw-input .gw-reveal:hover:not(:disabled) { color: var(--gw-color-text-strong); background: var(--gw-color-fill-neutral-hovered); } gw-input .gw-reveal:focus-visible { outline: var(--gw-focus-width) solid var(--gw-color-focus); outline-offset: calc(var(--gw-focus-width) * -1); } gw-input .gw-reveal:disabled { cursor: not-allowed; opacity: 0.5; } gw-input .gw-reveal[hidden] { display: none; } gw-input .gw-reveal > .icon { width: calc(1.125rem * var(--gw-field-scale, 1)); height: calc(1.125rem * var(--gw-field-scale, 1)); fill: currentColor; } gw-input .gw-input-control:has(> .gw-reveal:not([hidden])) > input { padding-inline-end: calc( (var(--gw-space-1) + 2rem + var(--gw-space-1)) * var(--gw-field-scale, 1) ); } /* The trigger shows the chosen option, with its icon, and a chevron with room around it that turns over while the list is open */ gw-select .gw-select-trigger { display: flex; gap: calc(var(--gw-space-2) * var(--gw-field-size)); align-items: center; padding-inline-end: calc(var(--gw-space-3) * var(--gw-field-size)); text-align: start; cursor: pointer; } gw-select .gw-select-value { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } /* With an inline label the select sits among toolbar controls, so it's a pill that fits its content, like the toggles and buttons beside it */ gw-select[inline-label] { display: inline-flex; } gw-select[inline-label] .gw-select-trigger { width: auto; padding-inline: calc(var(--gw-space-4) * var(--gw-field-size)) calc(var(--gw-space-3) * var(--gw-field-size)); border-radius: var(--gw-radius-pill); } /* An inline label reads as a quiet prefix to the value */ gw-select .gw-select-inline-label { flex: none; color: var(--gw-color-text-secondary); } gw-select .gw-select-inline-label[hidden] { display: none; } gw-select .gw-select-value[data-placeholder] { color: var(--gw-color-text-secondary); } gw-select .gw-select-trigger > .icon { flex: none; width: var(--gw-icon-size, 1.25rem); height: var(--gw-icon-size, 1.25rem); fill: currentColor; } gw-select .gw-select-trigger > .icon:last-child { color: var(--gw-color-text-secondary); transition: rotate var(--gw-motion-normal) var(--gw-motion-easing); } gw-select[open] .gw-select-trigger > .icon:last-child { rotate: 180deg; } gw-select .gw-select-trigger > .icon[hidden] { display: none; } /* The list opens under the trigger, growing from it, or as a bottom sheet sliding up on phones */ gw-select .gw-select-popup { position: fixed; inset: unset; box-sizing: border-box; max-width: calc(100vw - 12px); padding: 0; margin: 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-panel); box-shadow: var(--gw-shadow-l); opacity: 0; scale: 1 0.96; transform-origin: top; transition: opacity var(--gw-motion-fast) var(--gw-motion-easing), scale var(--gw-motion-normal) 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; } gw-select .gw-select-popup[data-placement=\"above\"] { transform-origin: bottom; } gw-select .gw-select-popup:popover-open { opacity: 1; scale: 1; } @starting-style { gw-select .gw-select-popup:popover-open { opacity: 0; scale: 1 0.96; } } gw-select .gw-select-popup[data-placement=\"sheet\"] { inset: auto 0 0; width: 100%; max-width: none; padding-block-end: env(safe-area-inset-bottom); border-end-start-radius: 0; border-end-end-radius: 0; scale: 1; translate: 0 100%; --gw-option-list-height: 60vh; } gw-select .gw-select-popup[data-placement=\"sheet\"]:popover-open { translate: 0 0; } @starting-style { gw-select .gw-select-popup[data-placement=\"sheet\"]:popover-open { translate: 0 100%; } } gw-select .gw-select-popup[data-placement=\"sheet\"]::backdrop { background: rgb(0 0 0 / 40%); } /* With multiple, a full-width Clear under the list empties the choice */ gw-select .gw-select-clear { display: block; width: 100%; min-height: 2.5rem; padding: 0 var(--gw-space-3); font: inherit; font-size: var(--gw-font-size-s); font-weight: var(--gw-font-weight-strong); color: var(--gw-color-text-secondary); cursor: pointer; background: none; border: 0; border-block-start: var(--gw-border-width) solid var(--gw-color-border-quiet); transition: color var(--gw-motion-fast) var(--gw-motion-easing), background-color var(--gw-motion-fast) var(--gw-motion-easing); } gw-select .gw-select-clear:hover { color: var(--gw-color-text-strong); background: var(--gw-color-fill-neutral-hovered); } gw-select .gw-select-clear:focus-visible { outline: var(--gw-focus-width) solid var(--gw-color-focus); outline-offset: calc(var(--gw-focus-width) * -1); } gw-select .gw-select-clear[hidden] { display: none; } :where(gw-input) .gw-input-control:focus-within > .icon { color: var(--gw-color-text-strong); } :where(gw-input, gw-select) :where(input, textarea, .gw-select-trigger):hover { border-color: var(--gw-color-border-loud); } :where(gw-input, gw-select, gw-switch, gw-checkbox) :where(input, textarea, .gw-select-trigger):focus-visible { outline: var(--gw-focus-width) solid var(--gw-color-focus); outline-offset: var(--gw-focus-offset); } :where(gw-input, gw-select) [aria-invalid=\"true\"] { border-color: var(--gw-color-border-danger); } :where(gw-input, gw-select, gw-switch, gw-checkbox) :disabled { color: var(--gw-color-text-disabled); cursor: not-allowed; } :where(gw-input, gw-select, gw-switch, gw-checkbox) .gw-hint { margin: 0; font-size: var(--gw-font-size-xs); color: var(--gw-color-text-secondary); } :where(gw-input, gw-select, gw-switch, gw-checkbox) .gw-error { margin: 0; font-size: var(--gw-font-size-xs); color: var(--gw-color-danger); } :where(gw-switch, gw-checkbox) > label { display: inline-flex; gap: var(--gw-space-3); align-items: center; font-size: var(--gw-font-size-s); cursor: pointer; } :where(gw-input, gw-switch, gw-checkbox, gw-select) .gw-visually-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } gw-checkbox input { width: 1.125rem; height: 1.125rem; margin: 0; accent-color: var(--gw-color-fill-accent-resting); } gw-switch input { position: relative; flex: none; width: 2.5rem; height: 1.5rem; margin: 0; cursor: pointer; appearance: none; background: var(--gw-color-surface-lower); border: var(--gw-border-width) solid var(--gw-color-border-loud); border-radius: var(--gw-radius-pill); transition: background-color var(--gw-motion-normal) var(--gw-motion-easing), border-color var(--gw-motion-normal) var(--gw-motion-easing); } gw-switch input::after { position: absolute; inset-block-start: 0.1875rem; inset-inline-start: 0.1875rem; width: 1rem; height: 1rem; content: \"\"; background: var(--gw-color-text-secondary); border-radius: 50%; transition: translate var(--gw-motion-normal) var(--gw-motion-easing), background-color var(--gw-motion-normal) var(--gw-motion-easing); } gw-switch input:checked { background: var(--gw-color-fill-accent-resting); border-color: transparent; } gw-switch input:checked::after { translate: 1rem 0; background: var(--gw-color-on-accent); } gw-switch input:disabled { opacity: 0.5; } gw-range { display: flex; flex-direction: column; gap: var(--gw-space-2); min-width: 0; font-family: var(--gw-font-sans); color: var(--gw-color-text-primary); } gw-range > label { font-size: var(--gw-font-size-s); font-weight: var(--gw-font-weight-strong); color: var(--gw-color-text-strong); } gw-range .gw-range-row { display: flex; gap: var(--gw-space-3); align-items: center; } gw-range .gw-range-control { position: relative; display: block; flex: 1; min-width: 0; } /* The value beside the track, with readout */ gw-range .gw-range-readout { min-width: 5ch; font-size: var(--gw-font-size-s); font-variant-numeric: tabular-nums; color: var(--gw-color-text-secondary); text-align: end; white-space: nowrap; } gw-range .gw-range-readout[hidden] { display: none; } gw-range input { display: block; width: 100%; margin: 0; accent-color: var(--gw-color-fill-accent-resting); cursor: pointer; } gw-range input:focus-visible { outline: var(--gw-focus-width) solid var(--gw-color-focus); outline-offset: var(--gw-focus-offset); } gw-range input:disabled { cursor: not-allowed; opacity: 0.5; } /* The value above the thumb while it's held */ gw-range .gw-range-bubble { position: absolute; bottom: calc(100% + var(--gw-space-1)); padding: var(--gw-space-1) var(--gw-space-2); font-size: var(--gw-font-size-xs); font-variant-numeric: tabular-nums; color: var(--gw-color-on-accent); white-space: nowrap; pointer-events: none; background: var(--gw-color-fill-accent-resting); border-radius: var(--gw-radius-control); box-shadow: var(--gw-shadow-s); translate: -50% 0; } gw-range .gw-range-bubble[hidden] { display: none; } gw-range .gw-hint { margin: 0; font-size: var(--gw-font-size-xs); color: var(--gw-color-text-secondary); } gw-range .gw-visually-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } `",
633
633
  "description": "Field components render their native control in light DOM, never in a\nshadow root: password managers and `<label for>` can't reach across one,\nand a form only submits controls it can see. Their styles are adopted into\nwhichever document or shadow root they sit in."
634
634
  },
635
635
  {
@@ -3118,7 +3118,7 @@
3118
3118
  "text": "Properties"
3119
3119
  },
3120
3120
  "static": true,
3121
- "default": "{ variant: { reflect: true }, appearance: { reflect: true }, size: { reflect: true }, type: {}, href: {}, label: {}, disabled: { type: \"boolean\", reflect: true }, loading: { type: \"boolean\", reflect: true }, icon: {}, swatch: {}, labels: { reflect: true }, }",
3121
+ "default": "{ variant: { reflect: true }, appearance: { reflect: true }, size: { reflect: true }, type: {}, href: {}, label: {}, disabled: { type: \"boolean\", reflect: true }, loading: { type: \"boolean\", reflect: true }, icon: {}, swatch: {}, labels: { reflect: true }, toggle: { type: \"boolean\", reflect: true }, pressed: { type: \"boolean\", reflect: true }, }",
3122
3122
  "inheritedFrom": {
3123
3123
  "name": "GwElement",
3124
3124
  "module": "src/element.ts"
@@ -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; } /* 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; } } `, ]",
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, :host([toggle][pressed]:is([appearance=\"plain\"], [appearance=\"outlined\"])) .button { background: color-mix(in oklch, var(--gw-button-tone) 16%, transparent); } :host([toggle][pressed][appearance=\"filled\"]) .button, :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"
@@ -3243,6 +3243,24 @@
3243
3243
  "description": "Whether the text shows beside an icon: always, never, or not on narrow screens.",
3244
3244
  "default": "\"show\""
3245
3245
  },
3246
+ {
3247
+ "kind": "field",
3248
+ "name": "toggle",
3249
+ "type": {
3250
+ "text": "boolean"
3251
+ },
3252
+ "description": "Makes it a toggle button, announced as pressed or not.",
3253
+ "default": "false"
3254
+ },
3255
+ {
3256
+ "kind": "field",
3257
+ "name": "pressed",
3258
+ "type": {
3259
+ "text": "boolean"
3260
+ },
3261
+ "description": "Whether a toggle button is on.",
3262
+ "default": "false"
3263
+ },
3246
3264
  {
3247
3265
  "kind": "field",
3248
3266
  "name": "#icon",
@@ -3679,6 +3697,20 @@
3679
3697
  },
3680
3698
  "description": "Whether the text shows beside an icon: always, never, or not on narrow screens. Defaults to `show`.",
3681
3699
  "name": "labels"
3700
+ },
3701
+ {
3702
+ "type": {
3703
+ "text": "boolean"
3704
+ },
3705
+ "description": "Makes it a toggle button, which assistive technology announces as pressed or not.",
3706
+ "name": "toggle"
3707
+ },
3708
+ {
3709
+ "type": {
3710
+ "text": "boolean"
3711
+ },
3712
+ "description": "Whether a toggle button is on. It shows the same tint as a current link.",
3713
+ "name": "pressed"
3682
3714
  }
3683
3715
  ],
3684
3716
  "superclass": {
@@ -3761,7 +3793,7 @@
3761
3793
  "text": "Properties"
3762
3794
  },
3763
3795
  "static": true,
3764
- "default": "{ heading: {}, description: {}, level: { type: \"number\" }, href: {}, flush: { type: \"boolean\", reflect: true }, loading: { type: \"boolean\", reflect: true }, lines: { type: \"number\" }, }",
3796
+ "default": "{ heading: {}, description: {}, icon: {}, level: { type: \"number\" }, href: {}, flush: { type: \"boolean\", reflect: true }, loading: { type: \"boolean\", reflect: true }, lines: { type: \"number\" }, }",
3765
3797
  "inheritedFrom": {
3766
3798
  "name": "GwElement",
3767
3799
  "module": "src/element.ts"
@@ -3774,7 +3806,7 @@
3774
3806
  "text": "readonly CSSStyleSheet[]"
3775
3807
  },
3776
3808
  "static": true,
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; } `, ]",
3809
+ "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; } header > .icon { flex-shrink: 0; margin-inline-end: var(--gw-space-1); color: var(--gw-color-accent); } .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; } `, ]",
3778
3810
  "inheritedFrom": {
3779
3811
  "name": "GwElement",
3780
3812
  "module": "src/element.ts"
@@ -3796,6 +3828,15 @@
3796
3828
  },
3797
3829
  "default": "\"\""
3798
3830
  },
3831
+ {
3832
+ "kind": "field",
3833
+ "name": "icon",
3834
+ "type": {
3835
+ "text": "string"
3836
+ },
3837
+ "description": "SVG path data for an icon before the heading.",
3838
+ "default": "\"\""
3839
+ },
3799
3840
  {
3800
3841
  "kind": "field",
3801
3842
  "name": "level",
@@ -3868,6 +3909,12 @@
3868
3909
  "privacy": "private",
3869
3910
  "readonly": true
3870
3911
  },
3912
+ {
3913
+ "kind": "field",
3914
+ "name": "#icon",
3915
+ "privacy": "private",
3916
+ "readonly": true
3917
+ },
3871
3918
  {
3872
3919
  "kind": "field",
3873
3920
  "name": "#header",
@@ -3898,6 +3945,21 @@
3898
3945
  "privacy": "private",
3899
3946
  "readonly": true
3900
3947
  },
3948
+ {
3949
+ "kind": "field",
3950
+ "name": "#bodySlot",
3951
+ "privacy": "private",
3952
+ "readonly": true
3953
+ },
3954
+ {
3955
+ "kind": "field",
3956
+ "name": "#empty",
3957
+ "privacy": "private",
3958
+ "type": {
3959
+ "text": "boolean"
3960
+ },
3961
+ "default": "true"
3962
+ },
3901
3963
  {
3902
3964
  "kind": "field",
3903
3965
  "name": "#body",
@@ -4271,6 +4333,13 @@
4271
4333
  "description": "A line under the heading.",
4272
4334
  "name": "description"
4273
4335
  },
4336
+ {
4337
+ "type": {
4338
+ "text": "string"
4339
+ },
4340
+ "description": "SVG path data for an icon before the heading, such as on a card that links to a section.",
4341
+ "name": "icon"
4342
+ },
4274
4343
  {
4275
4344
  "type": {
4276
4345
  "text": "number"
@@ -8319,6 +8388,12 @@
8319
8388
  "name": "value"
8320
8389
  }
8321
8390
  ],
8391
+ "slots": [
8392
+ {
8393
+ "description": "`<dt>` and `<dd>` pairs, in place of `items`, for values with markup such as chips or code.",
8394
+ "name": ""
8395
+ }
8396
+ ],
8322
8397
  "members": [
8323
8398
  {
8324
8399
  "kind": "field",
@@ -8340,7 +8415,7 @@
8340
8415
  "text": "readonly CSSStyleSheet[]"
8341
8416
  },
8342
8417
  "static": true,
8343
- "default": "[ base, css` :host { display: block; } dl { display: grid; grid-template-columns: minmax(7rem, max-content) 1fr; gap: var(--gw-space-1) var(--gw-space-3); margin: 0; font-size: var(--gw-font-size-s); } dt { color: var(--gw-color-text-secondary); } dd { min-width: 0; margin: 0; font-variant-numeric: var(--gw-font-figures); color: var(--gw-color-text-strong); overflow-wrap: anywhere; } `, ]",
8418
+ "default": "[ base, css` :host { display: block; } dl { display: grid; grid-template-columns: minmax(7rem, max-content) 1fr; gap: var(--gw-space-1) var(--gw-space-3); margin: 0; font-size: var(--gw-font-size-s); } dt, ::slotted(dt) { color: var(--gw-color-text-secondary); } dd, ::slotted(dd) { min-width: 0; margin: 0; font-variant-numeric: var(--gw-font-figures); color: var(--gw-color-text-strong); overflow-wrap: anywhere; } `, ]",
8344
8419
  "inheritedFrom": {
8345
8420
  "name": "GwElement",
8346
8421
  "module": "src/element.ts"
@@ -9857,7 +9932,7 @@
9857
9932
  "text": "Properties"
9858
9933
  },
9859
9934
  "static": true,
9860
- "default": "{ label: {}, name: {}, type: {}, value: {}, placeholder: {}, autocomplete: {}, inputmode: {}, hint: {}, info: {}, error: {}, required: { type: \"boolean\" }, optional: { type: \"boolean\" }, disabled: { type: \"boolean\" }, multiline: { type: \"boolean\", reflect: true }, rows: { type: \"number\" }, monospace: { type: \"boolean\", reflect: true }, reveal: { type: \"boolean\" }, size: { reflect: true }, icon: {}, hideLabel: { type: \"boolean\", attribute: \"hide-label\", reflect: true }, }",
9935
+ "default": "{ label: {}, name: {}, type: {}, value: {}, placeholder: {}, autocomplete: {}, inputmode: {}, min: {}, max: {}, step: {}, hint: {}, info: {}, error: {}, required: { type: \"boolean\" }, optional: { type: \"boolean\" }, disabled: { type: \"boolean\" }, multiline: { type: \"boolean\", reflect: true }, rows: { type: \"number\" }, monospace: { type: \"boolean\", reflect: true }, reveal: { type: \"boolean\" }, size: { reflect: true }, icon: {}, labelHidden: { type: \"boolean\", attribute: \"label-hidden\", reflect: true }, }",
9861
9936
  "inheritedFrom": {
9862
9937
  "name": "GwElement",
9863
9938
  "module": "src/element.ts"
@@ -9920,6 +9995,33 @@
9920
9995
  "description": "The native inputmode hint, which picks the on-screen keyboard.",
9921
9996
  "default": "\"\""
9922
9997
  },
9998
+ {
9999
+ "kind": "field",
10000
+ "name": "min",
10001
+ "type": {
10002
+ "text": "string"
10003
+ },
10004
+ "description": "The lowest value of a number, date or time field.",
10005
+ "default": "\"\""
10006
+ },
10007
+ {
10008
+ "kind": "field",
10009
+ "name": "max",
10010
+ "type": {
10011
+ "text": "string"
10012
+ },
10013
+ "description": "The highest value of a number, date or time field.",
10014
+ "default": "\"\""
10015
+ },
10016
+ {
10017
+ "kind": "field",
10018
+ "name": "step",
10019
+ "type": {
10020
+ "text": "string"
10021
+ },
10022
+ "description": "The step between values of a number, date or time field.",
10023
+ "default": "\"\""
10024
+ },
9923
10025
  {
9924
10026
  "kind": "field",
9925
10027
  "name": "hint",
@@ -10025,7 +10127,7 @@
10025
10127
  },
10026
10128
  {
10027
10129
  "kind": "field",
10028
- "name": "hideLabel",
10130
+ "name": "labelHidden",
10029
10131
  "type": {
10030
10132
  "text": "boolean"
10031
10133
  },
@@ -10490,6 +10592,27 @@
10490
10592
  "description": "The native inputmode hint, such as `numeric` for a port, which picks the on-screen keyboard.",
10491
10593
  "name": "inputmode"
10492
10594
  },
10595
+ {
10596
+ "type": {
10597
+ "text": "string"
10598
+ },
10599
+ "description": "The lowest value of a `number`, date or time field.",
10600
+ "name": "min"
10601
+ },
10602
+ {
10603
+ "type": {
10604
+ "text": "string"
10605
+ },
10606
+ "description": "The highest value of a `number`, date or time field.",
10607
+ "name": "max"
10608
+ },
10609
+ {
10610
+ "type": {
10611
+ "text": "string"
10612
+ },
10613
+ "description": "The step between values of a `number`, date or time field, or `any`.",
10614
+ "name": "step"
10615
+ },
10493
10616
  {
10494
10617
  "type": {
10495
10618
  "text": "string"
@@ -10579,7 +10702,7 @@
10579
10702
  "text": "boolean"
10580
10703
  },
10581
10704
  "description": "Keeps the label as the field's accessible name but doesn't show it, for a field in a toolbar whose icon and placeholder say what it's for.",
10582
- "name": "hide-label"
10705
+ "name": "label-hidden"
10583
10706
  }
10584
10707
  ],
10585
10708
  "superclass": {
@@ -13721,7 +13844,7 @@
13721
13844
  "text": "Properties"
13722
13845
  },
13723
13846
  "static": true,
13724
- "default": "{ heading: {}, description: {}, icon: {}, }",
13847
+ "default": "{ heading: {}, description: {}, icon: {}, level: { type: \"number\", reflect: true }, }",
13725
13848
  "inheritedFrom": {
13726
13849
  "name": "GwElement",
13727
13850
  "module": "src/element.ts"
@@ -13734,7 +13857,7 @@
13734
13857
  "text": "readonly CSSStyleSheet[]"
13735
13858
  },
13736
13859
  "static": true,
13737
- "default": "[ base, css` :host { display: flex; gap: var(--gw-space-3); align-items: flex-start; } .start { display: flex; flex: none; align-items: center; min-height: 3rem; } .start[hidden] { display: none; } .tile { display: grid; flex: none; place-items: center; width: 3rem; height: 3rem; color: var(--gw-color-accent); background: var(--gw-color-fill-accent-quiet); border-radius: var(--gw-radius-l); --gw-icon-size: 1.625rem; } .titles { display: flex; flex: 0 1 auto; flex-direction: column; gap: var(--gw-space-0-5); min-width: 0; } h1 { margin: 0; font-size: var(--gw-font-size-2xl); font-weight: var(--gw-font-weight-strong); line-height: 1.2; color: var(--gw-color-text-strong); } p { margin: 0; font-size: var(--gw-font-size-s); color: var(--gw-color-text-secondary); } /* Takes the room the titles leave, and gives it up before them, down to its items' smallest */ .actions { display: flex; flex: 1 1000 auto; gap: var(--gw-space-1); align-items: center; justify-content: flex-end; } /* Centered on the tile when there is one */ .tile:not([hidden]) ~ .actions { min-height: 3rem; } `, ]",
13860
+ "default": "[ base, css` :host { display: flex; gap: var(--gw-space-3); align-items: flex-start; } .start { display: flex; flex: none; align-items: center; min-height: 3rem; } .start[hidden] { display: none; } .tile { display: grid; flex: none; place-items: center; width: 3rem; height: 3rem; color: var(--gw-color-accent); background: var(--gw-color-fill-accent-quiet); border-radius: var(--gw-radius-l); --gw-icon-size: 1.625rem; } .titles { display: flex; flex: 0 1 auto; flex-direction: column; gap: var(--gw-space-0-5); min-width: 0; } :host(:not([level=\"1\"])) .tile { width: 2.5rem; height: 2.5rem; border-radius: var(--gw-radius-m); --gw-icon-size: 1.375rem; } :host(:not([level=\"1\"])) .heading { font-size: var(--gw-font-size-l); } :host(:not([level=\"1\"])) .tile:not([hidden]) ~ .actions { min-height: 2.5rem; } .heading { margin: 0; font-size: var(--gw-font-size-2xl); font-weight: var(--gw-font-weight-strong); line-height: 1.2; color: var(--gw-color-text-strong); } p { margin: 0; font-size: var(--gw-font-size-s); color: var(--gw-color-text-secondary); } /* Takes the room the titles leave, and gives it up before them, down to its items' smallest */ .actions { display: flex; flex: 1 1000 auto; gap: var(--gw-space-1); align-items: center; justify-content: flex-end; } /* Centered on the tile when there is one */ .tile:not([hidden]) ~ .actions { min-height: 3rem; } `, ]",
13738
13861
  "inheritedFrom": {
13739
13862
  "name": "GwElement",
13740
13863
  "module": "src/element.ts"
@@ -13764,6 +13887,15 @@
13764
13887
  },
13765
13888
  "default": "\"\""
13766
13889
  },
13890
+ {
13891
+ "kind": "field",
13892
+ "name": "level",
13893
+ "type": {
13894
+ "text": "number"
13895
+ },
13896
+ "description": "The heading level, 1 to 6.",
13897
+ "default": "1"
13898
+ },
13767
13899
  {
13768
13900
  "kind": "field",
13769
13901
  "name": "#icon",
@@ -13792,7 +13924,9 @@
13792
13924
  "kind": "field",
13793
13925
  "name": "#heading",
13794
13926
  "privacy": "private",
13795
- "readonly": true
13927
+ "type": {
13928
+ "text": "HTMLHeadingElement"
13929
+ }
13796
13930
  },
13797
13931
  {
13798
13932
  "kind": "field",
@@ -14133,6 +14267,13 @@
14133
14267
  },
14134
14268
  "description": "SVG path data for the icon tile.",
14135
14269
  "name": "icon"
14270
+ },
14271
+ {
14272
+ "type": {
14273
+ "text": "number"
14274
+ },
14275
+ "description": "The heading level, 1 to 6. Defaults to 1. From 2 the header is smaller, for a section of a page that reads like a page of its own.",
14276
+ "name": "level"
14136
14277
  }
14137
14278
  ],
14138
14279
  "superclass": {
@@ -14750,7 +14891,7 @@
14750
14891
  "text": "Properties"
14751
14892
  },
14752
14893
  "static": true,
14753
- "default": "{ label: {}, labelHidden: { type: \"boolean\", attribute: \"label-hidden\" }, name: {}, value: { type: \"number\" }, min: { type: \"number\" }, max: { type: \"number\" }, step: { type: \"number\" }, unit: {}, hint: {}, disabled: { type: \"boolean\" }, }",
14894
+ "default": "{ label: {}, labelHidden: { type: \"boolean\", attribute: \"label-hidden\" }, name: {}, value: { type: \"number\" }, min: { type: \"number\" }, max: { type: \"number\" }, step: { type: \"number\" }, unit: {}, readout: { type: \"boolean\" }, hint: {}, disabled: { type: \"boolean\" }, }",
14754
14895
  "inheritedFrom": {
14755
14896
  "name": "GwElement",
14756
14897
  "module": "src/element.ts"
@@ -14822,6 +14963,15 @@
14822
14963
  "description": "Shown after the value, such as `%`.",
14823
14964
  "default": "\"\""
14824
14965
  },
14966
+ {
14967
+ "kind": "field",
14968
+ "name": "readout",
14969
+ "type": {
14970
+ "text": "boolean"
14971
+ },
14972
+ "description": "Shows the value beside the track.",
14973
+ "default": "false"
14974
+ },
14825
14975
  {
14826
14976
  "kind": "field",
14827
14977
  "name": "hint",
@@ -14862,6 +15012,12 @@
14862
15012
  "privacy": "private",
14863
15013
  "readonly": true
14864
15014
  },
15015
+ {
15016
+ "kind": "field",
15017
+ "name": "#readout",
15018
+ "privacy": "private",
15019
+ "readonly": true
15020
+ },
14865
15021
  {
14866
15022
  "kind": "field",
14867
15023
  "name": "#hint",
@@ -15276,9 +15432,16 @@
15276
15432
  "type": {
15277
15433
  "text": "string"
15278
15434
  },
15279
- "description": "Shown after the value in the bubble and for assistive technology, such as `%`. Empty for a plain number.",
15435
+ "description": "Shown after the value in the bubble, the readout and for assistive technology, such as `%`. Empty for a plain number.",
15280
15436
  "name": "unit"
15281
15437
  },
15438
+ {
15439
+ "type": {
15440
+ "text": "boolean"
15441
+ },
15442
+ "description": "Shows the value beside the track, kept up to date while it's dragged.",
15443
+ "name": "readout"
15444
+ },
15282
15445
  {
15283
15446
  "type": {
15284
15447
  "text": "string"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timmo001/groundwork-ui",
3
- "version": "0.9.24",
3
+ "version": "0.9.26",
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/fields.ts CHANGED
@@ -496,9 +496,31 @@ export const fieldStyles = css`
496
496
  color: var(--gw-color-text-strong);
497
497
  }
498
498
 
499
+ gw-range .gw-range-row {
500
+ display: flex;
501
+ gap: var(--gw-space-3);
502
+ align-items: center;
503
+ }
504
+
499
505
  gw-range .gw-range-control {
500
506
  position: relative;
501
507
  display: block;
508
+ flex: 1;
509
+ min-width: 0;
510
+ }
511
+
512
+ /* The value beside the track, with readout */
513
+ gw-range .gw-range-readout {
514
+ min-width: 5ch;
515
+ font-size: var(--gw-font-size-s);
516
+ font-variant-numeric: tabular-nums;
517
+ color: var(--gw-color-text-secondary);
518
+ text-align: end;
519
+ white-space: nowrap;
520
+ }
521
+
522
+ gw-range .gw-range-readout[hidden] {
523
+ display: none;
502
524
  }
503
525
 
504
526
  gw-range input {
package/src/gw-button.ts CHANGED
@@ -40,6 +40,8 @@ import { base, controlSizes, createIcon, setIconPath } from "./styles.js";
40
40
  * @attr {string} icon - SVG path data for an icon before the text.
41
41
  * @attr {string} swatch - A CSS color, drawn as a small round dot in the icon's place, such as a tag's own color.
42
42
  * @attr {"show"|"hide"|"auto"} labels - Whether the text shows beside an icon: always, never, or not on narrow screens. Defaults to `show`.
43
+ * @attr {boolean} toggle - Makes it a toggle button, which assistive technology announces as pressed or not.
44
+ * @attr {boolean} pressed - Whether a toggle button is on. It shows the same tint as a current link.
43
45
  *
44
46
  * @slot - The text, or an `<a>` holding it, in place of `href`.
45
47
  * @slot icon - An icon before the text, in place of `icon`.
@@ -64,6 +66,8 @@ export class GwButton extends GwElement {
64
66
  icon: {},
65
67
  swatch: {},
66
68
  labels: { reflect: true },
69
+ toggle: { type: "boolean", reflect: true },
70
+ pressed: { type: "boolean", reflect: true },
67
71
  } as const;
68
72
 
69
73
  /** Focusing the element focuses its native button, such as from a dialog opening. */
@@ -208,10 +212,13 @@ export class GwButton extends GwElement {
208
212
  }
209
213
 
210
214
  :host([appearance="plain"]) .button.current,
211
- :host([appearance="outlined"]) .button.current {
215
+ :host([appearance="outlined"]) .button.current,
216
+ :host([toggle][pressed]:is([appearance="plain"], [appearance="outlined"]))
217
+ .button {
212
218
  background: color-mix(in oklch, var(--gw-button-tone) 16%, transparent);
213
219
  }
214
220
 
221
+ :host([toggle][pressed][appearance="filled"]) .button,
215
222
  :host([appearance="filled"]) .button.current {
216
223
  background: color-mix(in oklch, var(--gw-button-tone) 32%, transparent);
217
224
  }
@@ -363,6 +370,10 @@ export class GwButton extends GwElement {
363
370
  declare swatch: string;
364
371
  /** Whether the text shows beside an icon: always, never, or not on narrow screens. */
365
372
  declare labels: "show" | "hide" | "auto";
373
+ /** Makes it a toggle button, announced as pressed or not. */
374
+ declare toggle: boolean;
375
+ /** Whether a toggle button is on. */
376
+ declare pressed: boolean;
366
377
 
367
378
  readonly #icon = createIcon("");
368
379
 
@@ -403,6 +414,8 @@ export class GwButton extends GwElement {
403
414
  this.icon = "";
404
415
  this.swatch = "";
405
416
  this.labels = "show";
417
+ this.toggle = false;
418
+ this.pressed = false;
406
419
  this.variant = "brand";
407
420
  this.appearance = "accent";
408
421
  this.size = "medium";
@@ -537,6 +550,11 @@ export class GwButton extends GwElement {
537
550
  } else if (control instanceof HTMLButtonElement) {
538
551
  control.type = this.type;
539
552
  control.disabled = this.disabled;
553
+ setAttribute(
554
+ control,
555
+ "aria-pressed",
556
+ this.toggle ? String(this.pressed) : null,
557
+ );
540
558
  }
541
559
  }
542
560
  }
package/src/gw-card.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  import { GwElement, css, h, setAttribute } from "./element.js";
8
8
  import "./gw-skeleton.js";
9
9
  import { animateHeight, fadeIn } from "./motion.js";
10
- import { base } from "./styles.js";
10
+ import { base, createIcon, setIconPath } from "./styles.js";
11
11
 
12
12
  /**
13
13
  * A surface for one group of content, with an optional heading and actions.
@@ -26,6 +26,7 @@ import { base } from "./styles.js";
26
26
  *
27
27
  * @attr {string} heading - The card's heading.
28
28
  * @attr {string} description - A line under the heading.
29
+ * @attr {string} icon - SVG path data for an icon before the heading, such as on a card that links to a section.
29
30
  * @attr {number} level - The heading's level for assistive tech, 2 by default. Use 3 for a card under a section heading.
30
31
  * @attr {string} href - Makes the whole card a link to this address.
31
32
  * @attr {boolean} flush - Drops the padding, for content that runs to the edges.
@@ -48,6 +49,7 @@ export class GwCard extends GwElement {
48
49
  static override properties = {
49
50
  heading: {},
50
51
  description: {},
52
+ icon: {},
51
53
  level: { type: "number" },
52
54
  href: {},
53
55
  flush: { type: "boolean", reflect: true },
@@ -117,6 +119,12 @@ export class GwCard extends GwElement {
117
119
  align-items: center;
118
120
  }
119
121
 
122
+ header > .icon {
123
+ flex-shrink: 0;
124
+ margin-inline-end: var(--gw-space-1);
125
+ color: var(--gw-color-accent);
126
+ }
127
+
120
128
  .titles {
121
129
  display: flex;
122
130
  flex: 1;
@@ -162,6 +170,8 @@ export class GwCard extends GwElement {
162
170
 
163
171
  declare heading: string;
164
172
  declare description: string;
173
+ /** SVG path data for an icon before the heading. */
174
+ declare icon: string;
165
175
  /** The heading's level for assistive tech. */
166
176
  declare level: number;
167
177
  declare href: string;
@@ -189,9 +199,12 @@ export class GwCard extends GwElement {
189
199
  part: "description",
190
200
  });
191
201
 
202
+ readonly #icon = createIcon("");
203
+
192
204
  readonly #header: HTMLElement = h(
193
205
  "header",
194
206
  { part: "header" },
207
+ this.#icon,
195
208
  h("div", { class: "titles" }, this.#heading, this.#description),
196
209
  h("slot", { name: "actions" }),
197
210
  );
@@ -206,10 +219,14 @@ export class GwCard extends GwElement {
206
219
 
207
220
  readonly #skeleton = h("gw-skeleton-lines");
208
221
 
222
+ readonly #bodySlot = h("slot");
223
+
224
+ #empty = true;
225
+
209
226
  readonly #body: HTMLDivElement = h(
210
227
  "div",
211
228
  { class: "body", part: "body" },
212
- h("slot"),
229
+ this.#bodySlot,
213
230
  );
214
231
 
215
232
  #card: HTMLElement = h("section", { class: "card", part: "card" });
@@ -222,6 +239,7 @@ export class GwCard extends GwElement {
222
239
  super();
223
240
  this.heading = "";
224
241
  this.description = "";
242
+ this.icon = "";
225
243
  this.level = 2;
226
244
  this.href = "";
227
245
  this.flush = false;
@@ -274,6 +292,17 @@ export class GwCard extends GwElement {
274
292
  this.#footer.hidden =
275
293
  this.#footerSlot.assignedNodes({ flatten: true }).length === 0;
276
294
  });
295
+
296
+ // A card with only a header, such as a link to a section, has no body gap
297
+ this.#bodySlot.addEventListener("slotchange", () => {
298
+ this.#empty = this.#bodySlot
299
+ .assignedNodes({ flatten: true })
300
+ .every(
301
+ (node) =>
302
+ node.nodeType === Node.TEXT_NODE && !node.textContent?.trim(),
303
+ );
304
+ this.requestUpdate("loading");
305
+ });
277
306
  }
278
307
 
279
308
  /** The `<a>` in the heading slot, or holding it, when there is one. */
@@ -322,14 +351,20 @@ export class GwCard extends GwElement {
322
351
  this.#heading.setAttribute("aria-level", String(this.level || 2));
323
352
  this.#description.textContent = this.description;
324
353
  this.#description.hidden = !this.description;
354
+ setIconPath(this.#icon, this.icon);
355
+ this.#icon.toggleAttribute("hidden", !this.icon);
325
356
  this.#titleSkeleton.hidden =
326
357
  this.heading !== "" || slotted || !this.loading;
327
358
  this.#header.hidden =
328
- !this.heading && !this.description && !slotted && !this.loading;
359
+ !this.heading &&
360
+ !this.description &&
361
+ !this.icon &&
362
+ !slotted &&
363
+ !this.loading;
329
364
  this.#heading.hidden = !this.heading && !slotted && !this.loading;
330
365
  this.#skeleton.lines = this.lines;
331
366
  this.#skeleton.hidden = !this.loading;
332
- this.#body.hidden = this.loading;
367
+ this.#body.hidden = this.loading || this.#empty;
333
368
 
334
369
  if (animate) {
335
370
  animateHeight(this, before);
package/src/gw-facts.ts CHANGED
@@ -29,6 +29,8 @@ const skeletonWidths = ["60%", "40%", "75%", "50%", "65%"];
29
29
  * @attr {boolean} loading - Shows skeleton pairs, keeping any labels already known.
30
30
  * @attr {number} rows - How many skeleton pairs while loading. Defaults to 4.
31
31
  *
32
+ * @slot - `<dt>` and `<dd>` pairs, in place of `items`, for values with markup such as chips or code.
33
+ *
32
34
  * @csspart list - The description list.
33
35
  * @csspart label - Each label.
34
36
  * @csspart value - Each value.
@@ -55,11 +57,13 @@ export class GwFacts extends GwElement {
55
57
  font-size: var(--gw-font-size-s);
56
58
  }
57
59
 
58
- dt {
60
+ dt,
61
+ ::slotted(dt) {
59
62
  color: var(--gw-color-text-secondary);
60
63
  }
61
64
 
62
- dd {
65
+ dd,
66
+ ::slotted(dd) {
63
67
  min-width: 0;
64
68
  margin: 0;
65
69
  font-variant-numeric: var(--gw-font-figures);
@@ -117,6 +121,8 @@ export class GwFacts extends GwElement {
117
121
  } else {
118
122
  this.#list.removeAttribute("aria-busy");
119
123
  this.#list.replaceChildren(
124
+ // The slot is display: contents, so slotted pairs join the grid
125
+ h("slot"),
120
126
  ...items.flatMap((item) => [
121
127
  h("dt", { part: "label" }, item.label),
122
128
  h(
package/src/gw-input.ts CHANGED
@@ -31,6 +31,9 @@ const hideIcon =
31
31
  * @attr {string} placeholder - Placeholder text.
32
32
  * @attr {string} autocomplete - The native autocomplete hint.
33
33
  * @attr {string} inputmode - The native inputmode hint, such as `numeric` for a port, which picks the on-screen keyboard.
34
+ * @attr {string} min - The lowest value of a `number`, date or time field.
35
+ * @attr {string} max - The highest value of a `number`, date or time field.
36
+ * @attr {string} step - The step between values of a `number`, date or time field, or `any`.
34
37
  * @attr {string} hint - One line of help under the field.
35
38
  * @attr {string} info - Longer help, opened from an (i) button by the label.
36
39
  * @attr {string} error - An error under the field, which marks it invalid and is announced.
@@ -43,7 +46,7 @@ const hideIcon =
43
46
  * @attr {boolean} reveal - On a password field, adds a button at the end to show and hide what's typed.
44
47
  * @attr {"xs"|"small"|"medium"|"large"|"xl"} size - The field's size, 40px tall at medium. Defaults to `medium`.
45
48
  * @attr {string} icon - SVG path data for an icon inside the field, before the text, such as a magnifier on a search.
46
- * @attr {boolean} hide-label - Keeps the label as the field's accessible name but doesn't show it, for a field in a toolbar whose icon and placeholder say what it's for.
49
+ * @attr {boolean} label-hidden - Keeps the label as the field's accessible name but doesn't show it, for a field in a toolbar whose icon and placeholder say what it's for.
47
50
  *
48
51
  * @cssprop --gw-field-scale - Scales the field, around 1 for medium.
49
52
  * @cssprop --gw-control-scale - Scales its height with the theme's density. The theme sets it.
@@ -57,6 +60,9 @@ export class GwInput extends LightElement {
57
60
  placeholder: {},
58
61
  autocomplete: {},
59
62
  inputmode: {},
63
+ min: {},
64
+ max: {},
65
+ step: {},
60
66
  hint: {},
61
67
  info: {},
62
68
  error: {},
@@ -69,7 +75,7 @@ export class GwInput extends LightElement {
69
75
  reveal: { type: "boolean" },
70
76
  size: { reflect: true },
71
77
  icon: {},
72
- hideLabel: { type: "boolean", attribute: "hide-label", reflect: true },
78
+ labelHidden: { type: "boolean", attribute: "label-hidden", reflect: true },
73
79
  } as const;
74
80
 
75
81
  declare label: string;
@@ -80,6 +86,12 @@ export class GwInput extends LightElement {
80
86
  declare autocomplete: string;
81
87
  /** The native inputmode hint, which picks the on-screen keyboard. */
82
88
  declare inputmode: string;
89
+ /** The lowest value of a number, date or time field. */
90
+ declare min: string;
91
+ /** The highest value of a number, date or time field. */
92
+ declare max: string;
93
+ /** The step between values of a number, date or time field. */
94
+ declare step: string;
83
95
  declare hint: string;
84
96
  /** Longer help, opened from an (i) button by the label. */
85
97
  declare info: string;
@@ -100,7 +112,7 @@ export class GwInput extends LightElement {
100
112
  /** SVG path data for an icon inside the field, before the text. */
101
113
  declare icon: string;
102
114
  /** Keeps the label as the accessible name without showing it. */
103
- declare hideLabel: boolean;
115
+ declare labelHidden: boolean;
104
116
 
105
117
  readonly #id = fieldId();
106
118
 
@@ -157,6 +169,9 @@ export class GwInput extends LightElement {
157
169
  this.placeholder = "";
158
170
  this.autocomplete = "";
159
171
  this.inputmode = "";
172
+ this.min = "";
173
+ this.max = "";
174
+ this.step = "";
160
175
  this.hint = "";
161
176
  this.info = "";
162
177
  this.error = "";
@@ -169,7 +184,7 @@ export class GwInput extends LightElement {
169
184
  this.reveal = false;
170
185
  this.size = "medium";
171
186
  this.icon = "";
172
- this.hideLabel = false;
187
+ this.labelHidden = false;
173
188
  }
174
189
 
175
190
  protected override create(root: HTMLElement) {
@@ -244,7 +259,7 @@ export class GwInput extends LightElement {
244
259
  this.#label.replaceChildren(this.label, this.#optional);
245
260
  this.#optional.hidden = !this.optional;
246
261
  this.#info.update(this.label, this.info);
247
- this.#info.heading.classList.toggle("gw-visually-hidden", this.hideLabel);
262
+ this.#info.heading.classList.toggle("gw-visually-hidden", this.labelHidden);
248
263
  setIconPath(this.#icon, this.icon);
249
264
  this.#icon.toggleAttribute("hidden", !this.icon);
250
265
  this.#input.type = reveal && this.#revealed ? "text" : this.type;
@@ -253,6 +268,9 @@ export class GwInput extends LightElement {
253
268
  setAttribute(control, "placeholder", this.placeholder || null);
254
269
  setAttribute(control, "autocomplete", this.autocomplete || null);
255
270
  setAttribute(control, "inputmode", this.inputmode || null);
271
+ setAttribute(this.#input, "min", this.min || null);
272
+ setAttribute(this.#input, "max", this.max || null);
273
+ setAttribute(this.#input, "step", this.step || null);
256
274
  setAttribute(control, "spellcheck", this.monospace ? "false" : null);
257
275
  setAttribute(control, "aria-invalid", this.error ? "true" : null);
258
276
  setAttribute(control, "aria-describedby", described || null);
@@ -7,6 +7,8 @@
7
7
  import { GwElement, css, h } from "./element.js";
8
8
  import { base, createIcon, setIconPath } from "./styles.js";
9
9
 
10
+ const headingTags = ["h1", "h2", "h3", "h4", "h5", "h6"] as const;
11
+
10
12
  /**
11
13
  * A page's title, with an optional icon tile and description, a back button
12
14
  * or similar in the `start` slot, and its controls at the top right in the
@@ -18,6 +20,7 @@ import { base, createIcon, setIconPath } from "./styles.js";
18
20
  * @attr {string} heading - The page's title.
19
21
  * @attr {string} description - A line under the title.
20
22
  * @attr {string} icon - SVG path data for the icon tile.
23
+ * @attr {number} level - The heading level, 1 to 6. Defaults to 1. From 2 the header is smaller, for a section of a page that reads like a page of its own.
21
24
  *
22
25
  * @slot start - Before the icon tile, such as a back button.
23
26
  * @slot actions - The page's controls.
@@ -31,6 +34,7 @@ export class GwPageHeader extends GwElement {
31
34
  heading: {},
32
35
  description: {},
33
36
  icon: {},
37
+ level: { type: "number", reflect: true },
34
38
  } as const;
35
39
 
36
40
  static override styles = [
@@ -73,7 +77,22 @@ export class GwPageHeader extends GwElement {
73
77
  min-width: 0;
74
78
  }
75
79
 
76
- h1 {
80
+ :host(:not([level="1"])) .tile {
81
+ width: 2.5rem;
82
+ height: 2.5rem;
83
+ border-radius: var(--gw-radius-m);
84
+ --gw-icon-size: 1.375rem;
85
+ }
86
+
87
+ :host(:not([level="1"])) .heading {
88
+ font-size: var(--gw-font-size-l);
89
+ }
90
+
91
+ :host(:not([level="1"])) .tile:not([hidden]) ~ .actions {
92
+ min-height: 2.5rem;
93
+ }
94
+
95
+ .heading {
77
96
  margin: 0;
78
97
  font-size: var(--gw-font-size-2xl);
79
98
  font-weight: var(--gw-font-weight-strong);
@@ -107,6 +126,8 @@ export class GwPageHeader extends GwElement {
107
126
  declare heading: string;
108
127
  declare description: string;
109
128
  declare icon: string;
129
+ /** The heading level, 1 to 6. */
130
+ declare level: number;
110
131
 
111
132
  readonly #icon = createIcon("");
112
133
 
@@ -117,7 +138,7 @@ export class GwPageHeader extends GwElement {
117
138
 
118
139
  readonly #tile = h("span", { class: "tile", part: "tile" }, this.#icon);
119
140
 
120
- readonly #heading = h("h1", { part: "heading" });
141
+ #heading: HTMLHeadingElement = h("h1", { class: "heading", part: "heading" });
121
142
 
122
143
  readonly #description = h("p", { part: "description" });
123
144
 
@@ -126,6 +147,7 @@ export class GwPageHeader extends GwElement {
126
147
  this.heading = "";
127
148
  this.description = "";
128
149
  this.icon = "";
150
+ this.level = 1;
129
151
  }
130
152
 
131
153
  protected override create(root: ShadowRoot) {
@@ -141,6 +163,19 @@ export class GwPageHeader extends GwElement {
141
163
  }
142
164
 
143
165
  protected override update() {
166
+ const level = Math.min(6, Math.max(1, Math.round(this.level) || 1));
167
+ const tag = headingTags[level - 1] ?? "h1";
168
+
169
+ if (this.#heading.localName !== tag) {
170
+ const next = h(tag, {
171
+ class: "heading",
172
+ part: "heading",
173
+ });
174
+
175
+ this.#heading.replaceWith(next);
176
+ this.#heading = next;
177
+ }
178
+
144
179
  setIconPath(this.#icon, this.icon);
145
180
  this.#tile.hidden = !this.icon;
146
181
  this.#heading.textContent = this.heading;
package/src/gw-range.ts CHANGED
@@ -23,7 +23,8 @@ import { LightElement, fieldId } from "./fields.js";
23
23
  * @attr {number} min - The lowest value. Defaults to 0.
24
24
  * @attr {number} max - The highest value. Defaults to 100.
25
25
  * @attr {number} step - The step between values. Defaults to 1.
26
- * @attr {string} unit - Shown after the value in the bubble and for assistive technology, such as `%`. Empty for a plain number.
26
+ * @attr {string} unit - Shown after the value in the bubble, the readout and for assistive technology, such as `%`. Empty for a plain number.
27
+ * @attr {boolean} readout - Shows the value beside the track, kept up to date while it's dragged.
27
28
  * @attr {string} hint - One line of help under it.
28
29
  * @attr {boolean} disabled - Disables it.
29
30
  *
@@ -39,6 +40,7 @@ export class GwRange extends LightElement {
39
40
  max: { type: "number" },
40
41
  step: { type: "number" },
41
42
  unit: {},
43
+ readout: { type: "boolean" },
42
44
  hint: {},
43
45
  disabled: { type: "boolean" },
44
46
  } as const;
@@ -53,6 +55,8 @@ export class GwRange extends LightElement {
53
55
  declare step: number;
54
56
  /** Shown after the value, such as `%`. */
55
57
  declare unit: string;
58
+ /** Shows the value beside the track. */
59
+ declare readout: boolean;
56
60
  declare hint: string;
57
61
  declare disabled: boolean;
58
62
 
@@ -68,6 +72,12 @@ export class GwRange extends LightElement {
68
72
  hidden: "",
69
73
  });
70
74
 
75
+ readonly #readout = h("output", {
76
+ class: "gw-range-readout",
77
+ for: this.#id,
78
+ "aria-hidden": "true",
79
+ });
80
+
71
81
  readonly #hint = h("p", { class: "gw-hint", id: `${this.#id}-hint` });
72
82
 
73
83
  constructor() {
@@ -80,6 +90,7 @@ export class GwRange extends LightElement {
80
90
  this.max = 100;
81
91
  this.step = 1;
82
92
  this.unit = "";
93
+ this.readout = false;
83
94
  this.hint = "";
84
95
  this.disabled = false;
85
96
  }
@@ -118,10 +129,16 @@ export class GwRange extends LightElement {
118
129
  this.#input.addEventListener("input", () => {
119
130
  this.value = this.#input.valueAsNumber;
120
131
  this.#placeBubble();
132
+ this.#readout.textContent = this.#text(this.value);
121
133
  });
122
134
  root.append(
123
135
  this.#label,
124
- h("span", { class: "gw-range-control" }, this.#input, this.#bubble),
136
+ h(
137
+ "span",
138
+ { class: "gw-range-row" },
139
+ h("span", { class: "gw-range-control" }, this.#input, this.#bubble),
140
+ this.#readout,
141
+ ),
125
142
  this.#hint,
126
143
  );
127
144
  }
@@ -150,6 +167,8 @@ export class GwRange extends LightElement {
150
167
  this.unit ? this.#text(this.value) : null,
151
168
  );
152
169
  setAttribute(input, "aria-describedby", this.hint ? this.#hint.id : null);
170
+ this.#readout.textContent = this.#text(this.value);
171
+ this.#readout.hidden = !this.readout;
153
172
  this.#hint.textContent = this.hint;
154
173
  this.#hint.hidden = !this.hint;
155
174
  }