flexdesk 0.17.15 → 0.17.16

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 (42) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/css/base.css +224 -4
  3. package/css/flexdesk.css +314 -12
  4. package/dist/charts.js +104 -34
  5. package/dist/charts.js.map +3 -3
  6. package/dist/{chunk-2PM6ZMNS.js → chunk-2Z2ENHR3.js} +2 -2
  7. package/dist/{chunk-AD7FIA3Z.js → chunk-7JI7HHSY.js} +2 -2
  8. package/dist/{chunk-TLIV6QSU.js → chunk-7OL6F2AM.js} +3 -3
  9. package/dist/{chunk-MADMGRX2.js → chunk-PGZ4EHSE.js} +156 -9
  10. package/dist/chunk-PGZ4EHSE.js.map +7 -0
  11. package/dist/{chunk-OEG6NPAN.js → chunk-XJNADRGR.js} +2 -2
  12. package/dist/flexdesk.css +314 -12
  13. package/dist/flow.js +3 -3
  14. package/dist/tiles.js +84 -9
  15. package/dist/tiles.js.map +2 -2
  16. package/dist/widgets.js +448 -5
  17. package/dist/widgets.js.map +4 -4
  18. package/dist/wm.js +102 -14
  19. package/dist/wm.js.map +2 -2
  20. package/docs/README.md +1 -1
  21. package/docs/components/charts.md +70 -3
  22. package/docs/components/forms-and-dialogs.md +61 -0
  23. package/docs/components/menus.md +85 -1
  24. package/docs/components/shell.md +37 -0
  25. package/docs/components/tiles.md +37 -0
  26. package/package.json +1 -1
  27. package/src/charts/plot_config_editor.js +125 -29
  28. package/src/tiles/tile_base.js +48 -2
  29. package/src/tiles/tile_grid.js +42 -1
  30. package/src/tiling/kind_taxonomy.js +45 -0
  31. package/src/tiling/wm.js +86 -11
  32. package/src/ui/components/category_picker.js +556 -0
  33. package/src/ui/components/computing_status_window.js +3 -0
  34. package/src/ui/components/confirm_dialog.js +3 -0
  35. package/src/ui/components/managed_window.js +204 -9
  36. package/src/ui/components/wizard.js +4 -1
  37. package/widgets.js +2 -1
  38. package/dist/chunk-MADMGRX2.js.map +0 -7
  39. /package/dist/{chunk-2PM6ZMNS.js.map → chunk-2Z2ENHR3.js.map} +0 -0
  40. /package/dist/{chunk-AD7FIA3Z.js.map → chunk-7JI7HHSY.js.map} +0 -0
  41. /package/dist/{chunk-TLIV6QSU.js.map → chunk-7OL6F2AM.js.map} +0 -0
  42. /package/dist/{chunk-OEG6NPAN.js.map → chunk-XJNADRGR.js.map} +0 -0
package/dist/flexdesk.css CHANGED
@@ -3869,20 +3869,25 @@ body.twm-window-maximized .twm-resize-handle {
3869
3869
  }
3870
3870
 
3871
3871
  /* Selected — set by `_onConfigClick()` while the config dialog is open, and
3872
- cleared from every sibling by the same handler. It reads `--accent-lighter`
3872
+ cleared from every sibling by the same handler. `--configuring` is the same
3873
+ look set by the embedder through `setConfiguring()` (0.17.16), one per open
3874
+ config window, and no handler clears it. It reads `--accent-lighter`
3873
3875
  (BUG-0045): `--accent-bright` is a border colour on this background and not
3874
3876
  a text one. It was #78b9ff, which no mix of `--accent-bright` with white
3875
3877
  gives exactly; `--accent-lighter` (about #73b6e3) is the nearest. */
3876
- .tile.tile--selected {
3878
+ .tile.tile--selected,
3879
+ .tile.tile--configuring {
3877
3880
  border-color: var(--accent-lighter);
3878
3881
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent-lighter) 30%, transparent), var(--shadow-md);
3879
3882
  }
3880
3883
 
3881
- .tile.tile--selected .tile-header {
3884
+ .tile.tile--selected .tile-header,
3885
+ .tile.tile--configuring .tile-header {
3882
3886
  background: color-mix(in srgb, var(--accent-lighter) 10%, transparent);
3883
3887
  }
3884
3888
 
3885
- .tile.tile--selected .tile-title {
3889
+ .tile.tile--selected .tile-title,
3890
+ .tile.tile--configuring .tile-title {
3886
3891
  color: var(--accent-lighter);
3887
3892
  }
3888
3893
 
@@ -4609,6 +4614,215 @@ body.twm-window-maximized .twm-resize-handle {
4609
4614
  text-overflow: ellipsis;
4610
4615
  }
4611
4616
 
4617
+ /* ══ CATEGORY PICKER (STORY-0126) ══════════════════════════════════════
4618
+ `CategoryPicker` (category_picker.js): a search field over three columns —
4619
+ categories, the active category's items, the highlighted item's detail.
4620
+ Placed by `ActionDropdown.position` (inline top/left/max-height), so it is
4621
+ `position: fixed` on --z-over-windows: over the managed window it opened
4622
+ from. It shrinks to the inline max-height and its columns scroll inside.
4623
+ A category's colour arrives as `--twm-category-picker-color` on its rows. */
4624
+ .twm-category-picker {
4625
+ position: fixed;
4626
+ z-index: var(--z-over-windows);
4627
+ display: flex;
4628
+ flex-direction: column;
4629
+ box-sizing: border-box;
4630
+ width: 640px;
4631
+ max-width: calc(100vw - 2 * var(--space-md));
4632
+ height: 440px;
4633
+ background: var(--surface-1);
4634
+ border: 1px solid var(--border-strong);
4635
+ border-radius: var(--radius-lg);
4636
+ box-shadow: var(--shadow-lg);
4637
+ color: var(--text-primary);
4638
+ font-family: inherit;
4639
+ font-size: var(--font-size-md);
4640
+ overflow: hidden;
4641
+ }
4642
+ .twm-category-picker[hidden] {
4643
+ display: none;
4644
+ }
4645
+ .twm-category-picker__search {
4646
+ flex: none;
4647
+ padding: var(--space-md);
4648
+ border-bottom: 1px solid var(--border-default);
4649
+ }
4650
+ .twm-category-picker__input {
4651
+ width: 100%;
4652
+ box-sizing: border-box;
4653
+ padding: var(--space-sm) var(--space-md);
4654
+ background: var(--surface-0);
4655
+ border: 1px solid var(--border-strong);
4656
+ border-radius: var(--radius-sm);
4657
+ color: var(--text-primary);
4658
+ font-family: inherit;
4659
+ font-size: var(--font-size-md);
4660
+ }
4661
+ .twm-category-picker__input:focus {
4662
+ outline: none;
4663
+ border-color: var(--accent-bright);
4664
+ }
4665
+ .twm-category-picker__body {
4666
+ flex: 1 1 auto;
4667
+ min-height: 0;
4668
+ display: flex;
4669
+ }
4670
+ .twm-category-picker__column {
4671
+ flex: none;
4672
+ box-sizing: border-box;
4673
+ padding: var(--space-sm) 0;
4674
+ border-right: 1px solid var(--border-default);
4675
+ overflow-y: auto;
4676
+ }
4677
+ .twm-category-picker__categories {
4678
+ width: 170px;
4679
+ }
4680
+ .twm-category-picker__items {
4681
+ width: 220px;
4682
+ }
4683
+ .twm-category-picker__category,
4684
+ .twm-category-picker__item {
4685
+ display: flex;
4686
+ align-items: center;
4687
+ gap: var(--space-md);
4688
+ width: 100%;
4689
+ box-sizing: border-box;
4690
+ padding: var(--space-sm) var(--space-lg);
4691
+ background: transparent;
4692
+ border: none;
4693
+ color: var(--text-primary);
4694
+ font-family: inherit;
4695
+ font-size: var(--font-size-md);
4696
+ text-align: left;
4697
+ cursor: pointer;
4698
+ }
4699
+ .twm-category-picker__category:hover,
4700
+ .twm-category-picker__item:hover {
4701
+ background: var(--state-hover);
4702
+ }
4703
+ .twm-category-picker__category--active,
4704
+ .twm-category-picker__item--highlighted,
4705
+ .twm-category-picker__category--active:hover,
4706
+ .twm-category-picker__item--highlighted:hover {
4707
+ background: var(--state-selected);
4708
+ }
4709
+ /* The column the arrow keys move in marks its row with an accent edge. */
4710
+ .twm-category-picker__column--current .twm-category-picker__category--active,
4711
+ .twm-category-picker__column--current .twm-category-picker__item--highlighted {
4712
+ box-shadow: inset 2px 0 0 var(--accent-bright);
4713
+ }
4714
+ .twm-category-picker__category--empty {
4715
+ color: var(--text-dim);
4716
+ }
4717
+ .twm-category-picker__dot {
4718
+ flex: none;
4719
+ width: 8px;
4720
+ height: 8px;
4721
+ border-radius: var(--radius-circle);
4722
+ /* background-color, not the shorthand: the colour is the caller's, and a
4723
+ url() in the shorthand would be fetched */
4724
+ background-color: var(--twm-category-picker-color, var(--text-muted));
4725
+ }
4726
+ .twm-category-picker__category-label,
4727
+ .twm-category-picker__item-label {
4728
+ flex: 1 1 auto;
4729
+ min-width: 0;
4730
+ overflow: hidden;
4731
+ white-space: nowrap;
4732
+ text-overflow: ellipsis;
4733
+ }
4734
+ .twm-category-picker__count {
4735
+ flex: none;
4736
+ color: var(--text-muted);
4737
+ font-size: var(--font-size-xs);
4738
+ font-variant-numeric: tabular-nums;
4739
+ }
4740
+ .twm-category-picker__badge,
4741
+ .twm-category-picker__item .twm-category-picker__icon {
4742
+ flex: none;
4743
+ display: flex;
4744
+ align-items: center;
4745
+ justify-content: center;
4746
+ width: 22px;
4747
+ height: 22px;
4748
+ box-sizing: border-box;
4749
+ color: var(--twm-category-picker-color, var(--text-muted));
4750
+ }
4751
+ .twm-category-picker__badge {
4752
+ border: 1px solid var(--twm-category-picker-color, var(--text-muted));
4753
+ border-radius: var(--radius-sm);
4754
+ font-family: var(--font-family-mono);
4755
+ font-size: var(--font-size-xs);
4756
+ font-weight: var(--font-weight-semibold);
4757
+ overflow: hidden;
4758
+ white-space: nowrap;
4759
+ }
4760
+ .twm-category-picker__item .twm-category-picker__icon {
4761
+ font-size: 18px;
4762
+ }
4763
+ /* One category's hits while a query is set: role="group", named by its header. */
4764
+ .twm-category-picker__hits {
4765
+ display: flex;
4766
+ flex-direction: column;
4767
+ }
4768
+ .twm-category-picker__group {
4769
+ display: flex;
4770
+ align-items: center;
4771
+ gap: var(--space-sm);
4772
+ padding: var(--space-md) var(--space-lg) var(--space-xs);
4773
+ color: var(--text-muted);
4774
+ font-size: var(--font-size-xs);
4775
+ text-transform: uppercase;
4776
+ letter-spacing: 0.06em;
4777
+ }
4778
+ .twm-category-picker__empty,
4779
+ .twm-category-picker__detail-empty {
4780
+ padding: var(--space-md) var(--space-lg);
4781
+ color: var(--text-muted);
4782
+ }
4783
+ .twm-category-picker__detail {
4784
+ flex: 1 1 auto;
4785
+ min-width: 0;
4786
+ display: flex;
4787
+ flex-direction: column;
4788
+ gap: var(--space-md);
4789
+ padding: var(--space-xl);
4790
+ overflow-y: auto;
4791
+ }
4792
+ .twm-category-picker__title {
4793
+ color: var(--text-bright);
4794
+ font-size: var(--font-size-xl);
4795
+ font-weight: var(--font-weight-semibold);
4796
+ }
4797
+ .twm-category-picker__description {
4798
+ color: var(--text-secondary);
4799
+ line-height: var(--line-height-normal);
4800
+ }
4801
+ .twm-category-picker__preview-label {
4802
+ margin-top: var(--space-sm);
4803
+ color: var(--text-muted);
4804
+ font-size: var(--font-size-xs);
4805
+ text-transform: uppercase;
4806
+ letter-spacing: 0.06em;
4807
+ }
4808
+ .twm-category-picker__preview {
4809
+ margin: 0;
4810
+ padding: var(--space-md);
4811
+ background: var(--surface-0);
4812
+ border: 1px solid var(--border-default);
4813
+ border-radius: var(--radius-sm);
4814
+ color: var(--text-body);
4815
+ font-family: var(--font-family-mono);
4816
+ font-size: var(--font-size-sm);
4817
+ white-space: pre-wrap;
4818
+ overflow-wrap: anywhere;
4819
+ }
4820
+ .twm-category-picker__add {
4821
+ margin-top: auto;
4822
+ align-self: flex-end;
4823
+ }
4824
+ /* ══ end CATEGORY PICKER ══ */
4825
+
4612
4826
  /* ── from code.css ───────────────────────── */
4613
4827
  /* Code language tabs */
4614
4828
  .twm-code-tabs-header {
@@ -4825,6 +5039,12 @@ body.twm-window-maximized .twm-resize-handle {
4825
5039
  .twm-managed-window:focus-within {
4826
5040
  border-color: #505050;
4827
5041
  }
5042
+ /* BUG-0053. The frame is focusable (tabindex -1) so a press on it keeps focus
5043
+ in the window; the focus-within border above already shows that, so the
5044
+ frame draws no outline of its own. */
5045
+ .twm-managed-window:focus {
5046
+ outline: none;
5047
+ }
4828
5048
  /* Minimize animation class — targets taskbar item via CSS custom properties */
4829
5049
  .twm-managed-window--minimizing {
4830
5050
  transform: translate(var(--mw-target-x, 0px), var(--mw-target-y, 100vh)) scale(var(--mw-target-scale, 0.1));
@@ -11084,6 +11304,11 @@ button.twm-alerts__ref:hover { text-decoration: underline; }
11084
11304
  flex: 1 1 140px;
11085
11305
  min-width: 0;
11086
11306
  }
11307
+ /* A picker has no helper text: its empty helper takes no room (it held a
11308
+ * 28px gap under every picker, and the series header line off its centre). */
11309
+ .twm-plot-config__picker > .twm-autocomplete-field__helper:empty {
11310
+ display: none;
11311
+ }
11087
11312
  .twm-plot-config__axes,
11088
11313
  .twm-plot-config__list,
11089
11314
  .twm-plot-config__series-list {
@@ -11117,37 +11342,114 @@ button.twm-alerts__ref:hover { text-decoration: underline; }
11117
11342
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
11118
11343
  gap: var(--space-xs) var(--space-md);
11119
11344
  }
11345
+ /* A series is one block (BUG-0052): a header line (picker, legend label,
11346
+ * remove) over its fields, one per line on the editor's 96px label column —
11347
+ * colour, width, dash, stack group, the embedder's `extraSeriesFields` (their
11348
+ * box is `display: contents`, so its rows are rows of the column) and the
11349
+ * overlay. Until 0.17.15 all of it was one wrapped line. */
11120
11350
  .twm-plot-config__series {
11121
11351
  display: flex;
11122
11352
  flex-direction: column;
11123
- gap: var(--space-xxs);
11124
- padding: var(--space-xs) 0;
11353
+ gap: var(--space-xs);
11354
+ padding: var(--space-sm) 0;
11125
11355
  border-top: 1px solid var(--border-subtle);
11356
+ min-width: 0;
11357
+ }
11358
+ .twm-plot-config__series-head {
11359
+ display: flex;
11360
+ flex-wrap: wrap;
11361
+ align-items: center;
11362
+ gap: var(--space-xs);
11363
+ min-width: 0;
11364
+ }
11365
+ .twm-plot-config__series-head > .twm-plot-config__picker {
11366
+ flex: 2 1 140px;
11367
+ }
11368
+ .twm-plot-config__series-head > .twm-plot-config__input {
11369
+ flex: 1 1 100px;
11370
+ }
11371
+ .twm-plot-config__series-fields {
11372
+ display: flex;
11373
+ flex-direction: column;
11374
+ gap: var(--space-xxs);
11375
+ min-width: 0;
11376
+ }
11377
+ .twm-plot-config__series-extra {
11378
+ display: contents;
11126
11379
  }
11127
- .twm-plot-config__series-main,
11128
11380
  .twm-plot-config__row {
11129
11381
  display: flex;
11130
11382
  flex-wrap: wrap;
11131
11383
  align-items: center;
11132
11384
  gap: var(--space-xs);
11133
11385
  }
11134
- .twm-plot-config__series-main > .twm-plot-config__input:not(.twm-plot-config__input--number):not(.twm-plot-config__input--select),
11135
11386
  .twm-plot-config__row > .twm-plot-config__input:not(.twm-plot-config__input--number):not(.twm-plot-config__input--select) {
11136
11387
  flex: 1 1 100px;
11137
11388
  }
11138
- .twm-plot-config__series-main > .twm-plot-config__input--select,
11139
11389
  .twm-plot-config__row > .twm-plot-config__input--select {
11140
11390
  flex: 0 1 auto;
11141
11391
  }
11142
- .twm-plot-config__series-extra:empty {
11143
- display: none;
11392
+ /* On a field line a colour is the swatch and a hex box sized to `#rrggbb`. */
11393
+ .twm-plot-config__field > .twm-color-field {
11394
+ flex: 0 1 auto;
11395
+ flex-wrap: nowrap;
11396
+ }
11397
+ .twm-plot-config__field > .twm-color-field > .twm-color-field__hex {
11398
+ flex: 0 1 auto;
11399
+ width: 10ch;
11144
11400
  }
11401
+ /* A colour's hex box is a well like the editor's other inputs. */
11402
+ .twm-plot-config .twm-color-field__hex {
11403
+ box-sizing: border-box;
11404
+ padding: var(--space-xxs) var(--space-xs);
11405
+ background: var(--surface-3);
11406
+ color: var(--text-primary);
11407
+ border: 1px solid var(--border-strong);
11408
+ border-radius: var(--radius-sm);
11409
+ }
11410
+ /* ... and still shows a hex that is not a colour (the well's border above
11411
+ * would otherwise hide ColorField's invalid border). */
11412
+ .twm-plot-config .twm-color-field--invalid .twm-color-field__hex {
11413
+ border-color: var(--color-danger);
11414
+ }
11415
+ /* A field line's select keeps to its options, as the number boxes do. */
11416
+ .twm-plot-config__series-fields .twm-plot-config__field > .twm-plot-config__input--select {
11417
+ flex: 0 1 auto;
11418
+ max-width: 100%;
11419
+ }
11420
+ /* An embedder's control drawn by a field spec's `render(slot)`. */
11421
+ .twm-plot-config__slot {
11422
+ flex: 1 1 auto;
11423
+ min-width: 0;
11424
+ }
11425
+ /* A field's hint, under its control: past the label column. Only a field
11426
+ * with a hint wraps, so a number box never drops under its label. */
11427
+ .twm-plot-config__field--hinted {
11428
+ flex-wrap: wrap;
11429
+ }
11430
+ .twm-plot-config__hint {
11431
+ box-sizing: border-box;
11432
+ flex: 1 0 100%;
11433
+ padding-left: calc(96px + var(--space-sm));
11434
+ font-size: var(--font-size-xs);
11435
+ color: var(--text-muted);
11436
+ }
11437
+ /* The overlay's own fields, under its source: set in by a rule, their label
11438
+ * column shortened by as much, so their controls stay on the series' column. */
11145
11439
  .twm-plot-config__overlay,
11146
11440
  .twm-plot-config__overlay-fields {
11147
11441
  display: flex;
11148
11442
  flex-direction: column;
11149
11443
  gap: var(--space-xxs);
11150
- padding-left: var(--space-md);
11444
+ min-width: 0;
11445
+ }
11446
+ .twm-plot-config__overlay-fields:not(:empty) {
11447
+ margin-left: var(--space-sm);
11448
+ padding-left: calc(var(--space-sm) - 1px);
11449
+ border-left: 1px solid var(--border-subtle);
11450
+ }
11451
+ .twm-plot-config__overlay-fields > .twm-plot-config__field > .twm-plot-config__label {
11452
+ flex-basis: calc(96px - 2 * var(--space-sm));
11151
11453
  }
11152
11454
  .twm-plot-config__overlay-source {
11153
11455
  color: var(--text-muted);
package/dist/flow.js CHANGED
@@ -1,15 +1,15 @@
1
1
  import {
2
2
  modalHost
3
- } from "./chunk-OEG6NPAN.js";
3
+ } from "./chunk-XJNADRGR.js";
4
4
  import {
5
5
  DataTable
6
- } from "./chunk-2PM6ZMNS.js";
6
+ } from "./chunk-2Z2ENHR3.js";
7
7
  import {
8
8
  choiceList,
9
9
  selectBox,
10
10
  shownValue,
11
11
  toggleChips
12
- } from "./chunk-MADMGRX2.js";
12
+ } from "./chunk-PGZ4EHSE.js";
13
13
  import "./chunk-HNU6MM2B.js";
14
14
  import "./chunk-WUFLGETN.js";
15
15
  import {
package/dist/tiles.js CHANGED
@@ -17,11 +17,11 @@ import {
17
17
  } from "./chunk-MIW7D6WC.js";
18
18
  import {
19
19
  showContextMenu
20
- } from "./chunk-AD7FIA3Z.js";
20
+ } from "./chunk-7JI7HHSY.js";
21
21
  import {
22
22
  modalHost,
23
23
  openModal
24
- } from "./chunk-OEG6NPAN.js";
24
+ } from "./chunk-XJNADRGR.js";
25
25
  import {
26
26
  buildHistogramTrace,
27
27
  buildLegendLayout,
@@ -36,11 +36,11 @@ import {
36
36
  purge,
37
37
  resize,
38
38
  strideDownsampleShared
39
- } from "./chunk-TLIV6QSU.js";
40
- import "./chunk-2PM6ZMNS.js";
39
+ } from "./chunk-7OL6F2AM.js";
40
+ import "./chunk-2Z2ENHR3.js";
41
41
  import {
42
42
  isHexColor
43
- } from "./chunk-MADMGRX2.js";
43
+ } from "./chunk-PGZ4EHSE.js";
44
44
  import "./chunk-HNU6MM2B.js";
45
45
  import "./chunk-WUFLGETN.js";
46
46
  import {
@@ -220,6 +220,9 @@ var TileBase = class {
220
220
  * @param {{resolve: Function}} [options.dataSource] - Where this tile asks for its
221
221
  * own rows. Handed down by `TileGrid`; absent for a grid whose data is
222
222
  * broadcast with `setData()`, and then nothing ever calls `loadData()`.
223
+ * @param {boolean} [options.configOnDoubleClick=true] - A double-click on the
224
+ * title strip (not on its buttons or an editable field in it) does what
225
+ * the cogwheel does. False leaves the cogwheel as the only way in.
223
226
  */
224
227
  constructor({
225
228
  id,
@@ -230,7 +233,8 @@ var TileBase = class {
230
233
  headless = false,
231
234
  host = null,
232
235
  stateGuard = null,
233
- dataSource = null
236
+ dataSource = null,
237
+ configOnDoubleClick = true
234
238
  }) {
235
239
  this.host = host;
236
240
  this.dataSource = dataSource;
@@ -241,6 +245,8 @@ var TileBase = class {
241
245
  this.config = { ...this.getDefaultConfig(), ...config };
242
246
  this.readonly = readonly;
243
247
  this.headless = headless;
248
+ this.configOnDoubleClick = configOnDoubleClick;
249
+ this._configuring = false;
244
250
  this.element = null;
245
251
  this.contentElement = null;
246
252
  this.chartInstance = null;
@@ -413,7 +419,7 @@ var TileBase = class {
413
419
  mount(container) {
414
420
  if (this._disposed) return;
415
421
  this.element = document.createElement("div");
416
- this.element.className = `tile${this.readonly ? " tile--readonly" : ""}`;
422
+ this.element.className = `tile${this.readonly ? " tile--readonly" : ""}${this._configuring ? " tile--configuring" : ""}`;
417
423
  const stateGuard = this.stateGuard;
418
424
  const setDataset = () => {
419
425
  this.element.dataset.tileId = this.id;
@@ -669,6 +675,12 @@ var TileBase = class {
669
675
  e.stopPropagation();
670
676
  this._onConfigClick();
671
677
  });
678
+ header.addEventListener("dblclick", (e) => {
679
+ if (!this.configOnDoubleClick || !configBtn) return;
680
+ if (e.target.closest?.('button, input, textarea, select, [contenteditable=""], [contenteditable="true"], .tile-menu-dropdown')) return;
681
+ e.stopPropagation();
682
+ this._onConfigClick();
683
+ });
672
684
  const menuBtn = header.querySelector(".tile-menu-btn");
673
685
  const menuDropdown = header.querySelector(".tile-menu-dropdown");
674
686
  if (menuBtn && menuDropdown) {
@@ -759,6 +771,33 @@ var TileBase = class {
759
771
  }
760
772
  if (this._menuHome && menu.parentNode !== this._menuHome) this._menuHome.appendChild(menu);
761
773
  }
774
+ /**
775
+ * Mark this tile as being configured, or clear the mark (STORY-0128).
776
+ * A convenience for `grid.setConfiguring(tile.id, on)`, which is the API:
777
+ * the grid holds the mark by tile id, so it outlives this tile instance
778
+ * (`setLayout` disposes and rebuilds every tile). A tile with no grid
779
+ * holds the mark itself.
780
+ *
781
+ * @param {boolean} [on=true]
782
+ */
783
+ setConfiguring(on = true) {
784
+ if (typeof this.grid?.setConfiguring === "function") {
785
+ this.grid.setConfiguring(this.id, on);
786
+ return;
787
+ }
788
+ this._applyConfiguring(on);
789
+ }
790
+ /** Draw (or clear) the configuring mark. Called by the grid.
791
+ * @param {boolean} on
792
+ * @private */
793
+ _applyConfiguring(on) {
794
+ this._configuring = !!on;
795
+ this.element?.classList.toggle("tile--configuring", this._configuring);
796
+ }
797
+ /** True while this tile is marked as being configured. */
798
+ get isConfiguring() {
799
+ return this._configuring;
800
+ }
762
801
  /**
763
802
  * Handle config button click — highlight tile and emit config request.
764
803
  * @private
@@ -3028,6 +3067,8 @@ var TileGrid = class {
3028
3067
  * @param {*} [options.scope=null] - The board's scope, a filter every tile
3029
3068
  * follows (0.16.1); see `setScope`.
3030
3069
  * @param {Function} [options.onScopeChange] - `(scope)` after `setScope` changed it.
3070
+ * @param {boolean} [options.configOnDoubleClick=true] - Whether a double-click
3071
+ * on a tile's title strip opens its config, as its cogwheel does (0.17.16).
3031
3072
  */
3032
3073
  constructor({
3033
3074
  container,
@@ -3052,13 +3093,18 @@ var TileGrid = class {
3052
3093
  // a binding is, only that a tile may have somewhere to ask.
3053
3094
  dataSource = null,
3054
3095
  scope = null,
3055
- onScopeChange = null
3096
+ onScopeChange = null,
3097
+ // STORY-0128. Handed to every tile: false turns off the
3098
+ // title strip's double-click to configure.
3099
+ configOnDoubleClick = true
3056
3100
  }) {
3057
3101
  this.host = host;
3058
3102
  this.scope = scope;
3059
3103
  this._onScopeChangeCallback = onScopeChange;
3060
3104
  this.stateGuard = stateGuard;
3061
3105
  this.dataSource = dataSource;
3106
+ this.configOnDoubleClick = configOnDoubleClick;
3107
+ this._configuringIds = /* @__PURE__ */ new Set();
3062
3108
  this.container = container;
3063
3109
  this.columns = columns;
3064
3110
  this.rowHeight = rowHeight;
@@ -3176,10 +3222,12 @@ var TileGrid = class {
3176
3222
  readonly: this.readonly,
3177
3223
  host: this.host,
3178
3224
  stateGuard: this.stateGuard,
3179
- dataSource: this.dataSource
3225
+ dataSource: this.dataSource,
3226
+ configOnDoubleClick: this.configOnDoubleClick
3180
3227
  });
3181
3228
  if (!tile) return null;
3182
3229
  tile.scope = this.scope;
3230
+ if (this._configuringIds.has(tileId)) tile._applyConfiguring(true);
3183
3231
  const layoutEntry = {
3184
3232
  id: tileId,
3185
3233
  type,
@@ -3298,6 +3346,33 @@ var TileGrid = class {
3298
3346
  config: this.tiles.get(l.id)?.config || l.config
3299
3347
  }));
3300
3348
  }
3349
+ /**
3350
+ * Mark a tile as being configured, or clear the mark (STORY-0128).
3351
+ *
3352
+ * A config request (cogwheel or title double-click) highlights the
3353
+ * requesting tile with `tile--selected` and takes that highlight off every
3354
+ * other tile: one selection per grid. An embedder that keeps a config
3355
+ * window open per tile marks each such tile here instead. The mark
3356
+ * (`tile--configuring`, drawn like the selection) is the embedder's alone:
3357
+ * no config request clears it, and it is held by id, so a tile that
3358
+ * `setLayout` rebuilds (or an id added later) is marked again. Clear it
3359
+ * when that tile's config window closes.
3360
+ *
3361
+ * @param {string} tileId
3362
+ * @param {boolean} [on=true]
3363
+ */
3364
+ setConfiguring(tileId, on = true) {
3365
+ if (on) this._configuringIds.add(tileId);
3366
+ else this._configuringIds.delete(tileId);
3367
+ this.tiles.get(tileId)?._applyConfiguring(!!on);
3368
+ }
3369
+ /**
3370
+ * @param {string} tileId
3371
+ * @returns {boolean} Whether the tile is marked as being configured.
3372
+ */
3373
+ isConfiguring(tileId) {
3374
+ return this._configuringIds.has(tileId);
3375
+ }
3301
3376
  /**
3302
3377
  * Set layout and rebuild tiles.
3303
3378
  * @param {Array|Object} layout - Layout array or object with tiles property