@timmo001/groundwork-ui 0.9.16 → 0.9.18
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.
- package/custom-elements.json +72 -15
- package/package.json +1 -1
- package/src/events.ts +1 -0
- package/src/gw-command-menu.ts +9 -3
- package/src/gw-menu-button.ts +176 -5
- package/src/gw-option-list.ts +9 -2
- package/src/gw-theme-gallery.ts +13 -29
- package/src/gw-theme-picker.ts +19 -1
package/custom-elements.json
CHANGED
|
@@ -5899,7 +5899,7 @@
|
|
|
5899
5899
|
"text": "readonly CSSStyleSheet[]"
|
|
5900
5900
|
},
|
|
5901
5901
|
"static": true,
|
|
5902
|
-
"default": "[ base, css` dialog { top: 12vh; width: min(36rem, calc(100vw - 2 * var(--gw-space-3))); max-height: 70vh; padding: 0; margin-block: 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-
|
|
5902
|
+
"default": "[ base, css` dialog { top: 12vh; width: min(36rem, calc(100vw - 2 * var(--gw-space-3))); max-height: 70vh; padding: 0; margin-block: 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-loud); border-radius: var(--gw-radius-panel); box-shadow: var(--gw-shadow-l); --gw-option-list-height: min(24rem, 56vh); } dialog::backdrop { background: rgb(0 0 0 / 60%); } dialog, dialog::backdrop { opacity: 0; transition: opacity var(--gw-motion-normal) 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; } dialog { scale: 0.96; } dialog[open], dialog[open]::backdrop { opacity: 1; } dialog[open] { scale: 1; } @starting-style { dialog[open], dialog[open]::backdrop { opacity: 0; } dialog[open] { scale: 0.96; } } dialog[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; } dialog[data-placement=\"sheet\"][open] { opacity: 1; translate: 0 0; } @starting-style { dialog[data-placement=\"sheet\"][open] { translate: 0 100%; } } footer { display: flex; gap: var(--gw-space-3); padding: var(--gw-space-2) var(--gw-space-3); font-size: var(--gw-font-size-xs); color: var(--gw-color-text-secondary); border-block-start: var(--gw-border-width) solid var(--gw-color-border-quiet); } kbd { padding: var(--gw-space-0-5) calc(var(--gw-space-1) * 1.5); font: inherit; line-height: 1; background: color-mix( in oklab, var(--gw-color-surface-overlay), var(--gw-color-text-primary) 8% ); border: 1px solid var(--gw-color-border-normal); border-radius: var(--gw-radius-control); } dialog[data-placement=\"sheet\"] footer { display: none; } `, ]",
|
|
5903
5903
|
"inheritedFrom": {
|
|
5904
5904
|
"name": "GwElement",
|
|
5905
5905
|
"module": "src/element.ts"
|
|
@@ -10402,7 +10402,7 @@
|
|
|
10402
10402
|
"declarations": [
|
|
10403
10403
|
{
|
|
10404
10404
|
"kind": "class",
|
|
10405
|
-
"description": "A button that opens a menu on the Popover API, so it shows in the top\nlayer and closes on Esc or a click outside. Set `items`; arrow keys, Home\nand End move between them, and choosing one fires `select` with its\n`value` and closes the menu.\n\nContextual actions, such as \"Add a schedule\", go in `actions`: they stay\nbelow the items as the list scrolls, so they're found without scrolling to\nthe end, and with no items the menu says `empty` above them.\n\nFor a menu of choices, such as sort and order, give each item `checked`:\nthe picked one shows a tick, and `separator` starts the next group.\n\nItems render inside the component, so a page's own button styles can't\nreach them.",
|
|
10405
|
+
"description": "A button that opens a menu on the Popover API, so it shows in the top\nlayer and closes on Esc or a click outside. Set `items`; arrow keys, Home\nand End move between them, and choosing one fires `select` with its\n`value` and closes the menu.\n\nContextual actions, such as \"Add a schedule\", go in `actions`: they stay\nbelow the items as the list scrolls, so they're found without scrolling to\nthe end, and with no items the menu says `empty` above them.\n\nFor a menu of choices, such as sort and order, give each item `checked`:\nthe picked one shows a tick, and `separator` starts the next group.\nItems with `checkbox` toggle instead, firing `check` and keeping the menu\nopen, and `group` lists items under a heading.\n\nItems render inside the component, so a page's own button styles can't\nreach them.",
|
|
10406
10406
|
"name": "GwMenuButton",
|
|
10407
10407
|
"cssProperties": [
|
|
10408
10408
|
{
|
|
@@ -10428,7 +10428,7 @@
|
|
|
10428
10428
|
"text": "Properties"
|
|
10429
10429
|
},
|
|
10430
10430
|
"static": true,
|
|
10431
|
-
"default": "{ label: {}, heading: {}, icon: {}, empty: {}, items: { type: \"json\" }, actions: { type: \"json\" }, size: { reflect: true }, appearance: { reflect: true }, active: { type: \"boolean\", reflect: true }, labels: { reflect: true }, }",
|
|
10431
|
+
"default": "{ label: {}, heading: {}, icon: {}, empty: {}, items: { type: \"json\" }, actions: { type: \"json\" }, size: { reflect: true }, appearance: { reflect: true }, active: { type: \"boolean\", reflect: true }, labels: { reflect: true }, loading: { type: \"boolean\", reflect: true }, }",
|
|
10432
10432
|
"inheritedFrom": {
|
|
10433
10433
|
"name": "GwElement",
|
|
10434
10434
|
"module": "src/element.ts"
|
|
@@ -10441,7 +10441,7 @@
|
|
|
10441
10441
|
"text": "readonly CSSStyleSheet[]"
|
|
10442
10442
|
},
|
|
10443
10443
|
"static": true,
|
|
10444
|
-
"default": "[ base, controlSizes(\"menu-button\"), css` :host { position: relative; display: inline-block; anchor-scope: --gw-menu; } .trigger { --gw-size: var(--gw-menu-button-scale, 1); --gw-scale: calc(var(--gw-size) * var(--gw-control-scale, 1)); --gw-icon-size: calc(1.25rem * var(--gw-scale)); display: inline-flex; gap: calc(var(--gw-space-2) * var(--gw-size)); align-items: center; box-sizing: border-box; height: calc(2.5rem * var(--gw-scale)); padding: 0 calc(var(--gw-space-4) * var(--gw-size)); font: inherit; font-size: calc(var(--gw-font-size-s) * var(--gw-size)); font-weight: var(--gw-font-weight-strong); color: var(--gw-color-text-strong); cursor: pointer; background: var(--gw-color-fill-neutral-resting); border: var(--gw-border-width) solid var(--gw-border-color); border-radius: var(--gw-radius-control); transition: background-color var(--gw-motion-fast) var(--gw-motion-easing); anchor-name: --gw-menu; } .trigger:hover { background: var(--gw-color-fill-neutral-hovered); } /* The other appearances draw the trigger as gw-button draws a neutral one, a pill, so it sits in a row of buttons without a seam */ :host([appearance=\"accent\"]) .trigger, :host([appearance=\"outlined\"]) .trigger, :host([appearance=\"plain\"]) .trigger { background: transparent; border: max(var(--gw-border-width), 1px) solid transparent; border-radius: var(--gw-radius-pill); transition: background-color var(--gw-motion-fast) var(--gw-motion-easing), border-color var(--gw-motion-fast) var(--gw-motion-easing), color var(--gw-motion-fast) var(--gw-motion-easing); } :host([appearance=\"outlined\"]) .trigger { border-color: var(--gw-color-text-secondary); } :host([appearance=\"outlined\"]) .trigger:hover, :host([appearance=\"plain\"]) .trigger:hover { background: color-mix( in oklch, var(--gw-color-text-secondary) 16%, transparent ); } /* A solid fill, with text that flips between black and white by the fill's lightness, as gw-button's accent does */ :host([appearance=\"accent\"]) .trigger { color: oklch( from var(--gw-color-accent) clamp(0, (0.66 - l) * 1000, 1) 0 0 ); background: var(--gw-color-accent); } :host([appearance=\"accent\"]) .trigger:hover { background: oklch( from var(--gw-color-accent) calc(l + (0.6 - l) * 0.2) c h ); } :host([active]:not([appearance=\"accent\"])) .trigger { color: var(--gw-color-accent); border-color: var(--gw-color-accent); } /* Icon only, square like an icon-only gw-button, such as a ⋮ for more actions, with the text kept for assistive technology */ :host([labels=\"hide\"]) .trigger.has-icon { justify-content: center; min-width: calc(2.5rem * var(--gw-scale)); padding: 0; } :host([labels=\"hide\"]) .has-icon .chevron { display: none; } :host([labels=\"hide\"]) .has-icon .label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } @media (width < 40rem) { :host([labels=\"auto\"]) .trigger.has-icon { padding: 0 calc(var(--gw-space-2) * var(--gw-size)); } :host([labels=\"auto\"]) .has-icon .label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } } .chevron { width: 0.4rem; height: 0.4rem; margin-inline-start: var(--gw-space-1); border-inline-end: 1.5px solid currentColor; border-block-end: 1.5px solid currentColor; rotate: 45deg; translate: 0 -0.125rem; transition: rotate var(--gw-motion-normal) var(--gw-motion-easing), translate var(--gw-motion-normal) var(--gw-motion-easing); } .trigger[aria-expanded=\"true\"] .chevron { rotate: 225deg; translate: 0 0.125rem; } /* Below the button down to the window's edge, or above it when there's more room there, growing to its content and scrolling past that */ [popover] { position-anchor: --gw-menu; position-try-fallbacks: flip-inline; /* The area is the room on that side, so 100% is all of it */ position-area: block-end span-inline-end; inset: auto; box-sizing: border-box; min-width: 15rem; max-width: calc(100vw - 2 * var(--gw-space-2)); max-height: min(24rem, calc(100% - 2 * var(--gw-space-2))); padding: 0; margin: var(--gw-space-1) 0; overflow: hidden; color: var(--gw-color-text-primary); background: var(--gw-color-surface-overlay); border: max(var(--gw-border-width), 1px) solid var(--gw-color-border-normal); border-radius: var(--gw-radius-l); box-shadow: var(--gw-shadow-m); opacity: 0; translate: 0 -0.25rem; transition: opacity var(--gw-motion-fast) var(--gw-motion-easing), translate var(--gw-motion-normal) var(--gw-motion-easing), display var(--gw-motion-normal) allow-discrete, overlay var(--gw-motion-normal) allow-discrete; } [popover]:popover-open { display: flex; flex-direction: column; opacity: 1; translate: 0 0; } @starting-style { [popover]:popover-open { opacity: 0; translate: 0 -0.25rem; } } /* The items scroll; the actions stay put below them, so \"Add\" is always in reach rather than at the end of a long list */ .items, .actions { display: flex; flex-direction: column; gap: var(--gw-space-0-5); padding: var(--gw-space-1); } .items { flex: 1; min-height: 0; overflow-y: auto; } .actions { border-block-start: max(var(--gw-border-width), 1px) solid var(--gw-color-border-normal); } .empty { padding: var(--gw-space-3); margin: 0; font-size: var(--gw-font-size-s); color: var(--gw-color-text-secondary); } .item { display: flex; gap: var(--gw-space-3); align-items: center; box-sizing: border-box; min-height: 2.5rem; padding: var(--gw-space-2) var(--gw-space-3); font: inherit; font-size: var(--gw-font-size-s); color: var(--gw-color-text-primary); text-align: start; cursor: pointer; background: transparent; border: 0; border-radius: var(--gw-radius-m); transition: background-color var(--gw-motion-fast) var(--gw-motion-easing); --gw-icon-size: 1.125rem; } .item:hover, .item:focus-visible { background: var(--gw-color-fill-neutral-hovered); outline: none; } .item:focus-visible { box-shadow: inset 0 0 0 var(--gw-focus-width) var(--gw-color-focus); } .item:disabled { color: var(--gw-color-text-disabled); cursor: not-allowed; background: transparent; } .action { font-weight: var(--gw-font-weight-strong); color: var(--gw-color-accent); } /* Holds the tick's place on every choice, so the labels line up */ .tick { flex: none; width: var(--gw-icon-size); height: var(--gw-icon-size); color: var(--gw-color-accent); } .item[aria-checked=\"false\"] .tick { visibility: hidden; } .separator { flex: none; height: 0; margin: var(--gw-space-0-5) calc(-1 * var(--gw-space-1)); border: 0; border-block-start: max(var(--gw-border-width), 1px) solid var(--gw-color-border-normal); } .text { display: flex; flex-direction: column; gap: var(--gw-space-0-5); } .description { font-size: var(--gw-font-size-xs); color: var(--gw-color-text-secondary); } `, ]",
|
|
10444
|
+
"default": "[ base, controlSizes(\"menu-button\"), css` :host { position: relative; display: inline-block; anchor-scope: --gw-menu; } .trigger { --gw-size: var(--gw-menu-button-scale, 1); --gw-scale: calc(var(--gw-size) * var(--gw-control-scale, 1)); --gw-icon-size: calc(1.25rem * var(--gw-scale)); display: inline-flex; gap: calc(var(--gw-space-2) * var(--gw-size)); align-items: center; box-sizing: border-box; height: calc(2.5rem * var(--gw-scale)); padding: 0 calc(var(--gw-space-4) * var(--gw-size)); font: inherit; font-size: calc(var(--gw-font-size-s) * var(--gw-size)); font-weight: var(--gw-font-weight-strong); color: var(--gw-color-text-strong); cursor: pointer; background: var(--gw-color-fill-neutral-resting); border: var(--gw-border-width) solid var(--gw-border-color); border-radius: var(--gw-radius-control); transition: background-color var(--gw-motion-fast) var(--gw-motion-easing); anchor-name: --gw-menu; } .trigger:hover { background: var(--gw-color-fill-neutral-hovered); } /* While a chosen action runs, a ring takes the icon's place */ .ring { display: none; flex: none; width: calc(var(--gw-icon-size) * 0.8); height: calc(var(--gw-icon-size) * 0.8); border: calc(2px * var(--gw-scale)) solid color-mix(in oklch, currentColor 25%, transparent); border-top-color: currentColor; border-radius: 50%; animation: spin 0.9s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } :host([loading]) .trigger { cursor: progress; } :host([loading]) .ring { display: block; } :host([loading]) .trigger > .icon { display: none; } /* The other appearances draw the trigger as gw-button draws a neutral one, a pill, so it sits in a row of buttons without a seam */ :host([appearance=\"accent\"]) .trigger, :host([appearance=\"outlined\"]) .trigger, :host([appearance=\"plain\"]) .trigger { background: transparent; border: max(var(--gw-border-width), 1px) solid transparent; border-radius: var(--gw-radius-pill); transition: background-color var(--gw-motion-fast) var(--gw-motion-easing), border-color var(--gw-motion-fast) var(--gw-motion-easing), color var(--gw-motion-fast) var(--gw-motion-easing); } :host([appearance=\"outlined\"]) .trigger { border-color: var(--gw-color-text-secondary); } :host([appearance=\"outlined\"]) .trigger:hover, :host([appearance=\"plain\"]) .trigger:hover { background: color-mix( in oklch, var(--gw-color-text-secondary) 16%, transparent ); } /* A solid fill, with text that flips between black and white by the fill's lightness, as gw-button's accent does */ :host([appearance=\"accent\"]) .trigger { color: oklch( from var(--gw-color-accent) clamp(0, (0.66 - l) * 1000, 1) 0 0 ); background: var(--gw-color-accent); } :host([appearance=\"accent\"]) .trigger:hover { background: oklch( from var(--gw-color-accent) calc(l + (0.6 - l) * 0.2) c h ); } :host([active]:not([appearance=\"accent\"])) .trigger { color: var(--gw-color-accent); border-color: var(--gw-color-accent); } /* Icon only, square like an icon-only gw-button, such as a ⋮ for more actions, with the text kept for assistive technology */ :host([labels=\"hide\"]) .trigger.has-icon { justify-content: center; min-width: calc(2.5rem * var(--gw-scale)); padding: 0; } :host([labels=\"hide\"]) .has-icon .chevron { display: none; } :host([labels=\"hide\"]) .has-icon .label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } @media (width < 40rem) { :host([labels=\"auto\"]) .trigger.has-icon { padding: 0 calc(var(--gw-space-2) * var(--gw-size)); } :host([labels=\"auto\"]) .has-icon .label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } } .chevron { width: 0.4rem; height: 0.4rem; margin-inline-start: var(--gw-space-1); border-inline-end: 1.5px solid currentColor; border-block-end: 1.5px solid currentColor; rotate: 45deg; translate: 0 -0.125rem; transition: rotate var(--gw-motion-normal) var(--gw-motion-easing), translate var(--gw-motion-normal) var(--gw-motion-easing); } .trigger[aria-expanded=\"true\"] .chevron { rotate: 225deg; translate: 0 0.125rem; } /* Below the button down to the window's edge, or above it when there's more room there, growing to its content and scrolling past that */ [popover] { position-anchor: --gw-menu; position-try-fallbacks: flip-inline; /* The area is the room on that side, so 100% is all of it */ position-area: block-end span-inline-end; inset: auto; box-sizing: border-box; min-width: 15rem; max-width: calc(100vw - 2 * var(--gw-space-2)); max-height: min(24rem, calc(100% - 2 * var(--gw-space-2))); padding: 0; margin: var(--gw-space-1) 0; overflow: hidden; color: var(--gw-color-text-primary); background: var(--gw-color-surface-overlay); border: max(var(--gw-border-width), 1px) solid var(--gw-color-border-normal); border-radius: var(--gw-radius-l); box-shadow: var(--gw-shadow-m); opacity: 0; translate: 0 -0.25rem; transition: opacity var(--gw-motion-fast) var(--gw-motion-easing), translate var(--gw-motion-normal) var(--gw-motion-easing), display var(--gw-motion-normal) allow-discrete, overlay var(--gw-motion-normal) allow-discrete; } [popover]:popover-open { display: flex; flex-direction: column; opacity: 1; translate: 0 0; } @starting-style { [popover]:popover-open { opacity: 0; translate: 0 -0.25rem; } } /* The items scroll; the actions stay put below them, so \"Add\" is always in reach rather than at the end of a long list */ .items, .actions { display: flex; flex-direction: column; gap: var(--gw-space-0-5); padding: var(--gw-space-1); } .items { flex: 1; min-height: 0; overflow-y: auto; } .actions { border-block-start: max(var(--gw-border-width), 1px) solid var(--gw-color-border-normal); } .empty { padding: var(--gw-space-3); margin: 0; font-size: var(--gw-font-size-s); color: var(--gw-color-text-secondary); } .item { display: flex; gap: var(--gw-space-3); align-items: center; box-sizing: border-box; min-height: 2.5rem; padding: var(--gw-space-2) var(--gw-space-3); font: inherit; font-size: var(--gw-font-size-s); color: var(--gw-color-text-primary); text-align: start; cursor: pointer; background: transparent; border: 0; border-radius: var(--gw-radius-m); transition: background-color var(--gw-motion-fast) var(--gw-motion-easing); --gw-icon-size: 1.125rem; } .item:hover, .item:focus-visible { background: var(--gw-color-fill-neutral-hovered); outline: none; } .item:focus-visible { box-shadow: inset 0 0 0 var(--gw-focus-width) var(--gw-color-focus); } .item:disabled { color: var(--gw-color-text-disabled); cursor: not-allowed; background: transparent; } .action { font-weight: var(--gw-font-weight-strong); color: var(--gw-color-accent); } /* Holds the tick's place on every choice, so the labels line up */ .tick { flex: none; width: var(--gw-icon-size); height: var(--gw-icon-size); color: var(--gw-color-accent); } .item[aria-checked=\"false\"] .tick { visibility: hidden; } .item[role=\"menuitemcheckbox\"] .box { flex: none; color: var(--gw-color-text-secondary); } .item[role=\"menuitemcheckbox\"][aria-checked=\"true\"] .box { color: var(--gw-color-accent); } .group { display: flex; flex-direction: column; gap: var(--gw-space-0-5); } .group:not(:first-child) { padding-block-start: var(--gw-space-1); margin-block-start: var(--gw-space-0-5); border-block-start: max(var(--gw-border-width), 1px) solid var(--gw-color-border-normal); } .group-heading { padding: var(--gw-space-1) var(--gw-space-3) 0; margin: 0; font-size: var(--gw-font-size-xs); font-weight: var(--gw-font-weight-strong); color: var(--gw-color-text-secondary); } .separator { flex: none; height: 0; margin: var(--gw-space-0-5) calc(-1 * var(--gw-space-1)); border: 0; border-block-start: max(var(--gw-border-width), 1px) solid var(--gw-color-border-normal); } .text { display: flex; flex-direction: column; gap: var(--gw-space-0-5); } .description { font-size: var(--gw-font-size-xs); color: var(--gw-color-text-secondary); } `, ]",
|
|
10445
10445
|
"inheritedFrom": {
|
|
10446
10446
|
"name": "GwElement",
|
|
10447
10447
|
"module": "src/element.ts"
|
|
@@ -10533,6 +10533,15 @@
|
|
|
10533
10533
|
"description": "Whether the trigger's text shows beside its icon: always, never, or not on narrow screens.",
|
|
10534
10534
|
"default": "\"show\""
|
|
10535
10535
|
},
|
|
10536
|
+
{
|
|
10537
|
+
"kind": "field",
|
|
10538
|
+
"name": "loading",
|
|
10539
|
+
"type": {
|
|
10540
|
+
"text": "boolean"
|
|
10541
|
+
},
|
|
10542
|
+
"description": "Shows a spinning ring in the icon's place and disables the trigger.",
|
|
10543
|
+
"default": "false"
|
|
10544
|
+
},
|
|
10536
10545
|
{
|
|
10537
10546
|
"kind": "field",
|
|
10538
10547
|
"name": "#trigger",
|
|
@@ -10613,6 +10622,19 @@
|
|
|
10613
10622
|
}
|
|
10614
10623
|
]
|
|
10615
10624
|
},
|
|
10625
|
+
{
|
|
10626
|
+
"kind": "method",
|
|
10627
|
+
"name": "#check",
|
|
10628
|
+
"privacy": "private",
|
|
10629
|
+
"parameters": [
|
|
10630
|
+
{
|
|
10631
|
+
"name": "item",
|
|
10632
|
+
"type": {
|
|
10633
|
+
"text": "MenuItem"
|
|
10634
|
+
}
|
|
10635
|
+
}
|
|
10636
|
+
]
|
|
10637
|
+
},
|
|
10616
10638
|
{
|
|
10617
10639
|
"kind": "method",
|
|
10618
10640
|
"name": "#renderItem",
|
|
@@ -10632,11 +10654,38 @@
|
|
|
10632
10654
|
}
|
|
10633
10655
|
]
|
|
10634
10656
|
},
|
|
10657
|
+
{
|
|
10658
|
+
"kind": "method",
|
|
10659
|
+
"name": "#renderItems",
|
|
10660
|
+
"privacy": "private",
|
|
10661
|
+
"parameters": [
|
|
10662
|
+
{
|
|
10663
|
+
"name": "items",
|
|
10664
|
+
"type": {
|
|
10665
|
+
"text": "readonly MenuItem[]"
|
|
10666
|
+
}
|
|
10667
|
+
}
|
|
10668
|
+
],
|
|
10669
|
+
"description": "Items in runs that share a group, each run under its heading."
|
|
10670
|
+
},
|
|
10635
10671
|
{
|
|
10636
10672
|
"kind": "method",
|
|
10637
10673
|
"name": "#tick",
|
|
10638
10674
|
"privacy": "private"
|
|
10639
10675
|
},
|
|
10676
|
+
{
|
|
10677
|
+
"kind": "method",
|
|
10678
|
+
"name": "#box",
|
|
10679
|
+
"privacy": "private",
|
|
10680
|
+
"parameters": [
|
|
10681
|
+
{
|
|
10682
|
+
"name": "checked",
|
|
10683
|
+
"type": {
|
|
10684
|
+
"text": "boolean"
|
|
10685
|
+
}
|
|
10686
|
+
}
|
|
10687
|
+
]
|
|
10688
|
+
},
|
|
10640
10689
|
{
|
|
10641
10690
|
"kind": "method",
|
|
10642
10691
|
"name": "create",
|
|
@@ -10956,6 +11005,13 @@
|
|
|
10956
11005
|
"text": "CustomEvent<{ value: string }>"
|
|
10957
11006
|
},
|
|
10958
11007
|
"description": "When an item or action is chosen, with its `value` in `detail`."
|
|
11008
|
+
},
|
|
11009
|
+
{
|
|
11010
|
+
"name": "check",
|
|
11011
|
+
"type": {
|
|
11012
|
+
"text": "CustomEvent<{ value: string, checked: boolean }>"
|
|
11013
|
+
},
|
|
11014
|
+
"description": "When a checkbox item is toggled, with its `value` and the state it should change to in `detail`."
|
|
10959
11015
|
}
|
|
10960
11016
|
],
|
|
10961
11017
|
"attributes": [
|
|
@@ -11028,6 +11084,13 @@
|
|
|
11028
11084
|
},
|
|
11029
11085
|
"description": "Whether the trigger's text shows beside its icon: always, never (a square icon button without the chevron, such as ⋮ for more actions), or not on narrow screens. Defaults to `show`.",
|
|
11030
11086
|
"name": "labels"
|
|
11087
|
+
},
|
|
11088
|
+
{
|
|
11089
|
+
"type": {
|
|
11090
|
+
"text": "boolean"
|
|
11091
|
+
},
|
|
11092
|
+
"description": "Shows a spinning ring in the icon's place and disables the trigger, while a chosen action runs.",
|
|
11093
|
+
"name": "loading"
|
|
11031
11094
|
}
|
|
11032
11095
|
],
|
|
11033
11096
|
"superclass": {
|
|
@@ -12667,7 +12730,7 @@
|
|
|
12667
12730
|
"text": "readonly CSSStyleSheet[]"
|
|
12668
12731
|
},
|
|
12669
12732
|
"static": true,
|
|
12670
|
-
"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); cursor: pointer; border-radius: var(--gw-radius-control); transition: background-color var(--gw-motion-fast) var(--gw-motion-easing); } [role=\"option\"][data-active] { background: var(--gw-color-
|
|
12733
|
+
"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; } `, ]",
|
|
12671
12734
|
"inheritedFrom": {
|
|
12672
12735
|
"name": "GwElement",
|
|
12673
12736
|
"module": "src/element.ts"
|
|
@@ -19076,7 +19139,7 @@
|
|
|
19076
19139
|
"declarations": [
|
|
19077
19140
|
{
|
|
19078
19141
|
"kind": "class",
|
|
19079
|
-
"description": "Picks a theme from a grid of cards, each a small picture of an app in that\ntheme's colors: a rail, a panel and the status tones. For a settings page,\nor inside `gw-theme-picker` with `gallery`.\n\nThemes with a `group` sit together under its name,
|
|
19142
|
+
"description": "Picks a theme from a grid of cards, each a small picture of an app in that\ntheme's colors: a rail, a panel and the status tones. For a settings page,\nor inside `gw-theme-picker` with `gallery`.\n\nThemes with a `group` sit together under its name, so a family's light and\ndark variants line up. Themes with none gather where the first of them\ncomes: with no heading when that's first, and under \"Other\" after a family. Previews draw in\n`mode`, or the mode of the page or themed container it sits in, from the\ntheme CSS already on the page, including a custom theme `pageTheme`\nimported. Call `refresh()` after that CSS changes.\n\nIt's a `<fieldset>` of native radio buttons, so arrow keys move the choice.\nIt fires `theme`.",
|
|
19080
19143
|
"name": "GwThemeGallery",
|
|
19081
19144
|
"cssProperties": [
|
|
19082
19145
|
{
|
|
@@ -19123,7 +19186,7 @@
|
|
|
19123
19186
|
"text": "readonly CSSStyleSheet[]"
|
|
19124
19187
|
},
|
|
19125
19188
|
"static": true,
|
|
19126
|
-
"default": "[ base, css` :host { display: block; } fieldset { display: flex; flex-direction: column; gap: var(--gw-space-4); min-width: 0; padding: 0; margin: 0; border: 0; } legend { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } .group { display: grid; grid-template-columns: repeat( auto-fill, minmax(var(--gw-theme-gallery-min, 9rem), 1fr) ); gap: var(--gw-space-3); } .group-name { grid-column: 1 / -1; margin: 0; font-size: var(--gw-font-size-s); font-weight: var(--gw-font-weight-strong); color: var(--gw-color-text-secondary); text-transform: var(--gw-label-transform); letter-spacing: var(--gw-label-tracking); } label { position: relative; display: flex; flex-direction: column; gap: var(--gw-space-2); padding: var(--gw-space-2); cursor: pointer; background: var(--gw-color-surface-raised); border: 2px solid var(--gw-color-border-normal); border-radius: var(--gw-radius-panel); transition: border-color var(--gw-motion-fast) var(--gw-motion-easing), background-color var(--gw-motion-fast) var(--gw-motion-easing); } label:hover { border-color: var(--gw-color-border-accent); } label:has(input:checked) { background: var(--gw-color-fill-accent-quiet); border-color: var(--gw-color-accent); } label:has(input:focus-visible) { outline: var(--gw-focus-width) solid var(--gw-color-focus); outline-offset: var(--gw-focus-offset); } input { position: absolute; inset: 0; margin: 0; cursor: pointer; opacity: 0; } /* A small app in the theme's own colors */ .preview { display: grid; grid-template-columns: 22% 1fr; gap: 6px; height: 5.5rem; padding: 6px; background: var(--p-bg); border-radius: calc(var(--gw-radius-panel) - 2px); } .rail { display: flex; flex-direction: column; gap: 4px; padding: 4px; } .rail i { height: 5px; background: var(--p-muted); border-radius: 3px; opacity: 0.5; } .rail i:first-child { background: var(--p-accent); opacity: 1; } .panel { display: flex; flex-direction: column; gap: 5px; min-width: 0; padding: 6px; background: var(--p-surface); border: 1px solid var(--p-border); border-radius: min(var(--p-radius), 6px); } .line { width: 60%; height: 5px; background: var(--p-text); border-radius: 3px; } .line.short { width: 40%; background: var(--p-muted); } .bars { display: flex; flex: 1; gap: 3px; align-items: flex-end; } .bars i { flex: 1; border-radius: 2px 2px 0 0; } .name { display: flex; flex-direction: column; padding: 0 var(--gw-space-1) var(--gw-space-1); font-size: var(--gw-font-size-s); font-weight: var(--gw-font-weight-strong); color: var(--gw-color-text-strong); }
|
|
19189
|
+
"default": "[ base, css` :host { display: block; } fieldset { display: flex; flex-direction: column; gap: var(--gw-space-4); min-width: 0; padding: 0; margin: 0; border: 0; } legend { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } .group { display: grid; grid-template-columns: repeat( auto-fill, minmax(var(--gw-theme-gallery-min, 9rem), 1fr) ); gap: var(--gw-space-3); } .group-name { grid-column: 1 / -1; margin: 0; font-size: var(--gw-font-size-s); font-weight: var(--gw-font-weight-strong); color: var(--gw-color-text-secondary); text-transform: var(--gw-label-transform); letter-spacing: var(--gw-label-tracking); } label { position: relative; display: flex; flex-direction: column; gap: var(--gw-space-2); padding: var(--gw-space-2); cursor: pointer; background: var(--gw-color-surface-raised); border: 2px solid var(--gw-color-border-normal); border-radius: var(--gw-radius-panel); transition: border-color var(--gw-motion-fast) var(--gw-motion-easing), background-color var(--gw-motion-fast) var(--gw-motion-easing); } label:hover { border-color: var(--gw-color-border-accent); } label:has(input:checked) { background: var(--gw-color-fill-accent-quiet); border-color: var(--gw-color-accent); } label:has(input:focus-visible) { outline: var(--gw-focus-width) solid var(--gw-color-focus); outline-offset: var(--gw-focus-offset); } input { position: absolute; inset: 0; margin: 0; cursor: pointer; opacity: 0; } /* A small app in the theme's own colors */ .preview { display: grid; grid-template-columns: 22% 1fr; gap: 6px; height: 5.5rem; padding: 6px; background: var(--p-bg); border-radius: calc(var(--gw-radius-panel) - 2px); } .rail { display: flex; flex-direction: column; gap: 4px; padding: 4px; } .rail i { height: 5px; background: var(--p-muted); border-radius: 3px; opacity: 0.5; } .rail i:first-child { background: var(--p-accent); opacity: 1; } .panel { display: flex; flex-direction: column; gap: 5px; min-width: 0; padding: 6px; background: var(--p-surface); border: 1px solid var(--p-border); border-radius: min(var(--p-radius), 6px); } .line { width: 60%; height: 5px; background: var(--p-text); border-radius: 3px; } .line.short { width: 40%; background: var(--p-muted); } .bars { display: flex; flex: 1; gap: 3px; align-items: flex-end; } .bars i { flex: 1; border-radius: 2px 2px 0 0; } .name { display: flex; flex-direction: column; padding: 0 var(--gw-space-1) var(--gw-space-1); font-size: var(--gw-font-size-s); font-weight: var(--gw-font-weight-strong); color: var(--gw-color-text-strong); } label:has(input:checked) .name { color: var(--gw-color-accent); } .check { position: absolute; inset-block-start: var(--gw-space-3); inset-inline-end: var(--gw-space-3); display: none; place-items: center; width: 1.5rem; height: 1.5rem; color: var(--gw-color-on-accent); background: var(--gw-color-fill-accent-resting); border-radius: 50%; --gw-icon-size: 1rem; } label:has(input:checked) .check { display: grid; } `, ]",
|
|
19127
19190
|
"inheritedFrom": {
|
|
19128
19191
|
"name": "GwElement",
|
|
19129
19192
|
"module": "src/element.ts"
|
|
@@ -19229,12 +19292,6 @@
|
|
|
19229
19292
|
"type": {
|
|
19230
19293
|
"text": "OptionItem"
|
|
19231
19294
|
}
|
|
19232
|
-
},
|
|
19233
|
-
{
|
|
19234
|
-
"name": "inOther",
|
|
19235
|
-
"type": {
|
|
19236
|
-
"text": "boolean"
|
|
19237
|
-
}
|
|
19238
19295
|
}
|
|
19239
19296
|
]
|
|
19240
19297
|
},
|
|
@@ -19615,7 +19672,7 @@
|
|
|
19615
19672
|
"declarations": [
|
|
19616
19673
|
{
|
|
19617
19674
|
"kind": "class",
|
|
19618
|
-
"description": "The page's mode, theme and import, in place where there's room and in a\npopover where there isn't. Offers the mode (auto, light, dark or Darker),\nthe theme, and buttons to import a theme file (a Groundwork theme's `.json`\nor an Omarchy `colors.toml`) or reset.\n\nIt only asks: it fires `mode`, `theme`, `import` and `reset`, and the page\napplies them and sets `mode`, `theme` and `themes` back. `pageTheme` from\n`@timmo001/groundwork-tokens/page` does all of that. Show an import's\nresult, such as contrast misses, in `status`.\n\n`gallery` swaps the theme select for `gw-theme-gallery`'s cards, which\npreview each theme in the chosen mode.\n\n`layout` sets how it shows. `auto`, the default, lays the mode out as icons\nand the theme as a select in place while they fit the room it's given,\nthen moves them into the popover, the theme first, as the room runs out.\nReset, Import, the gallery and `status` stay in the popover, opened from a\npalette button. Once the mode and theme are in place and only Reset and\nImport are left, they're a ⋮ menu instead.\nIn a flex row, such as `gw-page-header`'s actions, it gives up room before\nthe title does. `popover` keeps everything behind the palette button. `cycle` is one button\nshowing the mode in use, which steps to the next mode on each press, for an\napp bar. `inline` lays every control out in place, in one wrapping row with\nReset and Import, for a footer or a settings page; set `justify-content` on\nit to align the row. `modes` picks which modes it offers, such as\n`light dark` without Auto.",
|
|
19675
|
+
"description": "The page's mode, theme and import, in place where there's room and in a\npopover where there isn't. Offers the mode (auto, light, dark or Darker),\nthe theme, and buttons to import a theme file (a Groundwork theme's `.json`\nor an Omarchy `colors.toml`) or reset.\n\nIt only asks: it fires `mode`, `theme`, `import` and `reset`, and the page\napplies them and sets `mode`, `theme` and `themes` back. `pageTheme` from\n`@timmo001/groundwork-tokens/page` does all of that. Show an import's\nresult, such as contrast misses, in `status`.\n\n`gallery` swaps the theme select for `gw-theme-gallery`'s cards, which\npreview each theme in the chosen mode.\n\n`layout` sets how it shows. `auto`, the default, lays the mode out as icons\nand the theme as a select in place while they fit the room it's given,\nthen moves them into the popover, the theme first, as the room runs out.\nReset, Import, the gallery and `status` stay in the popover, opened from a\npalette button. Once the mode and theme are in place and only Reset and\nImport are left, they're a ⋮ menu instead.\nIn a flex row, such as `gw-page-header`'s actions, it gives up room before\nthe title does. `popover` keeps everything behind the palette button. `cycle` is one button\nshowing the mode in use, which steps to the next mode on each press, for an\napp bar. `inline` lays every control out in place, in one wrapping row with\nReset and Import, for a footer or a settings page; set `justify-content` on\nit to align the row. With `gallery`, the cards take a row of their own\nunder it. `modes` picks which modes it offers, such as\n`light dark` without Auto.",
|
|
19619
19676
|
"name": "GwThemePicker",
|
|
19620
19677
|
"cssParts": [
|
|
19621
19678
|
{
|
|
@@ -19652,7 +19709,7 @@
|
|
|
19652
19709
|
"text": "readonly CSSStyleSheet[]"
|
|
19653
19710
|
},
|
|
19654
19711
|
"static": true,
|
|
19655
|
-
"default": "[ base, css` :host { display: inline-block; } :host([gallery]) gw-popover { --gw-popover-width: 34rem; } .gallery { max-height: min(24rem, 50vh); overflow: auto; /* Room for the cards' focus rings */ padding: var(--gw-space-1); margin: calc(var(--gw-space-1) * -1); } .controls { display: flex; flex-direction: column; gap: var(--gw-space-4); margin-block-start: var(--gw-space-2); } .status { margin: 0; font-size: var(--gw-font-size-s); color: var(--gw-color-text-secondary); } /* One wrapping row: the controls, then Reset and Import. The host is the flex container, so a page can set its justify-content */ :host([layout=\"inline\"]) { display: flex; flex-wrap: wrap; gap: var(--gw-space-4); align-items: center; } :host([layout=\"inline\"]) .controls { flex-flow: row wrap; align-items: center; margin-block-start: 0; } .actions { display: flex; flex-wrap: wrap; gap: var(--gw-space-2); align-items: center; } /* Asks for the room to have all of it in place, but gives it up down to the button before a flex row's other items shrink */ :host([layout=\"auto\"]) { display: flex; flex: 0 1000 var(--gw-theme-picker-fit, auto); gap: var(--gw-space-1); align-items: center; min-width: 2.5rem; contain: inline-size; } :host([layout=\"auto\"]) > * { flex: none; } `, ]",
|
|
19712
|
+
"default": "[ base, css` :host { display: inline-block; } :host([gallery]) gw-popover { --gw-popover-width: 34rem; } .gallery { max-height: min(24rem, 50vh); overflow: auto; /* Room for the cards' focus rings */ padding: var(--gw-space-1); margin: calc(var(--gw-space-1) * -1); } .controls { display: flex; flex-direction: column; gap: var(--gw-space-4); margin-block-start: var(--gw-space-2); } .status { margin: 0; font-size: var(--gw-font-size-s); color: var(--gw-color-text-secondary); } /* One wrapping row: the controls, then Reset and Import. The host is the flex container, so a page can set its justify-content */ :host([layout=\"inline\"]) { display: flex; flex-wrap: wrap; gap: var(--gw-space-4); align-items: center; } :host([layout=\"inline\"]) .controls { flex-flow: row wrap; align-items: center; margin-block-start: 0; } /* The cards get a row of their own under the mode, Reset and Import */ :host([layout=\"inline\"][gallery]) .controls { display: contents; } :host([layout=\"inline\"]) .gallery { flex: 1 0 100%; order: 1; max-height: none; overflow: visible; } :host([layout=\"inline\"][gallery]) .status { flex: 1 0 100%; order: 2; } .actions { display: flex; flex-wrap: wrap; gap: var(--gw-space-2); align-items: center; } /* Asks for the room to have all of it in place, but gives it up down to the button before a flex row's other items shrink */ :host([layout=\"auto\"]) { display: flex; flex: 0 1000 var(--gw-theme-picker-fit, auto); gap: var(--gw-space-1); align-items: center; min-width: 2.5rem; contain: inline-size; } :host([layout=\"auto\"]) > * { flex: none; } `, ]",
|
|
19656
19713
|
"inheritedFrom": {
|
|
19657
19714
|
"name": "GwElement",
|
|
19658
19715
|
"module": "src/element.ts"
|
package/package.json
CHANGED
package/src/events.ts
CHANGED
package/src/gw-command-menu.ts
CHANGED
|
@@ -56,14 +56,14 @@ export class GwCommandMenu extends GwElement {
|
|
|
56
56
|
color: var(--gw-color-text-primary);
|
|
57
57
|
background: var(--gw-color-surface-overlay);
|
|
58
58
|
border: max(var(--gw-border-width), 1px) solid
|
|
59
|
-
var(--gw-color-border-
|
|
59
|
+
var(--gw-color-border-loud);
|
|
60
60
|
border-radius: var(--gw-radius-panel);
|
|
61
61
|
box-shadow: var(--gw-shadow-l);
|
|
62
62
|
--gw-option-list-height: min(24rem, 56vh);
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
dialog::backdrop {
|
|
66
|
-
background: rgb(0 0 0 /
|
|
66
|
+
background: rgb(0 0 0 / 60%);
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
dialog,
|
|
@@ -135,8 +135,14 @@ export class GwCommandMenu extends GwElement {
|
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
kbd {
|
|
138
|
-
padding: 0 var(--gw-space-1);
|
|
138
|
+
padding: var(--gw-space-0-5) calc(var(--gw-space-1) * 1.5);
|
|
139
139
|
font: inherit;
|
|
140
|
+
line-height: 1;
|
|
141
|
+
background: color-mix(
|
|
142
|
+
in oklab,
|
|
143
|
+
var(--gw-color-surface-overlay),
|
|
144
|
+
var(--gw-color-text-primary) 8%
|
|
145
|
+
);
|
|
140
146
|
border: 1px solid var(--gw-color-border-normal);
|
|
141
147
|
border-radius: var(--gw-radius-control);
|
|
142
148
|
}
|
package/src/gw-menu-button.ts
CHANGED
|
@@ -20,13 +20,31 @@ export interface MenuItem {
|
|
|
20
20
|
* order. Give it on every item in the group: the picked one shows a tick.
|
|
21
21
|
*/
|
|
22
22
|
readonly checked?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Makes the item a checkbox, such as a column to show: choosing it fires
|
|
25
|
+
* `check` with the state it should change to and keeps the menu open, so
|
|
26
|
+
* several can be changed in a row. `checked` is its current state.
|
|
27
|
+
*/
|
|
28
|
+
readonly checkbox?: boolean;
|
|
23
29
|
/** Draws a line above the item, starting a new group. */
|
|
24
30
|
readonly separator?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Names the group the item is in. Items in a row with the same group are
|
|
33
|
+
* listed together under it as a heading, after a line.
|
|
34
|
+
*/
|
|
35
|
+
readonly group?: string;
|
|
25
36
|
}
|
|
26
37
|
|
|
27
38
|
// mdi's check, for the picked item in a menu of choices
|
|
28
39
|
const tick = "M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z";
|
|
29
40
|
|
|
41
|
+
// mdi's checkbox-marked and checkbox-blank-outline, for checkbox items
|
|
42
|
+
const boxChecked =
|
|
43
|
+
"M10,17L5,12L6.41,10.58L10,14.17L17.59,6.58L19,8M19,3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3Z";
|
|
44
|
+
|
|
45
|
+
const boxEmpty =
|
|
46
|
+
"M19,3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3M19,5V19H5V5H19Z";
|
|
47
|
+
|
|
30
48
|
/**
|
|
31
49
|
* A button that opens a menu on the Popover API, so it shows in the top
|
|
32
50
|
* layer and closes on Esc or a click outside. Set `items`; arrow keys, Home
|
|
@@ -39,6 +57,8 @@ const tick = "M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z";
|
|
|
39
57
|
*
|
|
40
58
|
* For a menu of choices, such as sort and order, give each item `checked`:
|
|
41
59
|
* the picked one shows a tick, and `separator` starts the next group.
|
|
60
|
+
* Items with `checkbox` toggle instead, firing `check` and keeping the menu
|
|
61
|
+
* open, and `group` lists items under a heading.
|
|
42
62
|
*
|
|
43
63
|
* Items render inside the component, so a page's own button styles can't
|
|
44
64
|
* reach them.
|
|
@@ -56,6 +76,7 @@ const tick = "M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z";
|
|
|
56
76
|
* @attr {"accent"|"filled"|"outlined"|"plain"} appearance - How the trigger is drawn: `filled` is a neutral fill with a border, and the others match a neutral `gw-button` of that appearance, so it sits in a row of them. Defaults to `filled`.
|
|
57
77
|
* @attr {boolean} active - Marks the trigger as set, such as a grouping that's on, with an accent border and text.
|
|
58
78
|
* @attr {"show"|"hide"|"auto"} labels - Whether the trigger's text shows beside its icon: always, never (a square icon button without the chevron, such as ⋮ for more actions), or not on narrow screens. Defaults to `show`.
|
|
79
|
+
* @attr {boolean} loading - Shows a spinning ring in the icon's place and disables the trigger, while a chosen action runs.
|
|
59
80
|
*
|
|
60
81
|
* @csspart trigger - The button that opens the menu.
|
|
61
82
|
*
|
|
@@ -63,6 +84,7 @@ const tick = "M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z";
|
|
|
63
84
|
* @cssprop --gw-control-scale - Scales its height and icon with the theme's density. The theme sets it.
|
|
64
85
|
*
|
|
65
86
|
* @fires {CustomEvent<{ value: string }>} select - When an item or action is chosen, with its `value` in `detail`.
|
|
87
|
+
* @fires {CustomEvent<{ value: string, checked: boolean }>} check - When a checkbox item is toggled, with its `value` and the state it should change to in `detail`.
|
|
66
88
|
*/
|
|
67
89
|
export class GwMenuButton extends GwElement {
|
|
68
90
|
static override properties = {
|
|
@@ -76,6 +98,7 @@ export class GwMenuButton extends GwElement {
|
|
|
76
98
|
appearance: { reflect: true },
|
|
77
99
|
active: { type: "boolean", reflect: true },
|
|
78
100
|
labels: { reflect: true },
|
|
101
|
+
loading: { type: "boolean", reflect: true },
|
|
79
102
|
} as const;
|
|
80
103
|
|
|
81
104
|
static override styles = [
|
|
@@ -116,6 +139,37 @@ export class GwMenuButton extends GwElement {
|
|
|
116
139
|
background: var(--gw-color-fill-neutral-hovered);
|
|
117
140
|
}
|
|
118
141
|
|
|
142
|
+
/* While a chosen action runs, a ring takes the icon's place */
|
|
143
|
+
.ring {
|
|
144
|
+
display: none;
|
|
145
|
+
flex: none;
|
|
146
|
+
width: calc(var(--gw-icon-size) * 0.8);
|
|
147
|
+
height: calc(var(--gw-icon-size) * 0.8);
|
|
148
|
+
border: calc(2px * var(--gw-scale)) solid
|
|
149
|
+
color-mix(in oklch, currentColor 25%, transparent);
|
|
150
|
+
border-top-color: currentColor;
|
|
151
|
+
border-radius: 50%;
|
|
152
|
+
animation: spin 0.9s linear infinite;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
@keyframes spin {
|
|
156
|
+
to {
|
|
157
|
+
transform: rotate(360deg);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
:host([loading]) .trigger {
|
|
162
|
+
cursor: progress;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
:host([loading]) .ring {
|
|
166
|
+
display: block;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
:host([loading]) .trigger > .icon {
|
|
170
|
+
display: none;
|
|
171
|
+
}
|
|
172
|
+
|
|
119
173
|
/* The other appearances draw the trigger as gw-button draws a neutral
|
|
120
174
|
one, a pill, so it sits in a row of buttons without a seam */
|
|
121
175
|
:host([appearance="accent"]) .trigger,
|
|
@@ -342,6 +396,36 @@ export class GwMenuButton extends GwElement {
|
|
|
342
396
|
visibility: hidden;
|
|
343
397
|
}
|
|
344
398
|
|
|
399
|
+
.item[role="menuitemcheckbox"] .box {
|
|
400
|
+
flex: none;
|
|
401
|
+
color: var(--gw-color-text-secondary);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
.item[role="menuitemcheckbox"][aria-checked="true"] .box {
|
|
405
|
+
color: var(--gw-color-accent);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
.group {
|
|
409
|
+
display: flex;
|
|
410
|
+
flex-direction: column;
|
|
411
|
+
gap: var(--gw-space-0-5);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
.group:not(:first-child) {
|
|
415
|
+
padding-block-start: var(--gw-space-1);
|
|
416
|
+
margin-block-start: var(--gw-space-0-5);
|
|
417
|
+
border-block-start: max(var(--gw-border-width), 1px) solid
|
|
418
|
+
var(--gw-color-border-normal);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
.group-heading {
|
|
422
|
+
padding: var(--gw-space-1) var(--gw-space-3) 0;
|
|
423
|
+
margin: 0;
|
|
424
|
+
font-size: var(--gw-font-size-xs);
|
|
425
|
+
font-weight: var(--gw-font-weight-strong);
|
|
426
|
+
color: var(--gw-color-text-secondary);
|
|
427
|
+
}
|
|
428
|
+
|
|
345
429
|
.separator {
|
|
346
430
|
flex: none;
|
|
347
431
|
height: 0;
|
|
@@ -380,6 +464,8 @@ export class GwMenuButton extends GwElement {
|
|
|
380
464
|
declare active: boolean;
|
|
381
465
|
/** Whether the trigger's text shows beside its icon: always, never, or not on narrow screens. */
|
|
382
466
|
declare labels: "show" | "hide" | "auto";
|
|
467
|
+
/** Shows a spinning ring in the icon's place and disables the trigger. */
|
|
468
|
+
declare loading: boolean;
|
|
383
469
|
|
|
384
470
|
readonly #trigger = h("button", {
|
|
385
471
|
class: "trigger",
|
|
@@ -425,6 +511,7 @@ export class GwMenuButton extends GwElement {
|
|
|
425
511
|
this.appearance = "filled";
|
|
426
512
|
this.active = false;
|
|
427
513
|
this.labels = "show";
|
|
514
|
+
this.loading = false;
|
|
428
515
|
}
|
|
429
516
|
|
|
430
517
|
/** The enabled items then actions, in the order arrow keys move through them. */
|
|
@@ -493,18 +580,36 @@ export class GwMenuButton extends GwElement {
|
|
|
493
580
|
this.#trigger.focus();
|
|
494
581
|
}
|
|
495
582
|
|
|
583
|
+
// A checkbox keeps the menu open, so several can be changed in a row
|
|
584
|
+
#check(item: MenuItem) {
|
|
585
|
+
this.dispatchEvent(
|
|
586
|
+
new CustomEvent("check", {
|
|
587
|
+
detail: { value: item.value, checked: item.checked !== true },
|
|
588
|
+
bubbles: true,
|
|
589
|
+
composed: true,
|
|
590
|
+
}),
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
|
|
496
594
|
#renderItem(item: MenuItem, kind: "item" | "action") {
|
|
497
|
-
const
|
|
595
|
+
const checkbox = item.checkbox === true;
|
|
596
|
+
const choice = !checkbox && item.checked !== undefined;
|
|
498
597
|
|
|
499
598
|
const button = h(
|
|
500
599
|
"button",
|
|
501
600
|
{
|
|
502
601
|
class: kind === "action" ? "item action" : "item",
|
|
503
|
-
role:
|
|
504
|
-
|
|
602
|
+
role: checkbox
|
|
603
|
+
? "menuitemcheckbox"
|
|
604
|
+
: choice
|
|
605
|
+
? "menuitemradio"
|
|
606
|
+
: "menuitem",
|
|
607
|
+
"aria-checked":
|
|
608
|
+
checkbox || choice ? String(item.checked === true) : null,
|
|
505
609
|
value: item.value,
|
|
506
610
|
disabled: item.disabled === true,
|
|
507
611
|
},
|
|
612
|
+
checkbox ? this.#box(item.checked === true) : null,
|
|
508
613
|
choice ? this.#tick() : null,
|
|
509
614
|
item.icon ? createIcon(item.icon) : null,
|
|
510
615
|
h(
|
|
@@ -517,13 +622,55 @@ export class GwMenuButton extends GwElement {
|
|
|
517
622
|
),
|
|
518
623
|
);
|
|
519
624
|
|
|
520
|
-
button.addEventListener("click", () =>
|
|
625
|
+
button.addEventListener("click", () => {
|
|
626
|
+
if (checkbox) {
|
|
627
|
+
this.#check(item);
|
|
628
|
+
} else {
|
|
629
|
+
this.#choose(item.value);
|
|
630
|
+
}
|
|
631
|
+
});
|
|
521
632
|
|
|
522
633
|
return item.separator === true
|
|
523
634
|
? [h("hr", { class: "separator", role: "separator" }), button]
|
|
524
635
|
: [button];
|
|
525
636
|
}
|
|
526
637
|
|
|
638
|
+
/** Items in runs that share a group, each run under its heading. */
|
|
639
|
+
#renderItems(items: readonly MenuItem[]) {
|
|
640
|
+
if (!items.some((item) => item.group !== undefined)) {
|
|
641
|
+
return items.flatMap((item) => this.#renderItem(item, "item"));
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const runs: { group: string | undefined; items: MenuItem[] }[] = [];
|
|
645
|
+
|
|
646
|
+
for (const item of items) {
|
|
647
|
+
const run = runs.at(-1);
|
|
648
|
+
|
|
649
|
+
if (run !== undefined && run.group === item.group) {
|
|
650
|
+
run.items.push(item);
|
|
651
|
+
} else {
|
|
652
|
+
runs.push({ group: item.group, items: [item] });
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
return runs.map(({ group, items: grouped }, index) => {
|
|
657
|
+
const id = `group-${index}`;
|
|
658
|
+
|
|
659
|
+
return h(
|
|
660
|
+
"div",
|
|
661
|
+
{
|
|
662
|
+
class: "group",
|
|
663
|
+
role: "group",
|
|
664
|
+
"aria-labelledby": group === undefined ? null : id,
|
|
665
|
+
},
|
|
666
|
+
group === undefined
|
|
667
|
+
? null
|
|
668
|
+
: h("p", { class: "group-heading", id, role: "none" }, group),
|
|
669
|
+
...grouped.flatMap((item) => this.#renderItem(item, "item")),
|
|
670
|
+
);
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
|
|
527
674
|
#tick() {
|
|
528
675
|
const icon = createIcon(tick);
|
|
529
676
|
|
|
@@ -532,9 +679,18 @@ export class GwMenuButton extends GwElement {
|
|
|
532
679
|
return icon;
|
|
533
680
|
}
|
|
534
681
|
|
|
682
|
+
#box(checked: boolean) {
|
|
683
|
+
const icon = createIcon(checked ? boxChecked : boxEmpty);
|
|
684
|
+
|
|
685
|
+
icon.classList.add("box");
|
|
686
|
+
|
|
687
|
+
return icon;
|
|
688
|
+
}
|
|
689
|
+
|
|
535
690
|
protected override create(root: ShadowRoot) {
|
|
536
691
|
this.#trigger.append(
|
|
537
692
|
this.#triggerIcon,
|
|
693
|
+
h("span", { class: "ring", "aria-hidden": "true" }),
|
|
538
694
|
this.#triggerText,
|
|
539
695
|
h("span", { class: "chevron", "aria-hidden": "true" }),
|
|
540
696
|
);
|
|
@@ -564,15 +720,30 @@ export class GwMenuButton extends GwElement {
|
|
|
564
720
|
: null),
|
|
565
721
|
);
|
|
566
722
|
this.#popover.setAttribute("aria-label", this.heading || this.label);
|
|
723
|
+
this.#trigger.disabled = this.loading;
|
|
724
|
+
setAttribute(this.#trigger, "aria-busy", this.loading ? "true" : null);
|
|
567
725
|
|
|
568
726
|
if (changed.has("items") || changed.has("empty")) {
|
|
569
727
|
const items = Array.isArray(this.items) ? this.items : [];
|
|
570
728
|
|
|
729
|
+
// A checkbox's change re-renders the list with the menu open, so focus
|
|
730
|
+
// goes back to the item that had it
|
|
731
|
+
const focused = this.shadowRoot?.activeElement;
|
|
732
|
+
|
|
733
|
+
const value =
|
|
734
|
+
focused instanceof HTMLButtonElement && this.#items.contains(focused)
|
|
735
|
+
? focused.value
|
|
736
|
+
: undefined;
|
|
737
|
+
|
|
571
738
|
this.#items.replaceChildren(
|
|
572
739
|
...(items.length === 0
|
|
573
740
|
? [h("p", { class: "empty", role: "none" }, this.empty)]
|
|
574
|
-
:
|
|
741
|
+
: this.#renderItems(items)),
|
|
575
742
|
);
|
|
743
|
+
|
|
744
|
+
if (value !== undefined) {
|
|
745
|
+
this.#buttons.find((button) => button.value === value)?.focus();
|
|
746
|
+
}
|
|
576
747
|
}
|
|
577
748
|
|
|
578
749
|
if (changed.has("actions")) {
|
package/src/gw-option-list.ts
CHANGED
|
@@ -181,14 +181,21 @@ export class GwOptionList extends GwElement {
|
|
|
181
181
|
|
|
182
182
|
[role="option"] {
|
|
183
183
|
font-size: var(--gw-font-size-s);
|
|
184
|
+
color: var(--gw-color-text-strong);
|
|
184
185
|
cursor: pointer;
|
|
185
186
|
border-radius: var(--gw-radius-control);
|
|
186
187
|
transition: background-color var(--gw-motion-fast)
|
|
187
188
|
var(--gw-motion-easing);
|
|
188
189
|
}
|
|
189
190
|
|
|
191
|
+
/* Mixed from the text color, so the active row stands out on any
|
|
192
|
+
surface the list sits on, overlays included */
|
|
190
193
|
[role="option"][data-active] {
|
|
191
|
-
background:
|
|
194
|
+
background: color-mix(
|
|
195
|
+
in oklab,
|
|
196
|
+
transparent,
|
|
197
|
+
var(--gw-color-text-primary) 12%
|
|
198
|
+
);
|
|
192
199
|
}
|
|
193
200
|
|
|
194
201
|
[role="option"][aria-selected="true"] {
|
|
@@ -197,7 +204,7 @@ export class GwOptionList extends GwElement {
|
|
|
197
204
|
|
|
198
205
|
[role="option"] > .icon {
|
|
199
206
|
flex: none;
|
|
200
|
-
color: var(--gw-color-text-
|
|
207
|
+
color: var(--gw-color-text-primary);
|
|
201
208
|
}
|
|
202
209
|
|
|
203
210
|
[role="option"] > .check {
|
package/src/gw-theme-gallery.ts
CHANGED
|
@@ -72,29 +72,23 @@ const closestWith = (start: Element, attribute: string) => {
|
|
|
72
72
|
};
|
|
73
73
|
|
|
74
74
|
/**
|
|
75
|
-
* Themes in groups, in the order they come.
|
|
76
|
-
*
|
|
75
|
+
* Themes in groups, in the order they come. Themes with no group gather where
|
|
76
|
+
* the first of them came: with no heading when that's first, and under
|
|
77
|
+
* "Other" after a named group.
|
|
77
78
|
*/
|
|
78
79
|
const grouped = (themes: readonly OptionItem[]) => {
|
|
79
|
-
if (!themes.some((theme) => theme.group)) return [["", themes] as const];
|
|
80
|
-
|
|
81
80
|
const groups = new Map<string, OptionItem[]>();
|
|
82
81
|
|
|
83
82
|
for (const theme of themes) {
|
|
84
|
-
const name = theme.group ??
|
|
83
|
+
const name = theme.group ?? "";
|
|
85
84
|
|
|
86
85
|
groups.set(name, [...(groups.get(name) ?? []), theme]);
|
|
87
86
|
}
|
|
88
87
|
|
|
89
|
-
|
|
90
|
-
([name, members]) =>
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const rest = themes.filter(
|
|
94
|
-
(theme) => !own.some(([name]) => name === theme.group),
|
|
88
|
+
return [...groups].map(
|
|
89
|
+
([name, members], index) =>
|
|
90
|
+
[name === "" && index > 0 ? other : name, members] as const,
|
|
95
91
|
);
|
|
96
|
-
|
|
97
|
-
return rest.length === 0 ? own : [...own, [other, rest] as const];
|
|
98
92
|
};
|
|
99
93
|
|
|
100
94
|
/**
|
|
@@ -102,8 +96,9 @@ const grouped = (themes: readonly OptionItem[]) => {
|
|
|
102
96
|
* theme's colors: a rail, a panel and the status tones. For a settings page,
|
|
103
97
|
* or inside `gw-theme-picker` with `gallery`.
|
|
104
98
|
*
|
|
105
|
-
* Themes with a `group` sit together under its name,
|
|
106
|
-
*
|
|
99
|
+
* Themes with a `group` sit together under its name, so a family's light and
|
|
100
|
+
* dark variants line up. Themes with none gather where the first of them
|
|
101
|
+
* comes: with no heading when that's first, and under "Other" after a family. Previews draw in
|
|
107
102
|
* `mode`, or the mode of the page or themed container it sits in, from the
|
|
108
103
|
* theme CSS already on the page, including a custom theme `pageTheme`
|
|
109
104
|
* imported. Call `refresh()` after that CSS changes.
|
|
@@ -292,12 +287,6 @@ export class GwThemeGallery extends GwElement {
|
|
|
292
287
|
color: var(--gw-color-text-strong);
|
|
293
288
|
}
|
|
294
289
|
|
|
295
|
-
.name small {
|
|
296
|
-
font-size: var(--gw-font-size-xs);
|
|
297
|
-
font-weight: normal;
|
|
298
|
-
color: var(--gw-color-text-secondary);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
290
|
label:has(input:checked) .name {
|
|
302
291
|
color: var(--gw-color-accent);
|
|
303
292
|
}
|
|
@@ -393,7 +382,7 @@ export class GwThemeGallery extends GwElement {
|
|
|
393
382
|
this.#modeRoot.disconnect();
|
|
394
383
|
}
|
|
395
384
|
|
|
396
|
-
#card(theme: OptionItem
|
|
385
|
+
#card(theme: OptionItem) {
|
|
397
386
|
const input = h("input", {
|
|
398
387
|
type: "radio",
|
|
399
388
|
name: this.#name,
|
|
@@ -440,12 +429,7 @@ export class GwThemeGallery extends GwElement {
|
|
|
440
429
|
{ part: "card" },
|
|
441
430
|
input,
|
|
442
431
|
preview,
|
|
443
|
-
h(
|
|
444
|
-
"span",
|
|
445
|
-
{ class: "name", part: "name" },
|
|
446
|
-
theme.label,
|
|
447
|
-
inOther && theme.group ? h("small", {}, theme.group) : null,
|
|
448
|
-
),
|
|
432
|
+
h("span", { class: "name", part: "name" }, theme.label),
|
|
449
433
|
h("span", { class: "check" }, createIcon(checkIcon)),
|
|
450
434
|
);
|
|
451
435
|
|
|
@@ -472,7 +456,7 @@ export class GwThemeGallery extends GwElement {
|
|
|
472
456
|
"div",
|
|
473
457
|
{ class: "group" },
|
|
474
458
|
name ? h("h3", { class: "group-name", part: "group" }, name) : null,
|
|
475
|
-
...members.map((theme) => this.#card(theme
|
|
459
|
+
...members.map((theme) => this.#card(theme)),
|
|
476
460
|
),
|
|
477
461
|
),
|
|
478
462
|
);
|
package/src/gw-theme-picker.ts
CHANGED
|
@@ -65,7 +65,8 @@ const moreIcon =
|
|
|
65
65
|
* showing the mode in use, which steps to the next mode on each press, for an
|
|
66
66
|
* app bar. `inline` lays every control out in place, in one wrapping row with
|
|
67
67
|
* Reset and Import, for a footer or a settings page; set `justify-content` on
|
|
68
|
-
* it to align the row. `
|
|
68
|
+
* it to align the row. With `gallery`, the cards take a row of their own
|
|
69
|
+
* under it. `modes` picks which modes it offers, such as
|
|
69
70
|
* `light dark` without Auto.
|
|
70
71
|
*
|
|
71
72
|
* @element gw-theme-picker
|
|
@@ -148,6 +149,23 @@ export class GwThemePicker extends GwElement {
|
|
|
148
149
|
margin-block-start: 0;
|
|
149
150
|
}
|
|
150
151
|
|
|
152
|
+
/* The cards get a row of their own under the mode, Reset and Import */
|
|
153
|
+
:host([layout="inline"][gallery]) .controls {
|
|
154
|
+
display: contents;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
:host([layout="inline"]) .gallery {
|
|
158
|
+
flex: 1 0 100%;
|
|
159
|
+
order: 1;
|
|
160
|
+
max-height: none;
|
|
161
|
+
overflow: visible;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
:host([layout="inline"][gallery]) .status {
|
|
165
|
+
flex: 1 0 100%;
|
|
166
|
+
order: 2;
|
|
167
|
+
}
|
|
168
|
+
|
|
151
169
|
.actions {
|
|
152
170
|
display: flex;
|
|
153
171
|
flex-wrap: wrap;
|