@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.
- package/README.md +42 -2
- 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/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 +7 -0
- package/dist/public-api.d.ts.map +1 -1
- package/dist/public-api.js +7 -0
- package/dist/public-api.js.map +1 -1
- package/package.json +5 -5
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pure, framework-free state machine behind `<mj-workspace-tab-strip>`.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately extracted from the component so the tab semantics are exhaustively unit-testable in
|
|
5
|
+
* isolation (see `__tests__/workspace-tab-store.test.ts`); the Angular shell around it stays thin.
|
|
6
|
+
* No Angular, no DB, no app types — synchronous and total.
|
|
7
|
+
*
|
|
8
|
+
* v1 semantics: session-scoped only. Nothing here touches persistence.
|
|
9
|
+
*/
|
|
10
|
+
export class MJWorkspaceTabStore {
|
|
11
|
+
tabs = [];
|
|
12
|
+
activeId = null;
|
|
13
|
+
/** Current tabs, in strip order. Returns a copy — callers must not mutate internal state. */
|
|
14
|
+
get Tabs() {
|
|
15
|
+
return [...this.tabs];
|
|
16
|
+
}
|
|
17
|
+
get ActiveId() {
|
|
18
|
+
return this.activeId;
|
|
19
|
+
}
|
|
20
|
+
get ActiveTab() {
|
|
21
|
+
return this.tabs.find((t) => t.Id === this.activeId) ?? null;
|
|
22
|
+
}
|
|
23
|
+
get Count() {
|
|
24
|
+
return this.tabs.length;
|
|
25
|
+
}
|
|
26
|
+
/** True when any tab holds unsaved edits — the "you'll lose work" signal for the host. */
|
|
27
|
+
get HasDirtyTabs() {
|
|
28
|
+
return this.tabs.some((t) => t.Dirty === true);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Add a tab and make it active. An existing tab with the same Id is NOT duplicated — it is
|
|
32
|
+
* activated instead (re-opening the same draft focuses it, which is what a user means).
|
|
33
|
+
*/
|
|
34
|
+
Open(tab) {
|
|
35
|
+
const existing = this.tabs.find((t) => t.Id === tab.Id);
|
|
36
|
+
if (existing) {
|
|
37
|
+
this.activeId = existing.Id;
|
|
38
|
+
return existing;
|
|
39
|
+
}
|
|
40
|
+
this.tabs.push(tab);
|
|
41
|
+
this.activeId = tab.Id;
|
|
42
|
+
return tab;
|
|
43
|
+
}
|
|
44
|
+
Activate(id) {
|
|
45
|
+
if (!this.tabs.some((t) => t.Id === id))
|
|
46
|
+
return false;
|
|
47
|
+
this.activeId = id;
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Close a tab. When the closed tab was active, activation falls to its NEIGHBOUR — the tab to its
|
|
52
|
+
* right, else the one to its left, else null. (Closing the tab you're looking at should land you
|
|
53
|
+
* somewhere sensible, not on nothing.)
|
|
54
|
+
*/
|
|
55
|
+
Close(id) {
|
|
56
|
+
const index = this.tabs.findIndex((t) => t.Id === id);
|
|
57
|
+
if (index === -1)
|
|
58
|
+
return false;
|
|
59
|
+
const wasActive = this.activeId === id;
|
|
60
|
+
this.tabs.splice(index, 1);
|
|
61
|
+
if (wasActive) {
|
|
62
|
+
const neighbour = this.tabs[index] ?? this.tabs[index - 1] ?? null;
|
|
63
|
+
this.activeId = neighbour ? neighbour.Id : null;
|
|
64
|
+
}
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
/** Replace a tab's host-owned payload. Marks the tab dirty unless told otherwise. */
|
|
68
|
+
UpdateState(id, state, markDirty = true) {
|
|
69
|
+
const tab = this.tabs.find((t) => t.Id === id);
|
|
70
|
+
if (!tab)
|
|
71
|
+
return false;
|
|
72
|
+
tab.State = state;
|
|
73
|
+
if (markDirty)
|
|
74
|
+
tab.Dirty = true;
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
/** Move a tab through its lifecycle. Clears the rejection reason on any non-rejected state. */
|
|
78
|
+
SetStatus(id, status, rejectionReason = null) {
|
|
79
|
+
const tab = this.tabs.find((t) => t.Id === id);
|
|
80
|
+
if (!tab)
|
|
81
|
+
return false;
|
|
82
|
+
tab.Status = status;
|
|
83
|
+
tab.RejectionReason = status === 'rejected' ? rejectionReason : null;
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
/** Mark a tab saved/clean without touching its payload. */
|
|
87
|
+
MarkClean(id) {
|
|
88
|
+
const tab = this.tabs.find((t) => t.Id === id);
|
|
89
|
+
if (!tab)
|
|
90
|
+
return false;
|
|
91
|
+
tab.Dirty = false;
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Move a tab from one position to another (drag-reorder). Active tab + payloads are untouched — only
|
|
96
|
+
* strip ORDER changes. Out-of-range or no-op indices return false and change nothing.
|
|
97
|
+
*/
|
|
98
|
+
Reorder(fromIndex, toIndex) {
|
|
99
|
+
const n = this.tabs.length;
|
|
100
|
+
if (fromIndex === toIndex || fromIndex < 0 || fromIndex >= n || toIndex < 0 || toIndex >= n)
|
|
101
|
+
return false;
|
|
102
|
+
const [moved] = this.tabs.splice(fromIndex, 1);
|
|
103
|
+
this.tabs.splice(toIndex, 0, moved);
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
/** Drop every tab (session end / explicit discard-all). */
|
|
107
|
+
Clear() {
|
|
108
|
+
this.tabs = [];
|
|
109
|
+
this.activeId = null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=workspace-tab-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace-tab-store.js","sourceRoot":"","sources":["../../../src/lib/workspace-tabs/workspace-tab-store.ts"],"names":[],"mappings":"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","sourcesContent":["import { MJWorkspaceTab, MJWorkspaceTabState } from './workspace-tabs.types';\n\n/**\n * The pure, framework-free state machine behind `<mj-workspace-tab-strip>`.\n *\n * Deliberately extracted from the component so the tab semantics are exhaustively unit-testable in\n * isolation (see `__tests__/workspace-tab-store.test.ts`); the Angular shell around it stays thin.\n * No Angular, no DB, no app types — synchronous and total.\n *\n * v1 semantics: session-scoped only. Nothing here touches persistence.\n */\nexport class MJWorkspaceTabStore<TState = unknown> {\n private tabs: MJWorkspaceTab<TState>[] = [];\n private activeId: string | null = null;\n\n /** Current tabs, in strip order. Returns a copy — callers must not mutate internal state. */\n public get Tabs(): MJWorkspaceTab<TState>[] {\n return [...this.tabs];\n }\n\n public get ActiveId(): string | null {\n return this.activeId;\n }\n\n public get ActiveTab(): MJWorkspaceTab<TState> | null {\n return this.tabs.find((t) => t.Id === this.activeId) ?? null;\n }\n\n public get Count(): number {\n return this.tabs.length;\n }\n\n /** True when any tab holds unsaved edits — the \"you'll lose work\" signal for the host. */\n public get HasDirtyTabs(): boolean {\n return this.tabs.some((t) => t.Dirty === true);\n }\n\n /**\n * Add a tab and make it active. An existing tab with the same Id is NOT duplicated — it is\n * activated instead (re-opening the same draft focuses it, which is what a user means).\n */\n public Open(tab: MJWorkspaceTab<TState>): MJWorkspaceTab<TState> {\n const existing = this.tabs.find((t) => t.Id === tab.Id);\n if (existing) {\n this.activeId = existing.Id;\n return existing;\n }\n this.tabs.push(tab);\n this.activeId = tab.Id;\n return tab;\n }\n\n public Activate(id: string): boolean {\n if (!this.tabs.some((t) => t.Id === id)) return false;\n this.activeId = id;\n return true;\n }\n\n /**\n * Close a tab. When the closed tab was active, activation falls to its NEIGHBOUR — the tab to its\n * right, else the one to its left, else null. (Closing the tab you're looking at should land you\n * somewhere sensible, not on nothing.)\n */\n public Close(id: string): boolean {\n const index = this.tabs.findIndex((t) => t.Id === id);\n if (index === -1) return false;\n\n const wasActive = this.activeId === id;\n this.tabs.splice(index, 1);\n\n if (wasActive) {\n const neighbour = this.tabs[index] ?? this.tabs[index - 1] ?? null;\n this.activeId = neighbour ? neighbour.Id : null;\n }\n return true;\n }\n\n /** Replace a tab's host-owned payload. Marks the tab dirty unless told otherwise. */\n public UpdateState(id: string, state: TState, markDirty = true): boolean {\n const tab = this.tabs.find((t) => t.Id === id);\n if (!tab) return false;\n tab.State = state;\n if (markDirty) tab.Dirty = true;\n return true;\n }\n\n /** Move a tab through its lifecycle. Clears the rejection reason on any non-rejected state. */\n public SetStatus(id: string, status: MJWorkspaceTabState, rejectionReason: string | null = null): boolean {\n const tab = this.tabs.find((t) => t.Id === id);\n if (!tab) return false;\n tab.Status = status;\n tab.RejectionReason = status === 'rejected' ? rejectionReason : null;\n return true;\n }\n\n /** Mark a tab saved/clean without touching its payload. */\n public MarkClean(id: string): boolean {\n const tab = this.tabs.find((t) => t.Id === id);\n if (!tab) return false;\n tab.Dirty = false;\n return true;\n }\n\n /**\n * Move a tab from one position to another (drag-reorder). Active tab + payloads are untouched — only\n * strip ORDER changes. Out-of-range or no-op indices return false and change nothing.\n */\n public Reorder(fromIndex: number, toIndex: number): boolean {\n const n = this.tabs.length;\n if (fromIndex === toIndex || fromIndex < 0 || fromIndex >= n || toIndex < 0 || toIndex >= n) return false;\n const [moved] = this.tabs.splice(fromIndex, 1);\n this.tabs.splice(toIndex, 0, moved);\n return true;\n }\n\n /** Drop every tab (session end / explicit discard-all). */\n public Clear(): void {\n this.tabs = [];\n this.activeId = null;\n }\n}\n"]}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { AfterViewInit, EventEmitter } from '@angular/core';
|
|
2
|
+
import { CdkDragDrop } from '@angular/cdk/drag-drop';
|
|
3
|
+
import { MJWorkspaceTab } from './workspace-tabs.types';
|
|
4
|
+
import { MJTabListRequest } from '../tabs/tab-list.directive';
|
|
5
|
+
import * as i0 from "@angular/core";
|
|
6
|
+
/** A drag-reorder request from the strip: move the tab at `previousIndex` to `currentIndex`. */
|
|
7
|
+
export interface MJTabReorder {
|
|
8
|
+
previousIndex: number;
|
|
9
|
+
currentIndex: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* `<mj-workspace-tab-strip>` — browser-style draft tabs, presentation over `MJWorkspaceTabStore`.
|
|
13
|
+
*
|
|
14
|
+
* Dumb by design: it renders the tabs it is handed and emits intent. The store holds the state
|
|
15
|
+
* machine; the host owns what a tab's payload means. That split keeps it app-agnostic.
|
|
16
|
+
*
|
|
17
|
+
* **Browser-tab behavior lives HERE** so every consumer inherits it:
|
|
18
|
+
* - pinned new-tab button; the tab LIST scrolls horizontally *behind* it on overflow;
|
|
19
|
+
* - tabs size to their text up to `--mj-tab-max`, then ellipsize; the inline unsaved-dot pushes the label;
|
|
20
|
+
* - the tab body keeps the default arrow cursor (like browser tabs);
|
|
21
|
+
* - the full label shows via the reusable `mjTip` tooltip when a tab is truncated (delayed, non-interactive);
|
|
22
|
+
* - drag-reorder via CDK, emitted as intent for the host to apply to its store.
|
|
23
|
+
*
|
|
24
|
+
* **Appearance and keyboard behaviour are NOT owned here.** The look comes from the global
|
|
25
|
+
* `.mj-tabs*` chrome in `tabs.scss`, and the ARIA tabs keyboard contract from `mjTabList` — both
|
|
26
|
+
* shared with `mj-tabstrip`, so the two strips cannot drift apart visually or behaviourally even
|
|
27
|
+
* though they keep different state models.
|
|
28
|
+
*/
|
|
29
|
+
export declare class MJWorkspaceTabStripComponent implements AfterViewInit {
|
|
30
|
+
private readonly elementRef;
|
|
31
|
+
ngAfterViewInit(): void;
|
|
32
|
+
Tabs: MJWorkspaceTab[];
|
|
33
|
+
ActiveId: string | null;
|
|
34
|
+
ShowNewTab: boolean;
|
|
35
|
+
NewTabLabel: string;
|
|
36
|
+
/**
|
|
37
|
+
* Whether tabs can be drag-reordered at all. The escape hatch for hosts where even long-press
|
|
38
|
+
* dragging is wrong (a kiosk surface, a read-only review screen) — reorder simply isn't offered
|
|
39
|
+
* and every touch gesture belongs to scrolling.
|
|
40
|
+
*/
|
|
41
|
+
AllowReorder: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Per-pointer-type drag threshold, resolving the touch conflict between REORDER and SCROLL: an
|
|
44
|
+
* overflowing tab list scrolls by horizontal swipe, and a swipe starts on a tab — with an
|
|
45
|
+
* immediate drag threshold, CDK claims that touch and the list becomes unscrollable on phones.
|
|
46
|
+
* A hold-to-drag delay is the platform idiom for exactly this (iOS home screen, mobile browser
|
|
47
|
+
* tabs): a moving finger within the delay window is a SCROLL and CDK lets it through natively; a
|
|
48
|
+
* still finger past it is a DRAG. Mouse stays immediate — pointer users scroll with the wheel or
|
|
49
|
+
* scrollbar, so there is no gesture to disambiguate and a delay would just feel laggy.
|
|
50
|
+
*/
|
|
51
|
+
readonly DragStartDelay: {
|
|
52
|
+
touch: number;
|
|
53
|
+
mouse: number;
|
|
54
|
+
};
|
|
55
|
+
TabSelected: EventEmitter<string>;
|
|
56
|
+
TabClosed: EventEmitter<string>;
|
|
57
|
+
NewTabRequested: EventEmitter<void>;
|
|
58
|
+
TabReordered: EventEmitter<MJTabReorder>;
|
|
59
|
+
/** CDK drop — emit the reorder intent; the host applies it to the store (the strip stays dumb). */
|
|
60
|
+
onDrop(event: CdkDragDrop<MJWorkspaceTab[]>): void;
|
|
61
|
+
/**
|
|
62
|
+
* The tab's accessible name. The dirty dot and the rejected colour/icon are the only signals for
|
|
63
|
+
* those states, and neither survives being read aloud — so they are spelled out here instead.
|
|
64
|
+
*/
|
|
65
|
+
tabAccessibleName(tab: MJWorkspaceTab): string;
|
|
66
|
+
/** Keyboard arrow/Home/End/Enter from `mjTabList` — map the position back onto our id. */
|
|
67
|
+
onActivateRequested(request: MJTabListRequest): void;
|
|
68
|
+
/** Delete/Backspace on a focused tab — the APG close gesture for a closeable tab. */
|
|
69
|
+
onCloseRequested(request: MJTabListRequest): void;
|
|
70
|
+
/** The close button sits inside the tab, so its click would otherwise also select the tab. */
|
|
71
|
+
onCloseClick(event: MouseEvent, id: string): void;
|
|
72
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<MJWorkspaceTabStripComponent, never>;
|
|
73
|
+
static ɵcmp: i0.ɵɵComponentDeclaration<MJWorkspaceTabStripComponent, "mj-workspace-tab-strip", never, { "Tabs": { "alias": "Tabs"; "required": false; }; "ActiveId": { "alias": "ActiveId"; "required": false; }; "ShowNewTab": { "alias": "ShowNewTab"; "required": false; }; "NewTabLabel": { "alias": "NewTabLabel"; "required": false; }; "AllowReorder": { "alias": "AllowReorder"; "required": false; }; }, { "TabSelected": "TabSelected"; "TabClosed": "TabClosed"; "NewTabRequested": "NewTabRequested"; "TabReordered": "TabReordered"; }, never, never, true, never>;
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=workspace-tab-strip.component.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace-tab-strip.component.d.ts","sourceRoot":"","sources":["../../../src/lib/workspace-tabs/workspace-tab-strip.component.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAkD,YAAY,EAAyB,MAAM,eAAe,CAAC;AAEnI,OAAO,EAAwB,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC3E,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAExD,OAAO,EAAsB,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;;AAGlF,gGAAgG;AAChG,MAAM,WAAW,YAAY;IAC3B,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAoEa,4BAA6B,YAAW,aAAa;IAChE,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAsB;IAEjD,eAAe,IAAI,IAAI;IAId,IAAI,EAAE,cAAc,EAAE,CAAM;IAC5B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAQ;IAC/B,UAAU,UAAQ;IAClB,WAAW,SAAS;IAE7B;;;;OAIG;IACM,YAAY,UAAQ;IAE7B;;;;;;;;OAQG;IACH,SAAgB,cAAc;;;MAA4B;IAEhD,WAAW,uBAA8B;IACzC,SAAS,uBAA8B;IACvC,eAAe,qBAA4B;IAC3C,YAAY,6BAAoC;IAE1D,mGAAmG;IAC5F,MAAM,CAAC,KAAK,EAAE,WAAW,CAAC,cAAc,EAAE,CAAC,GAAG,IAAI;IAMzD;;;OAGG;IACI,iBAAiB,CAAC,GAAG,EAAE,cAAc,GAAG,MAAM;IAOrD,0FAA0F;IACnF,mBAAmB,CAAC,OAAO,EAAE,gBAAgB,GAAG,IAAI;IAO3D,qFAAqF;IAC9E,gBAAgB,CAAC,OAAO,EAAE,gBAAgB,GAAG,IAAI;IAOxD,8FAA8F;IACvF,YAAY,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI;yCAtE7C,4BAA4B;2CAA5B,4BAA4B;CA0ExC"}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { Component, ChangeDetectionStrategy, ElementRef, EventEmitter, Input, Output, inject } from '@angular/core';
|
|
2
|
+
import { CommonModule } from '@angular/common';
|
|
3
|
+
import { CdkDropList, CdkDrag } from '@angular/cdk/drag-drop';
|
|
4
|
+
import { MJWorkspaceTipDirective } from './workspace-tip.directive';
|
|
5
|
+
import { MJTabListDirective } from '../tabs/tab-list.directive';
|
|
6
|
+
import { warnIfTabChromeMissing } from '../tabs/tab-chrome-guard';
|
|
7
|
+
import * as i0 from "@angular/core";
|
|
8
|
+
const _forTrack0 = ($index, $item) => $item.Id;
|
|
9
|
+
function MJWorkspaceTabStripComponent_For_3_Conditional_1_Template(rf, ctx) { if (rf & 1) {
|
|
10
|
+
i0.ɵɵelement(0, "i", 11);
|
|
11
|
+
} if (rf & 2) {
|
|
12
|
+
const tab_r2 = i0.ɵɵnextContext().$implicit;
|
|
13
|
+
i0.ɵɵclassMap(tab_r2.Icon);
|
|
14
|
+
} }
|
|
15
|
+
function MJWorkspaceTabStripComponent_For_3_Conditional_2_Template(rf, ctx) { if (rf & 1) {
|
|
16
|
+
i0.ɵɵelement(0, "i", 6);
|
|
17
|
+
} }
|
|
18
|
+
function MJWorkspaceTabStripComponent_For_3_Conditional_5_Template(rf, ctx) { if (rf & 1) {
|
|
19
|
+
i0.ɵɵelementStart(0, "span", 8);
|
|
20
|
+
i0.ɵɵtext(1, "\u2022");
|
|
21
|
+
i0.ɵɵelementEnd();
|
|
22
|
+
} }
|
|
23
|
+
function MJWorkspaceTabStripComponent_For_3_Template(rf, ctx) { if (rf & 1) {
|
|
24
|
+
const _r1 = i0.ɵɵgetCurrentView();
|
|
25
|
+
i0.ɵɵelementStart(0, "div", 4);
|
|
26
|
+
i0.ɵɵlistener("click", function MJWorkspaceTabStripComponent_For_3_Template_div_click_0_listener() { const tab_r2 = i0.ɵɵrestoreView(_r1).$implicit; const ctx_r2 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r2.TabSelected.emit(tab_r2.Id)); });
|
|
27
|
+
i0.ɵɵconditionalCreate(1, MJWorkspaceTabStripComponent_For_3_Conditional_1_Template, 1, 2, "i", 5);
|
|
28
|
+
i0.ɵɵconditionalCreate(2, MJWorkspaceTabStripComponent_For_3_Conditional_2_Template, 1, 0, "i", 6);
|
|
29
|
+
i0.ɵɵelementStart(3, "span", 7);
|
|
30
|
+
i0.ɵɵtext(4);
|
|
31
|
+
i0.ɵɵelementEnd();
|
|
32
|
+
i0.ɵɵconditionalCreate(5, MJWorkspaceTabStripComponent_For_3_Conditional_5_Template, 2, 0, "span", 8);
|
|
33
|
+
i0.ɵɵelementStart(6, "button", 9);
|
|
34
|
+
i0.ɵɵlistener("click", function MJWorkspaceTabStripComponent_For_3_Template_button_click_6_listener($event) { const tab_r2 = i0.ɵɵrestoreView(_r1).$implicit; const ctx_r2 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r2.onCloseClick($event, tab_r2.Id)); });
|
|
35
|
+
i0.ɵɵelement(7, "i", 10);
|
|
36
|
+
i0.ɵɵelementEnd()();
|
|
37
|
+
} if (rf & 2) {
|
|
38
|
+
const tab_r2 = ctx.$implicit;
|
|
39
|
+
const ctx_r2 = i0.ɵɵnextContext();
|
|
40
|
+
i0.ɵɵclassProp("mj-tabs__tab--active", tab_r2.Id === ctx_r2.ActiveId)("mj-tabs__tab--rejected", tab_r2.Status === "rejected")("mj-tabs__tab--complete", tab_r2.Status === "complete");
|
|
41
|
+
i0.ɵɵproperty("cdkDragDisabled", !ctx_r2.AllowReorder)("cdkDragStartDelay", ctx_r2.DragStartDelay);
|
|
42
|
+
i0.ɵɵattribute("aria-selected", tab_r2.Id === ctx_r2.ActiveId)("aria-label", ctx_r2.tabAccessibleName(tab_r2));
|
|
43
|
+
i0.ɵɵadvance();
|
|
44
|
+
i0.ɵɵconditional(tab_r2.Icon ? 1 : -1);
|
|
45
|
+
i0.ɵɵadvance();
|
|
46
|
+
i0.ɵɵconditional(tab_r2.Status === "rejected" ? 2 : -1);
|
|
47
|
+
i0.ɵɵadvance();
|
|
48
|
+
i0.ɵɵproperty("mjTip", tab_r2.Label);
|
|
49
|
+
i0.ɵɵadvance();
|
|
50
|
+
i0.ɵɵtextInterpolate(tab_r2.Label);
|
|
51
|
+
i0.ɵɵadvance();
|
|
52
|
+
i0.ɵɵconditional(tab_r2.Dirty ? 5 : -1);
|
|
53
|
+
i0.ɵɵadvance();
|
|
54
|
+
i0.ɵɵattribute("aria-label", "Close " + tab_r2.Label);
|
|
55
|
+
} }
|
|
56
|
+
function MJWorkspaceTabStripComponent_Conditional_4_Template(rf, ctx) { if (rf & 1) {
|
|
57
|
+
const _r4 = i0.ɵɵgetCurrentView();
|
|
58
|
+
i0.ɵɵelementStart(0, "button", 12);
|
|
59
|
+
i0.ɵɵlistener("click", function MJWorkspaceTabStripComponent_Conditional_4_Template_button_click_0_listener() { i0.ɵɵrestoreView(_r4); const ctx_r2 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r2.NewTabRequested.emit()); });
|
|
60
|
+
i0.ɵɵelement(1, "i", 13);
|
|
61
|
+
i0.ɵɵelementStart(2, "span");
|
|
62
|
+
i0.ɵɵtext(3);
|
|
63
|
+
i0.ɵɵelementEnd()();
|
|
64
|
+
} if (rf & 2) {
|
|
65
|
+
const ctx_r2 = i0.ɵɵnextContext();
|
|
66
|
+
i0.ɵɵattribute("aria-label", ctx_r2.NewTabLabel);
|
|
67
|
+
i0.ɵɵadvance(3);
|
|
68
|
+
i0.ɵɵtextInterpolate(ctx_r2.NewTabLabel);
|
|
69
|
+
} }
|
|
70
|
+
/**
|
|
71
|
+
* `<mj-workspace-tab-strip>` — browser-style draft tabs, presentation over `MJWorkspaceTabStore`.
|
|
72
|
+
*
|
|
73
|
+
* Dumb by design: it renders the tabs it is handed and emits intent. The store holds the state
|
|
74
|
+
* machine; the host owns what a tab's payload means. That split keeps it app-agnostic.
|
|
75
|
+
*
|
|
76
|
+
* **Browser-tab behavior lives HERE** so every consumer inherits it:
|
|
77
|
+
* - pinned new-tab button; the tab LIST scrolls horizontally *behind* it on overflow;
|
|
78
|
+
* - tabs size to their text up to `--mj-tab-max`, then ellipsize; the inline unsaved-dot pushes the label;
|
|
79
|
+
* - the tab body keeps the default arrow cursor (like browser tabs);
|
|
80
|
+
* - the full label shows via the reusable `mjTip` tooltip when a tab is truncated (delayed, non-interactive);
|
|
81
|
+
* - drag-reorder via CDK, emitted as intent for the host to apply to its store.
|
|
82
|
+
*
|
|
83
|
+
* **Appearance and keyboard behaviour are NOT owned here.** The look comes from the global
|
|
84
|
+
* `.mj-tabs*` chrome in `tabs.scss`, and the ARIA tabs keyboard contract from `mjTabList` — both
|
|
85
|
+
* shared with `mj-tabstrip`, so the two strips cannot drift apart visually or behaviourally even
|
|
86
|
+
* though they keep different state models.
|
|
87
|
+
*/
|
|
88
|
+
export class MJWorkspaceTabStripComponent {
|
|
89
|
+
elementRef = inject(ElementRef);
|
|
90
|
+
ngAfterViewInit() {
|
|
91
|
+
warnIfTabChromeMissing(this.elementRef.nativeElement);
|
|
92
|
+
}
|
|
93
|
+
Tabs = [];
|
|
94
|
+
ActiveId = null;
|
|
95
|
+
ShowNewTab = true;
|
|
96
|
+
NewTabLabel = 'New';
|
|
97
|
+
/**
|
|
98
|
+
* Whether tabs can be drag-reordered at all. The escape hatch for hosts where even long-press
|
|
99
|
+
* dragging is wrong (a kiosk surface, a read-only review screen) — reorder simply isn't offered
|
|
100
|
+
* and every touch gesture belongs to scrolling.
|
|
101
|
+
*/
|
|
102
|
+
AllowReorder = true;
|
|
103
|
+
/**
|
|
104
|
+
* Per-pointer-type drag threshold, resolving the touch conflict between REORDER and SCROLL: an
|
|
105
|
+
* overflowing tab list scrolls by horizontal swipe, and a swipe starts on a tab — with an
|
|
106
|
+
* immediate drag threshold, CDK claims that touch and the list becomes unscrollable on phones.
|
|
107
|
+
* A hold-to-drag delay is the platform idiom for exactly this (iOS home screen, mobile browser
|
|
108
|
+
* tabs): a moving finger within the delay window is a SCROLL and CDK lets it through natively; a
|
|
109
|
+
* still finger past it is a DRAG. Mouse stays immediate — pointer users scroll with the wheel or
|
|
110
|
+
* scrollbar, so there is no gesture to disambiguate and a delay would just feel laggy.
|
|
111
|
+
*/
|
|
112
|
+
DragStartDelay = { touch: 400, mouse: 0 };
|
|
113
|
+
TabSelected = new EventEmitter();
|
|
114
|
+
TabClosed = new EventEmitter();
|
|
115
|
+
NewTabRequested = new EventEmitter();
|
|
116
|
+
TabReordered = new EventEmitter();
|
|
117
|
+
/** CDK drop — emit the reorder intent; the host applies it to the store (the strip stays dumb). */
|
|
118
|
+
onDrop(event) {
|
|
119
|
+
if (event.previousIndex !== event.currentIndex) {
|
|
120
|
+
this.TabReordered.emit({ previousIndex: event.previousIndex, currentIndex: event.currentIndex });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* The tab's accessible name. The dirty dot and the rejected colour/icon are the only signals for
|
|
125
|
+
* those states, and neither survives being read aloud — so they are spelled out here instead.
|
|
126
|
+
*/
|
|
127
|
+
tabAccessibleName(tab) {
|
|
128
|
+
const parts = [tab.Label];
|
|
129
|
+
if (tab.Status === 'rejected')
|
|
130
|
+
parts.push('(rejected)');
|
|
131
|
+
if (tab.Dirty)
|
|
132
|
+
parts.push('(unsaved changes)');
|
|
133
|
+
return parts.join(' ');
|
|
134
|
+
}
|
|
135
|
+
/** Keyboard arrow/Home/End/Enter from `mjTabList` — map the position back onto our id. */
|
|
136
|
+
onActivateRequested(request) {
|
|
137
|
+
const tab = this.Tabs[request.Index];
|
|
138
|
+
if (tab) {
|
|
139
|
+
this.TabSelected.emit(tab.Id);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/** Delete/Backspace on a focused tab — the APG close gesture for a closeable tab. */
|
|
143
|
+
onCloseRequested(request) {
|
|
144
|
+
const tab = this.Tabs[request.Index];
|
|
145
|
+
if (tab) {
|
|
146
|
+
this.TabClosed.emit(tab.Id);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/** The close button sits inside the tab, so its click would otherwise also select the tab. */
|
|
150
|
+
onCloseClick(event, id) {
|
|
151
|
+
event.stopPropagation();
|
|
152
|
+
this.TabClosed.emit(id);
|
|
153
|
+
}
|
|
154
|
+
static ɵfac = function MJWorkspaceTabStripComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || MJWorkspaceTabStripComponent)(); };
|
|
155
|
+
static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: MJWorkspaceTabStripComponent, selectors: [["mj-workspace-tab-strip"]], inputs: { Tabs: "Tabs", ActiveId: "ActiveId", ShowNewTab: "ShowNewTab", NewTabLabel: "NewTabLabel", AllowReorder: "AllowReorder" }, outputs: { TabSelected: "TabSelected", TabClosed: "TabClosed", NewTabRequested: "NewTabRequested", TabReordered: "TabReordered" }, decls: 5, vars: 1, consts: [[1, "mj-tabs", "mj-tabs__bar"], ["mjTabList", "", "cdkDropList", "", "cdkDropListOrientation", "horizontal", 1, "mj-tabs__list", 3, "TabActivateRequested", "TabCloseRequested", "cdkDropListDropped"], ["cdkDrag", "", "role", "tab", 1, "mj-tabs__tab", 3, "cdkDragDisabled", "cdkDragStartDelay", "mj-tabs__tab--active", "mj-tabs__tab--rejected", "mj-tabs__tab--complete"], ["type", "button", 1, "mj-tabs__new"], ["cdkDrag", "", "role", "tab", 1, "mj-tabs__tab", 3, "click", "cdkDragDisabled", "cdkDragStartDelay"], ["aria-hidden", "true", 1, "mj-tabs__icon", 3, "class"], ["aria-hidden", "true", 1, "fa-solid", "fa-triangle-exclamation", "mj-tabs__status-icon"], [1, "mj-tabs__label", 3, "mjTip"], ["aria-hidden", "true", 1, "mj-tabs__dirty"], ["type", "button", "tabindex", "-1", 1, "mj-tabs__close", 3, "click"], ["aria-hidden", "true", 1, "fa-solid", "fa-xmark"], ["aria-hidden", "true", 1, "mj-tabs__icon"], ["type", "button", 1, "mj-tabs__new", 3, "click"], ["aria-hidden", "true", 1, "fa-solid", "fa-plus"]], template: function MJWorkspaceTabStripComponent_Template(rf, ctx) { if (rf & 1) {
|
|
156
|
+
i0.ɵɵelementStart(0, "div", 0)(1, "div", 1);
|
|
157
|
+
i0.ɵɵlistener("TabActivateRequested", function MJWorkspaceTabStripComponent_Template_div_TabActivateRequested_1_listener($event) { return ctx.onActivateRequested($event); })("TabCloseRequested", function MJWorkspaceTabStripComponent_Template_div_TabCloseRequested_1_listener($event) { return ctx.onCloseRequested($event); })("cdkDropListDropped", function MJWorkspaceTabStripComponent_Template_div_cdkDropListDropped_1_listener($event) { return ctx.onDrop($event); });
|
|
158
|
+
i0.ɵɵrepeaterCreate(2, MJWorkspaceTabStripComponent_For_3_Template, 8, 16, "div", 2, _forTrack0);
|
|
159
|
+
i0.ɵɵelementEnd();
|
|
160
|
+
i0.ɵɵconditionalCreate(4, MJWorkspaceTabStripComponent_Conditional_4_Template, 4, 2, "button", 3);
|
|
161
|
+
i0.ɵɵelementEnd();
|
|
162
|
+
} if (rf & 2) {
|
|
163
|
+
i0.ɵɵadvance(2);
|
|
164
|
+
i0.ɵɵrepeater(ctx.Tabs);
|
|
165
|
+
i0.ɵɵadvance(2);
|
|
166
|
+
i0.ɵɵconditional(ctx.ShowNewTab ? 4 : -1);
|
|
167
|
+
} }, dependencies: [CommonModule, CdkDropList, CdkDrag, MJWorkspaceTipDirective, MJTabListDirective], styles: ["\n\n\n\n\n\n[_nghost-%COMP%] {\n display: block;\n}\n\n\n\n\n\n\n.mj-tabs__tab.cdk-drag[_ngcontent-%COMP%] {\n cursor: default;\n}\n\n\n\n\n.cdk-drag-preview.mj-tabs__tab[_ngcontent-%COMP%] {\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.22);\n border-radius: 8px 8px 0 0;\n}\n\n\n\n.mj-tabs__tab.cdk-drag-placeholder[_ngcontent-%COMP%] {\n opacity: 0.35;\n}\n\n\n\n.cdk-drag-animating[_ngcontent-%COMP%], \n.mj-tabs__list.cdk-drop-list-dragging[_ngcontent-%COMP%] .mj-tabs__tab[_ngcontent-%COMP%]:not(.cdk-drag-placeholder) {\n transition: transform 180ms cubic-bezier(0, 0, 0.2, 1);\n}"], changeDetection: 0 });
|
|
168
|
+
}
|
|
169
|
+
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(MJWorkspaceTabStripComponent, [{
|
|
170
|
+
type: Component,
|
|
171
|
+
args: [{ standalone: true, selector: 'mj-workspace-tab-strip', imports: [CommonModule, CdkDropList, CdkDrag, MJWorkspaceTipDirective, MJTabListDirective], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
172
|
+
<div class="mj-tabs mj-tabs__bar">
|
|
173
|
+
<div
|
|
174
|
+
class="mj-tabs__list"
|
|
175
|
+
mjTabList
|
|
176
|
+
(TabActivateRequested)="onActivateRequested($event)"
|
|
177
|
+
(TabCloseRequested)="onCloseRequested($event)"
|
|
178
|
+
cdkDropList
|
|
179
|
+
cdkDropListOrientation="horizontal"
|
|
180
|
+
(cdkDropListDropped)="onDrop($event)">
|
|
181
|
+
@for (tab of Tabs; track tab.Id) {
|
|
182
|
+
<!-- role="tab" sits on the FOCUSABLE element, not on a wrapper around one. tabindex is
|
|
183
|
+
owned by mjTabList (roving), so it is deliberately not bound here. The accessible
|
|
184
|
+
name folds in the unsaved/rejected state, because those are conveyed visually by a
|
|
185
|
+
dot and a colour that a screen reader cannot see. -->
|
|
186
|
+
<div
|
|
187
|
+
class="mj-tabs__tab"
|
|
188
|
+
cdkDrag
|
|
189
|
+
[cdkDragDisabled]="!AllowReorder"
|
|
190
|
+
[cdkDragStartDelay]="DragStartDelay"
|
|
191
|
+
[class.mj-tabs__tab--active]="tab.Id === ActiveId"
|
|
192
|
+
[class.mj-tabs__tab--rejected]="tab.Status === 'rejected'"
|
|
193
|
+
[class.mj-tabs__tab--complete]="tab.Status === 'complete'"
|
|
194
|
+
role="tab"
|
|
195
|
+
[attr.aria-selected]="tab.Id === ActiveId"
|
|
196
|
+
[attr.aria-label]="tabAccessibleName(tab)"
|
|
197
|
+
(click)="TabSelected.emit(tab.Id)">
|
|
198
|
+
@if (tab.Icon) {
|
|
199
|
+
<i [class]="tab.Icon" class="mj-tabs__icon" aria-hidden="true"></i>
|
|
200
|
+
}
|
|
201
|
+
@if (tab.Status === 'rejected') {
|
|
202
|
+
<i class="fa-solid fa-triangle-exclamation mj-tabs__status-icon" aria-hidden="true"></i>
|
|
203
|
+
}
|
|
204
|
+
<span class="mj-tabs__label" [mjTip]="tab.Label">{{ tab.Label }}</span>
|
|
205
|
+
@if (tab.Dirty) {
|
|
206
|
+
<!-- Inline dot (no reserved slot): it PUSHES the label, so the ellipsis shifts left on
|
|
207
|
+
a capped tab and a short tab grows by the dot's width. Silent to assistive tech —
|
|
208
|
+
the state is already in the tab's accessible name. -->
|
|
209
|
+
<span class="mj-tabs__dirty" aria-hidden="true">•</span>
|
|
210
|
+
}
|
|
211
|
+
<!-- tabindex="-1": the TAB is the focus stop and Delete closes it, so a second stop per
|
|
212
|
+
tab would double the length of the strip in the page tab order. -->
|
|
213
|
+
<button
|
|
214
|
+
type="button"
|
|
215
|
+
class="mj-tabs__close"
|
|
216
|
+
tabindex="-1"
|
|
217
|
+
[attr.aria-label]="'Close ' + tab.Label"
|
|
218
|
+
(click)="onCloseClick($event, tab.Id)">
|
|
219
|
+
<i class="fa-solid fa-xmark" aria-hidden="true"></i>
|
|
220
|
+
</button>
|
|
221
|
+
</div>
|
|
222
|
+
}
|
|
223
|
+
</div>
|
|
224
|
+
@if (ShowNewTab) {
|
|
225
|
+
<button type="button" class="mj-tabs__new" (click)="NewTabRequested.emit()" [attr.aria-label]="NewTabLabel">
|
|
226
|
+
<i class="fa-solid fa-plus" aria-hidden="true"></i>
|
|
227
|
+
<span>{{ NewTabLabel }}</span>
|
|
228
|
+
</button>
|
|
229
|
+
}
|
|
230
|
+
</div>
|
|
231
|
+
`, styles: ["/* Only what is LOCAL to this strip. The tab chrome \u2014 bar, tabs, active accent, label ellipsis,\n dirty dot, close button, pinned new-tab control \u2014 lives in the global `.mj-tabs*` stylesheet\n (ui-components `lib/tabs/tabs.scss`) so this strip and `mj-tabstrip` cannot drift apart.\n Anything added here is, by definition, a claim that this strip should look DIFFERENT. */\n\n:host {\n display: block;\n}\n\n/* \u2500\u2500\u2500 Drag-reorder (CDK) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n Specific to this strip: `mj-tabstrip` has no reorder capability, so these affordances would be\n dead weight in the shared sheet. Default arrow cursor over tabs \u2014 not a grab hand \u2014 matching\n browser tab convention. */\n.mj-tabs__tab.cdk-drag {\n cursor: default;\n}\n\n/* The floating clone while dragging \u2014 still reads as a tab, plus a lift shadow. Neutral rgba so it\n reads as a shadow in both themes; a token-tinted shadow glows in dark. */\n.cdk-drag-preview.mj-tabs__tab {\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.22);\n border-radius: 8px 8px 0 0;\n}\n\n/* The gap the tab leaves behind \u2014 a faded outline, not a solid block. */\n.mj-tabs__tab.cdk-drag-placeholder {\n opacity: 0.35;\n}\n\n/* Smooth settle on drop, and neighbours sliding aside during a drag. */\n.cdk-drag-animating,\n.mj-tabs__list.cdk-drop-list-dragging .mj-tabs__tab:not(.cdk-drag-placeholder) {\n transition: transform 180ms cubic-bezier(0, 0, 0.2, 1);\n}\n"] }]
|
|
232
|
+
}], null, { Tabs: [{
|
|
233
|
+
type: Input
|
|
234
|
+
}], ActiveId: [{
|
|
235
|
+
type: Input
|
|
236
|
+
}], ShowNewTab: [{
|
|
237
|
+
type: Input
|
|
238
|
+
}], NewTabLabel: [{
|
|
239
|
+
type: Input
|
|
240
|
+
}], AllowReorder: [{
|
|
241
|
+
type: Input
|
|
242
|
+
}], TabSelected: [{
|
|
243
|
+
type: Output
|
|
244
|
+
}], TabClosed: [{
|
|
245
|
+
type: Output
|
|
246
|
+
}], NewTabRequested: [{
|
|
247
|
+
type: Output
|
|
248
|
+
}], TabReordered: [{
|
|
249
|
+
type: Output
|
|
250
|
+
}] }); })();
|
|
251
|
+
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(MJWorkspaceTabStripComponent, { className: "MJWorkspaceTabStripComponent", filePath: "lib/workspace-tabs/workspace-tab-strip.component.ts", lineNumber: 101 }); })();
|
|
252
|
+
//# sourceMappingURL=workspace-tab-strip.component.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace-tab-strip.component.js","sourceRoot":"","sources":["../../../src/lib/workspace-tabs/workspace-tab-strip.component.ts"],"names":[],"mappings":"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","sourcesContent":["import { AfterViewInit, Component, ChangeDetectionStrategy, ElementRef, EventEmitter, Input, Output, inject } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { CdkDropList, CdkDrag, CdkDragDrop } from '@angular/cdk/drag-drop';\nimport { MJWorkspaceTab } from './workspace-tabs.types';\nimport { MJWorkspaceTipDirective } from './workspace-tip.directive';\nimport { MJTabListDirective, MJTabListRequest } from '../tabs/tab-list.directive';\nimport { warnIfTabChromeMissing } from '../tabs/tab-chrome-guard';\n\n/** A drag-reorder request from the strip: move the tab at `previousIndex` to `currentIndex`. */\nexport interface MJTabReorder {\n previousIndex: number;\n currentIndex: number;\n}\n\n/**\n * `<mj-workspace-tab-strip>` — browser-style draft tabs, presentation over `MJWorkspaceTabStore`.\n *\n * Dumb by design: it renders the tabs it is handed and emits intent. The store holds the state\n * machine; the host owns what a tab's payload means. That split keeps it app-agnostic.\n *\n * **Browser-tab behavior lives HERE** so every consumer inherits it:\n * - pinned new-tab button; the tab LIST scrolls horizontally *behind* it on overflow;\n * - tabs size to their text up to `--mj-tab-max`, then ellipsize; the inline unsaved-dot pushes the label;\n * - the tab body keeps the default arrow cursor (like browser tabs);\n * - the full label shows via the reusable `mjTip` tooltip when a tab is truncated (delayed, non-interactive);\n * - drag-reorder via CDK, emitted as intent for the host to apply to its store.\n *\n * **Appearance and keyboard behaviour are NOT owned here.** The look comes from the global\n * `.mj-tabs*` chrome in `tabs.scss`, and the ARIA tabs keyboard contract from `mjTabList` — both\n * shared with `mj-tabstrip`, so the two strips cannot drift apart visually or behaviourally even\n * though they keep different state models.\n */\n@Component({\n standalone: true,\n selector: 'mj-workspace-tab-strip',\n imports: [CommonModule, CdkDropList, CdkDrag, MJWorkspaceTipDirective, MJTabListDirective],\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `\n <div class=\"mj-tabs mj-tabs__bar\">\n <div\n class=\"mj-tabs__list\"\n mjTabList\n (TabActivateRequested)=\"onActivateRequested($event)\"\n (TabCloseRequested)=\"onCloseRequested($event)\"\n cdkDropList\n cdkDropListOrientation=\"horizontal\"\n (cdkDropListDropped)=\"onDrop($event)\">\n @for (tab of Tabs; track tab.Id) {\n <!-- role=\"tab\" sits on the FOCUSABLE element, not on a wrapper around one. tabindex is\n owned by mjTabList (roving), so it is deliberately not bound here. The accessible\n name folds in the unsaved/rejected state, because those are conveyed visually by a\n dot and a colour that a screen reader cannot see. -->\n <div\n class=\"mj-tabs__tab\"\n cdkDrag\n [cdkDragDisabled]=\"!AllowReorder\"\n [cdkDragStartDelay]=\"DragStartDelay\"\n [class.mj-tabs__tab--active]=\"tab.Id === ActiveId\"\n [class.mj-tabs__tab--rejected]=\"tab.Status === 'rejected'\"\n [class.mj-tabs__tab--complete]=\"tab.Status === 'complete'\"\n role=\"tab\"\n [attr.aria-selected]=\"tab.Id === ActiveId\"\n [attr.aria-label]=\"tabAccessibleName(tab)\"\n (click)=\"TabSelected.emit(tab.Id)\">\n @if (tab.Icon) {\n <i [class]=\"tab.Icon\" class=\"mj-tabs__icon\" aria-hidden=\"true\"></i>\n }\n @if (tab.Status === 'rejected') {\n <i class=\"fa-solid fa-triangle-exclamation mj-tabs__status-icon\" aria-hidden=\"true\"></i>\n }\n <span class=\"mj-tabs__label\" [mjTip]=\"tab.Label\">{{ tab.Label }}</span>\n @if (tab.Dirty) {\n <!-- Inline dot (no reserved slot): it PUSHES the label, so the ellipsis shifts left on\n a capped tab and a short tab grows by the dot's width. Silent to assistive tech —\n the state is already in the tab's accessible name. -->\n <span class=\"mj-tabs__dirty\" aria-hidden=\"true\">•</span>\n }\n <!-- tabindex=\"-1\": the TAB is the focus stop and Delete closes it, so a second stop per\n tab would double the length of the strip in the page tab order. -->\n <button\n type=\"button\"\n class=\"mj-tabs__close\"\n tabindex=\"-1\"\n [attr.aria-label]=\"'Close ' + tab.Label\"\n (click)=\"onCloseClick($event, tab.Id)\">\n <i class=\"fa-solid fa-xmark\" aria-hidden=\"true\"></i>\n </button>\n </div>\n }\n </div>\n @if (ShowNewTab) {\n <button type=\"button\" class=\"mj-tabs__new\" (click)=\"NewTabRequested.emit()\" [attr.aria-label]=\"NewTabLabel\">\n <i class=\"fa-solid fa-plus\" aria-hidden=\"true\"></i>\n <span>{{ NewTabLabel }}</span>\n </button>\n }\n </div>\n `,\n styleUrls: ['./workspace-tab-strip.component.css'],\n})\nexport class MJWorkspaceTabStripComponent implements AfterViewInit {\n private readonly elementRef = inject(ElementRef);\n\n ngAfterViewInit(): void {\n warnIfTabChromeMissing(this.elementRef.nativeElement);\n }\n\n @Input() Tabs: MJWorkspaceTab[] = [];\n @Input() ActiveId: string | null = null;\n @Input() ShowNewTab = true;\n @Input() NewTabLabel = 'New';\n\n /**\n * Whether tabs can be drag-reordered at all. The escape hatch for hosts where even long-press\n * dragging is wrong (a kiosk surface, a read-only review screen) — reorder simply isn't offered\n * and every touch gesture belongs to scrolling.\n */\n @Input() AllowReorder = true;\n\n /**\n * Per-pointer-type drag threshold, resolving the touch conflict between REORDER and SCROLL: an\n * overflowing tab list scrolls by horizontal swipe, and a swipe starts on a tab — with an\n * immediate drag threshold, CDK claims that touch and the list becomes unscrollable on phones.\n * A hold-to-drag delay is the platform idiom for exactly this (iOS home screen, mobile browser\n * tabs): a moving finger within the delay window is a SCROLL and CDK lets it through natively; a\n * still finger past it is a DRAG. Mouse stays immediate — pointer users scroll with the wheel or\n * scrollbar, so there is no gesture to disambiguate and a delay would just feel laggy.\n */\n public readonly DragStartDelay = { touch: 400, mouse: 0 };\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\n /** CDK drop — emit the reorder intent; the host applies it to the store (the strip stays dumb). */\n public onDrop(event: CdkDragDrop<MJWorkspaceTab[]>): void {\n if (event.previousIndex !== event.currentIndex) {\n this.TabReordered.emit({ previousIndex: event.previousIndex, currentIndex: event.currentIndex });\n }\n }\n\n /**\n * The tab's accessible name. The dirty dot and the rejected colour/icon are the only signals for\n * those states, and neither survives being read aloud — so they are spelled out here instead.\n */\n public tabAccessibleName(tab: MJWorkspaceTab): string {\n const parts = [tab.Label];\n if (tab.Status === 'rejected') parts.push('(rejected)');\n if (tab.Dirty) parts.push('(unsaved changes)');\n return parts.join(' ');\n }\n\n /** Keyboard arrow/Home/End/Enter from `mjTabList` — map the position back onto our id. */\n public onActivateRequested(request: MJTabListRequest): void {\n const tab = this.Tabs[request.Index];\n if (tab) {\n this.TabSelected.emit(tab.Id);\n }\n }\n\n /** Delete/Backspace on a focused tab — the APG close gesture for a closeable tab. */\n public onCloseRequested(request: MJTabListRequest): void {\n const tab = this.Tabs[request.Index];\n if (tab) {\n this.TabClosed.emit(tab.Id);\n }\n }\n\n /** The close button sits inside the tab, so its click would otherwise also select the tab. */\n public onCloseClick(event: MouseEvent, id: string): void {\n event.stopPropagation();\n this.TabClosed.emit(id);\n }\n}\n"]}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace-tab framework contract.
|
|
3
|
+
*
|
|
4
|
+
* A "workspace" is a screen where the user builds several drafts in parallel — each draft lives in
|
|
5
|
+
* a tab (browser-tab semantics: open, switch, drag-reorder, close, dirty-dot). Originated in the
|
|
6
|
+
* BizApps accounting JE/batch workspaces and hoisted here as the canonical implementation.
|
|
7
|
+
*
|
|
8
|
+
* v1 is **session-scoped**: a tab's state lives until the tab is closed or the session ends, and is
|
|
9
|
+
* deliberately **NOT DB-persisted** (DB persistence is the v2 fork). App types never appear here; a
|
|
10
|
+
* tab carries an opaque `State` the host owns.
|
|
11
|
+
*/
|
|
12
|
+
/** Lifecycle state of a draft tab. */
|
|
13
|
+
export type MJWorkspaceTabState =
|
|
14
|
+
/** Normal in-progress draft. */
|
|
15
|
+
'draft'
|
|
16
|
+
/** Round-tripped after a rejection — the tab returns for rework carrying its rejection context. */
|
|
17
|
+
| 'rejected'
|
|
18
|
+
/** Committed/built — kept open read-only until the user closes it. */
|
|
19
|
+
| 'complete';
|
|
20
|
+
/**
|
|
21
|
+
* One workspace tab. `State` is intentionally `unknown`: the framework moves tabs around and never
|
|
22
|
+
* inspects their payload, which is what keeps it app-agnostic. Hosts narrow it themselves — this is
|
|
23
|
+
* the layer boundary, not weak typing.
|
|
24
|
+
*/
|
|
25
|
+
export interface MJWorkspaceTab<TState = unknown> {
|
|
26
|
+
/** Stable identity within the strip. */
|
|
27
|
+
Id: string;
|
|
28
|
+
/** Tab caption. */
|
|
29
|
+
Label: string;
|
|
30
|
+
/** Optional Font Awesome class rendered before the label. */
|
|
31
|
+
Icon?: string;
|
|
32
|
+
Status: MJWorkspaceTabState;
|
|
33
|
+
/** Host-owned payload — the in-progress draft. */
|
|
34
|
+
State: TState;
|
|
35
|
+
/** Set when Status is 'rejected'; surfaced as the tab tooltip so the reason travels with the tab. */
|
|
36
|
+
RejectionReason?: string | null;
|
|
37
|
+
/** True when the tab has unsaved edits — drives the discard confirm. */
|
|
38
|
+
Dirty?: boolean;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=workspace-tabs.types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace-tabs.types.d.ts","sourceRoot":"","sources":["../../../src/lib/workspace-tabs/workspace-tabs.types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,sCAAsC;AACtC,MAAM,MAAM,mBAAmB;AAC7B,gCAAgC;AAC9B,OAAO;AACT,mGAAmG;GACjG,UAAU;AACZ,sEAAsE;GACpE,UAAU,CAAC;AAEf;;;;GAIG;AACH,MAAM,WAAW,cAAc,CAAC,MAAM,GAAG,OAAO;IAC9C,wCAAwC;IACxC,EAAE,EAAE,MAAM,CAAC;IACX,mBAAmB;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,6DAA6D;IAC7D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,mBAAmB,CAAC;IAC5B,kDAAkD;IAClD,KAAK,EAAE,MAAM,CAAC;IACd,qGAAqG;IACrG,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,wEAAwE;IACxE,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace-tab framework contract.
|
|
3
|
+
*
|
|
4
|
+
* A "workspace" is a screen where the user builds several drafts in parallel — each draft lives in
|
|
5
|
+
* a tab (browser-tab semantics: open, switch, drag-reorder, close, dirty-dot). Originated in the
|
|
6
|
+
* BizApps accounting JE/batch workspaces and hoisted here as the canonical implementation.
|
|
7
|
+
*
|
|
8
|
+
* v1 is **session-scoped**: a tab's state lives until the tab is closed or the session ends, and is
|
|
9
|
+
* deliberately **NOT DB-persisted** (DB persistence is the v2 fork). App types never appear here; a
|
|
10
|
+
* tab carries an opaque `State` the host owns.
|
|
11
|
+
*/
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=workspace-tabs.types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace-tabs.types.js","sourceRoot":"","sources":["../../../src/lib/workspace-tabs/workspace-tabs.types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG","sourcesContent":["/**\n * Workspace-tab framework contract.\n *\n * A \"workspace\" is a screen where the user builds several drafts in parallel — each draft lives in\n * a tab (browser-tab semantics: open, switch, drag-reorder, close, dirty-dot). Originated in the\n * BizApps accounting JE/batch workspaces and hoisted here as the canonical implementation.\n *\n * v1 is **session-scoped**: a tab's state lives until the tab is closed or the session ends, and is\n * deliberately **NOT DB-persisted** (DB persistence is the v2 fork). App types never appear here; a\n * tab carries an opaque `State` the host owns.\n */\n\n/** Lifecycle state of a draft tab. */\nexport type MJWorkspaceTabState =\n /** Normal in-progress draft. */\n | 'draft'\n /** Round-tripped after a rejection — the tab returns for rework carrying its rejection context. */\n | 'rejected'\n /** Committed/built — kept open read-only until the user closes it. */\n | 'complete';\n\n/**\n * One workspace tab. `State` is intentionally `unknown`: the framework moves tabs around and never\n * inspects their payload, which is what keeps it app-agnostic. Hosts narrow it themselves — this is\n * the layer boundary, not weak typing.\n */\nexport interface MJWorkspaceTab<TState = unknown> {\n /** Stable identity within the strip. */\n Id: string;\n /** Tab caption. */\n Label: string;\n /** Optional Font Awesome class rendered before the label. */\n Icon?: string;\n Status: MJWorkspaceTabState;\n /** Host-owned payload — the in-progress draft. */\n State: TState;\n /** Set when Status is 'rejected'; surfaced as the tab tooltip so the reason travels with the tab. */\n RejectionReason?: string | null;\n /** True when the tab has unsaved edits — drives the discard confirm. */\n Dirty?: boolean;\n}\n"]}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { OnDestroy } from '@angular/core';
|
|
2
|
+
import * as i0 from "@angular/core";
|
|
3
|
+
/**
|
|
4
|
+
* `mjTip` — a small, reusable, THEMED hover tooltip for dense surfaces (tab labels, truncated
|
|
5
|
+
* selects, …). Behavior:
|
|
6
|
+
* - shows the full text only after the pointer holds STILL over the host (~450ms);
|
|
7
|
+
* - NON-INTERACTIVE (`pointer-events:none`) so it never blocks a click, and leaves on the next mousemove;
|
|
8
|
+
* - by default shows ONLY when the host is truncated (`scrollWidth > clientWidth`) — pass `mjTipAlways`
|
|
9
|
+
* for hosts whose truncation can't be measured (a native `<select>`), so the full value shows regardless;
|
|
10
|
+
* - themed via design tokens (inline `var()` styles, since it is appended to `<body>`), with a NEUTRAL
|
|
11
|
+
* `rgba(0,0,0,…)` shadow so it reads as a shadow in BOTH light and dark mode (a token-tinted shadow
|
|
12
|
+
* glows in dark). Capped at ~1.5 tab-widths × 3 lines — never a screen-wide banner.
|
|
13
|
+
*/
|
|
14
|
+
export declare class MJWorkspaceTipDirective implements OnDestroy {
|
|
15
|
+
private host;
|
|
16
|
+
private r;
|
|
17
|
+
Text: string;
|
|
18
|
+
/** Show even when the host isn't measurably truncated (e.g. a native <select>). */
|
|
19
|
+
Always: boolean;
|
|
20
|
+
private timer;
|
|
21
|
+
private tip;
|
|
22
|
+
onEnter(): void;
|
|
23
|
+
onMove(): void;
|
|
24
|
+
onLeave(): void;
|
|
25
|
+
ngOnDestroy(): void;
|
|
26
|
+
private arm;
|
|
27
|
+
private clear;
|
|
28
|
+
private show;
|
|
29
|
+
private styleTip;
|
|
30
|
+
private hide;
|
|
31
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<MJWorkspaceTipDirective, never>;
|
|
32
|
+
static ɵdir: i0.ɵɵDirectiveDeclaration<MJWorkspaceTipDirective, "[mjTip]", never, { "Text": { "alias": "mjTip"; "required": false; }; "Always": { "alias": "mjTipAlways"; "required": false; }; }, {}, never, never, true, never>;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=workspace-tip.directive.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace-tip.directive.d.ts","sourceRoot":"","sources":["../../../src/lib/workspace-tabs/workspace-tip.directive.ts"],"names":[],"mappings":"AAAA,OAAO,EAA8C,SAAS,EAAqB,MAAM,eAAe,CAAC;;AAKzG;;;;;;;;;;GAUG;AACH,qBACa,uBAAwB,YAAW,SAAS;IACvD,OAAO,CAAC,IAAI,CAAmD;IAC/D,OAAO,CAAC,CAAC,CAAqB;IAEd,IAAI,SAAM;IAC1B,mFAAmF;IAC7D,MAAM,UAAS;IAErC,OAAO,CAAC,KAAK,CAA8C;IAC3D,OAAO,CAAC,GAAG,CAA4B;IAEX,OAAO,IAAI,IAAI;IAGhB,MAAM,IAAI,IAAI;IAIb,OAAO,IAAI,IAAI;IAI3C,WAAW,IAAI,IAAI;IAKnB,OAAO,CAAC,GAAG;IAIX,OAAO,CAAC,KAAK;IAOb,OAAO,CAAC,IAAI;IAqBZ,OAAO,CAAC,QAAQ;IAuBhB,OAAO,CAAC,IAAI;yCAlFD,uBAAuB;2CAAvB,uBAAuB;CAwFnC"}
|