@kubex/zinc 1.1.101 → 1.1.103

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.
Files changed (82) hide show
  1. package/dist/custom-elements.json +261 -82
  2. package/dist/vscode.html-custom-data.json +13 -13
  3. package/dist/web-types.json +25 -25
  4. package/dist/zn.d.ts +128 -6
  5. package/dist/zn.min.css +1 -1
  6. package/dist/zn.min.js +469 -393
  7. package/docs/pages/components/remarkd-editor.md +59 -0
  8. package/package.json +1 -1
  9. package/scss/_root.scss +121 -8
  10. package/scss/shared/_base.scss +10 -4
  11. package/scss/shared/_button-mixin.scss +29 -0
  12. package/scss/shared/_button.scss +3 -33
  13. package/scss/themes/_aura.scss +87 -8
  14. package/scss/themes/_dark.scss +14 -2
  15. package/scss/themes/_light.scss +4 -3
  16. package/scss/themes/_modes.scss +32 -0
  17. package/src/components/animated-button/animated-button.component.ts +2 -1
  18. package/src/components/animated-button/animated-button.scss +1 -1
  19. package/src/components/bulk-actions/bulk-actions.component.ts +2 -1
  20. package/src/components/bulk-actions/bulk-actions.scss +0 -1
  21. package/src/components/button/button.component.ts +6 -1
  22. package/src/components/button/button.scss +1 -1
  23. package/src/components/chart/chart.component.ts +4 -0
  24. package/src/components/checkbox/checkbox.component.ts +3 -1
  25. package/src/components/checkbox/checkbox.scss +0 -2
  26. package/src/components/checkbox-group/checkbox-group.component.ts +2 -1
  27. package/src/components/checkbox-group/checkbox-group.scss +0 -1
  28. package/src/components/data-table/data-table.component.ts +3 -1
  29. package/src/components/data-table/data-table.scss +0 -1
  30. package/src/components/datepicker/datepicker.component.ts +2 -1
  31. package/src/components/datepicker/datepicker.scss +0 -1
  32. package/src/components/editor/modules/toolbar/toolbar.component.ts +3 -1
  33. package/src/components/editor/modules/toolbar/toolbar.scss +0 -1
  34. package/src/components/file/file.component.ts +2 -1
  35. package/src/components/file/file.scss +0 -1
  36. package/src/components/form-group/form-group.component.ts +2 -1
  37. package/src/components/form-group/form-group.scss +0 -1
  38. package/src/components/icon-picker/icon-picker.component.ts +8 -3
  39. package/src/components/icon-picker/icon-picker.scss +0 -1
  40. package/src/components/inline-edit/inline-edit.component.ts +2 -1
  41. package/src/components/inline-edit/inline-edit.scss +0 -1
  42. package/src/components/input/input.component.ts +2 -1
  43. package/src/components/input/input.scss +0 -1
  44. package/src/components/input-group/input-group.component.ts +2 -1
  45. package/src/components/input-group/input-group.scss +0 -1
  46. package/src/components/menu/menu.component.ts +4 -1
  47. package/src/components/menu/menu.scss +9 -1
  48. package/src/components/page/page.scss +19 -5
  49. package/src/components/priority-list/priority-list.component.ts +2 -1
  50. package/src/components/priority-list/priority-list.scss +0 -1
  51. package/src/components/query-builder/query-builder.component.ts +2 -1
  52. package/src/components/query-builder/query-builder.scss +0 -1
  53. package/src/components/radio/radio.component.ts +3 -1
  54. package/src/components/radio/radio.scss +0 -2
  55. package/src/components/radio-group/radio-group.component.ts +2 -1
  56. package/src/components/radio-group/radio-group.scss +0 -1
  57. package/src/components/rating/rating.component.ts +2 -1
  58. package/src/components/rating/rating.scss +0 -1
  59. package/src/components/remarkd-editor/actions.ts +155 -0
  60. package/src/components/remarkd-editor/feature-keys.ts +17 -0
  61. package/src/components/remarkd-editor/remarkd-editor.component.ts +509 -49
  62. package/src/components/remarkd-editor/remarkd-editor.scss +68 -1
  63. package/src/components/remarkd-editor/remarkd-editor.test.ts +677 -9
  64. package/src/components/schedule-builder/schedule-builder.component.ts +2 -1
  65. package/src/components/schedule-builder/schedule-builder.scss +0 -1
  66. package/src/components/select/select.component.ts +2 -1
  67. package/src/components/select/select.scss +0 -1
  68. package/src/components/slash-menu/slash-menu.scss +2 -0
  69. package/src/components/tabs/tabs.component.ts +3 -1
  70. package/src/components/tabs/tabs.scss +0 -1
  71. package/src/components/textarea/textarea.component.ts +2 -1
  72. package/src/components/textarea/textarea.scss +0 -1
  73. package/src/components/tile/tile.component.ts +33 -20
  74. package/src/components/tile/tile.scss +2 -0
  75. package/src/components/tile/tile.test.ts +64 -3
  76. package/src/components/toggle/toggle.component.ts +2 -1
  77. package/src/components/toggle/toggle.scss +0 -1
  78. package/src/components/translations/translations.component.ts +2 -1
  79. package/src/components/translations/translations.scss +0 -1
  80. package/src/internal/theme.ts +37 -12
  81. package/src/internal/toolbar-overflow.ts +100 -0
  82. package/src/internal/zinc-element.ts +7 -5
@@ -14,6 +14,7 @@ import ZnOption from '../option';
14
14
  import ZnSelect from '../select';
15
15
  import type {ZincFormControl} from '../../internal/zinc-element';
16
16
 
17
+ import formControlStyles from '../../form-control.scss';
17
18
  import styles from './schedule-builder.scss';
18
19
 
19
20
  /** The seven weekday keys used throughout the schedule. */
@@ -533,7 +534,7 @@ type SlotState = 'closed' | 'open' | 'reduced';
533
534
  * @cssproperty --reduced-color - The fill used for hours an exception removes.
534
535
  */
535
536
  export default class ZnScheduleBuilder extends ZincElement implements ZincFormControl {
536
- static styles: CSSResultGroup = unsafeCSS(styles);
537
+ static styles: CSSResultGroup = [unsafeCSS(formControlStyles), unsafeCSS(styles)];
537
538
 
538
539
  static formAssociated = true;
539
540
 
@@ -1,5 +1,4 @@
1
1
  @use "../../wc";
2
- @use "../../form-control";
3
2
 
4
3
  :host {
5
4
  --slot-height: 18px;
@@ -21,6 +21,7 @@ import type {CSSResultGroup, PropertyValues, TemplateResult} from 'lit';
21
21
  import type {ZincFormControl} from '../../internal/zinc-element';
22
22
  import type {ZnRemoveEvent} from "../../events/zn-remove";
23
23
 
24
+ import formControlStyles from '../../form-control.scss';
24
25
  import styles from './select.scss';
25
26
 
26
27
  /**
@@ -77,7 +78,7 @@ import styles from './select.scss';
77
78
  * @csspart expand-icon - The container that wraps the expand icon.
78
79
  */
79
80
  export default class ZnSelect extends ZincElement implements ZincFormControl {
80
- static styles: CSSResultGroup = unsafeCSS(styles);
81
+ static styles: CSSResultGroup = [unsafeCSS(formControlStyles), unsafeCSS(styles)];
81
82
  static dependencies = {
82
83
  'zn-icon': ZnIcon,
83
84
  'zn-opt-group': ZnOptGroup,
@@ -1,5 +1,4 @@
1
1
  @use "../../wc";
2
- @use "../../form-control";
3
2
 
4
3
  :host {
5
4
  display: block;
@@ -1,6 +1,8 @@
1
1
  @use "../../wc";
2
2
 
3
3
  :host {
4
+ @include wc.scrollbars;
5
+
4
6
  --slash-menu-width: 320px;
5
7
  --slash-menu-max-height: 260px;
6
8
  --slash-menu-border-radius: 12px;
@@ -17,6 +17,8 @@ import {property} from 'lit/decorators.js';
17
17
  import {Store} from "../../internal/storage";
18
18
  import ZincElement from '../../internal/zinc-element';
19
19
 
20
+ // Tabs load in such a way that the global sheet has to be re-applied inside the shadow root.
21
+ import bootStyles from '../../../scss/boot.scss';
20
22
  import styles from './tabs.scss';
21
23
 
22
24
  // Every element that manages its own tabs (ZnTabs and its subclasses). Tab
@@ -42,7 +44,7 @@ const tabContainerSelector = 'zn-tabs, zn-page, zn-page-nav';
42
44
  * @cssproperty --example - An example CSS custom property.
43
45
  */
44
46
  export default class ZnTabs extends ZincElement {
45
- static styles: CSSResultGroup = unsafeCSS(styles);
47
+ static styles: CSSResultGroup = [unsafeCSS(bootStyles), unsafeCSS(styles)];
46
48
  @property({attribute: 'master-id', reflect: true}) masterId: string;
47
49
  @property({attribute: 'default-uri', reflect: true}) defaultUri = '';
48
50
  @property({attribute: 'active', reflect: true}) _current = '';
@@ -1,5 +1,4 @@
1
1
  @use "../../wc";
2
- @use "../../../scss/boot"; // Tabs load in such a way we need to include the global styles on them again
3
2
 
4
3
  :host {
5
4
  @include wc.scrollbars;
@@ -14,6 +14,7 @@ import ZincElement, {type ZincFormControl} from '../../internal/zinc-element';
14
14
  import ZnSlashItem from "../slash-item";
15
15
  import ZnSlashMenu from "../slash-menu";
16
16
 
17
+ import formControlStyles from '../../form-control.scss';
17
18
  import styles from './textarea.scss';
18
19
 
19
20
  /**
@@ -52,7 +53,7 @@ import styles from './textarea.scss';
52
53
  * @csspart slash-menu - The slash menu shown at the caret.
53
54
  */
54
55
  export default class ZnTextarea extends ZincElement implements ZincFormControl {
55
- static styles: CSSResultGroup = unsafeCSS(styles);
56
+ static styles: CSSResultGroup = [unsafeCSS(formControlStyles), unsafeCSS(styles)];
56
57
  static dependencies = {
57
58
  'zn-slash-item': ZnSlashItem,
58
59
  'zn-slash-menu': ZnSlashMenu
@@ -1,5 +1,4 @@
1
1
  @use "../../wc";
2
- @use "../../form-control";
3
2
 
4
3
  :host {
5
4
  display: block;
@@ -61,10 +61,21 @@ export default class ZnTile extends ZincElement {
61
61
  }
62
62
 
63
63
  private _handleActionsClick(e: MouseEvent) {
64
- if (this._isLink()) {
65
- e.preventDefault();
66
- e.stopPropagation();
64
+ if (!this._isLink()) return;
65
+
66
+ // Let Rubix's delegated pagelet handler see an action's own link. For controls without
67
+ // navigation metadata, stop before the handler can walk up to the tile host's href.
68
+ for (const target of e.composedPath()) {
69
+ if (target === this) break;
70
+ if (!(target instanceof Element)) continue;
71
+
72
+ const href = target.getAttribute('href');
73
+ if (target.hasAttribute('data-uri') || (href !== null && href !== '' && !href.startsWith('#'))) {
74
+ return;
75
+ }
67
76
  }
77
+
78
+ e.stopPropagation();
68
79
  }
69
80
 
70
81
  render() {
@@ -77,11 +88,7 @@ export default class ZnTile extends ZincElement {
77
88
  const hasImage = this.hasSlotController.test('image');
78
89
 
79
90
  return html`
80
- <${tag}
81
- href="${ifDefined(this.href)}"
82
- data-uri="${ifDefined(this.dataUri)}"
83
- gaid="${ifDefined(this.gaid)}"
84
- data-target="${ifDefined(this.dataTarget)}"
91
+ <div
85
92
  class="${classMap({
86
93
  tile: true,
87
94
  'tile--flush': this.flush,
@@ -96,11 +103,15 @@ export default class ZnTile extends ZincElement {
96
103
  'tile--has-actions': hasActions,
97
104
  'tile--has-image': hasImage,
98
105
  })}">
99
- ${!hasCaption && !hasDescription && !hasProperties && !hasActions ? html`
100
- <slot></slot>
101
- ` : html`
102
- <div
103
- class="tile__link">
106
+ <${tag}
107
+ href="${ifDefined(this.href)}"
108
+ data-uri="${ifDefined(this.dataUri)}"
109
+ gaid="${ifDefined(this.gaid)}"
110
+ data-target="${ifDefined(this.dataTarget)}"
111
+ class="tile__link">
112
+ ${!hasCaption && !hasDescription && !hasProperties && !hasActions ? html`
113
+ <slot></slot>
114
+ ` : html`
104
115
  <div class="tile__left">
105
116
  ${hasImage ? html`
106
117
  <slot name="image" part="image" class="tile__image"></slot>` : html``}
@@ -113,15 +124,17 @@ export default class ZnTile extends ZincElement {
113
124
  ${this.description}</p>` : html`<slot name="description" class="tile__description"></slot>`}
114
125
  </div>
115
126
  </div>
116
- </div>
117
-
118
- <div class="tile__right">
119
127
  ${hasProperties ? html`
120
128
  <slot name="properties" part="properties" class="tile__properties"></slot>` : ''}
121
- ${hasActions ? html`
129
+ `}
130
+ </${tag}>
131
+
132
+ ${hasActions ? html`
133
+ <div class="tile__right">
134
+ <!-- Kept outside the link so slotted controls retain their default behaviour. -->
122
135
  <slot name="actions" part="actions" class="tile__actions"
123
- @click=${this._handleActionsClick}></slot>` : ''}
124
- </div>`}
125
- </${tag}>`;
136
+ @click=${this._handleActionsClick}></slot>
137
+ </div>` : ''}
138
+ </div>`;
126
139
  }
127
140
  }
@@ -69,6 +69,8 @@ a {
69
69
  .tile__link {
70
70
  display: flex;
71
71
  align-items: center;
72
+ justify-content: space-between;
73
+ flex: 1;
72
74
  column-gap: var(--zn-spacing-small);
73
75
  min-width: 0;
74
76
  }
@@ -1,10 +1,71 @@
1
1
  import '../../../dist/zn.min.js';
2
- import { expect, fixture, html } from '@open-wc/testing';
2
+ import {expect, fixture, html} from '@open-wc/testing';
3
+ import type ZnTile from './tile.component';
3
4
 
4
- describe('<zn-list-tile>', () => {
5
+ describe('<zn-tile>', () => {
5
6
  it('should render a component', async () => {
6
- const el = await fixture(html` <zn-list-tile></zn-list-tile> `);
7
+ const el = await fixture(html`<zn-tile></zn-tile>`);
7
8
 
8
9
  expect(el).to.exist;
9
10
  });
11
+
12
+ it('should render its content as a link when href is set', async () => {
13
+ const el = await fixture<ZnTile>(html`
14
+ <zn-tile href="/users/leslie" caption="Leslie Alexander"></zn-tile>`);
15
+
16
+ const link = el.shadowRoot!.querySelector<HTMLAnchorElement>('a.tile__link')!;
17
+ expect(link).to.exist;
18
+ expect(link.getAttribute('href')).to.equal('/users/leslie');
19
+ });
20
+
21
+ it('should render actions outside the link', async () => {
22
+ const el = await fixture<ZnTile>(html`
23
+ <zn-tile href="/users/leslie" caption="Leslie Alexander">
24
+ <zn-button slot="actions">Edit</zn-button>
25
+ </zn-tile>`);
26
+
27
+ const link = el.shadowRoot!.querySelector<HTMLAnchorElement>('a.tile__link')!;
28
+ const actions = el.shadowRoot!.querySelector<HTMLSlotElement>('slot[name="actions"]')!;
29
+ expect(link.contains(actions)).to.be.false;
30
+ });
31
+
32
+ it('should not cancel clicks from a slotted action', async () => {
33
+ const el = await fixture<ZnTile>(html`
34
+ <zn-tile href="/users/leslie" caption="Leslie Alexander">
35
+ <zn-button slot="actions">Edit</zn-button>
36
+ </zn-tile>`);
37
+
38
+ const action = el.querySelector<HTMLElement>('zn-button')!;
39
+ const button = action.shadowRoot!.querySelector<HTMLButtonElement>('button')!;
40
+ let click: MouseEvent | undefined;
41
+ action.addEventListener('click', event => (click = event as MouseEvent));
42
+
43
+ button.click();
44
+
45
+ expect(click).to.exist;
46
+ expect(click!.defaultPrevented).to.be.false;
47
+ });
48
+
49
+ it('should let a linked action reach delegated navigation handlers', async () => {
50
+ const el = await fixture<ZnTile>(html`
51
+ <zn-tile href="/users/leslie" caption="Leslie Alexander">
52
+ <zn-button slot="actions" href="/users/leslie/edit" data-target="modal">Edit</zn-button>
53
+ </zn-tile>`);
54
+
55
+ const action = el.querySelector<HTMLElement>('zn-button')!;
56
+ const buttonLink = action.shadowRoot!.querySelector<HTMLAnchorElement>('a')!;
57
+ let selectedLink: Element | undefined;
58
+ const handleDocumentClick = (event: MouseEvent) => {
59
+ selectedLink = event.composedPath().find(target =>
60
+ target instanceof Element && target.matches('[href]')) as Element | undefined;
61
+ event.preventDefault();
62
+ };
63
+ document.addEventListener('click', handleDocumentClick, {once: true});
64
+
65
+ buttonLink.click();
66
+
67
+ expect(selectedLink).to.equal(buttonLink);
68
+ expect(selectedLink!.getAttribute('href')).to.equal('/users/leslie/edit');
69
+ expect(selectedLink!.getAttribute('data-target')).to.equal('modal');
70
+ });
10
71
  });
@@ -8,6 +8,7 @@ import { live } from "lit/directives/live.js";
8
8
  import { property, query, state } from 'lit/decorators.js';
9
9
  import ZincElement, { type ZincFormControl } from '../../internal/zinc-element';
10
10
 
11
+ import formControlStyles from '../../form-control.scss';
11
12
  import styles from './toggle.scss';
12
13
 
13
14
  /**
@@ -32,7 +33,7 @@ import styles from './toggle.scss';
32
33
  * @cssproperty --zn-toggle-margin - The margin around the toggle switch. Defaults to `8px 0`.
33
34
  */
34
35
  export default class ZnToggle extends ZincElement implements ZincFormControl {
35
- static styles: CSSResultGroup = unsafeCSS(styles);
36
+ static styles: CSSResultGroup = [unsafeCSS(formControlStyles), unsafeCSS(styles)];
36
37
 
37
38
  private readonly hasSlotController = new HasSlotController(this, 'help-text', 'description');
38
39
 
@@ -1,5 +1,4 @@
1
1
  @use "../../wc";
2
- @use "../../form-control";
3
2
 
4
3
  :host {
5
4
  --zn-toggle-margin: 8px 0;
@@ -19,10 +19,11 @@ import type {PropertyValues} from 'lit';
19
19
  import type {ZincFormControl} from '../../internal/zinc-element';
20
20
  import type {ZnMenuSelectEvent} from '../../events/zn-menu-select';
21
21
 
22
+ import formControlStyles from '../../form-control.scss';
22
23
  import styles from './translations.scss';
23
24
 
24
25
  export default class ZnTranslations extends ZincElement implements ZincFormControl {
25
- static styles = unsafeCSS(styles);
26
+ static styles = [unsafeCSS(formControlStyles), unsafeCSS(styles)];
26
27
  static dependencies = {
27
28
  'zn-button': ZnButton,
28
29
  'zn-button-group': ZnButtonGroup,
@@ -1,4 +1,3 @@
1
- @use "../../form-control";
2
1
 
3
2
  :host {
4
3
  display: block;
@@ -1,13 +1,28 @@
1
1
  import {signal, SignalWatcher} from '@lit-labs/signals';
2
2
 
3
+ // Theming has two independent dimensions:
4
+ // `t` — the theme family (base, aura, ...), carrying the identity/palette.
5
+ // `m` — the rendered mode ('light' | 'dark') the family is currently drawn in.
6
+ // The host shell owns both attributes on <html>; components mirror them so CSS
7
+ // selectors like `[m="dark"]` and `:host-context([t="aura"])` keep working.
8
+
9
+ function readRootAttribute(name: string, fallback: string): string {
10
+ if (typeof document === 'undefined') return fallback;
11
+ return document.documentElement.getAttribute(name)
12
+ || document.body?.getAttribute(name)
13
+ || fallback;
14
+ }
15
+
3
16
  function getDefaultTheme(): string {
4
- if (typeof document === 'undefined') return 'light';
5
- return document.documentElement.getAttribute('t')
6
- || document.body.getAttribute('t')
7
- || 'light';
17
+ return readRootAttribute('t', 'light');
18
+ }
19
+
20
+ function getDefaultMode(): string {
21
+ return readRootAttribute('m', 'light');
8
22
  }
9
23
 
10
24
  export const themeSignal = signal<string>(getDefaultTheme());
25
+ export const modeSignal = signal<string>(getDefaultMode());
11
26
 
12
27
  let listenerInstalled = false;
13
28
 
@@ -17,18 +32,28 @@ export function installThemeListener(): void {
17
32
 
18
33
  window.addEventListener('theme-change', (e: Event) => {
19
34
  const detail: unknown = (e as CustomEvent<unknown>).detail;
20
- let next: string;
35
+ let theme: string | undefined;
36
+ let mode: string | undefined;
37
+
21
38
  if (typeof detail === 'string') {
22
- next = detail;
23
- } else if (detail && typeof detail === 'object' && 'theme' in detail && typeof (detail as {theme: unknown}).theme === 'string') {
24
- next = (detail as {theme: string}).theme;
25
- } else {
26
- next = getDefaultTheme();
39
+ // Legacy payload: the theme name on its own.
40
+ theme = detail;
41
+ } else if (detail && typeof detail === 'object') {
42
+ const d = detail as {theme?: unknown; mode?: unknown};
43
+ if (typeof d.theme === 'string') theme = d.theme;
44
+ if (typeof d.mode === 'string') mode = d.mode;
27
45
  }
28
- if (next !== themeSignal.get()) themeSignal.set(next);
46
+
47
+ // Anything the event did not carry is read back off the document, which the
48
+ // shell always writes before dispatching.
49
+ if (theme === undefined) theme = getDefaultTheme();
50
+ if (mode === undefined) mode = getDefaultMode();
51
+
52
+ if (theme !== themeSignal.get()) themeSignal.set(theme);
53
+ if (mode !== modeSignal.get()) modeSignal.set(mode);
29
54
  });
30
55
  }
31
56
 
32
57
  installThemeListener();
33
58
 
34
- export {SignalWatcher};
59
+ export {SignalWatcher};
@@ -0,0 +1,100 @@
1
+ import {ResizeController} from '@lit-labs/observers/resize-controller.js';
2
+ import type {ReactiveController, ReactiveControllerHost} from 'lit';
3
+
4
+ interface ToolbarOverflowOptions {
5
+ /** The measurable group elements, in toolbar order. */
6
+ groups: () => HTMLElement[];
7
+ /** The element whose width the groups have to fit inside. */
8
+ container: () => HTMLElement | null | undefined;
9
+ /** Space to keep for the overflow trigger. Defaults to 44px. */
10
+ reserve?: number;
11
+ }
12
+
13
+ /**
14
+ * Reports how many leading toolbar groups fit the container, so the host can render the
15
+ * rest into an overflow menu. Two passes: the first ignores the trigger, because when
16
+ * everything fits there is no trigger to make room for.
17
+ */
18
+ export class ToolbarOverflowController implements ReactiveController {
19
+ visibleCount = Infinity;
20
+
21
+ private readonly host: ReactiveControllerHost & Element;
22
+ private readonly options: ToolbarOverflowOptions;
23
+ private resizeRafId = 0;
24
+
25
+ constructor(host: ReactiveControllerHost & Element, options: ToolbarOverflowOptions) {
26
+ this.host = host;
27
+ this.options = options;
28
+ host.addController(this);
29
+ // Deferred to a rAF rather than measuring inline: measure() mutates the display of
30
+ // elements inside the observed host, and doing that synchronously in the observer's own
31
+ // callback is what trips "ResizeObserver loop completed with undelivered notifications"
32
+ // (fatal in WebKit's test runner, not just a console warning). Same fix as button-menu.
33
+ // eslint-disable-next-line no-new -- ResizeController registers itself with the host.
34
+ new ResizeController(host, {
35
+ callback: () => {
36
+ if (this.resizeRafId) return;
37
+ this.resizeRafId = requestAnimationFrame(() => {
38
+ this.resizeRafId = 0;
39
+ this.measure();
40
+ });
41
+ },
42
+ });
43
+ }
44
+
45
+ hostUpdated() {
46
+ this.measure();
47
+ }
48
+
49
+ private measure() {
50
+ const groups = this.options.groups();
51
+ const container = this.options.container();
52
+ if (!groups.length || !container) return;
53
+
54
+ // clientWidth includes the container's own padding, so it has to come back out — and so
55
+ // does any non-group sibling (the raw-source toggle, or the trigger itself once it
56
+ // exists) that is really sitting in the same row and eating into the same space. Without
57
+ // this, `total <= available` can read true while a sibling's real footprint still clips
58
+ // trailing groups off-screen with no trigger rendered to reach them.
59
+ const style = getComputedStyle(container);
60
+ const paddingX = parseFloat(style.paddingLeft) + parseFloat(style.paddingRight);
61
+ const siblingsWidth = [...container.children]
62
+ .filter(child => !child.classList.contains('toolbar__group'))
63
+ .reduce((sum, child) => sum + child.getBoundingClientRect().width, 0);
64
+ const available = container.clientWidth - paddingX - siblingsWidth;
65
+ if (available <= 0) return;
66
+
67
+ // Show everything before measuring: a collapsed group measures 0, which would let the
68
+ // next pass re-expand it and oscillate forever. Same reason the editor toolbar resets
69
+ // display before each pass. Reads and writes stay in this frame, so nothing flickers.
70
+ groups.forEach(group => (group.style.display = ''));
71
+ const widths = groups.map(group => group.getBoundingClientRect().width);
72
+ const total = widths.reduce((sum, width) => sum + width, 0);
73
+
74
+ const next = total <= available
75
+ ? groups.length
76
+ : this.countThatFit(widths, available - (this.options.reserve ?? 44));
77
+
78
+ groups.forEach((group, index) => (group.style.display = index < next ? '' : 'none'));
79
+
80
+ if (next !== this.visibleCount) {
81
+ this.visibleCount = next;
82
+ this.host.requestUpdate();
83
+ }
84
+ }
85
+
86
+ private countThatFit(widths: number[], available: number): number {
87
+ let used = 0;
88
+ let count = 0;
89
+ for (const width of widths) {
90
+ if (used + width > available) break;
91
+ used += width;
92
+ count++;
93
+ }
94
+ // No `Math.max(count, 1)` floor here: forcing at least one group into the bar when even
95
+ // that group alone doesn't fit `available` pushed the trigger past the host's own
96
+ // `overflow: hidden` edge — clipped and unreachable, not just visually tight. Zero visible
97
+ // groups is the correct answer in that case; every action still reaches the menu.
98
+ return count;
99
+ }
100
+ }
@@ -1,6 +1,6 @@
1
1
  import {LitElement, type PropertyValues} from "lit";
2
2
  import {property} from "lit/decorators.js";
3
- import {SignalWatcher, themeSignal} from "./theme";
3
+ import {modeSignal, SignalWatcher, themeSignal} from "./theme";
4
4
  import type {
5
5
  EventTypeDoesNotRequireDetail,
6
6
  EventTypeRequiresDetail,
@@ -19,13 +19,15 @@ const ZincElementBase: typeof LitElement & Constructor<SignalWatcherApi> = Signa
19
19
  export default class ZincElement extends ZincElementBase {
20
20
  @property() dir: string; // LTR or RTL direction
21
21
  @property() lang: string; // Language
22
- @property({reflect: true}) t: string; // Theme (light or dark)
22
+ @property({reflect: true}) t: string; // Theme family (base, aura, ...)
23
+ @property({reflect: true}) m: string; // Rendered mode (light or dark)
23
24
 
24
25
  protected override willUpdate(changed: PropertyValues): void {
25
- // Reading themeSignal here subscribes this component to theme changes
26
- // via the SignalWatcher mixin. The reflected `t` attribute keeps existing
27
- // CSS selectors like `[t="dark"]` working.
26
+ // Reading these signals here subscribes this component to theme changes via
27
+ // the SignalWatcher mixin. The reflected attributes keep CSS selectors like
28
+ // `[m="dark"]` and `:host-context([t="aura"])` working inside shadow roots.
28
29
  this.t = themeSignal.get();
30
+ this.m = modeSignal.get();
29
31
  super.willUpdate(changed);
30
32
  }
31
33