@teleporthq/teleport-project-generator-next 0.43.45 → 0.43.46

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,206 @@
1
+ /**
2
+ * A minimal Rules-of-Hooks analyser for the React source the Next project
3
+ * generator emits as plain strings.
4
+ *
5
+ * Those component files are template literals, so TypeScript, ESLint and the
6
+ * react-hooks plugin never see them — a hook placed after an early `return`
7
+ * type-checks, lints and builds cleanly and then throws
8
+ * "Rendered more hooks than during the previous render" in the browser on the
9
+ * second render. `tq-countdown` shipped exactly that defect: its `targetDate`
10
+ * useMemo sat below the mounted-guard return, so the component rendered 7 hooks
11
+ * on the first pass and 8 on the next, crashing every page that embedded it.
12
+ *
13
+ * The two conditions checked here are the two that React enforces at runtime:
14
+ * 1. a hook may not run after a statement that can return early
15
+ * 2. a hook may not run inside a condition, loop or try block
16
+ *
17
+ * Hook calls inside NESTED functions (a `useEffect` callback, an event handler,
18
+ * a `.map()` body) belong to that inner function, not the component, so the walk
19
+ * deliberately stops at every function boundary.
20
+ */
21
+
22
+ import { parse } from '@babel/parser'
23
+
24
+ type Node = Record<string, unknown> & { type: string; loc?: { start: { line: number } } }
25
+
26
+ const FUNCTION_TYPES = new Set([
27
+ 'FunctionDeclaration',
28
+ 'FunctionExpression',
29
+ 'ArrowFunctionExpression',
30
+ 'ObjectMethod',
31
+ 'ClassMethod',
32
+ ])
33
+
34
+ const CONTROL_FLOW_TYPES = new Set([
35
+ 'IfStatement',
36
+ 'ForStatement',
37
+ 'ForInStatement',
38
+ 'ForOfStatement',
39
+ 'WhileStatement',
40
+ 'DoWhileStatement',
41
+ 'SwitchStatement',
42
+ 'TryStatement',
43
+ 'ConditionalExpression',
44
+ 'LogicalExpression',
45
+ ])
46
+
47
+ const HOOK_NAME = /^use[A-Z]/
48
+
49
+ export type HookViolationKind = 'after-early-return' | 'conditional'
50
+
51
+ export interface HookViolation {
52
+ kind: HookViolationKind
53
+ hook: string
54
+ /** Enclosing function name, or `<anonymous>`. */
55
+ fn: string
56
+ line: number
57
+ }
58
+
59
+ function isNode(value: unknown): value is Node {
60
+ return !!value && typeof value === 'object' && typeof (value as Node).type === 'string'
61
+ }
62
+
63
+ /** Every child node of `node`, in source order. */
64
+ function childNodes(node: Node): Node[] {
65
+ const out: Node[] = []
66
+ for (const key of Object.keys(node)) {
67
+ if (key === 'loc' || key === 'leadingComments' || key === 'trailingComments') {
68
+ continue
69
+ }
70
+ const value = (node as Record<string, unknown>)[key]
71
+ if (Array.isArray(value)) {
72
+ for (const entry of value) {
73
+ if (isNode(entry)) {
74
+ out.push(entry)
75
+ }
76
+ }
77
+ continue
78
+ }
79
+ if (isNode(value)) {
80
+ out.push(value)
81
+ }
82
+ }
83
+ return out
84
+ }
85
+
86
+ function callName(node: Node): string | null {
87
+ if (node.type !== 'CallExpression') {
88
+ return null
89
+ }
90
+ const callee = node.callee as Node | undefined
91
+ if (!callee) {
92
+ return null
93
+ }
94
+ if (callee.type === 'Identifier') {
95
+ return String((callee as unknown as { name: string }).name)
96
+ }
97
+ // React.useMemo(...) — the hook is the property, not the object.
98
+ if (callee.type === 'MemberExpression') {
99
+ const property = (callee as unknown as { property?: Node }).property
100
+ if (property && property.type === 'Identifier') {
101
+ return String((property as unknown as { name: string }).name)
102
+ }
103
+ }
104
+ return null
105
+ }
106
+
107
+ /** Walks `node` and its descendants, stopping at nested function boundaries. */
108
+ function walkOwnScope(
109
+ node: Node,
110
+ visit: (current: Node, insideControlFlow: boolean) => void,
111
+ insideControlFlow = false
112
+ ): void {
113
+ for (const child of childNodes(node)) {
114
+ if (FUNCTION_TYPES.has(child.type)) {
115
+ continue
116
+ }
117
+ const nested = insideControlFlow || CONTROL_FLOW_TYPES.has(child.type)
118
+ visit(child, insideControlFlow)
119
+ walkOwnScope(child, visit, nested)
120
+ }
121
+ }
122
+
123
+ /** True when `statement` can hand control back to the caller before the next statement runs. */
124
+ function canReturnEarly(statement: Node): boolean {
125
+ if (statement.type === 'ReturnStatement' || statement.type === 'ThrowStatement') {
126
+ return true
127
+ }
128
+ let found = false
129
+ walkOwnScope(statement, (current) => {
130
+ if (current.type === 'ReturnStatement' || current.type === 'ThrowStatement') {
131
+ found = true
132
+ }
133
+ })
134
+ return found
135
+ }
136
+
137
+ function functionName(node: Node, parentName: string | null): string {
138
+ const id = (node as unknown as { id?: { name?: string } }).id
139
+ if (id && id.name) {
140
+ return id.name
141
+ }
142
+ return parentName ?? '<anonymous>'
143
+ }
144
+
145
+ function analyseFunction(node: Node, name: string, violations: HookViolation[]): void {
146
+ const body = (node as unknown as { body?: Node }).body
147
+ if (!body || body.type !== 'BlockStatement') {
148
+ return
149
+ }
150
+ const statements = ((body as unknown as { body?: Node[] }).body ?? []).filter(isNode)
151
+
152
+ let earlyReturnIndex = -1
153
+ for (let index = 0; index < statements.length; index++) {
154
+ if (canReturnEarly(statements[index])) {
155
+ earlyReturnIndex = index
156
+ break
157
+ }
158
+ }
159
+
160
+ statements.forEach((statement, index) => {
161
+ const inspect = (current: Node, insideControlFlow: boolean): void => {
162
+ const called = callName(current)
163
+ if (!called || !HOOK_NAME.test(called)) {
164
+ return
165
+ }
166
+ const line = current.loc ? current.loc.start.line : 0
167
+ if (insideControlFlow || CONTROL_FLOW_TYPES.has(statement.type)) {
168
+ violations.push({ kind: 'conditional', hook: called, fn: name, line })
169
+ return
170
+ }
171
+ if (earlyReturnIndex !== -1 && index > earlyReturnIndex) {
172
+ violations.push({ kind: 'after-early-return', hook: called, fn: name, line })
173
+ }
174
+ }
175
+ inspect(statement, false)
176
+ walkOwnScope(statement, inspect)
177
+ })
178
+ }
179
+
180
+ /**
181
+ * Returns every Rules-of-Hooks violation in `source`.
182
+ *
183
+ * `source` must be a complete ES module — the generators emit exactly that.
184
+ */
185
+ export function findRulesOfHooksViolations(source: string): HookViolation[] {
186
+ const ast = parse(source, { sourceType: 'module', plugins: ['jsx'] }) as unknown as Node
187
+ const violations: HookViolation[] = []
188
+
189
+ const visit = (node: Node, inferredName: string | null): void => {
190
+ if (FUNCTION_TYPES.has(node.type)) {
191
+ analyseFunction(node, functionName(node, inferredName), violations)
192
+ }
193
+ for (const child of childNodes(node)) {
194
+ // `const TqCountdown = (props) => {...}` — carry the binding name onto the arrow.
195
+ let nextName: string | null = null
196
+ if (node.type === 'VariableDeclarator') {
197
+ const id = (node as unknown as { id?: { name?: string } }).id
198
+ nextName = id && id.name ? id.name : null
199
+ }
200
+ visit(child, nextName)
201
+ }
202
+ }
203
+
204
+ visit(ast, null)
205
+ return violations
206
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Every React component the Next project generator emits as a source STRING is
3
+ * invisible to TypeScript, ESLint and eslint-plugin-react-hooks. This suite is
4
+ * the only thing standing between a Rules-of-Hooks mistake in one of those
5
+ * templates and a runtime crash on every published page that uses it.
6
+ *
7
+ * Regression origin: `tq-countdown` computed its `targetDate` with `useMemo`
8
+ * BELOW the `if (!mounted) return …` guard. React counted 7 hooks on the first
9
+ * render and 8 on the second and threw "Rendered more hooks than during the
10
+ * previous render". The countdown lives in the Navigation component, so the
11
+ * whole site fell through to the error boundary — on localhost and on Vercel.
12
+ */
13
+
14
+ import { findRulesOfHooksViolations } from './_helpers/rules-of-hooks'
15
+
16
+ import { generateCountdownComponentCode } from '../src/countdown/component-generator'
17
+ import { generateKanbanComponentCode } from '../src/kanban/component-generator'
18
+ import { generateDragDropComponentCode } from '../src/drag-drop/component-generator'
19
+ import { generateRichTextEditorComponentCode } from '../src/rich-text-editor/component-generator'
20
+ import { generateMotionComponentCode } from '../src/widgets/motion-component'
21
+ import { generateSignatureComponentCode } from '../src/widgets/signature-component'
22
+ import { generateCategoriesFilterComponentCode } from '../src/widgets/categories-filter-component'
23
+ import { generateCategoriesMegamenuComponentCode } from '../src/widgets/categories-megamenu-component'
24
+ import { generateBarcodeComponentCode } from '../src/widgets/barcode-component'
25
+ import { generateQrCodeComponentCode } from '../src/widgets/qrcode-component'
26
+ import { generateEmojiPickerComponentCode } from '../src/widgets/emoji-picker-component'
27
+ import { generateColorPickerComponentCode } from '../src/widgets/color-picker-component'
28
+ import { generateFormFileInputComponentCode } from '../src/widgets/form-file-input-component'
29
+ import { COLLAPSIBLE_TEXT_OVERFLOW_COMPONENT_SOURCE } from '../src/collapsible-text/collapsible-text-overflow-component'
30
+ import { NAV_ACTIVE_LINK_COMPONENT_SOURCE } from '../src/nav-active-link/nav-active-link-component'
31
+ import { TRACKER_COMPONENT_SOURCE } from '../src/analytics/tracker-component'
32
+
33
+ const EMITTED_COMPONENTS: Array<[string, string]> = [
34
+ ['tq-countdown', generateCountdownComponentCode()],
35
+ ['tq-kanban', generateKanbanComponentCode()],
36
+ ['tq-drag-drop', generateDragDropComponentCode()],
37
+ ['rich-text-editor', generateRichTextEditorComponentCode()],
38
+ ['tq-motion', generateMotionComponentCode()],
39
+ ['tq-signature', generateSignatureComponentCode()],
40
+ ['tq-categories-filter', generateCategoriesFilterComponentCode()],
41
+ ['tq-categories-megamenu', generateCategoriesMegamenuComponentCode()],
42
+ ['tq-barcode', generateBarcodeComponentCode()],
43
+ ['tq-qrcode', generateQrCodeComponentCode()],
44
+ ['tq-emoji-picker', generateEmojiPickerComponentCode()],
45
+ ['tq-color-picker', generateColorPickerComponentCode()],
46
+ ['tq-form-file-input', generateFormFileInputComponentCode()],
47
+ ['tq-collapsible-text-overflow', COLLAPSIBLE_TEXT_OVERFLOW_COMPONENT_SOURCE],
48
+ ['nav-active-links', NAV_ACTIVE_LINK_COMPONENT_SOURCE],
49
+ ['analytics-tracker', TRACKER_COMPONENT_SOURCE],
50
+ ]
51
+
52
+ describe('emitted React components obey the Rules of Hooks', () => {
53
+ it.each(EMITTED_COMPONENTS)(
54
+ '%s calls no hook conditionally or after an early return',
55
+ (_name, source) => {
56
+ const violations = findRulesOfHooksViolations(source)
57
+ expect(violations).toEqual([])
58
+ }
59
+ )
60
+
61
+ it('covers every component source the generator can emit', () => {
62
+ // A new widget added without a row here would ship unguarded.
63
+ expect(EMITTED_COMPONENTS).toHaveLength(16)
64
+ })
65
+ })
66
+
67
+ describe('the Rules-of-Hooks analyser itself', () => {
68
+ it('flags a hook placed after a conditional early return', () => {
69
+ const violations = findRulesOfHooksViolations(`
70
+ import React, { useState, useMemo } from 'react'
71
+ const Broken = ({ value }) => {
72
+ const [mounted, setMounted] = useState(false)
73
+ if (!mounted) {
74
+ return null
75
+ }
76
+ const doubled = useMemo(() => value * 2, [value])
77
+ return <span>{doubled}</span>
78
+ }
79
+ export default Broken
80
+ `)
81
+ expect(violations).toEqual([
82
+ { kind: 'after-early-return', hook: 'useMemo', fn: 'Broken', line: 8 },
83
+ ])
84
+ })
85
+
86
+ it('flags a hook called inside a condition', () => {
87
+ const violations = findRulesOfHooksViolations(`
88
+ import React, { useEffect } from 'react'
89
+ const Broken = ({ enabled }) => {
90
+ if (enabled) {
91
+ useEffect(() => {}, [])
92
+ }
93
+ return null
94
+ }
95
+ export default Broken
96
+ `)
97
+ expect(violations).toEqual([{ kind: 'conditional', hook: 'useEffect', fn: 'Broken', line: 5 }])
98
+ })
99
+
100
+ it('does not flag an early return INSIDE a hook callback', () => {
101
+ const violations = findRulesOfHooksViolations(`
102
+ import React, { useEffect, useMemo } from 'react'
103
+ const Fine = ({ mode, value }) => {
104
+ useEffect(() => {
105
+ if (mode !== 'numeric') {
106
+ return undefined
107
+ }
108
+ const id = setInterval(() => {}, 1000)
109
+ return () => clearInterval(id)
110
+ }, [mode])
111
+ const doubled = useMemo(() => value * 2, [value])
112
+ return <span>{doubled}</span>
113
+ }
114
+ export default Fine
115
+ `)
116
+ expect(violations).toEqual([])
117
+ })
118
+
119
+ it('does not flag hooks in a sibling component declared after one that returns early', () => {
120
+ const violations = findRulesOfHooksViolations(`
121
+ import React, { useState } from 'react'
122
+ const First = () => {
123
+ return null
124
+ }
125
+ const Second = () => {
126
+ const [value] = useState(0)
127
+ return <span>{value}</span>
128
+ }
129
+ export default Second
130
+ `)
131
+ expect(violations).toEqual([])
132
+ })
133
+ })
@@ -1 +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"}
1
+ {"version":3,"file":"component-generator.d.ts","sourceRoot":"","sources":["../../../src/countdown/component-generator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,8BAA8B,QAAO,MA4JjD,CAAA"}
@@ -15,7 +15,7 @@ exports.generateCountdownComponentCode = void 0;
15
15
  * to the live countdown — so hydration matches and ticking starts client-side.
16
16
  */
17
17
  var generateCountdownComponentCode = function () {
18
- return "import React, { useState, useEffect, useMemo } from 'react'\nimport Countdown, { zeroPad } from 'react-countdown'\n\nconst ALL_UNITS = ['days', 'hours', 'minutes', 'seconds']\nconst DEFAULT_LABELS = { days: 'Days', hours: 'Hours', minutes: 'Minutes', seconds: 'Seconds' }\n\nconst parseCsv = (value) =>\n typeof value === 'string' ? value.split(',').map((s) => s.trim()).filter(Boolean) : []\n\nconst resolveUnits = (units) => {\n const parsed = parseCsv(units).filter((u) => ALL_UNITS.indexOf(u) !== -1)\n return parsed.length > 0 ? parsed : ALL_UNITS\n}\n\nconst resolveLabels = (units, labels) => {\n const raw = parseCsv(labels)\n return units.map((u, i) => raw[i] || DEFAULT_LABELS[u] || u)\n}\n\nconst wrapperStyle = (format) => ({\n display: 'flex',\n alignItems: 'center',\n gap: format === 'inline' ? 0 : 16,\n fontVariantNumeric: 'tabular-nums',\n})\n\nconst renderSegments = (segments, format) => {\n if (format === 'inline') {\n return (\n <div className=\"tq-countdown tq-countdown-inline\" style={wrapperStyle('inline')}>\n <span className=\"tq-countdown-value\">{segments.map((s) => s.value).join(':')}</span>\n </div>\n )\n }\n return (\n <div className=\"tq-countdown tq-countdown-blocks\" style={wrapperStyle('blocks')}>\n {segments.map((s, i) => (\n <div\n key={i}\n className=\"tq-countdown-unit\"\n style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}\n >\n <span className=\"tq-countdown-value\" style={{ fontSize: '2em', fontWeight: 700, lineHeight: 1 }}>\n {s.value}\n </span>\n {s.label ? (\n <span className=\"tq-countdown-label\" style={{ fontSize: '0.75em', opacity: 0.7 }}>\n {s.label}\n </span>\n ) : null}\n </div>\n ))}\n </div>\n )\n}\n\nconst dateSegments = (timeDelta, units, labels) => {\n const showDays = units.indexOf('days') !== -1\n const hours = showDays ? timeDelta.hours : timeDelta.days * 24 + timeDelta.hours\n const byUnit = {\n days: timeDelta.days,\n hours: hours,\n minutes: timeDelta.minutes,\n seconds: timeDelta.seconds,\n }\n return units.map((unit, i) => ({ value: zeroPad(byUnit[unit] != null ? byUnit[unit] : 0), label: labels[i] || '' }))\n}\n\nconst TqCountdown = ({\n mode = 'date',\n target,\n from = '100',\n to = '0',\n format = 'blocks',\n units,\n labels,\n onComplete = 'keep-zero',\n completeMessage = '',\n ...rest\n}) => {\n const [mounted, setMounted] = useState(false)\n const [numericStart] = useState(() => Date.now())\n const [now, setNow] = useState(() => Date.now())\n\n useEffect(() => {\n setMounted(true)\n }, [])\n\n // Numeric mode ticks via its own interval.\n useEffect(() => {\n if (mode !== 'numeric') {\n return undefined\n }\n const id = setInterval(() => setNow(Date.now()), 1000)\n return () => clearInterval(id)\n }, [mode])\n\n const resolvedUnits = useMemo(() => resolveUnits(units), [units])\n const resolvedLabels = useMemo(() => resolveLabels(resolvedUnits, labels), [resolvedUnits, labels])\n\n // Deterministic placeholder (zeros) for SSR + first paint, so hydration matches.\n if (!mounted) {\n const zeros =\n mode === 'numeric'\n ? [{ value: String(from), label: resolvedLabels[0] || '' }]\n : resolvedUnits.map((u, i) => ({ value: '00', label: resolvedLabels[i] || '' }))\n return <div {...rest}>{renderSegments(zeros, format)}</div>\n }\n\n if (mode === 'numeric') {\n const fromN = parseFloat(from) || 0\n const toN = parseFloat(to) || 0\n const elapsed = Math.max(0, Math.floor((now - numericStart) / 1000))\n const current = fromN > toN ? Math.max(toN, fromN - elapsed) : Math.min(toN, fromN + elapsed)\n const completed = current === toN\n if (completed && onComplete === 'hide') {\n return <div {...rest} />\n }\n if (completed && onComplete === 'show-message') {\n return (\n <div {...rest}>\n <span className=\"tq-countdown-complete\">{completeMessage}</span>\n </div>\n )\n }\n return <div {...rest}>{renderSegments([{ value: String(current), label: resolvedLabels[0] || '' }], format)}</div>\n }\n\n const renderer = ({ days, hours, minutes, seconds, completed }) => {\n if (completed && onComplete === 'hide') {\n return <span className=\"tq-countdown-complete\" />\n }\n if (completed && onComplete === 'show-message') {\n return <span className=\"tq-countdown-complete\">{completeMessage}</span>\n }\n const segments = dateSegments({ days, hours, minutes, seconds }, resolvedUnits, resolvedLabels)\n return renderSegments(segments, format)\n }\n\n const targetDate = useMemo(() => {\n const d = new Date(target)\n return isNaN(d.getTime()) ? new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) : d\n }, [target])\n\n return (\n <div {...rest}>\n <Countdown date={targetDate} renderer={renderer} overtime={false} />\n </div>\n )\n}\n\nexport default TqCountdown\n";
18
+ return "import React, { useState, useEffect, useMemo } from 'react'\nimport Countdown, { zeroPad } from 'react-countdown'\n\nconst ALL_UNITS = ['days', 'hours', 'minutes', 'seconds']\nconst DEFAULT_LABELS = { days: 'Days', hours: 'Hours', minutes: 'Minutes', seconds: 'Seconds' }\n\nconst parseCsv = (value) =>\n typeof value === 'string' ? value.split(',').map((s) => s.trim()).filter(Boolean) : []\n\nconst resolveUnits = (units) => {\n const parsed = parseCsv(units).filter((u) => ALL_UNITS.indexOf(u) !== -1)\n return parsed.length > 0 ? parsed : ALL_UNITS\n}\n\nconst resolveLabels = (units, labels) => {\n const raw = parseCsv(labels)\n return units.map((u, i) => raw[i] || DEFAULT_LABELS[u] || u)\n}\n\nconst wrapperStyle = (format) => ({\n display: 'flex',\n alignItems: 'center',\n gap: format === 'inline' ? 0 : 16,\n fontVariantNumeric: 'tabular-nums',\n})\n\nconst renderSegments = (segments, format) => {\n if (format === 'inline') {\n return (\n <div className=\"tq-countdown tq-countdown-inline\" style={wrapperStyle('inline')}>\n <span className=\"tq-countdown-value\">{segments.map((s) => s.value).join(':')}</span>\n </div>\n )\n }\n return (\n <div className=\"tq-countdown tq-countdown-blocks\" style={wrapperStyle('blocks')}>\n {segments.map((s, i) => (\n <div\n key={i}\n className=\"tq-countdown-unit\"\n style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}\n >\n <span className=\"tq-countdown-value\" style={{ fontSize: '2em', fontWeight: 700, lineHeight: 1 }}>\n {s.value}\n </span>\n {s.label ? (\n <span className=\"tq-countdown-label\" style={{ fontSize: '0.75em', opacity: 0.7 }}>\n {s.label}\n </span>\n ) : null}\n </div>\n ))}\n </div>\n )\n}\n\nconst dateSegments = (timeDelta, units, labels) => {\n const showDays = units.indexOf('days') !== -1\n const hours = showDays ? timeDelta.hours : timeDelta.days * 24 + timeDelta.hours\n const byUnit = {\n days: timeDelta.days,\n hours: hours,\n minutes: timeDelta.minutes,\n seconds: timeDelta.seconds,\n }\n return units.map((unit, i) => ({ value: zeroPad(byUnit[unit] != null ? byUnit[unit] : 0), label: labels[i] || '' }))\n}\n\nconst TqCountdown = ({\n mode = 'date',\n target,\n from = '100',\n to = '0',\n format = 'blocks',\n units,\n labels,\n onComplete = 'keep-zero',\n completeMessage = '',\n ...rest\n}) => {\n const [mounted, setMounted] = useState(false)\n const [numericStart] = useState(() => Date.now())\n const [now, setNow] = useState(() => Date.now())\n\n useEffect(() => {\n setMounted(true)\n }, [])\n\n // Numeric mode ticks via its own interval.\n useEffect(() => {\n if (mode !== 'numeric') {\n return undefined\n }\n const id = setInterval(() => setNow(Date.now()), 1000)\n return () => clearInterval(id)\n }, [mode])\n\n const resolvedUnits = useMemo(() => resolveUnits(units), [units])\n const resolvedLabels = useMemo(() => resolveLabels(resolvedUnits, labels), [resolvedUnits, labels])\n // Every hook must run on EVERY render: the mounted-guard and the numeric-mode\n // branch below both return early, so a hook placed after them would only run\n // on some renders (\"Rendered more hooks than during the previous render\").\n const targetDate = useMemo(() => {\n const d = new Date(target)\n return isNaN(d.getTime()) ? new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) : d\n }, [target])\n\n // Deterministic placeholder (zeros) for SSR + first paint, so hydration matches.\n if (!mounted) {\n const zeros =\n mode === 'numeric'\n ? [{ value: String(from), label: resolvedLabels[0] || '' }]\n : resolvedUnits.map((u, i) => ({ value: '00', label: resolvedLabels[i] || '' }))\n return <div {...rest}>{renderSegments(zeros, format)}</div>\n }\n\n if (mode === 'numeric') {\n const fromN = parseFloat(from) || 0\n const toN = parseFloat(to) || 0\n const elapsed = Math.max(0, Math.floor((now - numericStart) / 1000))\n const current = fromN > toN ? Math.max(toN, fromN - elapsed) : Math.min(toN, fromN + elapsed)\n const completed = current === toN\n if (completed && onComplete === 'hide') {\n return <div {...rest} />\n }\n if (completed && onComplete === 'show-message') {\n return (\n <div {...rest}>\n <span className=\"tq-countdown-complete\">{completeMessage}</span>\n </div>\n )\n }\n return <div {...rest}>{renderSegments([{ value: String(current), label: resolvedLabels[0] || '' }], format)}</div>\n }\n\n const renderer = ({ days, hours, minutes, seconds, completed }) => {\n if (completed && onComplete === 'hide') {\n return <span className=\"tq-countdown-complete\" />\n }\n if (completed && onComplete === 'show-message') {\n return <span className=\"tq-countdown-complete\">{completeMessage}</span>\n }\n const segments = dateSegments({ days, hours, minutes, seconds }, resolvedUnits, resolvedLabels)\n return renderSegments(segments, format)\n }\n\n return (\n <div {...rest}>\n <Countdown date={targetDate} renderer={renderer} overtime={false} />\n </div>\n )\n}\n\nexport default TqCountdown\n";
19
19
  };
20
20
  exports.generateCountdownComponentCode = generateCountdownComponentCode;
21
21
  //# sourceMappingURL=component-generator.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"component-generator.js","sourceRoot":"","sources":["../../../src/countdown/component-generator.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;GAYG;AACI,IAAM,8BAA8B,GAAG;IAC5C,OAAO,29JAwJR,CAAA;AACD,CAAC,CAAA;AA1JY,QAAA,8BAA8B,kCA0J1C"}
1
+ {"version":3,"file":"component-generator.js","sourceRoot":"","sources":["../../../src/countdown/component-generator.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;GAYG;AACI,IAAM,8BAA8B,GAAG;IAC5C,OAAO,6sKA0JR,CAAA;AACD,CAAC,CAAA;AA5JY,QAAA,8BAA8B,kCA4J1C"}