@teleporthq/teleport-project-generator-next 0.43.21 → 0.43.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/__tests__/analytics-tracker-double-count.test.ts +47 -0
  2. package/__tests__/calendarkit-project-plugin.test.ts +5 -0
  3. package/__tests__/motion-end2end.test.ts +82 -0
  4. package/__tests__/widget-project-plugins.test.ts +252 -0
  5. package/dist/cjs/analytics/tracker-component.d.ts +1 -1
  6. package/dist/cjs/analytics/tracker-component.d.ts.map +1 -1
  7. package/dist/cjs/analytics/tracker-component.js +1 -1
  8. package/dist/cjs/analytics/tracker-component.js.map +1 -1
  9. package/dist/cjs/analytics/tracker-source.d.ts +1 -1
  10. package/dist/cjs/analytics/tracker-source.d.ts.map +1 -1
  11. package/dist/cjs/analytics/tracker-source.js +1 -1
  12. package/dist/cjs/analytics/tracker-source.js.map +1 -1
  13. package/dist/cjs/calendar/project-plugin.d.ts +2 -0
  14. package/dist/cjs/calendar/project-plugin.d.ts.map +1 -1
  15. package/dist/cjs/calendar/project-plugin.js +4 -0
  16. package/dist/cjs/calendar/project-plugin.js.map +1 -1
  17. package/dist/cjs/countdown/component-generator.d.ts +15 -0
  18. package/dist/cjs/countdown/component-generator.d.ts.map +1 -0
  19. package/dist/cjs/countdown/component-generator.js +21 -0
  20. package/dist/cjs/countdown/component-generator.js.map +1 -0
  21. package/dist/cjs/countdown/project-plugin.d.ts +15 -0
  22. package/dist/cjs/countdown/project-plugin.d.ts.map +1 -0
  23. package/dist/cjs/countdown/project-plugin.js +89 -0
  24. package/dist/cjs/countdown/project-plugin.js.map +1 -0
  25. package/dist/cjs/index.d.ts +3 -0
  26. package/dist/cjs/index.d.ts.map +1 -1
  27. package/dist/cjs/index.js +31 -17
  28. package/dist/cjs/index.js.map +1 -1
  29. package/dist/cjs/kanban/project-plugin.d.ts.map +1 -1
  30. package/dist/cjs/kanban/project-plugin.js +5 -15
  31. package/dist/cjs/kanban/project-plugin.js.map +1 -1
  32. package/dist/cjs/local-component-path-plugin.d.ts +1 -1
  33. package/dist/cjs/local-component-path-plugin.d.ts.map +1 -1
  34. package/dist/cjs/local-component-path-plugin.js +8 -1
  35. package/dist/cjs/local-component-path-plugin.js.map +1 -1
  36. package/dist/cjs/next-project-mapping.d.ts.map +1 -1
  37. package/dist/cjs/next-project-mapping.js +49 -0
  38. package/dist/cjs/next-project-mapping.js.map +1 -1
  39. package/dist/cjs/npmrc-legacy-peer-deps.d.ts +22 -0
  40. package/dist/cjs/npmrc-legacy-peer-deps.d.ts.map +1 -0
  41. package/dist/cjs/npmrc-legacy-peer-deps.js +35 -0
  42. package/dist/cjs/npmrc-legacy-peer-deps.js.map +1 -0
  43. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  44. package/dist/cjs/url-search-params-plugin.d.ts.map +1 -1
  45. package/dist/cjs/url-search-params-plugin.js +6 -154
  46. package/dist/cjs/url-search-params-plugin.js.map +1 -1
  47. package/dist/cjs/widgets/barcode-component.d.ts +7 -0
  48. package/dist/cjs/widgets/barcode-component.d.ts.map +1 -0
  49. package/dist/cjs/widgets/barcode-component.js +13 -0
  50. package/dist/cjs/widgets/barcode-component.js.map +1 -0
  51. package/dist/cjs/widgets/color-picker-component.d.ts +10 -0
  52. package/dist/cjs/widgets/color-picker-component.d.ts.map +1 -0
  53. package/dist/cjs/widgets/color-picker-component.js +16 -0
  54. package/dist/cjs/widgets/color-picker-component.js.map +1 -0
  55. package/dist/cjs/widgets/emoji-picker-component.d.ts +10 -0
  56. package/dist/cjs/widgets/emoji-picker-component.d.ts.map +1 -0
  57. package/dist/cjs/widgets/emoji-picker-component.js +16 -0
  58. package/dist/cjs/widgets/emoji-picker-component.js.map +1 -0
  59. package/dist/cjs/widgets/index.d.ts +7 -0
  60. package/dist/cjs/widgets/index.d.ts.map +1 -0
  61. package/dist/cjs/widgets/index.js +70 -0
  62. package/dist/cjs/widgets/index.js.map +1 -0
  63. package/dist/cjs/widgets/motion-component.d.ts +19 -0
  64. package/dist/cjs/widgets/motion-component.d.ts.map +1 -0
  65. package/dist/cjs/widgets/motion-component.js +25 -0
  66. package/dist/cjs/widgets/motion-component.js.map +1 -0
  67. package/dist/cjs/widgets/project-plugin-factory.d.ts +32 -0
  68. package/dist/cjs/widgets/project-plugin-factory.d.ts.map +1 -0
  69. package/dist/cjs/widgets/project-plugin-factory.js +98 -0
  70. package/dist/cjs/widgets/project-plugin-factory.js.map +1 -0
  71. package/dist/cjs/widgets/qrcode-component.d.ts +7 -0
  72. package/dist/cjs/widgets/qrcode-component.d.ts.map +1 -0
  73. package/dist/cjs/widgets/qrcode-component.js +13 -0
  74. package/dist/cjs/widgets/qrcode-component.js.map +1 -0
  75. package/dist/cjs/widgets/signature-component.d.ts +10 -0
  76. package/dist/cjs/widgets/signature-component.d.ts.map +1 -0
  77. package/dist/cjs/widgets/signature-component.js +16 -0
  78. package/dist/cjs/widgets/signature-component.js.map +1 -0
  79. package/dist/esm/analytics/tracker-component.d.ts +1 -1
  80. package/dist/esm/analytics/tracker-component.d.ts.map +1 -1
  81. package/dist/esm/analytics/tracker-component.js +1 -1
  82. package/dist/esm/analytics/tracker-component.js.map +1 -1
  83. package/dist/esm/analytics/tracker-source.d.ts +1 -1
  84. package/dist/esm/analytics/tracker-source.d.ts.map +1 -1
  85. package/dist/esm/analytics/tracker-source.js +1 -1
  86. package/dist/esm/analytics/tracker-source.js.map +1 -1
  87. package/dist/esm/calendar/project-plugin.d.ts +2 -0
  88. package/dist/esm/calendar/project-plugin.d.ts.map +1 -1
  89. package/dist/esm/calendar/project-plugin.js +4 -0
  90. package/dist/esm/calendar/project-plugin.js.map +1 -1
  91. package/dist/esm/countdown/component-generator.d.ts +15 -0
  92. package/dist/esm/countdown/component-generator.d.ts.map +1 -0
  93. package/dist/esm/countdown/component-generator.js +17 -0
  94. package/dist/esm/countdown/component-generator.js.map +1 -0
  95. package/dist/esm/countdown/project-plugin.d.ts +15 -0
  96. package/dist/esm/countdown/project-plugin.d.ts.map +1 -0
  97. package/dist/esm/countdown/project-plugin.js +86 -0
  98. package/dist/esm/countdown/project-plugin.js.map +1 -0
  99. package/dist/esm/index.d.ts +3 -0
  100. package/dist/esm/index.d.ts.map +1 -1
  101. package/dist/esm/index.js +11 -0
  102. package/dist/esm/index.js.map +1 -1
  103. package/dist/esm/kanban/project-plugin.d.ts.map +1 -1
  104. package/dist/esm/kanban/project-plugin.js +5 -15
  105. package/dist/esm/kanban/project-plugin.js.map +1 -1
  106. package/dist/esm/local-component-path-plugin.d.ts +1 -1
  107. package/dist/esm/local-component-path-plugin.d.ts.map +1 -1
  108. package/dist/esm/local-component-path-plugin.js +8 -1
  109. package/dist/esm/local-component-path-plugin.js.map +1 -1
  110. package/dist/esm/next-project-mapping.d.ts.map +1 -1
  111. package/dist/esm/next-project-mapping.js +49 -0
  112. package/dist/esm/next-project-mapping.js.map +1 -1
  113. package/dist/esm/npmrc-legacy-peer-deps.d.ts +22 -0
  114. package/dist/esm/npmrc-legacy-peer-deps.d.ts.map +1 -0
  115. package/dist/esm/npmrc-legacy-peer-deps.js +31 -0
  116. package/dist/esm/npmrc-legacy-peer-deps.js.map +1 -0
  117. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  118. package/dist/esm/url-search-params-plugin.d.ts.map +1 -1
  119. package/dist/esm/url-search-params-plugin.js +6 -154
  120. package/dist/esm/url-search-params-plugin.js.map +1 -1
  121. package/dist/esm/widgets/barcode-component.d.ts +7 -0
  122. package/dist/esm/widgets/barcode-component.d.ts.map +1 -0
  123. package/dist/esm/widgets/barcode-component.js +9 -0
  124. package/dist/esm/widgets/barcode-component.js.map +1 -0
  125. package/dist/esm/widgets/color-picker-component.d.ts +10 -0
  126. package/dist/esm/widgets/color-picker-component.d.ts.map +1 -0
  127. package/dist/esm/widgets/color-picker-component.js +12 -0
  128. package/dist/esm/widgets/color-picker-component.js.map +1 -0
  129. package/dist/esm/widgets/emoji-picker-component.d.ts +10 -0
  130. package/dist/esm/widgets/emoji-picker-component.d.ts.map +1 -0
  131. package/dist/esm/widgets/emoji-picker-component.js +12 -0
  132. package/dist/esm/widgets/emoji-picker-component.js.map +1 -0
  133. package/dist/esm/widgets/index.d.ts +7 -0
  134. package/dist/esm/widgets/index.d.ts.map +1 -0
  135. package/dist/esm/widgets/index.js +66 -0
  136. package/dist/esm/widgets/index.js.map +1 -0
  137. package/dist/esm/widgets/motion-component.d.ts +19 -0
  138. package/dist/esm/widgets/motion-component.d.ts.map +1 -0
  139. package/dist/esm/widgets/motion-component.js +21 -0
  140. package/dist/esm/widgets/motion-component.js.map +1 -0
  141. package/dist/esm/widgets/project-plugin-factory.d.ts +32 -0
  142. package/dist/esm/widgets/project-plugin-factory.d.ts.map +1 -0
  143. package/dist/esm/widgets/project-plugin-factory.js +94 -0
  144. package/dist/esm/widgets/project-plugin-factory.js.map +1 -0
  145. package/dist/esm/widgets/qrcode-component.d.ts +7 -0
  146. package/dist/esm/widgets/qrcode-component.d.ts.map +1 -0
  147. package/dist/esm/widgets/qrcode-component.js +9 -0
  148. package/dist/esm/widgets/qrcode-component.js.map +1 -0
  149. package/dist/esm/widgets/signature-component.d.ts +10 -0
  150. package/dist/esm/widgets/signature-component.d.ts.map +1 -0
  151. package/dist/esm/widgets/signature-component.js +12 -0
  152. package/dist/esm/widgets/signature-component.js.map +1 -0
  153. package/package.json +19 -19
  154. package/src/analytics/tracker-component.ts +2 -2
  155. package/src/analytics/tracker-source.ts +35 -3
  156. package/src/calendar/project-plugin.ts +4 -0
  157. package/src/countdown/component-generator.ts +168 -0
  158. package/src/countdown/project-plugin.ts +43 -0
  159. package/src/index.ts +11 -0
  160. package/src/kanban/project-plugin.ts +5 -16
  161. package/src/local-component-path-plugin.ts +8 -1
  162. package/src/next-project-mapping.ts +49 -0
  163. package/src/npmrc-legacy-peer-deps.ts +36 -0
  164. package/src/url-search-params-plugin.ts +12 -250
  165. package/src/widgets/barcode-component.ts +53 -0
  166. package/src/widgets/color-picker-component.ts +93 -0
  167. package/src/widgets/emoji-picker-component.ts +61 -0
  168. package/src/widgets/index.ts +68 -0
  169. package/src/widgets/motion-component.ts +205 -0
  170. package/src/widgets/project-plugin-factory.ts +80 -0
  171. package/src/widgets/qrcode-component.ts +82 -0
  172. package/src/widgets/signature-component.ts +97 -0
@@ -0,0 +1,47 @@
1
+ import { TRACKER_SOURCE } from '../src/analytics/tracker-source'
2
+ import { TRACKER_COMPONENT_SOURCE } from '../src/analytics/tracker-component'
3
+
4
+ // Regression guard for the page-view / page-leave double-count: the pages-router
5
+ // emits routeChangeStart/Complete for the SAME path during hydration (and on a
6
+ // link to the current page), which used to fire a second pageview (new
7
+ // pageLoadId — not server-deduped) and a 0ms page_leave. The fix guards the two
8
+ // route entry points on the router's destination url, with a defensive same-path
9
+ // guard inside trackPageview.
10
+ describe('tracker — same-path double-count guard', () => {
11
+ it('defines a same-path helper over the router url', () => {
12
+ expect(TRACKER_SOURCE).toContain('function samePathAsCurrent(url)')
13
+ expect(TRACKER_SOURCE).toContain('function pathnameOf(url)')
14
+ })
15
+
16
+ it('guards trackRouteChange against a same-path re-fire', () => {
17
+ expect(TRACKER_SOURCE).toMatch(
18
+ /export function trackRouteChange\(url\)\s*{\s*if \(samePathAsCurrent\(url\)\)\s*{\s*return\s*}/
19
+ )
20
+ })
21
+
22
+ it('guards trackRouteLeave so hydration cannot enqueue a bogus page_leave', () => {
23
+ expect(TRACKER_SOURCE).toMatch(
24
+ /export function trackRouteLeave\(url\)\s*{[\s\S]*?if \(samePathAsCurrent\(url\)\)\s*{\s*return\s*}/
25
+ )
26
+ })
27
+
28
+ it('keeps the defensive same-path guard inside trackPageview', () => {
29
+ expect(TRACKER_SOURCE).toContain(
30
+ '!isFirstLoad && currentPath !== null && newPath === currentPath'
31
+ )
32
+ })
33
+
34
+ it('keeps pagehide end-of-visit unconditional (genuine leave)', () => {
35
+ expect(TRACKER_SOURCE).toContain("window.addEventListener('pagehide', () => {")
36
+ expect(TRACKER_SOURCE).toContain('trackLeave(true)')
37
+ })
38
+
39
+ it('passes the router destination url into both handlers', () => {
40
+ expect(TRACKER_COMPONENT_SOURCE).toContain(
41
+ 'const handleRouteChangeStart = (url) => trackRouteLeave(url)'
42
+ )
43
+ expect(TRACKER_COMPONENT_SOURCE).toContain(
44
+ 'const handleRouteChangeComplete = (url) => trackRouteChange(url)'
45
+ )
46
+ })
47
+ })
@@ -54,6 +54,11 @@ describe('NextCalendarKitProjectPlugin', () => {
54
54
  expect(structure.dependencies.react).toBe('^18.3.1')
55
55
  expect(structure.dependencies['react-dom']).toBe('^18.3.1')
56
56
 
57
+ const npmrc = structure.files.get('calendarkit-npmrc')
58
+ expect(npmrc?.path).toEqual([])
59
+ expect(npmrc?.files[0].name).toBe('.npmrc')
60
+ expect(npmrc?.files[0].content).toContain('legacy-peer-deps=true')
61
+
57
62
  const cssRecord = structure.files.get('calendarkit-css')
58
63
  expect(cssRecord?.path).toEqual(['pages'])
59
64
  expect(cssRecord?.files[0]).toEqual({
@@ -0,0 +1,82 @@
1
+ import { GeneratedFolder, ProjectUIDL } from '@teleporthq/teleport-types'
2
+ import uidlSample from '../../../examples/test-samples/project-sample.json'
3
+ import { createNextProjectGenerator } from '../src'
4
+ import NextTemplate from '../src/project-template'
5
+
6
+ // The production template (react ^17) — the same one the GUI publish path feeds
7
+ // into packProject, so the react bump is asserted against real deps.
8
+ const template = JSON.parse(JSON.stringify(NextTemplate)) as GeneratedFolder
9
+
10
+ // A Motion CONTAINER wrapping a child — mirrors what the GUI's motionNodeToUIDL
11
+ // emits (elementType 'motion-node', camelCase props, children preserved).
12
+ const MOTION_ELEMENT_NODE = {
13
+ type: 'element',
14
+ content: {
15
+ elementType: 'motion-node',
16
+ name: 'motion',
17
+ attrs: {
18
+ preset: { type: 'static', content: 'slide-up' },
19
+ trigger: { type: 'static', content: 'in-view' },
20
+ stagger: { type: 'static', content: 0.08 },
21
+ },
22
+ children: [
23
+ {
24
+ type: 'element',
25
+ content: {
26
+ elementType: 'container',
27
+ attrs: { id: { type: 'static', content: 'motion-inner-child' } },
28
+ children: [],
29
+ },
30
+ },
31
+ ],
32
+ },
33
+ }
34
+
35
+ const buildUidlWithMotion = (): ProjectUIDL => {
36
+ const uidl = JSON.parse(JSON.stringify(uidlSample)) as ProjectUIDL
37
+ const indexPage = (uidl.root.node.content.children || []).find(
38
+ (child) =>
39
+ child.type === 'conditional' && (child.content as { value?: string }).value === 'index'
40
+ )
41
+ const pageElement = (indexPage as { content: { node: { content: { children: unknown[] } } } })
42
+ .content.node.content
43
+ pageElement.children.push(MOTION_ELEMENT_NODE)
44
+ return uidl
45
+ }
46
+
47
+ const findFile = (folder: GeneratedFolder, folderName: string, fileName: string) =>
48
+ folder.subFolders
49
+ .find((sub) => sub.name === folderName)
50
+ ?.files.find((file) => file.name === fileName)
51
+
52
+ describe('Next generator with a Motion element', () => {
53
+ const generator = createNextProjectGenerator()
54
+
55
+ it('renders <TqMotion> wrapping its children, ships the wrapper and bumps react', async () => {
56
+ const outputFolder = await generator.generateProject(buildUidlWithMotion(), template)
57
+
58
+ // The page imports and renders TqMotion, wrapping the child (not self-closing).
59
+ const indexPage = findFile(outputFolder, 'pages', 'index')
60
+ expect(indexPage?.content).toContain('TqMotion')
61
+ expect(indexPage?.content).toContain('preset')
62
+ expect(indexPage?.content).toMatch(/<TqMotion[\s\S]*motion-inner-child[\s\S]*<\/TqMotion>/)
63
+
64
+ // The local wrapper component is emitted and uses framer-motion.
65
+ const component = findFile(outputFolder, 'components', 'tq-motion')
66
+ expect(component?.content).toContain("from 'framer-motion'")
67
+ expect(component?.content).toContain('{children}')
68
+
69
+ // framer-motion is added and react is bumped to 18 (next left on its caret).
70
+ const packageFile = outputFolder.files.find((file) => file.name === 'package')
71
+ const packageJson = JSON.parse(packageFile?.content || '{}')
72
+ expect(packageJson.dependencies['framer-motion']).toBe('^11.18.0')
73
+ expect(packageJson.dependencies.react).toBe('^18.3.1')
74
+ expect(packageJson.dependencies['react-dom']).toBe('^18.3.1')
75
+ expect(packageJson.dependencies.next).toBe('^12.1.10')
76
+
77
+ // A root .npmrc (legacy-peer-deps) ships so the React-18 bump survives
78
+ // template deps with React-17-only peer ranges (e.g. dangerous-html embeds).
79
+ const npmrc = outputFolder.files.find((file) => file.name === '.npmrc')
80
+ expect(npmrc?.content).toContain('legacy-peer-deps=true')
81
+ })
82
+ })
@@ -0,0 +1,252 @@
1
+ import {
2
+ FileType,
3
+ InMemoryFileRecord,
4
+ ProjectPlugin,
5
+ ProjectPluginStructure,
6
+ ProjectUIDL,
7
+ UIDLElementNode,
8
+ } from '@teleporthq/teleport-types'
9
+ import { createNextWidgetProjectPlugins } from '../src/widgets'
10
+
11
+ const APP_CONTENT = `import './style.css'
12
+
13
+ export default function MyApp({ Component, pageProps }) {
14
+ return <Component {...pageProps} />
15
+ }
16
+ `
17
+
18
+ const elementNode = (elementType: string, children: UIDLElementNode[] = []): UIDLElementNode => ({
19
+ type: 'element',
20
+ content: {
21
+ elementType,
22
+ children,
23
+ },
24
+ })
25
+
26
+ const buildStructure = (pageChildren: UIDLElementNode[]): ProjectPluginStructure => {
27
+ const files = new Map<string, InMemoryFileRecord>()
28
+ files.set('_app', {
29
+ path: ['pages'],
30
+ files: [{ name: '_app', fileType: FileType.JS, content: APP_CONTENT }],
31
+ })
32
+
33
+ const uidl = {
34
+ name: 'test',
35
+ root: {
36
+ name: 'App',
37
+ node: elementNode('container', pageChildren),
38
+ },
39
+ components: {},
40
+ } as unknown as ProjectUIDL
41
+
42
+ return {
43
+ uidl,
44
+ files,
45
+ dependencies: {},
46
+ devDependencies: {},
47
+ } as unknown as ProjectPluginStructure
48
+ }
49
+
50
+ const runAll = async (structure: ProjectPluginStructure): Promise<void> => {
51
+ const plugins: ProjectPlugin[] = createNextWidgetProjectPlugins()
52
+ for (const plugin of plugins) {
53
+ await plugin.runAfter(structure)
54
+ }
55
+ }
56
+
57
+ interface WidgetCase {
58
+ elementType: string
59
+ fileKey: string
60
+ fileName: string
61
+ componentName: string
62
+ dependency: string
63
+ version: string
64
+ }
65
+
66
+ const WIDGET_CASES: WidgetCase[] = [
67
+ {
68
+ elementType: 'qrcode-node',
69
+ fileKey: 'tq-qrcode-component',
70
+ fileName: 'tq-qrcode',
71
+ componentName: 'TqQrCode',
72
+ dependency: 'qrcode',
73
+ version: '1.5.4',
74
+ },
75
+ {
76
+ elementType: 'barcode-node',
77
+ fileKey: 'tq-barcode-component',
78
+ fileName: 'tq-barcode',
79
+ componentName: 'TqBarcode',
80
+ dependency: 'jsbarcode',
81
+ version: '3.12.1',
82
+ },
83
+ {
84
+ elementType: 'signature-node',
85
+ fileKey: 'tq-signature-component',
86
+ fileName: 'tq-signature',
87
+ componentName: 'TqSignature',
88
+ dependency: 'signature_pad',
89
+ version: '5.0.4',
90
+ },
91
+ {
92
+ elementType: 'color-picker-node',
93
+ fileKey: 'tq-color-picker-component',
94
+ fileName: 'tq-color-picker',
95
+ componentName: 'TqColorPicker',
96
+ dependency: '@simonwep/pickr',
97
+ version: '1.9.1',
98
+ },
99
+ {
100
+ elementType: 'emoji-picker-node',
101
+ fileKey: 'tq-emoji-picker-component',
102
+ fileName: 'tq-emoji-picker',
103
+ componentName: 'TqEmojiPicker',
104
+ dependency: 'emoji-picker-element',
105
+ version: '1.26.3',
106
+ },
107
+ {
108
+ elementType: 'motion-node',
109
+ fileKey: 'tq-motion-component',
110
+ fileName: 'tq-motion',
111
+ componentName: 'TqMotion',
112
+ dependency: 'framer-motion',
113
+ version: '^11.18.0',
114
+ },
115
+ ]
116
+
117
+ describe('Next widget project plugins', () => {
118
+ it('are a no-op for projects without any widget primitive', async () => {
119
+ const structure = buildStructure([elementNode('container')])
120
+
121
+ await runAll(structure)
122
+
123
+ for (const widget of WIDGET_CASES) {
124
+ expect(structure.files.has(widget.fileKey)).toBe(false)
125
+ expect(structure.dependencies[widget.dependency]).toBeUndefined()
126
+ }
127
+ expect(structure.files.get('_app')?.files[0].content).toBe(APP_CONTENT)
128
+ })
129
+
130
+ it.each(WIDGET_CASES)(
131
+ 'emits the $componentName wrapper + dependency when $elementType is used',
132
+ async (widget) => {
133
+ const structure = buildStructure([elementNode(widget.elementType)])
134
+
135
+ await runAll(structure)
136
+
137
+ const record = structure.files.get(widget.fileKey)
138
+ expect(record?.path).toEqual(['components'])
139
+ expect(record?.files[0].name).toBe(widget.fileName)
140
+ expect(record?.files[0].fileType).toBe(FileType.JS)
141
+ expect(record?.files[0].content).toContain(`const ${widget.componentName} =`)
142
+ expect(record?.files[0].content).toContain(`export default ${widget.componentName}`)
143
+ expect(structure.dependencies[widget.dependency]).toBe(widget.version)
144
+ }
145
+ )
146
+
147
+ it('detects a widget primitive nested deep in the tree', async () => {
148
+ const structure = buildStructure([
149
+ elementNode('container', [elementNode('container', [elementNode('qrcode-node')])]),
150
+ ])
151
+
152
+ await runAll(structure)
153
+
154
+ expect(structure.files.has('tq-qrcode-component')).toBe(true)
155
+ })
156
+
157
+ it('injects the Pickr theme stylesheet into _app only for the color picker', async () => {
158
+ const structure = buildStructure([elementNode('color-picker-node')])
159
+
160
+ await runAll(structure)
161
+
162
+ const appContent = structure.files.get('_app')?.files[0].content as string
163
+ expect(appContent).toContain("import '@simonwep/pickr/dist/themes/nano.min.css'")
164
+ })
165
+
166
+ it('does not inject any stylesheet for a widget without CSS (qr code)', async () => {
167
+ const structure = buildStructure([elementNode('qrcode-node')])
168
+
169
+ await runAll(structure)
170
+
171
+ const appContent = structure.files.get('_app')?.files[0].content as string
172
+ expect(appContent).toBe(APP_CONTENT)
173
+ })
174
+
175
+ it('bumps react/react-dom to ^18 only when the motion widget is used', async () => {
176
+ const withoutMotion = buildStructure([elementNode('qrcode-node')])
177
+ await runAll(withoutMotion)
178
+ expect(withoutMotion.dependencies.react).toBeUndefined()
179
+ expect(withoutMotion.dependencies['react-dom']).toBeUndefined()
180
+
181
+ const withMotion = buildStructure([elementNode('motion-node')])
182
+ await runAll(withMotion)
183
+ expect(withMotion.dependencies['framer-motion']).toBe('^11.18.0')
184
+ expect(withMotion.dependencies.react).toBe('^18.3.1')
185
+ expect(withMotion.dependencies['react-dom']).toBe('^18.3.1')
186
+ })
187
+
188
+ it('emits a root .npmrc (legacy-peer-deps) only when the React-18 bump runs', async () => {
189
+ const withoutMotion = buildStructure([elementNode('qrcode-node')])
190
+ await runAll(withoutMotion)
191
+ expect(withoutMotion.files.get('tq-motion-component-npmrc')).toBeUndefined()
192
+
193
+ const withMotion = buildStructure([elementNode('motion-node')])
194
+ await runAll(withMotion)
195
+ const npmrc = withMotion.files.get('tq-motion-component-npmrc')
196
+ expect(npmrc?.path).toEqual([])
197
+ expect(npmrc?.files[0].name).toBe('.npmrc')
198
+ expect(npmrc?.files[0].content).toContain('legacy-peer-deps=true')
199
+ })
200
+
201
+ it('generates a TqMotion wrapper that imports framer-motion and renders children', async () => {
202
+ const structure = buildStructure([elementNode('motion-node')])
203
+ await runAll(structure)
204
+ const content = structure.files.get('tq-motion-component')?.files[0].content as string
205
+ expect(content).toContain(
206
+ "import { motion, useInView, useReducedMotion, useScroll, useTransform } from 'framer-motion'"
207
+ )
208
+ expect(content).toContain('<motion.div')
209
+ expect(content).toContain('{children}')
210
+ expect(content).toContain('useReducedMotion')
211
+ })
212
+
213
+ it('drives in-view via useInView + a timed in-viewport failsafe (never trapped at opacity:0)', async () => {
214
+ const structure = buildStructure([elementNode('motion-node')])
215
+ await runAll(structure)
216
+ const content = structure.files.get('tq-motion-component')?.files[0].content as string
217
+ // in-view is no longer the bare whileInView (which could miss an already-in-view
218
+ // hero) — it is gated on useInView OR a forced reveal after a timed visibility check.
219
+ expect(content).toContain('useInView(ref')
220
+ expect(content).toContain('setForceReveal')
221
+ expect(content).toContain('getBoundingClientRect')
222
+ expect(content).toContain('animate: revealed ? toVars : fromVars')
223
+ expect(content).not.toContain('whileInView')
224
+ })
225
+
226
+ it('staggers the real repeated items (descends grid/array-mapper wrappers), not the block', async () => {
227
+ const structure = buildStructure([elementNode('motion-node')])
228
+ await runAll(structure)
229
+ const content = structure.files.get('tq-motion-component')?.files[0].content as string
230
+ // The descent helper + per-child wrapping must be generated so a wrapped grid
231
+ // cascades its cards rather than animating as one block.
232
+ expect(content).toContain('mapStaggerTargets')
233
+ expect(content).toContain('cloneElement')
234
+ expect(content).toContain('index * Number(stagger)')
235
+ })
236
+
237
+ it('uses next/dynamic ssr:false for the window-dependent wrappers', async () => {
238
+ const structure = buildStructure([
239
+ elementNode('color-picker-node'),
240
+ elementNode('emoji-picker-node'),
241
+ ])
242
+
243
+ await runAll(structure)
244
+
245
+ expect(structure.files.get('tq-color-picker-component')?.files[0].content).toContain(
246
+ "import('@simonwep/pickr')"
247
+ )
248
+ expect(structure.files.get('tq-emoji-picker-component')?.files[0].content).toContain(
249
+ "import('emoji-picker-element')"
250
+ )
251
+ })
252
+ })
@@ -1,2 +1,2 @@
1
- export declare const TRACKER_COMPONENT_SOURCE = "import { useEffect } from 'react'\nimport { useRouter } from 'next/router'\nimport {\n initTeleportAnalytics,\n trackRouteChange,\n trackRouteLeave,\n} from '../../lib/teleport-analytics'\n\nconst AnalyticsTracker = () => {\n const router = useRouter()\n\n useEffect(() => {\n initTeleportAnalytics()\n\n const handleRouteChangeStart = () => trackRouteLeave()\n const handleRouteChangeComplete = () => trackRouteChange()\n\n router.events.on('routeChangeStart', handleRouteChangeStart)\n router.events.on('routeChangeComplete', handleRouteChangeComplete)\n\n return () => {\n router.events.off('routeChangeStart', handleRouteChangeStart)\n router.events.off('routeChangeComplete', handleRouteChangeComplete)\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [])\n\n return null\n}\n\nexport default AnalyticsTracker\n";
1
+ export declare const TRACKER_COMPONENT_SOURCE = "import { useEffect } from 'react'\nimport { useRouter } from 'next/router'\nimport {\n initTeleportAnalytics,\n trackRouteChange,\n trackRouteLeave,\n} from '../../lib/teleport-analytics'\n\nconst AnalyticsTracker = () => {\n const router = useRouter()\n\n useEffect(() => {\n initTeleportAnalytics()\n\n const handleRouteChangeStart = (url) => trackRouteLeave(url)\n const handleRouteChangeComplete = (url) => trackRouteChange(url)\n\n router.events.on('routeChangeStart', handleRouteChangeStart)\n router.events.on('routeChangeComplete', handleRouteChangeComplete)\n\n return () => {\n router.events.off('routeChangeStart', handleRouteChangeStart)\n router.events.off('routeChangeComplete', handleRouteChangeComplete)\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [])\n\n return null\n}\n\nexport default AnalyticsTracker\n";
2
2
  //# sourceMappingURL=tracker-component.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tracker-component.d.ts","sourceRoot":"","sources":["../../../src/analytics/tracker-component.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,wBAAwB,g3BA+BpC,CAAA"}
1
+ {"version":3,"file":"tracker-component.d.ts","sourceRoot":"","sources":["../../../src/analytics/tracker-component.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,wBAAwB,43BA+BpC,CAAA"}
@@ -4,5 +4,5 @@ exports.TRACKER_COMPONENT_SOURCE = void 0;
4
4
  // Source of the generated `components/analytics/AnalyticsTracker.js` —
5
5
  // null-rendering component that boots the tracker and bridges Next.js
6
6
  // pages-router navigation events to pageview/page_leave tracking.
7
- exports.TRACKER_COMPONENT_SOURCE = "import { useEffect } from 'react'\nimport { useRouter } from 'next/router'\nimport {\n initTeleportAnalytics,\n trackRouteChange,\n trackRouteLeave,\n} from '../../lib/teleport-analytics'\n\nconst AnalyticsTracker = () => {\n const router = useRouter()\n\n useEffect(() => {\n initTeleportAnalytics()\n\n const handleRouteChangeStart = () => trackRouteLeave()\n const handleRouteChangeComplete = () => trackRouteChange()\n\n router.events.on('routeChangeStart', handleRouteChangeStart)\n router.events.on('routeChangeComplete', handleRouteChangeComplete)\n\n return () => {\n router.events.off('routeChangeStart', handleRouteChangeStart)\n router.events.off('routeChangeComplete', handleRouteChangeComplete)\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [])\n\n return null\n}\n\nexport default AnalyticsTracker\n";
7
+ exports.TRACKER_COMPONENT_SOURCE = "import { useEffect } from 'react'\nimport { useRouter } from 'next/router'\nimport {\n initTeleportAnalytics,\n trackRouteChange,\n trackRouteLeave,\n} from '../../lib/teleport-analytics'\n\nconst AnalyticsTracker = () => {\n const router = useRouter()\n\n useEffect(() => {\n initTeleportAnalytics()\n\n const handleRouteChangeStart = (url) => trackRouteLeave(url)\n const handleRouteChangeComplete = (url) => trackRouteChange(url)\n\n router.events.on('routeChangeStart', handleRouteChangeStart)\n router.events.on('routeChangeComplete', handleRouteChangeComplete)\n\n return () => {\n router.events.off('routeChangeStart', handleRouteChangeStart)\n router.events.off('routeChangeComplete', handleRouteChangeComplete)\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [])\n\n return null\n}\n\nexport default AnalyticsTracker\n";
8
8
  //# sourceMappingURL=tracker-component.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"tracker-component.js","sourceRoot":"","sources":["../../../src/analytics/tracker-component.ts"],"names":[],"mappings":";;;AAAA,uEAAuE;AACvE,sEAAsE;AACtE,kEAAkE;AACrD,QAAA,wBAAwB,GAAG,62BA+BvC,CAAA"}
1
+ {"version":3,"file":"tracker-component.js","sourceRoot":"","sources":["../../../src/analytics/tracker-component.ts"],"names":[],"mappings":";;;AAAA,uEAAuE;AACvE,sEAAsE;AACtE,kEAAkE;AACrD,QAAA,wBAAwB,GAAG,y3BA+BvC,CAAA"}
@@ -1,2 +1,2 @@
1
- export declare const TRACKER_SOURCE = "/* TeleportHQ first-party analytics tracker. Anonymous, cookieless by default. */\nconst SERVER_URL = process.env.NEXT_PUBLIC_TELEPORT_ANALYTICS_URL\nconst PUBLIC_KEY = process.env.NEXT_PUBLIC_TELEPORT_ANALYTICS_KEY\n\nconst HEARTBEAT_INTERVAL_MS = 30000\nconst FLUSH_INTERVAL_MS = 5000\nconst FLUSH_AT_QUEUE_SIZE = 10\nconst MAX_BATCH = 25\nconst RETRY_DELAYS_MS = [1000, 5000, 15000]\nconst SESSION_WINDOW_MS = 30 * 60 * 1000\n\nlet initialized = false\nlet disabled = false\nlet forbiddenCount = 0\n\nlet sessionId = null\nlet visitorId = null\nlet pageLoadId = null\nlet seq = 0\nlet currentPath = null\nlet initialReferrer = null\nlet utm = null\n\nlet visibleSince = null\nlet visibleAccumMs = 0\nlet maxScrollPct = 0\n\nlet queue = []\nlet flushTimer = null\nlet heartbeatTimer = null\nlet retryAttempt = 0\n\nfunction uuid() {\n if (typeof crypto !== 'undefined' && crypto.randomUUID) {\n return crypto.randomUUID()\n }\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0\n return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16)\n })\n}\n\nfunction hasConsent() {\n try {\n return window.localStorage.getItem('cookieConsent') === 'accepted'\n } catch (e) {\n return false\n }\n}\n\nfunction isTrackingPossible() {\n if (disabled || typeof window === 'undefined') {\n return false\n }\n if (!SERVER_URL || !PUBLIC_KEY) {\n return false\n }\n // Fail safe: a misconfigured deploy can leave the unresolved\n // \"teleporthq.secrets.*\" placeholder (or any non-absolute value) baked into\n // these build-time vars. Never fire requests at a non-absolute URL \u2014 that\n // would point every beacon at the host site's own origin instead of the\n // analytics API.\n if (SERVER_URL.indexOf('http') !== 0 || PUBLIC_KEY.indexOf('teleporthq.secrets.') === 0) {\n return false\n }\n const host = window.location.hostname\n if (host === 'localhost' || host === '127.0.0.1' || host === '0.0.0.0') {\n return false\n }\n if (typeof navigator !== 'undefined' && navigator.webdriver) {\n return false\n }\n return true\n}\n\nfunction setupIdentity() {\n if (!hasConsent()) {\n // Consentless mode: nothing is stored on the device. The session id only\n // lives in JS memory; the server stitches sessions via its anonymized\n // daily visitor hash.\n sessionId = uuid()\n visitorId = null\n return\n }\n\n try {\n visitorId = window.localStorage.getItem('tp_aid')\n if (!visitorId) {\n visitorId = uuid()\n window.localStorage.setItem('tp_aid', visitorId)\n }\n\n const now = Date.now()\n const storedSession = window.sessionStorage.getItem('tp_sid')\n const storedAt = Number(window.sessionStorage.getItem('tp_sid_t') || 0)\n\n if (storedSession && now - storedAt < SESSION_WINDOW_MS) {\n sessionId = storedSession\n } else {\n sessionId = uuid()\n }\n window.sessionStorage.setItem('tp_sid', sessionId)\n window.sessionStorage.setItem('tp_sid_t', String(now))\n } catch (e) {\n sessionId = uuid()\n visitorId = null\n }\n}\n\nfunction touchSession() {\n if (visitorId === null) {\n return\n }\n try {\n window.sessionStorage.setItem('tp_sid_t', String(Date.now()))\n } catch (e) {\n /* storage unavailable */\n }\n}\n\nfunction parseUtm() {\n try {\n const params = new URLSearchParams(window.location.search)\n const read = (key) => {\n const value = params.get(key)\n return value ? value.slice(0, 255) : null\n }\n const parsed = {\n source: read('utm_source'),\n medium: read('utm_medium'),\n campaign: read('utm_campaign'),\n term: read('utm_term'),\n content: read('utm_content'),\n }\n const hasAny = Object.keys(parsed).some((key) => parsed[key])\n return hasAny ? parsed : null\n } catch (e) {\n return null\n }\n}\n\nfunction endpoint(suffix) {\n return SERVER_URL.replace(/\\/$/, '') + '/events/' + PUBLIC_KEY + (suffix || '')\n}\n\nfunction markForbidden(status) {\n if (status === 403 || status === 401) {\n forbiddenCount += 1\n if (forbiddenCount >= 3) {\n // Analytics disabled server-side \u2014 go silent for this page lifetime\n disabled = true\n queue = []\n if (flushTimer) clearInterval(flushTimer)\n if (heartbeatTimer) clearInterval(heartbeatTimer)\n }\n } else {\n forbiddenCount = 0\n }\n}\n\nfunction sendBatch(events, useBeacon) {\n if (events.length === 0) {\n return Promise.resolve(true)\n }\n\n const body = JSON.stringify({ events: events })\n const url = endpoint('/batch')\n\n // text/plain is a CORS-safelisted content type, so the request skips the\n // preflight. That preflight is what makes an application/json beacon fail on\n // page unload (the browser can't complete OPTIONS while the page is dying),\n // and it also doubles every normal batch into OPTIONS+POST. The server reads\n // the JSON body regardless of this content type.\n if (useBeacon && typeof navigator !== 'undefined' && navigator.sendBeacon) {\n try {\n const blob = new Blob([body], { type: 'text/plain;charset=UTF-8' })\n return Promise.resolve(navigator.sendBeacon(url, blob))\n } catch (e) {\n /* fall through to fetch */\n }\n }\n\n return fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'text/plain;charset=UTF-8' },\n body: body,\n keepalive: true,\n })\n .then((response) => {\n markForbidden(response.status)\n return response.status < 400\n })\n .catch(() => false)\n}\n\nfunction flush(useBeacon) {\n if (queue.length === 0 || disabled) {\n return\n }\n\n const batch = queue.slice(0, MAX_BATCH)\n queue = queue.slice(batch.length)\n\n sendBatch(batch, useBeacon).then((ok) => {\n if (ok) {\n retryAttempt = 0\n return\n }\n if (retryAttempt < RETRY_DELAYS_MS.length && !disabled) {\n // Re-queue once per backoff step, then drop \u2014 analytics is lossy-tolerant\n queue = batch.concat(queue)\n const delay = RETRY_DELAYS_MS[retryAttempt]\n retryAttempt += 1\n setTimeout(() => flush(false), delay)\n }\n })\n}\n\nfunction enqueue(event) {\n if (disabled) {\n return\n }\n queue.push(event)\n touchSession()\n if (queue.length >= FLUSH_AT_QUEUE_SIZE) {\n flush(false)\n }\n}\n\nfunction baseEvent(type) {\n seq += 1\n return {\n type: type,\n pageLoadId: pageLoadId,\n seq: seq - 1,\n sessionId: sessionId,\n visitorId: visitorId,\n path: currentPath,\n clientTs: Date.now(),\n }\n}\n\nfunction visibleTimeMs() {\n let total = visibleAccumMs\n if (visibleSince !== null) {\n total += Date.now() - visibleSince\n }\n return Math.max(0, Math.round(total))\n}\n\nfunction resetPageMetrics() {\n visibleAccumMs = 0\n visibleSince = document.visibilityState === 'visible' ? Date.now() : null\n maxScrollPct = 0\n}\n\nfunction currentScrollPct() {\n try {\n const doc = document.documentElement\n const scrollable = doc.scrollHeight - window.innerHeight\n if (scrollable <= 0) {\n return 100\n }\n return Math.min(100, Math.round((window.scrollY / scrollable) * 100))\n } catch (e) {\n return 0\n }\n}\n\nfunction trackPageview(isFirstLoad) {\n if (!isTrackingPossible()) {\n return\n }\n\n pageLoadId = uuid()\n seq = 0\n currentPath = window.location.pathname || '/'\n resetPageMetrics()\n\n const event = baseEvent('pageview')\n event.referrer = isFirstLoad ? (document.referrer || null) : null\n event.utm = isFirstLoad ? utm : null\n event.screenW = window.screen && window.screen.width ? window.screen.width : null\n enqueue(event)\n}\n\nfunction trackLeave(useBeacon) {\n if (!isTrackingPossible() || !pageLoadId) {\n return\n }\n\n const event = baseEvent('page_leave')\n event.timeOnPageMs = visibleTimeMs()\n event.maxScrollPct = maxScrollPct\n enqueue(event)\n\n if (useBeacon) {\n flush(true)\n }\n}\n\nfunction sendHeartbeat() {\n if (!isTrackingPossible() || document.visibilityState !== 'visible' || !pageLoadId) {\n return\n }\n\n // Heartbeats go direct (never queued) \u2014 a stale heartbeat is worthless.\n // text/plain keeps this preflight-free too (see sendBatch).\n fetch(endpoint('/heartbeat'), {\n method: 'POST',\n headers: { 'Content-Type': 'text/plain;charset=UTF-8' },\n body: JSON.stringify({\n sessionId: sessionId,\n visitorId: visitorId,\n pageLoadId: pageLoadId,\n path: currentPath,\n }),\n keepalive: true,\n })\n .then((response) => markForbidden(response.status))\n .catch(() => undefined)\n}\n\nfunction onClickCapture(domEvent) {\n if (!isTrackingPossible() || !pageLoadId) {\n return\n }\n\n try {\n const target = domEvent.target && domEvent.target.closest\n ? domEvent.target.closest('a, button, [role=\"button\"], input[type=\"submit\"], [data-tp-event]')\n : null\n if (!target) {\n return\n }\n\n const namedAttr = target.getAttribute('data-tp-event')\n let href = null\n if (target.tagName === 'A' && target.getAttribute('href')) {\n const raw = target.getAttribute('href')\n if (raw.indexOf('http') === 0) {\n try {\n const parsed = new URL(raw)\n href = parsed.origin === window.location.origin ? parsed.pathname : raw.slice(0, 512)\n } catch (e) {\n href = raw.slice(0, 512)\n }\n } else {\n href = raw.split('?')[0].split('#')[0].slice(0, 512) || null\n }\n }\n\n const event = baseEvent('click')\n event.click = {\n tag: target.tagName ? target.tagName.toLowerCase().slice(0, 16) : null,\n id: target.id ? target.id.slice(0, 64) : null,\n text: target.textContent ? target.textContent.trim().slice(0, 64) : null,\n href: href,\n name: namedAttr && /^[a-zA-Z0-9_-]{1,64}$/.test(namedAttr) ? namedAttr : null,\n }\n enqueue(event)\n } catch (e) {\n /* never break the host page */\n }\n}\n\nexport function initTeleportAnalytics() {\n if (initialized || typeof window === 'undefined') {\n return\n }\n initialized = true\n\n if (!isTrackingPossible()) {\n return\n }\n\n setupIdentity()\n utm = parseUtm()\n\n document.addEventListener('click', onClickCapture, true)\n\n document.addEventListener('visibilitychange', () => {\n if (document.visibilityState === 'visible') {\n visibleSince = Date.now()\n sendHeartbeat()\n } else {\n if (visibleSince !== null) {\n visibleAccumMs += Date.now() - visibleSince\n visibleSince = null\n }\n flush(true)\n }\n })\n\n window.addEventListener(\n 'scroll',\n () => {\n const pct = currentScrollPct()\n if (pct > maxScrollPct) {\n maxScrollPct = pct\n }\n },\n { passive: true }\n )\n\n // pagehide covers tab close, navigation away and most mobile terminations.\n // When even this never fires (power loss, process kill), the server's\n // heartbeat window + session finalizer close the visit.\n window.addEventListener('pagehide', () => {\n trackLeave(true)\n })\n\n flushTimer = setInterval(() => flush(false), FLUSH_INTERVAL_MS)\n heartbeatTimer = setInterval(sendHeartbeat, HEARTBEAT_INTERVAL_MS)\n\n trackPageview(true)\n sendHeartbeat()\n}\n\nexport function trackRouteLeave() {\n trackLeave(false)\n}\n\nexport function trackRouteChange() {\n trackPageview(false)\n sendHeartbeat()\n}\n";
1
+ export declare const TRACKER_SOURCE = "/* TeleportHQ first-party analytics tracker. Anonymous, cookieless by default. */\nconst SERVER_URL = process.env.NEXT_PUBLIC_TELEPORT_ANALYTICS_URL\nconst PUBLIC_KEY = process.env.NEXT_PUBLIC_TELEPORT_ANALYTICS_KEY\n\nconst HEARTBEAT_INTERVAL_MS = 30000\nconst FLUSH_INTERVAL_MS = 5000\nconst FLUSH_AT_QUEUE_SIZE = 10\nconst MAX_BATCH = 25\nconst RETRY_DELAYS_MS = [1000, 5000, 15000]\nconst SESSION_WINDOW_MS = 30 * 60 * 1000\n\nlet initialized = false\nlet disabled = false\nlet forbiddenCount = 0\n\nlet sessionId = null\nlet visitorId = null\nlet pageLoadId = null\nlet seq = 0\nlet currentPath = null\nlet initialReferrer = null\nlet utm = null\n\nlet visibleSince = null\nlet visibleAccumMs = 0\nlet maxScrollPct = 0\n\nlet queue = []\nlet flushTimer = null\nlet heartbeatTimer = null\nlet retryAttempt = 0\n\nfunction uuid() {\n if (typeof crypto !== 'undefined' && crypto.randomUUID) {\n return crypto.randomUUID()\n }\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0\n return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16)\n })\n}\n\nfunction hasConsent() {\n try {\n return window.localStorage.getItem('cookieConsent') === 'accepted'\n } catch (e) {\n return false\n }\n}\n\nfunction isTrackingPossible() {\n if (disabled || typeof window === 'undefined') {\n return false\n }\n if (!SERVER_URL || !PUBLIC_KEY) {\n return false\n }\n // Fail safe: a misconfigured deploy can leave the unresolved\n // \"teleporthq.secrets.*\" placeholder (or any non-absolute value) baked into\n // these build-time vars. Never fire requests at a non-absolute URL \u2014 that\n // would point every beacon at the host site's own origin instead of the\n // analytics API.\n if (SERVER_URL.indexOf('http') !== 0 || PUBLIC_KEY.indexOf('teleporthq.secrets.') === 0) {\n return false\n }\n const host = window.location.hostname\n if (host === 'localhost' || host === '127.0.0.1' || host === '0.0.0.0') {\n return false\n }\n if (typeof navigator !== 'undefined' && navigator.webdriver) {\n return false\n }\n return true\n}\n\nfunction setupIdentity() {\n if (!hasConsent()) {\n // Consentless mode: nothing is stored on the device. The session id only\n // lives in JS memory; the server stitches sessions via its anonymized\n // daily visitor hash.\n sessionId = uuid()\n visitorId = null\n return\n }\n\n try {\n visitorId = window.localStorage.getItem('tp_aid')\n if (!visitorId) {\n visitorId = uuid()\n window.localStorage.setItem('tp_aid', visitorId)\n }\n\n const now = Date.now()\n const storedSession = window.sessionStorage.getItem('tp_sid')\n const storedAt = Number(window.sessionStorage.getItem('tp_sid_t') || 0)\n\n if (storedSession && now - storedAt < SESSION_WINDOW_MS) {\n sessionId = storedSession\n } else {\n sessionId = uuid()\n }\n window.sessionStorage.setItem('tp_sid', sessionId)\n window.sessionStorage.setItem('tp_sid_t', String(now))\n } catch (e) {\n sessionId = uuid()\n visitorId = null\n }\n}\n\nfunction touchSession() {\n if (visitorId === null) {\n return\n }\n try {\n window.sessionStorage.setItem('tp_sid_t', String(Date.now()))\n } catch (e) {\n /* storage unavailable */\n }\n}\n\nfunction parseUtm() {\n try {\n const params = new URLSearchParams(window.location.search)\n const read = (key) => {\n const value = params.get(key)\n return value ? value.slice(0, 255) : null\n }\n const parsed = {\n source: read('utm_source'),\n medium: read('utm_medium'),\n campaign: read('utm_campaign'),\n term: read('utm_term'),\n content: read('utm_content'),\n }\n const hasAny = Object.keys(parsed).some((key) => parsed[key])\n return hasAny ? parsed : null\n } catch (e) {\n return null\n }\n}\n\nfunction endpoint(suffix) {\n return SERVER_URL.replace(/\\/$/, '') + '/events/' + PUBLIC_KEY + (suffix || '')\n}\n\nfunction markForbidden(status) {\n if (status === 403 || status === 401) {\n forbiddenCount += 1\n if (forbiddenCount >= 3) {\n // Analytics disabled server-side \u2014 go silent for this page lifetime\n disabled = true\n queue = []\n if (flushTimer) clearInterval(flushTimer)\n if (heartbeatTimer) clearInterval(heartbeatTimer)\n }\n } else {\n forbiddenCount = 0\n }\n}\n\nfunction sendBatch(events, useBeacon) {\n if (events.length === 0) {\n return Promise.resolve(true)\n }\n\n const body = JSON.stringify({ events: events })\n const url = endpoint('/batch')\n\n // text/plain is a CORS-safelisted content type, so the request skips the\n // preflight. That preflight is what makes an application/json beacon fail on\n // page unload (the browser can't complete OPTIONS while the page is dying),\n // and it also doubles every normal batch into OPTIONS+POST. The server reads\n // the JSON body regardless of this content type.\n if (useBeacon && typeof navigator !== 'undefined' && navigator.sendBeacon) {\n try {\n const blob = new Blob([body], { type: 'text/plain;charset=UTF-8' })\n return Promise.resolve(navigator.sendBeacon(url, blob))\n } catch (e) {\n /* fall through to fetch */\n }\n }\n\n return fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'text/plain;charset=UTF-8' },\n body: body,\n keepalive: true,\n })\n .then((response) => {\n markForbidden(response.status)\n return response.status < 400\n })\n .catch(() => false)\n}\n\nfunction flush(useBeacon) {\n if (queue.length === 0 || disabled) {\n return\n }\n\n const batch = queue.slice(0, MAX_BATCH)\n queue = queue.slice(batch.length)\n\n sendBatch(batch, useBeacon).then((ok) => {\n if (ok) {\n retryAttempt = 0\n return\n }\n if (retryAttempt < RETRY_DELAYS_MS.length && !disabled) {\n // Re-queue once per backoff step, then drop \u2014 analytics is lossy-tolerant\n queue = batch.concat(queue)\n const delay = RETRY_DELAYS_MS[retryAttempt]\n retryAttempt += 1\n setTimeout(() => flush(false), delay)\n }\n })\n}\n\nfunction enqueue(event) {\n if (disabled) {\n return\n }\n queue.push(event)\n touchSession()\n if (queue.length >= FLUSH_AT_QUEUE_SIZE) {\n flush(false)\n }\n}\n\nfunction baseEvent(type) {\n seq += 1\n return {\n type: type,\n pageLoadId: pageLoadId,\n seq: seq - 1,\n sessionId: sessionId,\n visitorId: visitorId,\n path: currentPath,\n clientTs: Date.now(),\n }\n}\n\nfunction visibleTimeMs() {\n let total = visibleAccumMs\n if (visibleSince !== null) {\n total += Date.now() - visibleSince\n }\n return Math.max(0, Math.round(total))\n}\n\nfunction resetPageMetrics() {\n visibleAccumMs = 0\n visibleSince = document.visibilityState === 'visible' ? Date.now() : null\n maxScrollPct = 0\n}\n\nfunction currentScrollPct() {\n try {\n const doc = document.documentElement\n const scrollable = doc.scrollHeight - window.innerHeight\n if (scrollable <= 0) {\n return 100\n }\n return Math.min(100, Math.round((window.scrollY / scrollable) * 100))\n } catch (e) {\n return 0\n }\n}\n\nfunction pathnameOf(url) {\n if (typeof url !== 'string' || !url) {\n return null\n }\n return url.split('?')[0].split('#')[0] || '/'\n}\n\n// True when a route event resolves to the page we are already tracking. The\n// pages-router emits routeChangeStart/Complete for the SAME path during initial\n// hydration (and when a link points at the current page), which would otherwise\n// double-count the pageview and emit a bogus 0ms page_leave.\nfunction samePathAsCurrent(url) {\n const next = pathnameOf(url)\n return currentPath !== null && next !== null && next === currentPath\n}\n\nfunction trackPageview(isFirstLoad) {\n if (!isTrackingPossible()) {\n return\n }\n\n const newPath = window.location.pathname || '/'\n // Defensive same-path guard (the route handlers also guard on the router's\n // destination url): never emit a second pageview for the page already shown.\n if (!isFirstLoad && currentPath !== null && newPath === currentPath) {\n return\n }\n\n pageLoadId = uuid()\n seq = 0\n currentPath = newPath\n resetPageMetrics()\n\n const event = baseEvent('pageview')\n event.referrer = isFirstLoad ? (document.referrer || null) : null\n event.utm = isFirstLoad ? utm : null\n event.screenW = window.screen && window.screen.width ? window.screen.width : null\n enqueue(event)\n}\n\nfunction trackLeave(useBeacon) {\n if (!isTrackingPossible() || !pageLoadId) {\n return\n }\n\n const event = baseEvent('page_leave')\n event.timeOnPageMs = visibleTimeMs()\n event.maxScrollPct = maxScrollPct\n enqueue(event)\n\n if (useBeacon) {\n flush(true)\n }\n}\n\nfunction sendHeartbeat() {\n if (!isTrackingPossible() || document.visibilityState !== 'visible' || !pageLoadId) {\n return\n }\n\n // Heartbeats go direct (never queued) \u2014 a stale heartbeat is worthless.\n // text/plain keeps this preflight-free too (see sendBatch).\n fetch(endpoint('/heartbeat'), {\n method: 'POST',\n headers: { 'Content-Type': 'text/plain;charset=UTF-8' },\n body: JSON.stringify({\n sessionId: sessionId,\n visitorId: visitorId,\n pageLoadId: pageLoadId,\n path: currentPath,\n }),\n keepalive: true,\n })\n .then((response) => markForbidden(response.status))\n .catch(() => undefined)\n}\n\nfunction onClickCapture(domEvent) {\n if (!isTrackingPossible() || !pageLoadId) {\n return\n }\n\n try {\n const target = domEvent.target && domEvent.target.closest\n ? domEvent.target.closest('a, button, [role=\"button\"], input[type=\"submit\"], [data-tp-event]')\n : null\n if (!target) {\n return\n }\n\n const namedAttr = target.getAttribute('data-tp-event')\n let href = null\n if (target.tagName === 'A' && target.getAttribute('href')) {\n const raw = target.getAttribute('href')\n if (raw.indexOf('http') === 0) {\n try {\n const parsed = new URL(raw)\n href = parsed.origin === window.location.origin ? parsed.pathname : raw.slice(0, 512)\n } catch (e) {\n href = raw.slice(0, 512)\n }\n } else {\n href = raw.split('?')[0].split('#')[0].slice(0, 512) || null\n }\n }\n\n const event = baseEvent('click')\n event.click = {\n tag: target.tagName ? target.tagName.toLowerCase().slice(0, 16) : null,\n id: target.id ? target.id.slice(0, 64) : null,\n text: target.textContent ? target.textContent.trim().slice(0, 64) : null,\n href: href,\n name: namedAttr && /^[a-zA-Z0-9_-]{1,64}$/.test(namedAttr) ? namedAttr : null,\n }\n enqueue(event)\n } catch (e) {\n /* never break the host page */\n }\n}\n\nexport function initTeleportAnalytics() {\n if (initialized || typeof window === 'undefined') {\n return\n }\n initialized = true\n\n if (!isTrackingPossible()) {\n return\n }\n\n setupIdentity()\n utm = parseUtm()\n\n document.addEventListener('click', onClickCapture, true)\n\n document.addEventListener('visibilitychange', () => {\n if (document.visibilityState === 'visible') {\n visibleSince = Date.now()\n sendHeartbeat()\n } else {\n if (visibleSince !== null) {\n visibleAccumMs += Date.now() - visibleSince\n visibleSince = null\n }\n flush(true)\n }\n })\n\n window.addEventListener(\n 'scroll',\n () => {\n const pct = currentScrollPct()\n if (pct > maxScrollPct) {\n maxScrollPct = pct\n }\n },\n { passive: true }\n )\n\n // pagehide covers tab close, navigation away and most mobile terminations.\n // When even this never fires (power loss, process kill), the server's\n // heartbeat window + session finalizer close the visit.\n window.addEventListener('pagehide', () => {\n trackLeave(true)\n })\n\n flushTimer = setInterval(() => flush(false), FLUSH_INTERVAL_MS)\n heartbeatTimer = setInterval(sendHeartbeat, HEARTBEAT_INTERVAL_MS)\n\n trackPageview(true)\n sendHeartbeat()\n}\n\nexport function trackRouteLeave(url) {\n // A same-path routeChangeStart (hydration) is not a real leave \u2014 skip it so we\n // don't enqueue a 0ms page_leave. Genuine end-of-visit goes through pagehide\n // (trackLeave(true)), which is intentionally unconditional.\n if (samePathAsCurrent(url)) {\n return\n }\n trackLeave(false)\n}\n\nexport function trackRouteChange(url) {\n if (samePathAsCurrent(url)) {\n return\n }\n trackPageview(false)\n sendHeartbeat()\n}\n";
2
2
  //# sourceMappingURL=tracker-source.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tracker-source.d.ts","sourceRoot":"","sources":["../../../src/analytics/tracker-source.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,cAAc,utWA2a1B,CAAA"}
1
+ {"version":3,"file":"tracker-source.d.ts","sourceRoot":"","sources":["../../../src/analytics/tracker-source.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,cAAc,q5YA2c1B,CAAA"}
@@ -11,5 +11,5 @@ exports.TRACKER_SOURCE = void 0;
11
11
  // - Only pathnames are sent — never query strings or fragments.
12
12
  // - The tracker self-disables on localhost, for bots (navigator.webdriver)
13
13
  // and after repeated 403s (analytics turned off server-side).
14
- exports.TRACKER_SOURCE = "/* TeleportHQ first-party analytics tracker. Anonymous, cookieless by default. */\nconst SERVER_URL = process.env.NEXT_PUBLIC_TELEPORT_ANALYTICS_URL\nconst PUBLIC_KEY = process.env.NEXT_PUBLIC_TELEPORT_ANALYTICS_KEY\n\nconst HEARTBEAT_INTERVAL_MS = 30000\nconst FLUSH_INTERVAL_MS = 5000\nconst FLUSH_AT_QUEUE_SIZE = 10\nconst MAX_BATCH = 25\nconst RETRY_DELAYS_MS = [1000, 5000, 15000]\nconst SESSION_WINDOW_MS = 30 * 60 * 1000\n\nlet initialized = false\nlet disabled = false\nlet forbiddenCount = 0\n\nlet sessionId = null\nlet visitorId = null\nlet pageLoadId = null\nlet seq = 0\nlet currentPath = null\nlet initialReferrer = null\nlet utm = null\n\nlet visibleSince = null\nlet visibleAccumMs = 0\nlet maxScrollPct = 0\n\nlet queue = []\nlet flushTimer = null\nlet heartbeatTimer = null\nlet retryAttempt = 0\n\nfunction uuid() {\n if (typeof crypto !== 'undefined' && crypto.randomUUID) {\n return crypto.randomUUID()\n }\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0\n return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16)\n })\n}\n\nfunction hasConsent() {\n try {\n return window.localStorage.getItem('cookieConsent') === 'accepted'\n } catch (e) {\n return false\n }\n}\n\nfunction isTrackingPossible() {\n if (disabled || typeof window === 'undefined') {\n return false\n }\n if (!SERVER_URL || !PUBLIC_KEY) {\n return false\n }\n // Fail safe: a misconfigured deploy can leave the unresolved\n // \"teleporthq.secrets.*\" placeholder (or any non-absolute value) baked into\n // these build-time vars. Never fire requests at a non-absolute URL \u2014 that\n // would point every beacon at the host site's own origin instead of the\n // analytics API.\n if (SERVER_URL.indexOf('http') !== 0 || PUBLIC_KEY.indexOf('teleporthq.secrets.') === 0) {\n return false\n }\n const host = window.location.hostname\n if (host === 'localhost' || host === '127.0.0.1' || host === '0.0.0.0') {\n return false\n }\n if (typeof navigator !== 'undefined' && navigator.webdriver) {\n return false\n }\n return true\n}\n\nfunction setupIdentity() {\n if (!hasConsent()) {\n // Consentless mode: nothing is stored on the device. The session id only\n // lives in JS memory; the server stitches sessions via its anonymized\n // daily visitor hash.\n sessionId = uuid()\n visitorId = null\n return\n }\n\n try {\n visitorId = window.localStorage.getItem('tp_aid')\n if (!visitorId) {\n visitorId = uuid()\n window.localStorage.setItem('tp_aid', visitorId)\n }\n\n const now = Date.now()\n const storedSession = window.sessionStorage.getItem('tp_sid')\n const storedAt = Number(window.sessionStorage.getItem('tp_sid_t') || 0)\n\n if (storedSession && now - storedAt < SESSION_WINDOW_MS) {\n sessionId = storedSession\n } else {\n sessionId = uuid()\n }\n window.sessionStorage.setItem('tp_sid', sessionId)\n window.sessionStorage.setItem('tp_sid_t', String(now))\n } catch (e) {\n sessionId = uuid()\n visitorId = null\n }\n}\n\nfunction touchSession() {\n if (visitorId === null) {\n return\n }\n try {\n window.sessionStorage.setItem('tp_sid_t', String(Date.now()))\n } catch (e) {\n /* storage unavailable */\n }\n}\n\nfunction parseUtm() {\n try {\n const params = new URLSearchParams(window.location.search)\n const read = (key) => {\n const value = params.get(key)\n return value ? value.slice(0, 255) : null\n }\n const parsed = {\n source: read('utm_source'),\n medium: read('utm_medium'),\n campaign: read('utm_campaign'),\n term: read('utm_term'),\n content: read('utm_content'),\n }\n const hasAny = Object.keys(parsed).some((key) => parsed[key])\n return hasAny ? parsed : null\n } catch (e) {\n return null\n }\n}\n\nfunction endpoint(suffix) {\n return SERVER_URL.replace(/\\/$/, '') + '/events/' + PUBLIC_KEY + (suffix || '')\n}\n\nfunction markForbidden(status) {\n if (status === 403 || status === 401) {\n forbiddenCount += 1\n if (forbiddenCount >= 3) {\n // Analytics disabled server-side \u2014 go silent for this page lifetime\n disabled = true\n queue = []\n if (flushTimer) clearInterval(flushTimer)\n if (heartbeatTimer) clearInterval(heartbeatTimer)\n }\n } else {\n forbiddenCount = 0\n }\n}\n\nfunction sendBatch(events, useBeacon) {\n if (events.length === 0) {\n return Promise.resolve(true)\n }\n\n const body = JSON.stringify({ events: events })\n const url = endpoint('/batch')\n\n // text/plain is a CORS-safelisted content type, so the request skips the\n // preflight. That preflight is what makes an application/json beacon fail on\n // page unload (the browser can't complete OPTIONS while the page is dying),\n // and it also doubles every normal batch into OPTIONS+POST. The server reads\n // the JSON body regardless of this content type.\n if (useBeacon && typeof navigator !== 'undefined' && navigator.sendBeacon) {\n try {\n const blob = new Blob([body], { type: 'text/plain;charset=UTF-8' })\n return Promise.resolve(navigator.sendBeacon(url, blob))\n } catch (e) {\n /* fall through to fetch */\n }\n }\n\n return fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'text/plain;charset=UTF-8' },\n body: body,\n keepalive: true,\n })\n .then((response) => {\n markForbidden(response.status)\n return response.status < 400\n })\n .catch(() => false)\n}\n\nfunction flush(useBeacon) {\n if (queue.length === 0 || disabled) {\n return\n }\n\n const batch = queue.slice(0, MAX_BATCH)\n queue = queue.slice(batch.length)\n\n sendBatch(batch, useBeacon).then((ok) => {\n if (ok) {\n retryAttempt = 0\n return\n }\n if (retryAttempt < RETRY_DELAYS_MS.length && !disabled) {\n // Re-queue once per backoff step, then drop \u2014 analytics is lossy-tolerant\n queue = batch.concat(queue)\n const delay = RETRY_DELAYS_MS[retryAttempt]\n retryAttempt += 1\n setTimeout(() => flush(false), delay)\n }\n })\n}\n\nfunction enqueue(event) {\n if (disabled) {\n return\n }\n queue.push(event)\n touchSession()\n if (queue.length >= FLUSH_AT_QUEUE_SIZE) {\n flush(false)\n }\n}\n\nfunction baseEvent(type) {\n seq += 1\n return {\n type: type,\n pageLoadId: pageLoadId,\n seq: seq - 1,\n sessionId: sessionId,\n visitorId: visitorId,\n path: currentPath,\n clientTs: Date.now(),\n }\n}\n\nfunction visibleTimeMs() {\n let total = visibleAccumMs\n if (visibleSince !== null) {\n total += Date.now() - visibleSince\n }\n return Math.max(0, Math.round(total))\n}\n\nfunction resetPageMetrics() {\n visibleAccumMs = 0\n visibleSince = document.visibilityState === 'visible' ? Date.now() : null\n maxScrollPct = 0\n}\n\nfunction currentScrollPct() {\n try {\n const doc = document.documentElement\n const scrollable = doc.scrollHeight - window.innerHeight\n if (scrollable <= 0) {\n return 100\n }\n return Math.min(100, Math.round((window.scrollY / scrollable) * 100))\n } catch (e) {\n return 0\n }\n}\n\nfunction trackPageview(isFirstLoad) {\n if (!isTrackingPossible()) {\n return\n }\n\n pageLoadId = uuid()\n seq = 0\n currentPath = window.location.pathname || '/'\n resetPageMetrics()\n\n const event = baseEvent('pageview')\n event.referrer = isFirstLoad ? (document.referrer || null) : null\n event.utm = isFirstLoad ? utm : null\n event.screenW = window.screen && window.screen.width ? window.screen.width : null\n enqueue(event)\n}\n\nfunction trackLeave(useBeacon) {\n if (!isTrackingPossible() || !pageLoadId) {\n return\n }\n\n const event = baseEvent('page_leave')\n event.timeOnPageMs = visibleTimeMs()\n event.maxScrollPct = maxScrollPct\n enqueue(event)\n\n if (useBeacon) {\n flush(true)\n }\n}\n\nfunction sendHeartbeat() {\n if (!isTrackingPossible() || document.visibilityState !== 'visible' || !pageLoadId) {\n return\n }\n\n // Heartbeats go direct (never queued) \u2014 a stale heartbeat is worthless.\n // text/plain keeps this preflight-free too (see sendBatch).\n fetch(endpoint('/heartbeat'), {\n method: 'POST',\n headers: { 'Content-Type': 'text/plain;charset=UTF-8' },\n body: JSON.stringify({\n sessionId: sessionId,\n visitorId: visitorId,\n pageLoadId: pageLoadId,\n path: currentPath,\n }),\n keepalive: true,\n })\n .then((response) => markForbidden(response.status))\n .catch(() => undefined)\n}\n\nfunction onClickCapture(domEvent) {\n if (!isTrackingPossible() || !pageLoadId) {\n return\n }\n\n try {\n const target = domEvent.target && domEvent.target.closest\n ? domEvent.target.closest('a, button, [role=\"button\"], input[type=\"submit\"], [data-tp-event]')\n : null\n if (!target) {\n return\n }\n\n const namedAttr = target.getAttribute('data-tp-event')\n let href = null\n if (target.tagName === 'A' && target.getAttribute('href')) {\n const raw = target.getAttribute('href')\n if (raw.indexOf('http') === 0) {\n try {\n const parsed = new URL(raw)\n href = parsed.origin === window.location.origin ? parsed.pathname : raw.slice(0, 512)\n } catch (e) {\n href = raw.slice(0, 512)\n }\n } else {\n href = raw.split('?')[0].split('#')[0].slice(0, 512) || null\n }\n }\n\n const event = baseEvent('click')\n event.click = {\n tag: target.tagName ? target.tagName.toLowerCase().slice(0, 16) : null,\n id: target.id ? target.id.slice(0, 64) : null,\n text: target.textContent ? target.textContent.trim().slice(0, 64) : null,\n href: href,\n name: namedAttr && /^[a-zA-Z0-9_-]{1,64}$/.test(namedAttr) ? namedAttr : null,\n }\n enqueue(event)\n } catch (e) {\n /* never break the host page */\n }\n}\n\nexport function initTeleportAnalytics() {\n if (initialized || typeof window === 'undefined') {\n return\n }\n initialized = true\n\n if (!isTrackingPossible()) {\n return\n }\n\n setupIdentity()\n utm = parseUtm()\n\n document.addEventListener('click', onClickCapture, true)\n\n document.addEventListener('visibilitychange', () => {\n if (document.visibilityState === 'visible') {\n visibleSince = Date.now()\n sendHeartbeat()\n } else {\n if (visibleSince !== null) {\n visibleAccumMs += Date.now() - visibleSince\n visibleSince = null\n }\n flush(true)\n }\n })\n\n window.addEventListener(\n 'scroll',\n () => {\n const pct = currentScrollPct()\n if (pct > maxScrollPct) {\n maxScrollPct = pct\n }\n },\n { passive: true }\n )\n\n // pagehide covers tab close, navigation away and most mobile terminations.\n // When even this never fires (power loss, process kill), the server's\n // heartbeat window + session finalizer close the visit.\n window.addEventListener('pagehide', () => {\n trackLeave(true)\n })\n\n flushTimer = setInterval(() => flush(false), FLUSH_INTERVAL_MS)\n heartbeatTimer = setInterval(sendHeartbeat, HEARTBEAT_INTERVAL_MS)\n\n trackPageview(true)\n sendHeartbeat()\n}\n\nexport function trackRouteLeave() {\n trackLeave(false)\n}\n\nexport function trackRouteChange() {\n trackPageview(false)\n sendHeartbeat()\n}\n";
14
+ exports.TRACKER_SOURCE = "/* TeleportHQ first-party analytics tracker. Anonymous, cookieless by default. */\nconst SERVER_URL = process.env.NEXT_PUBLIC_TELEPORT_ANALYTICS_URL\nconst PUBLIC_KEY = process.env.NEXT_PUBLIC_TELEPORT_ANALYTICS_KEY\n\nconst HEARTBEAT_INTERVAL_MS = 30000\nconst FLUSH_INTERVAL_MS = 5000\nconst FLUSH_AT_QUEUE_SIZE = 10\nconst MAX_BATCH = 25\nconst RETRY_DELAYS_MS = [1000, 5000, 15000]\nconst SESSION_WINDOW_MS = 30 * 60 * 1000\n\nlet initialized = false\nlet disabled = false\nlet forbiddenCount = 0\n\nlet sessionId = null\nlet visitorId = null\nlet pageLoadId = null\nlet seq = 0\nlet currentPath = null\nlet initialReferrer = null\nlet utm = null\n\nlet visibleSince = null\nlet visibleAccumMs = 0\nlet maxScrollPct = 0\n\nlet queue = []\nlet flushTimer = null\nlet heartbeatTimer = null\nlet retryAttempt = 0\n\nfunction uuid() {\n if (typeof crypto !== 'undefined' && crypto.randomUUID) {\n return crypto.randomUUID()\n }\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0\n return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16)\n })\n}\n\nfunction hasConsent() {\n try {\n return window.localStorage.getItem('cookieConsent') === 'accepted'\n } catch (e) {\n return false\n }\n}\n\nfunction isTrackingPossible() {\n if (disabled || typeof window === 'undefined') {\n return false\n }\n if (!SERVER_URL || !PUBLIC_KEY) {\n return false\n }\n // Fail safe: a misconfigured deploy can leave the unresolved\n // \"teleporthq.secrets.*\" placeholder (or any non-absolute value) baked into\n // these build-time vars. Never fire requests at a non-absolute URL \u2014 that\n // would point every beacon at the host site's own origin instead of the\n // analytics API.\n if (SERVER_URL.indexOf('http') !== 0 || PUBLIC_KEY.indexOf('teleporthq.secrets.') === 0) {\n return false\n }\n const host = window.location.hostname\n if (host === 'localhost' || host === '127.0.0.1' || host === '0.0.0.0') {\n return false\n }\n if (typeof navigator !== 'undefined' && navigator.webdriver) {\n return false\n }\n return true\n}\n\nfunction setupIdentity() {\n if (!hasConsent()) {\n // Consentless mode: nothing is stored on the device. The session id only\n // lives in JS memory; the server stitches sessions via its anonymized\n // daily visitor hash.\n sessionId = uuid()\n visitorId = null\n return\n }\n\n try {\n visitorId = window.localStorage.getItem('tp_aid')\n if (!visitorId) {\n visitorId = uuid()\n window.localStorage.setItem('tp_aid', visitorId)\n }\n\n const now = Date.now()\n const storedSession = window.sessionStorage.getItem('tp_sid')\n const storedAt = Number(window.sessionStorage.getItem('tp_sid_t') || 0)\n\n if (storedSession && now - storedAt < SESSION_WINDOW_MS) {\n sessionId = storedSession\n } else {\n sessionId = uuid()\n }\n window.sessionStorage.setItem('tp_sid', sessionId)\n window.sessionStorage.setItem('tp_sid_t', String(now))\n } catch (e) {\n sessionId = uuid()\n visitorId = null\n }\n}\n\nfunction touchSession() {\n if (visitorId === null) {\n return\n }\n try {\n window.sessionStorage.setItem('tp_sid_t', String(Date.now()))\n } catch (e) {\n /* storage unavailable */\n }\n}\n\nfunction parseUtm() {\n try {\n const params = new URLSearchParams(window.location.search)\n const read = (key) => {\n const value = params.get(key)\n return value ? value.slice(0, 255) : null\n }\n const parsed = {\n source: read('utm_source'),\n medium: read('utm_medium'),\n campaign: read('utm_campaign'),\n term: read('utm_term'),\n content: read('utm_content'),\n }\n const hasAny = Object.keys(parsed).some((key) => parsed[key])\n return hasAny ? parsed : null\n } catch (e) {\n return null\n }\n}\n\nfunction endpoint(suffix) {\n return SERVER_URL.replace(/\\/$/, '') + '/events/' + PUBLIC_KEY + (suffix || '')\n}\n\nfunction markForbidden(status) {\n if (status === 403 || status === 401) {\n forbiddenCount += 1\n if (forbiddenCount >= 3) {\n // Analytics disabled server-side \u2014 go silent for this page lifetime\n disabled = true\n queue = []\n if (flushTimer) clearInterval(flushTimer)\n if (heartbeatTimer) clearInterval(heartbeatTimer)\n }\n } else {\n forbiddenCount = 0\n }\n}\n\nfunction sendBatch(events, useBeacon) {\n if (events.length === 0) {\n return Promise.resolve(true)\n }\n\n const body = JSON.stringify({ events: events })\n const url = endpoint('/batch')\n\n // text/plain is a CORS-safelisted content type, so the request skips the\n // preflight. That preflight is what makes an application/json beacon fail on\n // page unload (the browser can't complete OPTIONS while the page is dying),\n // and it also doubles every normal batch into OPTIONS+POST. The server reads\n // the JSON body regardless of this content type.\n if (useBeacon && typeof navigator !== 'undefined' && navigator.sendBeacon) {\n try {\n const blob = new Blob([body], { type: 'text/plain;charset=UTF-8' })\n return Promise.resolve(navigator.sendBeacon(url, blob))\n } catch (e) {\n /* fall through to fetch */\n }\n }\n\n return fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'text/plain;charset=UTF-8' },\n body: body,\n keepalive: true,\n })\n .then((response) => {\n markForbidden(response.status)\n return response.status < 400\n })\n .catch(() => false)\n}\n\nfunction flush(useBeacon) {\n if (queue.length === 0 || disabled) {\n return\n }\n\n const batch = queue.slice(0, MAX_BATCH)\n queue = queue.slice(batch.length)\n\n sendBatch(batch, useBeacon).then((ok) => {\n if (ok) {\n retryAttempt = 0\n return\n }\n if (retryAttempt < RETRY_DELAYS_MS.length && !disabled) {\n // Re-queue once per backoff step, then drop \u2014 analytics is lossy-tolerant\n queue = batch.concat(queue)\n const delay = RETRY_DELAYS_MS[retryAttempt]\n retryAttempt += 1\n setTimeout(() => flush(false), delay)\n }\n })\n}\n\nfunction enqueue(event) {\n if (disabled) {\n return\n }\n queue.push(event)\n touchSession()\n if (queue.length >= FLUSH_AT_QUEUE_SIZE) {\n flush(false)\n }\n}\n\nfunction baseEvent(type) {\n seq += 1\n return {\n type: type,\n pageLoadId: pageLoadId,\n seq: seq - 1,\n sessionId: sessionId,\n visitorId: visitorId,\n path: currentPath,\n clientTs: Date.now(),\n }\n}\n\nfunction visibleTimeMs() {\n let total = visibleAccumMs\n if (visibleSince !== null) {\n total += Date.now() - visibleSince\n }\n return Math.max(0, Math.round(total))\n}\n\nfunction resetPageMetrics() {\n visibleAccumMs = 0\n visibleSince = document.visibilityState === 'visible' ? Date.now() : null\n maxScrollPct = 0\n}\n\nfunction currentScrollPct() {\n try {\n const doc = document.documentElement\n const scrollable = doc.scrollHeight - window.innerHeight\n if (scrollable <= 0) {\n return 100\n }\n return Math.min(100, Math.round((window.scrollY / scrollable) * 100))\n } catch (e) {\n return 0\n }\n}\n\nfunction pathnameOf(url) {\n if (typeof url !== 'string' || !url) {\n return null\n }\n return url.split('?')[0].split('#')[0] || '/'\n}\n\n// True when a route event resolves to the page we are already tracking. The\n// pages-router emits routeChangeStart/Complete for the SAME path during initial\n// hydration (and when a link points at the current page), which would otherwise\n// double-count the pageview and emit a bogus 0ms page_leave.\nfunction samePathAsCurrent(url) {\n const next = pathnameOf(url)\n return currentPath !== null && next !== null && next === currentPath\n}\n\nfunction trackPageview(isFirstLoad) {\n if (!isTrackingPossible()) {\n return\n }\n\n const newPath = window.location.pathname || '/'\n // Defensive same-path guard (the route handlers also guard on the router's\n // destination url): never emit a second pageview for the page already shown.\n if (!isFirstLoad && currentPath !== null && newPath === currentPath) {\n return\n }\n\n pageLoadId = uuid()\n seq = 0\n currentPath = newPath\n resetPageMetrics()\n\n const event = baseEvent('pageview')\n event.referrer = isFirstLoad ? (document.referrer || null) : null\n event.utm = isFirstLoad ? utm : null\n event.screenW = window.screen && window.screen.width ? window.screen.width : null\n enqueue(event)\n}\n\nfunction trackLeave(useBeacon) {\n if (!isTrackingPossible() || !pageLoadId) {\n return\n }\n\n const event = baseEvent('page_leave')\n event.timeOnPageMs = visibleTimeMs()\n event.maxScrollPct = maxScrollPct\n enqueue(event)\n\n if (useBeacon) {\n flush(true)\n }\n}\n\nfunction sendHeartbeat() {\n if (!isTrackingPossible() || document.visibilityState !== 'visible' || !pageLoadId) {\n return\n }\n\n // Heartbeats go direct (never queued) \u2014 a stale heartbeat is worthless.\n // text/plain keeps this preflight-free too (see sendBatch).\n fetch(endpoint('/heartbeat'), {\n method: 'POST',\n headers: { 'Content-Type': 'text/plain;charset=UTF-8' },\n body: JSON.stringify({\n sessionId: sessionId,\n visitorId: visitorId,\n pageLoadId: pageLoadId,\n path: currentPath,\n }),\n keepalive: true,\n })\n .then((response) => markForbidden(response.status))\n .catch(() => undefined)\n}\n\nfunction onClickCapture(domEvent) {\n if (!isTrackingPossible() || !pageLoadId) {\n return\n }\n\n try {\n const target = domEvent.target && domEvent.target.closest\n ? domEvent.target.closest('a, button, [role=\"button\"], input[type=\"submit\"], [data-tp-event]')\n : null\n if (!target) {\n return\n }\n\n const namedAttr = target.getAttribute('data-tp-event')\n let href = null\n if (target.tagName === 'A' && target.getAttribute('href')) {\n const raw = target.getAttribute('href')\n if (raw.indexOf('http') === 0) {\n try {\n const parsed = new URL(raw)\n href = parsed.origin === window.location.origin ? parsed.pathname : raw.slice(0, 512)\n } catch (e) {\n href = raw.slice(0, 512)\n }\n } else {\n href = raw.split('?')[0].split('#')[0].slice(0, 512) || null\n }\n }\n\n const event = baseEvent('click')\n event.click = {\n tag: target.tagName ? target.tagName.toLowerCase().slice(0, 16) : null,\n id: target.id ? target.id.slice(0, 64) : null,\n text: target.textContent ? target.textContent.trim().slice(0, 64) : null,\n href: href,\n name: namedAttr && /^[a-zA-Z0-9_-]{1,64}$/.test(namedAttr) ? namedAttr : null,\n }\n enqueue(event)\n } catch (e) {\n /* never break the host page */\n }\n}\n\nexport function initTeleportAnalytics() {\n if (initialized || typeof window === 'undefined') {\n return\n }\n initialized = true\n\n if (!isTrackingPossible()) {\n return\n }\n\n setupIdentity()\n utm = parseUtm()\n\n document.addEventListener('click', onClickCapture, true)\n\n document.addEventListener('visibilitychange', () => {\n if (document.visibilityState === 'visible') {\n visibleSince = Date.now()\n sendHeartbeat()\n } else {\n if (visibleSince !== null) {\n visibleAccumMs += Date.now() - visibleSince\n visibleSince = null\n }\n flush(true)\n }\n })\n\n window.addEventListener(\n 'scroll',\n () => {\n const pct = currentScrollPct()\n if (pct > maxScrollPct) {\n maxScrollPct = pct\n }\n },\n { passive: true }\n )\n\n // pagehide covers tab close, navigation away and most mobile terminations.\n // When even this never fires (power loss, process kill), the server's\n // heartbeat window + session finalizer close the visit.\n window.addEventListener('pagehide', () => {\n trackLeave(true)\n })\n\n flushTimer = setInterval(() => flush(false), FLUSH_INTERVAL_MS)\n heartbeatTimer = setInterval(sendHeartbeat, HEARTBEAT_INTERVAL_MS)\n\n trackPageview(true)\n sendHeartbeat()\n}\n\nexport function trackRouteLeave(url) {\n // A same-path routeChangeStart (hydration) is not a real leave \u2014 skip it so we\n // don't enqueue a 0ms page_leave. Genuine end-of-visit goes through pagehide\n // (trackLeave(true)), which is intentionally unconditional.\n if (samePathAsCurrent(url)) {\n return\n }\n trackLeave(false)\n}\n\nexport function trackRouteChange(url) {\n if (samePathAsCurrent(url)) {\n return\n }\n trackPageview(false)\n sendHeartbeat()\n}\n";
15
15
  //# sourceMappingURL=tracker-source.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"tracker-source.js","sourceRoot":"","sources":["../../../src/analytics/tracker-source.ts"],"names":[],"mappings":";;;AAAA,4EAA4E;AAC5E,6DAA6D;AAC7D,EAAE;AACF,mDAAmD;AACnD,yEAAyE;AACzE,6EAA6E;AAC7E,0EAA0E;AAC1E,gEAAgE;AAChE,2EAA2E;AAC3E,gEAAgE;AACnD,QAAA,cAAc,GAAG,otWA2a7B,CAAA"}
1
+ {"version":3,"file":"tracker-source.js","sourceRoot":"","sources":["../../../src/analytics/tracker-source.ts"],"names":[],"mappings":";;;AAAA,4EAA4E;AAC5E,6DAA6D;AAC7D,EAAE;AACF,mDAAmD;AACnD,yEAAyE;AACzE,6EAA6E;AAC7E,0EAA0E;AAC1E,gEAAgE;AAChE,2EAA2E;AAC3E,gEAAgE;AACnD,QAAA,cAAc,GAAG,k5YA2c7B,CAAA"}
@@ -5,6 +5,8 @@ import { ProjectPlugin, ProjectPluginStructure } from '@teleporthq/teleport-type
5
5
  * - bumps react/react-dom to ^18 — calendarkit-basic requires React 18
6
6
  * (the template's `next` range already supports React 18, so `next`
7
7
  * itself is left untouched)
8
+ * - writes an .npmrc (legacy-peer-deps) so the React-18 bump survives template
9
+ * deps with React-17-only peer ranges (e.g. dangerous-html embeds)
8
10
  * - writes the precompiled CalendarKit stylesheet to pages/calendarkit.css
9
11
  * (the library ships uncompiled Tailwind, see scripts/generate-calendarkit-css.mjs)
10
12
  * - imports the stylesheet from _app, where Next.js requires global CSS
@@ -1 +1 @@
1
- {"version":3,"file":"project-plugin.d.ts","sourceRoot":"","sources":["../../../src/calendar/project-plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,aAAa,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAA;AAO5F;;;;;;;;;GASG;AACH,qBAAa,4BAA6B,YAAW,aAAa;IAC1D,SAAS,CAAC,SAAS,EAAE,sBAAsB,GAAG,OAAO,CAAC,sBAAsB,CAAC;IAI7E,QAAQ,CAAC,SAAS,EAAE,sBAAsB,GAAG,OAAO,CAAC,sBAAsB,CAAC;CAyBnF"}
1
+ {"version":3,"file":"project-plugin.d.ts","sourceRoot":"","sources":["../../../src/calendar/project-plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,aAAa,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAA;AAQ5F;;;;;;;;;;;GAWG;AACH,qBAAa,4BAA6B,YAAW,aAAa;IAC1D,SAAS,CAAC,SAAS,EAAE,sBAAsB,GAAG,OAAO,CAAC,sBAAsB,CAAC;IAI7E,QAAQ,CAAC,SAAS,EAAE,sBAAsB,GAAG,OAAO,CAAC,sBAAsB,CAAC;CA0BnF"}
@@ -39,6 +39,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.NextCalendarKitProjectPlugin = void 0;
40
40
  var teleport_types_1 = require("@teleporthq/teleport-types");
41
41
  var app_import_injection_1 = require("../app-import-injection");
42
+ var npmrc_legacy_peer_deps_1 = require("../npmrc-legacy-peer-deps");
42
43
  var calendarkit_css_1 = require("./calendarkit-css");
43
44
  var CALENDARKIT_PACKAGE = 'calendarkit-basic';
44
45
  var CALENDARKIT_CSS_IMPORT = "import './calendarkit.css'";
@@ -48,6 +49,8 @@ var CALENDARKIT_CSS_IMPORT = "import './calendarkit.css'";
48
49
  * - bumps react/react-dom to ^18 — calendarkit-basic requires React 18
49
50
  * (the template's `next` range already supports React 18, so `next`
50
51
  * itself is left untouched)
52
+ * - writes an .npmrc (legacy-peer-deps) so the React-18 bump survives template
53
+ * deps with React-17-only peer ranges (e.g. dangerous-html embeds)
51
54
  * - writes the precompiled CalendarKit stylesheet to pages/calendarkit.css
52
55
  * (the library ships uncompiled Tailwind, see scripts/generate-calendarkit-css.mjs)
53
56
  * - imports the stylesheet from _app, where Next.js requires global CSS
@@ -72,6 +75,7 @@ var NextCalendarKitProjectPlugin = /** @class */ (function () {
72
75
  }
73
76
  dependencies.react = '^18.3.1';
74
77
  dependencies['react-dom'] = '^18.3.1';
78
+ (0, npmrc_legacy_peer_deps_1.emitLegacyPeerDepsNpmrc)(structure, 'calendarkit-npmrc');
75
79
  files.set('calendarkit-css', {
76
80
  path: ['pages'],
77
81
  files: [
@@ -1 +1 @@
1
- {"version":3,"file":"project-plugin.js","sourceRoot":"","sources":["../../../src/calendar/project-plugin.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,6DAA4F;AAC5F,gEAA6D;AAC7D,qDAAmD;AAEnD,IAAM,mBAAmB,GAAG,mBAAmB,CAAA;AAC/C,IAAM,sBAAsB,GAAG,4BAA4B,CAAA;AAE3D;;;;;;;;;GASG;AACH;IAAA;IA8BA,CAAC;IA7BO,gDAAS,GAAf,UAAgB,SAAiC;;;gBAC/C,sBAAO,SAAS,EAAA;;;KACjB;IAEK,+CAAQ,GAAd,UAAe,SAAiC;;;;gBACtC,YAAY,GAAY,SAAS,aAArB,EAAE,KAAK,GAAK,SAAS,MAAd,CAAc;gBAEzC,IAAI,CAAC,YAAY,CAAC,mBAAmB,CAAC,EAAE;oBACtC,sBAAO,SAAS,EAAA;iBACjB;gBAED,YAAY,CAAC,KAAK,GAAG,SAAS,CAAA;gBAC9B,YAAY,CAAC,WAAW,CAAC,GAAG,SAAS,CAAA;gBAErC,KAAK,CAAC,GAAG,CAAC,iBAAiB,EAAE;oBAC3B,IAAI,EAAE,CAAC,OAAO,CAAC;oBACf,KAAK,EAAE;wBACL;4BACE,IAAI,EAAE,aAAa;4BACnB,QAAQ,EAAE,yBAAQ,CAAC,GAAG;4BACtB,OAAO,EAAE,iCAAe;yBACzB;qBACF;iBACF,CAAC,CAAA;gBAEF,IAAA,0CAAmB,EAAC,SAAS,EAAE,sBAAsB,CAAC,CAAA;gBAEtD,sBAAO,SAAS,EAAA;;;KACjB;IACH,mCAAC;AAAD,CAAC,AA9BD,IA8BC;AA9BY,oEAA4B"}
1
+ {"version":3,"file":"project-plugin.js","sourceRoot":"","sources":["../../../src/calendar/project-plugin.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,6DAA4F;AAC5F,gEAA6D;AAC7D,oEAAmE;AACnE,qDAAmD;AAEnD,IAAM,mBAAmB,GAAG,mBAAmB,CAAA;AAC/C,IAAM,sBAAsB,GAAG,4BAA4B,CAAA;AAE3D;;;;;;;;;;;GAWG;AACH;IAAA;IA+BA,CAAC;IA9BO,gDAAS,GAAf,UAAgB,SAAiC;;;gBAC/C,sBAAO,SAAS,EAAA;;;KACjB;IAEK,+CAAQ,GAAd,UAAe,SAAiC;;;;gBACtC,YAAY,GAAY,SAAS,aAArB,EAAE,KAAK,GAAK,SAAS,MAAd,CAAc;gBAEzC,IAAI,CAAC,YAAY,CAAC,mBAAmB,CAAC,EAAE;oBACtC,sBAAO,SAAS,EAAA;iBACjB;gBAED,YAAY,CAAC,KAAK,GAAG,SAAS,CAAA;gBAC9B,YAAY,CAAC,WAAW,CAAC,GAAG,SAAS,CAAA;gBACrC,IAAA,gDAAuB,EAAC,SAAS,EAAE,mBAAmB,CAAC,CAAA;gBAEvD,KAAK,CAAC,GAAG,CAAC,iBAAiB,EAAE;oBAC3B,IAAI,EAAE,CAAC,OAAO,CAAC;oBACf,KAAK,EAAE;wBACL;4BACE,IAAI,EAAE,aAAa;4BACnB,QAAQ,EAAE,yBAAQ,CAAC,GAAG;4BACtB,OAAO,EAAE,iCAAe;yBACzB;qBACF;iBACF,CAAC,CAAA;gBAEF,IAAA,0CAAmB,EAAC,SAAS,EAAE,sBAAsB,CAAC,CAAA;gBAEtD,sBAAO,SAAS,EAAA;;;KACjB;IACH,mCAAC;AAAD,CAAC,AA/BD,IA+BC;AA/BY,oEAA4B"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Generates the TqCountdown wrapper component source.
3
+ *
4
+ * It uses react-countdown (the SAME library the editor canvas drives via
5
+ * `calcTimeDelta`/`zeroPad`) so the published countdown shows identical digits.
6
+ * The markup + class names (.tq-countdown / .tq-countdown-unit /
7
+ * .tq-countdown-value / .tq-countdown-label) match the renderer one-for-one.
8
+ *
9
+ * SSR safety: the live value depends on the visitor's clock, which differs from
10
+ * the server render and would throw a hydration mismatch. The component renders
11
+ * a deterministic placeholder (zeros, same structure) until mounted, then swaps
12
+ * to the live countdown — so hydration matches and ticking starts client-side.
13
+ */
14
+ export declare const generateCountdownComponentCode: () => string;
15
+ //# sourceMappingURL=component-generator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"component-generator.d.ts","sourceRoot":"","sources":["../../../src/countdown/component-generator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,8BAA8B,QAAO,MA0JjD,CAAA"}