@memberjunction/ng-whiteboard 5.43.0 → 5.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/whiteboard-agent-sees-popover.component.dom.test.d.ts +2 -0
- package/dist/lib/whiteboard-agent-sees-popover.component.dom.test.d.ts.map +1 -0
- package/dist/lib/whiteboard-agent-sees-popover.component.dom.test.js +54 -0
- package/dist/lib/whiteboard-agent-sees-popover.component.dom.test.js.map +1 -0
- package/dist/lib/whiteboard-pages.component.dom.test.d.ts +2 -0
- package/dist/lib/whiteboard-pages.component.dom.test.d.ts.map +1 -0
- package/dist/lib/whiteboard-pages.component.dom.test.js +101 -0
- package/dist/lib/whiteboard-pages.component.dom.test.js.map +1 -0
- package/dist/lib/whiteboard-snapshot.component.d.ts.map +1 -1
- package/dist/lib/whiteboard-snapshot.component.js +9 -13
- package/dist/lib/whiteboard-snapshot.component.js.map +1 -1
- package/dist/lib/whiteboard-toolbar.component.dom.test.d.ts +2 -0
- package/dist/lib/whiteboard-toolbar.component.dom.test.d.ts.map +1 -0
- package/dist/lib/whiteboard-toolbar.component.dom.test.js +108 -0
- package/dist/lib/whiteboard-toolbar.component.dom.test.js.map +1 -0
- package/dist/lib/whiteboard-zoom.component.dom.test.d.ts +2 -0
- package/dist/lib/whiteboard-zoom.component.dom.test.d.ts.map +1 -0
- package/dist/lib/whiteboard-zoom.component.dom.test.js +58 -0
- package/dist/lib/whiteboard-zoom.component.dom.test.js.map +1 -0
- package/dist/lib/whiteboard-zoom.component.js +2 -2
- package/dist/lib/whiteboard-zoom.component.js.map +1 -1
- package/package.json +6 -4
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"whiteboard-agent-sees-popover.component.dom.test.d.ts","sourceRoot":"","sources":["../../src/lib/whiteboard-agent-sees-popover.component.dom.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { renderComponentFixture } from '@memberjunction/ng-test-utils';
|
|
3
|
+
import { RealtimeWhiteboardAgentSeesPopoverComponent } from './whiteboard-agent-sees-popover.component';
|
|
4
|
+
/**
|
|
5
|
+
* DOM spec for <mj-realtime-whiteboard-agent-sees-popover> — the "what the agent sees"
|
|
6
|
+
* perception popover. Covers the AgentName binding (appears in head + caption), the
|
|
7
|
+
* footer stat bindings (debounce / element count / last delta), and the @if/@else
|
|
8
|
+
* branch on TintedHtml: the empty placeholder when there is no delta, and the tinted
|
|
9
|
+
* <pre> when a delta is present (with the setter producing escaped, span-wrapped HTML).
|
|
10
|
+
*/
|
|
11
|
+
describe('RealtimeWhiteboardAgentSeesPopoverComponent (DOM)', () => {
|
|
12
|
+
it('renders the AgentName in the head and the caption', () => {
|
|
13
|
+
const f = renderComponentFixture(RealtimeWhiteboardAgentSeesPopoverComponent, { inputs: { AgentName: 'Sage' } });
|
|
14
|
+
expect(f.nativeElement.querySelector('.sees-pop__head')?.textContent).toContain("Sage's context");
|
|
15
|
+
expect(f.nativeElement.querySelector('.sees-pop__cap')?.textContent).toContain('Sage');
|
|
16
|
+
});
|
|
17
|
+
it('renders the footer stats from DebounceMs / ElementCount / LastDeltaLabel', () => {
|
|
18
|
+
const f = renderComponentFixture(RealtimeWhiteboardAgentSeesPopoverComponent, {
|
|
19
|
+
inputs: { DebounceMs: 500, ElementCount: 7, LastDeltaLabel: '2 s ago' },
|
|
20
|
+
});
|
|
21
|
+
const foot = f.nativeElement.querySelector('.sees-pop__foot')?.textContent ?? '';
|
|
22
|
+
expect(foot).toContain('500 ms');
|
|
23
|
+
expect(foot).toContain('7');
|
|
24
|
+
expect(foot).toContain('2 s ago');
|
|
25
|
+
});
|
|
26
|
+
it('shows the "no deltas yet" placeholder when DeltaJson is empty', () => {
|
|
27
|
+
const f = renderComponentFixture(RealtimeWhiteboardAgentSeesPopoverComponent);
|
|
28
|
+
const code = f.nativeElement.querySelector('.codeblock')?.textContent ?? '';
|
|
29
|
+
expect(code).toContain('no deltas yet');
|
|
30
|
+
// the tinted-delta branch (which carries the replace-current-state comment) is absent
|
|
31
|
+
expect(code).not.toContain('replace-current-state');
|
|
32
|
+
});
|
|
33
|
+
it('renders the tinted delta block when DeltaJson is set', () => {
|
|
34
|
+
const f = renderComponentFixture(RealtimeWhiteboardAgentSeesPopoverComponent, {
|
|
35
|
+
inputs: { DeltaJson: '{"added": 3}' },
|
|
36
|
+
});
|
|
37
|
+
const pre = f.nativeElement.querySelector('.codeblock');
|
|
38
|
+
expect(pre).not.toBeNull();
|
|
39
|
+
// the @if-true branch carries the replace-current-state semantics comment
|
|
40
|
+
expect(pre.textContent).toContain('replace-current-state');
|
|
41
|
+
// the setter tints JSON keys (.ck) and numbers (.cn) into spans via [innerHTML]
|
|
42
|
+
expect(pre.querySelector('.ck')).not.toBeNull();
|
|
43
|
+
expect(pre.querySelector('.cn')).not.toBeNull();
|
|
44
|
+
});
|
|
45
|
+
it('escapes HTML in the delta JSON (no live markup is injected)', () => {
|
|
46
|
+
const f = renderComponentFixture(RealtimeWhiteboardAgentSeesPopoverComponent, {
|
|
47
|
+
inputs: { DeltaJson: '{"x": "<script>bad</script>"}' },
|
|
48
|
+
});
|
|
49
|
+
// the injected value is escaped, so no real <script> element exists in the popover
|
|
50
|
+
expect(f.nativeElement.querySelector('script')).toBeNull();
|
|
51
|
+
expect(f.nativeElement.querySelector('.codeblock')?.textContent).toContain('<script>');
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
//# sourceMappingURL=whiteboard-agent-sees-popover.component.dom.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"whiteboard-agent-sees-popover.component.dom.test.js","sourceRoot":"","sources":["../../src/lib/whiteboard-agent-sees-popover.component.dom.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, it, expect } from 'vitest';\nimport { renderComponentFixture } from '@memberjunction/ng-test-utils';\nimport { RealtimeWhiteboardAgentSeesPopoverComponent } from './whiteboard-agent-sees-popover.component';\n\n/**\n * DOM spec for <mj-realtime-whiteboard-agent-sees-popover> — the \"what the agent sees\"\n * perception popover. Covers the AgentName binding (appears in head + caption), the\n * footer stat bindings (debounce / element count / last delta), and the @if/@else\n * branch on TintedHtml: the empty placeholder when there is no delta, and the tinted\n * <pre> when a delta is present (with the setter producing escaped, span-wrapped HTML).\n */\ndescribe('RealtimeWhiteboardAgentSeesPopoverComponent (DOM)', () => {\n it('renders the AgentName in the head and the caption', () => {\n const f = renderComponentFixture(RealtimeWhiteboardAgentSeesPopoverComponent, { inputs: { AgentName: 'Sage' } });\n expect(f.nativeElement.querySelector('.sees-pop__head')?.textContent).toContain(\"Sage's context\");\n expect(f.nativeElement.querySelector('.sees-pop__cap')?.textContent).toContain('Sage');\n });\n\n it('renders the footer stats from DebounceMs / ElementCount / LastDeltaLabel', () => {\n const f = renderComponentFixture(RealtimeWhiteboardAgentSeesPopoverComponent, {\n inputs: { DebounceMs: 500, ElementCount: 7, LastDeltaLabel: '2 s ago' },\n });\n const foot = f.nativeElement.querySelector('.sees-pop__foot')?.textContent ?? '';\n expect(foot).toContain('500 ms');\n expect(foot).toContain('7');\n expect(foot).toContain('2 s ago');\n });\n\n it('shows the \"no deltas yet\" placeholder when DeltaJson is empty', () => {\n const f = renderComponentFixture(RealtimeWhiteboardAgentSeesPopoverComponent);\n const code = f.nativeElement.querySelector('.codeblock')?.textContent ?? '';\n expect(code).toContain('no deltas yet');\n // the tinted-delta branch (which carries the replace-current-state comment) is absent\n expect(code).not.toContain('replace-current-state');\n });\n\n it('renders the tinted delta block when DeltaJson is set', () => {\n const f = renderComponentFixture(RealtimeWhiteboardAgentSeesPopoverComponent, {\n inputs: { DeltaJson: '{\"added\": 3}' },\n });\n const pre = f.nativeElement.querySelector('.codeblock');\n expect(pre).not.toBeNull();\n // the @if-true branch carries the replace-current-state semantics comment\n expect(pre!.textContent).toContain('replace-current-state');\n // the setter tints JSON keys (.ck) and numbers (.cn) into spans via [innerHTML]\n expect(pre!.querySelector('.ck')).not.toBeNull();\n expect(pre!.querySelector('.cn')).not.toBeNull();\n });\n\n it('escapes HTML in the delta JSON (no live markup is injected)', () => {\n const f = renderComponentFixture(RealtimeWhiteboardAgentSeesPopoverComponent, {\n inputs: { DeltaJson: '{\"x\": \"<script>bad</script>\"}' },\n });\n // the injected value is escaped, so no real <script> element exists in the popover\n expect(f.nativeElement.querySelector('script')).toBeNull();\n expect(f.nativeElement.querySelector('.codeblock')?.textContent).toContain('<script>');\n });\n});\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"whiteboard-pages.component.dom.test.d.ts","sourceRoot":"","sources":["../../src/lib/whiteboard-pages.component.dom.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { renderComponentFixture } from '@memberjunction/ng-test-utils';
|
|
3
|
+
import { RealtimeWhiteboardPagesComponent } from './whiteboard-pages.component';
|
|
4
|
+
import { WhiteboardState } from './whiteboard-state';
|
|
5
|
+
/**
|
|
6
|
+
* DOM spec for <mj-realtime-whiteboard-pages> — the OneNote-style page strip. This is a
|
|
7
|
+
* data-bound component, but its data source (WhiteboardState) is a plain Angular-free
|
|
8
|
+
* engine we can `new` directly — no metadata provider / RunView needed. Covers: a chip
|
|
9
|
+
* per page via @for, the [class.active] on the active page, the agent-authorship garnish,
|
|
10
|
+
* the @if(CanDelete) close-affordance gating (hidden on the lone page), the @if(!ReadOnly)
|
|
11
|
+
* add button, the ChipContextMenu @Output, and that clicking a chip / add button mutates
|
|
12
|
+
* the engine.
|
|
13
|
+
*/
|
|
14
|
+
describe('RealtimeWhiteboardPagesComponent (DOM)', () => {
|
|
15
|
+
const chips = (f) => Array.from(f.nativeElement.querySelectorAll('.wb-page-chip'));
|
|
16
|
+
const renderWith = (state, readOnly = false) => renderComponentFixture(RealtimeWhiteboardPagesComponent, { inputs: { State: state, ReadOnly: readOnly } });
|
|
17
|
+
it('renders one chip per page in the engine', () => {
|
|
18
|
+
const state = new WhiteboardState();
|
|
19
|
+
state.AddPage('Second');
|
|
20
|
+
const f = renderWith(state);
|
|
21
|
+
expect(chips(f).length).toBe(2);
|
|
22
|
+
});
|
|
23
|
+
it('marks the active page chip with .active (and only one chip is active)', () => {
|
|
24
|
+
const state = new WhiteboardState();
|
|
25
|
+
state.AddPage('Second'); // AddPage switches to the new page, so it is active
|
|
26
|
+
const f = renderWith(state);
|
|
27
|
+
const active = chips(f).filter((c) => c.classList.contains('active'));
|
|
28
|
+
expect(active.length).toBe(1);
|
|
29
|
+
expect(active[0].querySelector('.wb-page-chip__name')?.textContent?.trim()).toBe('Second');
|
|
30
|
+
});
|
|
31
|
+
it('does NOT render the delete affordance when there is only one page', () => {
|
|
32
|
+
const state = new WhiteboardState();
|
|
33
|
+
const f = renderWith(state);
|
|
34
|
+
expect(f.nativeElement.querySelector('.wb-page-chip__close')).toBeNull();
|
|
35
|
+
});
|
|
36
|
+
it('renders a delete affordance on chips once there is more than one page', () => {
|
|
37
|
+
const state = new WhiteboardState();
|
|
38
|
+
state.AddPage('Second');
|
|
39
|
+
const f = renderWith(state);
|
|
40
|
+
expect(f.nativeElement.querySelectorAll('.wb-page-chip__close').length).toBe(2);
|
|
41
|
+
});
|
|
42
|
+
it('adds the agent garnish only to agent-authored page chips', () => {
|
|
43
|
+
const state = new WhiteboardState();
|
|
44
|
+
state.AddPage('Agent Page', 'agent');
|
|
45
|
+
const f = renderWith(state);
|
|
46
|
+
const agentChips = chips(f).filter((c) => c.classList.contains('wb-page-chip--agent'));
|
|
47
|
+
expect(agentChips.length).toBe(1);
|
|
48
|
+
expect(agentChips[0].querySelector('.wb-page-chip__agent-dot')).not.toBeNull();
|
|
49
|
+
});
|
|
50
|
+
it('renders the add (+) button when not read-only', () => {
|
|
51
|
+
const f = renderWith(new WhiteboardState(), false);
|
|
52
|
+
expect(f.nativeElement.querySelector('.wb-page-add')).not.toBeNull();
|
|
53
|
+
});
|
|
54
|
+
it('hides the add (+) button and delete affordances in read-only mode', () => {
|
|
55
|
+
const state = new WhiteboardState();
|
|
56
|
+
state.AddPage('Second');
|
|
57
|
+
const f = renderWith(state, true);
|
|
58
|
+
expect(f.nativeElement.querySelector('.wb-page-add')).toBeNull();
|
|
59
|
+
expect(f.nativeElement.querySelector('.wb-page-chip__close')).toBeNull();
|
|
60
|
+
});
|
|
61
|
+
it('switches the active page in the engine when an inactive chip is clicked', () => {
|
|
62
|
+
const state = new WhiteboardState();
|
|
63
|
+
const firstId = state.Pages[0].ID;
|
|
64
|
+
state.AddPage('Second'); // now "Second" is active, first is inactive
|
|
65
|
+
const f = renderWith(state);
|
|
66
|
+
// click the first (inactive) chip
|
|
67
|
+
const firstChip = chips(f).find((c) => c.querySelector('.wb-page-chip__name')?.textContent?.trim() === 'Page 1');
|
|
68
|
+
firstChip.click();
|
|
69
|
+
expect(state.ActivePageID).toBe(firstId);
|
|
70
|
+
});
|
|
71
|
+
it('adds a page to the engine when the + button is clicked', () => {
|
|
72
|
+
const state = new WhiteboardState();
|
|
73
|
+
const before = state.Pages.length;
|
|
74
|
+
const f = renderWith(state);
|
|
75
|
+
f.nativeElement.querySelector('.wb-page-add').click();
|
|
76
|
+
expect(state.Pages.length).toBe(before + 1);
|
|
77
|
+
});
|
|
78
|
+
it('emits ChipContextMenu (and suppresses the native menu) on right-click', () => {
|
|
79
|
+
const state = new WhiteboardState();
|
|
80
|
+
const spy = vi.fn();
|
|
81
|
+
const f = renderComponentFixture(RealtimeWhiteboardPagesComponent, {
|
|
82
|
+
inputs: { State: state, ReadOnly: false },
|
|
83
|
+
setup: (c) => c.ChipContextMenu.subscribe(spy),
|
|
84
|
+
});
|
|
85
|
+
const evt = new MouseEvent('contextmenu', { bubbles: true, cancelable: true });
|
|
86
|
+
chips(f)[0].dispatchEvent(evt);
|
|
87
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
88
|
+
expect(evt.defaultPrevented).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
it('does NOT emit ChipContextMenu in read-only mode', () => {
|
|
91
|
+
const state = new WhiteboardState();
|
|
92
|
+
const spy = vi.fn();
|
|
93
|
+
const f = renderComponentFixture(RealtimeWhiteboardPagesComponent, {
|
|
94
|
+
inputs: { State: state, ReadOnly: true },
|
|
95
|
+
setup: (c) => c.ChipContextMenu.subscribe(spy),
|
|
96
|
+
});
|
|
97
|
+
chips(f)[0].dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, cancelable: true }));
|
|
98
|
+
expect(spy).not.toHaveBeenCalled();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
//# sourceMappingURL=whiteboard-pages.component.dom.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"whiteboard-pages.component.dom.test.js","sourceRoot":"","sources":["../../src/lib/whiteboard-pages.component.dom.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, it, expect, vi } from 'vitest';\nimport { renderComponentFixture } from '@memberjunction/ng-test-utils';\nimport { RealtimeWhiteboardPagesComponent } from './whiteboard-pages.component';\nimport { WhiteboardState } from './whiteboard-state';\n\n/**\n * DOM spec for <mj-realtime-whiteboard-pages> — the OneNote-style page strip. This is a\n * data-bound component, but its data source (WhiteboardState) is a plain Angular-free\n * engine we can `new` directly — no metadata provider / RunView needed. Covers: a chip\n * per page via @for, the [class.active] on the active page, the agent-authorship garnish,\n * the @if(CanDelete) close-affordance gating (hidden on the lone page), the @if(!ReadOnly)\n * add button, the ChipContextMenu @Output, and that clicking a chip / add button mutates\n * the engine.\n */\ndescribe('RealtimeWhiteboardPagesComponent (DOM)', () => {\n type Fix = ReturnType<typeof renderComponentFixture<RealtimeWhiteboardPagesComponent>>;\n const chips = (f: Fix) => Array.from(f.nativeElement.querySelectorAll('.wb-page-chip')) as HTMLButtonElement[];\n\n const renderWith = (state: WhiteboardState, readOnly = false): Fix =>\n renderComponentFixture(RealtimeWhiteboardPagesComponent, { inputs: { State: state, ReadOnly: readOnly } });\n\n it('renders one chip per page in the engine', () => {\n const state = new WhiteboardState();\n state.AddPage('Second');\n const f = renderWith(state);\n expect(chips(f).length).toBe(2);\n });\n\n it('marks the active page chip with .active (and only one chip is active)', () => {\n const state = new WhiteboardState();\n state.AddPage('Second'); // AddPage switches to the new page, so it is active\n const f = renderWith(state);\n const active = chips(f).filter((c) => c.classList.contains('active'));\n expect(active.length).toBe(1);\n expect(active[0].querySelector('.wb-page-chip__name')?.textContent?.trim()).toBe('Second');\n });\n\n it('does NOT render the delete affordance when there is only one page', () => {\n const state = new WhiteboardState();\n const f = renderWith(state);\n expect(f.nativeElement.querySelector('.wb-page-chip__close')).toBeNull();\n });\n\n it('renders a delete affordance on chips once there is more than one page', () => {\n const state = new WhiteboardState();\n state.AddPage('Second');\n const f = renderWith(state);\n expect(f.nativeElement.querySelectorAll('.wb-page-chip__close').length).toBe(2);\n });\n\n it('adds the agent garnish only to agent-authored page chips', () => {\n const state = new WhiteboardState();\n state.AddPage('Agent Page', 'agent');\n const f = renderWith(state);\n const agentChips = chips(f).filter((c) => c.classList.contains('wb-page-chip--agent'));\n expect(agentChips.length).toBe(1);\n expect(agentChips[0].querySelector('.wb-page-chip__agent-dot')).not.toBeNull();\n });\n\n it('renders the add (+) button when not read-only', () => {\n const f = renderWith(new WhiteboardState(), false);\n expect(f.nativeElement.querySelector('.wb-page-add')).not.toBeNull();\n });\n\n it('hides the add (+) button and delete affordances in read-only mode', () => {\n const state = new WhiteboardState();\n state.AddPage('Second');\n const f = renderWith(state, true);\n expect(f.nativeElement.querySelector('.wb-page-add')).toBeNull();\n expect(f.nativeElement.querySelector('.wb-page-chip__close')).toBeNull();\n });\n\n it('switches the active page in the engine when an inactive chip is clicked', () => {\n const state = new WhiteboardState();\n const firstId = state.Pages[0].ID;\n state.AddPage('Second'); // now \"Second\" is active, first is inactive\n const f = renderWith(state);\n // click the first (inactive) chip\n const firstChip = chips(f).find((c) => c.querySelector('.wb-page-chip__name')?.textContent?.trim() === 'Page 1')!;\n firstChip.click();\n expect(state.ActivePageID).toBe(firstId);\n });\n\n it('adds a page to the engine when the + button is clicked', () => {\n const state = new WhiteboardState();\n const before = state.Pages.length;\n const f = renderWith(state);\n (f.nativeElement.querySelector('.wb-page-add') as HTMLButtonElement).click();\n expect(state.Pages.length).toBe(before + 1);\n });\n\n it('emits ChipContextMenu (and suppresses the native menu) on right-click', () => {\n const state = new WhiteboardState();\n const spy = vi.fn();\n const f = renderComponentFixture(RealtimeWhiteboardPagesComponent, {\n inputs: { State: state, ReadOnly: false },\n setup: (c) => c.ChipContextMenu.subscribe(spy),\n });\n const evt = new MouseEvent('contextmenu', { bubbles: true, cancelable: true });\n chips(f)[0].dispatchEvent(evt);\n expect(spy).toHaveBeenCalledTimes(1);\n expect(evt.defaultPrevented).toBe(true);\n });\n\n it('does NOT emit ChipContextMenu in read-only mode', () => {\n const state = new WhiteboardState();\n const spy = vi.fn();\n const f = renderComponentFixture(RealtimeWhiteboardPagesComponent, {\n inputs: { State: state, ReadOnly: true },\n setup: (c) => c.ChipContextMenu.subscribe(spy),\n });\n chips(f)[0].dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, cancelable: true }));\n expect(spy).not.toHaveBeenCalled();\n });\n});\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"whiteboard-snapshot.component.d.ts","sourceRoot":"","sources":["../../src/lib/whiteboard-snapshot.component.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;;
|
|
1
|
+
{"version":3,"file":"whiteboard-snapshot.component.d.ts","sourceRoot":"","sources":["../../src/lib/whiteboard-snapshot.component.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;;AAGrD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,eAAe,GAAG,IAAI,CAS3F;AAED;;;;;;;GAOG;AACH,qBA+Ca,2BAA2B;IACtC,OAAO,CAAC,UAAU,CAAM;IAExB,0EAA0E;IACnE,KAAK,EAAE,eAAe,GAAG,IAAI,CAAQ;IAE5C,mFAAmF;IACnF,IACI,SAAS,CAAC,KAAK,EAAE,MAAM,EAK1B;IACD,IAAI,SAAS,IAAI,MAAM,CAEtB;IAED,mFAAmF;IAC1E,SAAS,SAAW;yCAnBlB,2BAA2B;2CAA3B,2BAA2B;CAoBvC"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Component, Input } from '@angular/core';
|
|
2
2
|
import { RealtimeWhiteboardBoardComponent } from './whiteboard-board.component';
|
|
3
3
|
import { WhiteboardState } from './whiteboard-state';
|
|
4
|
+
import { MJEmptyStateComponent } from '@memberjunction/ng-ui-components';
|
|
4
5
|
import * as i0 from "@angular/core";
|
|
5
6
|
function WhiteboardSnapshotComponent_Conditional_0_Template(rf, ctx) { if (rf & 1) {
|
|
6
7
|
i0.ɵɵelementStart(0, "div", 0);
|
|
@@ -12,11 +13,7 @@ function WhiteboardSnapshotComponent_Conditional_0_Template(rf, ctx) { if (rf &
|
|
|
12
13
|
i0.ɵɵproperty("State", ctx_r0.State)("AgentName", ctx_r0.AgentName)("ReadOnly", true)("Tool", "pan");
|
|
13
14
|
} }
|
|
14
15
|
function WhiteboardSnapshotComponent_Conditional_1_Template(rf, ctx) { if (rf & 1) {
|
|
15
|
-
i0.ɵɵ
|
|
16
|
-
i0.ɵɵelement(1, "i", 3);
|
|
17
|
-
i0.ɵɵelementStart(2, "span");
|
|
18
|
-
i0.ɵɵtext(3, "No board content");
|
|
19
|
-
i0.ɵɵelementEnd()();
|
|
16
|
+
i0.ɵɵelement(0, "mj-empty-state", 1);
|
|
20
17
|
} }
|
|
21
18
|
/**
|
|
22
19
|
* Parse a persisted whiteboard payload ({@link WhiteboardState.ToJSON} output) into a
|
|
@@ -62,24 +59,23 @@ export class WhiteboardSnapshotComponent {
|
|
|
62
59
|
return this._stateJson;
|
|
63
60
|
}
|
|
64
61
|
static { this.ɵfac = function WhiteboardSnapshotComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || WhiteboardSnapshotComponent)(); }; }
|
|
65
|
-
static { this.ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: WhiteboardSnapshotComponent, selectors: [["mj-whiteboard-snapshot"]], inputs: { StateJson: "StateJson", AgentName: "AgentName" }, decls: 2, vars: 1, consts: [[1, "wb-snapshot-board"], [
|
|
66
|
-
i0.ɵɵconditionalCreate(0, WhiteboardSnapshotComponent_Conditional_0_Template, 2, 4, "div", 0)(1, WhiteboardSnapshotComponent_Conditional_1_Template,
|
|
62
|
+
static { this.ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: WhiteboardSnapshotComponent, selectors: [["mj-whiteboard-snapshot"]], inputs: { StateJson: "StateJson", AgentName: "AgentName" }, decls: 2, vars: 1, consts: [[1, "wb-snapshot-board"], ["Size", "compact", "Icon", "fa-solid fa-chalkboard", "Title", "No board content", 1, "wb-snapshot-empty"], [3, "State", "AgentName", "ReadOnly", "Tool"]], template: function WhiteboardSnapshotComponent_Template(rf, ctx) { if (rf & 1) {
|
|
63
|
+
i0.ɵɵconditionalCreate(0, WhiteboardSnapshotComponent_Conditional_0_Template, 2, 4, "div", 0)(1, WhiteboardSnapshotComponent_Conditional_1_Template, 1, 0, "mj-empty-state", 1);
|
|
67
64
|
} if (rf & 2) {
|
|
68
65
|
i0.ɵɵconditional(ctx.State ? 0 : 1);
|
|
69
|
-
} }, dependencies: [RealtimeWhiteboardBoardComponent], styles: ["[_nghost-%COMP%] {\n display: flex;\n flex-direction: column;\n flex: 1;\n height: 100%;\n min-height: 320px;\n }\n \n\n .wb-snapshot-board[_ngcontent-%COMP%] {\n position: relative;\n flex: 1;\n min-height: 0;\n }\n .wb-snapshot-empty[_ngcontent-%COMP%] {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 8px;\n color: var(--mj-text-muted);\n background: var(--mj-bg-surface-sunken);\n border-radius: var(--mj-radius-md, 6px);\n font-size: 13px;\n }\n .wb-snapshot-empty[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n font-size: 22px;\n color: var(--mj-text-disabled);\n }"] }); }
|
|
66
|
+
} }, dependencies: [RealtimeWhiteboardBoardComponent, MJEmptyStateComponent], styles: ["[_nghost-%COMP%] {\n display: flex;\n flex-direction: column;\n flex: 1;\n height: 100%;\n min-height: 320px;\n }\n \n\n .wb-snapshot-board[_ngcontent-%COMP%] {\n position: relative;\n flex: 1;\n min-height: 0;\n }\n .wb-snapshot-empty[_ngcontent-%COMP%] {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 8px;\n color: var(--mj-text-muted);\n background: var(--mj-bg-surface-sunken);\n border-radius: var(--mj-radius-md, 6px);\n font-size: 13px;\n }\n .wb-snapshot-empty[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n font-size: 22px;\n color: var(--mj-text-disabled);\n }"] }); }
|
|
70
67
|
}
|
|
71
68
|
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(WhiteboardSnapshotComponent, [{
|
|
72
69
|
type: Component,
|
|
73
|
-
args: [{ standalone: true, selector: 'mj-whiteboard-snapshot', imports: [RealtimeWhiteboardBoardComponent], template: `
|
|
70
|
+
args: [{ standalone: true, selector: 'mj-whiteboard-snapshot', imports: [RealtimeWhiteboardBoardComponent, MJEmptyStateComponent], template: `
|
|
74
71
|
@if (State) {
|
|
75
72
|
<div class="wb-snapshot-board">
|
|
76
73
|
<mj-realtime-whiteboard [State]="State" [AgentName]="AgentName" [ReadOnly]="true" [Tool]="'pan'" />
|
|
77
74
|
</div>
|
|
78
75
|
} @else {
|
|
79
|
-
<
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
</div>
|
|
76
|
+
<mj-empty-state class="wb-snapshot-empty" Size="compact"
|
|
77
|
+
Icon="fa-solid fa-chalkboard"
|
|
78
|
+
Title="No board content" />
|
|
83
79
|
}
|
|
84
80
|
`, styles: ["\n :host {\n display: flex;\n flex-direction: column;\n flex: 1;\n height: 100%;\n min-height: 320px;\n }\n /* the board component positions itself absolute, so give it a relative frame */\n .wb-snapshot-board {\n position: relative;\n flex: 1;\n min-height: 0;\n }\n .wb-snapshot-empty {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 8px;\n color: var(--mj-text-muted);\n background: var(--mj-bg-surface-sunken);\n border-radius: var(--mj-radius-md, 6px);\n font-size: 13px;\n }\n .wb-snapshot-empty i {\n font-size: 22px;\n color: var(--mj-text-disabled);\n }\n "] }]
|
|
85
81
|
}], null, { StateJson: [{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"whiteboard-snapshot.component.js","sourceRoot":"","sources":["../../src/lib/whiteboard-snapshot.component.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,gCAAgC,EAAE,MAAM,8BAA8B,CAAC;AAChF,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;;;
|
|
1
|
+
{"version":3,"file":"whiteboard-snapshot.component.js","sourceRoot":"","sources":["../../src/lib/whiteboard-snapshot.component.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,gCAAgC,EAAE,MAAM,8BAA8B,CAAC;AAChF,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,qBAAqB,EAAE,MAAM,kCAAkC,CAAC;;;IAiCnE,8BAA+B;IAC7B,4CAAmG;IACrG,iBAAM;;;IADoB,cAAe;IAA2C,AAAlB,AAAxB,AAAhB,oCAAe,+BAAwB,kBAAkB,eAAe;;;IAGlG,oCAE6B;;AArCnC;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAA+B;IACjE,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtC,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,CAAC;QACH,OAAO,eAAe,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IACxC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AAgDH,MAAM,OAAO,2BAA2B;IA/CxC;QAgDU,eAAU,GAAG,EAAE,CAAC;QAExB,0EAA0E;QACnE,UAAK,GAA2B,IAAI,CAAC;QAc5C,mFAAmF;QAC1E,cAAS,GAAG,OAAO,CAAC;KAC9B;IAdC,mFAAmF;IACnF,IACI,SAAS,CAAC,KAAa;QACzB,IAAI,KAAK,KAAK,IAAI,CAAC,UAAU,EAAE,CAAC;YAC9B,IAAI,CAAC,UAAU,GAAG,KAAK,CAAC;YACxB,IAAI,CAAC,KAAK,GAAG,mBAAmB,CAAC,KAAK,CAAC,CAAC;QAC1C,CAAC;IACH,CAAC;IACD,IAAI,SAAS;QACX,OAAO,IAAI,CAAC,UAAU,CAAC;IACzB,CAAC;4HAhBU,2BAA2B;oEAA3B,2BAA2B;YAtClC,AAJF,6FAAa,kFAIJ;;YAJT,mCAQC;4BAVO,gCAAgC,EAAE,qBAAqB;;iFA4CtD,2BAA2B;cA/CvC,SAAS;6BACI,IAAI,YACN,wBAAwB,WACzB,CAAC,gCAAgC,EAAE,qBAAqB,CAAC,YACxD;;;;;;;;;;GAUT;;kBAwCA,KAAK;;kBAYL,KAAK;;kFAnBK,2BAA2B","sourcesContent":["import { Component, Input } from '@angular/core';\nimport { RealtimeWhiteboardBoardComponent } from './whiteboard-board.component';\nimport { WhiteboardState } from './whiteboard-state';\nimport { MJEmptyStateComponent } from '@memberjunction/ng-ui-components';\n\n/**\n * Parse a persisted whiteboard payload ({@link WhiteboardState.ToJSON} output) into a\n * rehydrated engine instance. PURE + TOLERANT: any malformed input (empty, non-JSON,\n * JSON of the wrong shape) returns `null` instead of throwing — callers render an\n * empty state rather than breaking the surface they're embedded in.\n */\nexport function ParseBoardStateJson(json: string | null | undefined): WhiteboardState | null {\n if (!json || json.trim().length === 0) {\n return null;\n }\n try {\n return WhiteboardState.FromJSON(json);\n } catch {\n return null;\n }\n}\n\n/**\n * READ-ONLY SNAPSHOT of a whiteboard — renders a persisted board payload (the\n * session-channel artifact JSON) through the real board component in `ReadOnly` mode,\n * so a saved board looks exactly like the live one: same chips, shapes, ink and\n * connectors, with pan + zoom still available for navigation but every mutating\n * interaction disabled. Used by the whiteboard artifact viewer plugin and any\n * session-review surface that wants to show a board without an engine wired to tools.\n */\n@Component({\n standalone: true,\n selector: 'mj-whiteboard-snapshot',\n imports: [RealtimeWhiteboardBoardComponent, MJEmptyStateComponent],\n template: `\n @if (State) {\n <div class=\"wb-snapshot-board\">\n <mj-realtime-whiteboard [State]=\"State\" [AgentName]=\"AgentName\" [ReadOnly]=\"true\" [Tool]=\"'pan'\" />\n </div>\n } @else {\n <mj-empty-state class=\"wb-snapshot-empty\" Size=\"compact\"\n Icon=\"fa-solid fa-chalkboard\"\n Title=\"No board content\" />\n }\n `,\n styles: [`\n :host {\n display: flex;\n flex-direction: column;\n flex: 1;\n height: 100%;\n min-height: 320px;\n }\n /* the board component positions itself absolute, so give it a relative frame */\n .wb-snapshot-board {\n position: relative;\n flex: 1;\n min-height: 0;\n }\n .wb-snapshot-empty {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 8px;\n color: var(--mj-text-muted);\n background: var(--mj-bg-surface-sunken);\n border-radius: var(--mj-radius-md, 6px);\n font-size: 13px;\n }\n .wb-snapshot-empty i {\n font-size: 22px;\n color: var(--mj-text-disabled);\n }\n `]\n})\nexport class WhiteboardSnapshotComponent {\n private _stateJson = '';\n\n /** Rehydrated board engine (null when the payload is empty/malformed). */\n public State: WhiteboardState | null = null;\n\n /** Persisted board payload — setter-driven so a new artifact version re-parses. */\n @Input()\n set StateJson(value: string) {\n if (value !== this._stateJson) {\n this._stateJson = value;\n this.State = ParseBoardStateJson(value);\n }\n }\n get StateJson(): string {\n return this._stateJson;\n }\n\n /** Display name of the session's agent — ownership chips on the rendered board. */\n @Input() AgentName = 'Agent';\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"whiteboard-toolbar.component.dom.test.d.ts","sourceRoot":"","sources":["../../src/lib/whiteboard-toolbar.component.dom.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { renderComponentFixture } from '@memberjunction/ng-test-utils';
|
|
3
|
+
import { RealtimeWhiteboardToolbarComponent } from './whiteboard-toolbar.component';
|
|
4
|
+
/**
|
|
5
|
+
* DOM spec for <mj-realtime-whiteboard-toolbar> — the floating left-edge tool palette.
|
|
6
|
+
* Covers the @for tool rendering, [class.active] on the active tool, the @if pen/text/
|
|
7
|
+
* shape flyouts that only render when their tool is active, the undo/redo [disabled]
|
|
8
|
+
* gating, and the @Output emissions (tool change, pen color/width, undo/redo, and the
|
|
9
|
+
* compound text-style picks that emit BOTH a *Change and StyleSelection).
|
|
10
|
+
*/
|
|
11
|
+
describe('RealtimeWhiteboardToolbarComponent (DOM)', () => {
|
|
12
|
+
const toolButtons = (f) => Array.from(f.nativeElement.querySelectorAll('button.tool'));
|
|
13
|
+
it('renders one tool button per entry in Tools (plus undo + redo)', () => {
|
|
14
|
+
const f = renderComponentFixture(RealtimeWhiteboardToolbarComponent);
|
|
15
|
+
// 11 tools + undo + redo = 13 .tool buttons
|
|
16
|
+
expect(toolButtons(f).length).toBe(f.componentInstance.Tools.length + 2);
|
|
17
|
+
});
|
|
18
|
+
it('marks the active tool button with the .active class and only that one', () => {
|
|
19
|
+
const f = renderComponentFixture(RealtimeWhiteboardToolbarComponent, { inputs: { ActiveTool: 'pen' } });
|
|
20
|
+
const active = toolButtons(f).filter((b) => b.classList.contains('active'));
|
|
21
|
+
expect(active.length).toBe(1);
|
|
22
|
+
expect(active[0].getAttribute('title')).toBe('Pen');
|
|
23
|
+
});
|
|
24
|
+
it('does not render the pen flyout when pen is not the active tool', () => {
|
|
25
|
+
const f = renderComponentFixture(RealtimeWhiteboardToolbarComponent, { inputs: { ActiveTool: 'select' } });
|
|
26
|
+
expect(f.nativeElement.querySelector('.pen-flyout')).toBeNull();
|
|
27
|
+
});
|
|
28
|
+
it('renders the pen flyout (with one swatch per pen color) when pen is active', () => {
|
|
29
|
+
const f = renderComponentFixture(RealtimeWhiteboardToolbarComponent, { inputs: { ActiveTool: 'pen' } });
|
|
30
|
+
const flyout = f.nativeElement.querySelector('.pen-flyout');
|
|
31
|
+
expect(flyout).not.toBeNull();
|
|
32
|
+
expect(flyout.querySelectorAll('.pen-c').length).toBe(f.componentInstance.PenColors.length);
|
|
33
|
+
});
|
|
34
|
+
it('shows the agent name in the pen-flyout violet note', () => {
|
|
35
|
+
const f = renderComponentFixture(RealtimeWhiteboardToolbarComponent, { inputs: { ActiveTool: 'pen', AgentName: 'Sage' } });
|
|
36
|
+
expect(f.nativeElement.querySelector('.pen-note')?.textContent).toContain("violet = Sage's");
|
|
37
|
+
});
|
|
38
|
+
it('renders the text-style flyout only when text is the active tool', () => {
|
|
39
|
+
const f = renderComponentFixture(RealtimeWhiteboardToolbarComponent, { inputs: { ActiveTool: 'text' } });
|
|
40
|
+
expect(f.nativeElement.querySelector('.text-flyout')).not.toBeNull();
|
|
41
|
+
});
|
|
42
|
+
it('renders the shape flyout only when shape is the active tool', () => {
|
|
43
|
+
const f = renderComponentFixture(RealtimeWhiteboardToolbarComponent, { inputs: { ActiveTool: 'shape' } });
|
|
44
|
+
const flyout = f.nativeElement.querySelector('.shape-flyout');
|
|
45
|
+
expect(flyout).not.toBeNull();
|
|
46
|
+
expect(flyout.querySelectorAll('.shape-c').length).toBe(f.componentInstance.ShapeKinds.length);
|
|
47
|
+
});
|
|
48
|
+
it('disables undo/redo by default and enables them when CanUndo/CanRedo are true', () => {
|
|
49
|
+
const disabled = renderComponentFixture(RealtimeWhiteboardToolbarComponent);
|
|
50
|
+
const undoD = disabled.nativeElement.querySelector('button[title="Undo"]');
|
|
51
|
+
const redoD = disabled.nativeElement.querySelector('button[title="Redo"]');
|
|
52
|
+
expect(undoD.disabled).toBe(true);
|
|
53
|
+
expect(redoD.disabled).toBe(true);
|
|
54
|
+
const enabled = renderComponentFixture(RealtimeWhiteboardToolbarComponent, { inputs: { CanUndo: true, CanRedo: true } });
|
|
55
|
+
expect(enabled.nativeElement.querySelector('button[title="Undo"]').disabled).toBe(false);
|
|
56
|
+
expect(enabled.nativeElement.querySelector('button[title="Redo"]').disabled).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
it('emits ToolChange with the clicked tool', () => {
|
|
59
|
+
const spy = vi.fn();
|
|
60
|
+
const f = renderComponentFixture(RealtimeWhiteboardToolbarComponent, { setup: (c) => c.ToolChange.subscribe(spy) });
|
|
61
|
+
// the pen tool button (3rd entry, index 2)
|
|
62
|
+
const penBtn = f.nativeElement.querySelector('button[title="Pen"]');
|
|
63
|
+
penBtn.click();
|
|
64
|
+
expect(spy).toHaveBeenCalledWith('pen');
|
|
65
|
+
});
|
|
66
|
+
it('emits PenColorChange when a pen swatch is clicked', () => {
|
|
67
|
+
const spy = vi.fn();
|
|
68
|
+
const f = renderComponentFixture(RealtimeWhiteboardToolbarComponent, {
|
|
69
|
+
inputs: { ActiveTool: 'pen' },
|
|
70
|
+
setup: (c) => c.PenColorChange.subscribe(spy),
|
|
71
|
+
});
|
|
72
|
+
const swatch = f.nativeElement.querySelector('.pen-flyout .pen-c');
|
|
73
|
+
swatch.click();
|
|
74
|
+
expect(spy).toHaveBeenCalledWith(f.componentInstance.PenColors[0]);
|
|
75
|
+
});
|
|
76
|
+
it('emits BOTH TextSizeChange and StyleSelection when a text size is picked', () => {
|
|
77
|
+
const sizeSpy = vi.fn();
|
|
78
|
+
const styleSpy = vi.fn();
|
|
79
|
+
const f = renderComponentFixture(RealtimeWhiteboardToolbarComponent, {
|
|
80
|
+
inputs: { ActiveTool: 'text' },
|
|
81
|
+
setup: (c) => {
|
|
82
|
+
c.TextSizeChange.subscribe(sizeSpy);
|
|
83
|
+
c.StyleSelection.subscribe(styleSpy);
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
const sizeEl = f.nativeElement.querySelector('.text-flyout .txt-s');
|
|
87
|
+
sizeEl.click();
|
|
88
|
+
const firstSize = f.componentInstance.TextSizes[0];
|
|
89
|
+
expect(sizeSpy).toHaveBeenCalledWith(firstSize);
|
|
90
|
+
expect(styleSpy).toHaveBeenCalledWith({ FontSize: firstSize });
|
|
91
|
+
});
|
|
92
|
+
it('emits Undo / Redo when those buttons are enabled and clicked', () => {
|
|
93
|
+
const undoSpy = vi.fn();
|
|
94
|
+
const redoSpy = vi.fn();
|
|
95
|
+
const f = renderComponentFixture(RealtimeWhiteboardToolbarComponent, {
|
|
96
|
+
inputs: { CanUndo: true, CanRedo: true },
|
|
97
|
+
setup: (c) => {
|
|
98
|
+
c.Undo.subscribe(undoSpy);
|
|
99
|
+
c.Redo.subscribe(redoSpy);
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
f.nativeElement.querySelector('button[title="Undo"]').click();
|
|
103
|
+
f.nativeElement.querySelector('button[title="Redo"]').click();
|
|
104
|
+
expect(undoSpy).toHaveBeenCalledTimes(1);
|
|
105
|
+
expect(redoSpy).toHaveBeenCalledTimes(1);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
//# sourceMappingURL=whiteboard-toolbar.component.dom.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"whiteboard-toolbar.component.dom.test.js","sourceRoot":"","sources":["../../src/lib/whiteboard-toolbar.component.dom.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, it, expect, vi } from 'vitest';\nimport { renderComponentFixture } from '@memberjunction/ng-test-utils';\nimport { RealtimeWhiteboardToolbarComponent } from './whiteboard-toolbar.component';\n\n/**\n * DOM spec for <mj-realtime-whiteboard-toolbar> — the floating left-edge tool palette.\n * Covers the @for tool rendering, [class.active] on the active tool, the @if pen/text/\n * shape flyouts that only render when their tool is active, the undo/redo [disabled]\n * gating, and the @Output emissions (tool change, pen color/width, undo/redo, and the\n * compound text-style picks that emit BOTH a *Change and StyleSelection).\n */\ndescribe('RealtimeWhiteboardToolbarComponent (DOM)', () => {\n type Fix = ReturnType<typeof renderComponentFixture<RealtimeWhiteboardToolbarComponent>>;\n const toolButtons = (f: Fix) => Array.from(f.nativeElement.querySelectorAll('button.tool')) as HTMLButtonElement[];\n\n it('renders one tool button per entry in Tools (plus undo + redo)', () => {\n const f = renderComponentFixture(RealtimeWhiteboardToolbarComponent);\n // 11 tools + undo + redo = 13 .tool buttons\n expect(toolButtons(f).length).toBe(f.componentInstance.Tools.length + 2);\n });\n\n it('marks the active tool button with the .active class and only that one', () => {\n const f = renderComponentFixture(RealtimeWhiteboardToolbarComponent, { inputs: { ActiveTool: 'pen' } });\n const active = toolButtons(f).filter((b) => b.classList.contains('active'));\n expect(active.length).toBe(1);\n expect(active[0].getAttribute('title')).toBe('Pen');\n });\n\n it('does not render the pen flyout when pen is not the active tool', () => {\n const f = renderComponentFixture(RealtimeWhiteboardToolbarComponent, { inputs: { ActiveTool: 'select' } });\n expect(f.nativeElement.querySelector('.pen-flyout')).toBeNull();\n });\n\n it('renders the pen flyout (with one swatch per pen color) when pen is active', () => {\n const f = renderComponentFixture(RealtimeWhiteboardToolbarComponent, { inputs: { ActiveTool: 'pen' } });\n const flyout = f.nativeElement.querySelector('.pen-flyout');\n expect(flyout).not.toBeNull();\n expect(flyout!.querySelectorAll('.pen-c').length).toBe(f.componentInstance.PenColors.length);\n });\n\n it('shows the agent name in the pen-flyout violet note', () => {\n const f = renderComponentFixture(RealtimeWhiteboardToolbarComponent, { inputs: { ActiveTool: 'pen', AgentName: 'Sage' } });\n expect(f.nativeElement.querySelector('.pen-note')?.textContent).toContain(\"violet = Sage's\");\n });\n\n it('renders the text-style flyout only when text is the active tool', () => {\n const f = renderComponentFixture(RealtimeWhiteboardToolbarComponent, { inputs: { ActiveTool: 'text' } });\n expect(f.nativeElement.querySelector('.text-flyout')).not.toBeNull();\n });\n\n it('renders the shape flyout only when shape is the active tool', () => {\n const f = renderComponentFixture(RealtimeWhiteboardToolbarComponent, { inputs: { ActiveTool: 'shape' } });\n const flyout = f.nativeElement.querySelector('.shape-flyout');\n expect(flyout).not.toBeNull();\n expect(flyout!.querySelectorAll('.shape-c').length).toBe(f.componentInstance.ShapeKinds.length);\n });\n\n it('disables undo/redo by default and enables them when CanUndo/CanRedo are true', () => {\n const disabled = renderComponentFixture(RealtimeWhiteboardToolbarComponent);\n const undoD = disabled.nativeElement.querySelector('button[title=\"Undo\"]') as HTMLButtonElement;\n const redoD = disabled.nativeElement.querySelector('button[title=\"Redo\"]') as HTMLButtonElement;\n expect(undoD.disabled).toBe(true);\n expect(redoD.disabled).toBe(true);\n\n const enabled = renderComponentFixture(RealtimeWhiteboardToolbarComponent, { inputs: { CanUndo: true, CanRedo: true } });\n expect((enabled.nativeElement.querySelector('button[title=\"Undo\"]') as HTMLButtonElement).disabled).toBe(false);\n expect((enabled.nativeElement.querySelector('button[title=\"Redo\"]') as HTMLButtonElement).disabled).toBe(false);\n });\n\n it('emits ToolChange with the clicked tool', () => {\n const spy = vi.fn();\n const f = renderComponentFixture(RealtimeWhiteboardToolbarComponent, { setup: (c) => c.ToolChange.subscribe(spy) });\n // the pen tool button (3rd entry, index 2)\n const penBtn = f.nativeElement.querySelector('button[title=\"Pen\"]') as HTMLButtonElement;\n penBtn.click();\n expect(spy).toHaveBeenCalledWith('pen');\n });\n\n it('emits PenColorChange when a pen swatch is clicked', () => {\n const spy = vi.fn();\n const f = renderComponentFixture(RealtimeWhiteboardToolbarComponent, {\n inputs: { ActiveTool: 'pen' },\n setup: (c) => c.PenColorChange.subscribe(spy),\n });\n const swatch = f.nativeElement.querySelector('.pen-flyout .pen-c') as HTMLElement;\n swatch.click();\n expect(spy).toHaveBeenCalledWith(f.componentInstance.PenColors[0]);\n });\n\n it('emits BOTH TextSizeChange and StyleSelection when a text size is picked', () => {\n const sizeSpy = vi.fn();\n const styleSpy = vi.fn();\n const f = renderComponentFixture(RealtimeWhiteboardToolbarComponent, {\n inputs: { ActiveTool: 'text' },\n setup: (c) => {\n c.TextSizeChange.subscribe(sizeSpy);\n c.StyleSelection.subscribe(styleSpy);\n },\n });\n const sizeEl = f.nativeElement.querySelector('.text-flyout .txt-s') as HTMLElement;\n sizeEl.click();\n const firstSize = f.componentInstance.TextSizes[0];\n expect(sizeSpy).toHaveBeenCalledWith(firstSize);\n expect(styleSpy).toHaveBeenCalledWith({ FontSize: firstSize });\n });\n\n it('emits Undo / Redo when those buttons are enabled and clicked', () => {\n const undoSpy = vi.fn();\n const redoSpy = vi.fn();\n const f = renderComponentFixture(RealtimeWhiteboardToolbarComponent, {\n inputs: { CanUndo: true, CanRedo: true },\n setup: (c) => {\n c.Undo.subscribe(undoSpy);\n c.Redo.subscribe(redoSpy);\n },\n });\n (f.nativeElement.querySelector('button[title=\"Undo\"]') as HTMLButtonElement).click();\n (f.nativeElement.querySelector('button[title=\"Redo\"]') as HTMLButtonElement).click();\n expect(undoSpy).toHaveBeenCalledTimes(1);\n expect(redoSpy).toHaveBeenCalledTimes(1);\n });\n});\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"whiteboard-zoom.component.dom.test.d.ts","sourceRoot":"","sources":["../../src/lib/whiteboard-zoom.component.dom.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { renderComponentFixture } from '@memberjunction/ng-test-utils';
|
|
3
|
+
import { RealtimeWhiteboardZoomComponent } from './whiteboard-zoom.component';
|
|
4
|
+
/**
|
|
5
|
+
* DOM spec for <mj-realtime-whiteboard-zoom> — the zoom cluster. Covers the percentage
|
|
6
|
+
* binding, the minimap on-class toggle, and the single-click @Output emissions. The
|
|
7
|
+
* hold-to-zoom continuous path is timer-driven and exercised in the class-level suite;
|
|
8
|
+
* here we assert the click contract (a plain click steps once).
|
|
9
|
+
*/
|
|
10
|
+
describe('RealtimeWhiteboardZoomComponent (DOM)', () => {
|
|
11
|
+
const buttons = (f) => Array.from(f.nativeElement.querySelectorAll('button'));
|
|
12
|
+
// template order: [0]=zoom out (−), [1]=zoom in (+), [2]=Fit, [3]=Minimap
|
|
13
|
+
const minusBtn = (f) => buttons(f)[0];
|
|
14
|
+
const plusBtn = (f) => buttons(f)[1];
|
|
15
|
+
const fitBtn = (f) => buttons(f)[2];
|
|
16
|
+
const mapBtn = (f) => buttons(f)[3];
|
|
17
|
+
it('renders the ZoomPercent with a trailing % sign', () => {
|
|
18
|
+
const f = renderComponentFixture(RealtimeWhiteboardZoomComponent, { inputs: { ZoomPercent: 90 } });
|
|
19
|
+
expect(f.nativeElement.querySelector('.zoom-pct')?.textContent?.trim()).toBe('90%');
|
|
20
|
+
});
|
|
21
|
+
it('defaults the percentage label to 100%', () => {
|
|
22
|
+
const f = renderComponentFixture(RealtimeWhiteboardZoomComponent);
|
|
23
|
+
expect(f.nativeElement.querySelector('.zoom-pct')?.textContent?.trim()).toBe('100%');
|
|
24
|
+
});
|
|
25
|
+
it('does not apply the .on class to the minimap button when MinimapOpen is false', () => {
|
|
26
|
+
const f = renderComponentFixture(RealtimeWhiteboardZoomComponent, { inputs: { MinimapOpen: false } });
|
|
27
|
+
expect(mapBtn(f).classList.contains('on')).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
it('applies the .on class to the minimap button when MinimapOpen is true', () => {
|
|
30
|
+
const f = renderComponentFixture(RealtimeWhiteboardZoomComponent, { inputs: { MinimapOpen: true } });
|
|
31
|
+
expect(mapBtn(f).classList.contains('on')).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
it('emits ZoomIn on a plain click of the + button', () => {
|
|
34
|
+
const spy = vi.fn();
|
|
35
|
+
const f = renderComponentFixture(RealtimeWhiteboardZoomComponent, { setup: (c) => c.ZoomIn.subscribe(spy) });
|
|
36
|
+
plusBtn(f).click();
|
|
37
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
38
|
+
});
|
|
39
|
+
it('emits ZoomOut on a plain click of the − button', () => {
|
|
40
|
+
const spy = vi.fn();
|
|
41
|
+
const f = renderComponentFixture(RealtimeWhiteboardZoomComponent, { setup: (c) => c.ZoomOut.subscribe(spy) });
|
|
42
|
+
minusBtn(f).click();
|
|
43
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
44
|
+
});
|
|
45
|
+
it('emits Fit when the fit-to-content button is clicked', () => {
|
|
46
|
+
const spy = vi.fn();
|
|
47
|
+
const f = renderComponentFixture(RealtimeWhiteboardZoomComponent, { setup: (c) => c.Fit.subscribe(spy) });
|
|
48
|
+
fitBtn(f).click();
|
|
49
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
50
|
+
});
|
|
51
|
+
it('emits ToggleMinimap when the minimap button is clicked', () => {
|
|
52
|
+
const spy = vi.fn();
|
|
53
|
+
const f = renderComponentFixture(RealtimeWhiteboardZoomComponent, { setup: (c) => c.ToggleMinimap.subscribe(spy) });
|
|
54
|
+
mapBtn(f).click();
|
|
55
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
//# sourceMappingURL=whiteboard-zoom.component.dom.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"whiteboard-zoom.component.dom.test.js","sourceRoot":"","sources":["../../src/lib/whiteboard-zoom.component.dom.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, it, expect, vi } from 'vitest';\nimport { renderComponentFixture } from '@memberjunction/ng-test-utils';\nimport { RealtimeWhiteboardZoomComponent } from './whiteboard-zoom.component';\n\n/**\n * DOM spec for <mj-realtime-whiteboard-zoom> — the zoom cluster. Covers the percentage\n * binding, the minimap on-class toggle, and the single-click @Output emissions. The\n * hold-to-zoom continuous path is timer-driven and exercised in the class-level suite;\n * here we assert the click contract (a plain click steps once).\n */\ndescribe('RealtimeWhiteboardZoomComponent (DOM)', () => {\n const buttons = (f: ReturnType<typeof renderComponentFixture<RealtimeWhiteboardZoomComponent>>) =>\n Array.from(f.nativeElement.querySelectorAll('button')) as HTMLButtonElement[];\n // template order: [0]=zoom out (−), [1]=zoom in (+), [2]=Fit, [3]=Minimap\n const minusBtn = (f: ReturnType<typeof renderComponentFixture<RealtimeWhiteboardZoomComponent>>) => buttons(f)[0];\n const plusBtn = (f: ReturnType<typeof renderComponentFixture<RealtimeWhiteboardZoomComponent>>) => buttons(f)[1];\n const fitBtn = (f: ReturnType<typeof renderComponentFixture<RealtimeWhiteboardZoomComponent>>) => buttons(f)[2];\n const mapBtn = (f: ReturnType<typeof renderComponentFixture<RealtimeWhiteboardZoomComponent>>) => buttons(f)[3];\n\n it('renders the ZoomPercent with a trailing % sign', () => {\n const f = renderComponentFixture(RealtimeWhiteboardZoomComponent, { inputs: { ZoomPercent: 90 } });\n expect(f.nativeElement.querySelector('.zoom-pct')?.textContent?.trim()).toBe('90%');\n });\n\n it('defaults the percentage label to 100%', () => {\n const f = renderComponentFixture(RealtimeWhiteboardZoomComponent);\n expect(f.nativeElement.querySelector('.zoom-pct')?.textContent?.trim()).toBe('100%');\n });\n\n it('does not apply the .on class to the minimap button when MinimapOpen is false', () => {\n const f = renderComponentFixture(RealtimeWhiteboardZoomComponent, { inputs: { MinimapOpen: false } });\n expect(mapBtn(f).classList.contains('on')).toBe(false);\n });\n\n it('applies the .on class to the minimap button when MinimapOpen is true', () => {\n const f = renderComponentFixture(RealtimeWhiteboardZoomComponent, { inputs: { MinimapOpen: true } });\n expect(mapBtn(f).classList.contains('on')).toBe(true);\n });\n\n it('emits ZoomIn on a plain click of the + button', () => {\n const spy = vi.fn();\n const f = renderComponentFixture(RealtimeWhiteboardZoomComponent, { setup: (c) => c.ZoomIn.subscribe(spy) });\n plusBtn(f).click();\n expect(spy).toHaveBeenCalledTimes(1);\n });\n\n it('emits ZoomOut on a plain click of the − button', () => {\n const spy = vi.fn();\n const f = renderComponentFixture(RealtimeWhiteboardZoomComponent, { setup: (c) => c.ZoomOut.subscribe(spy) });\n minusBtn(f).click();\n expect(spy).toHaveBeenCalledTimes(1);\n });\n\n it('emits Fit when the fit-to-content button is clicked', () => {\n const spy = vi.fn();\n const f = renderComponentFixture(RealtimeWhiteboardZoomComponent, { setup: (c) => c.Fit.subscribe(spy) });\n fitBtn(f).click();\n expect(spy).toHaveBeenCalledTimes(1);\n });\n\n it('emits ToggleMinimap when the minimap button is clicked', () => {\n const spy = vi.fn();\n const f = renderComponentFixture(RealtimeWhiteboardZoomComponent, { setup: (c) => c.ToggleMinimap.subscribe(spy) });\n mapBtn(f).click();\n expect(spy).toHaveBeenCalledTimes(1);\n });\n});\n"]}
|
|
@@ -89,7 +89,7 @@ export class RealtimeWhiteboardZoomComponent {
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
static { this.ɵfac = function RealtimeWhiteboardZoomComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || RealtimeWhiteboardZoomComponent)(); }; }
|
|
92
|
-
static { this.ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: RealtimeWhiteboardZoomComponent, selectors: [["mj-realtime-whiteboard-zoom"]], inputs: { ZoomPercent: "ZoomPercent", MinimapOpen: "MinimapOpen" }, outputs: { ZoomOut: "ZoomOut", ZoomIn: "ZoomIn", ZoomBy: "ZoomBy", Fit: "Fit", ToggleMinimap: "ToggleMinimap" }, decls: 12, vars: 3, consts: [[1, "zoom-cluster"], ["type", "button", "title", "Zoom out (hold for continuous)", 3, "pointerdown", "pointerup", "pointerleave", "pointercancel", "click"], [1, "fa-solid", "fa-minus"], [1, "zoom-pct"], ["type", "button", "title", "Zoom in (hold for continuous)", 3, "pointerdown", "pointerup", "pointerleave", "pointercancel", "click"], [1, "fa-solid", "fa-plus"], [1, "zoom-sep"], ["type", "button", "title", "Fit to content", 3, "click"], [1, "fa-solid", "fa-compress"], ["type", "button", "title", "Mini-map", 3, "click"], [1, "fa-regular", "fa-map"]], template: function RealtimeWhiteboardZoomComponent_Template(rf, ctx) { if (rf & 1) {
|
|
92
|
+
static { this.ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: RealtimeWhiteboardZoomComponent, selectors: [["mj-realtime-whiteboard-zoom"]], inputs: { ZoomPercent: "ZoomPercent", MinimapOpen: "MinimapOpen" }, outputs: { ZoomOut: "ZoomOut", ZoomIn: "ZoomIn", ZoomBy: "ZoomBy", Fit: "Fit", ToggleMinimap: "ToggleMinimap" }, decls: 12, vars: 3, consts: [[1, "zoom-cluster"], ["type", "button", "title", "Zoom out (hold for continuous)", "aria-label", "Zoom out", 3, "pointerdown", "pointerup", "pointerleave", "pointercancel", "click"], ["aria-hidden", "true", 1, "fa-solid", "fa-minus"], [1, "zoom-pct"], ["type", "button", "title", "Zoom in (hold for continuous)", "aria-label", "Zoom in", 3, "pointerdown", "pointerup", "pointerleave", "pointercancel", "click"], ["aria-hidden", "true", 1, "fa-solid", "fa-plus"], [1, "zoom-sep"], ["type", "button", "title", "Fit to content", "aria-label", "Fit to content", 3, "click"], ["aria-hidden", "true", 1, "fa-solid", "fa-compress"], ["type", "button", "title", "Mini-map", "aria-label", "Toggle mini-map", 3, "click"], ["aria-hidden", "true", 1, "fa-regular", "fa-map"]], template: function RealtimeWhiteboardZoomComponent_Template(rf, ctx) { if (rf & 1) {
|
|
93
93
|
i0.ɵɵdomElementStart(0, "div", 0)(1, "button", 1);
|
|
94
94
|
i0.ɵɵdomListener("pointerdown", function RealtimeWhiteboardZoomComponent_Template_button_pointerdown_1_listener($event) { return ctx.OnZoomPressStart($event, -1); })("pointerup", function RealtimeWhiteboardZoomComponent_Template_button_pointerup_1_listener() { return ctx.OnZoomPressEnd(); })("pointerleave", function RealtimeWhiteboardZoomComponent_Template_button_pointerleave_1_listener() { return ctx.OnZoomPressEnd(); })("pointercancel", function RealtimeWhiteboardZoomComponent_Template_button_pointercancel_1_listener() { return ctx.OnZoomPressEnd(); })("click", function RealtimeWhiteboardZoomComponent_Template_button_click_1_listener() { return ctx.OnZoomClick(-1); });
|
|
95
95
|
i0.ɵɵdomElement(2, "i", 2);
|
|
@@ -119,7 +119,7 @@ export class RealtimeWhiteboardZoomComponent {
|
|
|
119
119
|
}
|
|
120
120
|
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(RealtimeWhiteboardZoomComponent, [{
|
|
121
121
|
type: Component,
|
|
122
|
-
args: [{ standalone: true, selector: 'mj-realtime-whiteboard-zoom', imports: [CommonModule], template: "<div class=\"zoom-cluster\">\n <button type=\"button\" title=\"Zoom out (hold for continuous)\"\n (pointerdown)=\"OnZoomPressStart($event, -1)\"\n (pointerup)=\"OnZoomPressEnd()\"\n (pointerleave)=\"OnZoomPressEnd()\"\n (pointercancel)=\"OnZoomPressEnd()\"\n (click)=\"OnZoomClick(-1)\"><i class=\"fa-solid fa-minus\"></i></button>\n <span class=\"zoom-pct\">{{ ZoomPercent }}%</span>\n <button type=\"button\" title=\"Zoom in (hold for continuous)\"\n (pointerdown)=\"OnZoomPressStart($event, 1)\"\n (pointerup)=\"OnZoomPressEnd()\"\n (pointerleave)=\"OnZoomPressEnd()\"\n (pointercancel)=\"OnZoomPressEnd()\"\n (click)=\"OnZoomClick(1)\"><i class=\"fa-solid fa-plus\"></i></button>\n <span class=\"zoom-sep\"></span>\n <button type=\"button\" title=\"Fit to content\" (click)=\"Fit.emit()\"><i class=\"fa-solid fa-compress\"></i></button>\n <button type=\"button\" title=\"Mini-map\" [class.on]=\"MinimapOpen\" (click)=\"ToggleMinimap.emit()\"><i class=\"fa-regular fa-map\"></i></button>\n</div>\n", styles: ["/* Bottom-right zoom cluster \u2014 ported pixel-faithfully from\n plans/realtime/mockups/whiteboard.html (.zoom-cluster). */\n\n:host {\n display: block;\n position: absolute;\n right: 14px;\n bottom: 14px;\n z-index: 10;\n}\n\n.zoom-cluster {\n display: flex;\n align-items: center;\n gap: 2px;\n background: var(--mj-bg-surface-card);\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-full, 9999px);\n box-shadow: var(--mj-shadow-xl, 0 20px 25px -5px rgba(0, 0, 0, 0.3));\n padding: 4px;\n}\n\n.zoom-cluster button {\n width: 30px;\n height: 30px;\n border-radius: 50%;\n border: 0;\n background: transparent;\n color: var(--mj-text-secondary);\n cursor: pointer;\n font-size: 12px;\n display: grid;\n place-items: center;\n font-family: inherit;\n}\n.zoom-cluster button:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n}\n.zoom-cluster button.on {\n background: color-mix(in srgb, var(--mj-brand-primary) 14%, transparent);\n color: var(--mj-brand-accent, var(--mj-brand-primary));\n}\n\n.zoom-pct {\n font-size: 11.5px;\n font-weight: 700;\n color: var(--mj-text-secondary);\n padding: 0 7px;\n font-family: var(--mj-font-mono, ui-monospace, monospace);\n}\n\n.zoom-sep {\n width: 1px;\n height: 18px;\n background: var(--mj-border-default);\n margin: 0 3px;\n}\n"] }]
|
|
122
|
+
args: [{ standalone: true, selector: 'mj-realtime-whiteboard-zoom', imports: [CommonModule], template: "<div class=\"zoom-cluster\">\n <button type=\"button\" title=\"Zoom out (hold for continuous)\" aria-label=\"Zoom out\"\n (pointerdown)=\"OnZoomPressStart($event, -1)\"\n (pointerup)=\"OnZoomPressEnd()\"\n (pointerleave)=\"OnZoomPressEnd()\"\n (pointercancel)=\"OnZoomPressEnd()\"\n (click)=\"OnZoomClick(-1)\"><i class=\"fa-solid fa-minus\" aria-hidden=\"true\"></i></button>\n <span class=\"zoom-pct\">{{ ZoomPercent }}%</span>\n <button type=\"button\" title=\"Zoom in (hold for continuous)\" aria-label=\"Zoom in\"\n (pointerdown)=\"OnZoomPressStart($event, 1)\"\n (pointerup)=\"OnZoomPressEnd()\"\n (pointerleave)=\"OnZoomPressEnd()\"\n (pointercancel)=\"OnZoomPressEnd()\"\n (click)=\"OnZoomClick(1)\"><i class=\"fa-solid fa-plus\" aria-hidden=\"true\"></i></button>\n <span class=\"zoom-sep\"></span>\n <button type=\"button\" title=\"Fit to content\" aria-label=\"Fit to content\" (click)=\"Fit.emit()\"><i class=\"fa-solid fa-compress\" aria-hidden=\"true\"></i></button>\n <button type=\"button\" title=\"Mini-map\" aria-label=\"Toggle mini-map\" [class.on]=\"MinimapOpen\" (click)=\"ToggleMinimap.emit()\"><i class=\"fa-regular fa-map\" aria-hidden=\"true\"></i></button>\n</div>\n", styles: ["/* Bottom-right zoom cluster \u2014 ported pixel-faithfully from\n plans/realtime/mockups/whiteboard.html (.zoom-cluster). */\n\n:host {\n display: block;\n position: absolute;\n right: 14px;\n bottom: 14px;\n z-index: 10;\n}\n\n.zoom-cluster {\n display: flex;\n align-items: center;\n gap: 2px;\n background: var(--mj-bg-surface-card);\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-full, 9999px);\n box-shadow: var(--mj-shadow-xl, 0 20px 25px -5px rgba(0, 0, 0, 0.3));\n padding: 4px;\n}\n\n.zoom-cluster button {\n width: 30px;\n height: 30px;\n border-radius: 50%;\n border: 0;\n background: transparent;\n color: var(--mj-text-secondary);\n cursor: pointer;\n font-size: 12px;\n display: grid;\n place-items: center;\n font-family: inherit;\n}\n.zoom-cluster button:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n}\n.zoom-cluster button.on {\n background: color-mix(in srgb, var(--mj-brand-primary) 14%, transparent);\n color: var(--mj-brand-accent, var(--mj-brand-primary));\n}\n\n.zoom-pct {\n font-size: 11.5px;\n font-weight: 700;\n color: var(--mj-text-secondary);\n padding: 0 7px;\n font-family: var(--mj-font-mono, ui-monospace, monospace);\n}\n\n.zoom-sep {\n width: 1px;\n height: 18px;\n background: var(--mj-border-default);\n margin: 0 3px;\n}\n"] }]
|
|
123
123
|
}], null, { ZoomPercent: [{
|
|
124
124
|
type: Input
|
|
125
125
|
}], MinimapOpen: [{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"whiteboard-zoom.component.js","sourceRoot":"","sources":["../../src/lib/whiteboard-zoom.component.ts","../../src/lib/whiteboard-zoom.component.html"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"whiteboard-zoom.component.js","sourceRoot":"","sources":["../../src/lib/whiteboard-zoom.component.ts","../../src/lib/whiteboard-zoom.component.html"],"names":[],"mappings":"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","sourcesContent":["import { Component, EventEmitter, Input, OnDestroy, Output } from '@angular/core';\nimport { CommonModule } from '@angular/common';\n\n/** Which way a zoom button points. */\ntype ZoomDirection = 1 | -1;\n\n/**\n * The bottom-right zoom cluster of the live whiteboard: zoom out / percentage / zoom in,\n * fit-to-content, and the minimap toggle. Pure presentational; the board owns the\n * viewport math and the minimap rendering.\n *\n * HOLD-TO-ZOOM: a plain click on +/− still emits the existing single preset step\n * ({@link ZoomIn} / {@link ZoomOut}); holding the button down (pointerdown held past a\n * short delay) emits {@link ZoomBy} factors (~3.5%) every 50 ms for smooth continuous\n * zooming, stopping on pointerup / pointerleave / pointercancel / destroy — the timers\n * never leak. The board clamps to its usual 25%–200% limits.\n */\n@Component({\n standalone: true,\n selector: 'mj-realtime-whiteboard-zoom',\n imports: [CommonModule],\n templateUrl: './whiteboard-zoom.component.html',\n styleUrl: './whiteboard-zoom.component.css'\n})\nexport class RealtimeWhiteboardZoomComponent implements OnDestroy {\n /** How long a press must be held before continuous zooming kicks in. */\n private static readonly HoldDelayMs = 350;\n /** Continuous-zoom tick cadence while held. */\n private static readonly HoldIntervalMs = 50;\n /** Multiplicative zoom factor per held tick (~3.5%). */\n private static readonly HoldFactor = 1.035;\n\n /** Current zoom as a percentage (e.g. 90). */\n @Input() ZoomPercent = 100;\n /** Whether the minimap is currently shown (highlights the map button). */\n @Input() MinimapOpen = false;\n\n /** Single preset step out (a plain click on −). */\n @Output() ZoomOut = new EventEmitter<void>();\n /** Single preset step in (a plain click on +). */\n @Output() ZoomIn = new EventEmitter<void>();\n /**\n * Continuous (held-button) zoom tick: a multiplicative factor (>1 zooms in, <1 zooms\n * out) emitted every 50 ms while + or − is held. Wire to the board's `ZoomByFactor`.\n */\n @Output() ZoomBy = new EventEmitter<number>();\n @Output() Fit = new EventEmitter<void>();\n @Output() ToggleMinimap = new EventEmitter<void>();\n\n private holdDelayTimer: ReturnType<typeof setTimeout> | null = null;\n private holdIntervalTimer: ReturnType<typeof setInterval> | null = null;\n /** Set once continuous ticks started, so the trailing click doesn't ALSO step. */\n private suppressNextClick = false;\n\n ngOnDestroy(): void {\n this.stopHold();\n }\n\n /** Pointer down on +/−: arm the hold — after the delay, tick {@link ZoomBy} repeatedly. */\n public OnZoomPressStart(event: PointerEvent, direction: ZoomDirection): void {\n if (event.button !== 0) {\n return;\n }\n this.stopHold();\n this.suppressNextClick = false;\n this.holdDelayTimer = setTimeout(() => {\n this.holdDelayTimer = null;\n this.suppressNextClick = true;\n const factor = direction === 1\n ? RealtimeWhiteboardZoomComponent.HoldFactor\n : 1 / RealtimeWhiteboardZoomComponent.HoldFactor;\n this.ZoomBy.emit(factor); // first tick fires immediately when the hold engages\n this.holdIntervalTimer = setInterval(() => this.ZoomBy.emit(factor), RealtimeWhiteboardZoomComponent.HoldIntervalMs);\n }, RealtimeWhiteboardZoomComponent.HoldDelayMs);\n }\n\n /** Pointer up / leave / cancel: stop any pending or running continuous zoom. */\n public OnZoomPressEnd(): void {\n this.stopHold();\n }\n\n /** The click that follows the press: a single preset step unless a hold already zoomed. */\n public OnZoomClick(direction: ZoomDirection): void {\n if (this.suppressNextClick) {\n this.suppressNextClick = false;\n return;\n }\n if (direction === 1) {\n this.ZoomIn.emit();\n }\n else {\n this.ZoomOut.emit();\n }\n }\n\n /** Clear both hold timers (idempotent — safe from any stop path). */\n private stopHold(): void {\n if (this.holdDelayTimer !== null) {\n clearTimeout(this.holdDelayTimer);\n this.holdDelayTimer = null;\n }\n if (this.holdIntervalTimer !== null) {\n clearInterval(this.holdIntervalTimer);\n this.holdIntervalTimer = null;\n }\n }\n}\n","<div class=\"zoom-cluster\">\n <button type=\"button\" title=\"Zoom out (hold for continuous)\" aria-label=\"Zoom out\"\n (pointerdown)=\"OnZoomPressStart($event, -1)\"\n (pointerup)=\"OnZoomPressEnd()\"\n (pointerleave)=\"OnZoomPressEnd()\"\n (pointercancel)=\"OnZoomPressEnd()\"\n (click)=\"OnZoomClick(-1)\"><i class=\"fa-solid fa-minus\" aria-hidden=\"true\"></i></button>\n <span class=\"zoom-pct\">{{ ZoomPercent }}%</span>\n <button type=\"button\" title=\"Zoom in (hold for continuous)\" aria-label=\"Zoom in\"\n (pointerdown)=\"OnZoomPressStart($event, 1)\"\n (pointerup)=\"OnZoomPressEnd()\"\n (pointerleave)=\"OnZoomPressEnd()\"\n (pointercancel)=\"OnZoomPressEnd()\"\n (click)=\"OnZoomClick(1)\"><i class=\"fa-solid fa-plus\" aria-hidden=\"true\"></i></button>\n <span class=\"zoom-sep\"></span>\n <button type=\"button\" title=\"Fit to content\" aria-label=\"Fit to content\" (click)=\"Fit.emit()\"><i class=\"fa-solid fa-compress\" aria-hidden=\"true\"></i></button>\n <button type=\"button\" title=\"Mini-map\" aria-label=\"Toggle mini-map\" [class.on]=\"MinimapOpen\" (click)=\"ToggleMinimap.emit()\"><i class=\"fa-regular fa-map\" aria-hidden=\"true\"></i></button>\n</div>\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memberjunction/ng-whiteboard",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.45.0",
|
|
4
4
|
"description": "MemberJunction: Generic collaborative whiteboard for any Angular application — Angular-free state engine with undo/redo + change journal, agent perception deltas, a programmatic (AI-agent-ready) mutation tool API, sandboxed HTML widgets with an input bridge, markdown panels, and self-contained HTML/SVG export",
|
|
5
5
|
"main": "./dist/public-api.js",
|
|
6
6
|
"typings": "./dist/public-api.d.ts",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@angular/compiler": "21.1.3",
|
|
27
27
|
"@angular/compiler-cli": "21.1.3",
|
|
28
|
+
"@memberjunction/ng-test-utils": "5.45.0",
|
|
28
29
|
"vitest": "^4.0.18"
|
|
29
30
|
},
|
|
30
31
|
"peerDependencies": {
|
|
@@ -33,9 +34,10 @@
|
|
|
33
34
|
"@angular/platform-browser": "21.1.3"
|
|
34
35
|
},
|
|
35
36
|
"dependencies": {
|
|
36
|
-
"@memberjunction/
|
|
37
|
-
"@memberjunction/
|
|
38
|
-
"@memberjunction/ng-
|
|
37
|
+
"@memberjunction/ng-ui-components": "5.45.0",
|
|
38
|
+
"@memberjunction/global": "5.45.0",
|
|
39
|
+
"@memberjunction/ng-code-editor": "5.45.0",
|
|
40
|
+
"@memberjunction/ng-markdown": "5.45.0",
|
|
39
41
|
"rxjs": "^7.8.2",
|
|
40
42
|
"tslib": "^2.8.1"
|
|
41
43
|
},
|