@feedmepos/mf-report 5.28.5 → 5.29.0

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 (35) hide show
  1. package/dist/{App-Bc8Mp5vR.js → App-CuBsRwfx.js} +4 -4
  2. package/dist/{DateRangeSelect-cbSEHMzW.js → DateRangeSelect-CzjlwjuC.js} +1 -1
  3. package/dist/{Default.vue_vue_type_script_setup_true_lang-BpJFA_ET.js → Default.vue_vue_type_script_setup_true_lang-Di9_F5Yk.js} +1 -1
  4. package/dist/{DynamicWidget.vue_vue_type_script_setup_true_lang-Dx-cxJP6.js → DynamicWidget.vue_vue_type_script_setup_true_lang-C73R6Fb9.js} +5 -5
  5. package/dist/{GlobalFilterSelect.vue_vue_type_script_setup_true_lang-Cxrti7_L.js → GlobalFilterSelect.vue_vue_type_script_setup_true_lang-tLRFdLNs.js} +9 -10
  6. package/dist/{InsightView-Bo0Fb5QG.js → InsightView-BcnmLQ8c.js} +5 -5
  7. package/dist/{Integration-B4tC2MlL.js → Integration-D4VFT1M8.js} +3 -3
  8. package/dist/{Integrations-RB8iuvdJ.js → Integrations-v8oF1TL7.js} +2 -2
  9. package/dist/{Layout-DBCPtUH7.js → Layout-Ds8RSWmi.js} +1 -1
  10. package/dist/{LocationSelect.vue_vue_type_script_setup_true_lang-w2H9qOgJ.js → LocationSelect.vue_vue_type_script_setup_true_lang-Dk6u8nUH.js} +1638 -1634
  11. package/dist/{MenuTab.vue_vue_type_script_setup_true_lang-BLVGUSWf.js → MenuTab.vue_vue_type_script_setup_true_lang-BWt7pqFb.js} +5 -5
  12. package/dist/{NavigationTab.vue_vue_type_script_setup_true_lang-N31ee-oZ.js → NavigationTab.vue_vue_type_script_setup_true_lang-CU00GINo.js} +3853 -3823
  13. package/dist/OverviewView-1v80g6KX.js +81 -0
  14. package/dist/{Report-Di6GyP1r.js → Report-DcFB4gM6.js} +3 -3
  15. package/dist/ReportCanvasView-BQO6Mywc.js +5190 -0
  16. package/dist/{ReportEditor-BfNmiEVn.js → ReportEditor-Bgcisf1s.js} +7 -7
  17. package/dist/{ReportView-JWJ-6FCu.js → ReportView-Q_juD8Kr.js} +6 -6
  18. package/dist/{ReportViewRoute-BMjjJqN2.js → ReportViewRoute-DwStGqyz.js} +2 -2
  19. package/dist/{Setting-D13NIemB.js → Setting-C9rlM23O.js} +3 -3
  20. package/dist/{Template-D16bqzrx.js → Template-BZd0NB5w.js} +3 -3
  21. package/dist/{TileEditor-1Sn9ZSb2.js → TileEditor-B2F9r3RS.js} +6 -6
  22. package/dist/{TilePageStack.vue_vue_type_script_setup_true_lang-BOXsV7-r.js → TilePageStack.vue_vue_type_script_setup_true_lang-BmbrJAph.js} +2 -2
  23. package/dist/{TransitionFade-DwJkmMTb.js → TransitionFade-Dkeo9qr0.js} +5 -5
  24. package/dist/{app-CVBllEjW.js → app-CoQ_GRKo.js} +1819 -1326
  25. package/dist/app.js +1 -1
  26. package/dist/assets/{formatChartData.worker-DZp5dSDQ.js → formatChartData.worker-D7sGqQhX.js} +34 -34
  27. package/dist/assets/{processTableData.worker-Kyuhw4lS.js → processTableData.worker-CltxwQxU.js} +34 -34
  28. package/dist/{report-Cn4Ik1zm.js → report-Dau_so1j.js} +1 -1
  29. package/dist/report-canvas-libs/components-1.js +1042 -0
  30. package/dist/report-canvas-libs/fm-ui-1.js +599 -180
  31. package/dist/style.css +1 -1
  32. package/dist/{useRestaurantPermission-C8bLPfk8.js → useRestaurantPermission-CPoHZTxx.js} +1 -1
  33. package/package.json +2 -1
  34. package/dist/OverviewView-WKe6Zxe1.js +0 -61
  35. package/dist/ReportCanvasView-Dg2ItHBO.js +0 -1336
@@ -12,56 +12,115 @@
12
12
  if (FM.ui) return
13
13
 
14
14
  var STYLE_ID = "fm-ui-style"
15
- // Chrome follows the classic report filter row: a filled chip trigger, orange for the active state.
15
+ // Rebus v2 owns the look: each control renders the anatomy documented in
16
+ // canvas-report-builder/guide/11-rebus-ui. The .fm-* rules below are behavior and test hooks,
17
+ // plus the four things Rebus ships no component for: popover, option row, dialog, report table.
16
18
  var CSS = [
17
- ".fm-c{position:relative;display:inline-block;font:inherit;vertical-align:top}",
19
+ ".fm-c{position:relative;display:inline-block;max-width:100%;font:inherit;vertical-align:top}",
18
20
  ".fm-c *{box-sizing:border-box}",
19
- ".fm-btn{display:inline-flex;align-items:center;gap:6px;min-height:32px;padding:6px 12px;",
20
- "border:1px solid transparent;border-radius:999px;background:#f2f2f7;color:#6b6b6b;",
21
- "font:inherit;font-weight:600;cursor:pointer;white-space:nowrap}",
22
- ".fm-btn:hover{background:#eaeaef}",
23
- ".fm-btn[aria-expanded=true]{border-color:var(--fm-signature,#ff7823);color:var(--fm-fg,#1c1c1e)}",
24
- ".fm-btn .fm-caret{color:#6b6b6b;font-size:10px}",
25
- ".fm-pop{position:absolute;z-index:20;top:calc(100% + 4px);left:0;min-width:100%;",
26
- "max-height:280px;overflow:auto;padding:4px;border:1px solid var(--fm-line,#f0f0f0);",
27
- "border-radius:8px;background:#fff;box-shadow:0 6px 24px rgba(28,28,30,.12)}",
21
+ // the UA font on <button> beats inheritance, and a Rebus text class only sets the node it is on
22
+ ".fm-c button,.fm-dlg button{font-family:inherit}",
23
+ ".fm-c>.fm-chip{max-width:100%}",
24
+ ".fm-caret{flex:none;color:var(--fm-icon-muted,#6b6c70);font-size:10px;line-height:1}",
25
+ ".fm-chip__icon svg,.fm-search-bar__leading svg{display:block;width:var(--fm-icon-size);height:var(--fm-icon-size)}",
26
+ // fixed to the viewport, so a card's overflow clip never cuts it off — popover() places it by its chip;
27
+ // head / list / foot column: only the list scrolls, so search and Apply never leave the panel
28
+ ".fm-pop{position:fixed;z-index:20;min-width:248px;",
29
+ "display:flex;flex-direction:column;max-height:min(420px,70vh);overflow:hidden;",
30
+ "padding:var(--fm-spacing-8,8px);",
31
+ "border:var(--fm-border-width-thin,1px) solid var(--fm-border-default,#f0f0f0);",
32
+ "border-radius:var(--fm-radius-md,12px);background:var(--fm-surface-floating,#fff);",
33
+ "box-shadow:var(--fm-shadow-xl,0 8px 16px rgb(28 28 30/.12))}",
28
34
  ".fm-pop[hidden]{display:none}",
29
- ".fm-opt{display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:4px;cursor:pointer;white-space:nowrap}",
30
- ".fm-opt:hover{background:#fafafa}",
31
- ".fm-opt[aria-selected=true]{background:var(--fm-signature-tint,#fff3e6);color:#6b3100;font-weight:600}",
32
- ".fm-opt input[type=checkbox]{accent-color:var(--fm-signature,#ff7823)}",
33
- ".fm-search{width:100%;margin:2px 0 4px;padding:6px 8px;border:1px solid var(--fm-line,#f0f0f0);",
34
- "border-radius:4px;font:inherit}",
35
- ".fm-empty{padding:8px;color:var(--fm-muted,#6b6c70)}",
36
- ".fm-seg{display:inline-flex;border:1px solid var(--fm-line,#f0f0f0);border-radius:999px;overflow:hidden}",
37
- ".fm-seg button{min-height:32px;padding:6px 14px;border:0;border-right:1px solid var(--fm-line,#f0f0f0);",
38
- "background:#fff;color:var(--fm-muted,#6b6c70);font:inherit;font-weight:600;cursor:pointer}",
39
- ".fm-seg button:last-child{border-right:0}",
40
- ".fm-seg button[aria-pressed=true]{background:var(--fm-signature,#ff7823);color:#fff}",
41
- ".fm-dr-body{display:flex;gap:8px;padding:4px}",
42
- ".fm-dr-presets{min-width:132px;border-right:1px solid var(--fm-line,#f0f0f0);padding-right:4px}",
43
- ".fm-dr-custom{display:flex;flex-direction:column;gap:6px;padding:4px 4px 4px 0}",
44
- ".fm-dr-custom label{display:flex;flex-direction:column;gap:2px;color:var(--fm-muted,#6b6c70);font-size:12px}",
45
- ".fm-dr-custom input{padding:6px 8px;border:1px solid var(--fm-line,#f0f0f0);border-radius:4px;font:inherit}",
46
- ".fm-dr-apply{align-self:flex-end;padding:6px 14px;border:0;border-radius:999px;",
47
- "background:var(--fm-signature,#ff7823);color:#fff;font:inherit;font-weight:600;cursor:pointer}",
48
- ".fm-ui-table tr.fm-row-btn{cursor:pointer}",
49
- ".fm-ui-table tr.fm-row-btn:hover td{background:#fafafa}",
50
- ".fm-ui-table tr.fm-row-btn[aria-selected=true] td{background:var(--fm-signature-tint,#fff3e6)}",
51
- ".fm-ui-table tr.fm-row-btn:focus-visible{outline:2px solid var(--fm-signature,#ff7823);outline-offset:-2px}",
35
+ ".fm-pop-list{flex:1 1 auto;min-height:0;overflow:auto;overscroll-behavior:contain}",
36
+ ".fm-pop-foot{flex:none;display:flex;align-items:center;justify-content:flex-end;",
37
+ "gap:var(--fm-spacing-8,8px);margin-top:var(--fm-spacing-8,8px);padding-top:var(--fm-spacing-8,8px);",
38
+ "border-top:var(--fm-border-width-thin,1px) solid var(--fm-border-default,#f0f0f0)}",
39
+ // the panel is only as wide as the chip until something refuses to shrink; a wrapped Apply is that
40
+ ".fm-pop-foot .fm-button{flex:none}",
41
+ ".fm-pop-foot .fm-button__label{white-space:nowrap}",
42
+ ".fm-opt{display:flex;align-items:center;gap:var(--fm-spacing-8,8px);",
43
+ "padding:var(--fm-spacing-6,6px) var(--fm-spacing-10,10px);min-height:40px;",
44
+ "border-radius:var(--fm-radius-xs,4px);cursor:pointer;white-space:nowrap}",
45
+ ".fm-opt:hover{background:var(--fm-bg-subtle,#f6f6f8)}",
46
+ // a row carrying a checkbox already states its selection; only a bare row paints itself
47
+ ".fm-opt[aria-selected=true]:not(.fm-opt-check){background:var(--fm-action-primary-bg-subtle,#fff3e6);",
48
+ "color:var(--fm-text-on-brand,#b25300);font-weight:600}",
49
+ // the frame carries no icon registry, so the Rebus checkbox needs its tick supplied inline
50
+ ".fm-opt .fm-checkbox{flex:none}",
51
+ // the named All row is a mode, not an option: it sits above the search behind its own rule,
52
+ // so it never reads as a tick row with the box missing
53
+ ".fm-pop-all{flex:none;margin-bottom:var(--fm-spacing-8,8px);",
54
+ "padding-bottom:var(--fm-spacing-8,8px);",
55
+ "border-bottom:var(--fm-border-width-thin,1px) solid var(--fm-border-default,#f0f0f0)}",
56
+ ".fm-opt-all{justify-content:space-between;gap:var(--fm-spacing-12,12px)}",
57
+ ".fm-opt-mark{display:none;flex:none;color:var(--fm-icon-brand,#ff7823)}",
58
+ ".fm-opt-mark svg{display:block;width:20px;height:20px}",
59
+ ".fm-opt-all[aria-selected=true] .fm-opt-mark{display:inline-flex}",
60
+ ".fm-opt .fm-checkbox__glyph svg{display:block;width:var(--fm-icon-size);height:var(--fm-icon-size)}",
61
+ ".fm-pop>.fm-search-bar{flex:none;margin-bottom:var(--fm-spacing-8,8px)}",
62
+ ".fm-empty{padding:var(--fm-spacing-8,8px) var(--fm-spacing-10,10px);color:var(--fm-text-muted,#6b6c70)}",
63
+ // Rebus has no segmented control. Two bordered chips side by side read as two independent
64
+ // buttons; one sunken track with a raised selected pill reads as one choice, and matches the
65
+ // toggle the classic report puts on a chart. 28px + 4px padding = the 36px chip row height.
66
+ ".fm-seg{display:inline-flex;align-items:center;gap:2px;max-width:100%;flex-wrap:wrap;",
67
+ "padding:var(--fm-spacing-4,4px);border-radius:var(--fm-radius-sm,8px);",
68
+ "background:var(--fm-bg-subtle,#f6f6f8)}",
69
+ ".fm-seg-item{display:inline-flex;align-items:center;justify-content:center;height:28px;",
70
+ "padding:0 var(--fm-spacing-12,12px);border:0;border-radius:var(--fm-radius-xs,4px);",
71
+ "background:none;color:var(--fm-text-muted,#6b6c70);font:inherit;cursor:pointer;white-space:nowrap}",
72
+ ".fm-seg-item:hover:not([aria-pressed=true]):not(:disabled){color:var(--fm-text-primary,#1c1c1e)}",
73
+ ".fm-seg-item[aria-pressed=true]{background:var(--fm-surface-default,#fff);",
74
+ "color:var(--fm-text-primary,#1c1c1e);box-shadow:var(--fm-shadow-xs,0 1px 2px rgb(28 28 30/.06))}",
75
+ ".fm-seg-item:disabled{color:var(--fm-text-disabled,#b2b3b6);cursor:not-allowed}",
76
+ ".fm-seg-item:focus-visible{outline:var(--fm-border-width-regular,1.5px) solid var(--fm-border-brand,#ff7823);outline-offset:-2px}",
77
+ // date: presets column beside the month grid, both over one shared Cancel/Apply foot
78
+ ".fm-dr-body{display:flex;flex:1 1 auto;min-height:0;gap:var(--fm-spacing-8,8px)}",
79
+ ".fm-dr-presets{flex:none;min-width:132px;overflow:auto;",
80
+ "border-right:var(--fm-border-width-thin,1px) solid var(--fm-border-default,#f0f0f0);",
81
+ "padding-right:var(--fm-spacing-4,4px)}",
82
+ ".fm-cal{display:flex;flex-direction:column;gap:var(--fm-spacing-8,8px);padding:0 var(--fm-spacing-4,4px)}",
83
+ ".fm-cal-head{display:flex;align-items:center;justify-content:space-between;gap:var(--fm-spacing-8,8px)}",
84
+ ".fm-cal-title{color:var(--fm-text-primary,#1c1c1e)}",
85
+ ".fm-cal-nav{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;",
86
+ "flex:none;padding:0;border:0;border-radius:var(--fm-radius-xs,4px);background:none;",
87
+ "color:var(--fm-icon-primary,#1c1c1e);cursor:pointer}",
88
+ ".fm-cal-nav:hover{background:var(--fm-bg-subtle,#f6f6f8)}",
89
+ ".fm-cal-nav svg{display:block;width:16px;height:16px}",
90
+ ".fm-cal-grid{display:grid;grid-template-columns:repeat(7,32px)}",
91
+ ".fm-cal-dow{display:flex;align-items:center;justify-content:center;height:28px;",
92
+ "color:var(--fm-text-muted,#6b6c70)}",
93
+ // the band is the cell and the endpoint is the button, so a range reads as one continuous run
94
+ ".fm-cal-cell{display:flex;align-items:center;justify-content:center;width:32px;height:32px}",
95
+ ".fm-cal-cell[data-in-range=true]{background:var(--fm-action-primary-bg-subtle,#fff3e6)}",
96
+ ".fm-cal-cell[data-edge=start]{border-radius:var(--fm-radius-full,999px) 0 0 var(--fm-radius-full,999px)}",
97
+ ".fm-cal-cell[data-edge=end]{border-radius:0 var(--fm-radius-full,999px) var(--fm-radius-full,999px) 0}",
98
+ ".fm-cal-cell[data-edge=both]{border-radius:var(--fm-radius-full,999px)}",
99
+ ".fm-cal-day{display:flex;align-items:center;justify-content:center;width:32px;height:32px;",
100
+ "padding:0;border:0;border-radius:var(--fm-radius-full,999px);background:none;font:inherit;",
101
+ "color:var(--fm-text-primary,#1c1c1e);cursor:pointer}",
102
+ ".fm-cal-day:hover:not([data-selected=true]){background:var(--fm-bg-subtle,#f6f6f8)}",
103
+ ".fm-cal-day[data-outside=true]{color:var(--fm-text-disabled,#b2b3b6)}",
104
+ ".fm-cal-day[data-today=true]:not([data-selected=true]){",
105
+ "box-shadow:inset 0 0 0 var(--fm-border-width-regular,1.5px) var(--fm-border-brand,#ff7823)}",
106
+ ".fm-cal-day[data-selected=true]{background:var(--fm-action-primary-bg,#ff7823);",
107
+ "color:var(--fm-text-inverse,#fff);font-weight:600}",
108
+ // a clickable row is `fmr-row-btn` in the frame's fmr-table rules, the same look a
109
+ // hand-written table gets; `fm-row-btn` stays as this table's own hook
52
110
  ".fm-dlg{position:fixed;inset:0;z-index:25;display:flex;align-items:center;justify-content:center;",
53
- "padding:24px;background:rgba(28,28,30,.4)}",
111
+ "padding:24px;background:var(--fm-overlay-scrim-2,rgb(18 18 18/.48))}",
54
112
  ".fm-dlg[hidden]{display:none}",
55
113
  ".fm-dlg-card{position:relative;display:flex;flex-direction:column;min-width:320px;",
56
- "max-width:min(720px,100%);max-height:100%;overflow:hidden;border-radius:12px;background:#fff;",
57
- "box-shadow:0 12px 48px rgba(28,28,30,.24);outline:none}",
114
+ "width:min(720px,100%);max-height:100%;overflow:hidden;border-radius:var(--fm-radius-md,12px);",
115
+ "background:var(--fm-surface-floating,#fff);box-shadow:var(--fm-shadow-2xl,0 12px 24px rgb(28 28 30/.14));outline:none}",
58
116
  ".fm-dlg-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;",
59
- "padding:16px 20px;border-bottom:1px solid var(--fm-line,#f0f0f0)}",
60
- ".fm-dlg-title{margin:0;font-size:16px;font-weight:600;color:var(--fm-fg,#1c1c1e)}",
61
- ".fm-dlg-subtitle{margin:2px 0 0;font-size:12px;color:var(--fm-muted,#6b6c70)}",
62
- ".fm-dlg-close{border:0;background:none;padding:4px 8px;font-size:14px;",
63
- "color:var(--fm-muted,#6b6c70);cursor:pointer;border-radius:4px}",
64
- ".fm-dlg-close:hover{background:#f2f2f7;color:var(--fm-fg,#1c1c1e)}",
117
+ "padding:16px 20px;border-bottom:var(--fm-border-width-thin,1px) solid var(--fm-border-default,#f0f0f0)}",
118
+ ".fm-dlg-title{margin:0;font-size:16px;font-weight:600;color:var(--fm-text-primary,#1c1c1e)}",
119
+ ".fm-dlg-subtitle{margin:2px 0 0;font-size:12px;color:var(--fm-text-muted,#6b6c70)}",
120
+ ".fm-dlg-close{border:0;background:none;padding:0 6px;font-size:14px;",
121
+ "min-width:28px;min-height:28px;display:inline-flex;align-items:center;justify-content:center;",
122
+ "color:var(--fm-icon-muted,#6b6c70);cursor:pointer;border-radius:var(--fm-radius-xs,4px)}",
123
+ ".fm-dlg-close:hover{background:var(--fm-bg-subtle,#f6f6f8);color:var(--fm-icon-primary,#1c1c1e)}",
65
124
  ".fm-dlg-body{overflow:auto;overscroll-behavior:contain;padding:0 20px 16px}",
66
125
  // the shell scrims busy sections; a dialog hosted inside one inherits the same treatment
67
126
  "section[data-fm-busy] .fm-dlg-card::after{content:'';position:absolute;inset:0;",
@@ -83,6 +142,86 @@
83
142
  return node
84
143
  }
85
144
 
145
+ var SVG_NS = "http://www.w3.org/2000/svg"
146
+
147
+ // Rebus draws every glyph with FmIcon, which the frame has no registry for, and the CSP
148
+ // (`font-src data:`) rules an icon font out — so the handful the controls need are drawn here.
149
+ var ICONS = {
150
+ calendar:
151
+ "M6.5 2.5v3|M13.5 2.5v3|M3.25 8.25h13.5|M4.75 4.5h10.5a1.5 1.5 0 0 1 1.5 1.5v10a1.5 1.5 0 0 1-1.5 1.5H4.75a1.5 1.5 0 0 1-1.5-1.5V6a1.5 1.5 0 0 1 1.5-1.5z",
152
+ store:
153
+ "M3.5 8.5V16a.5.5 0 0 0 .5.5h12a.5.5 0 0 0 .5-.5V8.5|M2.75 8.5 4.2 4.1a1 1 0 0 1 .95-.6h9.7a1 1 0 0 1 .95.6L17.25 8.5|M2.75 8.5h14.5",
154
+ search: "M9 15.5a6.5 6.5 0 1 0 0-13 6.5 6.5 0 0 0 0 13z|M13.8 13.8 17.5 17.5",
155
+ chevronLeft: "M12.5 4.5 7 10l5.5 5.5",
156
+ chevronRight: "M7.5 4.5 13 10l-5.5 5.5",
157
+ check: "M4.8 10.4 8.3 13.9 15.2 6.6",
158
+ minus: "M5.2 10h9.6"
159
+ }
160
+
161
+ /** A registry name, or raw path data; `|` separates subpaths. */
162
+ function icon(name, width) {
163
+ var svg = document.createElementNS(SVG_NS, "svg")
164
+ svg.setAttribute("viewBox", "0 0 20 20")
165
+ svg.setAttribute("fill", "none")
166
+ svg.setAttribute("aria-hidden", "true")
167
+ ;(ICONS[name] || name).split("|").forEach(function (segment) {
168
+ var path = document.createElementNS(SVG_NS, "path")
169
+ path.setAttribute("d", segment)
170
+ path.setAttribute("stroke", "currentColor")
171
+ path.setAttribute("stroke-width", String(width || 1.6))
172
+ path.setAttribute("stroke-linecap", "round")
173
+ path.setAttribute("stroke-linejoin", "round")
174
+ svg.appendChild(path)
175
+ })
176
+ return svg
177
+ }
178
+
179
+ /** The Rebus chip is the canvas trigger; only the caret is ours, since Rebus ships none. */
180
+ function chipTrigger(glyph) {
181
+ var button = el("button", "fm-chip fm-btn")
182
+ button.type = "button"
183
+ if (glyph) {
184
+ var slot = el("span", "fm-chip__icon")
185
+ slot.appendChild(icon(glyph))
186
+ button.appendChild(slot)
187
+ }
188
+ var label = el("span", "fm-chip__label fm-text-body-md-500")
189
+ button.appendChild(label)
190
+ button.appendChild(el("span", "fm-caret", "\u25be"))
191
+ return { button: button, label: label }
192
+ }
193
+
194
+ /** Rebus button anatomy; `hook` is the behavior class the tests address it by. */
195
+ function actionButton(text, variant, hook) {
196
+ var button = el("button", "fm-button fm-button--" + variant + " fm-button--sm fm-button--layout-default" + (hook ? " " + hook : ""))
197
+ button.type = "button"
198
+ var labels = el("span", "fm-button__labels")
199
+ labels.appendChild(el("span", "fm-button__label fm-text-body-md-500", text))
200
+ button.appendChild(labels)
201
+ return button
202
+ }
203
+
204
+ /** Rebus checkbox anatomy: the native input is the state, the box and glyph are the picture. */
205
+ function checkbox(checked, indeterminate) {
206
+ var root = el("span", "fm-checkbox fm-checkbox--md")
207
+ var marked = !!checked || !!indeterminate
208
+ if (marked) root.classList.add("fm-checkbox--selected")
209
+ var box = el("span", "fm-checkbox__box")
210
+ var input = el("input", "fm-checkbox__input")
211
+ input.type = "checkbox"
212
+ input.checked = !!checked
213
+ input.indeterminate = !!indeterminate
214
+ input.tabIndex = -1
215
+ box.appendChild(input)
216
+ if (marked) {
217
+ var glyph = el("span", "fm-checkbox__glyph")
218
+ glyph.appendChild(icon(indeterminate ? "minus" : "check", 2.2))
219
+ box.appendChild(glyph)
220
+ }
221
+ root.appendChild(box)
222
+ return root
223
+ }
224
+
86
225
  function resolve(target) {
87
226
  var node = typeof target === "string" ? document.querySelector(target) : target
88
227
  if (!node) throw new Error("FM.ui: no element for " + target)
@@ -119,12 +258,63 @@
119
258
  })
120
259
  })
121
260
 
122
- function popover(root, button, panel) {
261
+ var POP_GAP = 4
262
+ var POP_EDGE = 8
263
+ var POP_MIN_ROOM = 120
264
+
265
+ /**
266
+ * Places a fixed panel by its chip: below when it fits, above when only that fits, and no taller
267
+ * than the room on that side, so the Apply foot stays reachable and the list scrolls instead.
268
+ * `natural` is the panel's unconstrained height, measured once per opening.
269
+ */
270
+ function placePanel(button, panel, natural) {
271
+ var chip = button.getBoundingClientRect()
272
+ var vw = document.documentElement.clientWidth
273
+ var vh = document.documentElement.clientHeight
274
+ var below = vh - chip.bottom - POP_GAP - POP_EDGE
275
+ var above = chip.top - POP_GAP - POP_EDGE
276
+ var up = natural > below && above > below
277
+ var room = Math.max(up ? above : below, POP_MIN_ROOM)
278
+ panel.style.maxHeight = natural > room ? room + "px" : ""
279
+ panel.style.minWidth = Math.max(chip.width, 248) + "px"
280
+ var height = panel.offsetHeight
281
+ var width = panel.offsetWidth
282
+ var top = up ? Math.max(POP_EDGE, chip.top - POP_GAP - height) : chip.bottom + POP_GAP
283
+ var left = Math.min(Math.max(POP_EDGE, chip.left), Math.max(POP_EDGE, vw - POP_EDGE - width))
284
+ panel.style.top = Math.round(top) + "px"
285
+ panel.style.left = Math.round(left) + "px"
286
+ }
287
+
288
+ function popover(root, button, panel, hooks) {
123
289
  var open = false
290
+ var natural = 0
291
+ hooks = hooks || {}
292
+ function place() {
293
+ placePanel(button, panel, natural)
294
+ }
295
+ // the panel follows its chip while the report scrolls, and re-fits when its own content changes
296
+ var observer = typeof ResizeObserver === "function" ? new ResizeObserver(place) : null
297
+ /** `onOpen` seeds the draft, `onClose` discards it — an outside click is a Cancel. */
124
298
  function set(next) {
299
+ var was = open
125
300
  open = next
301
+ if (!was && open && hooks.onOpen) hooks.onOpen()
126
302
  panel.hidden = !open
127
303
  button.setAttribute("aria-expanded", String(open))
304
+ button.classList.toggle("fm-chip--selected", open)
305
+ if (open && !was) {
306
+ panel.style.maxHeight = ""
307
+ natural = panel.offsetHeight
308
+ place()
309
+ window.addEventListener("scroll", place, true)
310
+ window.addEventListener("resize", place)
311
+ if (observer) observer.observe(panel)
312
+ } else if (was && !open) {
313
+ window.removeEventListener("scroll", place, true)
314
+ window.removeEventListener("resize", place)
315
+ if (observer) observer.disconnect()
316
+ }
317
+ if (was && !open && hooks.onClose) hooks.onClose()
128
318
  }
129
319
  button.addEventListener("click", function (event) {
130
320
  event.stopPropagation()
@@ -133,18 +323,29 @@
133
323
  panel.addEventListener("click", function (event) {
134
324
  event.stopPropagation()
135
325
  })
136
- registerPopover(function (event) {
326
+ root.addEventListener("keydown", function (event) {
327
+ if (event.key !== "Escape" || !open) return
328
+ event.stopPropagation()
329
+ set(false)
330
+ button.focus()
331
+ })
332
+ var unregister = registerPopover(function (event) {
137
333
  if (open && !root.contains(event.target)) set(false)
138
334
  })
139
335
  set(false)
140
- return { close: function () { set(false) }, isOpen: function () { return open } }
336
+ return {
337
+ close: function () { set(false) },
338
+ isOpen: function () { return open },
339
+ destroy: function () { set(false); unregister() }
340
+ }
141
341
  }
142
342
 
143
343
  // --- dropdown ------------------------------------------------------------
144
344
 
145
345
  /**
146
346
  * Single or multi select. `value` is one value, or an array when `multiple`.
147
- * Multi-select fires onChange on close/Apply, not per click.
347
+ * Single commits on click; multi collects ticks and commits on Apply, as the classic
348
+ * outlet picker does — one query per decision, not one per tick.
148
349
  */
149
350
  function dropdown(target, options) {
150
351
  ensureStyle()
@@ -153,52 +354,97 @@
153
354
  var items = normalizeOptions(config.options)
154
355
  var multiple = !!config.multiple
155
356
  var searchable = config.searchable === undefined ? items.length > 8 : !!config.searchable
156
- var selected = multiple ? asArray(config.value) : config.value
357
+ var committed = multiple ? asArray(config.value) : config.value
358
+ var draft = multiple ? committed.slice() : committed
157
359
  var filter = ""
360
+ /** `{ label, active, onSelect }` — a named action, never inferred from a full selection. */
361
+ var all = config.all || null
158
362
 
159
363
  var root = el("div", "fm-c")
160
- var button = el("button", "fm-btn")
161
- var label = el("span")
162
- var caret = el("span", "fm-caret", "▾")
163
- button.appendChild(label)
164
- button.appendChild(caret)
364
+ var trigger = chipTrigger(config.icon)
365
+ var button = trigger.button
366
+ var label = trigger.label
165
367
  var panel = el("div", "fm-pop")
166
368
  panel.hidden = true
167
- var search = el("input", "fm-search")
369
+ var searchBar = el("div", "fm-search-bar fm-search-bar--sm")
370
+ var leading = el("span", "fm-search-bar__leading")
371
+ leading.appendChild(icon("search"))
372
+ searchBar.appendChild(leading)
373
+ var search = el("input", "fm-search fm-search-bar__control fm-text-body-md-400")
168
374
  search.type = "search"
169
375
  search.placeholder = "Search"
170
- var list = el("div")
171
- if (searchable) panel.appendChild(search)
376
+ search.setAttribute("aria-label", "Search options")
377
+ searchBar.appendChild(search)
378
+ var list = el("div", "fm-pop-list")
379
+ var allRow = null
380
+ if (all) {
381
+ allRow = el("div", "fm-opt fm-opt-all")
382
+ allRow.appendChild(el("span", null, all.label || "All"))
383
+ var mark = el("span", "fm-opt-mark")
384
+ mark.appendChild(icon("check", 2.2))
385
+ allRow.appendChild(mark)
386
+ allRow.addEventListener("click", function () {
387
+ control.close()
388
+ if (typeof all.onSelect === "function") all.onSelect()
389
+ })
390
+ var allWrap = el("div", "fm-pop-all")
391
+ allWrap.appendChild(allRow)
392
+ panel.appendChild(allWrap)
393
+ }
394
+ if (searchable) panel.appendChild(searchBar)
172
395
  panel.appendChild(list)
396
+ if (multiple) {
397
+ var foot = el("div", "fm-pop-foot")
398
+ var cancel = actionButton("Cancel", "ghost", "fm-dd-cancel")
399
+ var apply = actionButton("Apply", "primary", "fm-dd-apply")
400
+ cancel.addEventListener("click", function () { control.close() })
401
+ apply.addEventListener("click", function () {
402
+ committed = draft.slice()
403
+ control.close()
404
+ emit()
405
+ })
406
+ foot.appendChild(cancel)
407
+ foot.appendChild(apply)
408
+ panel.appendChild(foot)
409
+ }
173
410
  root.appendChild(button)
174
411
  root.appendChild(panel)
175
412
  host.innerHTML = ""
176
413
  host.appendChild(root)
177
414
 
178
415
  function isSelected(value) {
179
- return multiple ? selected.indexOf(value) >= 0 : selected === value
416
+ return multiple ? draft.indexOf(value) >= 0 : draft === value
180
417
  }
181
418
 
182
419
  function summary() {
420
+ if (all && all.active) return all.label || "All"
183
421
  if (multiple) {
184
- if (selected.length === 0) return config.placeholder || "None"
185
- if (selected.length === items.length && items.length > 0) return config.allLabel || "All"
186
- if (selected.length === 1) {
187
- var one = items.filter(function (item) { return item.value === selected[0] })[0]
188
- return one ? one.label : String(selected[0])
422
+ if (draft.length === 0) return config.placeholder || "None"
423
+ // with a named All action beside it, a full list is a full list, never a claim of All
424
+ if (!all && draft.length === items.length && items.length > 0)
425
+ return config.allLabel || "All"
426
+ if (draft.length === 1) {
427
+ var one = items.filter(function (item) { return item.value === draft[0] })[0]
428
+ return one ? one.label : String(draft[0])
189
429
  }
190
- return selected.length + " selected"
430
+ return draft.length + " selected"
191
431
  }
192
- var match = items.filter(function (item) { return item.value === selected })[0]
432
+ var match = items.filter(function (item) { return item.value === draft })[0]
193
433
  return match ? match.label : config.placeholder || "Select"
194
434
  }
195
435
 
196
436
  function emit() {
197
437
  if (typeof config.onChange === "function") {
198
- config.onChange(multiple ? selected.slice() : selected)
438
+ config.onChange(multiple ? committed.slice() : committed)
199
439
  }
200
440
  }
201
441
 
442
+ function repaint() {
443
+ label.textContent = summary()
444
+ if (allRow) allRow.setAttribute("aria-selected", String(!!(all && all.active)))
445
+ renderList()
446
+ }
447
+
202
448
  function renderList() {
203
449
  list.innerHTML = ""
204
450
  var needle = filter.toLowerCase()
@@ -208,46 +454,44 @@
208
454
  if (multiple && items.length > 1 && visible.length > 0) {
209
455
  // Select all acts on what the search leaves visible, as the classic outlet picker does.
210
456
  var shown = visible.map(function (item) { return item.value })
211
- var allShownSelected = shown.length > 0 && shown.every(isSelected)
212
- var all = el("div", "fm-opt", allShownSelected ? "Clear all" : "Select all")
213
- all.setAttribute("aria-selected", String(allShownSelected))
214
- all.addEventListener("click", function () {
215
- selected = allShownSelected
216
- ? selected.filter(function (value) { return shown.indexOf(value) < 0 })
217
- : selected.concat(shown.filter(function (value) { return !isSelected(value) }))
218
- label.textContent = summary()
219
- renderList()
220
- emit()
457
+ var picked = shown.filter(isSelected).length
458
+ var allShownSelected = picked === shown.length
459
+ var toggleAll = el("div", "fm-opt fm-opt-check")
460
+ toggleAll.setAttribute("aria-selected", String(allShownSelected))
461
+ toggleAll.appendChild(checkbox(allShownSelected, picked > 0 && !allShownSelected))
462
+ toggleAll.appendChild(
463
+ el("span", null, "Select all (" + picked + "/" + shown.length + ")")
464
+ )
465
+ toggleAll.addEventListener("click", function () {
466
+ draft = allShownSelected
467
+ ? draft.filter(function (value) { return shown.indexOf(value) < 0 })
468
+ : draft.concat(shown.filter(function (value) { return !isSelected(value) }))
469
+ repaint()
221
470
  })
222
- list.appendChild(all)
471
+ list.appendChild(toggleAll)
223
472
  }
224
473
  if (visible.length === 0) {
225
474
  list.appendChild(el("div", "fm-empty", "No matches"))
226
475
  return
227
476
  }
228
477
  visible.forEach(function (item) {
229
- var option = el("div", "fm-opt")
478
+ var option = el("div", multiple ? "fm-opt fm-opt-check" : "fm-opt")
230
479
  option.setAttribute("role", "option")
231
480
  option.setAttribute("aria-selected", String(isSelected(item.value)))
232
- if (multiple) {
233
- var box = el("input")
234
- box.type = "checkbox"
235
- box.checked = isSelected(item.value)
236
- box.tabIndex = -1
237
- option.appendChild(box)
238
- }
481
+ if (multiple) option.appendChild(checkbox(isSelected(item.value)))
239
482
  option.appendChild(el("span", null, item.label))
240
483
  option.addEventListener("click", function () {
241
484
  if (multiple) {
242
- selected = isSelected(item.value)
243
- ? selected.filter(function (value) { return value !== item.value })
244
- : selected.concat([item.value])
245
- } else {
246
- selected = item.value
247
- control.close()
485
+ draft = isSelected(item.value)
486
+ ? draft.filter(function (value) { return value !== item.value })
487
+ : draft.concat([item.value])
488
+ repaint()
489
+ return
248
490
  }
249
- label.textContent = summary()
250
- renderList()
491
+ draft = item.value
492
+ committed = draft
493
+ control.close()
494
+ repaint()
251
495
  emit()
252
496
  })
253
497
  list.appendChild(option)
@@ -259,24 +503,46 @@
259
503
  renderList()
260
504
  })
261
505
 
262
- var control = popover(root, button, panel)
263
- label.textContent = summary()
264
- renderList()
506
+ var control = popover(root, button, panel, {
507
+ onOpen: function () {
508
+ draft = multiple ? committed.slice() : committed
509
+ // a reopened picker showing the last search reads as a broken list
510
+ filter = ""
511
+ search.value = ""
512
+ repaint()
513
+ },
514
+ // an outside click, Cancel or Escape is a discard; Apply commits first, so this is a no-op
515
+ onClose: function () {
516
+ draft = multiple ? committed.slice() : committed
517
+ repaint()
518
+ }
519
+ })
520
+ repaint()
265
521
 
266
522
  return {
267
523
  element: root,
268
- getValue: function () { return multiple ? selected.slice() : selected },
524
+ getValue: function () { return multiple ? committed.slice() : committed },
269
525
  setValue: function (next) {
270
- selected = multiple ? asArray(next) : next
271
- label.textContent = summary()
272
- renderList()
526
+ committed = multiple ? asArray(next) : next
527
+ // a param push mid-edit must not wipe the merchant's ticks
528
+ if (!control.isOpen()) draft = multiple ? committed.slice() : committed
529
+ repaint()
273
530
  },
274
531
  setOptions: function (next) {
275
532
  items = normalizeOptions(next)
276
- label.textContent = summary()
277
- renderList()
533
+ repaint()
278
534
  },
279
- destroy: function () { host.innerHTML = "" }
535
+ /** Marks the named All action live; the caller owns what that means. */
536
+ setAll: function (active) {
537
+ if (!all) return
538
+ all.active = !!active
539
+ repaint()
540
+ },
541
+ setDisabled: function (flag) {
542
+ button.disabled = !!flag
543
+ if (flag) control.close()
544
+ },
545
+ destroy: function () { control.destroy(); host.innerHTML = "" }
280
546
  }
281
547
  }
282
548
 
@@ -374,69 +640,124 @@
374
640
  * Preset list plus a custom range. Emits `{ from, to, preset }` with ISO instants at the
375
641
  * report timezone's day boundaries — the section decides which params they land on.
376
642
  */
643
+ var SINGLE_PRESETS = DEFAULT_PRESETS.filter(function (preset) {
644
+ return preset.key === "today" || preset.key === "yesterday"
645
+ })
646
+
647
+ function dowOf(ymd) {
648
+ return new Date(Date.parse(ymd + "T00:00:00.000Z")).getUTCDay()
649
+ }
650
+
651
+ /** Grid labels and titles read in the report's locale; the ymd is a UTC instant by construction. */
652
+ function partsOf(ymd, options) {
653
+ try {
654
+ var spec = { timeZone: "UTC" }
655
+ Object.keys(options).forEach(function (key) { spec[key] = options[key] })
656
+ return new Intl.DateTimeFormat(
657
+ (window.env && window.env.locale) || "en-US",
658
+ spec
659
+ ).format(new Date(Date.parse(ymd + "T00:00:00.000Z")))
660
+ } catch (err) {
661
+ return ymd
662
+ }
663
+ }
664
+
665
+ function dayNames() {
666
+ var out = []
667
+ // 2026-03-01 is a Sunday: the grid is Sunday-first like the classic calendar, while the
668
+ // week presets above stay Monday-first like the classic report's week boundaries.
669
+ for (var i = 0; i < 7; i++) out.push(partsOf(shiftYmd("2026-03-01", i), { weekday: "short" }))
670
+ return out
671
+ }
672
+
673
+ // Day-first like the classic filter chip beside it, with the month name still localized:
674
+ // Intl's own en-US order ("Jul 01, 2026") would read differently from the classic report.
675
+ function dayLabel(ymd) {
676
+ return ymd.slice(8) + " " + partsOf(ymd, { month: "short" })
677
+ }
678
+
679
+ /** "09 Sep 2026", or "09 Sep \u2013 15 Sep 2026" with one year when the range shares it. */
680
+ function describeRange(fromYmd, toYmd) {
681
+ var year = " " + toYmd.slice(0, 4)
682
+ if (fromYmd === toYmd) return dayLabel(fromYmd) + year
683
+ if (fromYmd.slice(0, 4) !== toYmd.slice(0, 4)) {
684
+ return dayLabel(fromYmd) + " " + fromYmd.slice(0, 4) + " \u2013 " + dayLabel(toYmd) + year
685
+ }
686
+ return dayLabel(fromYmd) + " \u2013 " + dayLabel(toYmd) + year
687
+ }
688
+
689
+ /** `single: true` picks one day: one grid, and both bounds land on that day. */
377
690
  function dateRange(target, options) {
378
691
  ensureStyle()
379
692
  var host = resolve(target)
380
693
  var config = options || {}
381
694
  var timezone = config.timezone || (window.env && window.env.timezone) || "UTC"
382
- var presets = config.presets || DEFAULT_PRESETS
695
+ var single = !!config.single
696
+ var presets = config.presets || (single ? SINGLE_PRESETS : DEFAULT_PRESETS)
383
697
  var todayYmd = ymdIn(new Date(), timezone)
384
698
  var value = config.value || {}
385
- var fromYmd = value.from ? ymdIn(new Date(value.from), timezone) : shiftYmd(todayYmd, -29)
386
- var toYmd = value.to ? ymdIn(new Date(value.to), timezone) : todayYmd
699
+ var fromYmd = value.from ? ymdIn(new Date(value.from), timezone) : single ? todayYmd : shiftYmd(todayYmd, -29)
700
+ var toYmd = single ? fromYmd : value.to ? ymdIn(new Date(value.to), timezone) : todayYmd
387
701
  var presetKey = config.preset || null
702
+ // draft: what the open popover is editing, committed only by Apply
703
+ var dFrom = fromYmd
704
+ var dTo = toYmd
705
+ var dPreset = presetKey
706
+ var anchor = null
707
+ var viewYmd = monthStart(dFrom, 0)
388
708
 
389
709
  var root = el("div", "fm-c")
390
- var button = el("button", "fm-btn")
391
- var label = el("span")
392
- button.appendChild(label)
393
- button.appendChild(el("span", "fm-caret", "▾"))
710
+ var trigger = chipTrigger(config.icon === null ? null : config.icon || "calendar")
711
+ var button = trigger.button
712
+ var label = trigger.label
394
713
  var panel = el("div", "fm-pop")
395
714
  panel.hidden = true
396
715
  var body = el("div", "fm-dr-body")
397
716
  var presetList = el("div", "fm-dr-presets")
398
- var custom = el("div", "fm-dr-custom")
399
- var fromLabel = el("label")
400
- var toLabel = el("label")
401
- fromLabel.appendChild(el("span", null, "From"))
402
- toLabel.appendChild(el("span", null, "To"))
403
- var fromInput = el("input")
404
- var toInput = el("input")
405
- fromInput.type = "date"
406
- toInput.type = "date"
407
- fromLabel.appendChild(fromInput)
408
- toLabel.appendChild(toInput)
409
- var apply = el("button", "fm-dr-apply", "Apply")
410
- custom.appendChild(fromLabel)
411
- custom.appendChild(toLabel)
412
- custom.appendChild(apply)
717
+ var calendar = el("div", "fm-cal")
718
+ var head = el("div", "fm-cal-head")
719
+ var prev = el("button", "fm-cal-nav fm-cal-prev")
720
+ prev.type = "button"
721
+ prev.setAttribute("aria-label", "Previous month")
722
+ prev.appendChild(icon("chevronLeft"))
723
+ var next = el("button", "fm-cal-nav fm-cal-next")
724
+ next.type = "button"
725
+ next.setAttribute("aria-label", "Next month")
726
+ next.appendChild(icon("chevronRight"))
727
+ var title = el("div", "fm-cal-title fm-text-body-md-500")
728
+ var grid = el("div", "fm-cal-grid")
729
+ head.appendChild(prev)
730
+ head.appendChild(title)
731
+ head.appendChild(next)
732
+ calendar.appendChild(head)
733
+ calendar.appendChild(grid)
413
734
  body.appendChild(presetList)
414
- body.appendChild(custom)
735
+ body.appendChild(calendar)
415
736
  panel.appendChild(body)
737
+ var foot = el("div", "fm-pop-foot")
738
+ var cancel = actionButton("Cancel", "ghost", "fm-dr-cancel")
739
+ var apply = actionButton("Apply", "primary", "fm-dr-apply")
740
+ foot.appendChild(cancel)
741
+ foot.appendChild(apply)
742
+ panel.appendChild(foot)
416
743
  root.appendChild(button)
417
744
  root.appendChild(panel)
418
745
  host.innerHTML = ""
419
746
  host.appendChild(root)
420
747
 
748
+ var DOW = dayNames()
749
+
421
750
  function summary() {
422
- var active = presets.filter(function (preset) { return preset.key === presetKey })[0]
423
- if (active) return active.label
424
- return fromYmd === toYmd ? fromYmd : fromYmd + " – " + toYmd
751
+ return describeRange(fromYmd, toYmd)
425
752
  }
426
753
 
427
- function emit() {
428
- label.textContent = summary()
429
- fromInput.value = fromYmd
430
- toInput.value = toYmd
431
- renderPresets()
432
- if (typeof config.onChange === "function") {
433
- config.onChange({
434
- from: boundaryIso(fromYmd, timezone, false),
435
- to: boundaryIso(toYmd, timezone, true),
436
- fromDate: fromYmd,
437
- toDate: toYmd,
438
- preset: presetKey
439
- })
754
+ function pick() {
755
+ return {
756
+ from: boundaryIso(fromYmd, timezone, false),
757
+ to: boundaryIso(toYmd, timezone, true),
758
+ fromDate: fromYmd,
759
+ toDate: toYmd,
760
+ preset: presetKey
440
761
  }
441
762
  }
442
763
 
@@ -444,61 +765,119 @@
444
765
  presetList.innerHTML = ""
445
766
  presets.forEach(function (preset) {
446
767
  var option = el("div", "fm-opt", preset.label)
447
- option.setAttribute("aria-selected", String(preset.key === presetKey))
768
+ option.setAttribute("aria-selected", String(preset.key === dPreset))
448
769
  option.addEventListener("click", function () {
449
770
  var range = preset.range(ymdIn(new Date(), timezone))
450
- fromYmd = range[0]
451
- toYmd = range[1]
452
- presetKey = preset.key
453
- control.close()
454
- emit()
771
+ dFrom = range[0]
772
+ dTo = single ? range[0] : range[1]
773
+ dPreset = preset.key
774
+ anchor = null
775
+ viewYmd = monthStart(dFrom, 0)
776
+ renderDraft()
455
777
  })
456
778
  presetList.appendChild(option)
457
779
  })
458
780
  }
459
781
 
460
- apply.addEventListener("click", function () {
461
- if (!fromInput.value || !toInput.value) return
462
- fromYmd = fromInput.value
463
- toYmd = toInput.value
464
- if (fromYmd > toYmd) {
465
- var swap = fromYmd
466
- fromYmd = toYmd
467
- toYmd = swap
782
+ function renderCalendar() {
783
+ title.textContent = partsOf(viewYmd, { month: "long", year: "numeric" })
784
+ grid.innerHTML = ""
785
+ DOW.forEach(function (name) {
786
+ grid.appendChild(el("div", "fm-cal-dow fm-text-body-sm-400", name))
787
+ })
788
+ var start = shiftYmd(viewYmd, -dowOf(viewYmd))
789
+ var month = viewYmd.slice(0, 7)
790
+ // six rows always: the popover keeps one height as the merchant pages through months
791
+ for (var index = 0; index < 42; index++) {
792
+ var ymd = shiftYmd(start, index)
793
+ var inRange = ymd >= dFrom && ymd <= dTo
794
+ var edge = ymd === dFrom ? (ymd === dTo ? "both" : "start") : ymd === dTo ? "end" : ""
795
+ var cell = el("div", "fm-cal-cell")
796
+ if (inRange) cell.setAttribute("data-in-range", "true")
797
+ if (edge) cell.setAttribute("data-edge", edge)
798
+ var day = el("button", "fm-cal-day fm-text-body-md-400", String(Number(ymd.slice(8))))
799
+ day.type = "button"
800
+ day.setAttribute("data-ymd", ymd)
801
+ if (ymd.slice(0, 7) !== month) day.setAttribute("data-outside", "true")
802
+ if (ymd === todayYmd) day.setAttribute("data-today", "true")
803
+ if (edge) day.setAttribute("data-selected", "true")
804
+ day.addEventListener("click", pickDay.bind(null, ymd))
805
+ cell.appendChild(day)
806
+ grid.appendChild(cell)
807
+ }
808
+ }
809
+
810
+ /** First click sets one end, the second the other; a third starts over. */
811
+ function pickDay(ymd) {
812
+ dPreset = null
813
+ if (single || anchor === null) {
814
+ anchor = single ? null : ymd
815
+ dFrom = ymd
816
+ dTo = ymd
817
+ } else {
818
+ dFrom = anchor < ymd ? anchor : ymd
819
+ dTo = anchor < ymd ? ymd : anchor
820
+ anchor = null
468
821
  }
469
- presetKey = null
822
+ renderDraft()
823
+ }
824
+
825
+ function renderDraft() {
826
+ renderPresets()
827
+ renderCalendar()
828
+ }
829
+
830
+ function seedDraft() {
831
+ dFrom = fromYmd
832
+ dTo = toYmd
833
+ dPreset = presetKey
834
+ anchor = null
835
+ viewYmd = monthStart(dFrom, 0)
836
+ renderDraft()
837
+ }
838
+
839
+ prev.addEventListener("click", function () {
840
+ viewYmd = monthStart(viewYmd, 1)
841
+ renderCalendar()
842
+ })
843
+ next.addEventListener("click", function () {
844
+ viewYmd = monthStart(shiftYmd(monthEnd(viewYmd), 1), 0)
845
+ renderCalendar()
846
+ })
847
+ cancel.addEventListener("click", function () {
470
848
  control.close()
471
- emit()
849
+ })
850
+ apply.addEventListener("click", function () {
851
+ fromYmd = dFrom
852
+ toYmd = dTo
853
+ presetKey = dPreset
854
+ control.close()
855
+ label.textContent = summary()
856
+ if (typeof config.onChange === "function") config.onChange(pick())
472
857
  })
473
858
 
474
- var control = popover(root, button, panel)
859
+ var control = popover(root, button, panel, { onOpen: seedDraft, onClose: seedDraft })
475
860
  label.textContent = summary()
476
- fromInput.value = fromYmd
477
- toInput.value = toYmd
478
- renderPresets()
861
+ renderDraft()
479
862
 
480
863
  return {
481
864
  element: root,
482
- getValue: function () {
483
- return {
484
- from: boundaryIso(fromYmd, timezone, false),
485
- to: boundaryIso(toYmd, timezone, true),
486
- fromDate: fromYmd,
487
- toDate: toYmd,
488
- preset: presetKey
489
- }
490
- },
865
+ getValue: pick,
491
866
  setValue: function (next) {
492
867
  if (!next) return
493
868
  if (next.from) fromYmd = ymdIn(new Date(next.from), timezone)
494
- if (next.to) toYmd = ymdIn(new Date(next.to), timezone)
869
+ if (next.to && !single) toYmd = ymdIn(new Date(next.to), timezone)
870
+ if (single) toYmd = fromYmd
495
871
  presetKey = next.preset || null
496
872
  label.textContent = summary()
497
- fromInput.value = fromYmd
498
- toInput.value = toYmd
499
- renderPresets()
873
+ // a param push mid-edit must not move the calendar under the merchant's hand
874
+ if (!control.isOpen()) seedDraft()
500
875
  },
501
- destroy: function () { host.innerHTML = "" }
876
+ setDisabled: function (flag) {
877
+ button.disabled = !!flag
878
+ if (flag) control.close()
879
+ },
880
+ destroy: function () { control.destroy(); host.innerHTML = "" }
502
881
  }
503
882
  }
504
883
 
@@ -516,11 +895,14 @@
516
895
  host.innerHTML = ""
517
896
  host.appendChild(root)
518
897
 
898
+ var disabled = false
899
+
519
900
  function render() {
520
901
  root.innerHTML = ""
521
902
  items.forEach(function (item) {
522
- var button = el("button", null, item.label)
903
+ var button = el("button", "fm-seg-item fm-text-body-md-500", item.label)
523
904
  button.type = "button"
905
+ button.disabled = disabled
524
906
  button.setAttribute("aria-pressed", String(item.value === value))
525
907
  button.addEventListener("click", function () {
526
908
  if (item.value === value) return
@@ -541,6 +923,14 @@
541
923
  value = next
542
924
  render()
543
925
  },
926
+ setOptions: function (next) {
927
+ items = normalizeOptions(next)
928
+ render()
929
+ },
930
+ setDisabled: function (flag) {
931
+ disabled = !!flag
932
+ render()
933
+ },
544
934
  destroy: function () { host.innerHTML = "" }
545
935
  }
546
936
  }
@@ -561,6 +951,9 @@
561
951
  key: column.key,
562
952
  label: column.label === undefined ? column.key : column.label,
563
953
  className: classes.join(" "),
954
+ part: column.part,
955
+ imageOnly: column.format === "image",
956
+ image: column.image || null,
564
957
  format: typeof column.format === "function" ? column.format : null
565
958
  }
566
959
  })
@@ -572,6 +965,28 @@
572
965
  return value === null || value === undefined || value === "" ? "—" : String(value)
573
966
  }
574
967
 
968
+ /**
969
+ * Image values use their own key and occupy an ordinary cell at any column position.
970
+ * The legacy `image` binding composes a thumbnail with this column's text.
971
+ * Missing or failed images retain their box so rows stay aligned.
972
+ */
973
+ function cellNode(column, row) {
974
+ if (!column.imageOnly && !column.image) return el("td", column.className, cellText(column, row))
975
+ var cell = el("td", (column.className ? column.className + " " : "") +
976
+ (column.imageOnly ? "fmr-image-cell" : "fmr-thumb-cell"))
977
+ var img = document.createElement("img")
978
+ img.className = "fmr-thumb"
979
+ img.alt = ""
980
+ img.loading = "lazy"
981
+ var src = row ? row[column.imageOnly ? column.key : column.image] : null
982
+ if (src === null || src === undefined || src === "") img.style.visibility = "hidden"
983
+ else img.src = String(src)
984
+ img.addEventListener("error", function () { img.style.visibility = "hidden" })
985
+ cell.appendChild(img)
986
+ if (!column.imageOnly) cell.appendChild(el("span", "fmr-thumb-text", cellText(column, row)))
987
+ return cell
988
+ }
989
+
575
990
  /**
576
991
  * Paged data table. How long a page is, is a reading decision: a report that IS one table pages
577
992
  * large (50-100), a table under a summary takes the default 20, a tile beside a chart takes 5-10.
@@ -626,7 +1041,9 @@
626
1041
  next.type = "button"
627
1042
 
628
1043
  columns.forEach(function (column) {
629
- headRow.appendChild(el("th", column.className, column.label))
1044
+ var head = el("th", column.className, column.label)
1045
+ if (column.part) head.setAttribute("data-fm-part", column.part)
1046
+ headRow.appendChild(head)
630
1047
  })
631
1048
  thead.appendChild(headRow)
632
1049
  tableEl.appendChild(thead)
@@ -665,11 +1082,11 @@
665
1082
  slice.forEach(function (row, i) {
666
1083
  var tr = el("tr")
667
1084
  columns.forEach(function (column) {
668
- tr.appendChild(el("td", column.className, cellText(column, row)))
1085
+ tr.appendChild(cellNode(column, row))
669
1086
  })
670
1087
  if (onRowClick) {
671
1088
  var absIndex = from + i
672
- tr.className = "fm-row-btn"
1089
+ tr.className = "fm-row-btn fmr-row-btn"
673
1090
  tr.tabIndex = 0
674
1091
  tr.setAttribute("aria-selected", String(isSelected(absIndex, row)))
675
1092
  tr.addEventListener("click", function () { select(absIndex, row) })
@@ -754,7 +1171,9 @@
754
1171
  columns = normalizeColumns(nextColumns)
755
1172
  headRow.textContent = ""
756
1173
  columns.forEach(function (column) {
757
- headRow.appendChild(el("th", column.className, column.label))
1174
+ var head = el("th", column.className, column.label)
1175
+ if (column.part) head.setAttribute("data-fm-part", column.part)
1176
+ headRow.appendChild(head)
758
1177
  })
759
1178
  render()
760
1179
  },