@ixfx/components 0.8.1 → 0.8.3

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 (120) hide show
  1. package/bundle/index.d.ts +326 -103
  2. package/bundle/index.d.ts.map +1 -1
  3. package/bundle/index.js +2840 -1285
  4. package/bundle/index.js.map +1 -1
  5. package/dist/ac-text.js +3 -3
  6. package/dist/ac-token.js +3 -3
  7. package/dist/checkbox.js +1 -1
  8. package/dist/{colour-picker-Ddsk-7E_.js → colour-picker-CSxwUmtE.js} +2 -2
  9. package/dist/{colour-picker-Ddsk-7E_.js.map → colour-picker-CSxwUmtE.js.map} +1 -1
  10. package/dist/colour-picker.js +1 -1
  11. package/dist/crumbs.js +4 -4
  12. package/dist/data-display.js +1 -1
  13. package/dist/data-grid-D-z7bx5-.d.ts.map +1 -1
  14. package/dist/data-grid.js +9 -6
  15. package/dist/data-grid.js.map +1 -1
  16. package/dist/detail-list-_ZJHYEBr.d.ts.map +1 -1
  17. package/dist/detail-list.js +68 -5
  18. package/dist/detail-list.js.map +1 -1
  19. package/dist/editable-label.js +1 -1
  20. package/dist/{fallbacks-DNI9xt0F.js → fallbacks-BcKGq-2F.js} +17 -1
  21. package/dist/{fallbacks-DNI9xt0F.js.map → fallbacks-BcKGq-2F.js.map} +1 -1
  22. package/dist/{grid-list-nRUQOcEL.js → grid-list-BXfY3XXX.js} +531 -29
  23. package/dist/grid-list-BXfY3XXX.js.map +1 -0
  24. package/dist/{grid-list-u33vmKzb.d.ts → grid-list-x550wJ0X.d.ts} +115 -3
  25. package/dist/grid-list-x550wJ0X.d.ts.map +1 -0
  26. package/dist/grid-list.d.ts +2 -2
  27. package/dist/grid-list.js +1 -1
  28. package/dist/handle-BPOxBOKf.js +219 -0
  29. package/dist/handle-BPOxBOKf.js.map +1 -0
  30. package/dist/{icon-DcEEGQa_.js → icon-Dy33gu4Z.js} +2 -2
  31. package/dist/{icon-DcEEGQa_.js.map → icon-Dy33gu4Z.js.map} +1 -1
  32. package/dist/icons.js +1 -1
  33. package/dist/{index-BeuCXtiO.d.ts → index-DV3iaUBW.d.ts} +2 -2
  34. package/dist/{index-BeuCXtiO.d.ts.map → index-DV3iaUBW.d.ts.map} +1 -1
  35. package/dist/index.d.ts +132 -50
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +554 -206
  38. package/dist/index.js.map +1 -1
  39. package/dist/{interaction-Cr13azbM.js → interaction-B9AAlsFO.js} +1 -1
  40. package/dist/labelled-radial-input.js +2 -2
  41. package/dist/{labelled-slider-input-BInB5GKL.js → labelled-slider-input-BCH9968c.js} +3 -3
  42. package/dist/{labelled-slider-input-BInB5GKL.js.map → labelled-slider-input-BCH9968c.js.map} +1 -1
  43. package/dist/labelled-slider-input.js +1 -1
  44. package/dist/list-selection-controller-SA4piSBi.js +589 -0
  45. package/dist/list-selection-controller-SA4piSBi.js.map +1 -0
  46. package/dist/{menu-Bg90Qr0B.js → menu-HMoxCPY2.js} +5 -5
  47. package/dist/{menu-Bg90Qr0B.js.map → menu-HMoxCPY2.js.map} +1 -1
  48. package/dist/{menu-item-Bx-Fs7jq.js → menu-item-BQDYxk5A.js} +5 -5
  49. package/dist/{menu-item-Bx-Fs7jq.js.map → menu-item-BQDYxk5A.js.map} +1 -1
  50. package/dist/menu.js +2 -2
  51. package/dist/miller.d.ts +1 -1
  52. package/dist/miller.js +3 -3
  53. package/dist/narrowed-text.js +3 -3
  54. package/dist/notifier.js +1 -1
  55. package/dist/panel.d.ts +1 -1
  56. package/dist/panel.js +1 -1
  57. package/dist/plots.d.ts +2 -2
  58. package/dist/plots.js +1 -1
  59. package/dist/polar-pad.js +1 -1
  60. package/dist/{radial-input-BfA3GO0H.js → radial-input-DyxfbNzD.js} +2 -2
  61. package/dist/{radial-input-BfA3GO0H.js.map → radial-input-DyxfbNzD.js.map} +1 -1
  62. package/dist/radial-input.js +1 -1
  63. package/dist/range-input.js +1 -1
  64. package/dist/range.js +1 -1
  65. package/dist/select-horiz.js +3 -3
  66. package/dist/{slider-input-Bl1hFE89.js → slider-input-WR8_RwHg.js} +2 -2
  67. package/dist/{slider-input-Bl1hFE89.js.map → slider-input-WR8_RwHg.js.map} +1 -1
  68. package/dist/slider-input.js +1 -1
  69. package/dist/snackbar.js +1 -1
  70. package/dist/{split-layout-BFSbUZQv.js → split-layout-CA5jbNbG.js} +22 -33
  71. package/dist/split-layout-CA5jbNbG.js.map +1 -0
  72. package/dist/split-layout-JK3xWVX2.d.ts.map +1 -1
  73. package/dist/split-layout.js +1 -1
  74. package/dist/swipe.js +1 -1
  75. package/dist/tabs.js +1 -1
  76. package/dist/{tickled-controller-Bn3YdQsM.d.ts → tickled-controller-C1ZsHpyg.d.ts} +10 -1
  77. package/dist/tickled-controller-C1ZsHpyg.d.ts.map +1 -0
  78. package/dist/{tickled-controller-C66DgJHl.js → tickled-controller-Dh1Uh_vG.js} +4 -1
  79. package/dist/tickled-controller-Dh1Uh_vG.js.map +1 -0
  80. package/dist/{timeline-B2fgDL4U.js → timeline-THyvH8xy.js} +14 -8
  81. package/dist/timeline-THyvH8xy.js.map +1 -0
  82. package/dist/timeline.d.ts +1 -1
  83. package/dist/timeline.js +1 -1
  84. package/dist/{titlebar-YQ-jVQoS.d.ts → titlebar-Tqu-VvMU.d.ts} +5 -1
  85. package/dist/titlebar-Tqu-VvMU.d.ts.map +1 -0
  86. package/dist/titlebar.d.ts +1 -1
  87. package/dist/titlebar.js +5 -3
  88. package/dist/titlebar.js.map +1 -1
  89. package/dist/{tree-Chptss7I.js → tree-DTQon838.js} +4 -4
  90. package/dist/{tree-Chptss7I.js.map → tree-DTQon838.js.map} +1 -1
  91. package/dist/tree.d.ts +1 -1
  92. package/dist/tree.js +1 -1
  93. package/dist/vertical-list-CFuFDygi.d.ts.map +1 -1
  94. package/dist/vertical-list.js +68 -4
  95. package/dist/vertical-list.js.map +1 -1
  96. package/dist/{xy-axis-DzM1brBZ.js → xy-axis-CrrVdDFe.js} +28 -3
  97. package/dist/xy-axis-CrrVdDFe.js.map +1 -0
  98. package/dist/{xy-axis-BfP9hKLj.d.ts → xy-axis-DQqd2yXJ.d.ts} +28 -12
  99. package/dist/{xy-axis-BfP9hKLj.d.ts.map → xy-axis-DQqd2yXJ.d.ts.map} +1 -1
  100. package/dist/xy-pad.js +1 -1
  101. package/docs-user/data-grid.md +1 -1
  102. package/docs-user/detail-list.md +4 -0
  103. package/docs-user/dock.md +6 -3
  104. package/docs-user/grid-list.md +49 -0
  105. package/docs-user/index.json +2 -2
  106. package/docs-user/infra-tickled.md +2 -0
  107. package/docs-user/split-layout.md +2 -4
  108. package/docs-user/twosplit-layout.md +14 -16
  109. package/docs-user/vertical-list.md +4 -0
  110. package/package.json +1 -1
  111. package/dist/grid-list-nRUQOcEL.js.map +0 -1
  112. package/dist/grid-list-u33vmKzb.d.ts.map +0 -1
  113. package/dist/list-selection-controller-DTFI40li.js +0 -264
  114. package/dist/list-selection-controller-DTFI40li.js.map +0 -1
  115. package/dist/split-layout-BFSbUZQv.js.map +0 -1
  116. package/dist/tickled-controller-Bn3YdQsM.d.ts.map +0 -1
  117. package/dist/tickled-controller-C66DgJHl.js.map +0 -1
  118. package/dist/timeline-B2fgDL4U.js.map +0 -1
  119. package/dist/titlebar-YQ-jVQoS.d.ts.map +0 -1
  120. package/dist/xy-axis-DzM1brBZ.js.map +0 -1
package/bundle/index.js CHANGED
@@ -3,8 +3,8 @@ import { customElement, eventOptions, property, query, queryAssignedElements, st
3
3
  import { styleMap } from "lit/directives/style-map.js";
4
4
  import { unsafeHTML } from "lit/directives/unsafe-html.js";
5
5
  import { Fzf } from "fzf";
6
- import { Colour } from "@ixfx/visual";
7
6
  import { createCommandRegistry } from "@clinth/ui-commands";
7
+ import { Colour } from "@ixfx/visual";
8
8
  import { Trees } from "@ixfx/collections";
9
9
  import { clamp, clamper, rangeStream, round, scaler } from "@ixfx/numbers";
10
10
  import { repeat } from "lit/directives/repeat.js";
@@ -730,6 +730,7 @@ var TickledController = class TickledController {
730
730
  this.host = host;
731
731
  this.config = config;
732
732
  this._inputMode = config.initialInputMode ?? `mouse`;
733
+ this.hoverDisabled = config.hoverDisabled ?? (() => false);
733
734
  host.addController(this);
734
735
  }
735
736
  hostConnected() {
@@ -798,6 +799,7 @@ var TickledController = class TickledController {
798
799
  * Switches to mouse mode and tickles the item.
799
800
  */
800
801
  handlePointerEnter(id) {
802
+ if (this.hoverDisabled()) return;
801
803
  if (!TickledController._mouseMovedSinceKeyboard) return;
802
804
  const item = this.config.getItems().find((item) => item.id === id);
803
805
  if (item && item.enabled === false) return;
@@ -815,6 +817,7 @@ var TickledController = class TickledController {
815
817
  * @param _e The pointer event (not used, but kept for API compatibility)
816
818
  */
817
819
  handlePointerLeave(_e) {
820
+ if (this.hoverDisabled()) return;
818
821
  if (TickledController._mouseMovedSinceKeyboard) this.clearTickled();
819
822
  }
820
823
  /**
@@ -1287,6 +1290,22 @@ const _themeFallbacksCss = i$3`
1287
1290
  inherits: true;
1288
1291
  initial-value: 0 4px 24px rgba(0, 0, 0, 0.5);
1289
1292
  }
1293
+ @property --deboss-3 {
1294
+ syntax: '*';
1295
+ inherits: true;
1296
+ initial-value:
1297
+ inset 1px 1px 1px 1px color-mix(in oklch, oklch(8% 0.02 240) 60%, transparent),
1298
+ inset -1px -1px 1px 1px color-mix(in oklch, oklch(15% 0.02 240) 50%, transparent),
1299
+ -2px -2px 1px 0px color-mix(in oklch, oklch(12% 0.02 240) 30%, transparent);
1300
+ }
1301
+ @property --emboss-3 {
1302
+ syntax: '*';
1303
+ inherits: true;
1304
+ initial-value:
1305
+ inset 1px 1px 1px 1px color-mix(in oklch, oklch(22% 0.02 240) 30%, transparent),
1306
+ inset -1px -1px 1px 1px color-mix(in oklch, oklch(12% 0.02 240) 20%, transparent),
1307
+ 2px 2px 1px 0px color-mix(in oklch, oklch(12% 0.02 240) 30%, transparent);
1308
+ }
1290
1309
  @property --item-bg {
1291
1310
  syntax: '<color>';
1292
1311
  inherits: true;
@@ -3893,488 +3912,1605 @@ __decorate([state()], AcTokenElement.prototype, "_selectedTokenIndex", void 0);
3893
3912
  __decorate([state()], AcTokenElement.prototype, "_isOpen", void 0);
3894
3913
  AcTokenElement = __decorate([safeCustomElement(`ixfx-ac-token`)], AcTokenElement);
3895
3914
  //#endregion
3896
- //#region src/icons/defaults.ts
3897
- /**
3898
- * Chevron pointing down. Used for split-button dropdown toggle.
3899
- * fill="currentColor" so it inherits CSS color.
3900
- */
3901
- const ICON_CHEVRON_DOWN = `<svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"/></svg>`;
3902
- /**
3903
- * Caret pointing right. Used for tree-basic expand indicator as a CSS mask.
3904
- * fill="black" is correct for CSS mask-image use (shape only, color comes from background-color).
3905
- */
3906
- const ICON_CARET_RIGHT = `<svg viewBox="0 0 24 24" fill="black" xmlns="http://www.w3.org/2000/svg"><path d="M10 7l6 5-6 5V7z"/></svg>`;
3907
- /**
3908
- * Checkmark. Useful for menu-check-item or confirmation states.
3909
- * fill="currentColor" so it inherits CSS color.
3910
- */
3911
- const ICON_CHECK = `<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z"/></svg>`;
3912
- /**
3913
- * Close / X icon. Useful for dialogs, notifications, dismissible elements.
3914
- * fill="currentColor" so it inherits CSS color.
3915
- */
3916
- const ICON_CLOSE = `<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/></svg>`;
3917
- /**
3918
- * Plus / add icon. Pairs with `close` for add/remove actions.
3919
- * fill="currentColor" so it inherits CSS color.
3920
- */
3921
- const ICON_ADD = `<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>`;
3922
- /**
3923
- * Error / warning icon. Useful for error notifications.
3924
- * fill="currentColor" so it inherits CSS color.
3925
- */
3926
- const ICON_ERROR = `<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/></svg>`;
3927
- /**
3928
- * Info icon. Useful for info notifications.
3929
- * fill="currentColor" so it inherits CSS color.
3930
- */
3931
- const ICON_INFO = `<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/></svg>`;
3932
- /**
3933
- * Warning icon. Useful for warning notifications.
3934
- * fill="currentColor" so it inherits CSS color.
3935
- */
3936
- const ICON_WARNING = `<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/></svg>`;
3937
- /**
3938
- * Chevron pointing right. Used for split-button switcher toggle.
3939
- * fill="currentColor" so it inherits CSS color.
3940
- */
3941
- const ICON_CHEVRON_RIGHT = `<svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"/></svg>`;
3942
- /**
3943
- * Folder with up arrow. Angled line style for navigating up one level.
3944
- * fill="currentColor" so it inherits CSS color.
3945
- */
3946
- const ICON_FOLDER_UP = `<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 4L4 12h5v8h6v-8h5L12 4z"/></svg>`;
3947
- /**
3948
- * Folder. Useful for file explorers and sidebars.
3949
- * fill="currentColor" so it inherits CSS color.
3950
- */
3951
- const ICON_FOLDER = `<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 1.99-.9 1.99-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg>`;
3952
- /**
3953
- * Document / file page. Useful for file lists and document tabs.
3954
- * fill="currentColor" so it inherits CSS color.
3955
- */
3956
- const ICON_FILE = `<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"/></svg>`;
3957
- /**
3958
- * Magnifying glass. Useful for search panels and inputs.
3959
- * fill="currentColor" so it inherits CSS color.
3960
- */
3961
- const ICON_SEARCH = `<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>`;
3962
- function registerDefaults() {
3963
- registerIcon(`chevron-down`, ICON_CHEVRON_DOWN);
3964
- registerIcon(`chevron-right`, ICON_CHEVRON_RIGHT);
3965
- registerIcon(`caret-right`, ICON_CARET_RIGHT);
3966
- registerIcon(`check`, ICON_CHECK);
3967
- registerIcon(`close`, ICON_CLOSE);
3968
- registerIcon(`file`, ICON_FILE);
3969
- registerIcon(`folder`, ICON_FOLDER);
3970
- registerIcon(`search`, ICON_SEARCH);
3971
- registerIcon(`add`, ICON_ADD);
3972
- registerIcon(`error`, ICON_ERROR);
3973
- registerIcon(`folder-up`, ICON_FOLDER_UP);
3974
- registerIcon(`info`, ICON_INFO);
3975
- registerIcon(`warning`, ICON_WARNING);
3976
- }
3977
- //#endregion
3978
- //#region src/icons/index.ts
3979
- registerDefaults();
3980
- //#endregion
3981
- //#region src/styles/prominence.ts
3915
+ //#region src/styles/handle.ts
3982
3916
  /**
3983
- * Shared CSS variable defaults for prominence-based interaction feedback.
3917
+ * Shared drag / resize **handle** styling.
3984
3918
  *
3985
- * Defines the `--prominence-*` variables on `:host`. Include this if you
3986
- * are building custom prominence selectors; otherwise use `prominenceStyles`
3987
- * or `prominenceSubStyles` which include these variables automatically.
3988
- */
3989
- const prominenceVars = i$3`
3990
- :host {
3991
- --prominence-idle-opacity: 0.6;
3992
- --prominence-idle-saturate: 0.4;
3993
- --prominence-tickled-opacity: 0.85;
3994
- --prominence-tickled-saturate: 0.8;
3995
- --prominence-disabled-opacity: 0.3;
3996
- --prominence-disabled-saturate: 0.1;
3997
- }
3998
- `;
3999
- /**
4000
- * Host-based prominence interaction feedback.
3919
+ * A handle is the grab affordance for moving or resizing an object. Apply the
3920
+ * `.ixfx-handle` class to a small decorative element inside your drag / resize
3921
+ * hit-zone and choose a look with two independent attributes:
4001
3922
  *
4002
- * Applies CSS `filter` to **the whole component host** to convey state:
4003
- * - **Disabled** (`[disabled]` / `:disabled`): heavily muted
4004
- * - **Idle** (default): reduced opacity + desaturated
4005
- * - **Tickled** (`:hover` / `:focus-within`): elevated opacity + saturation
4006
- * - **Active** (`:active` / `[dragging]`): full vibrancy (filter removed)
3923
+ * - `data-handle` — texture: `flat` (default) · `dotted` · `dimpled` · `grated`
3924
+ * - `data-shape` topology: `horizontal` (default) · `vertical` · `spot`
4007
3925
  *
4008
- * Defaults are defined on `:host` and can be overridden per-instance.
3926
+ * - **flat** a solid rounded capsule (bar) or dot (`spot`), filled with
3927
+ * `--ixfx-handle-color`.
3928
+ * - **dotted** — marks in the [Lucide *grip*](https://lucide.dev/icons/grip)
3929
+ * arrangement: three in a row (`horizontal`), three in a column (`vertical`) or a
3930
+ * 3×3 block (`spot`). Tinted by `--ixfx-handle-color`. Each mark's corner
3931
+ * rounding is `--ixfx-handle-radius` (→ the theme `--radius-l`), so a flat,
3932
+ * squared-off theme renders them as **squares** instead of circles.
3933
+ * - **dimpled** — that same layout, but each bar mark is pressed in: a lighter
3934
+ * copy peeking out above-left and a darker copy below-right of the base mark.
3935
+ * - **grated** — fine 45° knurling: each ridge is a lit edge, the base colour,
3936
+ * then a shaded edge. `spot` is the same hatch on a square crop.
3937
+ *
3938
+ * `dotted` / `dimpled` / `grated` are texture only — no plate, box or border; the
3939
+ * gaps are transparent so the surface behind shows through. The base colour is
3940
+ * `--ixfx-handle-color` (a theme token); the deboss edges default to that colour
3941
+ * lightened / darkened.
3942
+ *
3943
+ * The handle is `pointer-events: none` — bind pointer handlers to the surrounding
3944
+ * hit-zone. It reaches its "active" look (`--ixfx-handle-opacity-active`, and for
3945
+ * `flat` / `dotted` the `--ixfx-handle-color-active` tint) when its immediate
3946
+ * parent (the hit-zone) is `:hover` / `:focus-visible` / `[data-active]`, or when
3947
+ * the handle itself is. Set `[data-active]` on the hit-zone yourself while a drag
3948
+ * is live so the handle stays lit after the pointer leaves it.
4009
3949
  *
4010
- * Usage:
4011
3950
  * ```ts
4012
- * import { prominenceStyles } from '../styles/prominence.js';
3951
+ * import { handleStyles } from '../styles/handle.js';
4013
3952
  *
4014
- * class MyComponent extends LitElement {
4015
- * static styles = [prominenceStyles, css`...`];
4016
- * }
3953
+ * static styles = [themeFallbacks, handleStyles, css`...`];
4017
3954
  * ```
3955
+ * ```html
3956
+ * <div class="hit-zone" @pointerdown=${this.#onDown}>
3957
+ * <span class="ixfx-handle" data-handle="dotted" data-shape="spot" part="handle"></span>
3958
+ * </div>
3959
+ * ```
3960
+ *
3961
+ * ## CSS variables (defaults shown; override on the element or any ancestor)
3962
+ * - `--ixfx-handle-color` `var(--surface-muted-text)` — `flat` / `dotted` fill
3963
+ * - `--ixfx-handle-color-active` `var(--accent)` — `flat` / `dotted` hover / focus
3964
+ * - `--ixfx-handle-thickness` `6px` — cross-axis size of a bar
3965
+ * - `--ixfx-handle-length` `44px` — long-axis size of a bar (`100%` to fill)
3966
+ * - `--ixfx-handle-spot-size` `14px` — width = height of a `spot`
3967
+ * - `--ixfx-handle-radius` `var(--radius-l)` — `flat` capsule / dotted & dimpled mark rounding
3968
+ * - `--ixfx-handle-opacity` `0.55` — rest opacity
3969
+ * - `--ixfx-handle-opacity-active` `1`
3970
+ * - `--ixfx-handle-dot-size` `3px` — mark size (dotted / dimpled)
3971
+ * - `--ixfx-handle-dot-gap` `length/4` (bar) · `spot/3` — mark centre-to-centre step
3972
+ * - `--ixfx-handle-tile` `5px` — knurl period (grated)
3973
+ * - `--ixfx-handle-highlight` `color-mix(currentColor 35%, #fff)` — lit deboss edge
3974
+ * - `--ixfx-handle-shadow` `color-mix(currentColor 60%, #000)` — shaded deboss edge
4018
3975
  */
4019
- const prominenceStyles = i$3`
4020
- ${prominenceVars}
4021
-
4022
- :host {
4023
- filter:
4024
- opacity(var(--prominence-idle-opacity))
4025
- saturate(var(--prominence-idle-saturate));
4026
- transition: filter var(--transition, 0.15s ease-out);
4027
- }
4028
-
4029
- :host(:hover),
4030
- :host(:focus-within) {
4031
- filter:
4032
- opacity(var(--prominence-tickled-opacity))
4033
- saturate(var(--prominence-tickled-saturate));
4034
- }
4035
-
4036
- :host(:active),
4037
- :host([dragging]) {
4038
- filter: none;
4039
- }
3976
+ const handleStyles = i$3`
3977
+ .ixfx-handle {
3978
+ /* Bridge: public --ixfx-handle-* (element or ancestor) → internal alias → fallback. */
3979
+ --_handle-color: var(--ixfx-handle-color, var(--surface-3-text));
3980
+ --_handle-color-active: var(--ixfx-handle-color-active, var(--surface-3-text));
3981
+ --_handle-thickness: var(--ixfx-handle-thickness, 6px);
3982
+ --_handle-length: var(--ixfx-handle-length, 44px);
3983
+ --_handle-spot: var(--ixfx-handle-spot-size, 14px);
3984
+ --_handle-radius: var(--ixfx-handle-radius, var(--radius-l));
3985
+ --_handle-opacity: var(--ixfx-handle-opacity, 0.55);
3986
+ --_handle-opacity-active: var(--ixfx-handle-opacity-active, 1);
3987
+ --_handle-dot: var(--ixfx-handle-dot-size, 3px);
3988
+ --_handle-tile: var(--ixfx-handle-tile, 5px);
3989
+ /* Centre-to-centre step of the dotted / dimpled marks (a length). Defaults to
3990
+ a share of the long axis; a bar shows ~3 marks. */
3991
+ --_handle-gap: var(--ixfx-handle-dot-gap, calc(var(--_handle-length) / 4));
3992
+ /* How far a dimpled mark's highlight / shadow copy is offset. */
3993
+ --_dimple-off: calc(var(--_handle-dot) * 0.15);
3994
+ /* The mark colour lightened / darkened for the two deboss edges. */
3995
+ --_handle-hi: var(--ixfx-handle-highlight, color-mix(in oklch, var(--surface-4) 50%, var(--_handle-color)));
3996
+ --_handle-lo: var(--ixfx-handle-shadow, color-mix(in oklch, var(--surface-2) 40%, var(--_handle-color)));
4040
3997
 
4041
- /* Disabled comes last to override tickled/active selectors of equal specificity */
4042
- :host([disabled]),
4043
- :host(:disabled) {
4044
- filter:
4045
- opacity(var(--prominence-disabled-opacity))
4046
- saturate(var(--prominence-disabled-saturate));
3998
+ box-sizing: border-box;
3999
+ display: inline-block;
4000
+ flex: none;
4001
+ width: var(--_handle-length);
4002
+ height: var(--_handle-thickness);
4003
+ color: var(--_handle-color);
4004
+ opacity: var(--_handle-opacity);
4047
4005
  pointer-events: none;
4048
- }
4049
- `;
4050
- i$3`
4051
- ${prominenceVars}
4052
-
4053
- .prominence {
4054
- filter:
4055
- opacity(var(--prominence-idle-opacity))
4056
- saturate(var(--prominence-idle-saturate));
4057
- transition: filter var(--transition, 0.15s ease-out);
4058
- }
4059
-
4060
- .prominence:hover,
4061
- .prominence:focus-within {
4062
- filter:
4063
- opacity(var(--prominence-tickled-opacity))
4064
- saturate(var(--prominence-tickled-saturate));
4006
+ transition:
4007
+ color var(--transition, 0.15s ease-out),
4008
+ opacity var(--transition, 0.15s ease-out);
4065
4009
  }
4066
4010
 
4067
- .prominence:active,
4068
- .prominence.dragging {
4069
- filter: none;
4011
+ .ixfx-handle[data-shape='vertical'] {
4012
+ width: var(--_handle-thickness);
4013
+ height: var(--_handle-length);
4070
4014
  }
4071
4015
 
4072
- .prominence[disabled],
4073
- .prominence:disabled {
4074
- filter:
4075
- opacity(var(--prominence-disabled-opacity))
4076
- saturate(var(--prominence-disabled-saturate));
4077
- pointer-events: none;
4078
- }
4079
- `;
4080
- //#endregion
4081
- //#region src/button/button-styles.ts
4082
- const buttonStyles = i$3`
4083
- :host {
4084
- display: inline-flex;
4085
- max-width: 100%;
4086
- vertical-align: middle;
4087
- user-select: none;
4088
- -webkit-user-select: none;
4016
+ .ixfx-handle[data-shape='spot'] {
4017
+ width: var(--_handle-spot);
4018
+ height: var(--_handle-spot);
4019
+ --_handle-gap: var(--ixfx-handle-dot-gap, calc(var(--_handle-spot) / 3));
4089
4020
  }
4090
4021
 
4091
- :host([hidden]) {
4092
- display: none;
4022
+ .ixfx-handle:hover,
4023
+ .ixfx-handle:focus-visible,
4024
+ .ixfx-handle[data-active],
4025
+ :hover > .ixfx-handle,
4026
+ :focus-visible > .ixfx-handle,
4027
+ [data-active] > .ixfx-handle {
4028
+ color: var(--_handle-color-active);
4029
+ opacity: var(--_handle-opacity-active);
4093
4030
  }
4094
4031
 
4095
- button {
4096
- cursor: pointer;
4097
- appearance: none;
4098
- background: var(--surface-4, #ccc);
4099
- color: var(--surface-3-text, #000);
4100
- border: var(--border, 1px solid #999);
4101
- outline: var(--ixfx-button-outline, none);
4102
- border-radius: var(--radius-xl, 8px);
4103
- corner-shape: superellipse(1);
4104
- padding: var(--ixfx-button-padding-block, var(--space-m, 8px)) var(--ixfx-button-padding-inline, var(--space-l, 12px));
4105
- font-family: var(--font-family, inherit);
4106
- font-size: var(--text-m, 14px);
4107
- margin: 0;
4108
- display: inline-flex;
4109
- align-items: center;
4110
- justify-content: center;
4111
- gap: 0.5em;
4112
- width: 100%;
4113
- overflow: hidden;
4114
- white-space: nowrap;
4115
- box-shadow:
4116
- var(--ixfx-button-shade, none),
4117
- inset 1px 1px 1px 1px color-mix(in oklch, var(--surface-4) 30%, transparent), inset -1px -1px 1px 1px color-mix(in oklch, var(--surface-2) 20%, transparent),
4118
- 2px 2px 1px 0px color-mix(in oklch, var(--surface-2) 30%, transparent);
4119
- transition: background var(--transition, 0.2s), border-color var(--transition, 0.2s);
4032
+ /* ── flat: solid capsule / dot ────────────────────────────────────────── */
4033
+ .ixfx-handle[data-handle='flat'] {
4034
+ background: currentColor;
4035
+ border-radius: var(--_handle-radius);
4120
4036
  }
4121
-
4122
- button:disabled {
4123
- opacity: 0.5;
4124
- cursor: not-allowed;
4037
+ .ixfx-handle[data-handle='flat'][data-shape='spot'] {
4038
+ border-radius: 50%;
4125
4039
  }
4126
4040
 
4127
- button:hover:not(:disabled) {
4128
- background: color-mix(in oklch, var(--surface-4) 80%, transparent);
4129
- border-color: var(--accent, #666);
4130
- box-shadow:
4131
- inset 1px 1px 1px 1px color-mix(in oklch, var(--surface-5) 70%, transparent),
4132
- inset -1px -1px 1px 1px color-mix(in oklch, var(--surface-2) 60%, transparent),
4133
- 2px 2px 1px 0px color-mix(in oklch, var(--surface-2) 90%, transparent);
4041
+ /* ── dotted / dimpled: 3 marks in a row / column (or 3×3 for spot), drawn as
4042
+ one ::before mark whose border-radius follows the theme — round normally,
4043
+ square in a flat / squared-off theme (--radius-l: 0). box-shadow paints
4044
+ the rest of the marks. ─────────────────────────────────────────────── */
4045
+ .ixfx-handle[data-handle='dotted'],
4046
+ .ixfx-handle[data-handle='dimpled'] {
4047
+ position: relative;
4134
4048
  }
4135
-
4136
- button:active:not(:disabled) {
4137
- background: var(--surface-2, #aaa);
4138
- transform: translateY(1px);
4139
- box-shadow: inset 1px 1px 0px 0.5px color-mix(in oklch,var(--surface-1),transparent);
4049
+ .ixfx-handle[data-handle='dotted']::before,
4050
+ .ixfx-handle[data-handle='dimpled']::before {
4051
+ content: '';
4052
+ position: absolute;
4053
+ top: 50%;
4054
+ left: 50%;
4055
+ width: var(--_handle-dot);
4056
+ height: var(--_handle-dot);
4057
+ transform: translate(-50%, -50%);
4058
+ border-radius: var(--_handle-radius);
4059
+ background: currentColor;
4140
4060
  }
4141
4061
 
4142
- .button-icon {
4143
- display: var(--ixfx-button-hide-icon, inline-flex);
4144
- align-items: center;
4145
- flex-shrink: 0;
4062
+ /* dotted — the two other marks are plain copies along the axis. */
4063
+ .ixfx-handle[data-handle='dotted']::before {
4064
+ box-shadow:
4065
+ calc(-1 * var(--_handle-gap)) 0 0 0 currentColor,
4066
+ var(--_handle-gap) 0 0 0 currentColor;
4146
4067
  }
4147
-
4148
- .button-label {
4149
- white-space: nowrap;
4068
+ .ixfx-handle[data-handle='dotted'][data-shape='vertical']::before {
4069
+ box-shadow:
4070
+ 0 calc(-1 * var(--_handle-gap)) 0 0 currentColor,
4071
+ 0 var(--_handle-gap) 0 0 currentColor;
4150
4072
  }
4151
-
4152
- :host([has-icon]) .button-label {
4153
- display: var(--ixfx-button-hide-label, inline);
4073
+ .ixfx-handle[data-handle='dotted'][data-shape='spot']::before,
4074
+ .ixfx-handle[data-handle='dimpled'][data-shape='spot']::before {
4075
+ box-shadow:
4076
+ calc(-1 * var(--_handle-gap)) calc(-1 * var(--_handle-gap)) 0 0 currentColor,
4077
+ 0 calc(-1 * var(--_handle-gap)) 0 0 currentColor,
4078
+ var(--_handle-gap) calc(-1 * var(--_handle-gap)) 0 0 currentColor,
4079
+ calc(-1 * var(--_handle-gap)) 0 0 0 currentColor,
4080
+ var(--_handle-gap) 0 0 0 currentColor,
4081
+ calc(-1 * var(--_handle-gap)) var(--_handle-gap) 0 0 currentColor,
4082
+ 0 var(--_handle-gap) 0 0 currentColor,
4083
+ var(--_handle-gap) var(--_handle-gap) 0 0 currentColor;
4154
4084
  }
4155
4085
 
4156
- :host([hide-label]) .button-label {
4157
- display: none;
4086
+ /* dimpled — each bar mark also gets a lighter copy up-left and a darker copy
4087
+ down-right (spot stays plain — the 3D is imperceptible at that size). */
4088
+ .ixfx-handle[data-handle='dimpled']:not([data-shape='spot'])::before {
4089
+ box-shadow:
4090
+ calc(-1 * var(--_dimple-off)) calc(-1 * var(--_dimple-off)) 0 0 var(--_handle-hi),
4091
+ var(--_dimple-off) var(--_dimple-off) 0 0 var(--_handle-lo),
4092
+ calc(-1 * var(--_handle-gap)) 0 0 0 currentColor,
4093
+ calc(-1 * var(--_handle-gap) - var(--_dimple-off)) calc(-1 * var(--_dimple-off)) 0 0 var(--_handle-hi),
4094
+ calc(-1 * var(--_handle-gap) + var(--_dimple-off)) var(--_dimple-off) 0 0 var(--_handle-lo),
4095
+ var(--_handle-gap) 0 0 0 currentColor,
4096
+ calc(var(--_handle-gap) - var(--_dimple-off)) calc(-1 * var(--_dimple-off)) 0 0 var(--_handle-hi),
4097
+ calc(var(--_handle-gap) + var(--_dimple-off)) var(--_dimple-off) 0 0 var(--_handle-lo);
4158
4098
  }
4159
-
4160
- :host([hide-icon]) .button-icon {
4161
- display: none;
4099
+ .ixfx-handle[data-handle='dimpled'][data-shape='vertical']::before {
4100
+ box-shadow:
4101
+ calc(-1 * var(--_dimple-off)) calc(-1 * var(--_dimple-off)) 0 0 var(--_handle-hi),
4102
+ var(--_dimple-off) var(--_dimple-off) 0 0 var(--_handle-lo),
4103
+ 0 calc(-1 * var(--_handle-gap)) 0 0 currentColor,
4104
+ calc(-1 * var(--_dimple-off)) calc(-1 * var(--_handle-gap) - var(--_dimple-off)) 0 0 var(--_handle-hi),
4105
+ var(--_dimple-off) calc(-1 * var(--_handle-gap) + var(--_dimple-off)) 0 0 var(--_handle-lo),
4106
+ 0 var(--_handle-gap) 0 0 currentColor,
4107
+ calc(-1 * var(--_dimple-off)) calc(var(--_handle-gap) - var(--_dimple-off)) 0 0 var(--_handle-hi),
4108
+ var(--_dimple-off) calc(var(--_handle-gap) + var(--_dimple-off)) 0 0 var(--_handle-lo);
4162
4109
  }
4163
4110
 
4164
- :host([subtle]) button {
4165
- background: color-mix(in oklch, var(--surface-4, #ccc) 40%, transparent);
4111
+ /* ── grated: fine 45° knurling — each ridge is a lit edge, the base colour,
4112
+ then a shaded edge. Same hatch at every shape (spot is a square crop). ── */
4113
+ .ixfx-handle[data-handle='grated'] {
4114
+ background-image: repeating-linear-gradient(
4115
+ 45deg,
4116
+ var(--_handle-hi) 0 0.9px,
4117
+ currentColor 0.9px 1.6px,
4118
+ var(--_handle-lo) 1.6px 2.5px,
4119
+ transparent 2.5px var(--_handle-tile)
4120
+ );
4166
4121
  }
4167
4122
 
4168
- :host([subtle]) button:hover:not(:disabled),
4169
- :host([subtle]) button:focus-visible:not(:disabled) {
4170
- background: color-mix(in oklch, var(--surface-4, #ccc) 70%, transparent);
4123
+ @media (prefers-reduced-motion: reduce) {
4124
+ .ixfx-handle {
4125
+ transition: none;
4126
+ }
4171
4127
  }
4172
4128
  `;
4173
4129
  //#endregion
4174
- //#region src/button/button.ts
4175
- let ButtonElement = class ButtonElement extends i {
4176
- constructor(..._args) {
4177
- super(..._args);
4178
- this.disabled = false;
4179
- this.hideIcon = false;
4180
- this.hideLabel = false;
4181
- this.hasIcon = false;
4182
- this.subtle = false;
4183
- }
4184
- _renderIcon() {
4185
- if (this.hideIcon || !this.icon) return A;
4186
- if (this.icon.startsWith(`<svg`)) return b`<span class="button-icon">${unsafeHTML(this.icon)}</span>`;
4187
- if (!hasIcon(this.icon)) return A;
4188
- return b`<ixfx-icon class="button-icon" name="${this.icon}"></ixfx-icon>`;
4189
- }
4190
- render() {
4191
- this.hasIcon = !this.hideIcon && !!this.icon;
4192
- const label = this.hideLabel ? A : b`<span class="button-label"><slot></slot></span>`;
4193
- return b`<button ?disabled=${this.disabled}>${this._renderIcon()}${label}</button>`;
4194
- }
4195
- static {
4196
- this.styles = [prominenceStyles, buttonStyles];
4197
- }
4198
- };
4199
- __decorate([property({ type: Boolean })], ButtonElement.prototype, "disabled", void 0);
4200
- __decorate([property({ type: String })], ButtonElement.prototype, "icon", void 0);
4201
- __decorate([property({
4202
- type: Boolean,
4203
- attribute: `hide-icon`,
4204
- reflect: true
4205
- })], ButtonElement.prototype, "hideIcon", void 0);
4206
- __decorate([property({
4207
- type: Boolean,
4208
- attribute: `hide-label`,
4209
- reflect: true
4210
- })], ButtonElement.prototype, "hideLabel", void 0);
4211
- __decorate([property({
4212
- type: Boolean,
4213
- attribute: `has-icon`,
4214
- reflect: true
4215
- })], ButtonElement.prototype, "hasIcon", void 0);
4216
- __decorate([property({
4217
- type: Boolean,
4218
- reflect: true
4219
- })], ButtonElement.prototype, "subtle", void 0);
4220
- ButtonElement = __decorate([safeCustomElement(`ixfx-button`)], ButtonElement);
4221
- //#endregion
4222
- //#region src/button/radio-group.ts
4223
- let RadioGroup = class RadioGroup extends i {
4130
+ //#region src/titlebar/titlebar.ts
4131
+ let TitlebarElement = class TitlebarElement extends i {
4224
4132
  constructor(..._args) {
4225
4133
  super(..._args);
4226
- this.value = ``;
4227
- this.orientation = `horizontal`;
4228
- this.items = [];
4229
- this.name = ``;
4230
- this.mode = `compact`;
4231
- this.checked = false;
4232
- this.valueOff = ``;
4134
+ this.title = ``;
4233
4135
  this.iconName = ``;
4234
- this.iconNameOff = ``;
4235
- this.hideLabels = false;
4236
- this._computedItems = [];
4237
- this._icons = /* @__PURE__ */ new Map();
4238
- }
4239
- get _isSingleToggle() {
4240
- return this._computedItems.length === 0 && Boolean(this.textContent?.trim());
4241
- }
4242
- get _singleToggleIconName() {
4243
- if (!this.iconName && !this.iconNameOff) return null;
4244
- return this.checked ? this.iconName : this.iconNameOff || this.iconName;
4245
- }
4246
- firstUpdated() {
4247
- this.#updateComputedItems();
4248
- }
4249
- updated(changedProperties) {
4250
- if (changedProperties.has(`checked`)) {
4251
- const input = this.shadowRoot?.querySelector(`.radio-toggle-input`);
4252
- if (input && input.checked !== this.checked) {
4253
- input.checked = this.checked;
4254
- const newValue = this.checked ? this.value || `on` : this.valueOff || void 0;
4255
- this.dispatchEvent(new CustomEvent(`change`, {
4256
- detail: {
4257
- checked: this.checked,
4258
- value: newValue
4259
- },
4260
- bubbles: true,
4261
- composed: true
4262
- }));
4263
- }
4264
- }
4265
- if (changedProperties.has(`value`) && !this._isSingleToggle) {
4266
- const inputs = this.shadowRoot?.querySelectorAll(`.radio-input`);
4267
- if (inputs) {
4268
- for (const input of inputs) if (input.checked !== (input.value === this.value)) input.checked = input.value === this.value;
4269
- }
4270
- }
4271
- }
4272
- #updateComputedItems() {
4273
- const options = this.querySelectorAll(`option`);
4274
- const icons = /* @__PURE__ */ new Map();
4275
- if (options.length > 0) {
4276
- this._computedItems = Array.from(options, (el) => {
4277
- const tempDiv = document.createElement(`div`);
4278
- tempDiv.innerHTML = el.innerHTML;
4279
- const iconEl = tempDiv.querySelector(`svg`);
4280
- if (iconEl) icons.set(el.getAttribute(`value`) || ``, iconEl.outerHTML);
4281
- return {
4282
- value: el.getAttribute(`value`) || ``,
4283
- label: el.textContent?.trim() || ``,
4284
- disabled: el.hasAttribute(`disabled`),
4285
- iconName: el.getAttribute(`icon-name`) || void 0,
4286
- iconNameOff: el.getAttribute(`icon-name-off`) || void 0
4287
- };
4288
- });
4289
- this._icons = icons;
4290
- } else this._computedItems = [...this.items];
4136
+ this.orientation = `horizontal`;
4137
+ this.variant = `titlebar`;
4138
+ this.closable = false;
4139
+ this.collapsible = false;
4140
+ this.collapsed = false;
4141
+ this.toggleInteraction = `implicit`;
4291
4142
  }
4292
- #handleRadioChange(e) {
4293
- e.stopImmediatePropagation();
4294
- const input = e.target;
4295
- this.value = input.value;
4296
- this.dispatchEvent(new CustomEvent(`change`, {
4297
- detail: {
4298
- checked: true,
4299
- value: input.value
4300
- },
4143
+ _onToggleClick() {
4144
+ if (!this.collapsible) return;
4145
+ this.collapsed = !this.collapsed;
4146
+ this.dispatchEvent(new CustomEvent(`toggle`, {
4147
+ detail: { collapsed: this.collapsed },
4301
4148
  bubbles: true,
4302
4149
  composed: true
4303
4150
  }));
4304
4151
  }
4305
- #handleToggleChange(e) {
4306
- e.stopImmediatePropagation();
4307
- const input = e.target;
4308
- this.checked = input.checked;
4309
- const newValue = this.checked ? this.value || `on` : this.valueOff || void 0;
4310
- this.dispatchEvent(new CustomEvent(`change`, {
4311
- detail: {
4312
- checked: this.checked,
4313
- value: newValue
4314
- },
4152
+ _onCloseClick() {
4153
+ this.dispatchEvent(new CustomEvent(`close`, {
4315
4154
  bubbles: true,
4316
4155
  composed: true
4317
4156
  }));
4318
4157
  }
4319
- #renderIcon(item) {
4320
- const iconName = item.value === this.value ? item.iconName || null : item.iconNameOff || item.iconName || null;
4321
- if (iconName) return b`<ixfx-icon class="radio-icon" name="${iconName}"></ixfx-icon>`;
4322
- const iconHtml = this._icons.get(item.value);
4323
- if (!iconHtml) return null;
4324
- return b`<span class="radio-icon">${unsafeHTML(iconHtml)}</span>`;
4158
+ _getCaretIcon() {
4159
+ return `chevron-down`;
4325
4160
  }
4326
4161
  render() {
4327
- if (this._isSingleToggle) {
4328
- const iconName = this._singleToggleIconName;
4329
- const iconPart = iconName || this.hideLabels ? b`<ixfx-icon class="radio-icon" name="${iconName || `check`}"></ixfx-icon>` : null;
4330
- const labelPart = !this.hideLabels ? b`<span class="radio-label" data-text="${this.textContent?.trim()}">${this.textContent?.trim()}</span>` : null;
4331
- return b`
4332
- <label class="radio-item ${this.checked ? `selected` : ``}">
4333
- <input
4334
- type="checkbox"
4335
- class="radio-toggle-input"
4336
- ?checked=${this.checked}
4337
- @change=${this.#handleToggleChange}
4338
- />
4339
- ${iconPart}
4340
- ${labelPart}
4341
- </label>
4342
- `;
4343
- }
4162
+ const caretIcon = this._getCaretIcon();
4163
+ const isVertical = this.orientation === `vertical`;
4164
+ const showCaret = this.collapsible && this.toggleInteraction !== `disabled`;
4165
+ const implicitToggle = this.collapsible && this.toggleInteraction === `implicit`;
4166
+ const caretRotation = isVertical ? this.collapsed ? `` : `-90deg` : this.collapsed ? `-90deg` : ``;
4167
+ const treeVariant = this.variant === `tree`;
4344
4168
  return b`
4345
- <slot @slotchange=${this.#updateComputedItems} style="display: none;"></slot>
4346
- <div class="radio-group ${this.orientation} ${this.mode}" role="radiogroup" aria-label=${this.name}>
4347
- ${this._computedItems.map((item) => {
4348
- const iconName = item.value === this.value ? item.iconName || null : item.iconNameOff || item.iconName || null;
4349
- const iconPart = iconName || this.hideLabels ? b`<ixfx-icon class="radio-icon" name="${iconName || `check`}"></ixfx-icon>` : null;
4350
- const labelPart = !this.hideLabels ? b`<span class="radio-label" data-text="${item.label}">${item.label}</span>` : null;
4351
- return b`
4352
- <label class="radio-item ${item.value === this.value ? `selected` : ``}" ?disabled=${item.disabled}>
4353
- <input
4354
- type="radio"
4355
- class="radio-input"
4356
- name=${this.name}
4357
- value=${item.value}
4358
- ?disabled=${item.disabled}
4359
- ?checked=${item.value === this.value}
4360
- @change=${this.#handleRadioChange}
4361
- />
4362
- ${iconPart}
4363
- ${labelPart}
4364
- </label>
4365
- `;
4366
- })}
4169
+ <div
4170
+ class="bar"
4171
+ role="toolbar"
4172
+ @click=${implicitToggle ? this._onToggleClick : A}
4173
+ >
4174
+ ${treeVariant && showCaret ? b`
4175
+ <button
4176
+ class="caret-btn"
4177
+ type="button"
4178
+ aria-label=${this.collapsed ? `Expand panel` : `Collapse panel`}
4179
+ @click=${this._onToggleClick}
4180
+ >
4181
+ <ixfx-icon name=${caretIcon} prominence .rotate=${caretRotation.length > 0 ? caretRotation : A}></ixfx-icon>
4182
+ </button>
4183
+ ` : A}
4184
+ <div class="title-text-wrapper">
4185
+ ${this.iconName ? b`<ixfx-icon class="title-icon" name=${this.iconName}></ixfx-icon>` : A}
4186
+ <span class="title-text">${this.title}</span>
4187
+ </div>
4188
+
4189
+ <div class="toolbar" @click=${(e) => e.stopPropagation()}>
4190
+ <slot name="toolbar"></slot>
4191
+ ${this.closable ? b`
4192
+ <button
4193
+ class="close-btn"
4194
+ type="button"
4195
+ aria-label="Close panel"
4196
+ @click=${this._onCloseClick}
4197
+ >
4198
+ <ixfx-icon name="close"></ixfx-icon>
4199
+ </button>
4200
+ ` : A}
4201
+ ${!treeVariant && showCaret ? b`
4202
+ <button
4203
+ class="caret-btn"
4204
+ type="button"
4205
+ aria-label=${this.collapsed ? `Expand panel` : `Collapse panel`}
4206
+ @click=${this._onToggleClick}
4207
+ >
4208
+ <ixfx-icon name=${caretIcon} prominence .rotate=${caretRotation.length > 0 ? caretRotation : A}></ixfx-icon>
4209
+ </button>
4210
+ ` : A}
4211
+ </div>
4367
4212
  </div>
4368
4213
  `;
4369
4214
  }
4370
4215
  static {
4371
- this.styles = [
4372
- themeFallbacks,
4373
- prominenceStyles,
4374
- i$3`
4216
+ this.styles = [themeFallbacks, i$3`
4375
4217
  :host {
4376
- display: inline-flex;
4377
- min-width: min-content;
4218
+ --titlebar-background: var(--surface-4);
4219
+
4220
+ display: block;
4221
+ box-sizing: border-box;
4222
+ }
4223
+
4224
+ .bar {
4225
+ display: flex;
4226
+ flex-direction: row;
4227
+ align-items: center;
4228
+ padding: var(--space-s) var(--space-m);
4229
+ background: var(--titlebar-background);
4230
+ color: var(--surface-3-text);
4231
+ min-height: 2em;
4232
+ height: 100%;
4233
+ flex-shrink: 0;
4234
+ box-sizing: border-box;
4235
+ }
4236
+
4237
+ :host([orientation="vertical"]) .bar {
4238
+ flex-direction: row;
4239
+ min-width: 2em;
4240
+ writing-mode: vertical-rl;
4241
+ text-orientation: mixed;
4242
+ }
4243
+
4244
+ .caret-btn,
4245
+ .close-btn {
4246
+ display: inline-flex;
4247
+ align-items: center;
4248
+ justify-content: center;
4249
+ background: none;
4250
+ border: none;
4251
+ padding: var(--space-xs);
4252
+ margin: 0;
4253
+ cursor: pointer;
4254
+ color: inherit;
4255
+ border-radius: var(--radius-s);
4256
+ transition: background var(--transition);
4257
+ opacity: var(--prominence-idle-opacity);
4258
+ }
4259
+
4260
+ .caret-btn:hover,
4261
+ .close-btn:hover {
4262
+ background: var(--surface-5);
4263
+ opacity: var(--prominence-tickled-opacity);
4264
+ }
4265
+
4266
+ .caret-btn:focus-visible,
4267
+ .close-btn:focus-visible {
4268
+ outline: 2px solid var(--accent);
4269
+ outline-offset: 1px;
4270
+ }
4271
+
4272
+ ixfx-icon {
4273
+ width: 1.25em;
4274
+ height: 1.25em;
4275
+ transition: transform var(--transition);
4276
+ }
4277
+
4278
+ .title-icon {
4279
+ flex-shrink: 0;
4280
+ }
4281
+
4282
+ .title-text {
4283
+ flex: 1;
4284
+ padding-left: 1ch;
4285
+ font-family: var(--font-family);
4286
+ font-size: var(--text-m);
4287
+ font-weight: var(--font-weight-bold, 600);
4288
+ white-space: nowrap;
4289
+ overflow: hidden;
4290
+ align-self: center;
4291
+ text-overflow: ellipsis;
4292
+ user-select: none;
4293
+ -webkit-user-select: none;
4294
+ }
4295
+
4296
+ :host([collapsible][toggle-interaction="implicit"]) .bar {
4297
+ cursor: pointer;
4298
+ }
4299
+
4300
+ :host([orientation="vertical"]) .title-text {
4301
+ writing-mode: vertical-rl;
4302
+ padding-left: 0;
4303
+ padding-bottom: 1ch;
4304
+ text-orientation: mixed;
4305
+ transform: rotate(180deg);
4306
+ }
4307
+
4308
+ .toolbar {
4309
+ display: flex;
4310
+ align-items: center;
4311
+ gap: var(--space-xs);
4312
+ margin-left: auto;
4313
+ }
4314
+
4315
+ :host([orientation="vertical"]) .toolbar {
4316
+ flex-direction: column;
4317
+ margin-left: 0;
4318
+ margin-top: auto;
4319
+ writing-mode: horizontal-tb;
4320
+ }
4321
+
4322
+ slot[name="toolbar"] {
4323
+ display: contents;
4324
+ }
4325
+
4326
+ ::slotted(slot[name="toolbar"]) {
4327
+ display: contents;
4328
+ }
4329
+
4330
+ .title-text-wrapper {
4331
+ display: flex;
4332
+ padding: 0 var(--space-s);
4333
+ }
4334
+
4335
+ /* === Midline variant === */
4336
+
4337
+ :host([variant="midline"]) .bar {
4338
+ background: transparent;
4339
+ padding: var(--space-xs) var(--space-m);
4340
+ min-height: unset;
4341
+ position: relative;
4342
+ }
4343
+
4344
+ :host([variant="midline"][orientation="vertical"]) .bar {
4345
+ padding: var(--space-m) var(--space-xs);
4346
+ min-width: unset;
4347
+ }
4348
+
4349
+ :host([variant="midline"]) .title-text-wrapper {
4350
+ position: relative;
4351
+ z-index: 1;
4352
+ display: flex;
4353
+ align-items: center;
4354
+ gap: var(--space-s);
4355
+ background: var(--surface-3);
4356
+ }
4357
+
4358
+ :host([variant="midline"]) .title-text {
4359
+ font-weight: normal;
4360
+ color: var(--surface-3-text);
4361
+ padding: 0 0.5ch 0 0.5ch;
4362
+ background: transparent;
4363
+ box-shadow: none;
4364
+ flex: unset;
4365
+ width: auto;
4366
+ white-space: nowrap;
4367
+ }
4368
+
4369
+ :host([variant="midline"]) .title-icon {
4370
+ background: transparent;
4371
+ flex-shrink: 0;
4372
+ }
4373
+
4374
+ :host([variant="midline"]) .caret-btn {
4375
+ position: relative;
4376
+ z-index: 1;
4377
+ background: var(--surface-3);
4378
+ opacity: 1;
4379
+ }
4380
+
4381
+ :host([variant="midline"][orientation="vertical"]) .title-text {
4382
+ transform: rotate(180deg);
4383
+ padding: 0.5ch 0 0.5ch 0ch;
4384
+ }
4385
+
4386
+ :host([variant="midline"]) .toolbar {
4387
+ position: relative;
4388
+ z-index: 2;
4389
+ background: var(--surface-3);
4390
+ margin-left: 0;
4391
+ }
4392
+
4393
+ :host([variant="midline"][orientation="horizontal"]) .toolbar {
4394
+ margin-left: auto;
4395
+ }
4396
+
4397
+ /* Debossed line runs from after title-text to before toolbar */
4398
+ :host([variant="midline"]) .bar::after {
4399
+ content: '';
4400
+ position: absolute;
4401
+ top: 50%;
4402
+ left: 0;
4403
+ right: 0;
4404
+ height: 1px;
4405
+ transform: translateY(-50%);
4406
+ background: var(--surface-2);
4407
+ box-shadow:
4408
+ 0 -1px 0 var(--surface-5),
4409
+ 0 1px 0 var(--surface-1);
4410
+ z-index: 0;
4411
+ }
4412
+
4413
+ /* For vertical midline, line runs vertically */
4414
+ :host([variant="midline"][orientation="vertical"]) .bar::after {
4415
+ top: 0;
4416
+ bottom: 0;
4417
+ left: 50%;
4418
+ right: unset;
4419
+ width: 1px;
4420
+ height: 100%;
4421
+ transform: translateX(-50%);
4422
+ box-shadow:
4423
+ -1px 0 0 var(--surface-5),
4424
+ 1px 0 0 var(--surface-1);
4425
+ }
4426
+
4427
+ /* === Tree variant: VS Code explorer / Outline style === */
4428
+ :host([variant="tree"]) .bar {
4429
+ background: transparent;
4430
+ padding: 0 var(--space-xs);
4431
+ min-height: unset;
4432
+ gap: var(--space-xs);
4433
+ }
4434
+
4435
+ :host([variant="tree"][orientation="vertical"]) .bar {
4436
+ padding: var(--space-xs) 0;
4437
+ flex-direction: column;
4438
+ min-width: unset;
4439
+ writing-mode: horizontal-tb;
4440
+ }
4441
+
4442
+ :host([variant="tree"]) .title-text-wrapper {
4443
+ display: inline-flex;
4444
+ align-items: center;
4445
+ gap: var(--space-xs);
4446
+ padding: 0;
4447
+ background: transparent;
4448
+ }
4449
+
4450
+ :host([variant="tree"]) .title-icon {
4451
+ background: transparent;
4452
+ flex-shrink: 0;
4453
+ }
4454
+
4455
+ :host([variant="tree"]) .title-text {
4456
+ font-weight: normal;
4457
+ color: var(--surface-3-text);
4458
+ padding: 0;
4459
+ background: transparent;
4460
+ box-shadow: none;
4461
+ flex: unset;
4462
+ width: auto;
4463
+ white-space: nowrap;
4464
+ }
4465
+
4466
+ :host([variant="tree"][orientation="vertical"]) .title-text {
4467
+ writing-mode: vertical-rl;
4468
+ transform: rotate(180deg);
4469
+ padding: 0;
4470
+ }
4471
+
4472
+ :host([variant="tree"]) .caret-btn {
4473
+ background: transparent;
4474
+ opacity: var(--prominence-idle-opacity);
4475
+ padding: 0;
4476
+ }
4477
+
4478
+ :host([variant="tree"]) .caret-btn:hover {
4479
+ background: transparent;
4480
+ opacity: var(--prominence-tickled-opacity);
4481
+ }
4482
+
4483
+ :host([variant="tree"]) .toolbar {
4484
+ margin-left: auto;
4485
+ }
4486
+
4487
+ :host([variant="tree"][orientation="vertical"]) .toolbar {
4488
+ flex-direction: row;
4489
+ margin-left: 0;
4490
+ margin-top: auto;
4491
+ }
4492
+
4493
+ :host([variant="tree"]) ixfx-icon {
4494
+ width: 1em;
4495
+ height: 1em;
4496
+ }
4497
+ `];
4498
+ }
4499
+ };
4500
+ __decorate([property({ type: String })], TitlebarElement.prototype, "title", void 0);
4501
+ __decorate([property({
4502
+ type: String,
4503
+ attribute: `icon-name`
4504
+ })], TitlebarElement.prototype, "iconName", void 0);
4505
+ __decorate([property({
4506
+ type: String,
4507
+ attribute: `orientation`,
4508
+ reflect: true
4509
+ })], TitlebarElement.prototype, "orientation", void 0);
4510
+ __decorate([property({
4511
+ type: String,
4512
+ attribute: `variant`,
4513
+ reflect: true
4514
+ })], TitlebarElement.prototype, "variant", void 0);
4515
+ __decorate([property({ type: Boolean })], TitlebarElement.prototype, "closable", void 0);
4516
+ __decorate([property({
4517
+ type: Boolean,
4518
+ reflect: true
4519
+ })], TitlebarElement.prototype, "collapsible", void 0);
4520
+ __decorate([property({
4521
+ type: Boolean,
4522
+ reflect: true
4523
+ })], TitlebarElement.prototype, "collapsed", void 0);
4524
+ __decorate([property({
4525
+ type: String,
4526
+ attribute: `toggle-interaction`
4527
+ })], TitlebarElement.prototype, "toggleInteraction", void 0);
4528
+ TitlebarElement = __decorate([safeCustomElement(`ixfx-titlebar`)], TitlebarElement);
4529
+ //#endregion
4530
+ //#region src/bottom-drawer/bottom-drawer.ts
4531
+ /** Builds the command id for a named drawer's command, e.g. `inspector.expand`. */
4532
+ function bottomDrawerCommandId(name, command) {
4533
+ return `${name.trim()}.${command}`;
4534
+ }
4535
+ function clampRange(value, min, max) {
4536
+ return Math.min(max, Math.max(min, value));
4537
+ }
4538
+ let BottomDrawerElement = class BottomDrawerElement extends i {
4539
+ constructor(..._args) {
4540
+ super(..._args);
4541
+ this.open = false;
4542
+ this.height = 320;
4543
+ this.minHeight = 160;
4544
+ this.dismissThreshold = 96;
4545
+ this.maxHeightRatio = .85;
4546
+ this.label = `Drawer`;
4547
+ this.iconName = ``;
4548
+ this.closable = true;
4549
+ this.collapsible = true;
4550
+ this.maintainHandle = false;
4551
+ this.name = ``;
4552
+ this.#dragStartY = 0;
4553
+ this.#dragStartHeight = 0;
4554
+ this.#collapsedDragging = false;
4555
+ this.#commandName = ``;
4556
+ this.#commandIds = [];
4557
+ this.#onResizeStart = (event) => {
4558
+ if (event.button !== 0) return;
4559
+ event.preventDefault();
4560
+ const edge = event.currentTarget;
4561
+ edge.setPointerCapture(event.pointerId);
4562
+ if (!this.open) {
4563
+ this.#collapsedDragging = false;
4564
+ this.#dragStartY = event.clientY;
4565
+ this.#dragStartHeight = this.dismissThreshold;
4566
+ edge.addEventListener(`pointermove`, this.#onTabPointerMove);
4567
+ edge.addEventListener(`pointerup`, this.#onTabPointerUp, { once: true });
4568
+ edge.addEventListener(`pointercancel`, this.#onTabPointerUp, { once: true });
4569
+ return;
4570
+ }
4571
+ this.#dragStartY = event.clientY;
4572
+ this.#dragStartHeight = this.#displayHeight();
4573
+ this._dragHeight = this.#dragStartHeight;
4574
+ this.toggleAttribute(`resizing`, true);
4575
+ edge.addEventListener(`pointermove`, this.#onResizeMove);
4576
+ edge.addEventListener(`pointerup`, this.#onResizeEnd, { once: true });
4577
+ edge.addEventListener(`pointercancel`, this.#onResizeEnd, { once: true });
4578
+ };
4579
+ this.#onResizeMove = (event) => {
4580
+ this._dragHeight = clampRange(this.#dragStartHeight + this.#dragStartY - event.clientY, 0, this.#maxHeight());
4581
+ };
4582
+ this.#onResizeEnd = (event) => {
4583
+ const edge = event.currentTarget;
4584
+ edge.removeEventListener(`pointermove`, this.#onResizeMove);
4585
+ edge.removeEventListener(`pointerup`, this.#onResizeEnd);
4586
+ edge.removeEventListener(`pointercancel`, this.#onResizeEnd);
4587
+ if (edge.hasPointerCapture(event.pointerId)) edge.releasePointerCapture(event.pointerId);
4588
+ this.toggleAttribute(`resizing`, false);
4589
+ const height = this._dragHeight ?? this.height;
4590
+ this._dragHeight = void 0;
4591
+ if (height < this.dismissThreshold) {
4592
+ this.close();
4593
+ return;
4594
+ }
4595
+ this.#setHeight(height);
4596
+ };
4597
+ this.#onResizeKeyDown = (event) => {
4598
+ if (!this.open) {
4599
+ if (event.key === `ArrowUp` || event.key === `Enter` || event.key === ` ` || event.key === `Home` || event.key === `End`) {
4600
+ event.preventDefault();
4601
+ this.show();
4602
+ }
4603
+ return;
4604
+ }
4605
+ const delta = event.shiftKey ? 64 : 16;
4606
+ if (event.key === `ArrowUp`) {
4607
+ event.preventDefault();
4608
+ this.#setHeight(this.height + delta);
4609
+ } else if (event.key === `ArrowDown`) {
4610
+ event.preventDefault();
4611
+ if (this.height - delta < this.dismissThreshold) this.close();
4612
+ else this.#setHeight(this.height - delta);
4613
+ } else if (event.key === `Home`) {
4614
+ event.preventDefault();
4615
+ this.#setHeight(this.minHeight);
4616
+ } else if (event.key === `End`) {
4617
+ event.preventDefault();
4618
+ this.#setHeight(this.#maxHeight());
4619
+ }
4620
+ };
4621
+ this.#onTabPointerMove = (event) => {
4622
+ const delta = this.#dragStartY - event.clientY;
4623
+ if (!this.#collapsedDragging) {
4624
+ if (delta < 4) return;
4625
+ this.#collapsedDragging = true;
4626
+ this.show();
4627
+ this.toggleAttribute(`resizing`, true);
4628
+ }
4629
+ this._dragHeight = clampRange(this.#dragStartHeight + this.#dragStartY - event.clientY, 0, this.#maxHeight());
4630
+ };
4631
+ this.#onTabPointerUp = (event) => {
4632
+ const target = event.currentTarget;
4633
+ target.removeEventListener(`pointermove`, this.#onTabPointerMove);
4634
+ target.removeEventListener(`pointerup`, this.#onTabPointerUp);
4635
+ target.removeEventListener(`pointercancel`, this.#onTabPointerUp);
4636
+ if (target.hasPointerCapture(event.pointerId)) target.releasePointerCapture(event.pointerId);
4637
+ if (!this.#collapsedDragging) {
4638
+ this.show();
4639
+ return;
4640
+ }
4641
+ this.#collapsedDragging = false;
4642
+ this.toggleAttribute(`resizing`, false);
4643
+ const height = this._dragHeight ?? this.height;
4644
+ this._dragHeight = void 0;
4645
+ if (height < this.dismissThreshold) {
4646
+ this.close();
4647
+ return;
4648
+ }
4649
+ this.#setHeight(height);
4650
+ };
4651
+ this.#onDrawerKeyDown = (event) => {
4652
+ if (event.key !== `Escape`) return;
4653
+ event.preventDefault();
4654
+ event.stopPropagation();
4655
+ this.close();
4656
+ };
4657
+ }
4658
+ static {
4659
+ this.styles = [
4660
+ themeFallbacks,
4661
+ handleStyles,
4662
+ i$3`
4663
+ :host {
4664
+ --bottom-drawer-background: color-mix(in srgb, var(--surface-2) 84%, transparent);
4665
+ --bottom-drawer-border: 1px solid color-mix(in srgb, var(--surface-2-text) 14%, transparent);
4666
+ --bottom-drawer-radius: var(--radius-l);
4667
+ --bottom-drawer-blur: 18px;
4668
+ --bottom-drawer-shadow: var(--shadow-l);
4669
+ --bottom-drawer-collapsed-width: 96px;
4670
+
4671
+ display: block;
4672
+ position: relative;
4673
+ height: 100%;
4674
+ min-width: 0;
4675
+ min-height: 0;
4676
+ overflow: hidden;
4677
+ }
4678
+ .content {
4679
+ position: absolute;
4680
+ inset: 0;
4681
+ min-width: 0;
4682
+ min-height: 0;
4683
+ }
4684
+ .content ::slotted(*) {
4685
+ width: 100%;
4686
+ height: 100%;
4687
+ }
4688
+ .drawer {
4689
+ --m: 0;
4690
+ position: absolute;
4691
+ z-index: 2;
4692
+ right: calc(var(--m) * (50% - var(--bottom-drawer-collapsed-width) / 2));
4693
+ bottom: 0;
4694
+ left: calc(var(--m) * (50% - var(--bottom-drawer-collapsed-width) / 2));
4695
+ display: flex;
4696
+ flex-direction: column;
4697
+ box-sizing: border-box;
4698
+ height: var(--bottom-drawer-height);
4699
+ max-height: 100%;
4700
+ min-height: 0;
4701
+ color: var(--surface-2-text);
4702
+ background: var(--bottom-drawer-background);
4703
+ border: var(--bottom-drawer-border);
4704
+ border-bottom: 0;
4705
+ border-radius: var(--bottom-drawer-radius) var(--bottom-drawer-radius) 0 0;
4706
+ box-shadow: var(--bottom-drawer-shadow);
4707
+ backdrop-filter: blur(var(--bottom-drawer-blur)) saturate(1.15);
4708
+ -webkit-backdrop-filter: blur(var(--bottom-drawer-blur)) saturate(1.15);
4709
+ opacity: 0;
4710
+ transform: translateY(100%);
4711
+ visibility: hidden;
4712
+ transition:
4713
+ transform var(--transition),
4714
+ opacity var(--transition),
4715
+ left var(--transition),
4716
+ right var(--transition),
4717
+ height var(--transition),
4718
+ visibility 0s linear 0.16s;
4719
+ will-change: transform, opacity;
4720
+ }
4721
+ :host([open]) .drawer {
4722
+ opacity: var(--bottom-drawer-opacity, 1);
4723
+ transform: translateY(0);
4724
+ visibility: visible;
4725
+ transition:
4726
+ transform var(--transition),
4727
+ opacity var(--transition),
4728
+ left var(--transition),
4729
+ right var(--transition),
4730
+ height var(--transition);
4731
+ }
4732
+ :host([resizing]) {
4733
+ cursor: ns-resize;
4734
+ user-select: none;
4735
+ }
4736
+ :host([resizing]) .drawer {
4737
+ transition: none;
4738
+ }
4739
+ .resize-edge {
4740
+ position: absolute;
4741
+ z-index: 1;
4742
+ top: -5px;
4743
+ right: 0;
4744
+ left: 0;
4745
+ height: 11px;
4746
+ cursor: ns-resize;
4747
+ touch-action: none;
4748
+ outline: none;
4749
+ }
4750
+ .resize-edge .ixfx-handle {
4751
+ position: absolute;
4752
+ top: 7px;
4753
+ left: 50%;
4754
+ transform: translateX(-50%);
4755
+ --ixfx-handle-length: 44px;
4756
+ --ixfx-handle-thickness: 3px;
4757
+ --ixfx-handle-radius: 999px;
4758
+ }
4759
+ /* Inner content fades as the drawer morphs into its collapsed tab. */
4760
+ ixfx-titlebar,
4761
+ .body {
4762
+ opacity: calc(1 - var(--m, 0));
4763
+ transition: opacity var(--transition);
4764
+ }
4765
+ :host([resizing]) ixfx-titlebar,
4766
+ :host([resizing]) .body {
4767
+ transition: none;
4768
+ }
4769
+ /* Collapsed tab: docked to the bottom edge, narrowed to handle width. */
4770
+ :host([maintain-handle]:not([open])) .drawer {
4771
+ --m: 1;
4772
+ height: 20px;
4773
+ opacity: 1;
4774
+ transform: translateY(0);
4775
+ visibility: visible;
4776
+ transition:
4777
+ transform var(--transition),
4778
+ opacity var(--transition),
4779
+ left var(--transition),
4780
+ right var(--transition),
4781
+ height var(--transition);
4782
+ }
4783
+ :host([maintain-handle]:not([open])) ixfx-titlebar,
4784
+ :host([maintain-handle]:not([open])) .body {
4785
+ display: none;
4786
+ }
4787
+ :host([maintain-handle]:not([open])) .resize-edge {
4788
+ top: 0;
4789
+ height: 100%;
4790
+ }
4791
+ :host([maintain-handle]:not([open])) .resize-edge .ixfx-handle {
4792
+ top: 50%;
4793
+ transform: translate(-50%, -50%);
4794
+ }
4795
+ ixfx-titlebar {
4796
+ flex: none;
4797
+ --titlebar-background: var(--bottom-drawer-title-background, transparent);
4798
+ border-bottom: 1px solid color-mix(in srgb, var(--surface-2-text) 10%, transparent);
4799
+ }
4800
+ .body {
4801
+ flex: 1;
4802
+ min-height: 0;
4803
+ overflow: auto;
4804
+ overscroll-behavior: contain;
4805
+ }
4806
+ .body ::slotted(*) {
4807
+ box-sizing: border-box;
4808
+ }
4809
+ @media (prefers-reduced-motion: reduce) {
4810
+ .drawer,
4811
+ ixfx-titlebar,
4812
+ .body {
4813
+ transition-duration: 0s;
4814
+ }
4815
+ }
4816
+ `
4817
+ ];
4818
+ }
4819
+ #dragStartY;
4820
+ #dragStartHeight;
4821
+ #collapsedDragging;
4822
+ #ownedRegistry;
4823
+ #commandTarget;
4824
+ #commandName;
4825
+ #commandIds;
4826
+ connectedCallback() {
4827
+ super.connectedCallback();
4828
+ this.#syncCommands();
4829
+ }
4830
+ disconnectedCallback() {
4831
+ super.disconnectedCallback();
4832
+ this.#teardownCommands();
4833
+ if (this.#ownedRegistry !== void 0) {
4834
+ if (this.registry === this.#ownedRegistry) this.registry = void 0;
4835
+ this.#ownedRegistry = void 0;
4836
+ }
4837
+ }
4838
+ updated(changed) {
4839
+ super.updated(changed);
4840
+ if (changed.has(`name`) || changed.has(`registry`)) this.#syncCommands();
4841
+ }
4842
+ show() {
4843
+ this.open = true;
4844
+ }
4845
+ close() {
4846
+ if (!this.open) return;
4847
+ this.open = false;
4848
+ this.dispatchEvent(new CustomEvent(`drawer-close`, {
4849
+ bubbles: true,
4850
+ composed: true
4851
+ }));
4852
+ }
4853
+ toggle() {
4854
+ if (this.open) this.close();
4855
+ else this.show();
4856
+ }
4857
+ #syncCommands() {
4858
+ const name = this.name.trim();
4859
+ const target = this.registry ?? this.#ownedRegistry;
4860
+ if (name !== `` && this.isConnected && target !== void 0 && this.#commandTarget === target && this.#commandName === name) return;
4861
+ this.#teardownCommands();
4862
+ if (name === `` || !this.isConnected) return;
4863
+ const resolved = this.registry ?? (this.#ownedRegistry ??= createCommandRegistry());
4864
+ if (this.#ownedRegistry !== void 0 && resolved !== this.#ownedRegistry) this.#ownedRegistry = void 0;
4865
+ if (this.registry === void 0) this.registry = resolved;
4866
+ const registered = [];
4867
+ const commands = [
4868
+ {
4869
+ command: `expand`,
4870
+ label: `Expand ${this.label}`,
4871
+ description: `Open the ${this.label} drawer`,
4872
+ execute: () => this.show()
4873
+ },
4874
+ {
4875
+ command: `collapse`,
4876
+ label: `Collapse ${this.label}`,
4877
+ description: `Close the ${this.label} drawer`,
4878
+ execute: () => this.close()
4879
+ },
4880
+ {
4881
+ command: `toggle`,
4882
+ label: `Toggle ${this.label}`,
4883
+ description: `Open the ${this.label} drawer if closed, close it if open`,
4884
+ execute: () => this.toggle()
4885
+ }
4886
+ ];
4887
+ for (const { command, label, description, execute } of commands) {
4888
+ const id = bottomDrawerCommandId(name, command);
4889
+ if (resolved.has(id)) continue;
4890
+ resolved.register({
4891
+ id,
4892
+ label,
4893
+ description,
4894
+ execute
4895
+ });
4896
+ registered.push(id);
4897
+ }
4898
+ this.#commandTarget = resolved;
4899
+ this.#commandName = name;
4900
+ this.#commandIds = registered;
4901
+ }
4902
+ #teardownCommands() {
4903
+ if (this.#commandTarget !== void 0) {
4904
+ for (const id of this.#commandIds) this.#commandTarget.unregister(id);
4905
+ this.#commandTarget = void 0;
4906
+ this.#commandName = ``;
4907
+ this.#commandIds = [];
4908
+ }
4909
+ }
4910
+ _onTitlebarToggle(event) {
4911
+ event.stopPropagation();
4912
+ if (event.detail.collapsed) this.close();
4913
+ else this.open = true;
4914
+ }
4915
+ _onTitlebarClose(event) {
4916
+ event.stopPropagation();
4917
+ this.close();
4918
+ }
4919
+ render() {
4920
+ const raw = this.#displayHeight();
4921
+ const max = this.#maxHeight();
4922
+ const morph = this.#morphProgress(raw);
4923
+ const height = this.open ? raw * (1 - morph) + 20 * morph : 20;
4924
+ const opacity = this.maintainHandle && this.open ? 1 : this.#dragOpacity(raw);
4925
+ return b`
4926
+ <div class="content"><slot></slot></div>
4927
+ <section
4928
+ class="drawer"
4929
+ style=${`--bottom-drawer-height: ${height}px; --bottom-drawer-opacity: ${opacity}; --m: ${morph}`}
4930
+ role="region"
4931
+ aria-label=${this.label}
4932
+ @keydown=${this.#onDrawerKeyDown}
4933
+ >
4934
+ <div
4935
+ class="resize-edge"
4936
+ role="separator"
4937
+ tabindex="0"
4938
+ aria-label=${`${this.open ? `Resize` : `Expand`} ${this.label}`}
4939
+ aria-orientation="horizontal"
4940
+ aria-valuemin=${this.minHeight}
4941
+ aria-valuemax=${max}
4942
+ aria-valuenow=${Math.round(raw)}
4943
+ @pointerdown=${this.#onResizeStart}
4944
+ @keydown=${this.#onResizeKeyDown}
4945
+ ><span class="ixfx-handle" data-handle="dimpled" data-shape="horizontal" part="handle"></span></div>
4946
+ <ixfx-titlebar
4947
+ .title=${this.label}
4948
+ .iconName=${this.iconName}
4949
+ .closable=${this.closable}
4950
+ .collapsible=${this.collapsible}
4951
+ .collapsed=${!this.open}
4952
+ ?inert=${!this.open}
4953
+ @toggle=${this._onTitlebarToggle}
4954
+ @close=${this._onTitlebarClose}
4955
+ >
4956
+ <slot name="toolbar" slot="toolbar"></slot>
4957
+ </ixfx-titlebar>
4958
+ <div class="body" ?inert=${!this.open}><slot name="drawer"></slot></div>
4959
+ </section>
4960
+ `;
4961
+ }
4962
+ #maxHeight() {
4963
+ const availableHeight = this.clientHeight || this.height;
4964
+ return Math.max(this.minHeight, availableHeight * clampRange(this.maxHeightRatio, 0, 1));
4965
+ }
4966
+ #displayHeight() {
4967
+ return clampRange(this._dragHeight ?? this.height, 0, this.#maxHeight());
4968
+ }
4969
+ #dragOpacity(height) {
4970
+ if (this._dragHeight === void 0 || height >= this.minHeight) return 1;
4971
+ const fadeRange = this.minHeight - this.dismissThreshold;
4972
+ if (fadeRange <= 0) return height >= this.dismissThreshold ? 1 : 0;
4973
+ return clampRange((height - this.dismissThreshold) / fadeRange, 0, 1);
4974
+ }
4975
+ #morphProgress(raw) {
4976
+ if (!this.open) return this.maintainHandle ? 1 : 0;
4977
+ if (this._dragHeight === void 0 || !this.maintainHandle) return 0;
4978
+ const range = this.minHeight - this.dismissThreshold;
4979
+ if (range <= 0) return raw < this.dismissThreshold ? 1 : 0;
4980
+ return clampRange((this.minHeight - raw) / range, 0, 1);
4981
+ }
4982
+ #setHeight(height) {
4983
+ this.height = clampRange(height, this.minHeight, this.#maxHeight());
4984
+ this.dispatchEvent(new CustomEvent(`drawer-resize`, {
4985
+ detail: { height: this.height },
4986
+ bubbles: true,
4987
+ composed: true
4988
+ }));
4989
+ }
4990
+ #onResizeStart;
4991
+ #onResizeMove;
4992
+ #onResizeEnd;
4993
+ #onResizeKeyDown;
4994
+ #onTabPointerMove;
4995
+ #onTabPointerUp;
4996
+ #onDrawerKeyDown;
4997
+ };
4998
+ __decorate([property({
4999
+ type: Boolean,
5000
+ reflect: true
5001
+ })], BottomDrawerElement.prototype, "open", void 0);
5002
+ __decorate([property({ type: Number })], BottomDrawerElement.prototype, "height", void 0);
5003
+ __decorate([property({
5004
+ type: Number,
5005
+ attribute: `min-height`
5006
+ })], BottomDrawerElement.prototype, "minHeight", void 0);
5007
+ __decorate([property({
5008
+ type: Number,
5009
+ attribute: `dismiss-threshold`
5010
+ })], BottomDrawerElement.prototype, "dismissThreshold", void 0);
5011
+ __decorate([property({
5012
+ type: Number,
5013
+ attribute: `max-height-ratio`
5014
+ })], BottomDrawerElement.prototype, "maxHeightRatio", void 0);
5015
+ __decorate([property({ type: String })], BottomDrawerElement.prototype, "label", void 0);
5016
+ __decorate([property({
5017
+ type: String,
5018
+ attribute: `icon-name`
5019
+ })], BottomDrawerElement.prototype, "iconName", void 0);
5020
+ __decorate([property({ type: Boolean })], BottomDrawerElement.prototype, "closable", void 0);
5021
+ __decorate([property({ type: Boolean })], BottomDrawerElement.prototype, "collapsible", void 0);
5022
+ __decorate([property({
5023
+ type: Boolean,
5024
+ attribute: `maintain-handle`,
5025
+ reflect: true
5026
+ })], BottomDrawerElement.prototype, "maintainHandle", void 0);
5027
+ __decorate([property({ type: String })], BottomDrawerElement.prototype, "name", void 0);
5028
+ __decorate([property({ attribute: false })], BottomDrawerElement.prototype, "registry", void 0);
5029
+ __decorate([state()], BottomDrawerElement.prototype, "_dragHeight", void 0);
5030
+ BottomDrawerElement = __decorate([safeCustomElement(`ixfx-bottom-drawer`)], BottomDrawerElement);
5031
+ //#endregion
5032
+ //#region src/icons/defaults.ts
5033
+ /**
5034
+ * Chevron pointing down. Used for split-button dropdown toggle.
5035
+ * fill="currentColor" so it inherits CSS color.
5036
+ */
5037
+ const ICON_CHEVRON_DOWN = `<svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"/></svg>`;
5038
+ /**
5039
+ * Caret pointing right. Used for tree-basic expand indicator as a CSS mask.
5040
+ * fill="black" is correct for CSS mask-image use (shape only, color comes from background-color).
5041
+ */
5042
+ const ICON_CARET_RIGHT = `<svg viewBox="0 0 24 24" fill="black" xmlns="http://www.w3.org/2000/svg"><path d="M10 7l6 5-6 5V7z"/></svg>`;
5043
+ /**
5044
+ * Checkmark. Useful for menu-check-item or confirmation states.
5045
+ * fill="currentColor" so it inherits CSS color.
5046
+ */
5047
+ const ICON_CHECK = `<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z"/></svg>`;
5048
+ /**
5049
+ * Close / X icon. Useful for dialogs, notifications, dismissible elements.
5050
+ * fill="currentColor" so it inherits CSS color.
5051
+ */
5052
+ const ICON_CLOSE = `<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/></svg>`;
5053
+ /**
5054
+ * Plus / add icon. Pairs with `close` for add/remove actions.
5055
+ * fill="currentColor" so it inherits CSS color.
5056
+ */
5057
+ const ICON_ADD = `<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>`;
5058
+ /**
5059
+ * Error / warning icon. Useful for error notifications.
5060
+ * fill="currentColor" so it inherits CSS color.
5061
+ */
5062
+ const ICON_ERROR = `<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/></svg>`;
5063
+ /**
5064
+ * Info icon. Useful for info notifications.
5065
+ * fill="currentColor" so it inherits CSS color.
5066
+ */
5067
+ const ICON_INFO = `<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/></svg>`;
5068
+ /**
5069
+ * Warning icon. Useful for warning notifications.
5070
+ * fill="currentColor" so it inherits CSS color.
5071
+ */
5072
+ const ICON_WARNING = `<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/></svg>`;
5073
+ /**
5074
+ * Chevron pointing right. Used for split-button switcher toggle.
5075
+ * fill="currentColor" so it inherits CSS color.
5076
+ */
5077
+ const ICON_CHEVRON_RIGHT = `<svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"/></svg>`;
5078
+ /**
5079
+ * Folder with up arrow. Angled line style for navigating up one level.
5080
+ * fill="currentColor" so it inherits CSS color.
5081
+ */
5082
+ const ICON_FOLDER_UP = `<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 4L4 12h5v8h6v-8h5L12 4z"/></svg>`;
5083
+ /**
5084
+ * Folder. Useful for file explorers and sidebars.
5085
+ * fill="currentColor" so it inherits CSS color.
5086
+ */
5087
+ const ICON_FOLDER = `<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 1.99-.9 1.99-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg>`;
5088
+ /**
5089
+ * Document / file page. Useful for file lists and document tabs.
5090
+ * fill="currentColor" so it inherits CSS color.
5091
+ */
5092
+ const ICON_FILE = `<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"/></svg>`;
5093
+ /**
5094
+ * Magnifying glass. Useful for search panels and inputs.
5095
+ * fill="currentColor" so it inherits CSS color.
5096
+ */
5097
+ const ICON_SEARCH = `<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>`;
5098
+ function registerDefaults() {
5099
+ registerIcon(`chevron-down`, ICON_CHEVRON_DOWN);
5100
+ registerIcon(`chevron-right`, ICON_CHEVRON_RIGHT);
5101
+ registerIcon(`caret-right`, ICON_CARET_RIGHT);
5102
+ registerIcon(`check`, ICON_CHECK);
5103
+ registerIcon(`close`, ICON_CLOSE);
5104
+ registerIcon(`file`, ICON_FILE);
5105
+ registerIcon(`folder`, ICON_FOLDER);
5106
+ registerIcon(`search`, ICON_SEARCH);
5107
+ registerIcon(`add`, ICON_ADD);
5108
+ registerIcon(`error`, ICON_ERROR);
5109
+ registerIcon(`folder-up`, ICON_FOLDER_UP);
5110
+ registerIcon(`info`, ICON_INFO);
5111
+ registerIcon(`warning`, ICON_WARNING);
5112
+ }
5113
+ //#endregion
5114
+ //#region src/icons/index.ts
5115
+ registerDefaults();
5116
+ //#endregion
5117
+ //#region src/styles/prominence.ts
5118
+ /**
5119
+ * Shared CSS variable defaults for prominence-based interaction feedback.
5120
+ *
5121
+ * Defines the `--prominence-*` variables on `:host`. Include this if you
5122
+ * are building custom prominence selectors; otherwise use `prominenceStyles`
5123
+ * or `prominenceSubStyles` which include these variables automatically.
5124
+ */
5125
+ const prominenceVars = i$3`
5126
+ :host {
5127
+ --prominence-idle-opacity: 0.6;
5128
+ --prominence-idle-saturate: 0.4;
5129
+ --prominence-tickled-opacity: 0.85;
5130
+ --prominence-tickled-saturate: 0.8;
5131
+ --prominence-disabled-opacity: 0.3;
5132
+ --prominence-disabled-saturate: 0.1;
5133
+ }
5134
+ `;
5135
+ /**
5136
+ * Host-based prominence interaction feedback.
5137
+ *
5138
+ * Applies CSS `filter` to **the whole component host** to convey state:
5139
+ * - **Disabled** (`[disabled]` / `:disabled`): heavily muted
5140
+ * - **Idle** (default): reduced opacity + desaturated
5141
+ * - **Tickled** (`:hover` / `:focus-within`): elevated opacity + saturation
5142
+ * - **Active** (`:active` / `[dragging]`): full vibrancy (filter removed)
5143
+ *
5144
+ * Defaults are defined on `:host` and can be overridden per-instance.
5145
+ *
5146
+ * Usage:
5147
+ * ```ts
5148
+ * import { prominenceStyles } from '../styles/prominence.js';
5149
+ *
5150
+ * class MyComponent extends LitElement {
5151
+ * static styles = [prominenceStyles, css`...`];
5152
+ * }
5153
+ * ```
5154
+ */
5155
+ const prominenceStyles = i$3`
5156
+ ${prominenceVars}
5157
+
5158
+ :host {
5159
+ filter:
5160
+ opacity(var(--prominence-idle-opacity))
5161
+ saturate(var(--prominence-idle-saturate));
5162
+ transition: filter var(--transition, 0.15s ease-out);
5163
+ }
5164
+
5165
+ :host(:hover),
5166
+ :host(:focus-within) {
5167
+ filter:
5168
+ opacity(var(--prominence-tickled-opacity))
5169
+ saturate(var(--prominence-tickled-saturate));
5170
+ }
5171
+
5172
+ :host(:active),
5173
+ :host([dragging]) {
5174
+ filter: none;
5175
+ }
5176
+
5177
+ /* Disabled comes last to override tickled/active selectors of equal specificity */
5178
+ :host([disabled]),
5179
+ :host(:disabled) {
5180
+ filter:
5181
+ opacity(var(--prominence-disabled-opacity))
5182
+ saturate(var(--prominence-disabled-saturate));
5183
+ pointer-events: none;
5184
+ }
5185
+ `;
5186
+ i$3`
5187
+ ${prominenceVars}
5188
+
5189
+ .prominence {
5190
+ filter:
5191
+ opacity(var(--prominence-idle-opacity))
5192
+ saturate(var(--prominence-idle-saturate));
5193
+ transition: filter var(--transition, 0.15s ease-out);
5194
+ }
5195
+
5196
+ .prominence:hover,
5197
+ .prominence:focus-within {
5198
+ filter:
5199
+ opacity(var(--prominence-tickled-opacity))
5200
+ saturate(var(--prominence-tickled-saturate));
5201
+ }
5202
+
5203
+ .prominence:active,
5204
+ .prominence.dragging {
5205
+ filter: none;
5206
+ }
5207
+
5208
+ .prominence[disabled],
5209
+ .prominence:disabled {
5210
+ filter:
5211
+ opacity(var(--prominence-disabled-opacity))
5212
+ saturate(var(--prominence-disabled-saturate));
5213
+ pointer-events: none;
5214
+ }
5215
+ `;
5216
+ //#endregion
5217
+ //#region src/button/button-styles.ts
5218
+ const buttonStyles = i$3`
5219
+ :host {
5220
+ display: inline-flex;
5221
+ max-width: 100%;
5222
+ vertical-align: middle;
5223
+ user-select: none;
5224
+ -webkit-user-select: none;
5225
+ }
5226
+
5227
+ :host([hidden]) {
5228
+ display: none;
5229
+ }
5230
+
5231
+ button {
5232
+ cursor: pointer;
5233
+ appearance: none;
5234
+ background: var(--surface-4, #ccc);
5235
+ color: var(--surface-3-text, #000);
5236
+ border: var(--border, 1px solid #999);
5237
+ outline: var(--ixfx-button-outline, none);
5238
+ border-radius: var(--radius-xl, 8px);
5239
+ corner-shape: superellipse(1);
5240
+ padding: var(--ixfx-button-padding-block, var(--space-m, 8px)) var(--ixfx-button-padding-inline, var(--space-l, 12px));
5241
+ font-family: var(--font-family, inherit);
5242
+ font-size: var(--text-m, 14px);
5243
+ margin: 0;
5244
+ display: inline-flex;
5245
+ align-items: center;
5246
+ justify-content: center;
5247
+ gap: 0.5em;
5248
+ width: 100%;
5249
+ overflow: hidden;
5250
+ white-space: nowrap;
5251
+ box-shadow:
5252
+ var(--ixfx-button-shade, none),
5253
+ inset 1px 1px 1px 1px color-mix(in oklch, var(--surface-4) 30%, transparent), inset -1px -1px 1px 1px color-mix(in oklch, var(--surface-2) 20%, transparent),
5254
+ 2px 2px 1px 0px color-mix(in oklch, var(--surface-2) 30%, transparent);
5255
+ transition: background var(--transition, 0.2s), border-color var(--transition, 0.2s);
5256
+ }
5257
+
5258
+ button:disabled {
5259
+ opacity: 0.5;
5260
+ cursor: not-allowed;
5261
+ }
5262
+
5263
+ button:hover:not(:disabled) {
5264
+ background: color-mix(in oklch, var(--surface-4) 80%, transparent);
5265
+ border-color: var(--accent, #666);
5266
+ box-shadow:
5267
+ inset 1px 1px 1px 1px color-mix(in oklch, var(--surface-5) 70%, transparent),
5268
+ inset -1px -1px 1px 1px color-mix(in oklch, var(--surface-2) 60%, transparent),
5269
+ 2px 2px 1px 0px color-mix(in oklch, var(--surface-2) 90%, transparent);
5270
+ }
5271
+
5272
+ button:active:not(:disabled) {
5273
+ background: var(--surface-2, #aaa);
5274
+ transform: translateY(1px);
5275
+ box-shadow: inset 1px 1px 0px 0.5px color-mix(in oklch,var(--surface-1),transparent);
5276
+ }
5277
+
5278
+ .button-icon {
5279
+ display: var(--ixfx-button-hide-icon, inline-flex);
5280
+ align-items: center;
5281
+ flex-shrink: 0;
5282
+ }
5283
+
5284
+ .button-label {
5285
+ white-space: nowrap;
5286
+ }
5287
+
5288
+ :host([has-icon]) .button-label {
5289
+ display: var(--ixfx-button-hide-label, inline);
5290
+ }
5291
+
5292
+ :host([hide-label]) .button-label {
5293
+ display: none;
5294
+ }
5295
+
5296
+ :host([hide-icon]) .button-icon {
5297
+ display: none;
5298
+ }
5299
+
5300
+ :host([subtle]) button {
5301
+ background: color-mix(in oklch, var(--surface-4, #ccc) 40%, transparent);
5302
+ }
5303
+
5304
+ :host([subtle]) button:hover:not(:disabled),
5305
+ :host([subtle]) button:focus-visible:not(:disabled) {
5306
+ background: color-mix(in oklch, var(--surface-4, #ccc) 70%, transparent);
5307
+ }
5308
+ `;
5309
+ //#endregion
5310
+ //#region src/button/button.ts
5311
+ let ButtonElement = class ButtonElement extends i {
5312
+ constructor(..._args) {
5313
+ super(..._args);
5314
+ this.disabled = false;
5315
+ this.hideIcon = false;
5316
+ this.hideLabel = false;
5317
+ this.hasIcon = false;
5318
+ this.subtle = false;
5319
+ }
5320
+ _renderIcon() {
5321
+ if (this.hideIcon || !this.icon) return A;
5322
+ if (this.icon.startsWith(`<svg`)) return b`<span class="button-icon">${unsafeHTML(this.icon)}</span>`;
5323
+ if (!hasIcon(this.icon)) return A;
5324
+ return b`<ixfx-icon class="button-icon" name="${this.icon}"></ixfx-icon>`;
5325
+ }
5326
+ render() {
5327
+ this.hasIcon = !this.hideIcon && !!this.icon;
5328
+ const label = this.hideLabel ? A : b`<span class="button-label"><slot></slot></span>`;
5329
+ return b`<button ?disabled=${this.disabled}>${this._renderIcon()}${label}</button>`;
5330
+ }
5331
+ static {
5332
+ this.styles = [prominenceStyles, buttonStyles];
5333
+ }
5334
+ };
5335
+ __decorate([property({ type: Boolean })], ButtonElement.prototype, "disabled", void 0);
5336
+ __decorate([property({ type: String })], ButtonElement.prototype, "icon", void 0);
5337
+ __decorate([property({
5338
+ type: Boolean,
5339
+ attribute: `hide-icon`,
5340
+ reflect: true
5341
+ })], ButtonElement.prototype, "hideIcon", void 0);
5342
+ __decorate([property({
5343
+ type: Boolean,
5344
+ attribute: `hide-label`,
5345
+ reflect: true
5346
+ })], ButtonElement.prototype, "hideLabel", void 0);
5347
+ __decorate([property({
5348
+ type: Boolean,
5349
+ attribute: `has-icon`,
5350
+ reflect: true
5351
+ })], ButtonElement.prototype, "hasIcon", void 0);
5352
+ __decorate([property({
5353
+ type: Boolean,
5354
+ reflect: true
5355
+ })], ButtonElement.prototype, "subtle", void 0);
5356
+ ButtonElement = __decorate([safeCustomElement(`ixfx-button`)], ButtonElement);
5357
+ //#endregion
5358
+ //#region src/button/radio-group.ts
5359
+ let RadioGroup = class RadioGroup extends i {
5360
+ constructor(..._args) {
5361
+ super(..._args);
5362
+ this.value = ``;
5363
+ this.orientation = `horizontal`;
5364
+ this.items = [];
5365
+ this.name = ``;
5366
+ this.mode = `compact`;
5367
+ this.checked = false;
5368
+ this.valueOff = ``;
5369
+ this.iconName = ``;
5370
+ this.iconNameOff = ``;
5371
+ this.hideLabels = false;
5372
+ this._computedItems = [];
5373
+ this._icons = /* @__PURE__ */ new Map();
5374
+ }
5375
+ get _isSingleToggle() {
5376
+ return this._computedItems.length === 0 && Boolean(this.textContent?.trim());
5377
+ }
5378
+ get _singleToggleIconName() {
5379
+ if (!this.iconName && !this.iconNameOff) return null;
5380
+ return this.checked ? this.iconName : this.iconNameOff || this.iconName;
5381
+ }
5382
+ firstUpdated() {
5383
+ this.#updateComputedItems();
5384
+ }
5385
+ updated(changedProperties) {
5386
+ if (changedProperties.has(`checked`)) {
5387
+ const input = this.shadowRoot?.querySelector(`.radio-toggle-input`);
5388
+ if (input && input.checked !== this.checked) {
5389
+ input.checked = this.checked;
5390
+ const newValue = this.checked ? this.value || `on` : this.valueOff || void 0;
5391
+ this.dispatchEvent(new CustomEvent(`change`, {
5392
+ detail: {
5393
+ checked: this.checked,
5394
+ value: newValue
5395
+ },
5396
+ bubbles: true,
5397
+ composed: true
5398
+ }));
5399
+ }
5400
+ }
5401
+ if (changedProperties.has(`value`) && !this._isSingleToggle) {
5402
+ const inputs = this.shadowRoot?.querySelectorAll(`.radio-input`);
5403
+ if (inputs) {
5404
+ for (const input of inputs) if (input.checked !== (input.value === this.value)) input.checked = input.value === this.value;
5405
+ }
5406
+ }
5407
+ }
5408
+ #updateComputedItems() {
5409
+ const options = this.querySelectorAll(`option`);
5410
+ const icons = /* @__PURE__ */ new Map();
5411
+ if (options.length > 0) {
5412
+ this._computedItems = Array.from(options, (el) => {
5413
+ const tempDiv = document.createElement(`div`);
5414
+ tempDiv.innerHTML = el.innerHTML;
5415
+ const iconEl = tempDiv.querySelector(`svg`);
5416
+ if (iconEl) icons.set(el.getAttribute(`value`) || ``, iconEl.outerHTML);
5417
+ return {
5418
+ value: el.getAttribute(`value`) || ``,
5419
+ label: el.textContent?.trim() || ``,
5420
+ disabled: el.hasAttribute(`disabled`),
5421
+ iconName: el.getAttribute(`icon-name`) || void 0,
5422
+ iconNameOff: el.getAttribute(`icon-name-off`) || void 0
5423
+ };
5424
+ });
5425
+ this._icons = icons;
5426
+ } else this._computedItems = [...this.items];
5427
+ }
5428
+ #handleRadioChange(e) {
5429
+ e.stopImmediatePropagation();
5430
+ const input = e.target;
5431
+ this.value = input.value;
5432
+ this.dispatchEvent(new CustomEvent(`change`, {
5433
+ detail: {
5434
+ checked: true,
5435
+ value: input.value
5436
+ },
5437
+ bubbles: true,
5438
+ composed: true
5439
+ }));
5440
+ }
5441
+ #handleToggleChange(e) {
5442
+ e.stopImmediatePropagation();
5443
+ const input = e.target;
5444
+ this.checked = input.checked;
5445
+ const newValue = this.checked ? this.value || `on` : this.valueOff || void 0;
5446
+ this.dispatchEvent(new CustomEvent(`change`, {
5447
+ detail: {
5448
+ checked: this.checked,
5449
+ value: newValue
5450
+ },
5451
+ bubbles: true,
5452
+ composed: true
5453
+ }));
5454
+ }
5455
+ #renderIcon(item) {
5456
+ const iconName = item.value === this.value ? item.iconName || null : item.iconNameOff || item.iconName || null;
5457
+ if (iconName) return b`<ixfx-icon class="radio-icon" name="${iconName}"></ixfx-icon>`;
5458
+ const iconHtml = this._icons.get(item.value);
5459
+ if (!iconHtml) return null;
5460
+ return b`<span class="radio-icon">${unsafeHTML(iconHtml)}</span>`;
5461
+ }
5462
+ render() {
5463
+ if (this._isSingleToggle) {
5464
+ const iconName = this._singleToggleIconName;
5465
+ const iconPart = iconName || this.hideLabels ? b`<ixfx-icon class="radio-icon" name="${iconName || `check`}"></ixfx-icon>` : null;
5466
+ const labelPart = !this.hideLabels ? b`<span class="radio-label" data-text="${this.textContent?.trim()}">${this.textContent?.trim()}</span>` : null;
5467
+ return b`
5468
+ <label class="radio-item ${this.checked ? `selected` : ``}">
5469
+ <input
5470
+ type="checkbox"
5471
+ class="radio-toggle-input"
5472
+ ?checked=${this.checked}
5473
+ @change=${this.#handleToggleChange}
5474
+ />
5475
+ ${iconPart}
5476
+ ${labelPart}
5477
+ </label>
5478
+ `;
5479
+ }
5480
+ return b`
5481
+ <slot @slotchange=${this.#updateComputedItems} style="display: none;"></slot>
5482
+ <div class="radio-group ${this.orientation} ${this.mode}" role="radiogroup" aria-label=${this.name}>
5483
+ ${this._computedItems.map((item) => {
5484
+ const iconName = item.value === this.value ? item.iconName || null : item.iconNameOff || item.iconName || null;
5485
+ const iconPart = iconName || this.hideLabels ? b`<ixfx-icon class="radio-icon" name="${iconName || `check`}"></ixfx-icon>` : null;
5486
+ const labelPart = !this.hideLabels ? b`<span class="radio-label" data-text="${item.label}">${item.label}</span>` : null;
5487
+ return b`
5488
+ <label class="radio-item ${item.value === this.value ? `selected` : ``}" ?disabled=${item.disabled}>
5489
+ <input
5490
+ type="radio"
5491
+ class="radio-input"
5492
+ name=${this.name}
5493
+ value=${item.value}
5494
+ ?disabled=${item.disabled}
5495
+ ?checked=${item.value === this.value}
5496
+ @change=${this.#handleRadioChange}
5497
+ />
5498
+ ${iconPart}
5499
+ ${labelPart}
5500
+ </label>
5501
+ `;
5502
+ })}
5503
+ </div>
5504
+ `;
5505
+ }
5506
+ static {
5507
+ this.styles = [
5508
+ themeFallbacks,
5509
+ prominenceStyles,
5510
+ i$3`
5511
+ :host {
5512
+ display: inline-flex;
5513
+ min-width: min-content;
4378
5514
  box-sizing: border-box;
4379
5515
  /* Default to looking like a cohesive control group */
4380
5516
  background: var(--surface-4, transparent);
@@ -4559,6 +5695,306 @@ __decorate([state()], RadioGroup.prototype, "_icons", void 0);
4559
5695
  __decorate([query(`slot`)], RadioGroup.prototype, "_slot", void 0);
4560
5696
  RadioGroup = __decorate([safeCustomElement(`ixfx-radio-group`)], RadioGroup);
4561
5697
  //#endregion
5698
+ //#region src/interaction/list-drag-select-controller.ts
5699
+ const DEFAULT_POINTER_TYPES = [`mouse`, `pen`];
5700
+ const DEFAULT_THRESHOLD = 4;
5701
+ /** Pointer distance (px) from an edge that triggers auto-scroll. */
5702
+ const EDGE_ZONE = 28;
5703
+ /** Max auto-scroll speed in px per frame. */
5704
+ const MAX_SCROLL_SPEED = 16;
5705
+ /**
5706
+ * Shared pointer-drag (marquee / rubber-band) selection for list components.
5707
+ *
5708
+ * Owns all pointer plumbing and rectangle state; knows nothing about DOM
5709
+ * geometry or the selection model — those are host callbacks. Mirrors the
5710
+ * `TickledController` attach pattern (`new ListDragSelectController(this, …)`).
5711
+ *
5712
+ * Behaviour: `pointerdown` on the surface records a start point; once the
5713
+ * pointer moves past `threshold`, the surface captures the pointer and every
5714
+ * move recomputes the rect, hit-tests, and calls `onPreview`. On release the
5715
+ * final set is delivered once via `onCommit`. A press that never crosses the
5716
+ * threshold does nothing (the normal `@click` path runs).
5717
+ */
5718
+ var ListDragSelectController = class {
5719
+ #host;
5720
+ #config;
5721
+ #boundSurface;
5722
+ #pointerId;
5723
+ #startClientX = 0;
5724
+ #startClientY = 0;
5725
+ #startX = 0;
5726
+ #startY = 0;
5727
+ #currentClientX = 0;
5728
+ #currentClientY = 0;
5729
+ #lastModifiers = {
5730
+ shiftKey: false,
5731
+ ctrlKey: false,
5732
+ metaKey: false
5733
+ };
5734
+ #dragging = false;
5735
+ #suppressClick = false;
5736
+ /** Selection as it was when the drag started — the combine baseline. */
5737
+ #base = /* @__PURE__ */ new Set();
5738
+ /** Combine mode of the most recent preview (drives the commit flag). */
5739
+ #lastMode = `replace`;
5740
+ #preview = /* @__PURE__ */ new Set();
5741
+ #raf = 0;
5742
+ constructor(host, config) {
5743
+ this.#host = host;
5744
+ this.#config = config;
5745
+ host.addController(this);
5746
+ }
5747
+ /** `true` while a drag is in progress or its trailing click is still pending. */
5748
+ get isDragging() {
5749
+ return this.#dragging || this.#suppressClick;
5750
+ }
5751
+ /** `true` once the press has crossed the threshold and a rect is live. */
5752
+ get isActive() {
5753
+ return this.#dragging;
5754
+ }
5755
+ hostConnected() {
5756
+ this.#bindSurface();
5757
+ window.addEventListener(`pointermove`, this.#onPointerMove);
5758
+ window.addEventListener(`pointerup`, this.#onPointerUp);
5759
+ window.addEventListener(`pointercancel`, this.#onPointerCancel);
5760
+ }
5761
+ hostDisconnected() {
5762
+ this.#unbindSurface();
5763
+ window.removeEventListener(`pointermove`, this.#onPointerMove);
5764
+ window.removeEventListener(`pointerup`, this.#onPointerUp);
5765
+ window.removeEventListener(`pointercancel`, this.#onPointerCancel);
5766
+ this.#stopAutoScroll();
5767
+ this.#reset();
5768
+ }
5769
+ /** Re-bind when the surface element appears/changes (e.g. first render). */
5770
+ hostUpdated() {
5771
+ if ((this.#safeSurface() ?? this.#host) !== this.#boundSurface) this.#bindSurface();
5772
+ }
5773
+ #safeSurface() {
5774
+ try {
5775
+ return this.#config.getSurface();
5776
+ } catch {
5777
+ return;
5778
+ }
5779
+ }
5780
+ #bindSurface() {
5781
+ const next = this.#safeSurface() ?? this.#host;
5782
+ if (next === this.#boundSurface) return;
5783
+ this.#unbindSurface();
5784
+ this.#boundSurface = next;
5785
+ next.addEventListener(`pointerdown`, this.#onPointerDown);
5786
+ }
5787
+ #unbindSurface() {
5788
+ this.#boundSurface?.removeEventListener(`pointerdown`, this.#onPointerDown);
5789
+ this.#boundSurface = void 0;
5790
+ }
5791
+ #enabledTypes() {
5792
+ return this.#config.pointerTypes ?? DEFAULT_POINTER_TYPES;
5793
+ }
5794
+ #threshold() {
5795
+ return this.#config.threshold ?? DEFAULT_THRESHOLD;
5796
+ }
5797
+ #onPointerDown = (event) => {
5798
+ if (event.button !== 0) return;
5799
+ if (!this.#enabledTypes().includes(event.pointerType)) return;
5800
+ let enabled = false;
5801
+ try {
5802
+ enabled = this.#config.isEnabled();
5803
+ } catch {
5804
+ return;
5805
+ }
5806
+ if (!enabled) return;
5807
+ const target = event.target;
5808
+ if (target instanceof Element && typeof target.closest === `function`) {
5809
+ if (target.closest(`input,button,a,select,textarea,[contenteditable]`)) return;
5810
+ }
5811
+ if (this.#config.shouldStart) try {
5812
+ if (!this.#config.shouldStart(event)) return;
5813
+ } catch {
5814
+ return;
5815
+ }
5816
+ const box = (this.#safeSurface() ?? this.#host).getBoundingClientRect();
5817
+ this.#pointerId = event.pointerId;
5818
+ this.#startClientX = event.clientX;
5819
+ this.#startClientY = event.clientY;
5820
+ this.#currentClientX = event.clientX;
5821
+ this.#currentClientY = event.clientY;
5822
+ this.#startX = Math.min(Math.max(event.clientX - box.left, 0), Math.max(0, box.width));
5823
+ this.#startY = Math.min(Math.max(event.clientY - box.top, 0), Math.max(0, box.height));
5824
+ this.#dragging = false;
5825
+ this.#suppressClick = false;
5826
+ this.#base = /* @__PURE__ */ new Set();
5827
+ this.#lastMode = `replace`;
5828
+ this.#lastModifiers = {
5829
+ shiftKey: event.shiftKey,
5830
+ ctrlKey: event.ctrlKey,
5831
+ metaKey: event.metaKey
5832
+ };
5833
+ };
5834
+ /** Combine mode for the current modifiers: Shift adds, Cmd/Ctrl inverts. */
5835
+ #modeFor(modifiers) {
5836
+ if (modifiers.metaKey || modifiers.ctrlKey) return `invert`;
5837
+ if (modifiers.shiftKey) return `add`;
5838
+ return `replace`;
5839
+ }
5840
+ #onPointerMove = (event) => {
5841
+ if (this.#pointerId === void 0 || event.pointerId !== this.#pointerId) return;
5842
+ this.#currentClientX = event.clientX;
5843
+ this.#currentClientY = event.clientY;
5844
+ this.#lastModifiers = {
5845
+ shiftKey: event.shiftKey,
5846
+ ctrlKey: event.ctrlKey,
5847
+ metaKey: event.metaKey
5848
+ };
5849
+ const moved = Math.hypot(event.clientX - this.#startClientX, event.clientY - this.#startClientY);
5850
+ if (!this.#dragging) {
5851
+ if (moved < this.#threshold()) return;
5852
+ this.#dragging = true;
5853
+ try {
5854
+ this.#base = new Set(this.#config.getBaseSelection());
5855
+ } catch {
5856
+ this.#base = /* @__PURE__ */ new Set();
5857
+ }
5858
+ const surface = this.#safeSurface() ?? this.#host;
5859
+ try {
5860
+ if (typeof surface.setPointerCapture === `function`) surface.setPointerCapture(event.pointerId);
5861
+ } catch {}
5862
+ }
5863
+ try {
5864
+ event.preventDefault?.();
5865
+ } catch {}
5866
+ this.#updatePreview();
5867
+ this.#kickAutoScroll();
5868
+ };
5869
+ #surfaceBox() {
5870
+ const surface = this.#safeSurface() ?? this.#host;
5871
+ try {
5872
+ return surface.getBoundingClientRect();
5873
+ } catch {
5874
+ return;
5875
+ }
5876
+ }
5877
+ #currentRect() {
5878
+ const box = this.#surfaceBox();
5879
+ if (!box) return void 0;
5880
+ const cx = Math.min(Math.max(this.#currentClientX - box.left, 0), Math.max(0, box.width));
5881
+ const cy = Math.min(Math.max(this.#currentClientY - box.top, 0), Math.max(0, box.height));
5882
+ return {
5883
+ x: Math.min(this.#startX, cx),
5884
+ y: Math.min(this.#startY, cy),
5885
+ width: Math.abs(cx - this.#startX),
5886
+ height: Math.abs(cy - this.#startY)
5887
+ };
5888
+ }
5889
+ #updatePreview() {
5890
+ const rect = this.#currentRect();
5891
+ if (!rect) return;
5892
+ let hit = [];
5893
+ try {
5894
+ hit = this.#config.hitTest(rect);
5895
+ } catch {
5896
+ hit = [];
5897
+ }
5898
+ const mode = this.#modeFor(this.#lastModifiers);
5899
+ this.#lastMode = mode;
5900
+ const hitSet = new Set(hit);
5901
+ let preview;
5902
+ if (mode === `add`) preview = /* @__PURE__ */ new Set([...this.#base, ...hitSet]);
5903
+ else if (mode === `invert`) {
5904
+ const toggled = new Set(this.#base);
5905
+ for (const item of hitSet) if (toggled.has(item)) toggled.delete(item);
5906
+ else toggled.add(item);
5907
+ preview = toggled;
5908
+ } else preview = hitSet;
5909
+ this.#preview = preview;
5910
+ try {
5911
+ this.#config.onPreview(preview, rect);
5912
+ } catch {}
5913
+ }
5914
+ #onPointerUp = (event) => {
5915
+ if (this.#pointerId === void 0 || event.pointerId !== this.#pointerId) return;
5916
+ this.#lastModifiers = {
5917
+ shiftKey: event.shiftKey,
5918
+ ctrlKey: event.ctrlKey,
5919
+ metaKey: event.metaKey
5920
+ };
5921
+ const wasDragging = this.#dragging;
5922
+ const surface = this.#safeSurface() ?? this.#host;
5923
+ try {
5924
+ if (typeof surface.hasPointerCapture === `function` && typeof surface.releasePointerCapture === `function` && surface.hasPointerCapture(event.pointerId)) surface.releasePointerCapture(event.pointerId);
5925
+ } catch {}
5926
+ this.#stopAutoScroll();
5927
+ this.#pointerId = void 0;
5928
+ if (!wasDragging) {
5929
+ this.#reset();
5930
+ return;
5931
+ }
5932
+ this.#dragging = false;
5933
+ const final = this.#preview;
5934
+ try {
5935
+ this.#config.onCommit(final, { additive: this.#lastMode !== `replace` });
5936
+ } catch {}
5937
+ try {
5938
+ this.#config.onPreview(/* @__PURE__ */ new Set(), void 0);
5939
+ } catch {}
5940
+ this.#preview = /* @__PURE__ */ new Set();
5941
+ this.#suppressClick = true;
5942
+ window.setTimeout(() => {
5943
+ this.#suppressClick = false;
5944
+ }, 0);
5945
+ };
5946
+ #onPointerCancel = (event) => {
5947
+ if (this.#pointerId === void 0 || event.pointerId !== this.#pointerId) return;
5948
+ this.#stopAutoScroll();
5949
+ this.#pointerId = void 0;
5950
+ this.#dragging = false;
5951
+ this.#preview = /* @__PURE__ */ new Set();
5952
+ try {
5953
+ this.#config.onPreview(/* @__PURE__ */ new Set(), void 0);
5954
+ } catch {}
5955
+ this.#suppressClick = false;
5956
+ };
5957
+ #kickAutoScroll() {
5958
+ if (!this.#config.autoScroll || this.#raf !== 0) return;
5959
+ this.#raf = requestAnimationFrame(this.#autoScrollTick);
5960
+ }
5961
+ #autoScrollTick = () => {
5962
+ this.#raf = 0;
5963
+ if (!this.#dragging || this.#pointerId === void 0) return;
5964
+ const box = this.#surfaceBox();
5965
+ if (!box) return;
5966
+ const px = this.#currentClientX - box.left;
5967
+ const py = this.#currentClientY - box.top;
5968
+ let dx = 0;
5969
+ let dy = 0;
5970
+ if (px < EDGE_ZONE) dx = -Math.ceil(MAX_SCROLL_SPEED * (1 - Math.max(0, px) / EDGE_ZONE));
5971
+ else if (px > box.width - EDGE_ZONE) dx = Math.ceil(MAX_SCROLL_SPEED * (1 - Math.max(0, box.width - px) / EDGE_ZONE));
5972
+ if (py < EDGE_ZONE) dy = -Math.ceil(MAX_SCROLL_SPEED * (1 - Math.max(0, py) / EDGE_ZONE));
5973
+ else if (py > box.height - EDGE_ZONE) dy = Math.ceil(MAX_SCROLL_SPEED * (1 - Math.max(0, box.height - py) / EDGE_ZONE));
5974
+ if (dx === 0 && dy === 0) return;
5975
+ const autoScroll = this.#config.autoScroll;
5976
+ if (!autoScroll) return;
5977
+ try {
5978
+ autoScroll(dx, dy);
5979
+ } catch {}
5980
+ this.#updatePreview();
5981
+ this.#raf = requestAnimationFrame(this.#autoScrollTick);
5982
+ };
5983
+ #stopAutoScroll() {
5984
+ if (this.#raf !== 0) {
5985
+ cancelAnimationFrame(this.#raf);
5986
+ this.#raf = 0;
5987
+ }
5988
+ }
5989
+ #reset() {
5990
+ this.#dragging = false;
5991
+ this.#suppressClick = false;
5992
+ this.#base = /* @__PURE__ */ new Set();
5993
+ this.#lastMode = `replace`;
5994
+ this.#preview = /* @__PURE__ */ new Set();
5995
+ }
5996
+ };
5997
+ //#endregion
4562
5998
  //#region src/interaction/list-item-animation.ts
4563
5999
  /**
4564
6000
  * Shared insertion / removal animation for the list & tree components.
@@ -4797,6 +6233,31 @@ var ListSelectionController = class {
4797
6233
  this.#anchor = [...next].at(-1);
4798
6234
  this.#apply(next);
4799
6235
  }
6236
+ /**
6237
+ * Whether a drag / marquee gesture may build a selection in the current
6238
+ * modes: `selectionMode="multiple"`, or a multi interaction mode
6239
+ * (`checked` / `sticky` / `vscode`) outside `selectionMode="none"`.
6240
+ * Stricter than `canSelectMultiple()` — `single` + `standard` does not
6241
+ * qualify (a drag there would be a surprising multi-select).
6242
+ */
6243
+ canDragSelect() {
6244
+ return this.#config.getModes().selectionMode !== `none` && this.#effectiveMulti();
6245
+ }
6246
+ /**
6247
+ * Replace the selection with `items` from a drag / marquee gesture. Like
6248
+ * `selectMany` but gated on `canDragSelect()` instead of throwing, and a
6249
+ * no-op in single/none modes. Non-selectable items are dropped; the anchor
6250
+ * moves to the last item. Fires `onChange` once. An empty result clears a
6251
+ * non-empty selection (e.g. a Cmd-drag that toggles everything off) but is
6252
+ * a no-op when nothing is selected.
6253
+ */
6254
+ applyDragSelection(items) {
6255
+ if (!this.canDragSelect()) return;
6256
+ const next = new Set([...items].filter((t) => this.#config.isSelectable(t)));
6257
+ if (next.size === 0 && this.#selected.size === 0) return;
6258
+ this.#anchor = [...next].at(-1);
6259
+ this.#apply(next);
6260
+ }
4800
6261
  /** Remove a single item from the selection. */
4801
6262
  deselect(item) {
4802
6263
  if (!this.#selected.has(item)) return;
@@ -11889,6 +13350,7 @@ let DataGridElement = class DataGridElement extends i {
11889
13350
  static {
11890
13351
  this.styles = [
11891
13352
  themeFallbacks,
13353
+ handleStyles,
11892
13354
  tickledStyles,
11893
13355
  tickledItemStyles,
11894
13356
  listItemFadeKeyframes,
@@ -11932,7 +13394,8 @@ let DataGridElement = class DataGridElement extends i {
11932
13394
  .header-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
11933
13395
  .sort-icon { font-size: 10px; opacity: 0.7; }
11934
13396
  .resize-handle { position: absolute; right: 0; top: 0; bottom: 0; width: 8px; cursor: col-resize; z-index: 1; }
11935
- .reorder-handle { cursor: grab; opacity: 0.5; padding: 0 2px; }
13397
+ .reorder-handle { cursor: grab; opacity: 0.5; padding: 0 2px; display: inline-flex; align-items: center; }
13398
+ .reorder-handle .ixfx-handle { --ixfx-handle-spot-size: 14px; --ixfx-handle-dot-size: 3px; }
11936
13399
  .flyout-trigger { flex-shrink: 0; width: 36px; display: flex; align-items: center; justify-content: center; cursor: pointer; border-left: var(--border-subtle); background: var(--data-grid-header-bg); }
11937
13400
  .viewport { flex: 1; overflow: auto; position: relative; }
11938
13401
  .spacer { position: relative; }
@@ -12838,7 +14301,7 @@ let DataGridElement = class DataGridElement extends i {
12838
14301
  @pointerenter=${() => this._tickled.handlePointerEnter(col.key)}>
12839
14302
  ${col.reorderable !== false ? b`<span class="reorder-handle" draggable="false"
12840
14303
  @pointerdown=${(e) => this._beginHeaderDrag(e, col, idx)}
12841
- >⠿</span>` : A}
14304
+ ><span class="ixfx-handle" data-handle="dotted" data-shape="spot" part="reorder-handle"></span></span>` : A}
12842
14305
  <span class="header-label">${col.label}</span>
12843
14306
  ${this.sortState?.key === col.key ? b`<span class="sort-icon">${this.sortState.direction === `asc` ? `▲` : `▼`}</span>` : A}
12844
14307
  ${col.resizable !== false ? b`<span class="resize-handle" @pointerdown=${(e) => this._onResizePointerDown(e, col)} @click=${(e) => e.stopPropagation()} @dblclick=${(e) => {
@@ -13426,7 +14889,28 @@ let DetailListElement = class DetailListElement extends i {
13426
14889
  if (id !== void 0) this._dispatch(`list-tickle`, { item: id });
13427
14890
  },
13428
14891
  onStateChange: () => this._syncItemStates(),
13429
- itemWrap: this.itemWrap
14892
+ itemWrap: this.itemWrap,
14893
+ hoverDisabled: () => this.#dragSelect.isDragging
14894
+ });
14895
+ this.#dragSelect = new ListDragSelectController(this, {
14896
+ getSurface: () => this.renderRoot.querySelector(`.container`),
14897
+ isEnabled: () => this.selection.canDragSelect() && this._getVisibleItems().length > 0 && !this.hasAttribute(`disable-drag-select`),
14898
+ hitTest: (rect) => this.#marqueeHitTest(rect),
14899
+ getBaseSelection: () => this.selection.selected,
14900
+ onPreview: (preview, rect) => {
14901
+ this.#marqueeRect = rect;
14902
+ for (const el of this._getVisibleItems()) el.toggleAttribute(`data-marquee`, preview.has(el));
14903
+ this.requestUpdate();
14904
+ },
14905
+ onCommit: (finalSet) => {
14906
+ this.selection.applyDragSelection(finalSet);
14907
+ for (const el of this._getVisibleItems()) el.removeAttribute(`data-marquee`);
14908
+ this.#marqueeRect = void 0;
14909
+ this.requestUpdate();
14910
+ },
14911
+ autoScroll: (dx, _dy) => {
14912
+ if (this._listEl) this._listEl.scrollLeft += dx;
14913
+ }
13430
14914
  });
13431
14915
  this.search = new ListSearchController(this, { adapter: createElementListSearchAdapter(this, {
13432
14916
  itemSelector: ITEM_SELECTOR$3,
@@ -13503,6 +14987,7 @@ let DetailListElement = class DetailListElement extends i {
13503
14987
  }
13504
14988
  };
13505
14989
  this._onClick = (e) => {
14990
+ if (this.#dragSelect.isDragging) return;
13506
14991
  const item = this._findListItem(e.composedPath());
13507
14992
  if (!item) return;
13508
14993
  this.focus();
@@ -13529,6 +15014,30 @@ let DetailListElement = class DetailListElement extends i {
13529
15014
  #builtFromProvider;
13530
15015
  #providerBuilt;
13531
15016
  #data;
15017
+ /** Live drag-select preview rect (surface-relative); the covered items carry `data-marquee`. */
15018
+ #marqueeRect;
15019
+ #dragSelect;
15020
+ /** Collect visible items whose surface-relative box intersects `rect`. */
15021
+ #marqueeHitTest(rect) {
15022
+ const surface = this.renderRoot.querySelector(`.container`);
15023
+ if (!surface) return [];
15024
+ const box = surface.getBoundingClientRect();
15025
+ const rx0 = rect.x;
15026
+ const ry0 = rect.y;
15027
+ const rx1 = rect.x + rect.width;
15028
+ const ry1 = rect.y + rect.height;
15029
+ const out = [];
15030
+ for (const el of this._getVisibleItems()) {
15031
+ const r = el.getBoundingClientRect();
15032
+ const left = r.left - box.left;
15033
+ const top = r.top - box.top;
15034
+ if (left <= rx1 && left + r.width >= rx0 && top <= ry1 && top + r.height >= ry0) out.push(el);
15035
+ }
15036
+ return out;
15037
+ }
15038
+ #marqueeStyle(rect) {
15039
+ return `left:${rect.x}px;top:${rect.y}px;width:${Math.max(0, rect.width)}px;height:${Math.max(0, rect.height)}px`;
15040
+ }
13532
15041
  #searchRegistry;
13533
15042
  #unregisterSearchCommands;
13534
15043
  /** Command registry carrying this list's `list.search.*` commands. */
@@ -14301,6 +15810,22 @@ let DetailListElement = class DetailListElement extends i {
14301
15810
  outline-offset: -2px;
14302
15811
  }
14303
15812
 
15813
+ /* Drag-select (marquee) rectangle + covered-row preview.
15814
+ Style hooks: part="marquee", --detail-list-marquee-border,
15815
+ --detail-list-marquee-fill. */
15816
+ .marquee {
15817
+ position: absolute;
15818
+ pointer-events: none;
15819
+ z-index: 5;
15820
+ border: 1px solid var(--detail-list-marquee-border, var(--accent));
15821
+ background: var(--detail-list-marquee-fill, color-mix(in srgb, var(--accent) 15%, transparent));
15822
+ }
15823
+
15824
+ ::slotted([data-marquee]) {
15825
+ background: var(--item-bg-selected);
15826
+ color: var(--item-text-selected);
15827
+ }
15828
+
14304
15829
  /* checked mode: the injected checkbox communicates selection — suppress bg */
14305
15830
  :host([interaction-mode="checked"]) ::slotted([data-selected]) {
14306
15831
  background: transparent;
@@ -14341,6 +15866,7 @@ let DetailListElement = class DetailListElement extends i {
14341
15866
  <ul part="list" class="scroll-fade-x">
14342
15867
  <slot></slot>
14343
15868
  </ul>
15869
+ ${this.#marqueeRect ? b`<div class="marquee" part="marquee" style=${this.#marqueeStyle(this.#marqueeRect)}></div>` : ``}
14344
15870
  <div class="loading" part="loading" ?hidden=${!loading}>
14345
15871
  ${loading ? progress !== void 0 ? `${Math.round(progress * 100)}%` : `loading…` : A}
14346
15872
  </div>
@@ -15994,16 +17520,20 @@ let SplitLayoutElement = class SplitLayoutElement extends i {
15994
17520
  style="cursor: ${cursor}; grid-column: ${gridColumn}; grid-row: ${gridRow};"
15995
17521
  @pointerdown=${(e) => this._onGutterPointerDown(e, groupId, index)}
15996
17522
  >
15997
- <div class="grab-handle">
15998
- <span class="dot"></span>
15999
- <span class="dot"></span>
16000
- <span class="dot"></span>
16001
- </div>
17523
+ <span
17524
+ class="ixfx-handle"
17525
+ part="grip"
17526
+ data-handle="dimpled"
17527
+ data-shape=${isRow ? `vertical` : `horizontal`}
17528
+ ></span>
16002
17529
  </div>
16003
17530
  `;
16004
17531
  }
16005
17532
  static {
16006
- this.styles = [themeFallbacks, i$3`
17533
+ this.styles = [
17534
+ themeFallbacks,
17535
+ handleStyles,
17536
+ i$3`
16007
17537
  :host {
16008
17538
  display: block;
16009
17539
  width: 100%;
@@ -16014,10 +17544,10 @@ let SplitLayoutElement = class SplitLayoutElement extends i {
16014
17544
  --split-gutter-active-color: var(--accent);
16015
17545
  --split-collapsed-size: 0px;
16016
17546
  --split-snap: 24px;
16017
- --split-grab-color: var(--border-subtle-color);
16018
- --split-grab-dot-size: 3px;
16019
- --split-grab-size: 16px;
16020
- --split-grab-thickness: 8px;
17547
+
17548
+ /* Gutter grip — shared handle primitive (docs/infra-handle.md). */
17549
+ --ixfx-handle-length: 16px;
17550
+ --ixfx-handle-thickness: 8px;
16021
17551
  }
16022
17552
 
16023
17553
  * {
@@ -16079,31 +17609,15 @@ let SplitLayoutElement = class SplitLayoutElement extends i {
16079
17609
  user-select: none;
16080
17610
  }
16081
17611
 
16082
- .grab-handle .dot {
16083
- width: var(--split-grab-dot-size);
16084
- height: var(--split-grab-dot-size);
16085
- border-radius: 50%;
16086
- background: var(--split-grab-color);
16087
- }
16088
-
16089
- :host([direction="row"]) .grab-handle {
16090
- flex-direction: column;
16091
- gap: var(--split-grab-dot-size);
16092
- }
16093
-
16094
- :host([direction="column"]) .grab-handle {
16095
- flex-direction: row;
16096
- gap: var(--split-grab-dot-size);
16097
- }
16098
-
16099
- :host([data-dragging]) .grab-handle {
17612
+ :host([data-dragging]) .gutter .ixfx-handle {
16100
17613
  display: none;
16101
17614
  }
16102
17615
 
16103
- :host(:not([data-dragging])) .gutter:hover .grab-handle {
16104
- display: flex;
17616
+ :host(:not([data-dragging])) .gutter:hover .ixfx-handle {
17617
+ display: none;
16105
17618
  }
16106
- `];
17619
+ `
17620
+ ];
16107
17621
  }
16108
17622
  };
16109
17623
  __decorate([property({
@@ -16817,636 +18331,238 @@ let TabListItemElement = class TabListItemElement extends i {
16817
18331
  /**
16818
18332
  * Mirror the default-slot text into `.label[data-label]` for the `::after`
16819
18333
  * width sizer. Slotted toolbar content is excluded.
16820
- */
16821
- _syncLabelSizer() {
16822
- const slot = this.shadowRoot?.querySelector(`slot:not([name])`);
16823
- const label = this.shadowRoot?.querySelector(`.label`);
16824
- if (slot === null || slot === void 0 || label === null || label === void 0) return;
16825
- const text = slot.assignedNodes().filter((node) => !(node instanceof HTMLElement && node.hasAttribute(`slot`))).map((node) => node.textContent ?? ``).join(``);
16826
- if (text) label.setAttribute(`data-label`, text);
16827
- else label.removeAttribute(`data-label`);
16828
- }
16829
- _updateToolbarContent() {
16830
- const slot = this.shadowRoot?.querySelector(`slot[name="toolbar"]`);
16831
- this._hasToolbarContent = (slot?.assignedElements().length ?? 0) > 0;
16832
- }
16833
- firstUpdated() {
16834
- this._updateToolbarContent();
16835
- this._syncLabelSizer();
16836
- }
16837
- render() {
16838
- const showLabel = !this.hideLabels || !this.iconName;
16839
- const hasActions = this.closeable || this._hasToolbarContent;
16840
- return b`
16841
- <div>
16842
- ${this.iconName ? b`<ixfx-icon name="${this.iconName}"></ixfx-icon>` : A}
16843
- ${showLabel ? b`<span class="label"><slot></slot></span>` : A}
16844
- ${hasActions ? b`
16845
- <div class="actions">
16846
- <div class="toolbar"><slot name="toolbar"></slot></div>
16847
- ${this.closeable ? b`
16848
- <button
16849
- class="close-btn"
16850
- type="button"
16851
- aria-label="Close tab"
16852
- @click=${this._onCloseClick}
16853
- >
16854
- <ixfx-icon name="close"></ixfx-icon>
16855
- </button>
16856
- ` : A}
16857
- </div>
16858
- ` : A}
16859
- </div>
16860
- `;
16861
- }
16862
- _onCloseClick(e) {
16863
- e.stopPropagation();
16864
- this.dispatchEvent(new CustomEvent(`close`, {
16865
- bubbles: true,
16866
- composed: true
16867
- }));
16868
- this.remove();
16869
- }
16870
- static {
16871
- this.styles = [
16872
- themeFallbacks,
16873
- prominenceStyles,
16874
- i$3`
16875
- :host {
16876
- --selected-tab-color: var(--ixfx-selected-tab-color, var(--surface-2-text));
16877
- --selected-tab-icon-color: var(--ixfx-selected-tab-icon-color, var(--accent));
16878
- --selected-tab-border-width: var(--ixfx-selected-tab-border-width, 1px);
16879
- --selected-tab-weight: var(--ixfx-selected-tab-weight, bold);
16880
- --selected-tab-text-decoration: var(--ixfx-selected-tab-text-decoration, none);
16881
- --selected-tab-text-decoration-color: var(--ixfx-selected-tab-text-decoration-color, var(--accent));
16882
- --selected-tab-text-decoration-thickness: var(--ixfx-selected-tab-text-decoration-thickness, 0px);
16883
- --selected-tab-text-decoration-offset: var(--ixfx-selected-tab-text-decoration-offset, 2px);
16884
- --selected-background: var(--ixfx-tab-selected-background, var(--surface-5));
16885
- --tab-padding: var(--ixfx-tab-padding, var(--space-s));
16886
- display: inline-flex;
16887
- min-width: 0;
16888
- user-select: none;
16889
- box-sizing: border-box;
16890
- font-size: var(--text-m);
16891
- color: inherit;
16892
- /* No explicit height: items stretch (align-self: stretch via the
16893
- list's flex container) so every tab in a strip shares one height
16894
- even when toolbars give some items taller content. */
16895
- }
16896
-
16897
- :host > div {
16898
- display: flex;
16899
- align-items: center;
16900
- padding: var(--tab-padding) calc(2 * var(--tab-padding-horizontal, var(--tab-padding)));
16901
- min-width: 0;
16902
- width: 100%;
16903
- font-weight: 500;
16904
- border: var(--selected-tab-border-width) solid transparent;
16905
- gap: var(--space-s);
16906
- transition: background var(--transition, 0.15s);
16907
- }
16908
-
16909
- /* Hover ("tickle") background. When --ixfx-tab-hover-background is
16910
- unset each state keeps its resting background, so existing
16911
- consumers see no change. */
16912
- :host(:hover:not([selected])) > div {
16913
- background: var(--ixfx-tab-hover-background, transparent);
16914
- }
16915
-
16916
- :host([selected]:hover) > div {
16917
- background: var(--ixfx-tab-hover-background, var(--selected-background));
16918
- }
16919
-
16920
- :host([selected]) > div {
16921
- color: var(--selected-tab-color);
16922
- border-top-color: var(--indicator-color-top, transparent);
16923
- border-bottom-color: var(--indicator-color-bottom, transparent);
16924
- border-left-color: var(--indicator-color-left, transparent);
16925
- border-right-color: var(--indicator-color-right, transparent);
16926
- background: var(--selected-background);
16927
- font-weight: var(--selected-tab-weight);
16928
- border-radius:var(--radius-m);
16929
- }
16930
-
16931
- :host([selected]) .label {
16932
- text-decoration: var(--selected-tab-text-decoration);
16933
- text-decoration-color: var(--selected-tab-text-decoration-color);
16934
- text-decoration-thickness: var(--selected-tab-text-decoration-thickness);
16935
- text-decoration-offset: var(--selected-tab-text-decoration-offset);
16936
- }
16937
-
16938
- .label {
16939
- overflow: var(--allow-overflow, hidden);
16940
- text-overflow: ellipsis;
16941
- white-space: nowrap;
16942
- min-width: 0;
16943
- flex: 1 1 auto;
16944
- }
16945
-
16946
- /* Width sizer: an invisible copy of the label, always set at the
16947
- selected weight, so activating a tab (bold) never shifts the strip.
16948
- Text is mirrored from the default slot via the data-label attribute
16949
- (see _syncLabelSizer). */
16950
- .label::after {
16951
- content: attr(data-label);
16952
- display: block;
16953
- height: 0;
16954
- overflow: hidden;
16955
- visibility: hidden;
16956
- font-weight: var(--selected-tab-weight);
16957
- }
16958
-
16959
- ixfx-icon {
16960
- flex-shrink: 0;
16961
- }
16962
-
16963
- /* Leading icon (direct child — not close/toolbar icons) picks up the
16964
- accent when selected, so icon-only tabs read as active. */
16965
- :host([selected]) > div > ixfx-icon {
16966
- color: var(--selected-tab-icon-color);
16967
- }
16968
-
16969
- .actions {
16970
- display: flex;
16971
- align-items: center;
16972
- gap: var(--space-xs);
16973
- margin-left: auto;
16974
- flex-shrink: 0;
16975
- }
16976
-
16977
- .toolbar {
16978
- /* Always laid out (display: flex) so the tab's width doesn't shift when
16979
- the toolbar appears on selection. visibility: hidden keeps the
16980
- toolbar from receiving pointer events or painting, while preserving
16981
- the space its contents take up. */
16982
- display: flex;
16983
- align-items: center;
16984
- gap: var(--space-xs);
16985
- visibility: hidden;
16986
- }
16987
-
16988
- :host([selected]) .toolbar {
16989
- visibility: visible;
16990
- }
16991
-
16992
- slot[name="toolbar"] {
16993
- display: contents;
16994
- }
16995
-
16996
- /* Slotted buttons inherit these through the shadow boundary; keeps a
16997
- toolbar button from inflating the strip height. */
16998
- .toolbar ::slotted(ixfx-button) {
16999
- --ixfx-button-padding-block: var(--space-xs);
17000
- }
17001
-
17002
- .close-btn {
17003
- display: inline-flex;
17004
- align-items: center;
17005
- justify-content: center;
17006
- background: none;
17007
- border: none;
17008
- padding: var(--space-xs);
17009
- cursor: pointer;
17010
- color: inherit;
17011
- border-radius: var(--radius-s);
17012
- opacity: var(--prominence-idle-opacity);
17013
- transition: opacity var(--transition), background var(--transition);
17014
- }
17015
-
17016
- .close-btn:hover {
17017
- background: var(--surface-5);
17018
- opacity: var(--prominence-tickled-opacity);
17019
- }
17020
-
17021
- .close-btn:focus-visible {
17022
- outline: 2px solid var(--accent);
17023
- outline-offset: 1px;
17024
- }
17025
-
17026
- :host([selected]) .close-btn,
17027
- :host(:hover) .close-btn {
17028
- opacity: var(--prominence-tickled-opacity);
17029
- }
17030
- `
17031
- ];
17032
- }
17033
- };
17034
- __decorate([property({ type: Boolean })], TabListItemElement.prototype, "closeable", void 0);
17035
- __decorate([property({
17036
- type: Boolean,
17037
- attribute: `hide-labels`
17038
- })], TabListItemElement.prototype, "hideLabels", void 0);
17039
- __decorate([property({
17040
- type: String,
17041
- attribute: `icon-name`
17042
- })], TabListItemElement.prototype, "iconName", void 0);
17043
- __decorate([property({ type: String })], TabListItemElement.prototype, "description", void 0);
17044
- __decorate([property({
17045
- type: String,
17046
- attribute: `command-active`
17047
- })], TabListItemElement.prototype, "commandActive", void 0);
17048
- __decorate([property({ attribute: false })], TabListItemElement.prototype, "registry", void 0);
17049
- __decorate([state()], TabListItemElement.prototype, "_compact", void 0);
17050
- __decorate([state()], TabListItemElement.prototype, "_hasToolbarContent", void 0);
17051
- TabListItemElement = __decorate([safeCustomElement(`ixfx-tab-list-item`)], TabListItemElement);
17052
- //#endregion
17053
- //#region src/titlebar/titlebar.ts
17054
- let TitlebarElement = class TitlebarElement extends i {
17055
- constructor(..._args) {
17056
- super(..._args);
17057
- this.title = ``;
17058
- this.iconName = ``;
17059
- this.orientation = `horizontal`;
17060
- this.variant = `titlebar`;
17061
- this.closable = false;
17062
- this.collapsible = false;
17063
- this.collapsed = false;
17064
- this.toggleInteraction = `implicit`;
17065
- }
17066
- _onToggleClick() {
17067
- if (!this.collapsible) return;
17068
- this.collapsed = !this.collapsed;
17069
- this.dispatchEvent(new CustomEvent(`toggle`, {
17070
- detail: { collapsed: this.collapsed },
17071
- bubbles: true,
17072
- composed: true
17073
- }));
17074
- }
17075
- _onCloseClick() {
17076
- this.dispatchEvent(new CustomEvent(`close`, {
17077
- bubbles: true,
17078
- composed: true
17079
- }));
17080
- }
17081
- _getCaretIcon() {
17082
- return `chevron-down`;
17083
- }
17084
- render() {
17085
- const caretIcon = this._getCaretIcon();
17086
- const isVertical = this.orientation === `vertical`;
17087
- const showCaret = this.collapsible && this.toggleInteraction !== `disabled`;
17088
- const implicitToggle = this.collapsible && this.toggleInteraction === `implicit`;
17089
- const caretRotation = isVertical ? this.collapsed ? `` : `-90deg` : this.collapsed ? `-90deg` : ``;
17090
- const treeVariant = this.variant === `tree`;
17091
- return b`
17092
- <div
17093
- class="bar"
17094
- role="toolbar"
17095
- @click=${implicitToggle ? this._onToggleClick : A}
17096
- >
17097
- ${treeVariant && showCaret ? b`
17098
- <button
17099
- class="caret-btn"
17100
- type="button"
17101
- aria-label=${this.collapsed ? `Expand panel` : `Collapse panel`}
17102
- @click=${this._onToggleClick}
17103
- >
17104
- <ixfx-icon name=${caretIcon} prominence .rotate=${caretRotation.length > 0 ? caretRotation : A}></ixfx-icon>
17105
- </button>
17106
- ` : A}
17107
- <div class="title-text-wrapper">
17108
- ${this.iconName ? b`<ixfx-icon class="title-icon" name=${this.iconName}></ixfx-icon>` : A}
17109
- <span class="title-text">${this.title}</span>
17110
- </div>
17111
-
17112
- <div class="toolbar" @click=${(e) => e.stopPropagation()}>
17113
- <slot name="toolbar"></slot>
17114
- ${this.closable ? b`
17115
- <button
17116
- class="close-btn"
17117
- type="button"
17118
- aria-label="Close panel"
17119
- @click=${this._onCloseClick}
17120
- >
17121
- <ixfx-icon name="close"></ixfx-icon>
17122
- </button>
17123
- ` : A}
17124
- ${!treeVariant && showCaret ? b`
17125
- <button
17126
- class="caret-btn"
17127
- type="button"
17128
- aria-label=${this.collapsed ? `Expand panel` : `Collapse panel`}
17129
- @click=${this._onToggleClick}
17130
- >
17131
- <ixfx-icon name=${caretIcon} prominence .rotate=${caretRotation.length > 0 ? caretRotation : A}></ixfx-icon>
17132
- </button>
17133
- ` : A}
17134
- </div>
17135
- </div>
17136
- `;
17137
- }
17138
- static {
17139
- this.styles = [themeFallbacks, i$3`
17140
- :host {
17141
- display: block;
17142
- box-sizing: border-box;
17143
- }
17144
-
17145
- .bar {
17146
- display: flex;
17147
- flex-direction: row;
17148
- align-items: center;
17149
- padding: var(--space-s) var(--space-m);
17150
- background: var(--surface-4);
17151
- color: var(--surface-3-text);
17152
- min-height: 2em;
17153
- height: 100%;
17154
- flex-shrink: 0;
17155
- box-sizing: border-box;
17156
- }
17157
-
17158
- :host([orientation="vertical"]) .bar {
17159
- flex-direction: row;
17160
- min-width: 2em;
17161
- writing-mode: vertical-rl;
17162
- text-orientation: mixed;
17163
- }
17164
-
17165
- .caret-btn,
17166
- .close-btn {
17167
- display: inline-flex;
17168
- align-items: center;
17169
- justify-content: center;
17170
- background: none;
17171
- border: none;
17172
- padding: var(--space-xs);
17173
- margin: 0;
17174
- cursor: pointer;
17175
- color: inherit;
17176
- border-radius: var(--radius-s);
17177
- transition: background var(--transition);
17178
- opacity: var(--prominence-idle-opacity);
17179
- }
17180
-
17181
- .caret-btn:hover,
17182
- .close-btn:hover {
17183
- background: var(--surface-5);
17184
- opacity: var(--prominence-tickled-opacity);
17185
- }
17186
-
17187
- .caret-btn:focus-visible,
17188
- .close-btn:focus-visible {
17189
- outline: 2px solid var(--accent);
17190
- outline-offset: 1px;
17191
- }
17192
-
17193
- ixfx-icon {
17194
- width: 1.25em;
17195
- height: 1.25em;
17196
- transition: transform var(--transition);
17197
- }
17198
-
17199
- .title-icon {
17200
- flex-shrink: 0;
17201
- }
17202
-
17203
- .title-text {
17204
- flex: 1;
17205
- padding-left: 1ch;
17206
- font-family: var(--font-family);
17207
- font-size: var(--text-m);
17208
- font-weight: var(--font-weight-bold, 600);
17209
- white-space: nowrap;
17210
- overflow: hidden;
17211
- align-self: center;
17212
- text-overflow: ellipsis;
17213
- user-select: none;
17214
- -webkit-user-select: none;
17215
- }
17216
-
17217
- :host([collapsible][toggle-interaction="implicit"]) .bar {
17218
- cursor: pointer;
17219
- }
17220
-
17221
- :host([orientation="vertical"]) .title-text {
17222
- writing-mode: vertical-rl;
17223
- padding-left: 0;
17224
- padding-bottom: 1ch;
17225
- text-orientation: mixed;
17226
- transform: rotate(180deg);
17227
- }
17228
-
17229
- .toolbar {
17230
- display: flex;
17231
- align-items: center;
17232
- gap: var(--space-xs);
17233
- margin-left: auto;
17234
- }
17235
-
17236
- :host([orientation="vertical"]) .toolbar {
17237
- flex-direction: column;
17238
- margin-left: 0;
17239
- margin-top: auto;
17240
- writing-mode: horizontal-tb;
17241
- }
17242
-
17243
- slot[name="toolbar"] {
17244
- display: contents;
17245
- }
17246
-
17247
- ::slotted(slot[name="toolbar"]) {
17248
- display: contents;
17249
- }
17250
-
17251
- .title-text-wrapper {
17252
- display: flex;
17253
- padding: 0 var(--space-s);
17254
- }
17255
-
17256
- /* === Midline variant === */
17257
-
17258
- :host([variant="midline"]) .bar {
17259
- background: transparent;
17260
- padding: var(--space-xs) var(--space-m);
17261
- min-height: unset;
17262
- position: relative;
17263
- }
17264
-
17265
- :host([variant="midline"][orientation="vertical"]) .bar {
17266
- padding: var(--space-m) var(--space-xs);
17267
- min-width: unset;
17268
- }
17269
-
17270
- :host([variant="midline"]) .title-text-wrapper {
17271
- position: relative;
17272
- z-index: 1;
17273
- display: flex;
17274
- align-items: center;
17275
- gap: var(--space-s);
17276
- background: var(--surface-3);
17277
- }
17278
-
17279
- :host([variant="midline"]) .title-text {
17280
- font-weight: normal;
17281
- color: var(--surface-3-text);
17282
- padding: 0 0.5ch 0 0.5ch;
17283
- background: transparent;
17284
- box-shadow: none;
17285
- flex: unset;
17286
- width: auto;
17287
- white-space: nowrap;
17288
- }
18334
+ */
18335
+ _syncLabelSizer() {
18336
+ const slot = this.shadowRoot?.querySelector(`slot:not([name])`);
18337
+ const label = this.shadowRoot?.querySelector(`.label`);
18338
+ if (slot === null || slot === void 0 || label === null || label === void 0) return;
18339
+ const text = slot.assignedNodes().filter((node) => !(node instanceof HTMLElement && node.hasAttribute(`slot`))).map((node) => node.textContent ?? ``).join(``);
18340
+ if (text) label.setAttribute(`data-label`, text);
18341
+ else label.removeAttribute(`data-label`);
18342
+ }
18343
+ _updateToolbarContent() {
18344
+ const slot = this.shadowRoot?.querySelector(`slot[name="toolbar"]`);
18345
+ this._hasToolbarContent = (slot?.assignedElements().length ?? 0) > 0;
18346
+ }
18347
+ firstUpdated() {
18348
+ this._updateToolbarContent();
18349
+ this._syncLabelSizer();
18350
+ }
18351
+ render() {
18352
+ const showLabel = !this.hideLabels || !this.iconName;
18353
+ const hasActions = this.closeable || this._hasToolbarContent;
18354
+ return b`
18355
+ <div>
18356
+ ${this.iconName ? b`<ixfx-icon name="${this.iconName}"></ixfx-icon>` : A}
18357
+ ${showLabel ? b`<span class="label"><slot></slot></span>` : A}
18358
+ ${hasActions ? b`
18359
+ <div class="actions">
18360
+ <div class="toolbar"><slot name="toolbar"></slot></div>
18361
+ ${this.closeable ? b`
18362
+ <button
18363
+ class="close-btn"
18364
+ type="button"
18365
+ aria-label="Close tab"
18366
+ @click=${this._onCloseClick}
18367
+ >
18368
+ <ixfx-icon name="close"></ixfx-icon>
18369
+ </button>
18370
+ ` : A}
18371
+ </div>
18372
+ ` : A}
18373
+ </div>
18374
+ `;
18375
+ }
18376
+ _onCloseClick(e) {
18377
+ e.stopPropagation();
18378
+ this.dispatchEvent(new CustomEvent(`close`, {
18379
+ bubbles: true,
18380
+ composed: true
18381
+ }));
18382
+ this.remove();
18383
+ }
18384
+ static {
18385
+ this.styles = [
18386
+ themeFallbacks,
18387
+ prominenceStyles,
18388
+ i$3`
18389
+ :host {
18390
+ --selected-tab-color: var(--ixfx-selected-tab-color, var(--surface-2-text));
18391
+ --selected-tab-icon-color: var(--ixfx-selected-tab-icon-color, var(--accent));
18392
+ --selected-tab-border-width: var(--ixfx-selected-tab-border-width, 1px);
18393
+ --selected-tab-weight: var(--ixfx-selected-tab-weight, bold);
18394
+ --selected-tab-text-decoration: var(--ixfx-selected-tab-text-decoration, none);
18395
+ --selected-tab-text-decoration-color: var(--ixfx-selected-tab-text-decoration-color, var(--accent));
18396
+ --selected-tab-text-decoration-thickness: var(--ixfx-selected-tab-text-decoration-thickness, 0px);
18397
+ --selected-tab-text-decoration-offset: var(--ixfx-selected-tab-text-decoration-offset, 2px);
18398
+ --selected-background: var(--ixfx-tab-selected-background, var(--surface-5));
18399
+ --tab-padding: var(--ixfx-tab-padding, var(--space-s));
18400
+ display: inline-flex;
18401
+ min-width: 0;
18402
+ user-select: none;
18403
+ box-sizing: border-box;
18404
+ font-size: var(--text-m);
18405
+ color: inherit;
18406
+ /* No explicit height: items stretch (align-self: stretch via the
18407
+ list's flex container) so every tab in a strip shares one height
18408
+ even when toolbars give some items taller content. */
18409
+ }
17289
18410
 
17290
- :host([variant="midline"]) .title-icon {
17291
- background: transparent;
17292
- flex-shrink: 0;
17293
- }
18411
+ :host > div {
18412
+ display: flex;
18413
+ align-items: center;
18414
+ padding: var(--tab-padding) calc(2 * var(--tab-padding-horizontal, var(--tab-padding)));
18415
+ min-width: 0;
18416
+ width: 100%;
18417
+ font-weight: 500;
18418
+ border: var(--selected-tab-border-width) solid transparent;
18419
+ gap: var(--space-s);
18420
+ transition: background var(--transition, 0.15s);
18421
+ }
17294
18422
 
17295
- :host([variant="midline"]) .caret-btn {
17296
- position: relative;
17297
- z-index: 1;
17298
- background: var(--surface-3);
17299
- opacity: 1;
17300
- }
18423
+ /* Hover ("tickle") background. When --ixfx-tab-hover-background is
18424
+ unset each state keeps its resting background, so existing
18425
+ consumers see no change. */
18426
+ :host(:hover:not([selected])) > div {
18427
+ background: var(--ixfx-tab-hover-background, transparent);
18428
+ }
17301
18429
 
17302
- :host([variant="midline"][orientation="vertical"]) .title-text {
17303
- transform: rotate(180deg);
17304
- padding: 0.5ch 0 0.5ch 0ch;
17305
- }
18430
+ :host([selected]:hover) > div {
18431
+ background: var(--ixfx-tab-hover-background, var(--selected-background));
18432
+ }
17306
18433
 
17307
- :host([variant="midline"]) .toolbar {
17308
- position: relative;
17309
- z-index: 2;
17310
- background: var(--surface-3);
17311
- margin-left: 0;
17312
- }
18434
+ :host([selected]) > div {
18435
+ color: var(--selected-tab-color);
18436
+ border-top-color: var(--indicator-color-top, transparent);
18437
+ border-bottom-color: var(--indicator-color-bottom, transparent);
18438
+ border-left-color: var(--indicator-color-left, transparent);
18439
+ border-right-color: var(--indicator-color-right, transparent);
18440
+ background: var(--selected-background);
18441
+ font-weight: var(--selected-tab-weight);
18442
+ border-radius:var(--radius-m);
18443
+ }
17313
18444
 
17314
- :host([variant="midline"][orientation="horizontal"]) .toolbar {
17315
- margin-left: auto;
17316
- }
18445
+ :host([selected]) .label {
18446
+ text-decoration: var(--selected-tab-text-decoration);
18447
+ text-decoration-color: var(--selected-tab-text-decoration-color);
18448
+ text-decoration-thickness: var(--selected-tab-text-decoration-thickness);
18449
+ text-decoration-offset: var(--selected-tab-text-decoration-offset);
18450
+ }
17317
18451
 
17318
- /* Debossed line runs from after title-text to before toolbar */
17319
- :host([variant="midline"]) .bar::after {
17320
- content: '';
17321
- position: absolute;
17322
- top: 50%;
17323
- left: 0;
17324
- right: 0;
17325
- height: 1px;
17326
- transform: translateY(-50%);
17327
- background: var(--surface-2);
17328
- box-shadow:
17329
- 0 -1px 0 var(--surface-5),
17330
- 0 1px 0 var(--surface-1);
17331
- z-index: 0;
17332
- }
18452
+ .label {
18453
+ overflow: var(--allow-overflow, hidden);
18454
+ text-overflow: ellipsis;
18455
+ white-space: nowrap;
18456
+ min-width: 0;
18457
+ flex: 1 1 auto;
18458
+ }
17333
18459
 
17334
- /* For vertical midline, line runs vertically */
17335
- :host([variant="midline"][orientation="vertical"]) .bar::after {
17336
- top: 0;
17337
- bottom: 0;
17338
- left: 50%;
17339
- right: unset;
17340
- width: 1px;
17341
- height: 100%;
17342
- transform: translateX(-50%);
17343
- box-shadow:
17344
- -1px 0 0 var(--surface-5),
17345
- 1px 0 0 var(--surface-1);
17346
- }
18460
+ /* Width sizer: an invisible copy of the label, always set at the
18461
+ selected weight, so activating a tab (bold) never shifts the strip.
18462
+ Text is mirrored from the default slot via the data-label attribute
18463
+ (see _syncLabelSizer). */
18464
+ .label::after {
18465
+ content: attr(data-label);
18466
+ display: block;
18467
+ height: 0;
18468
+ overflow: hidden;
18469
+ visibility: hidden;
18470
+ font-weight: var(--selected-tab-weight);
18471
+ }
17347
18472
 
17348
- /* === Tree variant: VS Code explorer / Outline style === */
17349
- :host([variant="tree"]) .bar {
17350
- background: transparent;
17351
- padding: 0 var(--space-xs);
17352
- min-height: unset;
17353
- gap: var(--space-xs);
17354
- }
18473
+ ixfx-icon {
18474
+ flex-shrink: 0;
18475
+ }
17355
18476
 
17356
- :host([variant="tree"][orientation="vertical"]) .bar {
17357
- padding: var(--space-xs) 0;
17358
- flex-direction: column;
17359
- min-width: unset;
17360
- writing-mode: horizontal-tb;
17361
- }
18477
+ /* Leading icon (direct child — not close/toolbar icons) picks up the
18478
+ accent when selected, so icon-only tabs read as active. */
18479
+ :host([selected]) > div > ixfx-icon {
18480
+ color: var(--selected-tab-icon-color);
18481
+ }
17362
18482
 
17363
- :host([variant="tree"]) .title-text-wrapper {
17364
- display: inline-flex;
17365
- align-items: center;
17366
- gap: var(--space-xs);
17367
- padding: 0;
17368
- background: transparent;
17369
- }
18483
+ .actions {
18484
+ display: flex;
18485
+ align-items: center;
18486
+ gap: var(--space-xs);
18487
+ margin-left: auto;
18488
+ flex-shrink: 0;
18489
+ }
17370
18490
 
17371
- :host([variant="tree"]) .title-icon {
17372
- background: transparent;
17373
- flex-shrink: 0;
17374
- }
18491
+ .toolbar {
18492
+ /* Always laid out (display: flex) so the tab's width doesn't shift when
18493
+ the toolbar appears on selection. visibility: hidden keeps the
18494
+ toolbar from receiving pointer events or painting, while preserving
18495
+ the space its contents take up. */
18496
+ display: flex;
18497
+ align-items: center;
18498
+ gap: var(--space-xs);
18499
+ visibility: hidden;
18500
+ }
17375
18501
 
17376
- :host([variant="tree"]) .title-text {
17377
- font-weight: normal;
17378
- color: var(--surface-3-text);
17379
- padding: 0;
17380
- background: transparent;
17381
- box-shadow: none;
17382
- flex: unset;
17383
- width: auto;
17384
- white-space: nowrap;
17385
- }
18502
+ :host([selected]) .toolbar {
18503
+ visibility: visible;
18504
+ }
17386
18505
 
17387
- :host([variant="tree"][orientation="vertical"]) .title-text {
17388
- writing-mode: vertical-rl;
17389
- transform: rotate(180deg);
17390
- padding: 0;
17391
- }
18506
+ slot[name="toolbar"] {
18507
+ display: contents;
18508
+ }
17392
18509
 
17393
- :host([variant="tree"]) .caret-btn {
17394
- background: transparent;
17395
- opacity: var(--prominence-idle-opacity);
17396
- padding: 0;
17397
- }
18510
+ /* Slotted buttons inherit these through the shadow boundary; keeps a
18511
+ toolbar button from inflating the strip height. */
18512
+ .toolbar ::slotted(ixfx-button) {
18513
+ --ixfx-button-padding-block: var(--space-xs);
18514
+ }
17398
18515
 
17399
- :host([variant="tree"]) .caret-btn:hover {
17400
- background: transparent;
17401
- opacity: var(--prominence-tickled-opacity);
17402
- }
18516
+ .close-btn {
18517
+ display: inline-flex;
18518
+ align-items: center;
18519
+ justify-content: center;
18520
+ background: none;
18521
+ border: none;
18522
+ padding: var(--space-xs);
18523
+ cursor: pointer;
18524
+ color: inherit;
18525
+ border-radius: var(--radius-s);
18526
+ opacity: var(--prominence-idle-opacity);
18527
+ transition: opacity var(--transition), background var(--transition);
18528
+ }
17403
18529
 
17404
- :host([variant="tree"]) .toolbar {
17405
- margin-left: auto;
17406
- }
18530
+ .close-btn:hover {
18531
+ background: var(--surface-5);
18532
+ opacity: var(--prominence-tickled-opacity);
18533
+ }
17407
18534
 
17408
- :host([variant="tree"][orientation="vertical"]) .toolbar {
17409
- flex-direction: row;
17410
- margin-left: 0;
17411
- margin-top: auto;
17412
- }
18535
+ .close-btn:focus-visible {
18536
+ outline: 2px solid var(--accent);
18537
+ outline-offset: 1px;
18538
+ }
17413
18539
 
17414
- :host([variant="tree"]) ixfx-icon {
17415
- width: 1em;
17416
- height: 1em;
17417
- }
17418
- `];
18540
+ :host([selected]) .close-btn,
18541
+ :host(:hover) .close-btn {
18542
+ opacity: var(--prominence-tickled-opacity);
18543
+ }
18544
+ `
18545
+ ];
17419
18546
  }
17420
18547
  };
17421
- __decorate([property({ type: String })], TitlebarElement.prototype, "title", void 0);
17422
- __decorate([property({
17423
- type: String,
17424
- attribute: `icon-name`
17425
- })], TitlebarElement.prototype, "iconName", void 0);
17426
- __decorate([property({
17427
- type: String,
17428
- attribute: `orientation`,
17429
- reflect: true
17430
- })], TitlebarElement.prototype, "orientation", void 0);
17431
- __decorate([property({
17432
- type: String,
17433
- attribute: `variant`,
17434
- reflect: true
17435
- })], TitlebarElement.prototype, "variant", void 0);
17436
- __decorate([property({ type: Boolean })], TitlebarElement.prototype, "closable", void 0);
18548
+ __decorate([property({ type: Boolean })], TabListItemElement.prototype, "closeable", void 0);
17437
18549
  __decorate([property({
17438
18550
  type: Boolean,
17439
- reflect: true
17440
- })], TitlebarElement.prototype, "collapsible", void 0);
18551
+ attribute: `hide-labels`
18552
+ })], TabListItemElement.prototype, "hideLabels", void 0);
17441
18553
  __decorate([property({
17442
- type: Boolean,
17443
- reflect: true
17444
- })], TitlebarElement.prototype, "collapsed", void 0);
18554
+ type: String,
18555
+ attribute: `icon-name`
18556
+ })], TabListItemElement.prototype, "iconName", void 0);
18557
+ __decorate([property({ type: String })], TabListItemElement.prototype, "description", void 0);
17445
18558
  __decorate([property({
17446
18559
  type: String,
17447
- attribute: `toggle-interaction`
17448
- })], TitlebarElement.prototype, "toggleInteraction", void 0);
17449
- TitlebarElement = __decorate([safeCustomElement(`ixfx-titlebar`)], TitlebarElement);
18560
+ attribute: `command-active`
18561
+ })], TabListItemElement.prototype, "commandActive", void 0);
18562
+ __decorate([property({ attribute: false })], TabListItemElement.prototype, "registry", void 0);
18563
+ __decorate([state()], TabListItemElement.prototype, "_compact", void 0);
18564
+ __decorate([state()], TabListItemElement.prototype, "_hasToolbarContent", void 0);
18565
+ TabListItemElement = __decorate([safeCustomElement(`ixfx-tab-list-item`)], TabListItemElement);
17450
18566
  //#endregion
17451
18567
  //#region src/dock/dock-panel.ts
17452
18568
  /** Pointer movement threshold before a titlebar press becomes a dock drag. */
@@ -18687,7 +19803,7 @@ let DockHostElement = class DockHostElement extends i {
18687
19803
  role="separator"
18688
19804
  aria-label="Resize adjacent panels"
18689
19805
  @pointerdown=${(e) => this._onJunctionPointerDown(e, junction)}
18690
- ></div>
19806
+ ><span class="ixfx-handle" data-handle="dotted" data-shape="spot" part="junction-handle"></span></div>
18691
19807
  `)}
18692
19808
  </div>
18693
19809
  <slot @slotchange=${this._onPoolSlotChange}></slot>
@@ -18781,7 +19897,10 @@ let DockHostElement = class DockHostElement extends i {
18781
19897
  };
18782
19898
  }
18783
19899
  static {
18784
- this.styles = [themeFallbacks, i$3`
19900
+ this.styles = [
19901
+ themeFallbacks,
19902
+ handleStyles,
19903
+ i$3`
18785
19904
  :host {
18786
19905
  display: block;
18787
19906
  width: 100%;
@@ -18909,26 +20028,23 @@ let DockHostElement = class DockHostElement extends i {
18909
20028
  border-radius: 50%;
18910
20029
  cursor: nwse-resize;
18911
20030
  pointer-events: auto;
20031
+ display: flex;
20032
+ align-items: center;
20033
+ justify-content: center;
18912
20034
  }
18913
20035
 
18914
- .junction-handle::after {
18915
- content: '';
18916
- position: absolute;
18917
- inset: 4px;
18918
- border-radius: 50%;
18919
- background: var(--dock-junction-color, var(--accent));
18920
- opacity: 0;
18921
- transition: opacity var(--transition);
18922
- }
18923
-
18924
- .junction-handle:hover::after {
18925
- opacity: 0.9;
20036
+ .junction-handle .ixfx-handle {
20037
+ --ixfx-handle-spot-size: 12px;
20038
+ --ixfx-handle-dot-size: 3px;
20039
+ --ixfx-handle-opacity: 0;
20040
+ --ixfx-handle-opacity-active: 0.9;
18926
20041
  }
18927
20042
 
18928
20043
  slot:not([name]) {
18929
20044
  display: none;
18930
20045
  }
18931
- `];
20046
+ `
20047
+ ];
18932
20048
  }
18933
20049
  };
18934
20050
  __decorate([property({
@@ -20398,6 +21514,233 @@ var DataGridListSource = class {
20398
21514
  }
20399
21515
  };
20400
21516
  //#endregion
21517
+ //#region src/pan-text/pan-text.ts
21518
+ const SPEED = 45;
21519
+ const END_PADDING = 4;
21520
+ const START_DELAY = 300;
21521
+ const END_DWELL = 2e3;
21522
+ const RESET_DURATION = 400;
21523
+ const PAN_EASING = `cubic-bezier(0.25, 0.46, 0.45, 0.94)`;
21524
+ let PanTextElement = class PanTextElement extends i {
21525
+ constructor(..._args) {
21526
+ super(..._args);
21527
+ this.direction = `end`;
21528
+ this.disabled = false;
21529
+ this.mode = `hover`;
21530
+ this._isOverflowing = false;
21531
+ this._isHovering = false;
21532
+ this._loopActive = false;
21533
+ this._loopAtEnd = false;
21534
+ this._overflow = 0;
21535
+ this._resizeObserver = null;
21536
+ this._loopTimer = 0;
21537
+ this._onMouseEnter = () => {
21538
+ this._isHovering = true;
21539
+ };
21540
+ this._onMouseLeave = () => {
21541
+ this._isHovering = false;
21542
+ };
21543
+ this._measure = () => {
21544
+ if (this._isHovering || this._loopAtEnd) return;
21545
+ const el = this._textEl;
21546
+ if (!el) return;
21547
+ this._overflow = Math.max(0, el.scrollWidth - el.clientWidth);
21548
+ const overflowing = this._overflow > 0;
21549
+ this.classList.toggle(`is-overflowing`, overflowing);
21550
+ if (overflowing !== this._isOverflowing) this._isOverflowing = overflowing;
21551
+ if (this._loopActive && this.mode === `loop` && !this.disabled && overflowing && this._loopTimer === 0) this._scheduleLoopPan(START_DELAY);
21552
+ if (!overflowing && this._loopAtEnd) {
21553
+ this._loopAtEnd = false;
21554
+ this.requestUpdate();
21555
+ }
21556
+ };
21557
+ }
21558
+ connectedCallback() {
21559
+ super.connectedCallback();
21560
+ this._resizeObserver = new ResizeObserver(() => this._measure());
21561
+ this.addEventListener(`mouseenter`, this._onMouseEnter);
21562
+ this.addEventListener(`mouseleave`, this._onMouseLeave);
21563
+ }
21564
+ disconnectedCallback() {
21565
+ super.disconnectedCallback();
21566
+ this._clearLoopTimer();
21567
+ this._loopActive = false;
21568
+ this._loopAtEnd = false;
21569
+ this._resizeObserver?.disconnect();
21570
+ this._resizeObserver = null;
21571
+ this.removeEventListener(`mouseenter`, this._onMouseEnter);
21572
+ this.removeEventListener(`mouseleave`, this._onMouseLeave);
21573
+ }
21574
+ firstUpdated() {
21575
+ this._measure();
21576
+ this._resizeObserver?.observe(this);
21577
+ document.fonts?.ready.then(() => this._measure());
21578
+ if (this.mode === `loop`) this.startLoop();
21579
+ }
21580
+ updated(changed) {
21581
+ if (changed.has(`mode`)) {
21582
+ if (this.mode === `loop`) this.startLoop();
21583
+ else this.stopLoop();
21584
+ }
21585
+ if (changed.has(`disabled`) && this.disabled) this.stopLoop();
21586
+ this._measure();
21587
+ }
21588
+ /**
21589
+ * Whether a loop pan is currently active (between `startLoop()` and `stopLoop()`).
21590
+ */
21591
+ get looping() {
21592
+ return this._loopActive;
21593
+ }
21594
+ /**
21595
+ * Begin looping: pan to the end, dwell there for 2 seconds, reset to the
21596
+ * start and pan again, repeating until `stopLoop()` is called. A no-op when
21597
+ * `disabled`, when there is nothing to reveal, or under reduced-motion.
21598
+ */
21599
+ startLoop() {
21600
+ if (this.disabled || this._prefersReducedMotion()) return;
21601
+ this._loopActive = true;
21602
+ this._loopAtEnd = false;
21603
+ this.requestUpdate();
21604
+ if (this._isOverflowing && this._loopTimer === 0) this._scheduleLoopPan(START_DELAY);
21605
+ }
21606
+ /**
21607
+ * Stop a loop started with `startLoop()` and snap back to the rest position.
21608
+ */
21609
+ stopLoop() {
21610
+ if (!this._loopActive && !this._loopAtEnd && this._loopTimer === 0) return;
21611
+ this._clearLoopTimer();
21612
+ this._loopActive = false;
21613
+ this._loopAtEnd = false;
21614
+ this.requestUpdate();
21615
+ }
21616
+ _prefersReducedMotion() {
21617
+ return globalThis.matchMedia?.(`(prefers-reduced-motion: reduce)`).matches ?? false;
21618
+ }
21619
+ _panDuration() {
21620
+ return Math.max(1.5, (this._overflow + END_PADDING) / SPEED);
21621
+ }
21622
+ _clearLoopTimer() {
21623
+ if (this._loopTimer !== 0) {
21624
+ clearTimeout(this._loopTimer);
21625
+ this._loopTimer = 0;
21626
+ }
21627
+ }
21628
+ _scheduleLoopPan(delay) {
21629
+ this._clearLoopTimer();
21630
+ this._loopTimer = window.setTimeout(() => {
21631
+ this._loopTimer = 0;
21632
+ if (!this._loopActive || this.disabled || !this._isOverflowing) return;
21633
+ this._loopAtEnd = true;
21634
+ this.requestUpdate();
21635
+ const dwell = this._panDuration() * 1e3 + END_DWELL;
21636
+ this._loopTimer = window.setTimeout(() => {
21637
+ this._loopTimer = 0;
21638
+ if (!this._loopActive) return;
21639
+ this._loopAtEnd = false;
21640
+ this.requestUpdate();
21641
+ this._scheduleLoopPan(700);
21642
+ }, dwell);
21643
+ }, delay);
21644
+ }
21645
+ _textStyle() {
21646
+ if (!this._isOverflowing) return ``;
21647
+ const distance = this._overflow + END_PADDING;
21648
+ const duration = Math.max(1.5, distance / SPEED);
21649
+ const revealEnd = this.direction === `end`;
21650
+ const restX = revealEnd ? 0 : -distance;
21651
+ const hoverX = revealEnd ? -distance : 0;
21652
+ if (this._loopActive && this.mode === `loop`) {
21653
+ if (this._loopAtEnd) return [
21654
+ `transform: translateX(${hoverX}px)`,
21655
+ `transition: transform ${duration}s ${PAN_EASING}`,
21656
+ `overflow: visible`,
21657
+ `text-overflow: clip`
21658
+ ].join(`; `);
21659
+ return [
21660
+ `transform: translateX(${restX}px)`,
21661
+ `transition: transform ${RESET_DURATION}ms ${PAN_EASING}`,
21662
+ revealEnd ? `` : `overflow: visible; text-overflow: clip`
21663
+ ].filter(Boolean).join(`; `);
21664
+ }
21665
+ if (this._isHovering) return [
21666
+ `transform: translateX(${hoverX}px)`,
21667
+ `transition: transform ${duration}s ${PAN_EASING} ${START_DELAY}ms`,
21668
+ `overflow: visible`,
21669
+ `text-overflow: clip`
21670
+ ].join(`; `);
21671
+ return [
21672
+ `transform: translateX(${restX}px)`,
21673
+ `transition: transform 400ms ${PAN_EASING}`,
21674
+ revealEnd ? `` : `overflow: visible; text-overflow: clip`
21675
+ ].filter(Boolean).join(`; `);
21676
+ }
21677
+ render() {
21678
+ const textClasses = {
21679
+ "is-overflowing": this._isOverflowing,
21680
+ "is-hovering": this._isHovering
21681
+ };
21682
+ return b`
21683
+ <span class="text ${classMap(textClasses)}" part="text" style=${this._textStyle()}>
21684
+ <slot @slotchange=${this._measure}></slot>
21685
+ </span>
21686
+ `;
21687
+ }
21688
+ static {
21689
+ this.styles = [themeFallbacks, i$3`
21690
+ /* Renders like plain inline text: no colours, border, padding or margin
21691
+ of its own — it only adds the clipping needed to pan on hover. */
21692
+ :host {
21693
+ display: inline-block;
21694
+ overflow: hidden;
21695
+ white-space: nowrap;
21696
+ max-width: 100%;
21697
+ box-sizing: border-box;
21698
+ font: inherit;
21699
+ color: inherit;
21700
+ }
21701
+
21702
+ :host([disabled]) {
21703
+ opacity: var(--disabled-opacity, 0.5);
21704
+ pointer-events: none;
21705
+ }
21706
+
21707
+ .text {
21708
+ display: block;
21709
+ overflow: hidden;
21710
+ text-overflow: ellipsis;
21711
+ white-space: nowrap;
21712
+ will-change: transform;
21713
+ }
21714
+
21715
+ @media (prefers-reduced-motion: reduce) {
21716
+ .text {
21717
+ transition: none !important;
21718
+ transform: none !important;
21719
+ }
21720
+ }
21721
+ `];
21722
+ }
21723
+ };
21724
+ __decorate([property({
21725
+ type: String,
21726
+ attribute: `direction`
21727
+ })], PanTextElement.prototype, "direction", void 0);
21728
+ __decorate([property({
21729
+ type: Boolean,
21730
+ attribute: `disabled`
21731
+ })], PanTextElement.prototype, "disabled", void 0);
21732
+ __decorate([property({
21733
+ type: String,
21734
+ attribute: `mode`,
21735
+ reflect: true
21736
+ })], PanTextElement.prototype, "mode", void 0);
21737
+ __decorate([state()], PanTextElement.prototype, "_isOverflowing", void 0);
21738
+ __decorate([state()], PanTextElement.prototype, "_isHovering", void 0);
21739
+ __decorate([state()], PanTextElement.prototype, "_loopActive", void 0);
21740
+ __decorate([state()], PanTextElement.prototype, "_loopAtEnd", void 0);
21741
+ __decorate([query(`.text`)], PanTextElement.prototype, "_textEl", void 0);
21742
+ PanTextElement = __decorate([safeCustomElement(`ixfx-pan-text`)], PanTextElement);
21743
+ //#endregion
20401
21744
  //#region src/grid-list/grid-list.ts
20402
21745
  const ITEM_SELECTOR$1 = `:scope > *`;
20403
21746
  const MIN_ITEM_SIZE = 16;
@@ -20483,6 +21826,7 @@ let GridListElement = class GridListElement extends i {
20483
21826
  super(..._args);
20484
21827
  this.selectionMode = `single`;
20485
21828
  this.interactionMode = `standard`;
21829
+ this.disableHoverTickle = false;
20486
21830
  this.itemSize = 160;
20487
21831
  this.gap = 8;
20488
21832
  this.orientation = `grid`;
@@ -20492,6 +21836,7 @@ let GridListElement = class GridListElement extends i {
20492
21836
  this.captionBackdrop = `scrim`;
20493
21837
  this.captionHeight = 24;
20494
21838
  this.itemWrap = `stop`;
21839
+ this.itemsDraggable = false;
20495
21840
  this.items = [];
20496
21841
  this._scrollTop = 0;
20497
21842
  this._scrollLeft = 0;
@@ -20528,10 +21873,56 @@ let GridListElement = class GridListElement extends i {
20528
21873
  enabled: this.#source.isSelectable(i)
20529
21874
  })),
20530
21875
  onTickle: (k) => {
21876
+ this.#stopActiveCaptionPan();
20531
21877
  if (k !== void 0) this.#emitItemEvent(`list-tickle`, k);
20532
21878
  },
20533
21879
  onStateChange: () => this._syncItemStates(),
20534
- itemWrap: this.#itemWrap
21880
+ itemWrap: this.#itemWrap,
21881
+ hoverDisabled: () => this.disableHoverTickle || this.#dragSelect.isDragging
21882
+ });
21883
+ this.#marqueePreview = /* @__PURE__ */ new Set();
21884
+ this.#nativeDragging = false;
21885
+ this.#dragOriginKey = void 0;
21886
+ this.#postDragSuppressTimer = 0;
21887
+ this.#activeCaptionPan = null;
21888
+ this.#dragSelect = new ListDragSelectController(this, {
21889
+ getSurface: () => this.#viewportRef.value,
21890
+ isEnabled: () => this.selection.canDragSelect() && this.#source.count > 0 && !this.hasAttribute(`disable-drag-select`),
21891
+ hitTest: (rect) => this.#marqueeHitTest(rect),
21892
+ getBaseSelection: () => this.selection.selected,
21893
+ onPreview: (preview, rect) => {
21894
+ this.#marqueePreview = preview;
21895
+ this.#marqueeRect = rect;
21896
+ this.requestUpdate();
21897
+ },
21898
+ onCommit: (finalSet) => {
21899
+ this.selection.applyDragSelection(finalSet);
21900
+ this.#marqueePreview = /* @__PURE__ */ new Set();
21901
+ this.#marqueeRect = void 0;
21902
+ this.requestUpdate();
21903
+ },
21904
+ autoScroll: (dx, dy) => {
21905
+ const vp = this.#viewportRef.value;
21906
+ if (!vp) return;
21907
+ if (dx !== 0) {
21908
+ vp.scrollLeft += dx;
21909
+ this._scrollLeft = vp.scrollLeft;
21910
+ }
21911
+ if (dy !== 0) {
21912
+ vp.scrollTop += dy;
21913
+ this._scrollTop = vp.scrollTop;
21914
+ }
21915
+ },
21916
+ shouldStart: (event) => {
21917
+ if (!this.itemsDraggable) return true;
21918
+ const cell = event.composedPath()[0]?.closest?.(`.cell`);
21919
+ if (!cell) return true;
21920
+ const key = cell.dataset.key;
21921
+ if (!key) return true;
21922
+ const index = this.#source.indexOfKey(key);
21923
+ if (index < 0) return true;
21924
+ return !this.#source.isSelectable(index);
21925
+ }
20535
21926
  });
20536
21927
  this.#selectedSnapshot = /* @__PURE__ */ new Set();
20537
21928
  this.#onScroll = (e) => {
@@ -20544,6 +21935,32 @@ let GridListElement = class GridListElement extends i {
20544
21935
  this.#emitViewportChange();
20545
21936
  });
20546
21937
  };
21938
+ this.#onDragStart = (e, key) => {
21939
+ if (!this.itemsDraggable) return;
21940
+ const index = this.#source.indexOfKey(key);
21941
+ if (index < 0 || !this.#source.isSelectable(index)) return;
21942
+ if (!this.selection.selectedKeys.has(key)) this.selection.replace(key);
21943
+ const selectedKeys = this.#source.keys().filter((k) => !isSyntheticKey(k)).filter((k) => this.selection.selectedKeys.has(k));
21944
+ const detail = {
21945
+ key,
21946
+ index: this.#source.indexOfKey(key),
21947
+ item: this.#source.resolveKeyToData(key) ?? this.#source.resolveKeyToElement(key),
21948
+ selectedKeys
21949
+ };
21950
+ if (!this._dispatch(`grid-list-native-drag-start`, detail, true)) {
21951
+ e.preventDefault();
21952
+ return;
21953
+ }
21954
+ this.#nativeDragging = true;
21955
+ this.#dragOriginKey = key;
21956
+ this.toggleAttribute(`data-native-dragging`, true);
21957
+ const count = selectedKeys.length;
21958
+ this.setAttribute(`aria-label`, count === 1 ? `Dragging 1 item` : `Dragging ${count} items`);
21959
+ };
21960
+ this.#onDragEnd = () => {
21961
+ this.#clearNativeDragState();
21962
+ this.#startPostDragSuppressTimer();
21963
+ };
20547
21964
  this._onKeyDown = (e) => {
20548
21965
  if ((e.ctrlKey || e.metaKey) && e.key === `f`) return;
20549
21966
  if ((e.ctrlKey || e.metaKey) && e.key === `a`) {
@@ -20613,6 +22030,7 @@ let GridListElement = class GridListElement extends i {
20613
22030
  break;
20614
22031
  }
20615
22032
  case `Escape`:
22033
+ if (this.#nativeDragging) this.#clearNativeDragState();
20616
22034
  this.selection.clear();
20617
22035
  break;
20618
22036
  default: return;
@@ -20680,7 +22098,18 @@ let GridListElement = class GridListElement extends i {
20680
22098
  border-radius: var(--grid-list-radius);
20681
22099
  }
20682
22100
 
20683
- /* The square that holds the slotted content. */
22101
+ /* The square that holds the slotted content — the "image well". Its
22102
+ background is a set of consumer-settable variables so a Finder-style
22103
+ look is possible: on selection the caption takes the accent colouring
22104
+ while the well takes a tint of the surface colour. Each variable is
22105
+ unset by default, so the fallback (today's look) applies until a
22106
+ consumer opts in.
22107
+ --grid-list-thumb-bg idle well background
22108
+ --grid-list-thumb-bg-tickled under the cursor
22109
+ --grid-list-thumb-bg-selected when the cell is selected
22110
+ --grid-list-thumb-bg-selected-tickled selected + under the cursor
22111
+ --grid-list-thumb-selection-wash colour of the box-mode wash (default: --accent)
22112
+ --grid-list-thumb-selection-wash-opacity strength of that wash (default: 0.18; set 0 to drop it) */
20684
22113
  .thumb {
20685
22114
  position: relative;
20686
22115
  flex: 1;
@@ -20690,7 +22119,7 @@ let GridListElement = class GridListElement extends i {
20690
22119
  box-sizing: border-box;
20691
22120
  overflow: hidden;
20692
22121
  border-radius: 0;
20693
- background: transparent;
22122
+ background: var(--grid-list-thumb-bg, transparent);
20694
22123
  border: 1px solid transparent;
20695
22124
  transition: background var(--transition), border-color var(--transition);
20696
22125
  }
@@ -20703,6 +22132,7 @@ let GridListElement = class GridListElement extends i {
20703
22132
  align-items: center;
20704
22133
  justify-content: center;
20705
22134
  overflow: hidden;
22135
+ draggable: false;
20706
22136
  }
20707
22137
  .thumb ::slotted(*:empty)::after {
20708
22138
  content: "Empty";
@@ -20714,19 +22144,27 @@ let GridListElement = class GridListElement extends i {
20714
22144
  }
20715
22145
 
20716
22146
  /* Selection / cursor wash — sits over the slotted content so it reads
20717
- on photos and arbitrary DOM alike. */
22147
+ on photos and arbitrary DOM alike. Set
22148
+ --grid-list-thumb-selection-wash-opacity: 0 to drop it in favour of a
22149
+ --grid-list-thumb-bg-selected tint. */
20718
22150
  .thumb::after {
20719
22151
  content: "";
20720
22152
  position: absolute;
20721
22153
  inset: 0;
20722
22154
  pointer-events: none;
20723
22155
  border-radius: inherit;
20724
- background: var(--accent);
22156
+ background: var(--grid-list-thumb-selection-wash, var(--accent));
20725
22157
  opacity: 0;
20726
22158
  transition: opacity var(--transition, 0.1s ease-out);
20727
22159
  }
20728
22160
  .cell.selected .thumb::after {
20729
- opacity: 0.18;
22161
+ opacity: var(--grid-list-thumb-selection-wash-opacity, 0.18);
22162
+ }
22163
+ :host(:not([caption-style="separate"])) .cell.selected .thumb {
22164
+ background: var(--grid-list-thumb-bg-selected, var(--grid-list-thumb-bg, transparent));
22165
+ }
22166
+ :host(:not([caption-style="separate"])) .cell.selected.tickled .thumb {
22167
+ background: var(--grid-list-thumb-bg-selected-tickled, var(--grid-list-thumb-bg-selected, var(--grid-list-thumb-bg, transparent)));
20730
22168
  }
20731
22169
 
20732
22170
  /* ---- Caption (base) ---- */
@@ -20744,6 +22182,18 @@ let GridListElement = class GridListElement extends i {
20744
22182
  }
20745
22183
  :host([caption-align="left"]) .caption { text-align: left; }
20746
22184
  :host([caption-align="right"]) .caption { text-align: right; }
22185
+ /* Tickled captions swap their plain label for an ixfx-pan-text in loop
22186
+ mode so a truncated label scrolls instead of staying an ellipsis. The
22187
+ pan fills the caption band (flex in 'separate', block otherwise) and
22188
+ inherits its alignment. */
22189
+ .caption ixfx-pan-text {
22190
+ display: block;
22191
+ flex: 1;
22192
+ min-width: 0;
22193
+ max-width: 100%;
22194
+ font: inherit;
22195
+ color: inherit;
22196
+ }
20747
22197
 
20748
22198
  /* ---- Box modes (hidden / under / overlaid): the cell is one box with a
20749
22199
  single selection outline ---- */
@@ -20753,8 +22203,8 @@ let GridListElement = class GridListElement extends i {
20753
22203
  overflow: hidden;
20754
22204
  transition: background var(--transition), border-color var(--transition);
20755
22205
  }
20756
- :host(:not([caption-style="separate"])) .cell:hover,
20757
- :host(:not([caption-style="separate"])) .cell.tickled {
22206
+ :host(:not([caption-style="separate"])) .cell.tickled,
22207
+ :host(:not([caption-style="separate"]):not([disable-hover-tickle])) .cell:hover {
20758
22208
  background: var(--item-bg-tickled);
20759
22209
  border: var(--item-border-tickled);
20760
22210
  }
@@ -20821,12 +22271,12 @@ let GridListElement = class GridListElement extends i {
20821
22271
  :host([caption-style="separate"]) .thumb {
20822
22272
  flex: none;
20823
22273
  border-radius: var(--grid-list-radius);
20824
- background: var(--grid-list-item-bg);
22274
+ background: var(--grid-list-thumb-bg, var(--grid-list-item-bg));
20825
22275
  border: 1px solid transparent;
20826
22276
  }
20827
- :host([caption-style="separate"]) .cell:hover .thumb,
20828
- :host([caption-style="separate"]) .cell.tickled .thumb {
20829
- background: var(--item-bg-tickled);
22277
+ :host([caption-style="separate"]) .cell.tickled .thumb,
22278
+ :host([caption-style="separate"]:not([disable-hover-tickle])) .cell:hover .thumb {
22279
+ background: var(--grid-list-thumb-bg-tickled, var(--item-bg-tickled));
20830
22280
  border: var(--item-border-tickled);
20831
22281
  }
20832
22282
  :host([caption-style="separate"]) .caption {
@@ -20839,14 +22289,17 @@ let GridListElement = class GridListElement extends i {
20839
22289
  }
20840
22290
  :host([caption-style="separate"][caption-align="left"]) .caption { align-self: flex-start; }
20841
22291
  :host([caption-style="separate"][caption-align="right"]) .caption { align-self: flex-end; }
22292
+ /* Inset the selection ring (negative offset) so it is never clipped by the
22293
+ scroll viewport's overflow when the cell sits against an edge. */
20842
22294
  :host([caption-style="separate"]) .cell.selected .thumb {
22295
+ background: var(--grid-list-thumb-bg-selected, var(--grid-list-thumb-bg, var(--grid-list-item-bg)));
20843
22296
  outline: 2px solid var(--accent);
20844
- outline-offset: 2px;
22297
+ outline-offset: -2px;
20845
22298
  }
20846
22299
  :host([caption-style="separate"]) .cell.selected.tickled .thumb {
20847
- background: var(--item-bg-selected-tickled);
22300
+ background: var(--grid-list-thumb-bg-selected-tickled, var(--item-bg-selected-tickled));
20848
22301
  outline: 2px dashed var(--surface-3);
20849
- outline-offset: 2px;
22302
+ outline-offset: -2px;
20850
22303
  }
20851
22304
  :host([caption-style="separate"]) .cell.selected .caption {
20852
22305
  background: var(--accent);
@@ -20884,6 +22337,26 @@ let GridListElement = class GridListElement extends i {
20884
22337
  :host([reflowing]) .cell { transition: background var(--transition), border-color var(--transition); }
20885
22338
  .cell.entering { animation: none; }
20886
22339
  }
22340
+
22341
+ /* Drag-select (marquee) rectangle + covered-cell preview. The preview
22342
+ reuses the selection wash so covered cells read as "about to be
22343
+ selected". Style hooks: part="marquee", --grid-list-marquee-border,
22344
+ --grid-list-marquee-fill. */
22345
+ .marquee {
22346
+ position: absolute;
22347
+ pointer-events: none;
22348
+ z-index: 5;
22349
+ border: 1px solid var(--grid-list-marquee-border, var(--accent));
22350
+ background: var(--grid-list-marquee-fill, color-mix(in srgb, var(--accent) 15%, transparent));
22351
+ }
22352
+ .cell.marquee .thumb::after {
22353
+ opacity: var(--grid-list-thumb-selection-wash-opacity, 0.18);
22354
+ }
22355
+
22356
+ /* Native item drag origin cell. */
22357
+ .cell.dragging-origin {
22358
+ opacity: 0.6;
22359
+ }
20887
22360
  `
20888
22361
  ];
20889
22362
  }
@@ -20916,6 +22389,62 @@ let GridListElement = class GridListElement extends i {
20916
22389
  */
20917
22390
  #sourceElement;
20918
22391
  #source;
22392
+ /** Live drag-select preview: covered keys + the surface-relative rect. */
22393
+ #marqueePreview;
22394
+ #marqueeRect;
22395
+ /** Native item drag state: true while a drag is active. */
22396
+ #nativeDragging;
22397
+ /** Key of the cell that originated the native drag. */
22398
+ #dragOriginKey;
22399
+ /** Timer handle for post-drag click suppression. */
22400
+ #postDragSuppressTimer;
22401
+ /**
22402
+ * The single caption pan-text currently scrolling. At most one caption is
22403
+ * a pan-text in loop mode at a time — when tickle moves, the old one is
22404
+ * stopped before the new one starts (see `#activateTickledPan`).
22405
+ */
22406
+ #activeCaptionPan;
22407
+ #dragSelect;
22408
+ /**
22409
+ * Map a surface-relative (viewport-visible) rect to covered item keys using
22410
+ * the same `computeGridWindow` geometry the renderer uses — pure arithmetic,
22411
+ * so virtualized off-screen items are covered without DOM measurement.
22412
+ */
22413
+ #marqueeHitTest(rect) {
22414
+ const count = this.#source.count;
22415
+ if (count === 0) return [];
22416
+ const horizontal = this.orientation === `row`;
22417
+ const win = computeGridWindow(count, this.itemSize, this.#rowHeight(), this.gap, Math.max(1, this._viewportWidth), Math.max(1, this._viewportHeight), horizontal ? this._scrollLeft : this._scrollTop, this.orientation, 0);
22418
+ const out = [];
22419
+ if (horizontal) {
22420
+ const x0 = rect.x + this._scrollLeft;
22421
+ const x1 = x0 + rect.width;
22422
+ const lo = Math.max(0, Math.floor(x0 / win.colStride));
22423
+ const hi = Math.min(count - 1, Math.floor(x1 / win.colStride));
22424
+ for (let i = lo; i <= hi; i++) out.push(i);
22425
+ } else {
22426
+ const x0 = rect.x + this._scrollLeft;
22427
+ const x1 = x0 + rect.width;
22428
+ const y0 = rect.y + this._scrollTop;
22429
+ const y1 = y0 + rect.height;
22430
+ const colLo = Math.max(0, Math.floor(x0 / win.colStride));
22431
+ const colHi = Math.min(win.cols - 1, Math.floor(x1 / win.colStride));
22432
+ const rowLo = Math.max(0, Math.floor(y0 / win.rowStride));
22433
+ const rowHi = Math.floor(y1 / win.rowStride);
22434
+ for (let row = rowLo; row <= rowHi; row++) for (let col = colLo; col <= colHi; col++) {
22435
+ const index = row * win.cols + col;
22436
+ if (index >= 0 && index < count) out.push(index);
22437
+ }
22438
+ }
22439
+ const keys = [];
22440
+ for (const index of out) {
22441
+ const key = this.#source.keyAt(index);
22442
+ if (!key || isSyntheticKey(key)) continue;
22443
+ if (!this.#source.isSelectable(index)) continue;
22444
+ keys.push(key);
22445
+ }
22446
+ return keys;
22447
+ }
20919
22448
  #searchMode;
20920
22449
  #searchFilterKeys;
20921
22450
  /** (Re)build {@link search} for the current mode; a no-op if it already matches. */
@@ -20988,6 +22517,7 @@ let GridListElement = class GridListElement extends i {
20988
22517
  }
20989
22518
  disconnectedCallback() {
20990
22519
  super.disconnectedCallback();
22520
+ this.#stopActiveCaptionPan();
20991
22521
  this.removeEventListener(`keydown`, this._onKeyDown);
20992
22522
  this.#resizeObserver?.disconnect();
20993
22523
  this.#resizeObserver = void 0;
@@ -21000,6 +22530,7 @@ let GridListElement = class GridListElement extends i {
21000
22530
  clearTimeout(this.#reflowTimer);
21001
22531
  this.#reflowTimer = 0;
21002
22532
  }
22533
+ if (this.#nativeDragging) this.#clearNativeDragState();
21003
22534
  }
21004
22535
  #onChildrenChanged(records) {
21005
22536
  const previousLength = this.#items.length;
@@ -21187,6 +22718,7 @@ let GridListElement = class GridListElement extends i {
21187
22718
  if (Math.abs(vp.scrollLeft - this._scrollLeft) > .5) vp.scrollLeft = this._scrollLeft;
21188
22719
  }
21189
22720
  this.#applyZoomAnchor();
22721
+ this.#activateTickledPan();
21190
22722
  }
21191
22723
  /** Re-anchor the scroll so the item captured in `willUpdate` stays centred. */
21192
22724
  #applyZoomAnchor() {
@@ -21517,15 +23049,19 @@ let GridListElement = class GridListElement extends i {
21517
23049
  element
21518
23050
  });
21519
23051
  }
21520
- _dispatch(name, detail) {
21521
- this.dispatchEvent(new CustomEvent(name, {
23052
+ _dispatch(name, detail, cancelable = false) {
23053
+ return this.dispatchEvent(new CustomEvent(name, {
21522
23054
  detail,
21523
23055
  bubbles: true,
21524
- composed: true
23056
+ composed: true,
23057
+ cancelable
21525
23058
  }));
21526
23059
  }
21527
23060
  /** Selected keys, snapshotted once per render so the cell renderers don't each rebuild the set. */
21528
23061
  #selectedSnapshot;
23062
+ #marqueeStyle(rect) {
23063
+ return `left:${rect.x}px;top:${rect.y}px;width:${Math.max(0, rect.width)}px;height:${Math.max(0, rect.height)}px`;
23064
+ }
21529
23065
  render() {
21530
23066
  const win = this.#currentWindow();
21531
23067
  this.#selectedSnapshot = this.selection.selectedKeys;
@@ -21542,6 +23078,7 @@ let GridListElement = class GridListElement extends i {
21542
23078
  <div class="spacer" style=${spacerStyle}>
21543
23079
  ${this.#source instanceof DataGridListSource ? this.#renderDataCells(win) : this.#renderElementCells(win)}
21544
23080
  </div>
23081
+ ${this.#marqueeRect ? b`<div class="marquee" part="marquee" style=${this.#marqueeStyle(this.#marqueeRect)}></div>` : ``}
21545
23082
  </div>
21546
23083
  ${this.search?.renderField()}
21547
23084
  `;
@@ -21553,7 +23090,10 @@ let GridListElement = class GridListElement extends i {
21553
23090
  #renderDataCell(index, win) {
21554
23091
  const key = this.#source.keyAt(index);
21555
23092
  const selected = this.#selectedSnapshot.has(key);
23093
+ const marquee = this.#marqueePreview.has(key);
21556
23094
  const tickled = this.tickled.isTickled(key);
23095
+ const selectable = this.#source.isSelectable(index);
23096
+ const draggable = this.itemsDraggable && selectable;
21557
23097
  const style = this.captionStyle;
21558
23098
  const separate = style === `separate`;
21559
23099
  const caption = style === `hidden` ? `` : this.#source.labelAt(index);
@@ -21564,24 +23104,69 @@ let GridListElement = class GridListElement extends i {
21564
23104
  const captionCss = separate ? `height:${Math.max(0, this.captionHeight - 4)}px` : style === `under` ? `height:${this.captionHeight}px` : ``;
21565
23105
  return b`
21566
23106
  <div
21567
- class="cell${selected ? ` selected` : ``}${tickled ? ` tickled` : ``}"
23107
+ class="cell${selected ? ` selected` : ``}${tickled ? ` tickled` : ``}${marquee ? ` marquee` : ``}${this.#nativeDragging && key === this.#dragOriginKey ? ` dragging-origin` : ``}"
21568
23108
  part="item${selected ? ` selected` : ``}${tickled ? ` tickled` : ``}"
21569
23109
  role="option"
21570
23110
  aria-selected=${selected ? `true` : `false`}
21571
23111
  style="width:${this.itemSize}px;height:${cellHeight}px;transform:translate(${x}px,${y}px)"
21572
23112
  data-index=${index}
21573
23113
  data-key=${key}
23114
+ draggable=${draggable ? `true` : `false`}
21574
23115
  ?data-selected=${selected}
21575
23116
  ?data-tickled=${tickled}
23117
+ ?data-marquee=${marquee}
21576
23118
  @click=${(e) => this.#onCellClick(e, key)}
21577
23119
  @dblclick=${() => this.#onCellActivate(key)}
21578
23120
  @pointerenter=${() => this.tickled.handlePointerEnter(key)}
23121
+ @dragstart=${(e) => this.#onDragStart(e, key)}
23122
+ @dragend=${() => this.#onDragEnd()}
21579
23123
  >
21580
23124
  <div class="content" part="content">${this.#source.renderCellContent(index)}</div>
21581
- ${showCaption ? b`<div class="caption" part="caption" style=${captionCss} title=${caption}>${caption}</div>` : ``}
23125
+ ${showCaption ? this.#renderCaption(caption, captionCss, tickled) : ``}
21582
23126
  </div>
21583
23127
  `;
21584
23128
  }
23129
+ /**
23130
+ * A tickled cell swaps its plain label for an `ixfx-pan-text` in loop mode
23131
+ * so a truncated label scrolls instead of staying an ellipsis. Unticked
23132
+ * cells keep the cheap plain-text caption.
23133
+ */
23134
+ #renderCaption(caption, captionCss, tickled) {
23135
+ if (tickled) return b`<div class="caption" part="caption" style=${captionCss} title=${caption}><ixfx-pan-text mode="loop">${caption}</ixfx-pan-text></div>`;
23136
+ return b`<div class="caption" part="caption" style=${captionCss} title=${caption}>${caption}</div>`;
23137
+ }
23138
+ /** Stop the active caption pan, if any, and forget it. */
23139
+ #stopActiveCaptionPan() {
23140
+ const active = this.#activeCaptionPan;
23141
+ this.#activeCaptionPan = null;
23142
+ try {
23143
+ active?.stopLoop?.();
23144
+ } catch {}
23145
+ }
23146
+ /**
23147
+ * Enforce a single scrolling caption: the tickled cell's pan-text loops,
23148
+ * every other pan-text is stopped. The pan-text also auto-starts when
23149
+ * `mode="loop"` is set; this covers the case where the element was already
23150
+ * connected before overflow was measured.
23151
+ */
23152
+ #activateTickledPan() {
23153
+ const tickledCell = this.renderRoot.querySelector(`.cell[data-tickled]`);
23154
+ const pans = [...this.renderRoot.querySelectorAll(`ixfx-pan-text[mode="loop"]`)];
23155
+ const current = tickledCell?.querySelector(`ixfx-pan-text[mode="loop"]`) ?? null;
23156
+ for (const pan of pans) if (pan !== current) try {
23157
+ pan.stopLoop?.();
23158
+ } catch {}
23159
+ const active = this.#activeCaptionPan;
23160
+ if (active && active !== current) try {
23161
+ active.stopLoop?.();
23162
+ } catch {}
23163
+ if (!current) {
23164
+ this.#activeCaptionPan = null;
23165
+ return;
23166
+ }
23167
+ this.#activeCaptionPan = current;
23168
+ current.startLoop?.();
23169
+ }
21585
23170
  #renderElementCells(win) {
21586
23171
  const slice = this.#items.slice(win.startIndex, win.endIndex);
21587
23172
  return repeat(slice, (_el, i) => this.#source.keyAt(win.startIndex + i), (el, i) => this.#renderCell(el, win.startIndex + i, win));
@@ -21589,8 +23174,11 @@ let GridListElement = class GridListElement extends i {
21589
23174
  #renderCell(el, index, win) {
21590
23175
  const key = this.#source.keyAt(index);
21591
23176
  const selected = this.#selectedSnapshot.has(key);
23177
+ const marquee = this.#marqueePreview.has(key);
21592
23178
  const tickled = this.tickled.isTickled(key);
21593
23179
  const entering = this.#entering.has(el);
23180
+ const selectable = this.#source.isSelectable(index);
23181
+ const draggable = this.itemsDraggable && selectable;
21594
23182
  const style = this.captionStyle;
21595
23183
  const separate = style === `separate`;
21596
23184
  const caption = style === `hidden` ? `` : this.#captionFor(el);
@@ -21602,23 +23190,27 @@ let GridListElement = class GridListElement extends i {
21602
23190
  const captionCss = separate ? `height:${Math.max(0, this.captionHeight - 4)}px` : style === `under` ? `height:${this.captionHeight}px` : ``;
21603
23191
  return b`
21604
23192
  <div
21605
- class="cell${selected ? ` selected` : ``}${tickled ? ` tickled` : ``}${entering ? ` entering` : ``}"
23193
+ class="cell${selected ? ` selected` : ``}${tickled ? ` tickled` : ``}${entering ? ` entering` : ``}${marquee ? ` marquee` : ``}${this.#nativeDragging && key === this.#dragOriginKey ? ` dragging-origin` : ``}"
21606
23194
  part="item${selected ? ` selected` : ``}${tickled ? ` tickled` : ``}"
21607
23195
  role="option"
21608
23196
  aria-selected=${selected ? `true` : `false`}
21609
23197
  style="width:${this.itemSize}px;height:${cellHeight}px;transform:translate(${x}px,${y}px)"
21610
23198
  data-index=${index}
21611
23199
  data-key=${key}
23200
+ draggable=${draggable ? `true` : `false`}
21612
23201
  ?data-selected=${selected}
21613
23202
  ?data-tickled=${tickled}
23203
+ ?data-marquee=${marquee}
21614
23204
  @click=${(e) => this.#onCellClick(e, key)}
21615
23205
  @dblclick=${() => this.#onCellActivate(key)}
21616
23206
  @pointerenter=${() => this.tickled.handlePointerEnter(key)}
23207
+ @dragstart=${(e) => this.#onDragStart(e, key)}
23208
+ @dragend=${() => this.#onDragEnd()}
21617
23209
  >
21618
23210
  <div class="thumb" part="thumb" style=${thumbStyle}>
21619
23211
  ${this.#source.renderCellContent(index)}
21620
23212
  </div>
21621
- ${showCaption ? b`<div class="caption" part="caption" style=${captionCss} title=${caption}>${caption}</div>` : ``}
23213
+ ${showCaption ? this.#renderCaption(caption, captionCss, tickled) : ``}
21622
23214
  </div>
21623
23215
  `;
21624
23216
  }
@@ -21630,9 +23222,24 @@ let GridListElement = class GridListElement extends i {
21630
23222
  }
21631
23223
  }
21632
23224
  #onCellClick(e, key) {
23225
+ if (this.#dragSelect.isDragging || this.#nativeDragging || this.#postDragSuppressTimer !== 0) return;
21633
23226
  this.selection.handleClick(key, e);
21634
23227
  this.#emitItemEvent(`list-item-click`, key);
21635
23228
  }
23229
+ #onDragStart;
23230
+ #onDragEnd;
23231
+ #startPostDragSuppressTimer() {
23232
+ if (this.#postDragSuppressTimer) clearTimeout(this.#postDragSuppressTimer);
23233
+ this.#postDragSuppressTimer = window.setTimeout(() => {
23234
+ this.#postDragSuppressTimer = 0;
23235
+ }, 0);
23236
+ }
23237
+ #clearNativeDragState() {
23238
+ this.#nativeDragging = false;
23239
+ this.#dragOriginKey = void 0;
23240
+ this.toggleAttribute(`data-native-dragging`, false);
23241
+ this.removeAttribute(`aria-label`);
23242
+ }
21636
23243
  #onCellActivate(key) {
21637
23244
  if (this.interactionMode !== `manual`) this.selection.replace(key);
21638
23245
  this.#emitItemEvent(`list-activate`, key);
@@ -21646,6 +23253,11 @@ __decorate([property({
21646
23253
  type: String,
21647
23254
  attribute: `interaction-mode`
21648
23255
  })], GridListElement.prototype, "interactionMode", void 0);
23256
+ __decorate([property({
23257
+ type: Boolean,
23258
+ attribute: `disable-hover-tickle`,
23259
+ reflect: true
23260
+ })], GridListElement.prototype, "disableHoverTickle", void 0);
21649
23261
  __decorate([property({
21650
23262
  type: Number,
21651
23263
  attribute: `item-size`
@@ -21685,6 +23297,11 @@ __decorate([property({
21685
23297
  })], GridListElement.prototype, "itemWrap", void 0);
21686
23298
  __decorate([property({ attribute: false })], GridListElement.prototype, "filterPredicate", void 0);
21687
23299
  __decorate([property({ attribute: false })], GridListElement.prototype, "itemFilterPredicate", void 0);
23300
+ __decorate([property({
23301
+ type: Boolean,
23302
+ attribute: `items-draggable`,
23303
+ reflect: true
23304
+ })], GridListElement.prototype, "itemsDraggable", void 0);
21688
23305
  __decorate([property({
21689
23306
  type: String,
21690
23307
  attribute: `search-modes`,
@@ -23571,7 +25188,29 @@ let VerticalListElement = class VerticalListElement extends i {
23571
25188
  if (id !== void 0) this._dispatch(`list-tickle`, { item: id });
23572
25189
  },
23573
25190
  onStateChange: () => this._syncItemStates(),
23574
- itemWrap: this.itemWrap
25191
+ itemWrap: this.itemWrap,
25192
+ hoverDisabled: () => this.#dragSelect.isDragging
25193
+ });
25194
+ this.#dragSelect = new ListDragSelectController(this, {
25195
+ getSurface: () => this.renderRoot.querySelector(`.container`),
25196
+ isEnabled: () => this.selection.canDragSelect() && this._getVisibleItems().length > 0 && !this.hasAttribute(`disable-drag-select`),
25197
+ hitTest: (rect) => this.#marqueeHitTest(rect),
25198
+ getBaseSelection: () => this.selection.selected,
25199
+ onPreview: (preview, rect) => {
25200
+ this.#marqueeRect = rect;
25201
+ for (const el of this._getVisibleItems()) el.toggleAttribute(`data-marquee`, preview.has(el));
25202
+ this.requestUpdate();
25203
+ },
25204
+ onCommit: (finalSet) => {
25205
+ this.selection.applyDragSelection(finalSet);
25206
+ for (const el of this._getVisibleItems()) el.removeAttribute(`data-marquee`);
25207
+ this.#marqueeRect = void 0;
25208
+ this.requestUpdate();
25209
+ },
25210
+ autoScroll: (_dx, dy) => {
25211
+ const list = this.renderRoot.querySelector(`ul`);
25212
+ if (list) list.scrollTop += dy;
25213
+ }
23575
25214
  });
23576
25215
  this.search = new ListSearchController(this, { adapter: createElementListSearchAdapter(this, {
23577
25216
  itemSelector: ITEM_SELECTOR,
@@ -23632,6 +25271,7 @@ let VerticalListElement = class VerticalListElement extends i {
23632
25271
  }
23633
25272
  };
23634
25273
  this._onClick = (e) => {
25274
+ if (this.#dragSelect.isDragging) return;
23635
25275
  const item = this._findListItem(e.composedPath());
23636
25276
  if (!item) return;
23637
25277
  this._dispatch(`list-item-click`, { item });
@@ -23648,6 +25288,30 @@ let VerticalListElement = class VerticalListElement extends i {
23648
25288
  #itemObserver;
23649
25289
  /** Items that have already played (or been skipped for) their enter animation. */
23650
25290
  #animatedItems;
25291
+ /** Live drag-select preview rect (surface-relative); the covered items carry `data-marquee`. */
25292
+ #marqueeRect;
25293
+ #dragSelect;
25294
+ /** Collect visible items whose surface-relative box intersects `rect`. */
25295
+ #marqueeHitTest(rect) {
25296
+ const surface = this.renderRoot.querySelector(`.container`);
25297
+ if (!surface) return [];
25298
+ const box = surface.getBoundingClientRect();
25299
+ const rx0 = rect.x;
25300
+ const ry0 = rect.y;
25301
+ const rx1 = rect.x + rect.width;
25302
+ const ry1 = rect.y + rect.height;
25303
+ const out = [];
25304
+ for (const el of this._getVisibleItems()) {
25305
+ const r = el.getBoundingClientRect();
25306
+ const left = r.left - box.left;
25307
+ const top = r.top - box.top;
25308
+ if (left <= rx1 && left + r.width >= rx0 && top <= ry1 && top + r.height >= ry0) out.push(el);
25309
+ }
25310
+ return out;
25311
+ }
25312
+ #marqueeStyle(rect) {
25313
+ return `left:${rect.x}px;top:${rect.y}px;width:${Math.max(0, rect.width)}px;height:${Math.max(0, rect.height)}px`;
25314
+ }
23651
25315
  #searchRegistry;
23652
25316
  #unregisterSearchCommands;
23653
25317
  /** Command registry carrying this list's `list.search.*` commands. */
@@ -24118,6 +25782,22 @@ let VerticalListElement = class VerticalListElement extends i {
24118
25782
  outline-offset: -2px;
24119
25783
  }
24120
25784
 
25785
+ /* Drag-select (marquee) rectangle + covered-row preview.
25786
+ Style hooks: part="marquee", --vertical-list-marquee-border,
25787
+ --vertical-list-marquee-fill. */
25788
+ .marquee {
25789
+ position: absolute;
25790
+ pointer-events: none;
25791
+ z-index: 5;
25792
+ border: 1px solid var(--vertical-list-marquee-border, var(--accent));
25793
+ background: var(--vertical-list-marquee-fill, color-mix(in srgb, var(--accent) 15%, transparent));
25794
+ }
25795
+
25796
+ ::slotted([data-marquee]) {
25797
+ background: var(--item-bg-selected);
25798
+ color: var(--item-text-selected);
25799
+ }
25800
+
24121
25801
  /* checked mode: checkbox (injected into li) communicates selection — suppress bg highlight */
24122
25802
  :host([interaction-mode="checked"]) ::slotted([data-selected]) {
24123
25803
  background: transparent;
@@ -24139,6 +25819,7 @@ let VerticalListElement = class VerticalListElement extends i {
24139
25819
  <ul part="list">
24140
25820
  <slot></slot>
24141
25821
  </ul>
25822
+ ${this.#marqueeRect ? b`<div class="marquee" part="marquee" style=${this.#marqueeStyle(this.#marqueeRect)}></div>` : ``}
24142
25823
  ${this.search.renderField()}
24143
25824
  </div>
24144
25825
  `;
@@ -30832,136 +32513,6 @@ function dismiss(id) {
30832
32513
  syncRenderer();
30833
32514
  }
30834
32515
  //#endregion
30835
- //#region src/pan-text/pan-text.ts
30836
- const SPEED = 45;
30837
- const END_PADDING = 4;
30838
- const START_DELAY = 300;
30839
- const PAN_EASING = `cubic-bezier(0.25, 0.46, 0.45, 0.94)`;
30840
- let PanTextElement = class PanTextElement extends i {
30841
- constructor(..._args) {
30842
- super(..._args);
30843
- this.direction = `end`;
30844
- this.disabled = false;
30845
- this._isOverflowing = false;
30846
- this._isHovering = false;
30847
- this._overflow = 0;
30848
- this._resizeObserver = null;
30849
- this._onMouseEnter = () => {
30850
- this._isHovering = true;
30851
- };
30852
- this._onMouseLeave = () => {
30853
- this._isHovering = false;
30854
- };
30855
- this._measure = () => {
30856
- if (this._isHovering) return;
30857
- const el = this._textEl;
30858
- if (!el) return;
30859
- this._overflow = Math.max(0, el.scrollWidth - el.clientWidth);
30860
- const overflowing = this._overflow > 0;
30861
- this.classList.toggle(`is-overflowing`, overflowing);
30862
- if (overflowing !== this._isOverflowing) this._isOverflowing = overflowing;
30863
- };
30864
- }
30865
- connectedCallback() {
30866
- super.connectedCallback();
30867
- this._resizeObserver = new ResizeObserver(() => this._measure());
30868
- this.addEventListener(`mouseenter`, this._onMouseEnter);
30869
- this.addEventListener(`mouseleave`, this._onMouseLeave);
30870
- }
30871
- disconnectedCallback() {
30872
- super.disconnectedCallback();
30873
- this._resizeObserver?.disconnect();
30874
- this._resizeObserver = null;
30875
- this.removeEventListener(`mouseenter`, this._onMouseEnter);
30876
- this.removeEventListener(`mouseleave`, this._onMouseLeave);
30877
- }
30878
- firstUpdated() {
30879
- this._measure();
30880
- this._resizeObserver?.observe(this);
30881
- document.fonts?.ready.then(() => this._measure());
30882
- }
30883
- updated() {
30884
- this._measure();
30885
- }
30886
- _textStyle() {
30887
- if (!this._isOverflowing) return ``;
30888
- const distance = this._overflow + END_PADDING;
30889
- const duration = Math.max(1.5, distance / SPEED);
30890
- const revealEnd = this.direction === `end`;
30891
- const restX = revealEnd ? 0 : -distance;
30892
- const hoverX = revealEnd ? -distance : 0;
30893
- if (this._isHovering) return [
30894
- `transform: translateX(${hoverX}px)`,
30895
- `transition: transform ${duration}s ${PAN_EASING} ${START_DELAY}ms`,
30896
- `overflow: visible`,
30897
- `text-overflow: clip`
30898
- ].join(`; `);
30899
- return [
30900
- `transform: translateX(${restX}px)`,
30901
- `transition: transform 400ms ${PAN_EASING}`,
30902
- revealEnd ? `` : `overflow: visible; text-overflow: clip`
30903
- ].filter(Boolean).join(`; `);
30904
- }
30905
- render() {
30906
- const textClasses = {
30907
- "is-overflowing": this._isOverflowing,
30908
- "is-hovering": this._isHovering
30909
- };
30910
- return b`
30911
- <span class="text ${classMap(textClasses)}" part="text" style=${this._textStyle()}>
30912
- <slot @slotchange=${this._measure}></slot>
30913
- </span>
30914
- `;
30915
- }
30916
- static {
30917
- this.styles = [themeFallbacks, i$3`
30918
- /* Renders like plain inline text: no colours, border, padding or margin
30919
- of its own — it only adds the clipping needed to pan on hover. */
30920
- :host {
30921
- display: inline-block;
30922
- overflow: hidden;
30923
- white-space: nowrap;
30924
- max-width: 100%;
30925
- box-sizing: border-box;
30926
- font: inherit;
30927
- color: inherit;
30928
- }
30929
-
30930
- :host([disabled]) {
30931
- opacity: var(--disabled-opacity, 0.5);
30932
- pointer-events: none;
30933
- }
30934
-
30935
- .text {
30936
- display: block;
30937
- overflow: hidden;
30938
- text-overflow: ellipsis;
30939
- white-space: nowrap;
30940
- will-change: transform;
30941
- }
30942
-
30943
- @media (prefers-reduced-motion: reduce) {
30944
- .text {
30945
- transition: none !important;
30946
- transform: none !important;
30947
- }
30948
- }
30949
- `];
30950
- }
30951
- };
30952
- __decorate([property({
30953
- type: String,
30954
- attribute: `direction`
30955
- })], PanTextElement.prototype, "direction", void 0);
30956
- __decorate([property({
30957
- type: Boolean,
30958
- attribute: `disabled`
30959
- })], PanTextElement.prototype, "disabled", void 0);
30960
- __decorate([state()], PanTextElement.prototype, "_isOverflowing", void 0);
30961
- __decorate([state()], PanTextElement.prototype, "_isHovering", void 0);
30962
- __decorate([query(`.text`)], PanTextElement.prototype, "_textEl", void 0);
30963
- PanTextElement = __decorate([safeCustomElement(`ixfx-pan-text`)], PanTextElement);
30964
- //#endregion
30965
32516
  //#region src/panel/panel-group.ts
30966
32517
  /** Pointer movement threshold (px) before a press becomes a drag. */
30967
32518
  const DRAG_THRESHOLD_PX = 5;
@@ -32115,6 +33666,7 @@ let PlotHistogram = class PlotHistogram extends i {
32115
33666
  this.#onPointerMove = (e) => {
32116
33667
  if (this.interactionMode === `select` && this._isDragging) {
32117
33668
  const index = this.#bucketIndexFromPosition(e.clientX, e.clientY);
33669
+ if (index === this._selectionEnd) return;
32118
33670
  this._selectionEnd = index;
32119
33671
  const start = Math.min(this._selectionStart, this._selectionEnd);
32120
33672
  const end = Math.max(this._selectionStart, this._selectionEnd);
@@ -32126,6 +33678,7 @@ let PlotHistogram = class PlotHistogram extends i {
32126
33678
  this._selectionOverlayText = `Select: ${startBar.valueMin.toFixed(2)} – ${endBar.valueMax.toFixed(2)}`;
32127
33679
  }
32128
33680
  this.requestUpdate();
33681
+ this.#emitSelectionChange(false);
32129
33682
  return;
32130
33683
  }
32131
33684
  if (this.interactionMode === `select`) return;
@@ -32141,6 +33694,7 @@ let PlotHistogram = class PlotHistogram extends i {
32141
33694
  this._isDragging = false;
32142
33695
  this._selectionOverlayText = null;
32143
33696
  this.requestUpdate();
33697
+ this.#emitSelectionChange(true);
32144
33698
  }
32145
33699
  return;
32146
33700
  }
@@ -32160,9 +33714,13 @@ let PlotHistogram = class PlotHistogram extends i {
32160
33714
  this.setColoredRange(startBar.valueMin, endBar.valueMax);
32161
33715
  this._selectionOverlayText = `Select: ${startBar.valueMin.toFixed(2)} – ${endBar.valueMax.toFixed(2)}`;
32162
33716
  this.requestUpdate();
33717
+ this.#emitSelectionChange(false);
32163
33718
  };
32164
33719
  this.#onPointerUp = () => {
32165
- if (this.interactionMode === `select`) this._isDragging = false;
33720
+ if (this.interactionMode === `select` && this._isDragging) {
33721
+ this._isDragging = false;
33722
+ this.#emitSelectionChange(true);
33723
+ }
32166
33724
  };
32167
33725
  }
32168
33726
  #series;
@@ -32211,11 +33769,14 @@ let PlotHistogram = class PlotHistogram extends i {
32211
33769
  this.setColoredRange(startBar.valueMin, endBar.valueMax);
32212
33770
  }
32213
33771
  this.requestUpdate();
33772
+ this.#emitSelectionChange(true);
32214
33773
  }
32215
33774
  clearSelection() {
33775
+ const had = this._selectionStart !== null || this._selectionEnd !== null;
32216
33776
  this._selectionStart = null;
32217
33777
  this._selectionEnd = null;
32218
33778
  this.requestUpdate();
33779
+ if (had) this.#emitSelectionChange(true);
32219
33780
  }
32220
33781
  getSelectedRange() {
32221
33782
  if (this._selectionStart === null || this._selectionEnd === null) return {
@@ -32300,6 +33861,21 @@ let PlotHistogram = class PlotHistogram extends i {
32300
33861
  composed: true
32301
33862
  }));
32302
33863
  }
33864
+ /**
33865
+ * Fire `plot-selection-change` with the current bar-range selection (indices +
33866
+ * data span). `committed` is `false` mid-drag, `true` on release / programmatic
33867
+ * change.
33868
+ */
33869
+ #emitSelectionChange(committed) {
33870
+ this.dispatchEvent(new CustomEvent(`plot-selection-change`, {
33871
+ detail: {
33872
+ ...this.getSelectedRange(),
33873
+ committed
33874
+ },
33875
+ bubbles: true,
33876
+ composed: true
33877
+ }));
33878
+ }
32303
33879
  /** Bucket layout + counts, shared by render() and pointer hit-testing. */
32304
33880
  #computeGeometry() {
32305
33881
  const { values, range } = this.#series;
@@ -32387,7 +33963,7 @@ let PlotHistogram = class PlotHistogram extends i {
32387
33963
  }
32388
33964
  /** Gets bucket index from position, clamping to valid range (0 to bucketCount-1). */
32389
33965
  #bucketIndexFromPosition(clientX, clientY) {
32390
- const { w, h, isHorizontal, padding, dataMin, dataSpan, bucketCount, availableMain } = this.#computeGeometry();
33966
+ const { isHorizontal, padding, bucketCount, availableMain } = this.#computeGeometry();
32391
33967
  const rect = this.getBoundingClientRect();
32392
33968
  const x = clientX - rect.left;
32393
33969
  const y = clientY - rect.top;
@@ -38173,7 +39749,10 @@ let TimelineTrackTabElement = class TimelineTrackTabElement extends i {
38173
39749
  };
38174
39750
  }
38175
39751
  static {
38176
- this.styles = i$3`
39752
+ this.styles = [
39753
+ themeFallbacks,
39754
+ handleStyles,
39755
+ i$3`
38177
39756
  :host {
38178
39757
  display: flex;
38179
39758
  align-items: center;
@@ -38200,13 +39779,13 @@ let TimelineTrackTabElement = class TimelineTrackTabElement extends i {
38200
39779
  .handle {
38201
39780
  display: none;
38202
39781
  cursor: grab;
38203
- padding: 0 4px;
38204
- opacity: 0.5;
38205
- font-size: 0.65rem;
39782
+ margin: 0 4px;
39783
+ --ixfx-handle-spot-size: 12px;
39784
+ --ixfx-handle-dot-size: 3px;
38206
39785
  }
38207
39786
 
38208
39787
  :host([reorderable]) .handle {
38209
- display: block;
39788
+ display: inline-block;
38210
39789
  }
38211
39790
 
38212
39791
  .label {
@@ -38215,7 +39794,8 @@ let TimelineTrackTabElement = class TimelineTrackTabElement extends i {
38215
39794
  text-overflow: ellipsis;
38216
39795
  padding: 0 8px;
38217
39796
  }
38218
- `;
39797
+ `
39798
+ ];
38219
39799
  }
38220
39800
  #dragging;
38221
39801
  #startScreenY;
@@ -38240,7 +39820,7 @@ let TimelineTrackTabElement = class TimelineTrackTabElement extends i {
38240
39820
  }
38241
39821
  render() {
38242
39822
  return b`
38243
- <span class="handle">\u2630</span>
39823
+ <span class="handle ixfx-handle" data-handle="dotted" data-shape="spot" part="handle"></span>
38244
39824
  <span class="label">${this.label}</span>
38245
39825
  `;
38246
39826
  }
@@ -40117,29 +41697,29 @@ let TwoSplitLayoutElement = class TwoSplitLayoutElement extends i {
40117
41697
  <slot style=${sizeA} name="a"></slot>
40118
41698
  ${this._hasCorner ? b`<div class="corner-zone" @pointerdown=${this.#onCornerResizeDown}></div>` : ``}
40119
41699
  <div @pointerdown=${this.#onResizeHandleDown} class="split-bar">
40120
- ${this.showGrabHandle ? b`
40121
- <div class="grab-handle">
40122
- <span class="dot"></span>
40123
- <span class="dot"></span>
40124
- <span class="dot"></span>
40125
- </div>
40126
- ` : ``}
41700
+ ${this.showGrabHandle ? b`<span
41701
+ class="ixfx-handle"
41702
+ part="grip"
41703
+ data-handle="dimpled"
41704
+ data-shape=${this.layout === `horizontal` ? `horizontal` : `vertical`}
41705
+ ></span>` : ``}
40127
41706
  </div>
40128
41707
  <slot style=${sizeB} name="b"></slot>
40129
41708
  </div>`;
40130
41709
  }
40131
41710
  static {
40132
- this.styles = [themeFallbacks, i$3`
41711
+ this.styles = [
41712
+ themeFallbacks,
41713
+ handleStyles,
41714
+ i$3`
40133
41715
  :host {
40134
41716
  --handle-size: 2px;
40135
41717
  --handle-colour: transparent;
40136
41718
  --handle-hover-colour: var(--surface-6);
40137
41719
  --handle-active-colour: var(--accent);
40138
- --handle-bg: var(--handle-colour);
40139
- --handle-grab-colour: var(--border-subtle-color);
40140
- --handle-grab-dot-size: 3px;
40141
- --handle-grab-size: 16px;
40142
- --handle-grab-thickness: 8px;
41720
+ /* Grip — shared handle primitive (docs/infra-handle.md). */
41721
+ --ixfx-handle-length: 16px;
41722
+ --ixfx-handle-thickness: 8px;
40143
41723
 
40144
41724
  display: block;
40145
41725
  box-sizing: border-box;
@@ -40182,40 +41762,14 @@ let TwoSplitLayoutElement = class TwoSplitLayoutElement extends i {
40182
41762
  to { background-color: var(--handle-hover-colour); }
40183
41763
  }
40184
41764
 
40185
- .grab-handle {
41765
+ .split-bar .ixfx-handle {
40186
41766
  position: absolute;
40187
41767
  top: 50%;
40188
41768
  left: 50%;
40189
41769
  transform: translate(-50%, -50%);
40190
- display: flex;
40191
- align-items: center;
40192
- justify-content: center;
40193
- gap: var(--handle-grab-dot-size);
40194
- background: var(--handle-bg);
40195
- border-radius: var(--radius-s);
40196
- }
40197
-
40198
- .grab-handle .dot {
40199
- width: var(--handle-grab-dot-size);
40200
- height: var(--handle-grab-dot-size);
40201
- border-radius: 50%;
40202
- background: var(--handle-grab-colour);
40203
- flex-shrink: 0;
40204
- }
40205
-
40206
- :host([layout="vertical"]) .grab-handle {
40207
- flex-direction: column;
40208
- width: var(--handle-grab-thickness);
40209
- height: var(--handle-grab-size);
40210
- }
40211
-
40212
- :host([layout="horizontal"]) .grab-handle {
40213
- flex-direction: row;
40214
- width: var(--handle-grab-size);
40215
- height: var(--handle-grab-thickness);
40216
41770
  }
40217
41771
 
40218
- #zone.user-resizing .grab-handle {
41772
+ #zone.user-resizing .ixfx-handle {
40219
41773
  display: none;
40220
41774
  }
40221
41775
  #zone.user-resizing {
@@ -40268,7 +41822,8 @@ let TwoSplitLayoutElement = class TwoSplitLayoutElement extends i {
40268
41822
  :host([data-collapsed-b]) slot[name="b"] {
40269
41823
  display: none;
40270
41824
  }
40271
- `];
41825
+ `
41826
+ ];
40272
41827
  }
40273
41828
  };
40274
41829
  __decorate([property({ type: String })], TwoSplitLayoutElement.prototype, "layout", void 0);
@@ -40820,6 +42375,6 @@ function init() {
40820
42375
  console.log(`Init`);
40821
42376
  }
40822
42377
  //#endregion
40823
- export { AcTextElement, AcTokenElement, ButtonElement, Checkbox, ColourPicker, ColourPickerPopup, CrumbNavigationElement, CrumbPathController, CrumbPathElement, DataController, DataDisplayComponent, DataGridElement, DataTableElement, DetailListElement, DockHostElement, DockPanelElement, EditableLabel, EditableNumber, FormElement, FormGroupElement, FormPartElement, GridListElement, GroupedItemListerElement, ICON_CARET_RIGHT, ICON_CHECK, ICON_CHEVRON_DOWN, ICON_CLOSE, IncrSearchTreeController as IncrSearchMillerController, IncrSearchTreeController, IxfxHexEditorElement, IxfxIconElement, IxfxTimelineElement, KeyedElementIndex, LIST_ITEM_MOTION_MS, LIST_SEARCH_COMMAND_IDS, LIST_SEARCH_MODES, LabelledRadialInput, LabelledRangeInput, LabelledSliderInput, LedElement, ListSearchController, ListSelectionController, menu_exports as Menu, MillerBaseElement, MillerListElement, MillerTreeController, NarrowedTextElement, NotificationManager, NotificationPillElement, PanTextElement, PanelElement, PanelGroupElement, PlotDataSeries, PlotHistogram, PlotMultiAxis, PlotSingleAxis, PlotXyAxis, PolarPad, RadialInputElement, RadioGroup, RangeElement, RangeInput, RangeMultiElement, RatingElement, SelectHorizElement, SliderInputElement, SlidingNumberElement, SnackbarElement, SnapContainer, SplitButton, SplitLayoutElement, SplitPanelElement, SwipeElement, TabController, TabListElement, TabListItemElement, TabPanelElement, TabPanelsElement, TextShimmerElement, TickledController, TimelineController, TimelineTrackTabElement, TitlebarElement, ToolbarElement, ToolbarGroupElement, TooltipElement, TransitoryLabelElement, TreeBaseElement, TreeController, TreeDataModel, TreeKeyboardNavController, TreeListElement, TwoSplitLayoutElement, VerticalListElement, XyPad, addPanelToStack, animateListItemIn, animateListItemOut, applyAgeModification, applyHighlights, applySnapshot, attachTimelineInteractions, buildHighlightTargets, buildModel, clearHighlights, collectContainerNames, collectDataResult, collectPanelIds, collectStackPaths, computeColour, computeLeafGeoms, createAcCompletionProvider, createDataListSearchAdapter, createElementIncrSearch, createElementListSearchAdapter, createEmptyLayout, createEmptyStack, createFuzzySearch, createIncrSearch, createPillRenderer, createSplit, createStack, createTreeListAdapter, createTreeListSearchAdapter, createVerticalListAdapter, distribute, extractStack, findNode, findParentOf, findStackOf, findStackPathByName, getActiveDragPanelId, getIcon, getIconEntry, getIconFont, getLeafNeighbours, hasIcon, iconBus, init, insertBeside, isDragging, isMultiMode, isSplit, isStack, keyForElement, listItemFadeKeyframes, listSearchStyles, monitorIndeterminate, moveLeafNodeToEdge, moveLeafStep, normalise, normalize, panelGapIndex, parseColourScaleAttr, parseLayout, pointerOverAnyHost, prefersReducedMotion, readBaseColour, registerFontIcon, registerIcon, registerListSearchCommands, removePanelFromStack, resolveCssVars, resolveGrid, resolveLeafPath, resolveTracks, serializeLayout, setActive, setIconFont, svgToDataUri, swapLeaves, tickledItemStyles, tickledStyles, timelineStyles, toSnapshot, walk, wrapRoot };
42378
+ export { AcTextElement, AcTokenElement, BottomDrawerElement, ButtonElement, Checkbox, ColourPicker, ColourPickerPopup, CrumbNavigationElement, CrumbPathController, CrumbPathElement, DataController, DataDisplayComponent, DataGridElement, DataTableElement, DetailListElement, DockHostElement, DockPanelElement, EditableLabel, EditableNumber, FormElement, FormGroupElement, FormPartElement, GridListElement, GroupedItemListerElement, ICON_CARET_RIGHT, ICON_CHECK, ICON_CHEVRON_DOWN, ICON_CLOSE, IncrSearchTreeController as IncrSearchMillerController, IncrSearchTreeController, IxfxHexEditorElement, IxfxIconElement, IxfxTimelineElement, KeyedElementIndex, LIST_ITEM_MOTION_MS, LIST_SEARCH_COMMAND_IDS, LIST_SEARCH_MODES, LabelledRadialInput, LabelledRangeInput, LabelledSliderInput, LedElement, ListDragSelectController, ListSearchController, ListSelectionController, menu_exports as Menu, MillerBaseElement, MillerListElement, MillerTreeController, NarrowedTextElement, NotificationManager, NotificationPillElement, PanTextElement, PanelElement, PanelGroupElement, PlotDataSeries, PlotHistogram, PlotMultiAxis, PlotSingleAxis, PlotXyAxis, PolarPad, RadialInputElement, RadioGroup, RangeElement, RangeInput, RangeMultiElement, RatingElement, SelectHorizElement, SliderInputElement, SlidingNumberElement, SnackbarElement, SnapContainer, SplitButton, SplitLayoutElement, SplitPanelElement, SwipeElement, TabController, TabListElement, TabListItemElement, TabPanelElement, TabPanelsElement, TextShimmerElement, TickledController, TimelineController, TimelineTrackTabElement, TitlebarElement, ToolbarElement, ToolbarGroupElement, TooltipElement, TransitoryLabelElement, TreeBaseElement, TreeController, TreeDataModel, TreeKeyboardNavController, TreeListElement, TwoSplitLayoutElement, VerticalListElement, XyPad, addPanelToStack, animateListItemIn, animateListItemOut, applyAgeModification, applyHighlights, applySnapshot, attachTimelineInteractions, bottomDrawerCommandId, buildHighlightTargets, buildModel, clearHighlights, collectContainerNames, collectDataResult, collectPanelIds, collectStackPaths, computeColour, computeLeafGeoms, createAcCompletionProvider, createDataListSearchAdapter, createElementIncrSearch, createElementListSearchAdapter, createEmptyLayout, createEmptyStack, createFuzzySearch, createIncrSearch, createPillRenderer, createSplit, createStack, createTreeListAdapter, createTreeListSearchAdapter, createVerticalListAdapter, distribute, extractStack, findNode, findParentOf, findStackOf, findStackPathByName, getActiveDragPanelId, getIcon, getIconEntry, getIconFont, getLeafNeighbours, hasIcon, iconBus, init, insertBeside, isDragging, isMultiMode, isSplit, isStack, keyForElement, listItemFadeKeyframes, listSearchStyles, monitorIndeterminate, moveLeafNodeToEdge, moveLeafStep, normalise, normalize, panelGapIndex, parseColourScaleAttr, parseLayout, pointerOverAnyHost, prefersReducedMotion, readBaseColour, registerFontIcon, registerIcon, registerListSearchCommands, removePanelFromStack, resolveCssVars, resolveGrid, resolveLeafPath, resolveTracks, serializeLayout, setActive, setIconFont, svgToDataUri, swapLeaves, tickledItemStyles, tickledStyles, timelineStyles, toSnapshot, walk, wrapRoot };
40824
42379
 
40825
42380
  //# sourceMappingURL=index.js.map