@svgrid/enterprise 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Tests for the unlicensed soft-gate watermark + console nudge.
3
+ * - The watermark reads "www.svgrid.com" and sits in the bottom-right of the
4
+ * grid root.
5
+ * - It fades out and is removed after 5 seconds.
6
+ * - The console nudge names the commercial license + sales contact.
7
+ */
8
+
9
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
10
+ import { emitUnlicensedNudge, dismissUnlicensedNudge } from './watermark'
11
+
12
+ const WATERMARK_ATTR = 'data-@svgrid/enterprise-watermark'
13
+
14
+ function makeGridRoot(): HTMLElement {
15
+ const el = document.createElement('div')
16
+ el.className = 'sv-grid-root'
17
+ document.body.appendChild(el)
18
+ return el
19
+ }
20
+
21
+ function flushMicrotasks() {
22
+ return Promise.resolve()
23
+ }
24
+
25
+ describe('unlicensed watermark', () => {
26
+ beforeEach(() => {
27
+ vi.useFakeTimers()
28
+ })
29
+
30
+ afterEach(() => {
31
+ dismissUnlicensedNudge()
32
+ vi.useRealTimers()
33
+ document.body.innerHTML = ''
34
+ })
35
+
36
+ it('attaches a "www.svgrid.com" watermark to the grid bottom-right', async () => {
37
+ const grid = makeGridRoot()
38
+ emitUnlicensedNudge()
39
+ await flushMicrotasks()
40
+
41
+ const mark = grid.querySelector(`[${WATERMARK_ATTR}]`) as HTMLAnchorElement | null
42
+ expect(mark).not.toBeNull()
43
+ expect(mark!.textContent).toBe('www.svgrid.com')
44
+ expect(mark!.href).toContain('svgrid.com')
45
+ expect(mark!.style.position).toBe('absolute')
46
+ expect(mark!.style.bottom).toBe('6px')
47
+ expect(mark!.style.right).toBe('8px')
48
+ // The grid gets a positioning context so the absolute watermark anchors.
49
+ expect(grid.style.position).toBe('relative')
50
+ })
51
+
52
+ it('fades out and removes the watermark after ~5 seconds', async () => {
53
+ const grid = makeGridRoot()
54
+ emitUnlicensedNudge()
55
+ await flushMicrotasks()
56
+
57
+ const mark = grid.querySelector(`[${WATERMARK_ATTR}]`) as HTMLElement
58
+ expect(mark.style.opacity).toBe('1')
59
+
60
+ // After 5s the fade starts...
61
+ vi.advanceTimersByTime(5000)
62
+ expect(mark.style.opacity).toBe('0')
63
+
64
+ // ...and after the fade transition the node is gone.
65
+ vi.advanceTimersByTime(700)
66
+ expect(grid.querySelector(`[${WATERMARK_ATTR}]`)).toBeNull()
67
+ })
68
+
69
+ it('does not re-add the watermark after it has faded out', async () => {
70
+ const grid = makeGridRoot()
71
+ emitUnlicensedNudge()
72
+ await flushMicrotasks()
73
+ vi.advanceTimersByTime(5700)
74
+ expect(grid.querySelector(`[${WATERMARK_ATTR}]`)).toBeNull()
75
+
76
+ // A later DOM mutation must not bring it back (grid is marked "nudged").
77
+ document.body.appendChild(document.createElement('span'))
78
+ await flushMicrotasks()
79
+ expect(grid.querySelector(`[${WATERMARK_ATTR}]`)).toBeNull()
80
+ })
81
+
82
+ it('logs a one-time commercial-license console nudge', async () => {
83
+ const spy = vi.spyOn(console, 'log').mockImplementation(() => {})
84
+ try {
85
+ emitUnlicensedNudge()
86
+ const msg = spy.mock.calls.map((c) => String(c[0])).join('\n')
87
+ expect(msg).toContain('Commercial version which requires a license')
88
+ expect(msg).toContain('sales@jqwidgets.com')
89
+ // One-time: a second call does not log again.
90
+ const callsAfterFirst = spy.mock.calls.length
91
+ emitUnlicensedNudge()
92
+ expect(spy.mock.calls.length).toBe(callsAfterFirst)
93
+ } finally {
94
+ spy.mockRestore()
95
+ }
96
+ })
97
+ })
@@ -0,0 +1,156 @@
1
+ // Soft-gate UX for unlicensed @svgrid/enterprise use. Shows a small clickable
2
+ // "www.svgrid.com" watermark in the bottom-right corner of every SvGrid on
3
+ // the page, which fades out after 5 seconds, and emits a one-time console
4
+ // nudge. Hard errors (revoked / bad-prefix keys) still throw via license.ts -
5
+ // this file is only for the "no key set" case where the feature continues to
6
+ // work but the user gets nudged.
7
+
8
+ const WATERMARK_ATTR = 'data-@svgrid/enterprise-watermark'
9
+ // Marks a grid that has already shown (and faded) its watermark, so the
10
+ // MutationObserver doesn't re-add it once it fades out and is removed.
11
+ const NUDGED_ATTR = 'data-@svgrid/enterprise-nudged'
12
+ const HOMEPAGE_URL = 'https://www.svgrid.com'
13
+ const LICENSE_URL = 'https://www.svgrid.com/pricing'
14
+ const WATERMARK_LABEL = 'www.svgrid.com'
15
+
16
+ // How long the watermark stays fully visible before it starts fading, and how
17
+ // long the fade itself takes.
18
+ const VISIBLE_MS = 5000
19
+ const FADE_MS = 700
20
+
21
+ let consoleNudgeShown = false
22
+ let observer: MutationObserver | null = null
23
+
24
+ export function emitUnlicensedNudge(): void {
25
+ showConsoleOnce()
26
+ if (typeof document === 'undefined' || typeof window === 'undefined') return
27
+ // Defer one tick: installPro may run inside onApiReady before the grid's
28
+ // root element is in the DOM. The microtask gives Svelte time to flush.
29
+ queueMicrotask(() => attachToAllGrids())
30
+ // Watch for grids that mount later (route changes, dynamic loads).
31
+ if (!observer) {
32
+ observer = new MutationObserver(() => attachToAllGrids())
33
+ observer.observe(document.body, { childList: true, subtree: true })
34
+ }
35
+ }
36
+
37
+ export function dismissUnlicensedNudge(): void {
38
+ if (observer) {
39
+ observer.disconnect()
40
+ observer = null
41
+ }
42
+ if (typeof document === 'undefined') return
43
+ document.querySelectorAll(`[${WATERMARK_ATTR}]`).forEach((el) => el.remove())
44
+ document
45
+ .querySelectorAll(`[${NUDGED_ATTR}]`)
46
+ .forEach((el) => el.removeAttribute(NUDGED_ATTR))
47
+ consoleNudgeShown = false
48
+ }
49
+
50
+ function showConsoleOnce(): void {
51
+ if (consoleNudgeShown) return
52
+ consoleNudgeShown = true
53
+ if (typeof console === 'undefined') return
54
+ // Per spec we use console.log (not warn/error) - this is a nudge, not an
55
+ // error condition. Styled with %c so it stands out in the devtools.
56
+ console.log(
57
+ '%c @svgrid/enterprise %c User is using a Commercial version which requires a license.\n' +
58
+ 'Contact sales@jqwidgets.com for details or visit the license page: ' +
59
+ LICENSE_URL,
60
+ 'background:#4f46e5;color:#fff;padding:2px 6px;border-radius:3px;font-weight:600',
61
+ 'color:inherit',
62
+ )
63
+ }
64
+
65
+ function attachToAllGrids(): void {
66
+ if (typeof document === 'undefined') return
67
+ // Primary selector targets SvGrid's root container class. Fall back to
68
+ // ARIA grid role for headless renderers that re-style the wrapper.
69
+ const grids = document.querySelectorAll<HTMLElement>(
70
+ '.sv-grid-root, [role="grid"]',
71
+ )
72
+ if (grids.length === 0) {
73
+ ensureFixedFallback()
74
+ return
75
+ }
76
+ // A grid root was found - remove any fixed fallback we may have installed
77
+ // earlier before the grid mounted.
78
+ document
79
+ .querySelectorAll(`[${WATERMARK_ATTR}="fixed"]`)
80
+ .forEach((el) => el.remove())
81
+ grids.forEach(attachWatermarkTo)
82
+ }
83
+
84
+ function attachWatermarkTo(grid: HTMLElement): void {
85
+ // Show the watermark once per grid. The marker survives the fade-out so the
86
+ // MutationObserver doesn't keep re-adding it on every DOM change.
87
+ if (grid.hasAttribute(NUDGED_ATTR)) return
88
+ grid.setAttribute(NUDGED_ATTR, '1')
89
+ const cs = getComputedStyle(grid)
90
+ if (cs.position === 'static') {
91
+ // Need a positioning context for absolute positioning. Only override when
92
+ // 'static' (the browser default) - don't clobber an explicit
93
+ // relative/absolute the consumer chose.
94
+ grid.style.position = 'relative'
95
+ }
96
+ const a = buildWatermark('grid')
97
+ applyWatermarkStyles(a, /* fixed */ false)
98
+ grid.appendChild(a)
99
+ scheduleFadeOut(a)
100
+ }
101
+
102
+ function ensureFixedFallback(): void {
103
+ if (typeof document === 'undefined') return
104
+ if (document.querySelector(`[${WATERMARK_ATTR}="fixed"]`)) return
105
+ const a = buildWatermark('fixed')
106
+ applyWatermarkStyles(a, /* fixed */ true)
107
+ document.body.appendChild(a)
108
+ scheduleFadeOut(a)
109
+ }
110
+
111
+ function buildWatermark(kind: 'grid' | 'fixed'): HTMLAnchorElement {
112
+ const a = document.createElement('a')
113
+ a.setAttribute(WATERMARK_ATTR, kind)
114
+ a.href = HOMEPAGE_URL
115
+ a.target = '_blank'
116
+ a.rel = 'noopener noreferrer'
117
+ a.textContent = WATERMARK_LABEL
118
+ a.title =
119
+ 'Unlicensed @svgrid/enterprise. Contact sales@jqwidgets.com or visit www.svgrid.com.'
120
+ return a
121
+ }
122
+
123
+ /** Keep the watermark visible for VISIBLE_MS, then fade it out and remove it. */
124
+ function scheduleFadeOut(el: HTMLElement): void {
125
+ if (typeof window === 'undefined') {
126
+ el.remove()
127
+ return
128
+ }
129
+ window.setTimeout(() => {
130
+ el.style.opacity = '0'
131
+ window.setTimeout(() => el.remove(), FADE_MS)
132
+ }, VISIBLE_MS)
133
+ }
134
+
135
+ function applyWatermarkStyles(el: HTMLAnchorElement, fixed: boolean): void {
136
+ Object.assign(el.style, {
137
+ position: fixed ? 'fixed' : 'absolute',
138
+ bottom: fixed ? '12px' : '6px',
139
+ right: fixed ? '12px' : '8px',
140
+ zIndex: '2147483647',
141
+ background: 'rgba(15,23,42,0.85)',
142
+ color: '#fff',
143
+ padding: fixed ? '6px 10px' : '4px 8px',
144
+ borderRadius: fixed ? '4px' : '3px',
145
+ fontFamily: '-apple-system, "Segoe UI", Roboto, sans-serif',
146
+ fontSize: fixed ? '11px' : '10px',
147
+ lineHeight: '1.2',
148
+ textDecoration: 'none',
149
+ boxShadow: fixed ? '0 2px 12px rgba(0,0,0,0.35)' : '0 1px 4px rgba(0,0,0,0.3)',
150
+ border: '1px solid rgba(255,255,255,0.18)',
151
+ pointerEvents: 'auto',
152
+ letterSpacing: '0.02em',
153
+ opacity: '1',
154
+ transition: `opacity ${FADE_MS}ms ease`,
155
+ } as Partial<CSSStyleDeclaration> as CSSStyleDeclaration)
156
+ }