@timmo001/groundwork-ui 0.9.8 → 0.9.10

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); } /* 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-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-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; } `",
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; } `",
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
  {
@@ -9771,7 +9771,7 @@
9771
9771
  "text": "Properties"
9772
9772
  },
9773
9773
  "static": true,
9774
- "default": "{ label: {}, name: {}, type: {}, value: {}, placeholder: {}, autocomplete: {}, hint: {}, info: {}, error: {}, required: { type: \"boolean\" }, disabled: { type: \"boolean\" }, size: { reflect: true }, }",
9774
+ "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 }, }",
9775
9775
  "inheritedFrom": {
9776
9776
  "name": "GwElement",
9777
9777
  "module": "src/element.ts"
@@ -9874,12 +9874,36 @@
9874
9874
  },
9875
9875
  "default": "\"medium\""
9876
9876
  },
9877
+ {
9878
+ "kind": "field",
9879
+ "name": "icon",
9880
+ "type": {
9881
+ "text": "string"
9882
+ },
9883
+ "description": "SVG path data for an icon inside the field, before the text.",
9884
+ "default": "\"\""
9885
+ },
9886
+ {
9887
+ "kind": "field",
9888
+ "name": "hideLabel",
9889
+ "type": {
9890
+ "text": "boolean"
9891
+ },
9892
+ "description": "Keeps the label as the accessible name without showing it.",
9893
+ "default": "false"
9894
+ },
9877
9895
  {
9878
9896
  "kind": "field",
9879
9897
  "name": "#id",
9880
9898
  "privacy": "private",
9881
9899
  "readonly": true
9882
9900
  },
9901
+ {
9902
+ "kind": "field",
9903
+ "name": "#icon",
9904
+ "privacy": "private",
9905
+ "readonly": true
9906
+ },
9883
9907
  {
9884
9908
  "kind": "field",
9885
9909
  "name": "#label",
@@ -10320,6 +10344,20 @@
10320
10344
  },
10321
10345
  "description": "The field's size, 40px tall at medium. Defaults to `medium`.",
10322
10346
  "name": "size"
10347
+ },
10348
+ {
10349
+ "type": {
10350
+ "text": "string"
10351
+ },
10352
+ "description": "SVG path data for an icon inside the field, before the text, such as a magnifier on a search.",
10353
+ "name": "icon"
10354
+ },
10355
+ {
10356
+ "type": {
10357
+ "text": "boolean"
10358
+ },
10359
+ "description": "Keeps the label as the field's accessible name but doesn't show it, for a field in a toolbar whose icon and placeholder say what it's for.",
10360
+ "name": "hide-label"
10323
10361
  }
10324
10362
  ],
10325
10363
  "superclass": {
@@ -16524,23 +16562,64 @@
16524
16562
  },
16525
16563
  {
16526
16564
  "kind": "javascript-module",
16527
- "path": "src/gw-skeleton.ts",
16565
+ "path": "src/gw-sheet.ts",
16528
16566
  "declarations": [
16529
16567
  {
16530
16568
  "kind": "class",
16531
- "description": "A placeholder for what's loading, sized by its container or by `width`\nand `height` in CSS. The shapes below extend it, and components with a\n`loading` attribute draw their own from them, so a spinner is only needed\nfor an action in progress. It's hidden from assistive technology, so mark\nthe region it sits in `aria-busy`.",
16532
- "name": "GwSkeleton",
16569
+ "description": "A panel of filters, settings or details that slides in from an edge of the\nscreen over the page, rather than taking a place in its layout. With\n`side=\"auto\"` it comes from the start edge, full height, and from the\nbottom on narrow screens.\n\nIt's on the Popover API, in the top layer, and closes on Esc, a click\noutside or its close button, whose chevron points the way it leaves. Open\nit with `show()`, or `toggle()` from the button that opens it. Its content\nscrolls under a header with the heading and an `actions` slot, such as a\nClear button.",
16570
+ "name": "GwSheet",
16533
16571
  "cssProperties": [
16534
16572
  {
16535
- "description": "The fill. Defaults to the neutral fill.",
16536
- "name": "--gw-skeleton-color"
16573
+ "description": "Its width from the start or end edge. Defaults to `20rem`.",
16574
+ "name": "--gw-sheet-width"
16537
16575
  },
16538
16576
  {
16539
- "description": "The corners. Defaults to the control radius.",
16540
- "name": "--gw-skeleton-radius"
16577
+ "description": "Its greatest height from the bottom. Defaults to `85vh`.",
16578
+ "name": "--gw-sheet-height"
16579
+ }
16580
+ ],
16581
+ "cssParts": [
16582
+ {
16583
+ "description": "The sheet's surface.",
16584
+ "name": "surface"
16585
+ },
16586
+ {
16587
+ "description": "The row with the close button, heading and actions.",
16588
+ "name": "header"
16589
+ },
16590
+ {
16591
+ "description": "The heading.",
16592
+ "name": "heading"
16593
+ },
16594
+ {
16595
+ "description": "The content.",
16596
+ "name": "body"
16597
+ }
16598
+ ],
16599
+ "slots": [
16600
+ {
16601
+ "description": "The content, which scrolls.",
16602
+ "name": ""
16603
+ },
16604
+ {
16605
+ "description": "Buttons in the header, after the heading.",
16606
+ "name": "actions"
16541
16607
  }
16542
16608
  ],
16543
16609
  "members": [
16610
+ {
16611
+ "kind": "field",
16612
+ "name": "properties",
16613
+ "type": {
16614
+ "text": "Properties"
16615
+ },
16616
+ "static": true,
16617
+ "default": "{ heading: {}, side: { reflect: true }, }",
16618
+ "inheritedFrom": {
16619
+ "name": "GwElement",
16620
+ "module": "src/element.ts"
16621
+ }
16622
+ },
16544
16623
  {
16545
16624
  "kind": "field",
16546
16625
  "name": "styles",
@@ -16548,7 +16627,7 @@
16548
16627
  "text": "readonly CSSStyleSheet[]"
16549
16628
  },
16550
16629
  "static": true,
16551
- "default": "[ base, css` :host { display: block; min-height: 1rem; background: var( --gw-skeleton-color, var(--gw-color-fill-neutral-resting) ); border-radius: var(--gw-skeleton-radius, var(--gw-radius-control)); animation: pulse 2s ease-in-out infinite; } @keyframes pulse { 50% { opacity: 0.45; } } @media (forced-colors: active) { :host { background: GrayText; } } `, ]",
16630
+ "default": "[ base, css` [popover] { position: fixed; box-sizing: border-box; flex-direction: column; padding: 0; margin: 0; overflow: hidden; color: var(--gw-color-text-primary); background: var(--gw-color-surface-overlay); border: 0 solid var(--gw-color-border-normal); box-shadow: var(--gw-shadow-l); transition: translate var(--gw-motion-normal) var(--gw-motion-easing), display var(--gw-motion-normal) allow-discrete, overlay var(--gw-motion-normal) allow-discrete; } [popover]:popover-open { display: flex; translate: 0 0; } [popover]::backdrop { background: rgb(0 0 0 / 40%); } /* Full height from the start or end edge */ [data-side=\"start\"], [data-side=\"end\"] { inset: 0 auto; width: min(var(--gw-sheet-width, 20rem), 100vw); height: 100%; max-height: none; } [data-side=\"start\"] { inset-inline-start: 0; border-inline-end-width: max(var(--gw-border-width), 1px); translate: -100% 0; } [data-side=\"end\"] { inset-inline-end: 0; border-inline-start-width: max(var(--gw-border-width), 1px); translate: 100% 0; } :host(:dir(rtl)) [data-side=\"start\"] { translate: 100% 0; } :host(:dir(rtl)) [data-side=\"end\"] { translate: -100% 0; } /* Up from the bottom, with room for the phone's home bar */ [data-side=\"bottom\"] { inset: auto 0 0; width: 100%; max-width: none; max-height: var(--gw-sheet-height, 85vh); padding-block-end: env(safe-area-inset-bottom); border-block-start-width: max(var(--gw-border-width), 1px); border-start-start-radius: var(--gw-radius-panel); border-start-end-radius: var(--gw-radius-panel); translate: 0 100%; } @starting-style { [data-side=\"start\"]:popover-open { translate: -100% 0; } [data-side=\"end\"]:popover-open { translate: 100% 0; } [data-side=\"bottom\"]:popover-open { translate: 0 100%; } } header { display: flex; flex: none; gap: var(--gw-space-2); align-items: center; padding: var(--gw-space-2) var(--gw-space-3); border-block-end: var(--gw-border-width) solid var(--gw-color-border-quiet); } h2 { flex: 1; min-width: 0; margin: 0; font-size: var(--gw-font-size-m); font-weight: var(--gw-font-weight-strong); color: var(--gw-color-text-strong); } .body { display: flex; flex: 1; flex-direction: column; gap: var(--gw-space-3); min-height: 0; padding: var(--gw-space-4); /* It scrolls down only, so content reaching into its padding never makes it scroll sideways */ overflow: hidden auto; overscroll-behavior: contain; scrollbar-width: thin; } `, ]",
16552
16631
  "inheritedFrom": {
16553
16632
  "name": "GwElement",
16554
16633
  "module": "src/element.ts"
@@ -16556,12 +16635,128 @@
16556
16635
  },
16557
16636
  {
16558
16637
  "kind": "field",
16559
- "name": "properties",
16638
+ "name": "heading",
16560
16639
  "type": {
16561
- "text": "Properties"
16640
+ "text": "string"
16562
16641
  },
16563
- "static": true,
16564
- "default": "{}",
16642
+ "default": "\"\""
16643
+ },
16644
+ {
16645
+ "kind": "field",
16646
+ "name": "side",
16647
+ "type": {
16648
+ "text": "\"auto\" | Side"
16649
+ },
16650
+ "default": "\"auto\""
16651
+ },
16652
+ {
16653
+ "kind": "field",
16654
+ "name": "#heading",
16655
+ "privacy": "private",
16656
+ "readonly": true
16657
+ },
16658
+ {
16659
+ "kind": "field",
16660
+ "name": "#closeButton",
16661
+ "privacy": "private",
16662
+ "readonly": true
16663
+ },
16664
+ {
16665
+ "kind": "field",
16666
+ "name": "#surface",
16667
+ "privacy": "private",
16668
+ "type": {
16669
+ "text": "HTMLDivElement"
16670
+ },
16671
+ "readonly": true
16672
+ },
16673
+ {
16674
+ "kind": "field",
16675
+ "name": "#narrow",
16676
+ "privacy": "private",
16677
+ "readonly": true
16678
+ },
16679
+ {
16680
+ "kind": "field",
16681
+ "name": "#returnFocus",
16682
+ "privacy": "private",
16683
+ "type": {
16684
+ "text": "HTMLElement | undefined"
16685
+ }
16686
+ },
16687
+ {
16688
+ "kind": "field",
16689
+ "name": "open",
16690
+ "description": "Whether it's open.",
16691
+ "readonly": true
16692
+ },
16693
+ {
16694
+ "kind": "method",
16695
+ "name": "show"
16696
+ },
16697
+ {
16698
+ "kind": "method",
16699
+ "name": "close"
16700
+ },
16701
+ {
16702
+ "kind": "method",
16703
+ "name": "toggle",
16704
+ "description": "Opens it, or closes it if it's open, as the button that opens it would."
16705
+ },
16706
+ {
16707
+ "kind": "method",
16708
+ "name": "#edge",
16709
+ "privacy": "private",
16710
+ "return": {
16711
+ "type": {
16712
+ "text": "Side"
16713
+ }
16714
+ },
16715
+ "description": "The edge it comes from now, with `auto` settled by the screen's width."
16716
+ },
16717
+ {
16718
+ "kind": "field",
16719
+ "name": "#place",
16720
+ "privacy": "private",
16721
+ "readonly": true
16722
+ },
16723
+ {
16724
+ "kind": "field",
16725
+ "name": "#onToggle",
16726
+ "privacy": "private",
16727
+ "readonly": true
16728
+ },
16729
+ {
16730
+ "kind": "method",
16731
+ "name": "create",
16732
+ "privacy": "protected",
16733
+ "parameters": [
16734
+ {
16735
+ "name": "root",
16736
+ "type": {
16737
+ "text": "ShadowRoot"
16738
+ }
16739
+ }
16740
+ ],
16741
+ "description": "Builds the shadow root's content once, on first connection.",
16742
+ "inheritedFrom": {
16743
+ "name": "GwElement",
16744
+ "module": "src/element.ts"
16745
+ }
16746
+ },
16747
+ {
16748
+ "kind": "method",
16749
+ "name": "update",
16750
+ "privacy": "protected",
16751
+ "parameters": [
16752
+ {
16753
+ "name": "_changed",
16754
+ "type": {
16755
+ "text": "ReadonlySet<string>"
16756
+ }
16757
+ }
16758
+ ],
16759
+ "description": "Sets what the changed properties affect.",
16565
16760
  "inheritedFrom": {
16566
16761
  "name": "GwElement",
16567
16762
  "module": "src/element.ts"
@@ -16804,42 +16999,6 @@
16804
16999
  "module": "src/element.ts"
16805
17000
  }
16806
17001
  },
16807
- {
16808
- "kind": "method",
16809
- "name": "create",
16810
- "privacy": "protected",
16811
- "parameters": [
16812
- {
16813
- "name": "_root",
16814
- "type": {
16815
- "text": "ShadowRoot | HTMLElement"
16816
- }
16817
- }
16818
- ],
16819
- "description": "Builds the shadow root's content once, on first connection.",
16820
- "inheritedFrom": {
16821
- "name": "GwElement",
16822
- "module": "src/element.ts"
16823
- }
16824
- },
16825
- {
16826
- "kind": "method",
16827
- "name": "update",
16828
- "privacy": "protected",
16829
- "parameters": [
16830
- {
16831
- "name": "_changed",
16832
- "type": {
16833
- "text": "ReadonlySet<string>"
16834
- }
16835
- }
16836
- ],
16837
- "description": "Sets what the changed properties affect.",
16838
- "inheritedFrom": {
16839
- "name": "GwElement",
16840
- "module": "src/element.ts"
16841
- }
16842
- },
16843
17002
  {
16844
17003
  "kind": "method",
16845
17004
  "name": "#flush",
@@ -16879,17 +17038,419 @@
16879
17038
  }
16880
17039
  }
16881
17040
  ],
17041
+ "events": [
17042
+ {
17043
+ "type": {
17044
+ "text": "Event"
17045
+ },
17046
+ "description": "When it closes.",
17047
+ "name": "close"
17048
+ }
17049
+ ],
17050
+ "attributes": [
17051
+ {
17052
+ "type": {
17053
+ "text": "string"
17054
+ },
17055
+ "description": "The heading beside the close button.",
17056
+ "name": "heading"
17057
+ },
17058
+ {
17059
+ "type": {
17060
+ "text": "\"auto\"|\"start\"|\"end\"|\"bottom\""
17061
+ },
17062
+ "description": "The edge it slides in from. `auto` is the start edge, or the bottom on narrow screens. Defaults to `auto`.",
17063
+ "name": "side"
17064
+ }
17065
+ ],
16882
17066
  "superclass": {
16883
17067
  "name": "GwElement",
16884
17068
  "module": "/src/element.js"
16885
17069
  },
16886
- "tagName": "gw-skeleton",
17070
+ "tagName": "gw-sheet",
16887
17071
  "customElement": true,
16888
- "summary": "A pulsing placeholder for what's loading."
16889
- },
17072
+ "summary": "A panel that slides in from an edge of the screen."
17073
+ }
17074
+ ],
17075
+ "exports": [
16890
17076
  {
16891
- "kind": "class",
16892
- "description": "A line of text. Its height follows the font size around it, so it lines up\nwith the text it stands in for.",
17077
+ "kind": "js",
17078
+ "name": "GwSheet",
17079
+ "declaration": {
17080
+ "name": "GwSheet",
17081
+ "module": "src/gw-sheet.ts"
17082
+ }
17083
+ }
17084
+ ]
17085
+ },
17086
+ {
17087
+ "kind": "javascript-module",
17088
+ "path": "src/gw-skeleton.ts",
17089
+ "declarations": [
17090
+ {
17091
+ "kind": "class",
17092
+ "description": "A placeholder for what's loading, sized by its container or by `width`\nand `height` in CSS. The shapes below extend it, and components with a\n`loading` attribute draw their own from them, so a spinner is only needed\nfor an action in progress. It's hidden from assistive technology, so mark\nthe region it sits in `aria-busy`.",
17093
+ "name": "GwSkeleton",
17094
+ "cssProperties": [
17095
+ {
17096
+ "description": "The fill. Defaults to the neutral fill.",
17097
+ "name": "--gw-skeleton-color"
17098
+ },
17099
+ {
17100
+ "description": "The corners. Defaults to the control radius.",
17101
+ "name": "--gw-skeleton-radius"
17102
+ }
17103
+ ],
17104
+ "members": [
17105
+ {
17106
+ "kind": "field",
17107
+ "name": "styles",
17108
+ "type": {
17109
+ "text": "readonly CSSStyleSheet[]"
17110
+ },
17111
+ "static": true,
17112
+ "default": "[ base, css` :host { display: block; min-height: 1rem; background: var( --gw-skeleton-color, var(--gw-color-fill-neutral-resting) ); border-radius: var(--gw-skeleton-radius, var(--gw-radius-control)); animation: pulse 2s ease-in-out infinite; } @keyframes pulse { 50% { opacity: 0.45; } } @media (forced-colors: active) { :host { background: GrayText; } } `, ]",
17113
+ "inheritedFrom": {
17114
+ "name": "GwElement",
17115
+ "module": "src/element.ts"
17116
+ }
17117
+ },
17118
+ {
17119
+ "kind": "field",
17120
+ "name": "properties",
17121
+ "type": {
17122
+ "text": "Properties"
17123
+ },
17124
+ "static": true,
17125
+ "default": "{}",
17126
+ "inheritedFrom": {
17127
+ "name": "GwElement",
17128
+ "module": "src/element.ts"
17129
+ }
17130
+ },
17131
+ {
17132
+ "kind": "field",
17133
+ "name": "shadow",
17134
+ "type": {
17135
+ "text": "boolean"
17136
+ },
17137
+ "static": true,
17138
+ "default": "true",
17139
+ "description": "Whether to attach a shadow root. Light DOM elements set this false.",
17140
+ "inheritedFrom": {
17141
+ "name": "GwElement",
17142
+ "module": "src/element.ts"
17143
+ }
17144
+ },
17145
+ {
17146
+ "kind": "field",
17147
+ "name": "shadowRootOptions",
17148
+ "type": {
17149
+ "text": "ShadowRootInit"
17150
+ },
17151
+ "static": true,
17152
+ "default": "{ mode: \"open\" }",
17153
+ "description": "How the shadow root is attached.",
17154
+ "inheritedFrom": {
17155
+ "name": "GwElement",
17156
+ "module": "src/element.ts"
17157
+ }
17158
+ },
17159
+ {
17160
+ "kind": "method",
17161
+ "name": "define",
17162
+ "static": true,
17163
+ "parameters": [
17164
+ {
17165
+ "name": "tag",
17166
+ "type": {
17167
+ "text": "string"
17168
+ }
17169
+ }
17170
+ ],
17171
+ "description": "Adds the property accessors, then defines the element.",
17172
+ "inheritedFrom": {
17173
+ "name": "GwElement",
17174
+ "module": "src/element.ts"
17175
+ }
17176
+ },
17177
+ {
17178
+ "kind": "field",
17179
+ "name": "#values",
17180
+ "privacy": "private",
17181
+ "readonly": true,
17182
+ "default": "new Map<string, PropertyValue>()",
17183
+ "inheritedFrom": {
17184
+ "name": "GwElement",
17185
+ "module": "src/element.ts"
17186
+ }
17187
+ },
17188
+ {
17189
+ "kind": "field",
17190
+ "name": "#defaults",
17191
+ "privacy": "private",
17192
+ "readonly": true,
17193
+ "default": "new Map<string, PropertyValue>()",
17194
+ "inheritedFrom": {
17195
+ "name": "GwElement",
17196
+ "module": "src/element.ts"
17197
+ }
17198
+ },
17199
+ {
17200
+ "kind": "field",
17201
+ "name": "#changed",
17202
+ "privacy": "private",
17203
+ "readonly": true,
17204
+ "default": "new Set<string>()",
17205
+ "inheritedFrom": {
17206
+ "name": "GwElement",
17207
+ "module": "src/element.ts"
17208
+ }
17209
+ },
17210
+ {
17211
+ "kind": "field",
17212
+ "name": "#unreflected",
17213
+ "privacy": "private",
17214
+ "readonly": true,
17215
+ "default": "new Set<string>()",
17216
+ "inheritedFrom": {
17217
+ "name": "GwElement",
17218
+ "module": "src/element.ts"
17219
+ }
17220
+ },
17221
+ {
17222
+ "kind": "field",
17223
+ "name": "#created",
17224
+ "privacy": "private",
17225
+ "type": {
17226
+ "text": "boolean"
17227
+ },
17228
+ "default": "false",
17229
+ "inheritedFrom": {
17230
+ "name": "GwElement",
17231
+ "module": "src/element.ts"
17232
+ }
17233
+ },
17234
+ {
17235
+ "kind": "field",
17236
+ "name": "#queued",
17237
+ "privacy": "private",
17238
+ "type": {
17239
+ "text": "boolean"
17240
+ },
17241
+ "default": "false",
17242
+ "inheritedFrom": {
17243
+ "name": "GwElement",
17244
+ "module": "src/element.ts"
17245
+ }
17246
+ },
17247
+ {
17248
+ "kind": "field",
17249
+ "name": "#pending",
17250
+ "privacy": "private",
17251
+ "type": {
17252
+ "text": "Promise<void> | undefined"
17253
+ },
17254
+ "inheritedFrom": {
17255
+ "name": "GwElement",
17256
+ "module": "src/element.ts"
17257
+ }
17258
+ },
17259
+ {
17260
+ "kind": "field",
17261
+ "name": "#reflecting",
17262
+ "privacy": "private",
17263
+ "type": {
17264
+ "text": "boolean"
17265
+ },
17266
+ "default": "false",
17267
+ "inheritedFrom": {
17268
+ "name": "GwElement",
17269
+ "module": "src/element.ts"
17270
+ }
17271
+ },
17272
+ {
17273
+ "kind": "field",
17274
+ "name": "#resolveUpdated",
17275
+ "privacy": "private",
17276
+ "type": {
17277
+ "text": "() => void"
17278
+ },
17279
+ "inheritedFrom": {
17280
+ "name": "GwElement",
17281
+ "module": "src/element.ts"
17282
+ }
17283
+ },
17284
+ {
17285
+ "kind": "field",
17286
+ "name": "#updated",
17287
+ "privacy": "private",
17288
+ "readonly": true,
17289
+ "default": "new Promise<void>((resolve) => { this.#resolveUpdated = resolve; })",
17290
+ "inheritedFrom": {
17291
+ "name": "GwElement",
17292
+ "module": "src/element.ts"
17293
+ }
17294
+ },
17295
+ {
17296
+ "kind": "field",
17297
+ "name": "renderRoot",
17298
+ "type": {
17299
+ "text": "ShadowRoot | HTMLElement"
17300
+ },
17301
+ "readonly": true,
17302
+ "description": "The shadow root, or the element itself for a light DOM element.",
17303
+ "inheritedFrom": {
17304
+ "name": "GwElement",
17305
+ "module": "src/element.ts"
17306
+ }
17307
+ },
17308
+ {
17309
+ "kind": "field",
17310
+ "name": "#own",
17311
+ "privacy": "private",
17312
+ "type": {
17313
+ "text": "typeof GwElement"
17314
+ },
17315
+ "readonly": true,
17316
+ "description": "The element's own class, for its static properties and styles.",
17317
+ "default": "new.target",
17318
+ "inheritedFrom": {
17319
+ "name": "GwElement",
17320
+ "module": "src/element.ts"
17321
+ }
17322
+ },
17323
+ {
17324
+ "kind": "field",
17325
+ "name": "updateComplete",
17326
+ "description": "Resolves once the pending update has run, or the first one before it connects.",
17327
+ "readonly": true,
17328
+ "inheritedFrom": {
17329
+ "name": "GwElement",
17330
+ "module": "src/element.ts"
17331
+ }
17332
+ },
17333
+ {
17334
+ "kind": "method",
17335
+ "name": "defaultOf",
17336
+ "privacy": "protected",
17337
+ "parameters": [
17338
+ {
17339
+ "name": "name",
17340
+ "type": {
17341
+ "text": "string"
17342
+ }
17343
+ }
17344
+ ],
17345
+ "description": "The value a property returns to when its attribute is removed. Defaults\nto the value it was given in the constructor.",
17346
+ "inheritedFrom": {
17347
+ "name": "GwElement",
17348
+ "module": "src/element.ts"
17349
+ }
17350
+ },
17351
+ {
17352
+ "kind": "method",
17353
+ "name": "requestUpdate",
17354
+ "parameters": [
17355
+ {
17356
+ "name": "names",
17357
+ "type": {
17358
+ "text": "readonly string[]"
17359
+ }
17360
+ }
17361
+ ],
17362
+ "description": "Asks for an update with the given properties marked as changed.",
17363
+ "inheritedFrom": {
17364
+ "name": "GwElement",
17365
+ "module": "src/element.ts"
17366
+ }
17367
+ },
17368
+ {
17369
+ "kind": "method",
17370
+ "name": "create",
17371
+ "privacy": "protected",
17372
+ "parameters": [
17373
+ {
17374
+ "name": "_root",
17375
+ "type": {
17376
+ "text": "ShadowRoot | HTMLElement"
17377
+ }
17378
+ }
17379
+ ],
17380
+ "description": "Builds the shadow root's content once, on first connection.",
17381
+ "inheritedFrom": {
17382
+ "name": "GwElement",
17383
+ "module": "src/element.ts"
17384
+ }
17385
+ },
17386
+ {
17387
+ "kind": "method",
17388
+ "name": "update",
17389
+ "privacy": "protected",
17390
+ "parameters": [
17391
+ {
17392
+ "name": "_changed",
17393
+ "type": {
17394
+ "text": "ReadonlySet<string>"
17395
+ }
17396
+ }
17397
+ ],
17398
+ "description": "Sets what the changed properties affect.",
17399
+ "inheritedFrom": {
17400
+ "name": "GwElement",
17401
+ "module": "src/element.ts"
17402
+ }
17403
+ },
17404
+ {
17405
+ "kind": "method",
17406
+ "name": "#flush",
17407
+ "privacy": "private",
17408
+ "inheritedFrom": {
17409
+ "name": "GwElement",
17410
+ "module": "src/element.ts"
17411
+ }
17412
+ },
17413
+ {
17414
+ "kind": "method",
17415
+ "name": "#setProperty",
17416
+ "privacy": "private",
17417
+ "parameters": [
17418
+ {
17419
+ "name": "name",
17420
+ "type": {
17421
+ "text": "string"
17422
+ }
17423
+ },
17424
+ {
17425
+ "name": "options",
17426
+ "type": {
17427
+ "text": "PropertyOptions"
17428
+ }
17429
+ },
17430
+ {
17431
+ "name": "value",
17432
+ "type": {
17433
+ "text": "PropertyValue"
17434
+ }
17435
+ }
17436
+ ],
17437
+ "inheritedFrom": {
17438
+ "name": "GwElement",
17439
+ "module": "src/element.ts"
17440
+ }
17441
+ }
17442
+ ],
17443
+ "superclass": {
17444
+ "name": "GwElement",
17445
+ "module": "/src/element.js"
17446
+ },
17447
+ "tagName": "gw-skeleton",
17448
+ "customElement": true,
17449
+ "summary": "A pulsing placeholder for what's loading."
17450
+ },
17451
+ {
17452
+ "kind": "class",
17453
+ "description": "A line of text. Its height follows the font size around it, so it lines up\nwith the text it stands in for.",
16893
17454
  "name": "GwSkeletonText",
16894
17455
  "cssProperties": [
16895
17456
  {
@@ -20333,6 +20894,14 @@
20333
20894
  "module": "src/gw-setting-row.js"
20334
20895
  }
20335
20896
  },
20897
+ {
20898
+ "kind": "js",
20899
+ "name": "*",
20900
+ "declaration": {
20901
+ "name": "*",
20902
+ "module": "src/gw-sheet.js"
20903
+ }
20904
+ },
20336
20905
  {
20337
20906
  "kind": "js",
20338
20907
  "name": "*",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timmo001/groundwork-ui",
3
- "version": "0.9.8",
3
+ "version": "0.9.10",
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/events.ts CHANGED
@@ -35,6 +35,9 @@ export interface GwEventMap {
35
35
  "gw-select": {
36
36
  change: Event;
37
37
  };
38
+ "gw-sheet": {
39
+ close: Event;
40
+ };
38
41
  "gw-theme-gallery": {
39
42
  theme: CustomEvent<{ id: string }>;
40
43
  };
package/src/fields.ts CHANGED
@@ -108,6 +108,34 @@ export const fieldStyles = css`
108
108
  transition: border-color var(--gw-motion-fast) var(--gw-motion-easing);
109
109
  }
110
110
 
111
+ /* An icon inside the field, before the text, with the text moved clear of
112
+ it */
113
+ gw-input .gw-input-control {
114
+ position: relative;
115
+ display: flex;
116
+ align-items: center;
117
+ }
118
+
119
+ gw-input .gw-input-control > .icon {
120
+ position: absolute;
121
+ inset-inline-start: calc(var(--gw-space-3) * var(--gw-field-scale, 1));
122
+ width: calc(1.125rem * var(--gw-field-scale, 1));
123
+ height: calc(1.125rem * var(--gw-field-scale, 1));
124
+ color: var(--gw-color-text-secondary);
125
+ pointer-events: none;
126
+ fill: currentColor;
127
+ }
128
+
129
+ gw-input .gw-input-control > .icon[hidden] {
130
+ display: none;
131
+ }
132
+
133
+ gw-input .gw-input-control > .icon:not([hidden]) + input {
134
+ padding-inline-start: calc(
135
+ (var(--gw-space-3) * 2 + 1.125rem) * var(--gw-field-scale, 1)
136
+ );
137
+ }
138
+
111
139
  /* The trigger shows the chosen option, with its icon, and a chevron with
112
140
  room around it that turns over while the list is open */
113
141
  gw-select .gw-select-trigger {
@@ -276,6 +304,10 @@ export const fieldStyles = css`
276
304
  display: none;
277
305
  }
278
306
 
307
+ :where(gw-input) .gw-input-control:focus-within > .icon {
308
+ color: var(--gw-color-text-strong);
309
+ }
310
+
279
311
  :where(gw-input, gw-select) :where(input, .gw-select-trigger):hover {
280
312
  border-color: var(--gw-color-border-loud);
281
313
  }
@@ -315,7 +347,7 @@ export const fieldStyles = css`
315
347
  cursor: pointer;
316
348
  }
317
349
 
318
- :where(gw-switch, gw-checkbox, gw-select) .gw-visually-hidden {
350
+ :where(gw-input, gw-switch, gw-checkbox, gw-select) .gw-visually-hidden {
319
351
  position: absolute;
320
352
  width: 1px;
321
353
  height: 1px;
package/src/gw-input.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  * @module
6
6
  */
7
7
  import { h, setAttribute } from "./element.js";
8
+ import { createIcon, setIconPath } from "./styles.js";
8
9
  import { FieldInfo } from "./field-info.js";
9
10
  import { LightElement, fieldId } from "./fields.js";
10
11
 
@@ -26,6 +27,8 @@ import { LightElement, fieldId } from "./fields.js";
26
27
  * @attr {boolean} required - Requires a value.
27
28
  * @attr {boolean} disabled - Disables the field.
28
29
  * @attr {"xs"|"small"|"medium"|"large"|"xl"} size - The field's size, 40px tall at medium. Defaults to `medium`.
30
+ * @attr {string} icon - SVG path data for an icon inside the field, before the text, such as a magnifier on a search.
31
+ * @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.
29
32
  *
30
33
  * @cssprop --gw-field-scale - Scales the field, around 1 for medium.
31
34
  * @cssprop --gw-control-scale - Scales its height with the theme's density. The theme sets it.
@@ -44,6 +47,8 @@ export class GwInput extends LightElement {
44
47
  required: { type: "boolean" },
45
48
  disabled: { type: "boolean" },
46
49
  size: { reflect: true },
50
+ icon: {},
51
+ hideLabel: { type: "boolean", attribute: "hide-label", reflect: true },
47
52
  } as const;
48
53
 
49
54
  declare label: string;
@@ -59,9 +64,15 @@ export class GwInput extends LightElement {
59
64
  declare required: boolean;
60
65
  declare disabled: boolean;
61
66
  declare size: "xs" | "small" | "medium" | "large" | "xl";
67
+ /** SVG path data for an icon inside the field, before the text. */
68
+ declare icon: string;
69
+ /** Keeps the label as the accessible name without showing it. */
70
+ declare hideLabel: boolean;
62
71
 
63
72
  readonly #id = fieldId();
64
73
 
74
+ readonly #icon = createIcon("");
75
+
65
76
  readonly #label = h("label", { for: this.#id });
66
77
 
67
78
  readonly #info = new FieldInfo(this.#label);
@@ -86,13 +97,20 @@ export class GwInput extends LightElement {
86
97
  this.required = false;
87
98
  this.disabled = false;
88
99
  this.size = "medium";
100
+ this.icon = "";
101
+ this.hideLabel = false;
89
102
  }
90
103
 
91
104
  protected override create(root: HTMLElement) {
92
105
  this.#input.addEventListener("input", () => {
93
106
  this.value = this.#input.value;
94
107
  });
95
- root.append(this.#info.heading, this.#input, this.#hint, this.#error);
108
+ root.append(
109
+ this.#info.heading,
110
+ h("span", { class: "gw-input-control" }, this.#icon, this.#input),
111
+ this.#hint,
112
+ this.#error,
113
+ );
96
114
  }
97
115
 
98
116
  protected override update() {
@@ -107,6 +125,9 @@ export class GwInput extends LightElement {
107
125
 
108
126
  this.#label.textContent = this.label;
109
127
  this.#info.update(this.label, this.info);
128
+ this.#info.heading.classList.toggle("gw-visually-hidden", this.hideLabel);
129
+ setIconPath(this.#icon, this.icon);
130
+ this.#icon.toggleAttribute("hidden", !this.icon);
110
131
  input.type = this.type;
111
132
  setAttribute(input, "name", this.name || null);
112
133
  setAttribute(input, "placeholder", this.placeholder || null);
@@ -0,0 +1,307 @@
1
+ /**
2
+ * `gw-sheet`: a panel that slides in from an edge of the screen.
3
+ * Docs: https://groundwork.timmo.dev/components/overlays/#sheet
4
+ *
5
+ * @module
6
+ */
7
+ import { GwElement, css, h, setAttribute } from "./element.js";
8
+ import "./gw-button.js";
9
+ import { base } from "./styles.js";
10
+
11
+ // MDI's chevrons, pointing the way the sheet leaves when it closes
12
+ const chevrons = {
13
+ start: "M15.41,16.58L10.83,12L15.41,7.41L14,6L8,12L14,18L15.41,16.58Z",
14
+ end: "M8.59,16.58L13.17,12L8.59,7.41L10,6L16,12L10,18L8.59,16.58Z",
15
+ bottom: "M7.41,8.58L12,13.17L16.59,8.58L18,10L12,16L6,10L7.41,8.58Z",
16
+ } as const;
17
+
18
+ type Side = keyof typeof chevrons;
19
+
20
+ /** Below this width, `side="auto"` slides up from the bottom. */
21
+ const narrow = "(width < 40rem)";
22
+
23
+ /**
24
+ * A panel of filters, settings or details that slides in from an edge of the
25
+ * screen over the page, rather than taking a place in its layout. With
26
+ * `side="auto"` it comes from the start edge, full height, and from the
27
+ * bottom on narrow screens.
28
+ *
29
+ * It's on the Popover API, in the top layer, and closes on Esc, a click
30
+ * outside or its close button, whose chevron points the way it leaves. Open
31
+ * it with `show()`, or `toggle()` from the button that opens it. Its content
32
+ * scrolls under a header with the heading and an `actions` slot, such as a
33
+ * Clear button.
34
+ *
35
+ * @element gw-sheet
36
+ * @summary A panel that slides in from an edge of the screen.
37
+ *
38
+ * @attr {string} heading - The heading beside the close button.
39
+ * @attr {"auto"|"start"|"end"|"bottom"} side - The edge it slides in from. `auto` is the start edge, or the bottom on narrow screens. Defaults to `auto`.
40
+ *
41
+ * @slot - The content, which scrolls.
42
+ * @slot actions - Buttons in the header, after the heading.
43
+ *
44
+ * @csspart surface - The sheet's surface.
45
+ * @csspart header - The row with the close button, heading and actions.
46
+ * @csspart heading - The heading.
47
+ * @csspart body - The content.
48
+ *
49
+ * @cssprop --gw-sheet-width - Its width from the start or end edge. Defaults to `20rem`.
50
+ * @cssprop --gw-sheet-height - Its greatest height from the bottom. Defaults to `85vh`.
51
+ *
52
+ * @fires {Event} close - When it closes.
53
+ */
54
+ export class GwSheet extends GwElement {
55
+ static override properties = {
56
+ heading: {},
57
+ side: { reflect: true },
58
+ } as const;
59
+
60
+ static override styles = [
61
+ base,
62
+ css`
63
+ [popover] {
64
+ position: fixed;
65
+ box-sizing: border-box;
66
+ flex-direction: column;
67
+ padding: 0;
68
+ margin: 0;
69
+ overflow: hidden;
70
+ color: var(--gw-color-text-primary);
71
+ background: var(--gw-color-surface-overlay);
72
+ border: 0 solid var(--gw-color-border-normal);
73
+ box-shadow: var(--gw-shadow-l);
74
+ transition:
75
+ translate var(--gw-motion-normal) var(--gw-motion-easing),
76
+ display var(--gw-motion-normal) allow-discrete,
77
+ overlay var(--gw-motion-normal) allow-discrete;
78
+ }
79
+
80
+ [popover]:popover-open {
81
+ display: flex;
82
+ translate: 0 0;
83
+ }
84
+
85
+ [popover]::backdrop {
86
+ background: rgb(0 0 0 / 40%);
87
+ }
88
+
89
+ /* Full height from the start or end edge */
90
+ [data-side="start"],
91
+ [data-side="end"] {
92
+ inset: 0 auto;
93
+ width: min(var(--gw-sheet-width, 20rem), 100vw);
94
+ height: 100%;
95
+ max-height: none;
96
+ }
97
+
98
+ [data-side="start"] {
99
+ inset-inline-start: 0;
100
+ border-inline-end-width: max(var(--gw-border-width), 1px);
101
+ translate: -100% 0;
102
+ }
103
+
104
+ [data-side="end"] {
105
+ inset-inline-end: 0;
106
+ border-inline-start-width: max(var(--gw-border-width), 1px);
107
+ translate: 100% 0;
108
+ }
109
+
110
+ :host(:dir(rtl)) [data-side="start"] {
111
+ translate: 100% 0;
112
+ }
113
+
114
+ :host(:dir(rtl)) [data-side="end"] {
115
+ translate: -100% 0;
116
+ }
117
+
118
+ /* Up from the bottom, with room for the phone's home bar */
119
+ [data-side="bottom"] {
120
+ inset: auto 0 0;
121
+ width: 100%;
122
+ max-width: none;
123
+ max-height: var(--gw-sheet-height, 85vh);
124
+ padding-block-end: env(safe-area-inset-bottom);
125
+ border-block-start-width: max(var(--gw-border-width), 1px);
126
+ border-start-start-radius: var(--gw-radius-panel);
127
+ border-start-end-radius: var(--gw-radius-panel);
128
+ translate: 0 100%;
129
+ }
130
+
131
+ @starting-style {
132
+ [data-side="start"]:popover-open {
133
+ translate: -100% 0;
134
+ }
135
+
136
+ [data-side="end"]:popover-open {
137
+ translate: 100% 0;
138
+ }
139
+
140
+ [data-side="bottom"]:popover-open {
141
+ translate: 0 100%;
142
+ }
143
+ }
144
+
145
+ header {
146
+ display: flex;
147
+ flex: none;
148
+ gap: var(--gw-space-2);
149
+ align-items: center;
150
+ padding: var(--gw-space-2) var(--gw-space-3);
151
+ border-block-end: var(--gw-border-width) solid
152
+ var(--gw-color-border-quiet);
153
+ }
154
+
155
+ h2 {
156
+ flex: 1;
157
+ min-width: 0;
158
+ margin: 0;
159
+ font-size: var(--gw-font-size-m);
160
+ font-weight: var(--gw-font-weight-strong);
161
+ color: var(--gw-color-text-strong);
162
+ }
163
+
164
+ .body {
165
+ display: flex;
166
+ flex: 1;
167
+ flex-direction: column;
168
+ gap: var(--gw-space-3);
169
+ min-height: 0;
170
+ padding: var(--gw-space-4);
171
+ /* It scrolls down only, so content reaching into its padding never
172
+ makes it scroll sideways */
173
+ overflow: hidden auto;
174
+ overscroll-behavior: contain;
175
+ scrollbar-width: thin;
176
+ }
177
+ `,
178
+ ];
179
+
180
+ declare heading: string;
181
+ declare side: "auto" | Side;
182
+
183
+ readonly #heading = h("h2", { id: "heading", part: "heading" });
184
+
185
+ readonly #closeButton = h(
186
+ "gw-button",
187
+ {
188
+ variant: "neutral",
189
+ appearance: "plain",
190
+ size: "small",
191
+ labels: "hide",
192
+ },
193
+ "Close",
194
+ );
195
+
196
+ readonly #surface: HTMLDivElement = h(
197
+ "div",
198
+ {
199
+ popover: "auto",
200
+ role: "dialog",
201
+ tabindex: "-1",
202
+ part: "surface",
203
+ },
204
+ h(
205
+ "header",
206
+ { part: "header" },
207
+ this.#closeButton,
208
+ this.#heading,
209
+ h("slot", { name: "actions" }),
210
+ ),
211
+ h("div", { class: "body", part: "body" }, h("slot")),
212
+ );
213
+
214
+ readonly #narrow = window.matchMedia(narrow);
215
+
216
+ // Where focus was before it opened, to go back to when it closes
217
+ #returnFocus: HTMLElement | undefined;
218
+
219
+ constructor() {
220
+ super();
221
+ this.heading = "";
222
+ this.side = "auto";
223
+ }
224
+
225
+ /** Whether it's open. */
226
+ get open() {
227
+ return this.#surface.matches(":popover-open");
228
+ }
229
+
230
+ show() {
231
+ if (this.open) return;
232
+
233
+ const active = document.activeElement;
234
+
235
+ this.#returnFocus = active instanceof HTMLElement ? active : undefined;
236
+ this.#place();
237
+ this.#surface.showPopover();
238
+ }
239
+
240
+ close() {
241
+ if (this.open) this.#surface.hidePopover();
242
+ }
243
+
244
+ /** Opens it, or closes it if it's open, as the button that opens it would. */
245
+ toggle() {
246
+ if (this.open) this.close();
247
+ else this.show();
248
+ }
249
+
250
+ /** The edge it comes from now, with `auto` settled by the screen's width. */
251
+ #edge(): Side {
252
+ if (this.side !== "auto") return this.side;
253
+
254
+ return this.#narrow.matches ? "bottom" : "start";
255
+ }
256
+
257
+ readonly #place = () => {
258
+ const edge = this.#edge();
259
+
260
+ this.#surface.dataset.side = edge;
261
+ this.#closeButton.icon = chevrons[edge];
262
+ };
263
+
264
+ readonly #onToggle = (event: ToggleEvent) => {
265
+ if (event.newState === "open") {
266
+ this.#narrow.addEventListener("change", this.#place);
267
+ this.#closeButton.focus();
268
+
269
+ return;
270
+ }
271
+
272
+ this.#narrow.removeEventListener("change", this.#place);
273
+ this.#returnFocus?.focus();
274
+ this.#returnFocus = undefined;
275
+ this.dispatchEvent(new Event("close", { bubbles: true, composed: true }));
276
+ };
277
+
278
+ override disconnectedCallback() {
279
+ this.#narrow.removeEventListener("change", this.#place);
280
+ super.disconnectedCallback();
281
+ }
282
+
283
+ protected override create(root: ShadowRoot) {
284
+ this.#surface.addEventListener("toggle", this.#onToggle);
285
+ this.#closeButton.addEventListener("click", () => this.close());
286
+ root.append(this.#surface);
287
+ }
288
+
289
+ protected override update() {
290
+ this.#heading.textContent = this.heading;
291
+ setAttribute(
292
+ this.#surface,
293
+ "aria-labelledby",
294
+ this.heading ? "heading" : null,
295
+ );
296
+ this.#heading.hidden = !this.heading;
297
+ this.#place();
298
+ }
299
+ }
300
+
301
+ GwSheet.define("gw-sheet");
302
+
303
+ declare global {
304
+ interface HTMLElementTagNameMap {
305
+ "gw-sheet": GwSheet;
306
+ }
307
+ }
package/src/index.ts CHANGED
@@ -73,6 +73,8 @@ export * from "./gw-select.js";
73
73
 
74
74
  export * from "./gw-setting-row.js";
75
75
 
76
+ export * from "./gw-sheet.js";
77
+
76
78
  export * from "./gw-skeleton.js";
77
79
 
78
80
  export * from "./gw-spinner.js";