@memberjunction/ng-ui-components 6.1.0-edge.0 → 6.1.0-edge.2

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 (39) hide show
  1. package/README.md +42 -2
  2. package/dist/lib/left-nav/left-nav.component.d.ts +142 -3
  3. package/dist/lib/left-nav/left-nav.component.d.ts.map +1 -1
  4. package/dist/lib/left-nav/left-nav.component.js +391 -117
  5. package/dist/lib/left-nav/left-nav.component.js.map +1 -1
  6. package/dist/lib/tabs/tab-chrome-guard.d.ts +14 -0
  7. package/dist/lib/tabs/tab-chrome-guard.d.ts.map +1 -0
  8. package/dist/lib/tabs/tab-chrome-guard.js +36 -0
  9. package/dist/lib/tabs/tab-chrome-guard.js.map +1 -0
  10. package/dist/lib/tabs/tab-list.directive.d.ts +93 -0
  11. package/dist/lib/tabs/tab-list.directive.d.ts.map +1 -0
  12. package/dist/lib/tabs/tab-list.directive.js +202 -0
  13. package/dist/lib/tabs/tab-list.directive.js.map +1 -0
  14. package/dist/lib/tabs/tabs.scss +242 -0
  15. package/dist/lib/workspace-tabs/workspace-card.component.d.ts +63 -0
  16. package/dist/lib/workspace-tabs/workspace-card.component.d.ts.map +1 -0
  17. package/dist/lib/workspace-tabs/workspace-card.component.js +256 -0
  18. package/dist/lib/workspace-tabs/workspace-card.component.js.map +1 -0
  19. package/dist/lib/workspace-tabs/workspace-tab-store.d.ts +47 -0
  20. package/dist/lib/workspace-tabs/workspace-tab-store.d.ts.map +1 -0
  21. package/dist/lib/workspace-tabs/workspace-tab-store.js +112 -0
  22. package/dist/lib/workspace-tabs/workspace-tab-store.js.map +1 -0
  23. package/dist/lib/workspace-tabs/workspace-tab-strip.component.d.ts +75 -0
  24. package/dist/lib/workspace-tabs/workspace-tab-strip.component.d.ts.map +1 -0
  25. package/dist/lib/workspace-tabs/workspace-tab-strip.component.js +252 -0
  26. package/dist/lib/workspace-tabs/workspace-tab-strip.component.js.map +1 -0
  27. package/dist/lib/workspace-tabs/workspace-tabs.types.d.ts +40 -0
  28. package/dist/lib/workspace-tabs/workspace-tabs.types.d.ts.map +1 -0
  29. package/dist/lib/workspace-tabs/workspace-tabs.types.js +13 -0
  30. package/dist/lib/workspace-tabs/workspace-tabs.types.js.map +1 -0
  31. package/dist/lib/workspace-tabs/workspace-tip.directive.d.ts +34 -0
  32. package/dist/lib/workspace-tabs/workspace-tip.directive.d.ts.map +1 -0
  33. package/dist/lib/workspace-tabs/workspace-tip.directive.js +123 -0
  34. package/dist/lib/workspace-tabs/workspace-tip.directive.js.map +1 -0
  35. package/dist/public-api.d.ts +7 -0
  36. package/dist/public-api.d.ts.map +1 -1
  37. package/dist/public-api.js +7 -0
  38. package/dist/public-api.js.map +1 -1
  39. package/package.json +5 -5
@@ -0,0 +1,242 @@
1
+ /**
2
+ * MJ tab chrome — the ONE visual definition of a browser-style tab strip.
3
+ *
4
+ * Loaded globally by the application (see explorer-app's `styles.scss`), exactly like
5
+ * `button.scss`, so that components in DIFFERENT packages can share one look without either
6
+ * package depending on the other for styling. Two components consume it today:
7
+ *
8
+ * - `mj-tabstrip` (@memberjunction/ng-tabstrip) — tabs declared as projected children,
9
+ * addressed by positional index, bodies owned by the strip.
10
+ * - `mj-workspace-tab-strip` (@memberjunction/ng-ui-components) — a data-driven strip over an
11
+ * array of draft descriptors, addressed by stable id, projecting nothing.
12
+ *
13
+ * Those two have genuinely different API contracts and keep their own state models. What they must
14
+ * NOT differ on is what a tab LOOKS like — hence this file. Anything here is chrome; anything a
15
+ * single strip alone needs (CDK drag affordances, scroll-button placement) stays in that
16
+ * component's own stylesheet.
17
+ *
18
+ * Design tokens only. The look is derived from the draft-workspace strip, which was the
19
+ * token-correct one: the older tabstrip styling used legacy `--gray-*` aliases that resolve to
20
+ * PRIMITIVES and therefore do not re-point in dark mode.
21
+ */
22
+
23
+ /* Max tab width. Tabs size to their text and only grow to this cap, then ellipsize — a fixed width
24
+ leaves short tabs mostly empty. Overridable per strip by re-declaring the custom property. */
25
+ .mj-tabs {
26
+ --mj-tab-max: 210px;
27
+ /* Sentinel read by `warnIfTabChromeMissing` (dev-mode guard): its absence on a rendered strip
28
+ means this sheet isn't loaded in the host app — the silent-unstyled failure mode. */
29
+ --mj-tabs-chrome-loaded: 1;
30
+ }
31
+
32
+ /* The bar = the scrolling tab list plus any pinned trailing control as siblings. The bottom rule
33
+ runs the full width here so it spans that control too, not just the tabs. */
34
+ .mj-tabs__bar {
35
+ display: flex;
36
+ align-items: stretch;
37
+ /* Full-width baseline across the top of the surface — the consistent separator. The active tab
38
+ comes forward by COLOR rather than by clearing this line, because a seamless merge gets
39
+ clipped by the horizontal scroller. */
40
+ border-bottom: 1px solid var(--mj-border-default);
41
+ /* Breathing room above the tabs so they don't jam against a card's top edge or get cut by its
42
+ rounded corner — they read as tabs ON the surface, not sliced off. */
43
+ padding-top: 8px;
44
+ /* Tall enough that a horizontal scrollbar isn't cramped against the tabs. */
45
+ min-height: 46px;
46
+ }
47
+
48
+ .mj-tabs__list {
49
+ display: flex;
50
+ align-items: stretch;
51
+ gap: 4px;
52
+ /* Indented, not flush: the first tab lines up with the surface's content gutter. */
53
+ padding: 0 4px 0 12px;
54
+ flex: 1;
55
+ min-width: 0;
56
+ overflow-x: auto;
57
+ /* MUST be explicit: a `visible` axis paired with a non-`visible` one computes to `auto`, so
58
+ `overflow-x: auto` alone would make the strip vertically scrollable too. (`clip` is NOT an
59
+ option here — paired with an `auto` axis it computes to `hidden` per the same spec rule, which
60
+ is why nothing may give the list vertical overhang: a `hidden` axis is still programmatically
61
+ scrollable, and the browser scrolls a focused tab into view. See the tab's margin note.) */
62
+ overflow-y: hidden;
63
+ scrollbar-width: thin;
64
+ }
65
+
66
+ .mj-tabs__tab {
67
+ /* The tab's ACCENT — one color expressing what state the tab is in, consumed by the active
68
+ state's border and top line so they can never disagree (a warning-bordered tab under a
69
+ brand-blue accent line reads as two components arguing). Brand primary for an ordinary tab;
70
+ the lifecycle modifiers below re-point it to their status color. */
71
+ --mj-tab-accent: var(--mj-brand-primary);
72
+ display: flex;
73
+ align-items: center;
74
+ gap: 7px;
75
+ /* Content-sized up to the cap; flex:none so a tab keeps its content width and the LIST scrolls on
76
+ overflow rather than the tabs squashing. */
77
+ max-width: var(--mj-tab-max, 210px);
78
+ flex: none;
79
+ min-width: 0;
80
+ padding: 8px 4px 8px 11px;
81
+ border: 1px solid var(--mj-border-default);
82
+ border-bottom: none;
83
+ border-radius: 8px 8px 0 0;
84
+ /* Inactive = the PAGE background (the darkest surface tier) so unselected tabs recede, while the
85
+ active tab on --mj-bg-surface comes forward. Consistent ordering in light AND dark. */
86
+ background: var(--mj-bg-page);
87
+ color: var(--mj-text-secondary);
88
+ font-family: inherit;
89
+ font-size: 13px;
90
+ /* Regular arrow over a tab body, like browser tabs — not the clicky finger. */
91
+ cursor: default;
92
+ /* NO negative bottom margin. The classic tab trick of -1px to overlap the bar's rule buys
93
+ nothing here — the active tab comes forward by COLOR and a brand accent, never by clearing
94
+ the baseline — and it cost a real bug: 1px of vertical overhang made the list scrollable by
95
+ exactly 1px, so the whole row jumped every time a tab took keyboard focus. The tab's bottom
96
+ edge simply meets the bar's rule; the active tab's own border matches it, so the seam reads
97
+ as continuous anyway. */
98
+ box-sizing: border-box;
99
+ }
100
+
101
+ .mj-tabs__tab:hover:not(.mj-tabs__tab--active) {
102
+ background: var(--mj-bg-surface-hover);
103
+ color: var(--mj-text-primary);
104
+ }
105
+
106
+ /* Focus lands on the TAB itself (it carries role="tab" and the roving tabindex), so the ring
107
+ belongs here rather than on anything nested. */
108
+ .mj-tabs__tab:focus-visible,
109
+ .mj-tabs__close:focus-visible,
110
+ .mj-tabs__new:focus-visible {
111
+ outline: 2px solid var(--mj-border-focus);
112
+ outline-offset: -2px;
113
+ }
114
+
115
+ /* Optional lifecycle states — only the draft strip sets these today, but a tab that can be
116
+ rejected or completed should look the same wherever that concept turns up. INACTIVE tabs keep
117
+ subtler status borders (they should recede); the full-strength status color is carried by the
118
+ accent variable, which the active state consumes below. */
119
+ .mj-tabs__tab--rejected {
120
+ --mj-tab-accent: var(--mj-status-warning);
121
+ border-color: var(--mj-status-warning-border, var(--mj-status-warning));
122
+ background: color-mix(in srgb, var(--mj-status-warning) 8%, var(--mj-bg-surface-card));
123
+ }
124
+ .mj-tabs__tab--complete {
125
+ --mj-tab-accent: var(--mj-status-success);
126
+ border-color: color-mix(in srgb, var(--mj-status-success) 40%, var(--mj-border-default));
127
+ }
128
+
129
+ /* AFTER the lifecycle modifiers, deliberately: when a tab is both active and rejected/complete,
130
+ the active surface and the accent-colored border must win over the modifier's inactive styling. */
131
+ .mj-tabs__tab--active {
132
+ /* Base surface + a top accent line, as an INSET shadow so it adds no height and shifts nothing.
133
+ Border and line both draw from --mj-tab-accent, so the ACTIVE tab's chrome states its status
134
+ in one voice: brand primary for an ordinary tab, warning when rejected, success when complete.
135
+ The status icon and accessible name carry the same information for anyone who can't rely on
136
+ the color alone. */
137
+ background: var(--mj-bg-surface);
138
+ border-color: var(--mj-tab-accent);
139
+ box-shadow: inset 0 2px 0 0 var(--mj-tab-accent);
140
+ color: var(--mj-text-primary);
141
+ font-weight: 600;
142
+ }
143
+
144
+ .mj-tabs__label {
145
+ flex: 0 1 auto;
146
+ min-width: 0;
147
+ overflow: hidden;
148
+ white-space: nowrap;
149
+ text-overflow: ellipsis;
150
+ }
151
+
152
+ .mj-tabs__icon {
153
+ flex: none;
154
+ font-size: 12px;
155
+ }
156
+
157
+ .mj-tabs__status-icon {
158
+ flex: none;
159
+ font-size: 12px;
160
+ color: var(--mj-status-warning);
161
+ }
162
+
163
+ /* Inline dot, present only when dirty — it PUSHES the label (so the ellipsis shifts left on a
164
+ capped tab) rather than occupying a permanently reserved slot. */
165
+ .mj-tabs__dirty {
166
+ flex: none;
167
+ padding-left: 3px;
168
+ color: var(--mj-brand-primary);
169
+ font-size: 18px;
170
+ line-height: 0;
171
+ pointer-events: none;
172
+ }
173
+
174
+ /* Not in the tab order: the tab itself is the focus stop, and Delete closes it (ARIA APG's pattern
175
+ for closeable tabs). Kept as a real button so it retains an accessible name and a pointer target. */
176
+ .mj-tabs__close {
177
+ display: inline-flex;
178
+ align-items: center;
179
+ justify-content: center;
180
+ width: 22px;
181
+ height: 22px;
182
+ margin-left: auto;
183
+ margin-right: 6px;
184
+ border: none;
185
+ border-radius: 5px;
186
+ background: transparent;
187
+ color: var(--mj-text-muted);
188
+ cursor: pointer;
189
+ font-size: 11px;
190
+ flex: none;
191
+ }
192
+ .mj-tabs__close:hover {
193
+ background: var(--mj-bg-surface-hover);
194
+ color: var(--mj-text-primary);
195
+ }
196
+
197
+ /* Pinned trailing control (new-tab / overflow scroll buttons): flex:none keeps it at the edge of
198
+ the bar while the list scrolls behind it. */
199
+ .mj-tabs__new {
200
+ display: inline-flex;
201
+ align-items: center;
202
+ gap: 6px;
203
+ padding: 8px 12px;
204
+ flex: none;
205
+ border: none;
206
+ border-left: 1px solid var(--mj-border-default);
207
+ background: var(--mj-bg-surface);
208
+ color: var(--mj-text-link);
209
+ font-size: 13px;
210
+ font-family: inherit;
211
+ cursor: pointer;
212
+ white-space: nowrap;
213
+ /* Fill to the top of the surface — cancel the bar's top padding for this control only, so it
214
+ doesn't cut off early above the tab row. */
215
+ align-self: stretch;
216
+ margin-top: -8px;
217
+ /* Round only the OUTER corner; the tab-side edge stays square and flush against the divider that
218
+ separates it from the tabs, matching how browsers draw their +. */
219
+ border-radius: 0 8px 0 0;
220
+ }
221
+ .mj-tabs__new:hover {
222
+ background: var(--mj-bg-surface-hover);
223
+ }
224
+
225
+ /* Overflow scroll affordances — same chrome as the pinned control, no bottom rounding. */
226
+ .mj-tabs__scroll {
227
+ display: inline-flex;
228
+ align-items: center;
229
+ justify-content: center;
230
+ padding: 8px 10px;
231
+ flex: none;
232
+ border: none;
233
+ background: var(--mj-bg-surface);
234
+ color: var(--mj-text-secondary);
235
+ cursor: pointer;
236
+ align-self: stretch;
237
+ margin-top: -8px;
238
+ }
239
+ .mj-tabs__scroll:hover {
240
+ background: var(--mj-bg-surface-hover);
241
+ color: var(--mj-text-primary);
242
+ }
@@ -0,0 +1,63 @@
1
+ import { EventEmitter } from '@angular/core';
2
+ import { MJWorkspaceTab } from './workspace-tabs.types';
3
+ import { MJTabReorder } from './workspace-tab-strip.component';
4
+ import * as i0 from "@angular/core";
5
+ /**
6
+ * `<mj-workspace-card>` — the reusable frame every "workspace" screen shares (a screen where the
7
+ * user builds several drafts in parallel, each in a tab). A THIN, slotted wrapper: it owns ONLY the
8
+ * invariant chrome —
9
+ * • the card surface (border + rounded corners + contained scroll),
10
+ * • the tab-strip row (delegated to `<mj-workspace-tab-strip>`, which carries all tab behavior),
11
+ * • an identity band beside the tabs, and
12
+ * • a single scrollable body —
13
+ * and PROJECTS everything that varies:
14
+ * • `[workspaceHeader]` — the per-workshop identity band content (e.g. number/status/currency badges);
15
+ * • the default slot — the workshop's form/body;
16
+ * • `[workspaceFooterNote]` — an optional caption beside the standardized footer verbs.
17
+ *
18
+ * The card deliberately bakes in NO workshop-specific header; those differ per workshop and are
19
+ * projected. Height chain: `:host` fills its parent, the card fills the host, the body is the
20
+ * flex:1 scroller — so the BODY scrolls in place while the strip + identity band stay pinned.
21
+ *
22
+ * SIZING RULE for content inside the card: the card is a query container (`container-type: size`),
23
+ * so content sizes with CONTAINER units (`cqh`/`cqw`/`cqi`) — relative to the card as a pane —
24
+ * never viewport units (`vh`/`vw`), which break the moment the card sits in a split or a smaller
25
+ * window.
26
+ */
27
+ export declare class MJWorkspaceCardComponent {
28
+ Tabs: MJWorkspaceTab[];
29
+ ActiveId: string | null;
30
+ NewTabLabel: string;
31
+ AriaLabel: string;
32
+ /** Passed through to the strip — see `MJWorkspaceTabStripComponent.AllowReorder`. */
33
+ AllowReorder: boolean;
34
+ /** Render the standardized action bar (confirm / save-as-draft / discard) below the body. */
35
+ ShowFooter: boolean;
36
+ /** The primary verb's label — the ONE thing that changes per workshop ("Create entry" / "Build batch"). */
37
+ ConfirmLabel: string;
38
+ /** Optional Font Awesome class for the confirm button (e.g. 'fa-solid fa-check'). */
39
+ ConfirmIcon: string | null;
40
+ ConfirmDisabled: boolean;
41
+ /** Tooltip / blocked-reason on the confirm button; falls back to ConfirmLabel. */
42
+ ConfirmTitle: string | null;
43
+ /** Show the spinner + busy label on the confirm button while an async confirm runs. */
44
+ ConfirmBusy: boolean;
45
+ ConfirmBusyLabel: string;
46
+ /** Whether the save-as-draft button appears (some workshops have no draft concept). */
47
+ ShowDraft: boolean;
48
+ DraftLabel: string;
49
+ DraftIcon: string | null;
50
+ DraftDisabled: boolean;
51
+ DiscardLabel: string;
52
+ TabSelected: EventEmitter<string>;
53
+ TabClosed: EventEmitter<string>;
54
+ NewTabRequested: EventEmitter<void>;
55
+ TabReordered: EventEmitter<MJTabReorder>;
56
+ /** The primary verb (create / build / …). */
57
+ Confirm: EventEmitter<void>;
58
+ SaveDraft: EventEmitter<void>;
59
+ Discard: EventEmitter<void>;
60
+ static ɵfac: i0.ɵɵFactoryDeclaration<MJWorkspaceCardComponent, never>;
61
+ static ɵcmp: i0.ɵɵComponentDeclaration<MJWorkspaceCardComponent, "mj-workspace-card", never, { "Tabs": { "alias": "Tabs"; "required": false; }; "ActiveId": { "alias": "ActiveId"; "required": false; }; "NewTabLabel": { "alias": "NewTabLabel"; "required": false; }; "AriaLabel": { "alias": "AriaLabel"; "required": false; }; "AllowReorder": { "alias": "AllowReorder"; "required": false; }; "ShowFooter": { "alias": "ShowFooter"; "required": false; }; "ConfirmLabel": { "alias": "ConfirmLabel"; "required": false; }; "ConfirmIcon": { "alias": "ConfirmIcon"; "required": false; }; "ConfirmDisabled": { "alias": "ConfirmDisabled"; "required": false; }; "ConfirmTitle": { "alias": "ConfirmTitle"; "required": false; }; "ConfirmBusy": { "alias": "ConfirmBusy"; "required": false; }; "ConfirmBusyLabel": { "alias": "ConfirmBusyLabel"; "required": false; }; "ShowDraft": { "alias": "ShowDraft"; "required": false; }; "DraftLabel": { "alias": "DraftLabel"; "required": false; }; "DraftIcon": { "alias": "DraftIcon"; "required": false; }; "DraftDisabled": { "alias": "DraftDisabled"; "required": false; }; "DiscardLabel": { "alias": "DiscardLabel"; "required": false; }; }, { "TabSelected": "TabSelected"; "TabClosed": "TabClosed"; "NewTabRequested": "NewTabRequested"; "TabReordered": "TabReordered"; "Confirm": "Confirm"; "SaveDraft": "SaveDraft"; "Discard": "Discard"; }, never, ["[workspaceHeader]", "*", "[workspaceFooterNote]"], true, never>;
62
+ }
63
+ //# sourceMappingURL=workspace-card.component.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace-card.component.d.ts","sourceRoot":"","sources":["../../../src/lib/workspace-tabs/workspace-card.component.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqD,YAAY,EAAE,MAAM,eAAe,CAAC;AAEhG,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,EAAgC,YAAY,EAAE,MAAM,iCAAiC,CAAC;;AAG7F;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,qBAwDa,wBAAwB;IAC1B,IAAI,EAAE,cAAc,EAAE,CAAM;IAC5B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAQ;IAC/B,WAAW,SAAS;IACpB,SAAS,SAAe;IACjC,qFAAqF;IAC5E,YAAY,UAAQ;IAG7B,6FAA6F;IACpF,UAAU,UAAS;IAC5B,2GAA2G;IAClG,YAAY,SAAa;IAClC,qFAAqF;IAC5E,WAAW,EAAE,MAAM,GAAG,IAAI,CAAQ;IAClC,eAAe,UAAS;IACjC,kFAAkF;IACzE,YAAY,EAAE,MAAM,GAAG,IAAI,CAAQ;IAC5C,uFAAuF;IAC9E,WAAW,UAAS;IACpB,gBAAgB,SAAc;IACvC,uFAAuF;IAC9E,SAAS,UAAQ;IACjB,UAAU,SAAmB;IAC7B,SAAS,EAAE,MAAM,GAAG,IAAI,CAA+B;IACvD,aAAa,UAAS;IACtB,YAAY,SAAa;IAExB,WAAW,uBAA8B;IACzC,SAAS,uBAA8B;IACvC,eAAe,qBAA4B;IAC3C,YAAY,6BAAoC;IAC1D,6CAA6C;IACnC,OAAO,qBAA4B;IACnC,SAAS,qBAA4B;IACrC,OAAO,qBAA4B;yCAnClC,wBAAwB;2CAAxB,wBAAwB;CAoCpC"}
@@ -0,0 +1,256 @@
1
+ import { Component, ChangeDetectionStrategy, Input, Output, EventEmitter } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+ import { MJWorkspaceTabStripComponent } from './workspace-tab-strip.component';
4
+ import { MJButtonDirective } from '../button/button.directive';
5
+ import * as i0 from "@angular/core";
6
+ const _c0 = [[["", "workspaceHeader", ""]], "*", [["", "workspaceFooterNote", ""]]];
7
+ const _c1 = ["[workspaceHeader]", "*", "[workspaceFooterNote]"];
8
+ function MJWorkspaceCardComponent_Conditional_7_Conditional_2_Template(rf, ctx) { if (rf & 1) {
9
+ i0.ɵɵelement(0, "i", 10);
10
+ i0.ɵɵtext(1);
11
+ } if (rf & 2) {
12
+ const ctx_r1 = i0.ɵɵnextContext(2);
13
+ i0.ɵɵadvance();
14
+ i0.ɵɵtextInterpolate1(" ", ctx_r1.ConfirmBusyLabel, " ");
15
+ } }
16
+ function MJWorkspaceCardComponent_Conditional_7_Conditional_3_Conditional_0_Template(rf, ctx) { if (rf & 1) {
17
+ i0.ɵɵelement(0, "i", 12);
18
+ } if (rf & 2) {
19
+ const ctx_r1 = i0.ɵɵnextContext(3);
20
+ i0.ɵɵclassMap(ctx_r1.ConfirmIcon);
21
+ } }
22
+ function MJWorkspaceCardComponent_Conditional_7_Conditional_3_Template(rf, ctx) { if (rf & 1) {
23
+ i0.ɵɵconditionalCreate(0, MJWorkspaceCardComponent_Conditional_7_Conditional_3_Conditional_0_Template, 1, 2, "i", 11);
24
+ i0.ɵɵtext(1);
25
+ } if (rf & 2) {
26
+ const ctx_r1 = i0.ɵɵnextContext(2);
27
+ i0.ɵɵconditional(ctx_r1.ConfirmIcon ? 0 : -1);
28
+ i0.ɵɵadvance();
29
+ i0.ɵɵtextInterpolate1(" ", ctx_r1.ConfirmLabel, " ");
30
+ } }
31
+ function MJWorkspaceCardComponent_Conditional_7_Conditional_4_Conditional_1_Template(rf, ctx) { if (rf & 1) {
32
+ i0.ɵɵelement(0, "i", 12);
33
+ } if (rf & 2) {
34
+ const ctx_r1 = i0.ɵɵnextContext(3);
35
+ i0.ɵɵclassMap(ctx_r1.DraftIcon);
36
+ } }
37
+ function MJWorkspaceCardComponent_Conditional_7_Conditional_4_Template(rf, ctx) { if (rf & 1) {
38
+ const _r3 = i0.ɵɵgetCurrentView();
39
+ i0.ɵɵelementStart(0, "button", 13);
40
+ i0.ɵɵlistener("click", function MJWorkspaceCardComponent_Conditional_7_Conditional_4_Template_button_click_0_listener() { i0.ɵɵrestoreView(_r3); const ctx_r1 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r1.SaveDraft.emit()); });
41
+ i0.ɵɵconditionalCreate(1, MJWorkspaceCardComponent_Conditional_7_Conditional_4_Conditional_1_Template, 1, 2, "i", 11);
42
+ i0.ɵɵtext(2);
43
+ i0.ɵɵelementEnd();
44
+ } if (rf & 2) {
45
+ const ctx_r1 = i0.ɵɵnextContext(2);
46
+ i0.ɵɵproperty("disabled", ctx_r1.DraftDisabled);
47
+ i0.ɵɵadvance();
48
+ i0.ɵɵconditional(ctx_r1.DraftIcon ? 1 : -1);
49
+ i0.ɵɵadvance();
50
+ i0.ɵɵtextInterpolate1(" ", ctx_r1.DraftLabel, " ");
51
+ } }
52
+ function MJWorkspaceCardComponent_Conditional_7_Template(rf, ctx) { if (rf & 1) {
53
+ const _r1 = i0.ɵɵgetCurrentView();
54
+ i0.ɵɵelementStart(0, "footer", 5)(1, "button", 6);
55
+ i0.ɵɵlistener("click", function MJWorkspaceCardComponent_Conditional_7_Template_button_click_1_listener() { i0.ɵɵrestoreView(_r1); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.Confirm.emit()); });
56
+ i0.ɵɵconditionalCreate(2, MJWorkspaceCardComponent_Conditional_7_Conditional_2_Template, 2, 1)(3, MJWorkspaceCardComponent_Conditional_7_Conditional_3_Template, 2, 2);
57
+ i0.ɵɵelementEnd();
58
+ i0.ɵɵconditionalCreate(4, MJWorkspaceCardComponent_Conditional_7_Conditional_4_Template, 3, 3, "button", 7);
59
+ i0.ɵɵelementStart(5, "button", 8);
60
+ i0.ɵɵlistener("click", function MJWorkspaceCardComponent_Conditional_7_Template_button_click_5_listener() { i0.ɵɵrestoreView(_r1); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.Discard.emit()); });
61
+ i0.ɵɵtext(6);
62
+ i0.ɵɵelementEnd();
63
+ i0.ɵɵelementStart(7, "span", 9);
64
+ i0.ɵɵprojection(8, 2);
65
+ i0.ɵɵelementEnd()();
66
+ } if (rf & 2) {
67
+ const ctx_r1 = i0.ɵɵnextContext();
68
+ i0.ɵɵadvance();
69
+ i0.ɵɵproperty("disabled", ctx_r1.ConfirmDisabled || ctx_r1.ConfirmBusy)("title", ctx_r1.ConfirmTitle || ctx_r1.ConfirmLabel);
70
+ i0.ɵɵadvance();
71
+ i0.ɵɵconditional(ctx_r1.ConfirmBusy ? 2 : 3);
72
+ i0.ɵɵadvance(2);
73
+ i0.ɵɵconditional(ctx_r1.ShowDraft ? 4 : -1);
74
+ i0.ɵɵadvance(2);
75
+ i0.ɵɵtextInterpolate(ctx_r1.DiscardLabel);
76
+ } }
77
+ /**
78
+ * `<mj-workspace-card>` — the reusable frame every "workspace" screen shares (a screen where the
79
+ * user builds several drafts in parallel, each in a tab). A THIN, slotted wrapper: it owns ONLY the
80
+ * invariant chrome —
81
+ * • the card surface (border + rounded corners + contained scroll),
82
+ * • the tab-strip row (delegated to `<mj-workspace-tab-strip>`, which carries all tab behavior),
83
+ * • an identity band beside the tabs, and
84
+ * • a single scrollable body —
85
+ * and PROJECTS everything that varies:
86
+ * • `[workspaceHeader]` — the per-workshop identity band content (e.g. number/status/currency badges);
87
+ * • the default slot — the workshop's form/body;
88
+ * • `[workspaceFooterNote]` — an optional caption beside the standardized footer verbs.
89
+ *
90
+ * The card deliberately bakes in NO workshop-specific header; those differ per workshop and are
91
+ * projected. Height chain: `:host` fills its parent, the card fills the host, the body is the
92
+ * flex:1 scroller — so the BODY scrolls in place while the strip + identity band stay pinned.
93
+ *
94
+ * SIZING RULE for content inside the card: the card is a query container (`container-type: size`),
95
+ * so content sizes with CONTAINER units (`cqh`/`cqw`/`cqi`) — relative to the card as a pane —
96
+ * never viewport units (`vh`/`vw`), which break the moment the card sits in a split or a smaller
97
+ * window.
98
+ */
99
+ export class MJWorkspaceCardComponent {
100
+ Tabs = [];
101
+ ActiveId = null;
102
+ NewTabLabel = 'New';
103
+ AriaLabel = 'Workspace';
104
+ /** Passed through to the strip — see `MJWorkspaceTabStripComponent.AllowReorder`. */
105
+ AllowReorder = true;
106
+ // ── Standardized footer (opt-in) ────────────────────────────────────────────
107
+ /** Render the standardized action bar (confirm / save-as-draft / discard) below the body. */
108
+ ShowFooter = false;
109
+ /** The primary verb's label — the ONE thing that changes per workshop ("Create entry" / "Build batch"). */
110
+ ConfirmLabel = 'Confirm';
111
+ /** Optional Font Awesome class for the confirm button (e.g. 'fa-solid fa-check'). */
112
+ ConfirmIcon = null;
113
+ ConfirmDisabled = false;
114
+ /** Tooltip / blocked-reason on the confirm button; falls back to ConfirmLabel. */
115
+ ConfirmTitle = null;
116
+ /** Show the spinner + busy label on the confirm button while an async confirm runs. */
117
+ ConfirmBusy = false;
118
+ ConfirmBusyLabel = 'Working…';
119
+ /** Whether the save-as-draft button appears (some workshops have no draft concept). */
120
+ ShowDraft = true;
121
+ DraftLabel = 'Keep as draft';
122
+ DraftIcon = 'fa-regular fa-floppy-disk';
123
+ DraftDisabled = false;
124
+ DiscardLabel = 'Discard';
125
+ TabSelected = new EventEmitter();
126
+ TabClosed = new EventEmitter();
127
+ NewTabRequested = new EventEmitter();
128
+ TabReordered = new EventEmitter();
129
+ /** The primary verb (create / build / …). */
130
+ Confirm = new EventEmitter();
131
+ SaveDraft = new EventEmitter();
132
+ Discard = new EventEmitter();
133
+ static ɵfac = function MJWorkspaceCardComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || MJWorkspaceCardComponent)(); };
134
+ static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: MJWorkspaceCardComponent, selectors: [["mj-workspace-card"]], inputs: { Tabs: "Tabs", ActiveId: "ActiveId", NewTabLabel: "NewTabLabel", AriaLabel: "AriaLabel", AllowReorder: "AllowReorder", ShowFooter: "ShowFooter", ConfirmLabel: "ConfirmLabel", ConfirmIcon: "ConfirmIcon", ConfirmDisabled: "ConfirmDisabled", ConfirmTitle: "ConfirmTitle", ConfirmBusy: "ConfirmBusy", ConfirmBusyLabel: "ConfirmBusyLabel", ShowDraft: "ShowDraft", DraftLabel: "DraftLabel", DraftIcon: "DraftIcon", DraftDisabled: "DraftDisabled", DiscardLabel: "DiscardLabel" }, outputs: { TabSelected: "TabSelected", TabClosed: "TabClosed", NewTabRequested: "NewTabRequested", TabReordered: "TabReordered", Confirm: "Confirm", SaveDraft: "SaveDraft", Discard: "Discard" }, ngContentSelectors: _c1, decls: 8, vars: 6, consts: [[1, "ws-card"], [1, "ws-card__head"], [3, "TabSelected", "TabClosed", "NewTabRequested", "TabReordered", "Tabs", "ActiveId", "NewTabLabel", "AllowReorder"], [1, "ws-card__headmeta"], [1, "ws-card__body"], [1, "ws-card__foot"], ["type", "button", "mjButton", "", "variant", "primary", 3, "click", "disabled", "title"], ["type", "button", "mjButton", "", "variant", "outline", 3, "disabled"], ["type", "button", "mjButton", "", "variant", "flat", 3, "click"], [1, "ws-card__footnote"], ["aria-hidden", "true", 1, "fa-solid", "fa-spinner", "fa-spin"], ["aria-hidden", "true", 3, "class"], ["aria-hidden", "true"], ["type", "button", "mjButton", "", "variant", "outline", 3, "click", "disabled"]], template: function MJWorkspaceCardComponent_Template(rf, ctx) { if (rf & 1) {
135
+ i0.ɵɵprojectionDef(_c0);
136
+ i0.ɵɵelementStart(0, "section", 0)(1, "header", 1)(2, "mj-workspace-tab-strip", 2);
137
+ i0.ɵɵlistener("TabSelected", function MJWorkspaceCardComponent_Template_mj_workspace_tab_strip_TabSelected_2_listener($event) { return ctx.TabSelected.emit($event); })("TabClosed", function MJWorkspaceCardComponent_Template_mj_workspace_tab_strip_TabClosed_2_listener($event) { return ctx.TabClosed.emit($event); })("NewTabRequested", function MJWorkspaceCardComponent_Template_mj_workspace_tab_strip_NewTabRequested_2_listener() { return ctx.NewTabRequested.emit(); })("TabReordered", function MJWorkspaceCardComponent_Template_mj_workspace_tab_strip_TabReordered_2_listener($event) { return ctx.TabReordered.emit($event); });
138
+ i0.ɵɵelementEnd();
139
+ i0.ɵɵelementStart(3, "div", 3);
140
+ i0.ɵɵprojection(4);
141
+ i0.ɵɵelementEnd()();
142
+ i0.ɵɵelementStart(5, "div", 4);
143
+ i0.ɵɵprojection(6, 1);
144
+ i0.ɵɵelementEnd();
145
+ i0.ɵɵconditionalCreate(7, MJWorkspaceCardComponent_Conditional_7_Template, 9, 5, "footer", 5);
146
+ i0.ɵɵelementEnd();
147
+ } if (rf & 2) {
148
+ i0.ɵɵattribute("aria-label", ctx.AriaLabel);
149
+ i0.ɵɵadvance(2);
150
+ i0.ɵɵproperty("Tabs", ctx.Tabs)("ActiveId", ctx.ActiveId)("NewTabLabel", ctx.NewTabLabel)("AllowReorder", ctx.AllowReorder);
151
+ i0.ɵɵadvance(5);
152
+ i0.ɵɵconditional(ctx.ShowFooter ? 7 : -1);
153
+ } }, dependencies: [CommonModule, MJWorkspaceTabStripComponent, MJButtonDirective], styles: ["\n\n\n\n\n[_nghost-%COMP%] {\n display: flex;\n flex-direction: column;\n height: 100%;\n min-height: 0;\n}\n\n\n\n.ws-card[_ngcontent-%COMP%] {\n flex: 1;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\n border-radius: 12px;\n overflow: hidden;\n \n\n\n\n\n container-type: size;\n container-name: workspace;\n}\n\n.ws-card__head[_ngcontent-%COMP%] {\n flex: none;\n}\n\n\n\n.ws-card__headmeta[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 10px;\n flex-wrap: wrap;\n padding: 10px 14px;\n}\n\n\n.ws-card__headmeta[_ngcontent-%COMP%]:empty {\n display: none;\n}\n\n\n\n\n.ws-card__body[_ngcontent-%COMP%] {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n padding: 0 14px 14px;\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n\n\n\n\n.ws-card__foot[_ngcontent-%COMP%] {\n flex: none;\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n padding: 4px 14px 12px;\n}\n\n\n.ws-card__footnote[_ngcontent-%COMP%] {\n margin-left: auto;\n max-width: 46ch;\n font-size: 12px;\n color: var(--mj-text-muted);\n}\n.ws-card__footnote[_ngcontent-%COMP%]:empty {\n display: none;\n}"], changeDetection: 0 });
154
+ }
155
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(MJWorkspaceCardComponent, [{
156
+ type: Component,
157
+ args: [{ standalone: true, selector: 'mj-workspace-card', imports: [CommonModule, MJWorkspaceTabStripComponent, MJButtonDirective], changeDetection: ChangeDetectionStrategy.OnPush, template: `
158
+ <section class="ws-card" [attr.aria-label]="AriaLabel">
159
+ <header class="ws-card__head">
160
+ <mj-workspace-tab-strip
161
+ [Tabs]="Tabs"
162
+ [ActiveId]="ActiveId"
163
+ [NewTabLabel]="NewTabLabel"
164
+ [AllowReorder]="AllowReorder"
165
+ (TabSelected)="TabSelected.emit($event)"
166
+ (TabClosed)="TabClosed.emit($event)"
167
+ (NewTabRequested)="NewTabRequested.emit()"
168
+ (TabReordered)="TabReordered.emit($event)">
169
+ </mj-workspace-tab-strip>
170
+ <div class="ws-card__headmeta">
171
+ <ng-content select="[workspaceHeader]"></ng-content>
172
+ </div>
173
+ </header>
174
+ <div class="ws-card__body">
175
+ <ng-content></ng-content>
176
+ </div>
177
+ <!-- Standardized action bar: the SAME three verbs on every workspace — a primary
178
+ confirm/submit whose LABEL is the only thing that varies per use case, a neutral
179
+ save-as-draft, and a neutral discard — so no workshop hand-rolls its own footer. Pinned
180
+ below the scrolling body. Opt-in via [ShowFooter]; the [workspaceFooterNote] slot carries
181
+ any per-workshop caption beside the buttons. Confirm sits LEFT of the neutral verbs per
182
+ the MJ dialog-button convention. -->
183
+ @if (ShowFooter) {
184
+ <footer class="ws-card__foot">
185
+ <button type="button" mjButton variant="primary"
186
+ [disabled]="ConfirmDisabled || ConfirmBusy"
187
+ [title]="ConfirmTitle || ConfirmLabel"
188
+ (click)="Confirm.emit()">
189
+ @if (ConfirmBusy) {
190
+ <i class="fa-solid fa-spinner fa-spin" aria-hidden="true"></i> {{ ConfirmBusyLabel }}
191
+ } @else {
192
+ @if (ConfirmIcon) { <i [class]="ConfirmIcon" aria-hidden="true"></i> } {{ ConfirmLabel }}
193
+ }
194
+ </button>
195
+ @if (ShowDraft) {
196
+ <button type="button" mjButton variant="outline" [disabled]="DraftDisabled" (click)="SaveDraft.emit()">
197
+ @if (DraftIcon) { <i [class]="DraftIcon" aria-hidden="true"></i> } {{ DraftLabel }}
198
+ </button>
199
+ }
200
+ <button type="button" mjButton variant="flat" (click)="Discard.emit()">{{ DiscardLabel }}</button>
201
+ <span class="ws-card__footnote"><ng-content select="[workspaceFooterNote]"></ng-content></span>
202
+ </footer>
203
+ }
204
+ </section>
205
+ `, styles: ["/* The reusable workspace card frame \u2014 design tokens only. The height chain (host fills parent \u2192 card\n fills host \u2192 body is the flex:1 scroller) is load-bearing: it's what lets the BODY scroll in place\n while the tab strip + identity band stay pinned. */\n\n:host {\n display: flex;\n flex-direction: column;\n height: 100%;\n min-height: 0;\n}\n\n/* One object against the page: --mj-bg-surface, rounded, with the body's scroll contained inside. */\n.ws-card {\n flex: 1;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\n border-radius: 12px;\n overflow: hidden;\n /* SIZING CONTAINER (design rule, Marcelo 2026-07-21): the card is a query container, so content\n inside sizes with CONTAINER units (cqh/cqw/cqi) \u2014 relative to THIS card (a window/pane), never the\n viewport (vh/vw). vh would size to the whole screen regardless of the pane the card sits in. The\n card's own size comes from the flex chain (parent-driven), so size containment is safe here. */\n container-type: size;\n container-name: workspace;\n}\n\n.ws-card__head {\n flex: none;\n}\n\n/* Identity band below the tab row (never in line with the tabs) \u2014 the slot the workshop projects into. */\n.ws-card__headmeta {\n display: flex;\n align-items: center;\n gap: 10px;\n flex-wrap: wrap;\n padding: 10px 14px;\n}\n/* Collapse the band entirely when the workshop projects nothing into it (no empty padded strip). */\n.ws-card__headmeta:empty {\n display: none;\n}\n\n/* The single scrollable body. Standard gutters + a stacked-column default (the compact-workspace\n layout); a workshop's projected content flows here. */\n.ws-card__body {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n padding: 0 14px 14px;\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n/* The standardized action bar \u2014 pinned below the scrolling body, so the verbs stay reachable without\n scrolling to the end of a long form. NO top border (Marcelo 2026-07-21): the buttons should read as\n part of the sheet, not a boxed-off strip \u2014 the card's own frame is the only border we want. */\n.ws-card__foot {\n flex: none;\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n padding: 4px 14px 12px;\n}\n/* Per-workshop caption beside the verbs (projected). Collapses when nothing is slotted. */\n.ws-card__footnote {\n margin-left: auto;\n max-width: 46ch;\n font-size: 12px;\n color: var(--mj-text-muted);\n}\n.ws-card__footnote:empty {\n display: none;\n}\n"] }]
206
+ }], null, { Tabs: [{
207
+ type: Input
208
+ }], ActiveId: [{
209
+ type: Input
210
+ }], NewTabLabel: [{
211
+ type: Input
212
+ }], AriaLabel: [{
213
+ type: Input
214
+ }], AllowReorder: [{
215
+ type: Input
216
+ }], ShowFooter: [{
217
+ type: Input
218
+ }], ConfirmLabel: [{
219
+ type: Input
220
+ }], ConfirmIcon: [{
221
+ type: Input
222
+ }], ConfirmDisabled: [{
223
+ type: Input
224
+ }], ConfirmTitle: [{
225
+ type: Input
226
+ }], ConfirmBusy: [{
227
+ type: Input
228
+ }], ConfirmBusyLabel: [{
229
+ type: Input
230
+ }], ShowDraft: [{
231
+ type: Input
232
+ }], DraftLabel: [{
233
+ type: Input
234
+ }], DraftIcon: [{
235
+ type: Input
236
+ }], DraftDisabled: [{
237
+ type: Input
238
+ }], DiscardLabel: [{
239
+ type: Input
240
+ }], TabSelected: [{
241
+ type: Output
242
+ }], TabClosed: [{
243
+ type: Output
244
+ }], NewTabRequested: [{
245
+ type: Output
246
+ }], TabReordered: [{
247
+ type: Output
248
+ }], Confirm: [{
249
+ type: Output
250
+ }], SaveDraft: [{
251
+ type: Output
252
+ }], Discard: [{
253
+ type: Output
254
+ }] }); })();
255
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(MJWorkspaceCardComponent, { className: "MJWorkspaceCardComponent", filePath: "lib/workspace-tabs/workspace-card.component.ts", lineNumber: 85 }); })();
256
+ //# sourceMappingURL=workspace-card.component.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace-card.component.js","sourceRoot":"","sources":["../../../src/lib/workspace-tabs/workspace-card.component.ts"],"names":[],"mappings":"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","sourcesContent":["import { Component, ChangeDetectionStrategy, Input, Output, EventEmitter } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { MJWorkspaceTab } from './workspace-tabs.types';\nimport { MJWorkspaceTabStripComponent, MJTabReorder } from './workspace-tab-strip.component';\nimport { MJButtonDirective } from '../button/button.directive';\n\n/**\n * `<mj-workspace-card>` — the reusable frame every \"workspace\" screen shares (a screen where the\n * user builds several drafts in parallel, each in a tab). A THIN, slotted wrapper: it owns ONLY the\n * invariant chrome —\n * • the card surface (border + rounded corners + contained scroll),\n * • the tab-strip row (delegated to `<mj-workspace-tab-strip>`, which carries all tab behavior),\n * • an identity band beside the tabs, and\n * • a single scrollable body —\n * and PROJECTS everything that varies:\n * • `[workspaceHeader]` — the per-workshop identity band content (e.g. number/status/currency badges);\n * • the default slot — the workshop's form/body;\n * • `[workspaceFooterNote]` — an optional caption beside the standardized footer verbs.\n *\n * The card deliberately bakes in NO workshop-specific header; those differ per workshop and are\n * projected. Height chain: `:host` fills its parent, the card fills the host, the body is the\n * flex:1 scroller — so the BODY scrolls in place while the strip + identity band stay pinned.\n *\n * SIZING RULE for content inside the card: the card is a query container (`container-type: size`),\n * so content sizes with CONTAINER units (`cqh`/`cqw`/`cqi`) — relative to the card as a pane —\n * never viewport units (`vh`/`vw`), which break the moment the card sits in a split or a smaller\n * window.\n */\n@Component({\n standalone: true,\n selector: 'mj-workspace-card',\n imports: [CommonModule, MJWorkspaceTabStripComponent, MJButtonDirective],\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `\n <section class=\"ws-card\" [attr.aria-label]=\"AriaLabel\">\n <header class=\"ws-card__head\">\n <mj-workspace-tab-strip\n [Tabs]=\"Tabs\"\n [ActiveId]=\"ActiveId\"\n [NewTabLabel]=\"NewTabLabel\"\n [AllowReorder]=\"AllowReorder\"\n (TabSelected)=\"TabSelected.emit($event)\"\n (TabClosed)=\"TabClosed.emit($event)\"\n (NewTabRequested)=\"NewTabRequested.emit()\"\n (TabReordered)=\"TabReordered.emit($event)\">\n </mj-workspace-tab-strip>\n <div class=\"ws-card__headmeta\">\n <ng-content select=\"[workspaceHeader]\"></ng-content>\n </div>\n </header>\n <div class=\"ws-card__body\">\n <ng-content></ng-content>\n </div>\n <!-- Standardized action bar: the SAME three verbs on every workspace — a primary\n confirm/submit whose LABEL is the only thing that varies per use case, a neutral\n save-as-draft, and a neutral discard — so no workshop hand-rolls its own footer. Pinned\n below the scrolling body. Opt-in via [ShowFooter]; the [workspaceFooterNote] slot carries\n any per-workshop caption beside the buttons. Confirm sits LEFT of the neutral verbs per\n the MJ dialog-button convention. -->\n @if (ShowFooter) {\n <footer class=\"ws-card__foot\">\n <button type=\"button\" mjButton variant=\"primary\"\n [disabled]=\"ConfirmDisabled || ConfirmBusy\"\n [title]=\"ConfirmTitle || ConfirmLabel\"\n (click)=\"Confirm.emit()\">\n @if (ConfirmBusy) {\n <i class=\"fa-solid fa-spinner fa-spin\" aria-hidden=\"true\"></i> {{ ConfirmBusyLabel }}\n } @else {\n @if (ConfirmIcon) { <i [class]=\"ConfirmIcon\" aria-hidden=\"true\"></i> } {{ ConfirmLabel }}\n }\n </button>\n @if (ShowDraft) {\n <button type=\"button\" mjButton variant=\"outline\" [disabled]=\"DraftDisabled\" (click)=\"SaveDraft.emit()\">\n @if (DraftIcon) { <i [class]=\"DraftIcon\" aria-hidden=\"true\"></i> } {{ DraftLabel }}\n </button>\n }\n <button type=\"button\" mjButton variant=\"flat\" (click)=\"Discard.emit()\">{{ DiscardLabel }}</button>\n <span class=\"ws-card__footnote\"><ng-content select=\"[workspaceFooterNote]\"></ng-content></span>\n </footer>\n }\n </section>\n `,\n styleUrls: ['./workspace-card.component.css'],\n})\nexport class MJWorkspaceCardComponent {\n @Input() Tabs: MJWorkspaceTab[] = [];\n @Input() ActiveId: string | null = null;\n @Input() NewTabLabel = 'New';\n @Input() AriaLabel = 'Workspace';\n /** Passed through to the strip — see `MJWorkspaceTabStripComponent.AllowReorder`. */\n @Input() AllowReorder = true;\n\n // ── Standardized footer (opt-in) ────────────────────────────────────────────\n /** Render the standardized action bar (confirm / save-as-draft / discard) below the body. */\n @Input() ShowFooter = false;\n /** The primary verb's label — the ONE thing that changes per workshop (\"Create entry\" / \"Build batch\"). */\n @Input() ConfirmLabel = 'Confirm';\n /** Optional Font Awesome class for the confirm button (e.g. 'fa-solid fa-check'). */\n @Input() ConfirmIcon: string | null = null;\n @Input() ConfirmDisabled = false;\n /** Tooltip / blocked-reason on the confirm button; falls back to ConfirmLabel. */\n @Input() ConfirmTitle: string | null = null;\n /** Show the spinner + busy label on the confirm button while an async confirm runs. */\n @Input() ConfirmBusy = false;\n @Input() ConfirmBusyLabel = 'Working…';\n /** Whether the save-as-draft button appears (some workshops have no draft concept). */\n @Input() ShowDraft = true;\n @Input() DraftLabel = 'Keep as draft';\n @Input() DraftIcon: string | null = 'fa-regular fa-floppy-disk';\n @Input() DraftDisabled = false;\n @Input() DiscardLabel = 'Discard';\n\n @Output() TabSelected = new EventEmitter<string>();\n @Output() TabClosed = new EventEmitter<string>();\n @Output() NewTabRequested = new EventEmitter<void>();\n @Output() TabReordered = new EventEmitter<MJTabReorder>();\n /** The primary verb (create / build / …). */\n @Output() Confirm = new EventEmitter<void>();\n @Output() SaveDraft = new EventEmitter<void>();\n @Output() Discard = new EventEmitter<void>();\n}\n"]}
@@ -0,0 +1,47 @@
1
+ import { MJWorkspaceTab, MJWorkspaceTabState } from './workspace-tabs.types';
2
+ /**
3
+ * The pure, framework-free state machine behind `<mj-workspace-tab-strip>`.
4
+ *
5
+ * Deliberately extracted from the component so the tab semantics are exhaustively unit-testable in
6
+ * isolation (see `__tests__/workspace-tab-store.test.ts`); the Angular shell around it stays thin.
7
+ * No Angular, no DB, no app types — synchronous and total.
8
+ *
9
+ * v1 semantics: session-scoped only. Nothing here touches persistence.
10
+ */
11
+ export declare class MJWorkspaceTabStore<TState = unknown> {
12
+ private tabs;
13
+ private activeId;
14
+ /** Current tabs, in strip order. Returns a copy — callers must not mutate internal state. */
15
+ get Tabs(): MJWorkspaceTab<TState>[];
16
+ get ActiveId(): string | null;
17
+ get ActiveTab(): MJWorkspaceTab<TState> | null;
18
+ get Count(): number;
19
+ /** True when any tab holds unsaved edits — the "you'll lose work" signal for the host. */
20
+ get HasDirtyTabs(): boolean;
21
+ /**
22
+ * Add a tab and make it active. An existing tab with the same Id is NOT duplicated — it is
23
+ * activated instead (re-opening the same draft focuses it, which is what a user means).
24
+ */
25
+ Open(tab: MJWorkspaceTab<TState>): MJWorkspaceTab<TState>;
26
+ Activate(id: string): boolean;
27
+ /**
28
+ * Close a tab. When the closed tab was active, activation falls to its NEIGHBOUR — the tab to its
29
+ * right, else the one to its left, else null. (Closing the tab you're looking at should land you
30
+ * somewhere sensible, not on nothing.)
31
+ */
32
+ Close(id: string): boolean;
33
+ /** Replace a tab's host-owned payload. Marks the tab dirty unless told otherwise. */
34
+ UpdateState(id: string, state: TState, markDirty?: boolean): boolean;
35
+ /** Move a tab through its lifecycle. Clears the rejection reason on any non-rejected state. */
36
+ SetStatus(id: string, status: MJWorkspaceTabState, rejectionReason?: string | null): boolean;
37
+ /** Mark a tab saved/clean without touching its payload. */
38
+ MarkClean(id: string): boolean;
39
+ /**
40
+ * Move a tab from one position to another (drag-reorder). Active tab + payloads are untouched — only
41
+ * strip ORDER changes. Out-of-range or no-op indices return false and change nothing.
42
+ */
43
+ Reorder(fromIndex: number, toIndex: number): boolean;
44
+ /** Drop every tab (session end / explicit discard-all). */
45
+ Clear(): void;
46
+ }
47
+ //# sourceMappingURL=workspace-tab-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace-tab-store.d.ts","sourceRoot":"","sources":["../../../src/lib/workspace-tabs/workspace-tab-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAE7E;;;;;;;;GAQG;AACH,qBAAa,mBAAmB,CAAC,MAAM,GAAG,OAAO;IAC/C,OAAO,CAAC,IAAI,CAAgC;IAC5C,OAAO,CAAC,QAAQ,CAAuB;IAEvC,6FAA6F;IAC7F,IAAW,IAAI,IAAI,cAAc,CAAC,MAAM,CAAC,EAAE,CAE1C;IAED,IAAW,QAAQ,IAAI,MAAM,GAAG,IAAI,CAEnC;IAED,IAAW,SAAS,IAAI,cAAc,CAAC,MAAM,CAAC,GAAG,IAAI,CAEpD;IAED,IAAW,KAAK,IAAI,MAAM,CAEzB;IAED,0FAA0F;IAC1F,IAAW,YAAY,IAAI,OAAO,CAEjC;IAED;;;OAGG;IACI,IAAI,CAAC,GAAG,EAAE,cAAc,CAAC,MAAM,CAAC,GAAG,cAAc,CAAC,MAAM,CAAC;IAWzD,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAMpC;;;;OAIG;IACI,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAcjC,qFAAqF;IAC9E,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,UAAO,GAAG,OAAO;IAQxE,+FAA+F;IACxF,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,mBAAmB,EAAE,eAAe,GAAE,MAAM,GAAG,IAAW,GAAG,OAAO;IAQzG,2DAA2D;IACpD,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAOrC;;;OAGG;IACI,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO;IAQ3D,2DAA2D;IACpD,KAAK,IAAI,IAAI;CAIrB"}