@memberjunction/ng-ui-components 5.51.0 → 6.1.0-edge.1
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.
- package/LICENSE +7 -0
- package/README.md +51 -2
- package/dist/lib/bottom-sheet/bottom-sheet.component.d.ts +96 -0
- package/dist/lib/bottom-sheet/bottom-sheet.component.d.ts.map +1 -0
- package/dist/lib/bottom-sheet/bottom-sheet.component.js +300 -0
- package/dist/lib/bottom-sheet/bottom-sheet.component.js.map +1 -0
- package/dist/lib/left-nav/left-nav.component.d.ts +142 -3
- package/dist/lib/left-nav/left-nav.component.d.ts.map +1 -1
- package/dist/lib/left-nav/left-nav.component.js +391 -117
- package/dist/lib/left-nav/left-nav.component.js.map +1 -1
- package/dist/lib/slide-panel/slide-panel.component.js +6 -6
- package/dist/lib/slide-panel/slide-panel.component.js.map +1 -1
- package/dist/lib/tabs/tab-chrome-guard.d.ts +14 -0
- package/dist/lib/tabs/tab-chrome-guard.d.ts.map +1 -0
- package/dist/lib/tabs/tab-chrome-guard.js +36 -0
- package/dist/lib/tabs/tab-chrome-guard.js.map +1 -0
- package/dist/lib/tabs/tab-list.directive.d.ts +93 -0
- package/dist/lib/tabs/tab-list.directive.d.ts.map +1 -0
- package/dist/lib/tabs/tab-list.directive.js +202 -0
- package/dist/lib/tabs/tab-list.directive.js.map +1 -0
- package/dist/lib/tabs/tabs.scss +242 -0
- package/dist/lib/workspace-tabs/workspace-card.component.d.ts +63 -0
- package/dist/lib/workspace-tabs/workspace-card.component.d.ts.map +1 -0
- package/dist/lib/workspace-tabs/workspace-card.component.js +256 -0
- package/dist/lib/workspace-tabs/workspace-card.component.js.map +1 -0
- package/dist/lib/workspace-tabs/workspace-tab-store.d.ts +47 -0
- package/dist/lib/workspace-tabs/workspace-tab-store.d.ts.map +1 -0
- package/dist/lib/workspace-tabs/workspace-tab-store.js +112 -0
- package/dist/lib/workspace-tabs/workspace-tab-store.js.map +1 -0
- package/dist/lib/workspace-tabs/workspace-tab-strip.component.d.ts +75 -0
- package/dist/lib/workspace-tabs/workspace-tab-strip.component.d.ts.map +1 -0
- package/dist/lib/workspace-tabs/workspace-tab-strip.component.js +252 -0
- package/dist/lib/workspace-tabs/workspace-tab-strip.component.js.map +1 -0
- package/dist/lib/workspace-tabs/workspace-tabs.types.d.ts +40 -0
- package/dist/lib/workspace-tabs/workspace-tabs.types.d.ts.map +1 -0
- package/dist/lib/workspace-tabs/workspace-tabs.types.js +13 -0
- package/dist/lib/workspace-tabs/workspace-tabs.types.js.map +1 -0
- package/dist/lib/workspace-tabs/workspace-tip.directive.d.ts +34 -0
- package/dist/lib/workspace-tabs/workspace-tip.directive.d.ts.map +1 -0
- package/dist/lib/workspace-tabs/workspace-tip.directive.js +123 -0
- package/dist/lib/workspace-tabs/workspace-tip.directive.js.map +1 -0
- package/dist/public-api.d.ts +8 -0
- package/dist/public-api.d.ts.map +1 -1
- package/dist/public-api.js +8 -0
- package/dist/public-api.js.map +1 -1
- package/package.json +14 -12
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { Directive, ElementRef, EventEmitter, HostListener, Output, inject, } from '@angular/core';
|
|
2
|
+
import * as i0 from "@angular/core";
|
|
3
|
+
/**
|
|
4
|
+
* `mjTabList` — the ARIA tabs keyboard contract, shared by every MJ tab strip.
|
|
5
|
+
*
|
|
6
|
+
* Applied to the element that holds the tabs. It manages the things the APG tabs pattern requires
|
|
7
|
+
* and that hand-rolled strips almost always miss:
|
|
8
|
+
*
|
|
9
|
+
* - **roving tabindex** — exactly ONE tab is in the page tab order at a time (the selected one),
|
|
10
|
+
* so Tab moves past the whole strip rather than through every tab in it;
|
|
11
|
+
* - **arrow-key navigation** — Left/Right move between tabs and activate, Home/End jump to the
|
|
12
|
+
* ends, wrapping at the edges;
|
|
13
|
+
* - **Enter / Space** activate the focused tab;
|
|
14
|
+
* - **Delete / Backspace** request closure of the focused tab, which is the APG answer for
|
|
15
|
+
* closeable tabs — a close button inside a tab cannot be its own tab stop without putting two
|
|
16
|
+
* stops per tab in the page order.
|
|
17
|
+
*
|
|
18
|
+
* ## Why a directive rather than a base class
|
|
19
|
+
*
|
|
20
|
+
* MJ's two strips disagree about what a tab IS: one projects `<mj-tab>` component instances and
|
|
21
|
+
* addresses them by positional index, the other renders a data array and addresses it by stable id.
|
|
22
|
+
* A shared base class would have to pick one of those models. This directive picks neither — it
|
|
23
|
+
* operates on the DOM, so each strip keeps its own contract and only has to agree that a tab is
|
|
24
|
+
* `[role="tab"]` and that the selected one carries `aria-selected="true"`. That agreement is the
|
|
25
|
+
* accessibility requirement anyway, so nothing is invented to make the sharing work.
|
|
26
|
+
*
|
|
27
|
+
* ## What the host must do
|
|
28
|
+
*
|
|
29
|
+
* Mark each tab `role="tab"`, keep `aria-selected` truthful, and act on `(TabActivateRequested)` /
|
|
30
|
+
* `(TabCloseRequested)`. The directive sets `role="tablist"` and owns `tabindex` — do not also bind
|
|
31
|
+
* it, or the two will fight.
|
|
32
|
+
*/
|
|
33
|
+
export class MJTabListDirective {
|
|
34
|
+
host = inject(ElementRef).nativeElement;
|
|
35
|
+
/** The user asked to move to / activate a tab. The host maps the index onto its own model. */
|
|
36
|
+
TabActivateRequested = new EventEmitter();
|
|
37
|
+
/** The user pressed Delete/Backspace on a tab. Hosts without closeable tabs simply ignore this. */
|
|
38
|
+
TabCloseRequested = new EventEmitter();
|
|
39
|
+
/**
|
|
40
|
+
* Backstop for tab changes that happen OUTSIDE the host's change detection — a strip that mutates
|
|
41
|
+
* its own DOM, or a projected tab whose component re-renders itself without the parent view being
|
|
42
|
+
* checked. Change detection is the primary trigger (see `ngAfterViewChecked`); this catches the
|
|
43
|
+
* rest. Both funnel into the same idempotent write.
|
|
44
|
+
*/
|
|
45
|
+
observer = typeof MutationObserver === 'function' ? new MutationObserver(() => this.applyRovingTabIndex()) : null;
|
|
46
|
+
constructor() {
|
|
47
|
+
this.observer?.observe(this.host, {
|
|
48
|
+
childList: true,
|
|
49
|
+
subtree: true,
|
|
50
|
+
attributes: true,
|
|
51
|
+
attributeFilter: ['aria-selected'],
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
ngAfterViewInit() {
|
|
55
|
+
this.applyRovingTabIndex();
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The primary trigger. Deliberately a lifecycle hook rather than only the MutationObserver:
|
|
59
|
+
* observer callbacks are asynchronous, so between a selection change and the flush the tab order
|
|
60
|
+
* would point at the PREVIOUS tab — and a tab with no `tabindex` at all is not focusable, making
|
|
61
|
+
* `focus()` a silent no-op. Running here keeps the tab order correct in the same frame the
|
|
62
|
+
* selection changed.
|
|
63
|
+
*
|
|
64
|
+
* Safe to run every cycle: it writes only when a value actually differs, so it cannot loop.
|
|
65
|
+
*/
|
|
66
|
+
ngAfterViewChecked() {
|
|
67
|
+
this.applyRovingTabIndex();
|
|
68
|
+
}
|
|
69
|
+
ngOnDestroy() {
|
|
70
|
+
this.observer?.disconnect();
|
|
71
|
+
}
|
|
72
|
+
/** All tabs in DOM order. Read live rather than cached — the host re-renders them freely. */
|
|
73
|
+
get tabs() {
|
|
74
|
+
return Array.from(this.host.querySelectorAll('[role="tab"]'));
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* A tab hidden with `display: none` still matches `[role="tab"]`, but it cannot take focus
|
|
78
|
+
* (`focus()` silently no-ops) and hosts reject activating it — `mj-tabstrip` throws on selecting
|
|
79
|
+
* an invisible tab, and Entity Actions really does hide tabs until a record is saved. So
|
|
80
|
+
* navigation and the roving tab stop must skip them.
|
|
81
|
+
*
|
|
82
|
+
* Checked via INLINE style up the chain rather than `offsetParent`/`getClientRects`, because
|
|
83
|
+
* jsdom computes no layout (those report every element as invisible in tests) while both strips
|
|
84
|
+
* hide tabs exactly this way — `mj-tab` binds `[style.display]` on its host.
|
|
85
|
+
*/
|
|
86
|
+
isTabVisible(tab) {
|
|
87
|
+
let el = tab;
|
|
88
|
+
while (el && el !== this.host) {
|
|
89
|
+
if (el.style.display === 'none')
|
|
90
|
+
return false;
|
|
91
|
+
el = el.parentElement;
|
|
92
|
+
}
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Exactly one tab in the page tab order. Falls back to the first tab when nothing is selected,
|
|
97
|
+
* so the strip is never a keyboard dead end (a list where every tab is -1 cannot be entered).
|
|
98
|
+
*/
|
|
99
|
+
applyRovingTabIndex() {
|
|
100
|
+
const tabs = this.tabs;
|
|
101
|
+
if (tabs.length === 0)
|
|
102
|
+
return;
|
|
103
|
+
// The single tab stop must be FOCUSABLE: the selected tab normally, but a hidden selected tab
|
|
104
|
+
// (or no selection) falls back to the first visible one — a stop on a display:none element
|
|
105
|
+
// would make the whole strip unreachable by keyboard.
|
|
106
|
+
let stop = tabs.findIndex(t => t.getAttribute('aria-selected') === 'true' && this.isTabVisible(t));
|
|
107
|
+
if (stop < 0)
|
|
108
|
+
stop = tabs.findIndex(t => this.isTabVisible(t));
|
|
109
|
+
for (let i = 0; i < tabs.length; i++) {
|
|
110
|
+
const next = i === stop ? '0' : '-1';
|
|
111
|
+
// Write only on an actual difference — this runs on every change-detection pass, and a
|
|
112
|
+
// needless setAttribute would both churn the DOM and re-trigger the MutationObserver.
|
|
113
|
+
if (tabs[i].getAttribute('tabindex') !== next) {
|
|
114
|
+
tabs[i].setAttribute('tabindex', next);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
OnKeydown(event) {
|
|
119
|
+
const tabs = this.tabs;
|
|
120
|
+
if (tabs.length === 0)
|
|
121
|
+
return;
|
|
122
|
+
const target = event.target;
|
|
123
|
+
// A tab may project arbitrary content. Keys typed INTO an editable control must keep their
|
|
124
|
+
// native meaning — hijacking Backspace inside an inline-rename input (or arrows moving its
|
|
125
|
+
// caret) would be indefensible.
|
|
126
|
+
if (target && (target.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(target.tagName))) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
// Only handle keys aimed at a tab — a control INSIDE the panel must keep its own behaviour.
|
|
130
|
+
const current = target?.closest('[role="tab"]');
|
|
131
|
+
if (!current || !this.host.contains(current))
|
|
132
|
+
return;
|
|
133
|
+
const index = tabs.indexOf(current);
|
|
134
|
+
if (index === -1)
|
|
135
|
+
return;
|
|
136
|
+
// Arrow/Home/End navigate the VISIBLE tabs only — a hidden tab cannot take focus and hosts
|
|
137
|
+
// reject activating it. Indices reported to the host stay positions in the FULL tab list, so
|
|
138
|
+
// the host's own index-addressed model is never off-by-hidden.
|
|
139
|
+
const visible = tabs.filter(t => this.isTabVisible(t));
|
|
140
|
+
const visibleIndex = visible.indexOf(current);
|
|
141
|
+
switch (event.key) {
|
|
142
|
+
case 'ArrowRight':
|
|
143
|
+
this.move(tabs, visible, (visibleIndex + 1) % visible.length, event);
|
|
144
|
+
break;
|
|
145
|
+
case 'ArrowLeft':
|
|
146
|
+
this.move(tabs, visible, (visibleIndex - 1 + visible.length) % visible.length, event);
|
|
147
|
+
break;
|
|
148
|
+
case 'Home':
|
|
149
|
+
this.move(tabs, visible, 0, event);
|
|
150
|
+
break;
|
|
151
|
+
case 'End':
|
|
152
|
+
this.move(tabs, visible, visible.length - 1, event);
|
|
153
|
+
break;
|
|
154
|
+
case 'Enter':
|
|
155
|
+
case ' ':
|
|
156
|
+
event.preventDefault();
|
|
157
|
+
this.TabActivateRequested.emit({ Index: index, Element: current });
|
|
158
|
+
break;
|
|
159
|
+
case 'Delete':
|
|
160
|
+
case 'Backspace':
|
|
161
|
+
event.preventDefault();
|
|
162
|
+
this.TabCloseRequested.emit({ Index: index, Element: current });
|
|
163
|
+
break;
|
|
164
|
+
default:
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Follow-focus activation: moving to a tab selects it, which is the APG default for tab lists
|
|
170
|
+
* whose panels are cheap to show, and is what every desktop browser does.
|
|
171
|
+
*/
|
|
172
|
+
move(all, visible, to, event) {
|
|
173
|
+
event.preventDefault();
|
|
174
|
+
const target = visible[to];
|
|
175
|
+
if (!target)
|
|
176
|
+
return;
|
|
177
|
+
// Focus BEFORE emitting: activation re-renders the strip in some hosts, and focusing a detached
|
|
178
|
+
// element afterwards would silently drop focus to <body>.
|
|
179
|
+
target.focus();
|
|
180
|
+
this.TabActivateRequested.emit({ Index: all.indexOf(target), Element: target });
|
|
181
|
+
}
|
|
182
|
+
static ɵfac = function MJTabListDirective_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || MJTabListDirective)(); };
|
|
183
|
+
static ɵdir = /*@__PURE__*/ i0.ɵɵdefineDirective({ type: MJTabListDirective, selectors: [["", "mjTabList", ""]], hostAttrs: ["role", "tablist"], hostBindings: function MJTabListDirective_HostBindings(rf, ctx) { if (rf & 1) {
|
|
184
|
+
i0.ɵɵlistener("keydown", function MJTabListDirective_keydown_HostBindingHandler($event) { return ctx.OnKeydown($event); });
|
|
185
|
+
} }, outputs: { TabActivateRequested: "TabActivateRequested", TabCloseRequested: "TabCloseRequested" } });
|
|
186
|
+
}
|
|
187
|
+
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(MJTabListDirective, [{
|
|
188
|
+
type: Directive,
|
|
189
|
+
args: [{
|
|
190
|
+
selector: '[mjTabList]',
|
|
191
|
+
standalone: true,
|
|
192
|
+
host: { role: 'tablist' },
|
|
193
|
+
}]
|
|
194
|
+
}], () => [], { TabActivateRequested: [{
|
|
195
|
+
type: Output
|
|
196
|
+
}], TabCloseRequested: [{
|
|
197
|
+
type: Output
|
|
198
|
+
}], OnKeydown: [{
|
|
199
|
+
type: HostListener,
|
|
200
|
+
args: ['keydown', ['$event']]
|
|
201
|
+
}] }); })();
|
|
202
|
+
//# sourceMappingURL=tab-list.directive.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tab-list.directive.js","sourceRoot":"","sources":["../../../src/lib/tabs/tab-list.directive.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n AfterViewChecked,\n AfterViewInit,\n Directive,\n ElementRef,\n EventEmitter,\n HostListener,\n OnDestroy,\n Output,\n inject,\n} from '@angular/core';\n\n/** A keyboard-driven request from the tab list, carrying the tab it refers to. */\nexport interface MJTabListRequest {\n /** Position of the tab among its siblings, which is all this directive can know about identity. */\n Index: number;\n /** The tab element itself, for hosts that key off a data attribute rather than position. */\n Element: HTMLElement;\n}\n\n/**\n * `mjTabList` — the ARIA tabs keyboard contract, shared by every MJ tab strip.\n *\n * Applied to the element that holds the tabs. It manages the things the APG tabs pattern requires\n * and that hand-rolled strips almost always miss:\n *\n * - **roving tabindex** — exactly ONE tab is in the page tab order at a time (the selected one),\n * so Tab moves past the whole strip rather than through every tab in it;\n * - **arrow-key navigation** — Left/Right move between tabs and activate, Home/End jump to the\n * ends, wrapping at the edges;\n * - **Enter / Space** activate the focused tab;\n * - **Delete / Backspace** request closure of the focused tab, which is the APG answer for\n * closeable tabs — a close button inside a tab cannot be its own tab stop without putting two\n * stops per tab in the page order.\n *\n * ## Why a directive rather than a base class\n *\n * MJ's two strips disagree about what a tab IS: one projects `<mj-tab>` component instances and\n * addresses them by positional index, the other renders a data array and addresses it by stable id.\n * A shared base class would have to pick one of those models. This directive picks neither — it\n * operates on the DOM, so each strip keeps its own contract and only has to agree that a tab is\n * `[role=\"tab\"]` and that the selected one carries `aria-selected=\"true\"`. That agreement is the\n * accessibility requirement anyway, so nothing is invented to make the sharing work.\n *\n * ## What the host must do\n *\n * Mark each tab `role=\"tab\"`, keep `aria-selected` truthful, and act on `(TabActivateRequested)` /\n * `(TabCloseRequested)`. The directive sets `role=\"tablist\"` and owns `tabindex` — do not also bind\n * it, or the two will fight.\n */\n@Directive({\n selector: '[mjTabList]',\n standalone: true,\n host: { role: 'tablist' },\n})\nexport class MJTabListDirective implements AfterViewInit, AfterViewChecked, OnDestroy {\n private readonly host = inject(ElementRef).nativeElement as HTMLElement;\n\n /** The user asked to move to / activate a tab. The host maps the index onto its own model. */\n @Output() TabActivateRequested = new EventEmitter<MJTabListRequest>();\n\n /** The user pressed Delete/Backspace on a tab. Hosts without closeable tabs simply ignore this. */\n @Output() TabCloseRequested = new EventEmitter<MJTabListRequest>();\n\n /**\n * Backstop for tab changes that happen OUTSIDE the host's change detection — a strip that mutates\n * its own DOM, or a projected tab whose component re-renders itself without the parent view being\n * checked. Change detection is the primary trigger (see `ngAfterViewChecked`); this catches the\n * rest. Both funnel into the same idempotent write.\n */\n private readonly observer: MutationObserver | null =\n typeof MutationObserver === 'function' ? new MutationObserver(() => this.applyRovingTabIndex()) : null;\n\n constructor() {\n this.observer?.observe(this.host, {\n childList: true,\n subtree: true,\n attributes: true,\n attributeFilter: ['aria-selected'],\n });\n }\n\n ngAfterViewInit(): void {\n this.applyRovingTabIndex();\n }\n\n /**\n * The primary trigger. Deliberately a lifecycle hook rather than only the MutationObserver:\n * observer callbacks are asynchronous, so between a selection change and the flush the tab order\n * would point at the PREVIOUS tab — and a tab with no `tabindex` at all is not focusable, making\n * `focus()` a silent no-op. Running here keeps the tab order correct in the same frame the\n * selection changed.\n *\n * Safe to run every cycle: it writes only when a value actually differs, so it cannot loop.\n */\n ngAfterViewChecked(): void {\n this.applyRovingTabIndex();\n }\n\n ngOnDestroy(): void {\n this.observer?.disconnect();\n }\n\n /** All tabs in DOM order. Read live rather than cached — the host re-renders them freely. */\n private get tabs(): HTMLElement[] {\n return Array.from(this.host.querySelectorAll<HTMLElement>('[role=\"tab\"]'));\n }\n\n /**\n * A tab hidden with `display: none` still matches `[role=\"tab\"]`, but it cannot take focus\n * (`focus()` silently no-ops) and hosts reject activating it — `mj-tabstrip` throws on selecting\n * an invisible tab, and Entity Actions really does hide tabs until a record is saved. So\n * navigation and the roving tab stop must skip them.\n *\n * Checked via INLINE style up the chain rather than `offsetParent`/`getClientRects`, because\n * jsdom computes no layout (those report every element as invisible in tests) while both strips\n * hide tabs exactly this way — `mj-tab` binds `[style.display]` on its host.\n */\n private isTabVisible(tab: HTMLElement): boolean {\n let el: HTMLElement | null = tab;\n while (el && el !== this.host) {\n if (el.style.display === 'none') return false;\n el = el.parentElement;\n }\n return true;\n }\n\n /**\n * Exactly one tab in the page tab order. Falls back to the first tab when nothing is selected,\n * so the strip is never a keyboard dead end (a list where every tab is -1 cannot be entered).\n */\n private applyRovingTabIndex(): void {\n const tabs = this.tabs;\n if (tabs.length === 0) return;\n // The single tab stop must be FOCUSABLE: the selected tab normally, but a hidden selected tab\n // (or no selection) falls back to the first visible one — a stop on a display:none element\n // would make the whole strip unreachable by keyboard.\n let stop = tabs.findIndex(t => t.getAttribute('aria-selected') === 'true' && this.isTabVisible(t));\n if (stop < 0) stop = tabs.findIndex(t => this.isTabVisible(t));\n for (let i = 0; i < tabs.length; i++) {\n const next = i === stop ? '0' : '-1';\n // Write only on an actual difference — this runs on every change-detection pass, and a\n // needless setAttribute would both churn the DOM and re-trigger the MutationObserver.\n if (tabs[i].getAttribute('tabindex') !== next) {\n tabs[i].setAttribute('tabindex', next);\n }\n }\n }\n\n @HostListener('keydown', ['$event'])\n public OnKeydown(event: KeyboardEvent): void {\n const tabs = this.tabs;\n if (tabs.length === 0) return;\n\n const target = event.target as HTMLElement | null;\n\n // A tab may project arbitrary content. Keys typed INTO an editable control must keep their\n // native meaning — hijacking Backspace inside an inline-rename input (or arrows moving its\n // caret) would be indefensible.\n if (target && (target.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(target.tagName))) {\n return;\n }\n\n // Only handle keys aimed at a tab — a control INSIDE the panel must keep its own behaviour.\n const current = target?.closest<HTMLElement>('[role=\"tab\"]');\n if (!current || !this.host.contains(current)) return;\n\n const index = tabs.indexOf(current);\n if (index === -1) return;\n\n // Arrow/Home/End navigate the VISIBLE tabs only — a hidden tab cannot take focus and hosts\n // reject activating it. Indices reported to the host stay positions in the FULL tab list, so\n // the host's own index-addressed model is never off-by-hidden.\n const visible = tabs.filter(t => this.isTabVisible(t));\n const visibleIndex = visible.indexOf(current);\n\n switch (event.key) {\n case 'ArrowRight':\n this.move(tabs, visible, (visibleIndex + 1) % visible.length, event);\n break;\n case 'ArrowLeft':\n this.move(tabs, visible, (visibleIndex - 1 + visible.length) % visible.length, event);\n break;\n case 'Home':\n this.move(tabs, visible, 0, event);\n break;\n case 'End':\n this.move(tabs, visible, visible.length - 1, event);\n break;\n case 'Enter':\n case ' ':\n event.preventDefault();\n this.TabActivateRequested.emit({ Index: index, Element: current });\n break;\n case 'Delete':\n case 'Backspace':\n event.preventDefault();\n this.TabCloseRequested.emit({ Index: index, Element: current });\n break;\n default:\n return;\n }\n }\n\n /**\n * Follow-focus activation: moving to a tab selects it, which is the APG default for tab lists\n * whose panels are cheap to show, and is what every desktop browser does.\n */\n private move(all: HTMLElement[], visible: HTMLElement[], to: number, event: KeyboardEvent): void {\n event.preventDefault();\n const target = visible[to];\n if (!target) return;\n // Focus BEFORE emitting: activation re-renders the strip in some hosts, and focusing a detached\n // element afterwards would silently drop focus to <body>.\n target.focus();\n this.TabActivateRequested.emit({ Index: all.indexOf(target), Element: target });\n }\n}\n"]}
|
|
@@ -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"}
|