@timmo001/groundwork-ui 0.9.23 → 0.9.24

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, .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) ); } /* 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, .gw-select-trigger):hover { border-color: var(--gw-color-border-loud); } :where(gw-input, gw-select, gw-switch, gw-checkbox) :where(input, .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-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; } `",
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
  {
@@ -9837,7 +9837,7 @@
9837
9837
  "declarations": [
9838
9838
  {
9839
9839
  "kind": "class",
9840
- "description": "A labeled native `<input>`, with an optional hint and error. Enter submits\nits form when the form's submit button is a `gw-button`, as it would with a\nnative one.",
9840
+ "description": "A labeled native `<input>`, or a `<textarea>` with `multiline`, with an\noptional hint and error. Enter submits its form when the form's submit\nbutton is a `gw-button`, as it would with a native one.",
9841
9841
  "name": "GwInput",
9842
9842
  "cssProperties": [
9843
9843
  {
@@ -9857,7 +9857,7 @@
9857
9857
  "text": "Properties"
9858
9858
  },
9859
9859
  "static": true,
9860
- "default": "{ label: {}, name: {}, type: {}, value: {}, placeholder: {}, autocomplete: {}, hint: {}, info: {}, error: {}, required: { type: \"boolean\" }, disabled: { type: \"boolean\" }, size: { reflect: true }, icon: {}, hideLabel: { type: \"boolean\", attribute: \"hide-label\", reflect: 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 }, }",
9861
9861
  "inheritedFrom": {
9862
9862
  "name": "GwElement",
9863
9863
  "module": "src/element.ts"
@@ -9911,6 +9911,15 @@
9911
9911
  },
9912
9912
  "default": "\"\""
9913
9913
  },
9914
+ {
9915
+ "kind": "field",
9916
+ "name": "inputmode",
9917
+ "type": {
9918
+ "text": "string"
9919
+ },
9920
+ "description": "The native inputmode hint, which picks the on-screen keyboard.",
9921
+ "default": "\"\""
9922
+ },
9914
9923
  {
9915
9924
  "kind": "field",
9916
9925
  "name": "hint",
@@ -9944,6 +9953,15 @@
9944
9953
  },
9945
9954
  "default": "false"
9946
9955
  },
9956
+ {
9957
+ "kind": "field",
9958
+ "name": "optional",
9959
+ "type": {
9960
+ "text": "boolean"
9961
+ },
9962
+ "description": "Adds \"(optional)\" after the label.",
9963
+ "default": "false"
9964
+ },
9947
9965
  {
9948
9966
  "kind": "field",
9949
9967
  "name": "disabled",
@@ -9952,6 +9970,42 @@
9952
9970
  },
9953
9971
  "default": "false"
9954
9972
  },
9973
+ {
9974
+ "kind": "field",
9975
+ "name": "multiline",
9976
+ "type": {
9977
+ "text": "boolean"
9978
+ },
9979
+ "description": "Uses a `<textarea>` instead of an `<input>`.",
9980
+ "default": "false"
9981
+ },
9982
+ {
9983
+ "kind": "field",
9984
+ "name": "rows",
9985
+ "type": {
9986
+ "text": "number"
9987
+ },
9988
+ "description": "The textarea's starting height in lines.",
9989
+ "default": "3"
9990
+ },
9991
+ {
9992
+ "kind": "field",
9993
+ "name": "monospace",
9994
+ "type": {
9995
+ "text": "boolean"
9996
+ },
9997
+ "description": "Sets the text in the monospace font and turns off spellcheck.",
9998
+ "default": "false"
9999
+ },
10000
+ {
10001
+ "kind": "field",
10002
+ "name": "reveal",
10003
+ "type": {
10004
+ "text": "boolean"
10005
+ },
10006
+ "description": "On a password field, adds a button to show and hide what's typed.",
10007
+ "default": "false"
10008
+ },
9955
10009
  {
9956
10010
  "kind": "field",
9957
10011
  "name": "size",
@@ -10009,6 +10063,39 @@
10009
10063
  "privacy": "private",
10010
10064
  "readonly": true
10011
10065
  },
10066
+ {
10067
+ "kind": "field",
10068
+ "name": "#textarea",
10069
+ "privacy": "private",
10070
+ "readonly": true
10071
+ },
10072
+ {
10073
+ "kind": "field",
10074
+ "name": "#optional",
10075
+ "privacy": "private",
10076
+ "readonly": true
10077
+ },
10078
+ {
10079
+ "kind": "field",
10080
+ "name": "#revealIcon",
10081
+ "privacy": "private",
10082
+ "readonly": true
10083
+ },
10084
+ {
10085
+ "kind": "field",
10086
+ "name": "#reveal",
10087
+ "privacy": "private",
10088
+ "readonly": true
10089
+ },
10090
+ {
10091
+ "kind": "field",
10092
+ "name": "#revealed",
10093
+ "privacy": "private",
10094
+ "type": {
10095
+ "text": "boolean"
10096
+ },
10097
+ "default": "false"
10098
+ },
10012
10099
  {
10013
10100
  "kind": "field",
10014
10101
  "name": "#hint",
@@ -10021,6 +10108,13 @@
10021
10108
  "privacy": "private",
10022
10109
  "readonly": true
10023
10110
  },
10111
+ {
10112
+ "kind": "field",
10113
+ "name": "#control",
10114
+ "privacy": "private",
10115
+ "description": "The native control in use: the textarea when multiline, else the input.",
10116
+ "readonly": true
10117
+ },
10024
10118
  {
10025
10119
  "kind": "method",
10026
10120
  "name": "create",
@@ -10389,6 +10483,13 @@
10389
10483
  "description": "The native autocomplete hint.",
10390
10484
  "name": "autocomplete"
10391
10485
  },
10486
+ {
10487
+ "type": {
10488
+ "text": "string"
10489
+ },
10490
+ "description": "The native inputmode hint, such as `numeric` for a port, which picks the on-screen keyboard.",
10491
+ "name": "inputmode"
10492
+ },
10392
10493
  {
10393
10494
  "type": {
10394
10495
  "text": "string"
@@ -10407,7 +10508,7 @@
10407
10508
  "type": {
10408
10509
  "text": "string"
10409
10510
  },
10410
- "description": "An error under the field, which marks it invalid.",
10511
+ "description": "An error under the field, which marks it invalid and is announced.",
10411
10512
  "name": "error"
10412
10513
  },
10413
10514
  {
@@ -10417,6 +10518,13 @@
10417
10518
  "description": "Requires a value.",
10418
10519
  "name": "required"
10419
10520
  },
10521
+ {
10522
+ "type": {
10523
+ "text": "boolean"
10524
+ },
10525
+ "description": "Adds \"(optional)\" after the label, for the few optional fields in a form that mostly requires them.",
10526
+ "name": "optional"
10527
+ },
10420
10528
  {
10421
10529
  "type": {
10422
10530
  "text": "boolean"
@@ -10424,6 +10532,34 @@
10424
10532
  "description": "Disables the field.",
10425
10533
  "name": "disabled"
10426
10534
  },
10535
+ {
10536
+ "type": {
10537
+ "text": "boolean"
10538
+ },
10539
+ "description": "Uses a `<textarea>` that grows by dragging its corner, for text that runs over lines.",
10540
+ "name": "multiline"
10541
+ },
10542
+ {
10543
+ "type": {
10544
+ "text": "number"
10545
+ },
10546
+ "description": "The textarea's starting height in lines. Defaults to 3.",
10547
+ "name": "rows"
10548
+ },
10549
+ {
10550
+ "type": {
10551
+ "text": "boolean"
10552
+ },
10553
+ "description": "Sets the text in the monospace font, for paths, commands, tokens and other machine text. It also turns off spellcheck.",
10554
+ "name": "monospace"
10555
+ },
10556
+ {
10557
+ "type": {
10558
+ "text": "boolean"
10559
+ },
10560
+ "description": "On a password field, adds a button at the end to show and hide what's typed.",
10561
+ "name": "reveal"
10562
+ },
10427
10563
  {
10428
10564
  "type": {
10429
10565
  "text": "\"xs\"|\"small\"|\"medium\"|\"large\"|\"xl\""
@@ -12865,7 +13001,7 @@
12865
13001
  "text": "readonly CSSStyleSheet[]"
12866
13002
  },
12867
13003
  "static": true,
12868
- "default": "[ base, css` :host { display: flex; flex-direction: column; min-height: 0; } .search { display: flex; gap: var(--gw-space-2); align-items: center; padding: var(--gw-space-1) var(--gw-space-3); color: var(--gw-color-text-secondary); border-block-end: var(--gw-border-width) solid var(--gw-color-border-quiet); } :host(:not([search])) .search { display: none; } input { flex: 1; min-width: 0; height: 2.5rem; padding: 0; font: inherit; font-size: var(--gw-font-size-s); color: var(--gw-color-text-strong); background: none; border: 0; outline: none; } /* The search field is always focused while the list is open, so the row's divider marks it rather than a ring */ input:focus-visible { outline: none; } input::placeholder { color: var(--gw-color-text-secondary); } [role=\"listbox\"] { position: relative; max-height: var(--gw-option-list-height, 20rem); overflow-y: auto; overscroll-behavior: contain; outline: none; } .spacer { position: relative; } .row { position: absolute; inset-inline: ${listPadding}px; box-sizing: border-box; display: flex; gap: var(--gw-space-3); align-items: center; padding: 0 var(--gw-space-2); } .group { align-items: flex-end; padding-block-end: var(--gw-space-1); font-size: var(--gw-font-size-xs); font-weight: var(--gw-font-weight-strong); color: var(--gw-color-text-secondary); } [role=\"option\"] { font-size: var(--gw-font-size-s); color: var(--gw-color-text-strong); cursor: pointer; border-radius: var(--gw-radius-control); transition: background-color var(--gw-motion-fast) var(--gw-motion-easing); } /* Mixed from the text color, so the active row stands out on any surface the list sits on, overlays included */ [role=\"option\"][data-active] { background: color-mix( in oklab, transparent, var(--gw-color-text-primary) 12% ); } [role=\"option\"][aria-selected=\"true\"] { color: var(--gw-color-text-strong); } [role=\"option\"] > .icon { flex: none; color: var(--gw-color-text-primary); } [role=\"option\"] > .check { color: var(--gw-color-accent); } .text { display: flex; flex: 1; flex-direction: column; min-width: 0; } .text span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .description { font-size: var(--gw-font-size-xs); color: var(--gw-color-text-secondary); } .swatches { display: flex; flex: none; } .swatches span, .swatch { width: 0.875rem; height: 0.875rem; margin-inline-start: -0.25rem; border: 1px solid color-mix(in oklab, var(--gw-color-text-primary) 20%, transparent); border-radius: 50%; } .swatch { flex: none; width: 0.625rem; height: 0.625rem; margin-inline: 0.125rem; } .none { padding: var(--gw-space-4); font-size: var(--gw-font-size-s); color: var(--gw-color-text-secondary); text-align: center; } .none[hidden] { display: none; } `, ]",
13004
+ "default": "[ base, css` :host { display: flex; flex-direction: column; min-height: 0; } .search { display: flex; gap: var(--gw-space-2); align-items: center; padding: var(--gw-space-1) var(--gw-space-3); color: var(--gw-color-text-secondary); border-block-end: var(--gw-border-width) solid var(--gw-color-border-quiet); } :host(:not([search])) .search { display: none; } input { flex: 1; min-width: 0; height: 2.5rem; padding: 0; font: inherit; font-size: var(--gw-font-size-s); color: var(--gw-color-text-strong); background: none; border: 0; outline: none; } /* The search field is always focused while the list is open, so the row's divider marks it rather than a ring */ input:focus-visible { outline: none; } input::placeholder { color: var(--gw-color-text-secondary); } [role=\"listbox\"] { position: relative; max-height: var(--gw-option-list-height, 20rem); overflow-y: auto; overscroll-behavior: contain; outline: none; } .spacer { position: relative; } .row { position: absolute; inset-inline: ${listPadding}px; box-sizing: border-box; display: flex; gap: var(--gw-space-3); align-items: center; padding: 0 var(--gw-space-2); } .group { align-items: flex-end; padding-block-end: var(--gw-space-1); font-size: var(--gw-font-size-xs); font-weight: var(--gw-font-weight-strong); color: var(--gw-color-text-secondary); } [role=\"option\"] { font-size: var(--gw-font-size-s); color: var(--gw-color-text-strong); cursor: pointer; border-radius: var(--gw-radius-control); transition: background-color var(--gw-motion-fast) var(--gw-motion-easing); } /* Mixed from the text color, so the active row stands out on any surface the list sits on, overlays included */ [role=\"option\"][data-active] { background: color-mix( in oklab, transparent, var(--gw-color-text-primary) 12% ); } [role=\"option\"][aria-selected=\"true\"] { color: var(--gw-color-text-strong); } /* A disabled option can still be reached, so its reason is read, but reads as unavailable and can't be picked */ [role=\"option\"][aria-disabled=\"true\"] { color: var(--gw-color-text-disabled); cursor: not-allowed; } [role=\"option\"] > .icon { flex: none; color: var(--gw-color-text-primary); } [role=\"option\"] > .check { color: var(--gw-color-accent); } .text { display: flex; flex: 1; flex-direction: column; min-width: 0; } .text span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .description { font-size: var(--gw-font-size-xs); color: var(--gw-color-text-secondary); } .swatches { display: flex; flex: none; } .swatches span, .swatch { width: 0.875rem; height: 0.875rem; margin-inline-start: -0.25rem; border: 1px solid color-mix(in oklab, var(--gw-color-text-primary) 20%, transparent); border-radius: 50%; } .swatch { flex: none; width: 0.625rem; height: 0.625rem; margin-inline: 0.125rem; } .none { padding: var(--gw-space-4); font-size: var(--gw-font-size-s); color: var(--gw-color-text-secondary); text-align: center; } .none[hidden] { display: none; } `, ]",
12869
13005
  "inheritedFrom": {
12870
13006
  "name": "GwElement",
12871
13007
  "module": "src/element.ts"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timmo001/groundwork-ui",
3
- "version": "0.9.23",
3
+ "version": "0.9.24",
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
@@ -88,7 +88,7 @@ export const fieldStyles = css`
88
88
  --gw-field-scale: 1.4;
89
89
  }
90
90
 
91
- :where(gw-input, gw-select) :where(input, .gw-select-trigger) {
91
+ :where(gw-input, gw-select) :where(input, textarea, .gw-select-trigger) {
92
92
  --gw-field-size: var(--gw-field-scale, 1);
93
93
  --gw-icon-size: calc(
94
94
  1.25rem * var(--gw-field-size) * var(--gw-control-scale, 1)
@@ -136,6 +136,79 @@ export const fieldStyles = css`
136
136
  );
137
137
  }
138
138
 
139
+ /* A textarea keeps the field's padding on every side and grows by its
140
+ corner, never sideways out of the layout */
141
+ gw-input textarea {
142
+ display: block;
143
+ padding-block: calc(var(--gw-space-2) * var(--gw-field-size));
144
+ line-height: 1.5;
145
+ resize: vertical;
146
+ }
147
+
148
+ gw-input[monospace] :where(input, textarea) {
149
+ font-family: var(--gw-font-mono);
150
+ }
151
+
152
+ gw-input .gw-optional {
153
+ font-weight: var(--gw-font-weight-regular);
154
+ color: var(--gw-color-text-secondary);
155
+ }
156
+
157
+ gw-input .gw-optional[hidden] {
158
+ display: none;
159
+ }
160
+
161
+ /* The show and hide button sits inside the end of a password field, with
162
+ the text kept clear of it */
163
+ gw-input .gw-reveal {
164
+ position: absolute;
165
+ inset-inline-end: calc(var(--gw-space-1) * var(--gw-field-scale, 1));
166
+ display: grid;
167
+ place-items: center;
168
+ width: calc(2rem * var(--gw-field-scale, 1));
169
+ height: calc(2rem * var(--gw-field-scale, 1));
170
+ padding: 0;
171
+ color: var(--gw-color-text-secondary);
172
+ cursor: pointer;
173
+ background: none;
174
+ border: 0;
175
+ border-radius: var(--gw-radius-control);
176
+ transition:
177
+ color var(--gw-motion-fast) var(--gw-motion-easing),
178
+ background-color var(--gw-motion-fast) var(--gw-motion-easing);
179
+ }
180
+
181
+ gw-input .gw-reveal:hover:not(:disabled) {
182
+ color: var(--gw-color-text-strong);
183
+ background: var(--gw-color-fill-neutral-hovered);
184
+ }
185
+
186
+ gw-input .gw-reveal:focus-visible {
187
+ outline: var(--gw-focus-width) solid var(--gw-color-focus);
188
+ outline-offset: calc(var(--gw-focus-width) * -1);
189
+ }
190
+
191
+ gw-input .gw-reveal:disabled {
192
+ cursor: not-allowed;
193
+ opacity: 0.5;
194
+ }
195
+
196
+ gw-input .gw-reveal[hidden] {
197
+ display: none;
198
+ }
199
+
200
+ gw-input .gw-reveal > .icon {
201
+ width: calc(1.125rem * var(--gw-field-scale, 1));
202
+ height: calc(1.125rem * var(--gw-field-scale, 1));
203
+ fill: currentColor;
204
+ }
205
+
206
+ gw-input .gw-input-control:has(> .gw-reveal:not([hidden])) > input {
207
+ padding-inline-end: calc(
208
+ (var(--gw-space-1) + 2rem + var(--gw-space-1)) * var(--gw-field-scale, 1)
209
+ );
210
+ }
211
+
139
212
  /* The trigger shows the chosen option, with its icon, and a chevron with
140
213
  room around it that turns over while the list is open */
141
214
  gw-select .gw-select-trigger {
@@ -308,12 +381,13 @@ export const fieldStyles = css`
308
381
  color: var(--gw-color-text-strong);
309
382
  }
310
383
 
311
- :where(gw-input, gw-select) :where(input, .gw-select-trigger):hover {
384
+ :where(gw-input, gw-select)
385
+ :where(input, textarea, .gw-select-trigger):hover {
312
386
  border-color: var(--gw-color-border-loud);
313
387
  }
314
388
 
315
389
  :where(gw-input, gw-select, gw-switch, gw-checkbox)
316
- :where(input, .gw-select-trigger):focus-visible {
390
+ :where(input, textarea, .gw-select-trigger):focus-visible {
317
391
  outline: var(--gw-focus-width) solid var(--gw-color-focus);
318
392
  outline-offset: var(--gw-focus-offset);
319
393
  }
package/src/gw-input.ts CHANGED
@@ -9,10 +9,17 @@ import { createIcon, setIconPath } from "./styles.js";
9
9
  import { FieldInfo } from "./field-info.js";
10
10
  import { LightElement, fieldId } from "./fields.js";
11
11
 
12
+ // MDI's eye-outline and eye-off-outline
13
+ const showIcon =
14
+ "M12,9A3,3 0 0,1 15,12A3,3 0 0,1 12,15A3,3 0 0,1 9,12A3,3 0 0,1 12,9M12,4.5C17,4.5 21.27,7.61 23,12C21.27,16.39 17,19.5 12,19.5C7,19.5 2.73,16.39 1,12C2.73,7.61 7,4.5 12,4.5M3.18,12C4.83,15.36 8.24,17.5 12,17.5C15.76,17.5 19.17,15.36 20.82,12C19.17,8.64 15.76,6.5 12,6.5C8.24,6.5 4.83,8.64 3.18,12Z";
15
+
16
+ const hideIcon =
17
+ "M2,5.27L3.28,4L20,20.72L18.73,22L15.65,18.92C14.5,19.3 13.28,19.5 12,19.5C7,19.5 2.73,16.39 1,12C1.69,10.24 2.79,8.69 4.19,7.46L2,5.27M12,9A3,3 0 0,1 15,12C15,12.35 14.94,12.69 14.83,13L11,9.17C11.31,9.06 11.65,9 12,9M12,4.5C17,4.5 21.27,7.61 23,12C22.18,14.08 20.79,15.88 19,17.19L17.58,15.76C18.94,14.82 20.06,13.54 20.82,12C19.17,8.64 15.76,6.5 12,6.5C10.91,6.5 9.84,6.68 8.84,7L7.3,5.47C8.74,4.85 10.33,4.5 12,4.5M3.18,12C4.83,15.36 8.24,17.5 12,17.5C12.69,17.5 13.37,17.43 14,17.29L11.72,15C10.29,14.85 9.15,13.71 9,12.28L5.6,8.87C4.61,9.72 3.78,10.78 3.18,12Z";
18
+
12
19
  /**
13
- * A labeled native `<input>`, with an optional hint and error. Enter submits
14
- * its form when the form's submit button is a `gw-button`, as it would with a
15
- * native one.
20
+ * A labeled native `<input>`, or a `<textarea>` with `multiline`, with an
21
+ * optional hint and error. Enter submits its form when the form's submit
22
+ * button is a `gw-button`, as it would with a native one.
16
23
  *
17
24
  * @element gw-input
18
25
  * @summary A labeled text field in light DOM, so forms and autofill see it.
@@ -23,11 +30,17 @@ import { LightElement, fieldId } from "./fields.js";
23
30
  * @attr {string} value - The value.
24
31
  * @attr {string} placeholder - Placeholder text.
25
32
  * @attr {string} autocomplete - The native autocomplete hint.
33
+ * @attr {string} inputmode - The native inputmode hint, such as `numeric` for a port, which picks the on-screen keyboard.
26
34
  * @attr {string} hint - One line of help under the field.
27
35
  * @attr {string} info - Longer help, opened from an (i) button by the label.
28
- * @attr {string} error - An error under the field, which marks it invalid.
36
+ * @attr {string} error - An error under the field, which marks it invalid and is announced.
29
37
  * @attr {boolean} required - Requires a value.
38
+ * @attr {boolean} optional - Adds "(optional)" after the label, for the few optional fields in a form that mostly requires them.
30
39
  * @attr {boolean} disabled - Disables the field.
40
+ * @attr {boolean} multiline - Uses a `<textarea>` that grows by dragging its corner, for text that runs over lines.
41
+ * @attr {number} rows - The textarea's starting height in lines. Defaults to 3.
42
+ * @attr {boolean} monospace - Sets the text in the monospace font, for paths, commands, tokens and other machine text. It also turns off spellcheck.
43
+ * @attr {boolean} reveal - On a password field, adds a button at the end to show and hide what's typed.
31
44
  * @attr {"xs"|"small"|"medium"|"large"|"xl"} size - The field's size, 40px tall at medium. Defaults to `medium`.
32
45
  * @attr {string} icon - SVG path data for an icon inside the field, before the text, such as a magnifier on a search.
33
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.
@@ -43,11 +56,17 @@ export class GwInput extends LightElement {
43
56
  value: {},
44
57
  placeholder: {},
45
58
  autocomplete: {},
59
+ inputmode: {},
46
60
  hint: {},
47
61
  info: {},
48
62
  error: {},
49
63
  required: { type: "boolean" },
64
+ optional: { type: "boolean" },
50
65
  disabled: { type: "boolean" },
66
+ multiline: { type: "boolean", reflect: true },
67
+ rows: { type: "number" },
68
+ monospace: { type: "boolean", reflect: true },
69
+ reveal: { type: "boolean" },
51
70
  size: { reflect: true },
52
71
  icon: {},
53
72
  hideLabel: { type: "boolean", attribute: "hide-label", reflect: true },
@@ -59,12 +78,24 @@ export class GwInput extends LightElement {
59
78
  declare value: string;
60
79
  declare placeholder: string;
61
80
  declare autocomplete: string;
81
+ /** The native inputmode hint, which picks the on-screen keyboard. */
82
+ declare inputmode: string;
62
83
  declare hint: string;
63
84
  /** Longer help, opened from an (i) button by the label. */
64
85
  declare info: string;
65
86
  declare error: string;
66
87
  declare required: boolean;
88
+ /** Adds "(optional)" after the label. */
89
+ declare optional: boolean;
67
90
  declare disabled: boolean;
91
+ /** Uses a `<textarea>` instead of an `<input>`. */
92
+ declare multiline: boolean;
93
+ /** The textarea's starting height in lines. */
94
+ declare rows: number;
95
+ /** Sets the text in the monospace font and turns off spellcheck. */
96
+ declare monospace: boolean;
97
+ /** On a password field, adds a button to show and hide what's typed. */
98
+ declare reveal: boolean;
68
99
  declare size: "xs" | "small" | "medium" | "large" | "xl";
69
100
  /** SVG path data for an icon inside the field, before the text. */
70
101
  declare icon: string;
@@ -81,9 +112,41 @@ export class GwInput extends LightElement {
81
112
 
82
113
  readonly #input = h("input", { id: this.#id });
83
114
 
115
+ readonly #textarea = h("textarea", { id: this.#id });
116
+
117
+ readonly #optional = h(
118
+ "span",
119
+ { class: "gw-optional", hidden: true },
120
+ " (optional)",
121
+ );
122
+
123
+ readonly #revealIcon = createIcon(showIcon);
124
+
125
+ readonly #reveal = h(
126
+ "button",
127
+ {
128
+ type: "button",
129
+ class: "gw-reveal",
130
+ hidden: true,
131
+ "aria-pressed": "false",
132
+ },
133
+ this.#revealIcon,
134
+ );
135
+
136
+ #revealed = false;
137
+
84
138
  readonly #hint = h("p", { class: "gw-hint", id: `${this.#id}-hint` });
85
139
 
86
- readonly #error = h("p", { class: "gw-error", id: `${this.#id}-error` });
140
+ readonly #error = h("p", {
141
+ class: "gw-error",
142
+ id: `${this.#id}-error`,
143
+ role: "alert",
144
+ });
145
+
146
+ /** The native control in use: the textarea when multiline, else the input. */
147
+ get #control() {
148
+ return this.multiline ? this.#textarea : this.#input;
149
+ }
87
150
 
88
151
  constructor() {
89
152
  super();
@@ -93,20 +156,29 @@ export class GwInput extends LightElement {
93
156
  this.value = "";
94
157
  this.placeholder = "";
95
158
  this.autocomplete = "";
159
+ this.inputmode = "";
96
160
  this.hint = "";
97
161
  this.info = "";
98
162
  this.error = "";
99
163
  this.required = false;
164
+ this.optional = false;
100
165
  this.disabled = false;
166
+ this.multiline = false;
167
+ this.rows = 3;
168
+ this.monospace = false;
169
+ this.reveal = false;
101
170
  this.size = "medium";
102
171
  this.icon = "";
103
172
  this.hideLabel = false;
104
173
  }
105
174
 
106
175
  protected override create(root: HTMLElement) {
107
- this.#input.addEventListener("input", () => {
108
- this.value = this.#input.value;
109
- });
176
+ for (const control of [this.#input, this.#textarea]) {
177
+ control.addEventListener("input", () => {
178
+ this.value = control.value;
179
+ });
180
+ }
181
+
110
182
  // A gw-button's native button is in its shadow root, out of the form's
111
183
  // reach, so Enter only submits on its own with a single field. Submit
112
184
  // here instead, as a native submit button would
@@ -131,16 +203,31 @@ export class GwInput extends LightElement {
131
203
  event.preventDefault();
132
204
  form.requestSubmit();
133
205
  });
206
+ this.#reveal.addEventListener("click", () => {
207
+ this.#revealed = !this.#revealed;
208
+ this.requestUpdate("reveal");
209
+ });
134
210
  root.append(
135
211
  this.#info.heading,
136
- h("span", { class: "gw-input-control" }, this.#icon, this.#input),
212
+ h(
213
+ "span",
214
+ { class: "gw-input-control" },
215
+ this.#icon,
216
+ this.#control,
217
+ this.#reveal,
218
+ ),
137
219
  this.#hint,
138
220
  this.#error,
139
221
  );
140
222
  }
141
223
 
142
224
  protected override update() {
143
- const input = this.#input;
225
+ const control = this.#control;
226
+ const other = this.multiline ? this.#input : this.#textarea;
227
+
228
+ // Only the control in use is in the field, so the label and the form
229
+ // see just the one
230
+ if (!control.isConnected && other.isConnected) other.replaceWith(control);
144
231
 
145
232
  const described = [
146
233
  this.hint ? this.#hint.id : "",
@@ -149,22 +236,40 @@ export class GwInput extends LightElement {
149
236
  .join(" ")
150
237
  .trim();
151
238
 
152
- this.#label.textContent = this.label;
239
+ const password = this.type === "password" && !this.multiline;
240
+ const reveal = password && this.reveal;
241
+
242
+ if (!reveal) this.#revealed = false;
243
+
244
+ this.#label.replaceChildren(this.label, this.#optional);
245
+ this.#optional.hidden = !this.optional;
153
246
  this.#info.update(this.label, this.info);
154
247
  this.#info.heading.classList.toggle("gw-visually-hidden", this.hideLabel);
155
248
  setIconPath(this.#icon, this.icon);
156
249
  this.#icon.toggleAttribute("hidden", !this.icon);
157
- input.type = this.type;
158
- setAttribute(input, "name", this.name || null);
159
- setAttribute(input, "placeholder", this.placeholder || null);
160
- setAttribute(input, "autocomplete", this.autocomplete || null);
161
- setAttribute(input, "aria-invalid", this.error ? "true" : null);
162
- setAttribute(input, "aria-describedby", described || null);
163
- input.required = this.required;
164
- input.disabled = this.disabled;
250
+ this.#input.type = reveal && this.#revealed ? "text" : this.type;
251
+ this.#textarea.rows = this.rows;
252
+ setAttribute(control, "name", this.name || null);
253
+ setAttribute(control, "placeholder", this.placeholder || null);
254
+ setAttribute(control, "autocomplete", this.autocomplete || null);
255
+ setAttribute(control, "inputmode", this.inputmode || null);
256
+ setAttribute(control, "spellcheck", this.monospace ? "false" : null);
257
+ setAttribute(control, "aria-invalid", this.error ? "true" : null);
258
+ setAttribute(control, "aria-describedby", described || null);
259
+ control.required = this.required;
260
+ control.disabled = this.disabled;
165
261
 
166
- if (input.value !== this.value) input.value = this.value;
262
+ if (control.value !== this.value) control.value = this.value;
167
263
 
264
+ this.#reveal.hidden = !reveal;
265
+ this.#reveal.disabled = this.disabled;
266
+ setAttribute(
267
+ this.#reveal,
268
+ "aria-label",
269
+ this.#revealed ? `Hide ${this.label}` : `Show ${this.label}`,
270
+ );
271
+ setAttribute(this.#reveal, "aria-pressed", String(this.#revealed));
272
+ setIconPath(this.#revealIcon, this.#revealed ? hideIcon : showIcon);
168
273
  this.#hint.textContent = this.hint;
169
274
  this.#hint.hidden = !this.hint;
170
275
  this.#error.textContent = this.error;
@@ -24,6 +24,11 @@ export interface OptionItem {
24
24
  readonly swatches?: readonly string[];
25
25
  /** One color shown as a dot before the label, such as a tag's color. */
26
26
  readonly swatch?: string;
27
+ /**
28
+ * Shows the option but stops it being picked, such as one that isn't
29
+ * available here. Say why in its `description`.
30
+ */
31
+ readonly disabled?: boolean;
27
32
  }
28
33
 
29
34
  // MDI's magnify and check
@@ -202,6 +207,13 @@ export class GwOptionList extends GwElement {
202
207
  color: var(--gw-color-text-strong);
203
208
  }
204
209
 
210
+ /* A disabled option can still be reached, so its reason is read, but
211
+ reads as unavailable and can't be picked */
212
+ [role="option"][aria-disabled="true"] {
213
+ color: var(--gw-color-text-disabled);
214
+ cursor: not-allowed;
215
+ }
216
+
205
217
  [role="option"] > .icon {
206
218
  flex: none;
207
219
  color: var(--gw-color-text-primary);
@@ -366,7 +378,7 @@ export class GwOptionList extends GwElement {
366
378
  }
367
379
 
368
380
  #pick(option: OptionItem | undefined) {
369
- if (option === undefined) return;
381
+ if (option === undefined || option.disabled === true) return;
370
382
 
371
383
  this.dispatchEvent(
372
384
  new CustomEvent("pick", {
@@ -630,6 +642,7 @@ export class GwOptionList extends GwElement {
630
642
  id: `${this.#id}-${index}`,
631
643
  "data-index": index,
632
644
  "aria-selected": selected ? "true" : "false",
645
+ "aria-disabled": option.disabled === true ? "true" : null,
633
646
  "aria-setsize": this.#filtered.length,
634
647
  "aria-posinset": index + 1,
635
648
  "data-active": index === this.#active,
package/src/gw-select.ts CHANGED
@@ -216,6 +216,8 @@ export class GwSelect extends LightElement {
216
216
  item.description = option.dataset.description;
217
217
  }
218
218
 
219
+ if (option.disabled) item.disabled = true;
220
+
219
221
  if (group instanceof HTMLOptGroupElement) item.group = group.label;
220
222
 
221
223
  return item;