@timmo001/groundwork-ui 0.9.28 → 0.9.30

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.
@@ -77,12 +77,43 @@
77
77
  "privacy": "private",
78
78
  "readonly": true
79
79
  },
80
+ {
81
+ "kind": "field",
82
+ "name": "#question",
83
+ "privacy": "private",
84
+ "readonly": true
85
+ },
80
86
  {
81
87
  "kind": "field",
82
88
  "name": "#view",
83
89
  "privacy": "private",
84
90
  "readonly": true
85
91
  },
92
+ {
93
+ "kind": "method",
94
+ "name": "text",
95
+ "parameters": [
96
+ {
97
+ "name": "question",
98
+ "type": {
99
+ "text": "string"
100
+ }
101
+ },
102
+ {
103
+ "name": "keep",
104
+ "type": {
105
+ "text": "string"
106
+ }
107
+ },
108
+ {
109
+ "name": "discard",
110
+ "type": {
111
+ "text": "string"
112
+ }
113
+ }
114
+ ],
115
+ "description": "Sets the question and its buttons' text, for an app in another language."
116
+ },
86
117
  {
87
118
  "kind": "field",
88
119
  "name": "asking",
@@ -629,7 +660,7 @@
629
660
  {
630
661
  "kind": "variable",
631
662
  "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-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; } `",
663
+ "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; /* Pill-shaped controls would make a tall box a stadium, so it rounds no more than a panel */ border-radius: min(var(--gw-radius-control), var(--gw-radius-panel)); 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
664
  "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
665
  },
635
666
  {
@@ -1076,7 +1107,7 @@
1076
1107
  "text": "readonly CSSStyleSheet[]"
1077
1108
  },
1078
1109
  "static": true,
1079
- "default": "[ base, css` :host { display: block; } .alert { display: flex; gap: var(--gw-space-2); align-items: flex-start; padding: var(--gw-space-2) var(--gw-space-3); font-size: var(--gw-font-size-s); color: var(--gw-color-text-primary); background: var(--gw-tone-fill, var(--gw-color-fill-info-quiet)); border: var(--gw-border-width) solid var(--gw-tone-border, var(--gw-color-border-info)); border-inline-start-width: calc(var(--gw-border-width) * 4); border-radius: var(--gw-radius-control); } ::slotted(p) { margin: 0; } ::slotted([slot=\"actions\"]) { align-self: center; } ::slotted([slot=\"icon\"]), .icon { flex: none; color: var(--gw-tone, var(--gw-color-info)); } .body { flex: 1; min-width: 0; } strong { display: block; font-weight: var(--gw-font-weight-strong); color: var(--gw-color-text-strong); } `, ]",
1110
+ "default": "[ base, css` :host { display: block; } .alert { display: flex; gap: var(--gw-space-2); align-items: flex-start; padding: var(--gw-space-2) var(--gw-space-3); font-size: var(--gw-font-size-s); color: var(--gw-color-text-primary); background: var(--gw-tone-fill, var(--gw-color-fill-info-quiet)); border: var(--gw-border-width) solid var(--gw-tone-border, var(--gw-color-border-info)); border-inline-start-width: calc(var(--gw-border-width) * 4); border-radius: min(var(--gw-radius-control), var(--gw-radius-panel)); } ::slotted(p) { margin: 0; } ::slotted([slot=\"actions\"]) { align-self: center; } ::slotted([slot=\"icon\"]), .icon { flex: none; color: var(--gw-tone, var(--gw-color-info)); } .body { flex: 1; min-width: 0; } strong { display: block; font-weight: var(--gw-font-weight-strong); color: var(--gw-color-text-strong); } `, ]",
1080
1111
  "inheritedFrom": {
1081
1112
  "name": "GwElement",
1082
1113
  "module": "src/element.ts"
@@ -3805,7 +3836,7 @@
3805
3836
  "text": "Properties"
3806
3837
  },
3807
3838
  "static": true,
3808
- "default": "{ heading: {}, description: {}, icon: {}, level: { type: \"number\" }, href: {}, flush: { type: \"boolean\", reflect: true }, loading: { type: \"boolean\", reflect: true }, lines: { type: \"number\" }, }",
3839
+ "default": "{ heading: {}, description: {}, icon: {}, level: { type: \"number\" }, href: {}, titleLink: { attribute: \"title-link\", type: \"boolean\", reflect: true }, flush: { type: \"boolean\", reflect: true }, loading: { type: \"boolean\", reflect: true }, lines: { type: \"number\" }, }",
3809
3840
  "inheritedFrom": {
3810
3841
  "name": "GwElement",
3811
3842
  "module": "src/element.ts"
@@ -3818,7 +3849,7 @@
3818
3849
  "text": "readonly CSSStyleSheet[]"
3819
3850
  },
3820
3851
  "static": true,
3821
- "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; } `, ]",
3852
+ "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; } /* With title-link, only the heading is the link */ .title a { color: inherit; text-decoration: none; border-radius: var(--gw-radius-chip); } .title a[href]:hover, :host([title-link]) ::slotted(a[slot=\"heading\"]:hover) { color: var(--gw-color-accent); text-decoration: underline; } .title a[href]:focus-visible, :host([title-link]) ::slotted(a[slot=\"heading\"]:focus-visible) { outline: var(--gw-focus-width) solid var(--gw-color-focus); outline-offset: var(--gw-focus-offset); } 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; } `, ]",
3822
3853
  "inheritedFrom": {
3823
3854
  "name": "GwElement",
3824
3855
  "module": "src/element.ts"
@@ -3866,6 +3897,15 @@
3866
3897
  },
3867
3898
  "default": "\"\""
3868
3899
  },
3900
+ {
3901
+ "kind": "field",
3902
+ "name": "titleLink",
3903
+ "type": {
3904
+ "text": "boolean"
3905
+ },
3906
+ "description": "Makes only the heading the link, rather than the whole card.",
3907
+ "default": "false"
3908
+ },
3869
3909
  {
3870
3910
  "kind": "field",
3871
3911
  "name": "flush",
@@ -3897,6 +3937,12 @@
3897
3937
  "privacy": "private",
3898
3938
  "readonly": true
3899
3939
  },
3940
+ {
3941
+ "kind": "field",
3942
+ "name": "#titleAnchor",
3943
+ "privacy": "private",
3944
+ "readonly": true
3945
+ },
3900
3946
  {
3901
3947
  "kind": "field",
3902
3948
  "name": "#titleSkeleton",
@@ -4366,6 +4412,13 @@
4366
4412
  "description": "Makes the whole card a link to this address.",
4367
4413
  "name": "href"
4368
4414
  },
4415
+ {
4416
+ "type": {
4417
+ "text": "boolean"
4418
+ },
4419
+ "description": "Makes only the heading the link, from `href` or an `<a>` in the `heading` slot, so the rest of the card stays plain.",
4420
+ "name": "title-link"
4421
+ },
4369
4422
  {
4370
4423
  "type": {
4371
4424
  "text": "boolean"
@@ -6037,7 +6090,7 @@
6037
6090
  "text": "Properties"
6038
6091
  },
6039
6092
  "static": true,
6040
- "default": "{ options: { type: \"json\" }, label: {}, placeholder: {}, shortcut: {}, open: { type: \"boolean\", reflect: true }, }",
6093
+ "default": "{ options: { type: \"json\" }, value: {}, label: {}, placeholder: {}, shortcut: {}, open: { type: \"boolean\", reflect: true }, }",
6041
6094
  "inheritedFrom": {
6042
6095
  "name": "GwElement",
6043
6096
  "module": "src/element.ts"
@@ -6064,6 +6117,15 @@
6064
6117
  },
6065
6118
  "default": "[]"
6066
6119
  },
6120
+ {
6121
+ "kind": "field",
6122
+ "name": "value",
6123
+ "type": {
6124
+ "text": "string"
6125
+ },
6126
+ "description": "The current command, marked and selected when the menu opens.",
6127
+ "default": "\"\""
6128
+ },
6067
6129
  {
6068
6130
  "kind": "field",
6069
6131
  "name": "label",
@@ -6455,6 +6517,13 @@
6455
6517
  "description": "The commands, as JSON.",
6456
6518
  "name": "options"
6457
6519
  },
6520
+ {
6521
+ "type": {
6522
+ "text": "string"
6523
+ },
6524
+ "description": "The current command, such as the page you're on. It's marked, and selected and scrolled into view each time the menu opens.",
6525
+ "name": "value"
6526
+ },
6458
6527
  {
6459
6528
  "type": {
6460
6529
  "text": "string"
@@ -6552,7 +6621,7 @@
6552
6621
  "text": "Properties"
6553
6622
  },
6554
6623
  "static": true,
6555
- "default": "{ heading: {}, open: { type: \"boolean\", reflect: true }, closeLabel: { attribute: \"close-label\" }, }",
6624
+ "default": "{ heading: {}, open: { type: \"boolean\", reflect: true }, closeLabel: { attribute: \"close-label\" }, discardQuestion: { attribute: \"discard-question\" }, keepLabel: { attribute: \"keep-label\" }, discardLabel: { attribute: \"discard-label\" }, }",
6556
6625
  "inheritedFrom": {
6557
6626
  "name": "GwElement",
6558
6627
  "module": "src/element.ts"
@@ -6596,6 +6665,30 @@
6596
6665
  "description": "The close button's name, for an app in another language.",
6597
6666
  "default": "\"Close\""
6598
6667
  },
6668
+ {
6669
+ "kind": "field",
6670
+ "name": "discardQuestion",
6671
+ "type": {
6672
+ "text": "string"
6673
+ },
6674
+ "default": "\"Discard your changes?\""
6675
+ },
6676
+ {
6677
+ "kind": "field",
6678
+ "name": "keepLabel",
6679
+ "type": {
6680
+ "text": "string"
6681
+ },
6682
+ "default": "\"Keep editing\""
6683
+ },
6684
+ {
6685
+ "kind": "field",
6686
+ "name": "discardLabel",
6687
+ "type": {
6688
+ "text": "string"
6689
+ },
6690
+ "default": "\"Discard\""
6691
+ },
6599
6692
  {
6600
6693
  "kind": "field",
6601
6694
  "name": "returnValue",
@@ -7021,6 +7114,27 @@
7021
7114
  },
7022
7115
  "description": "The close button's name, for an app in another language. Defaults to `Close`.",
7023
7116
  "name": "close-label"
7117
+ },
7118
+ {
7119
+ "type": {
7120
+ "text": "string"
7121
+ },
7122
+ "description": "The question asked before closing with edits, for an app in another language. Defaults to `Discard your changes?`.",
7123
+ "name": "discard-question"
7124
+ },
7125
+ {
7126
+ "type": {
7127
+ "text": "string"
7128
+ },
7129
+ "description": "The button that goes back to editing. Defaults to `Keep editing`.",
7130
+ "name": "keep-label"
7131
+ },
7132
+ {
7133
+ "type": {
7134
+ "text": "string"
7135
+ },
7136
+ "description": "The button that throws the edits away. Defaults to `Discard`.",
7137
+ "name": "discard-label"
7024
7138
  }
7025
7139
  ],
7026
7140
  "superclass": {
@@ -9944,7 +10058,7 @@
9944
10058
  "text": "Properties"
9945
10059
  },
9946
10060
  "static": true,
9947
- "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 }, }",
10061
+ "default": "{ label: {}, name: {}, type: {}, value: {}, placeholder: {}, autocomplete: {}, inputmode: {}, min: {}, max: {}, step: {}, maxlength: { type: \"number\" }, 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 }, }",
9948
10062
  "inheritedFrom": {
9949
10063
  "name": "GwElement",
9950
10064
  "module": "src/element.ts"
@@ -10034,6 +10148,15 @@
10034
10148
  "description": "The step between values of a number, date or time field.",
10035
10149
  "default": "\"\""
10036
10150
  },
10151
+ {
10152
+ "kind": "field",
10153
+ "name": "maxlength",
10154
+ "type": {
10155
+ "text": "number"
10156
+ },
10157
+ "description": "The most characters it takes, or 0 for no limit.",
10158
+ "default": "0"
10159
+ },
10037
10160
  {
10038
10161
  "kind": "field",
10039
10162
  "name": "hint",
@@ -10625,6 +10748,13 @@
10625
10748
  "description": "The step between values of a `number`, date or time field, or `any`.",
10626
10749
  "name": "step"
10627
10750
  },
10751
+ {
10752
+ "type": {
10753
+ "text": "number"
10754
+ },
10755
+ "description": "The most characters it takes.",
10756
+ "name": "maxlength"
10757
+ },
10628
10758
  {
10629
10759
  "type": {
10630
10760
  "text": "string"
@@ -12609,7 +12739,7 @@
12609
12739
  "text": "Properties"
12610
12740
  },
12611
12741
  "static": true,
12612
- "default": "{ heading: {}, icon: {}, tone: { reflect: true }, time: {}, unread: { type: \"boolean\", reflect: true }, }",
12742
+ "default": "{ heading: {}, icon: {}, tone: { reflect: true }, time: {}, unread: { type: \"boolean\", reflect: true }, dismissLabel: { attribute: \"dismiss-label\" }, }",
12613
12743
  "inheritedFrom": {
12614
12744
  "name": "GwElement",
12615
12745
  "module": "src/element.ts"
@@ -12622,7 +12752,7 @@
12622
12752
  "text": "readonly CSSStyleSheet[]"
12623
12753
  },
12624
12754
  "static": true,
12625
- "default": "[ base, css` :host { display: block; } .card { display: grid; grid-template-columns: auto 1fr auto; gap: var(--gw-space-1) var(--gw-space-2); align-items: start; padding: var(--gw-space-3); font-size: var(--gw-font-size-s); color: var(--gw-color-text-primary); background: var(--gw-color-surface-raised); border: var(--gw-border-width) solid var(--gw-color-border-quiet); border-radius: var(--gw-radius-control); } /* An inset stripe, so unread and read cards line up */ :host([unread]) .card { box-shadow: inset calc(var(--gw-border-width) * 4) 0 0 var(--gw-tone, var(--gw-color-accent)); } :host([unread]:dir(rtl)) .card { box-shadow: inset calc(var(--gw-border-width) * -4) 0 0 var(--gw-tone, var(--gw-color-accent)); } .card > .icon { margin-block-start: 0.1em; color: var(--gw-tone, var(--gw-color-text-secondary)); } .card > .icon[hidden] { display: none; } .body { display: grid; gap: var(--gw-space-1); min-width: 0; } strong { font-weight: var(--gw-font-weight-strong); color: var(--gw-color-text-strong); } time { font-size: var(--gw-font-size-xs); color: var(--gw-color-text-secondary); } time:empty { display: none; } ::slotted(p) { margin: 0; } .actions { display: flex; flex-wrap: wrap; gap: var(--gw-space-1); margin-block-start: var(--gw-space-1); } .close { margin: calc(var(--gw-space-1) * -1); } `, ]",
12755
+ "default": "[ base, css` :host { display: block; } .card { display: grid; grid-template-columns: auto 1fr auto; gap: var(--gw-space-1) var(--gw-space-2); align-items: start; padding: var(--gw-space-3); font-size: var(--gw-font-size-s); color: var(--gw-color-text-primary); background: var(--gw-color-surface-raised); border: var(--gw-border-width) solid var(--gw-color-border-quiet); border-radius: min(var(--gw-radius-control), var(--gw-radius-panel)); } /* An inset stripe, so unread and read cards line up */ :host([unread]) .card { box-shadow: inset calc(var(--gw-border-width) * 4) 0 0 var(--gw-tone, var(--gw-color-accent)); } :host([unread]:dir(rtl)) .card { box-shadow: inset calc(var(--gw-border-width) * -4) 0 0 var(--gw-tone, var(--gw-color-accent)); } .card > .icon { margin-block-start: 0.1em; color: var(--gw-tone, var(--gw-color-text-secondary)); } .card > .icon[hidden] { display: none; } .body { display: grid; gap: var(--gw-space-1); min-width: 0; } strong { font-weight: var(--gw-font-weight-strong); color: var(--gw-color-text-strong); } time { font-size: var(--gw-font-size-xs); color: var(--gw-color-text-secondary); } time:empty { display: none; } ::slotted(p) { margin: 0; } .actions { display: flex; flex-wrap: wrap; gap: var(--gw-space-1); margin-block-start: var(--gw-space-1); } .close { margin: calc(var(--gw-space-1) * -1); } `, ]",
12626
12756
  "inheritedFrom": {
12627
12757
  "name": "GwElement",
12628
12758
  "module": "src/element.ts"
@@ -12668,6 +12798,14 @@
12668
12798
  },
12669
12799
  "default": "false"
12670
12800
  },
12801
+ {
12802
+ "kind": "field",
12803
+ "name": "dismissLabel",
12804
+ "type": {
12805
+ "text": "string"
12806
+ },
12807
+ "default": "\"Dismiss\""
12808
+ },
12671
12809
  {
12672
12810
  "kind": "field",
12673
12811
  "name": "#icon",
@@ -13053,6 +13191,13 @@
13053
13191
  },
13054
13192
  "description": "Marks it with a stripe until it's seen.",
13055
13193
  "name": "unread"
13194
+ },
13195
+ {
13196
+ "type": {
13197
+ "text": "string"
13198
+ },
13199
+ "description": "The close button's name, for an app in another language. Defaults to `Dismiss`.",
13200
+ "name": "dismiss-label"
13056
13201
  }
13057
13202
  ],
13058
13203
  "superclass": {
@@ -14358,7 +14503,7 @@
14358
14503
  "text": "Properties"
14359
14504
  },
14360
14505
  "static": true,
14361
- "default": "{ heading: {}, confirm: { type: \"boolean\" }, closeLabel: { attribute: \"close-label\" }, }",
14506
+ "default": "{ heading: {}, confirm: { type: \"boolean\" }, closeLabel: { attribute: \"close-label\" }, discardQuestion: { attribute: \"discard-question\" }, keepLabel: { attribute: \"keep-label\" }, discardLabel: { attribute: \"discard-label\" }, }",
14362
14507
  "inheritedFrom": {
14363
14508
  "name": "GwElement",
14364
14509
  "module": "src/element.ts"
@@ -14402,6 +14547,30 @@
14402
14547
  "description": "The close button's name, for an app in another language.",
14403
14548
  "default": "\"Close\""
14404
14549
  },
14550
+ {
14551
+ "kind": "field",
14552
+ "name": "discardQuestion",
14553
+ "type": {
14554
+ "text": "string"
14555
+ },
14556
+ "default": "\"Discard your changes?\""
14557
+ },
14558
+ {
14559
+ "kind": "field",
14560
+ "name": "keepLabel",
14561
+ "type": {
14562
+ "text": "string"
14563
+ },
14564
+ "default": "\"Keep editing\""
14565
+ },
14566
+ {
14567
+ "kind": "field",
14568
+ "name": "discardLabel",
14569
+ "type": {
14570
+ "text": "string"
14571
+ },
14572
+ "default": "\"Discard\""
14573
+ },
14405
14574
  {
14406
14575
  "kind": "field",
14407
14576
  "name": "returnValue",
@@ -14859,6 +15028,27 @@
14859
15028
  },
14860
15029
  "description": "The close button's name, for an app in another language. Defaults to `Close`.",
14861
15030
  "name": "close-label"
15031
+ },
15032
+ {
15033
+ "type": {
15034
+ "text": "string"
15035
+ },
15036
+ "description": "The question asked before closing with edits, for an app in another language. Defaults to `Discard your changes?`.",
15037
+ "name": "discard-question"
15038
+ },
15039
+ {
15040
+ "type": {
15041
+ "text": "string"
15042
+ },
15043
+ "description": "The button that goes back to editing. Defaults to `Keep editing`.",
15044
+ "name": "keep-label"
15045
+ },
15046
+ {
15047
+ "type": {
15048
+ "text": "string"
15049
+ },
15050
+ "description": "The button that throws the edits away. Defaults to `Discard`.",
15051
+ "name": "discard-label"
14862
15052
  }
14863
15053
  ],
14864
15054
  "superclass": {
@@ -16473,7 +16663,7 @@
16473
16663
  "text": "Properties"
16474
16664
  },
16475
16665
  "static": true,
16476
- "default": "{ label: {}, name: {}, value: {}, multiple: { type: \"boolean\", reflect: true }, values: { type: \"json\" }, hint: {}, info: {}, placeholder: {}, search: {}, disabled: { type: \"boolean\" }, open: { type: \"boolean\", reflect: true }, options: { type: \"json\" }, labelHidden: { type: \"boolean\", attribute: \"label-hidden\" }, inlineLabel: { type: \"boolean\", attribute: \"inline-label\", reflect: true }, size: { reflect: true }, }",
16666
+ "default": "{ label: {}, name: {}, value: {}, multiple: { type: \"boolean\", reflect: true }, values: { type: \"json\" }, hint: {}, info: {}, placeholder: {}, search: {}, disabled: { type: \"boolean\" }, open: { type: \"boolean\", reflect: true }, options: { type: \"json\" }, labelHidden: { type: \"boolean\", attribute: \"label-hidden\" }, inlineLabel: { type: \"boolean\", attribute: \"inline-label\", reflect: true }, size: { reflect: true }, clearLabel: { attribute: \"clear-label\" }, }",
16477
16667
  "inheritedFrom": {
16478
16668
  "name": "GwElement",
16479
16669
  "module": "src/element.ts"
@@ -16601,6 +16791,14 @@
16601
16791
  },
16602
16792
  "default": "\"medium\""
16603
16793
  },
16794
+ {
16795
+ "kind": "field",
16796
+ "name": "clearLabel",
16797
+ "type": {
16798
+ "text": "string"
16799
+ },
16800
+ "default": "\"Clear\""
16801
+ },
16604
16802
  {
16605
16803
  "kind": "field",
16606
16804
  "name": "#id",
@@ -17170,6 +17368,13 @@
17170
17368
  },
17171
17369
  "description": "The field's size, 40px tall at medium. Defaults to `medium`.",
17172
17370
  "name": "size"
17371
+ },
17372
+ {
17373
+ "type": {
17374
+ "text": "string"
17375
+ },
17376
+ "description": "The button that clears a multiple choice, for an app in another language. Defaults to `Clear`.",
17377
+ "name": "clear-label"
17173
17378
  }
17174
17379
  ],
17175
17380
  "superclass": {
@@ -21462,7 +21667,7 @@
21462
21667
  "text": "readonly CSSStyleSheet[]"
21463
21668
  },
21464
21669
  "static": true,
21465
- "default": "[ base, css` :host { position: fixed; inset: auto var(--gw-space-4) var(--gw-space-4) auto; width: min(22rem, calc(100vw - 2 * var(--gw-space-4))); padding: 0; margin: 0; overflow: visible; color: inherit; pointer-events: none; user-select: none; background: none; border: 0; } :host(:dir(rtl)) { inset: auto auto var(--gw-space-4) var(--gw-space-4); } :host(:not(:popover-open)) { display: none; } .list { display: flex; flex-direction: column; gap: var(--gw-space-1); align-items: flex-end; } .toast { max-width: 100%; padding: var(--gw-space-2) var(--gw-space-3); font-size: var(--gw-font-size-s); color: var(--gw-color-text-strong); background: var(--gw-color-surface-overlay); border: var(--gw-border-width) solid var(--gw-color-border-normal); border-inline-start: calc(var(--gw-border-width) * 4) solid var(--gw-toast-tone); border-radius: var(--gw-radius-control); box-shadow: var(--gw-shadow-m); } `, ]",
21670
+ "default": "[ base, css` :host { position: fixed; inset: auto var(--gw-space-4) var(--gw-space-4) auto; width: min(22rem, calc(100vw - 2 * var(--gw-space-4))); padding: 0; margin: 0; overflow: visible; color: inherit; pointer-events: none; user-select: none; background: none; border: 0; } :host(:dir(rtl)) { inset: auto auto var(--gw-space-4) var(--gw-space-4); } :host(:not(:popover-open)) { display: none; } .list { display: flex; flex-direction: column; gap: var(--gw-space-1); align-items: flex-end; } .toast { max-width: 100%; padding: var(--gw-space-2) var(--gw-space-3); font-size: var(--gw-font-size-s); color: var(--gw-color-text-strong); background: var(--gw-color-surface-overlay); border: var(--gw-border-width) solid var(--gw-color-border-normal); border-inline-start: calc(var(--gw-border-width) * 4) solid var(--gw-toast-tone); border-radius: min(var(--gw-radius-control), var(--gw-radius-panel)); box-shadow: var(--gw-shadow-m); } `, ]",
21466
21671
  "inheritedFrom": {
21467
21672
  "name": "GwElement",
21468
21673
  "module": "src/element.ts"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timmo001/groundwork-ui",
3
- "version": "0.9.28",
3
+ "version": "0.9.30",
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/dismiss.ts CHANGED
@@ -74,10 +74,12 @@ export class Dismissal {
74
74
  "Discard",
75
75
  );
76
76
 
77
+ readonly #question = h("p", {}, "Discard your changes?");
78
+
77
79
  readonly #view = h(
78
80
  "div",
79
81
  { class: "discard", role: "alert" },
80
- h("p", {}, "Discard your changes?"),
82
+ this.#question,
81
83
  h("footer", {}, this.#keep, this.#discard),
82
84
  );
83
85
 
@@ -107,6 +109,13 @@ export class Dismissal {
107
109
  });
108
110
  }
109
111
 
112
+ /** Sets the question and its buttons' text, for an app in another language. */
113
+ text(question: string, keep: string, discard: string) {
114
+ this.#question.textContent = question;
115
+ this.#keep.textContent = keep;
116
+ this.#discard.textContent = discard;
117
+ }
118
+
110
119
  /** Whether it's showing the question rather than the content. */
111
120
  get asking() {
112
121
  return this.#surface.dataset.view === "discard";
package/src/fields.ts CHANGED
@@ -140,6 +140,9 @@ export const fieldStyles = css`
140
140
  corner, never sideways out of the layout */
141
141
  gw-input textarea {
142
142
  display: block;
143
+ /* Pill-shaped controls would make a tall box a stadium, so it rounds no
144
+ more than a panel */
145
+ border-radius: min(var(--gw-radius-control), var(--gw-radius-panel));
143
146
  padding-block: calc(var(--gw-space-2) * var(--gw-field-size));
144
147
  line-height: 1.5;
145
148
  resize: vertical;
package/src/gw-alert.ts CHANGED
@@ -70,7 +70,7 @@ export class GwAlert extends GwElement {
70
70
  border: var(--gw-border-width) solid
71
71
  var(--gw-tone-border, var(--gw-color-border-info));
72
72
  border-inline-start-width: calc(var(--gw-border-width) * 4);
73
- border-radius: var(--gw-radius-control);
73
+ border-radius: min(var(--gw-radius-control), var(--gw-radius-panel));
74
74
  }
75
75
 
76
76
  ::slotted(p) {
package/src/gw-card.ts CHANGED
@@ -29,6 +29,7 @@ import { base, createIcon, setIconPath } from "./styles.js";
29
29
  * @attr {string} icon - SVG path data for an icon before the heading, such as on a card that links to a section.
30
30
  * @attr {number} level - The heading's level for assistive tech, 2 by default. Use 3 for a card under a section heading.
31
31
  * @attr {string} href - Makes the whole card a link to this address.
32
+ * @attr {boolean} title-link - Makes only the heading the link, from `href` or an `<a>` in the `heading` slot, so the rest of the card stays plain.
32
33
  * @attr {boolean} flush - Drops the padding, for content that runs to the edges.
33
34
  * @attr {boolean} loading - Shows skeleton lines for the content and any unknown heading.
34
35
  * @attr {number} lines - How many skeleton lines while loading. Defaults to 3.
@@ -52,6 +53,7 @@ export class GwCard extends GwElement {
52
53
  icon: {},
53
54
  level: { type: "number" },
54
55
  href: {},
56
+ titleLink: { attribute: "title-link", type: "boolean", reflect: true },
55
57
  flush: { type: "boolean", reflect: true },
56
58
  loading: { type: "boolean", reflect: true },
57
59
  lines: { type: "number" },
@@ -113,6 +115,25 @@ export class GwCard extends GwElement {
113
115
  outline: none;
114
116
  }
115
117
 
118
+ /* With title-link, only the heading is the link */
119
+ .title a {
120
+ color: inherit;
121
+ text-decoration: none;
122
+ border-radius: var(--gw-radius-chip);
123
+ }
124
+
125
+ .title a[href]:hover,
126
+ :host([title-link]) ::slotted(a[slot="heading"]:hover) {
127
+ color: var(--gw-color-accent);
128
+ text-decoration: underline;
129
+ }
130
+
131
+ .title a[href]:focus-visible,
132
+ :host([title-link]) ::slotted(a[slot="heading"]:focus-visible) {
133
+ outline: var(--gw-focus-width) solid var(--gw-color-focus);
134
+ outline-offset: var(--gw-focus-offset);
135
+ }
136
+
116
137
  header {
117
138
  display: flex;
118
139
  gap: var(--gw-space-2);
@@ -175,6 +196,8 @@ export class GwCard extends GwElement {
175
196
  /** The heading's level for assistive tech. */
176
197
  declare level: number;
177
198
  declare href: string;
199
+ /** Makes only the heading the link, rather than the whole card. */
200
+ declare titleLink: boolean;
178
201
  declare flush: boolean;
179
202
  declare loading: boolean;
180
203
  /** How many lines of skeleton stand in for the content while loading. */
@@ -182,6 +205,9 @@ export class GwCard extends GwElement {
182
205
 
183
206
  readonly #title = h("span");
184
207
 
208
+ // A link only with title-link and href; without an href it's plain text
209
+ readonly #titleAnchor = h("a", {}, this.#title);
210
+
185
211
  readonly #titleSkeleton = h("gw-skeleton-text");
186
212
 
187
213
  readonly #headingSlot = h("slot", { name: "heading" });
@@ -189,7 +215,7 @@ export class GwCard extends GwElement {
189
215
  readonly #heading = h(
190
216
  "div",
191
217
  { class: "title", part: "heading", role: "heading", "aria-level": "2" },
192
- this.#title,
218
+ this.#titleAnchor,
193
219
  this.#headingSlot,
194
220
  this.#titleSkeleton,
195
221
  );
@@ -242,6 +268,7 @@ export class GwCard extends GwElement {
242
268
  this.icon = "";
243
269
  this.level = 2;
244
270
  this.href = "";
271
+ this.titleLink = false;
245
272
  this.flush = false;
246
273
  this.loading = false;
247
274
  this.lines = 3;
@@ -251,7 +278,7 @@ export class GwCard extends GwElement {
251
278
  this.addEventListener("click", (event) => {
252
279
  const link = this.#link();
253
280
 
254
- if (link === null || this.loading) return;
281
+ if (link === null || this.loading || this.titleLink) return;
255
282
 
256
283
  const path = event.composedPath();
257
284
  const own = path.slice(0, path.indexOf(this));
@@ -277,7 +304,7 @@ export class GwCard extends GwElement {
277
304
  const focus = () => {
278
305
  this.#card.classList.toggle(
279
306
  "focused",
280
- this.#link()?.matches(":focus-visible") === true,
307
+ !this.titleLink && this.#link()?.matches(":focus-visible") === true,
281
308
  );
282
309
  };
283
310
 
@@ -327,11 +354,13 @@ export class GwCard extends GwElement {
327
354
 
328
355
  const before = animate ? this.offsetHeight : 0;
329
356
 
357
+ const cardLink = Boolean(this.href) && !this.titleLink;
358
+
330
359
  if (
331
- changed.has("href") &&
332
- (this.#card.tagName === "A") !== Boolean(this.href)
360
+ (changed.has("href") || changed.has("titleLink")) &&
361
+ (this.#card.tagName === "A") !== cardLink
333
362
  ) {
334
- const next = this.href
363
+ const next = cardLink
335
364
  ? h("a", { class: "card", part: "card" })
336
365
  : h("section", { class: "card", part: "card" });
337
366
 
@@ -342,11 +371,20 @@ export class GwCard extends GwElement {
342
371
 
343
372
  if (this.#card instanceof HTMLAnchorElement) this.#card.href = this.href;
344
373
 
374
+ setAttribute(
375
+ this.#titleAnchor,
376
+ "href",
377
+ this.titleLink && this.href ? this.href : null,
378
+ );
379
+
345
380
  setAttribute(this.#card, "aria-busy", this.loading ? "true" : null);
346
381
 
347
382
  const slotted = this.querySelector(":scope > [slot='heading']") !== null;
348
383
 
349
- this.#card.classList.toggle("linked", this.#link() !== null && !this.href);
384
+ this.#card.classList.toggle(
385
+ "linked",
386
+ this.#link() !== null && !this.href && !this.titleLink,
387
+ );
350
388
  this.#title.textContent = this.heading;
351
389
  this.#heading.setAttribute("aria-level", String(this.level || 2));
352
390
  this.#description.textContent = this.description;
@@ -23,6 +23,7 @@ const sheetBelow = 600;
23
23
  * @summary A searchable list of commands on Cmd+K or Ctrl+K.
24
24
  *
25
25
  * @attr {OptionItem[]} options - The commands, as JSON.
26
+ * @attr {string} value - The current command, such as the page you're on. It's marked, and selected and scrolled into view each time the menu opens.
26
27
  * @attr {string} label - The accessible name. Defaults to `Commands`.
27
28
  * @attr {string} placeholder - The search placeholder. Defaults to `Search commands`.
28
29
  * @attr {string} shortcut - The key that opens it with Cmd or Ctrl, such as `mod+k`, or `none`. Defaults to `mod+k`.
@@ -37,6 +38,7 @@ const sheetBelow = 600;
37
38
  export class GwCommandMenu extends GwElement {
38
39
  static override properties = {
39
40
  options: { type: "json" },
41
+ value: {},
40
42
  label: {},
41
43
  placeholder: {},
42
44
  shortcut: {},
@@ -154,6 +156,8 @@ export class GwCommandMenu extends GwElement {
154
156
  ];
155
157
 
156
158
  declare options: readonly OptionItem[];
159
+ /** The current command, marked and selected when the menu opens. */
160
+ declare value: string;
157
161
  declare label: string;
158
162
  declare placeholder: string;
159
163
  /** `mod+k` (the default) opens it on Cmd+K or Ctrl+K; `none` turns that off. */
@@ -178,6 +182,7 @@ export class GwCommandMenu extends GwElement {
178
182
  constructor() {
179
183
  super();
180
184
  this.options = [];
185
+ this.value = "";
181
186
  this.label = "Commands";
182
187
  this.placeholder = "Search commands";
183
188
  this.shortcut = "mod+k";
@@ -268,6 +273,7 @@ export class GwCommandMenu extends GwElement {
268
273
  const dialog = this.#dialog;
269
274
 
270
275
  list.options = Array.isArray(this.options) ? this.options : [];
276
+ list.value = this.value;
271
277
  list.label = this.label;
272
278
  list.placeholder = this.placeholder;
273
279
  dialog.setAttribute("aria-label", this.label);
package/src/gw-dialog.ts CHANGED
@@ -25,6 +25,9 @@ const closeIcon =
25
25
  * @attr {string} heading - The heading beside the close button.
26
26
  * @attr {boolean} open - Whether it shows.
27
27
  * @attr {string} close-label - The close button's name, for an app in another language. Defaults to `Close`.
28
+ * @attr {string} discard-question - The question asked before closing with edits, for an app in another language. Defaults to `Discard your changes?`.
29
+ * @attr {string} keep-label - The button that goes back to editing. Defaults to `Keep editing`.
30
+ * @attr {string} discard-label - The button that throws the edits away. Defaults to `Discard`.
28
31
  *
29
32
  * @slot - The content.
30
33
  * @slot actions - Buttons; one with a `value` closes it with that value.
@@ -42,6 +45,9 @@ export class GwDialog extends GwElement {
42
45
  heading: {},
43
46
  open: { type: "boolean", reflect: true },
44
47
  closeLabel: { attribute: "close-label" },
48
+ discardQuestion: { attribute: "discard-question" },
49
+ keepLabel: { attribute: "keep-label" },
50
+ discardLabel: { attribute: "discard-label" },
45
51
  } as const;
46
52
 
47
53
  static override styles = [
@@ -148,6 +154,9 @@ export class GwDialog extends GwElement {
148
154
  declare open: boolean;
149
155
  /** The close button's name, for an app in another language. */
150
156
  declare closeLabel: string;
157
+ declare discardQuestion: string;
158
+ declare keepLabel: string;
159
+ declare discardLabel: string;
151
160
 
152
161
  /** The value of the action that closed the dialog, or "" for Esc. */
153
162
  returnValue = "";
@@ -157,6 +166,9 @@ export class GwDialog extends GwElement {
157
166
  this.heading = "";
158
167
  this.open = false;
159
168
  this.closeLabel = "Close";
169
+ this.discardQuestion = "Discard your changes?";
170
+ this.keepLabel = "Keep editing";
171
+ this.discardLabel = "Discard";
160
172
  }
161
173
 
162
174
  readonly #heading = h("h2", { id: "heading", part: "heading" });
@@ -251,6 +263,11 @@ export class GwDialog extends GwElement {
251
263
 
252
264
  this.#heading.textContent = this.heading;
253
265
  this.#closeButton.textContent = this.closeLabel;
266
+ this.#dismissal.text(
267
+ this.discardQuestion,
268
+ this.keepLabel,
269
+ this.discardLabel,
270
+ );
254
271
 
255
272
  if (this.open && !dialog.open) {
256
273
  dialog.showModal();
package/src/gw-input.ts CHANGED
@@ -34,6 +34,7 @@ const hideIcon =
34
34
  * @attr {string} min - The lowest value of a `number`, date or time field.
35
35
  * @attr {string} max - The highest value of a `number`, date or time field.
36
36
  * @attr {string} step - The step between values of a `number`, date or time field, or `any`.
37
+ * @attr {number} maxlength - The most characters it takes.
37
38
  * @attr {string} hint - One line of help under the field.
38
39
  * @attr {string} info - Longer help, opened from an (i) button by the label.
39
40
  * @attr {string} error - An error under the field, which marks it invalid and is announced.
@@ -63,6 +64,7 @@ export class GwInput extends LightElement {
63
64
  min: {},
64
65
  max: {},
65
66
  step: {},
67
+ maxlength: { type: "number" },
66
68
  hint: {},
67
69
  info: {},
68
70
  error: {},
@@ -92,6 +94,8 @@ export class GwInput extends LightElement {
92
94
  declare max: string;
93
95
  /** The step between values of a number, date or time field. */
94
96
  declare step: string;
97
+ /** The most characters it takes, or 0 for no limit. */
98
+ declare maxlength: number;
95
99
  declare hint: string;
96
100
  /** Longer help, opened from an (i) button by the label. */
97
101
  declare info: string;
@@ -172,6 +176,7 @@ export class GwInput extends LightElement {
172
176
  this.min = "";
173
177
  this.max = "";
174
178
  this.step = "";
179
+ this.maxlength = 0;
175
180
  this.hint = "";
176
181
  this.info = "";
177
182
  this.error = "";
@@ -271,6 +276,11 @@ export class GwInput extends LightElement {
271
276
  setAttribute(this.#input, "min", this.min || null);
272
277
  setAttribute(this.#input, "max", this.max || null);
273
278
  setAttribute(this.#input, "step", this.step || null);
279
+ setAttribute(
280
+ control,
281
+ "maxlength",
282
+ this.maxlength > 0 ? String(this.maxlength) : null,
283
+ );
274
284
  setAttribute(control, "spellcheck", this.monospace ? "false" : null);
275
285
  setAttribute(control, "aria-invalid", this.error ? "true" : null);
276
286
  setAttribute(control, "aria-describedby", described || null);
@@ -28,6 +28,7 @@ const closeIcon =
28
28
  * @attr {"neutral"|"accent"|"info"|"success"|"warning"|"danger"|"important"} tone - The icon's color. Defaults to `neutral`.
29
29
  * @attr {string} time - When it happened, as shown, such as `2 min ago`.
30
30
  * @attr {boolean} unread - Marks it with a stripe until it's seen.
31
+ * @attr {string} dismiss-label - The close button's name, for an app in another language. Defaults to `Dismiss`.
31
32
  *
32
33
  * @slot - A line of text.
33
34
  * @slot actions - Up to two buttons.
@@ -45,6 +46,7 @@ export class GwNotification extends GwElement {
45
46
  tone: { reflect: true },
46
47
  time: {},
47
48
  unread: { type: "boolean", reflect: true },
49
+ dismissLabel: { attribute: "dismiss-label" },
48
50
  } as const;
49
51
 
50
52
  static override styles = [
@@ -64,7 +66,7 @@ export class GwNotification extends GwElement {
64
66
  color: var(--gw-color-text-primary);
65
67
  background: var(--gw-color-surface-raised);
66
68
  border: var(--gw-border-width) solid var(--gw-color-border-quiet);
67
- border-radius: var(--gw-radius-control);
69
+ border-radius: min(var(--gw-radius-control), var(--gw-radius-panel));
68
70
  }
69
71
 
70
72
  /* An inset stripe, so unread and read cards line up */
@@ -129,6 +131,7 @@ export class GwNotification extends GwElement {
129
131
  declare tone: Tone;
130
132
  declare time: string;
131
133
  declare unread: boolean;
134
+ declare dismissLabel: string;
132
135
 
133
136
  readonly #icon = createIcon("");
134
137
 
@@ -156,6 +159,7 @@ export class GwNotification extends GwElement {
156
159
  this.tone = "neutral";
157
160
  this.time = "";
158
161
  this.unread = false;
162
+ this.dismissLabel = "Dismiss";
159
163
  }
160
164
 
161
165
  /** Fires `dismiss`, then folds away and removes itself unless that's cancelled. */
@@ -212,6 +216,7 @@ export class GwNotification extends GwElement {
212
216
  this.#heading.textContent = this.heading;
213
217
  this.#heading.hidden = !this.heading;
214
218
  this.#time.textContent = this.time;
219
+ this.#close.textContent = this.dismissLabel;
215
220
  }
216
221
  }
217
222
 
@@ -557,8 +557,10 @@ export class GwOptionList extends GwElement {
557
557
  const headed =
558
558
  previous?.kind === "group" ? (this.#tops[position - 1] ?? top) : top;
559
559
 
560
+ // A third of the way down, so what's above it shows too without
561
+ // the option sitting at either edge
560
562
  if (center) {
561
- listbox.scrollTop = Math.max(0, top - listbox.clientHeight / 2);
563
+ listbox.scrollTop = Math.max(0, top - listbox.clientHeight / 3);
562
564
  } else if (headed - listPadding < listbox.scrollTop) {
563
565
  listbox.scrollTop = Math.max(0, headed - listPadding);
564
566
  } else if (
package/src/gw-popover.ts CHANGED
@@ -35,6 +35,9 @@ const sheetBelow = 600;
35
35
  * @attr {string} heading - The heading beside the close button.
36
36
  * @attr {boolean} confirm - Makes it an `alertdialog`, for a question that needs an answer.
37
37
  * @attr {string} close-label - The close button's name, for an app in another language. Defaults to `Close`.
38
+ * @attr {string} discard-question - The question asked before closing with edits, for an app in another language. Defaults to `Discard your changes?`.
39
+ * @attr {string} keep-label - The button that goes back to editing. Defaults to `Keep editing`.
40
+ * @attr {string} discard-label - The button that throws the edits away. Defaults to `Discard`.
38
41
  *
39
42
  * @slot - The content.
40
43
  * @slot actions - Buttons; one with a `value` closes it with that value.
@@ -53,6 +56,9 @@ export class GwPopover extends GwElement {
53
56
  heading: {},
54
57
  confirm: { type: "boolean" },
55
58
  closeLabel: { attribute: "close-label" },
59
+ discardQuestion: { attribute: "discard-question" },
60
+ keepLabel: { attribute: "keep-label" },
61
+ discardLabel: { attribute: "discard-label" },
56
62
  } as const;
57
63
 
58
64
  static override styles = [
@@ -197,6 +203,9 @@ export class GwPopover extends GwElement {
197
203
  declare confirm: boolean;
198
204
  /** The close button's name, for an app in another language. */
199
205
  declare closeLabel: string;
206
+ declare discardQuestion: string;
207
+ declare keepLabel: string;
208
+ declare discardLabel: string;
200
209
 
201
210
  /** The value of the action that closed it, or "" for Esc or a click outside. */
202
211
  returnValue = "";
@@ -252,6 +261,9 @@ export class GwPopover extends GwElement {
252
261
  this.heading = "";
253
262
  this.confirm = false;
254
263
  this.closeLabel = "Close";
264
+ this.discardQuestion = "Discard your changes?";
265
+ this.keepLabel = "Keep editing";
266
+ this.discardLabel = "Discard";
255
267
  }
256
268
 
257
269
  /** Whether its content has changed since it opened, so closing it asks first. */
@@ -399,6 +411,11 @@ export class GwPopover extends GwElement {
399
411
  this.#heading.textContent = this.heading;
400
412
  this.#heading.hidden = !this.heading;
401
413
  this.#closeButton.textContent = this.closeLabel;
414
+ this.#dismissal.text(
415
+ this.discardQuestion,
416
+ this.keepLabel,
417
+ this.discardLabel,
418
+ );
402
419
  }
403
420
  }
404
421
 
package/src/gw-select.ts CHANGED
@@ -63,6 +63,7 @@ const chevronIcon =
63
63
  * @attr {boolean} label-hidden - Hides the label but keeps it for assistive technology.
64
64
  * @attr {boolean} inline-label - Shows the label inside the trigger, before the value, for toolbars and filters.
65
65
  * @attr {"xs"|"small"|"medium"|"large"|"xl"} size - The field's size, 40px tall at medium. Defaults to `medium`.
66
+ * @attr {string} clear-label - The button that clears a multiple choice, for an app in another language. Defaults to `Clear`.
66
67
  *
67
68
  * @cssprop --gw-field-scale - Scales the field, around 1 for medium.
68
69
  * @cssprop --gw-control-scale - Scales its height with the theme's density. The theme sets it.
@@ -86,6 +87,7 @@ export class GwSelect extends LightElement {
86
87
  labelHidden: { type: "boolean", attribute: "label-hidden" },
87
88
  inlineLabel: { type: "boolean", attribute: "inline-label", reflect: true },
88
89
  size: { reflect: true },
90
+ clearLabel: { attribute: "clear-label" },
89
91
  } as const;
90
92
 
91
93
  declare label: string;
@@ -105,6 +107,7 @@ export class GwSelect extends LightElement {
105
107
  declare labelHidden: boolean;
106
108
  declare inlineLabel: boolean;
107
109
  declare size: "xs" | "small" | "medium" | "large" | "xl";
110
+ declare clearLabel: string;
108
111
 
109
112
  readonly #id = fieldId();
110
113
 
@@ -179,6 +182,7 @@ export class GwSelect extends LightElement {
179
182
  this.labelHidden = false;
180
183
  this.inlineLabel = false;
181
184
  this.size = "medium";
185
+ this.clearLabel = "Clear";
182
186
  }
183
187
 
184
188
  /** Opens the list, such as from a keyboard shortcut. */
@@ -439,6 +443,7 @@ export class GwSelect extends LightElement {
439
443
  : []),
440
444
  );
441
445
  this.#clear.hidden = !this.multiple || values.length === 0;
446
+ this.#clear.textContent = this.clearLabel;
442
447
 
443
448
  const list = this.#list;
444
449
 
@@ -472,6 +472,15 @@ export class GwThemePicker extends GwElement {
472
472
  this.#button.addEventListener("click", () => {
473
473
  this.show();
474
474
  });
475
+
476
+ // A theme or mode applies at once, so it isn't an edit for the popover
477
+ // to ask about discarding, and a click outside still closes it
478
+ for (const type of ["input", "change"]) {
479
+ this.#controls.addEventListener(type, (event) => {
480
+ if (event.target !== this.#controls) event.stopPropagation();
481
+ });
482
+ }
483
+
475
484
  this.#mode.addEventListener("change", (event) => {
476
485
  event.stopPropagation();
477
486
  this.mode = this.#mode.value;
package/src/gw-toast.ts CHANGED
@@ -71,7 +71,7 @@ export class GwToaster extends GwElement {
71
71
  border: var(--gw-border-width) solid var(--gw-color-border-normal);
72
72
  border-inline-start: calc(var(--gw-border-width) * 4) solid
73
73
  var(--gw-toast-tone);
74
- border-radius: var(--gw-radius-control);
74
+ border-radius: min(var(--gw-radius-control), var(--gw-radius-panel));
75
75
  box-shadow: var(--gw-shadow-m);
76
76
  }
77
77
  `,