@telegraph/helpers 0.0.13 → 0.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @telegraph/helpers
2
2
 
3
+ ## 0.0.15
4
+
5
+ ### Patch Changes
6
+
7
+ - [#653](https://github.com/knocklabs/telegraph/pull/653) [`d6c6aa9`](https://github.com/knocklabs/telegraph/commit/d6c6aa9cb0e11ba96df7d7efd479c8e4652fc029) Thanks [@dependabot](https://github.com/apps/dependabot)! - chore(deps): bump react and @types/react
8
+
9
+ ## 0.0.14
10
+
11
+ ### Patch Changes
12
+
13
+ - [#650](https://github.com/knocklabs/telegraph/pull/650) [`c7ffe1d`](https://github.com/knocklabs/telegraph/commit/c7ffe1d85a0320dec6a05b1fd386ba0092c48e37) Thanks [@kylemcd](https://github.com/kylemcd)! - fix: infinite render issue with RefToTgphRef's interaction with radix's ref
14
+
3
15
  ## 0.0.13
4
16
 
5
17
  ### Patch Changes
package/README.md CHANGED
@@ -1,8 +1,582 @@
1
+ # 🛠️ Helpers
2
+
3
+ > TypeScript utilities, React components, and hooks for building robust Telegraph components.
4
+
1
5
  ![Telegraph by Knock](https://github.com/knocklabs/telegraph/assets/29106675/9b5022e3-b02c-4582-ba57-3d6171e45e44)
2
6
 
3
7
  [![npm version](https://img.shields.io/npm/v/@telegraph/helpers.svg)](https://www.npmjs.com/package/@telegraph/helpers)
8
+ [![minzipped size](https://img.shields.io/bundlephobia/minzip/@telegraph/helpers)](https://bundlephobia.com/result?p=@telegraph/helpers)
9
+ [![license](https://img.shields.io/npm/l/@telegraph/helpers)](https://github.com/knocklabs/telegraph/blob/main/LICENSE)
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ npm install @telegraph/helpers
15
+ ```
16
+
17
+ > **Note**: This package contains TypeScript utilities and React helpers. No stylesheets required.
18
+
19
+ ## Quick Start
20
+
21
+ ```tsx
22
+ import {
23
+ PolymorphicProps,
24
+ RefToTgphRef,
25
+ useDeterminateState,
26
+ } from "@telegraph/helpers";
27
+
28
+ // Type-safe polymorphic component
29
+ type ButtonProps<T extends TgphElement> = PolymorphicProps<T> & {
30
+ variant?: "solid" | "outline";
31
+ };
32
+
33
+ // Hook for loading states with minimum duration
34
+ const { state } = useDeterminateState({
35
+ value: isLoading ? "loading" : "idle",
36
+ determinateValue: "loading",
37
+ minDurationMs: 1000,
38
+ });
39
+ ```
40
+
41
+ ## API Reference
42
+
43
+ ### Type Utilities
44
+
45
+ #### `Required<T, K>`
46
+
47
+ Make specific properties of a type required.
48
+
49
+ ```tsx
50
+ import { Required } from "@telegraph/helpers";
51
+
52
+ type User = {
53
+ name?: string;
54
+ email?: string;
55
+ age?: number;
56
+ };
57
+
58
+ // Make name and email required
59
+ type UserWithRequiredFields = Required<User, "name" | "email">;
60
+ // Result: { name: string; email: string; age?: number; }
61
+ ```
62
+
63
+ #### `Optional<T, K>`
64
+
65
+ Make specific properties of a type optional.
66
+
67
+ ```tsx
68
+ import { Optional } from "@telegraph/helpers";
69
+
70
+ type User = {
71
+ name: string;
72
+ email: string;
73
+ age: number;
74
+ };
75
+
76
+ // Make age optional
77
+ type UserWithOptionalAge = Optional<User, "age">;
78
+ // Result: { name: string; email: string; age?: number; }
79
+ ```
80
+
81
+ #### `RemappedOmit<T, K>`
82
+
83
+ Enhanced version of TypeScript's `Omit` that ensures complete field removal.
84
+
85
+ ```tsx
86
+ import { RemappedOmit } from "@telegraph/helpers";
87
+
88
+ type User = {
89
+ id: string;
90
+ name: string;
91
+ password: string;
92
+ };
93
+
94
+ // Remove sensitive fields
95
+ type PublicUser = RemappedOmit<User, "password">;
96
+ // Result: { id: string; name: string; }
97
+ ```
98
+
99
+ ### Polymorphic Component Types
100
+
101
+ #### `TgphElement`
102
+
103
+ Type alias for `React.ElementType` used throughout Telegraph.
104
+
105
+ ```tsx
106
+ import { TgphElement } from "@telegraph/helpers";
107
+
108
+ type ComponentProps<T extends TgphElement> = {
109
+ as?: T;
110
+ children: React.ReactNode;
111
+ };
112
+ ```
113
+
114
+ #### `TgphComponentProps<T>`
115
+
116
+ Type alias for `React.ComponentProps<T>`.
117
+
118
+ ```tsx
119
+ import { TgphComponentProps } from "@telegraph/helpers";
120
+
121
+ type ButtonProps = TgphComponentProps<"button"> & {
122
+ variant?: string;
123
+ };
124
+ ```
125
+
126
+ #### `AsProp<C>`
127
+
128
+ Type for the `as` prop used in polymorphic components.
129
+
130
+ ```tsx
131
+ import { AsProp } from "@telegraph/helpers";
132
+
133
+ type BaseProps = AsProp<React.ElementType> & {
134
+ children: React.ReactNode;
135
+ };
136
+ ```
137
+
138
+ #### `PolymorphicProps<E>`
139
+
140
+ Complete props type for polymorphic components.
141
+
142
+ ```tsx
143
+ import { PolymorphicProps, TgphElement } from "@telegraph/helpers";
144
+
145
+ type BoxProps<T extends TgphElement> = PolymorphicProps<T> & {
146
+ padding?: string;
147
+ margin?: string;
148
+ };
149
+
150
+ const Box = <T extends TgphElement = "div">({
151
+ as,
152
+ padding,
153
+ margin,
154
+ ...props
155
+ }: BoxProps<T>) => {
156
+ const Component = as || "div";
157
+ return <Component style={{ padding, margin }} {...props} />;
158
+ };
159
+
160
+ // Usage examples:
161
+ <Box>Default div</Box>
162
+ <Box as="section">Semantic section</Box>
163
+ <Box as="button" onClick={() => {}}>Button element</Box>
164
+ ```
165
+
166
+ #### `PolymorphicPropsWithTgphRef<E, R>`
167
+
168
+ Polymorphic props with Telegraph-specific ref handling.
169
+
170
+ ```tsx
171
+ import { PolymorphicPropsWithTgphRef, TgphElement } from "@telegraph/helpers";
172
+
173
+ type InputProps<T extends TgphElement> = PolymorphicPropsWithTgphRef<
174
+ T,
175
+ HTMLInputElement
176
+ > & {
177
+ placeholder?: string;
178
+ };
179
+
180
+ const Input = <T extends TgphElement = "input">({
181
+ as,
182
+ tgphRef,
183
+ ...props
184
+ }: InputProps<T>) => {
185
+ const Component = as || "input";
186
+ return <Component ref={tgphRef} {...props} />;
187
+ };
188
+ ```
189
+
190
+ #### `PropsWithAs<C, P>`
191
+
192
+ Utility for combining element props with custom props.
193
+
194
+ ```tsx
195
+ import { PropsWithAs } from "@telegraph/helpers";
196
+
197
+ type CustomButtonProps = PropsWithAs<
198
+ "button",
199
+ {
200
+ variant: "primary" | "secondary";
201
+ loading?: boolean;
202
+ }
203
+ >;
204
+
205
+ const CustomButton = ({
206
+ as: Component = "button",
207
+ variant,
208
+ loading,
209
+ ...props
210
+ }: CustomButtonProps) => {
211
+ return <Component disabled={loading} data-variant={variant} {...props} />;
212
+ };
213
+ ```
214
+
215
+ ## React Components
216
+
217
+ ### `RefToTgphRef`
218
+
219
+ Component for handling ref forwarding between external libraries (like Radix) and Telegraph components.
220
+
221
+ ```tsx
222
+ import * as Popover from "@radix-ui/react-popover";
223
+ import { Button } from "@telegraph/button";
224
+ import { RefToTgphRef } from "@telegraph/helpers";
225
+
226
+ // Radix expects a `ref` prop, but Telegraph uses `tgphRef`
227
+ <Popover.Trigger asChild>
228
+ <RefToTgphRef>
229
+ <Button>Open Popover</Button>
230
+ </RefToTgphRef>
231
+ </Popover.Trigger>;
232
+ ```
233
+
234
+ #### Use Cases
235
+
236
+ **With Radix UI Primitives:**
237
+
238
+ ```tsx
239
+ import * as Dialog from "@radix-ui/react-dialog";
240
+ import { Button } from "@telegraph/button";
241
+ import { RefToTgphRef } from "@telegraph/helpers";
242
+
243
+ const DialogExample = () => (
244
+ <Dialog.Root>
245
+ <Dialog.Trigger asChild>
246
+ <RefToTgphRef>
247
+ <Button>Open Dialog</Button>
248
+ </RefToTgphRef>
249
+ </Dialog.Trigger>
250
+ <Dialog.Content>{/* Dialog content */}</Dialog.Content>
251
+ </Dialog.Root>
252
+ );
253
+ ```
254
+
255
+ **With Form Libraries:**
256
+
257
+ ```tsx
258
+ import { RefToTgphRef } from "@telegraph/helpers";
259
+ import { Input } from "@telegraph/input";
260
+ import { Controller, useForm } from "react-hook-form";
261
+
262
+ const FormExample = () => {
263
+ const { control } = useForm();
264
+
265
+ return (
266
+ <Controller
267
+ name="email"
268
+ control={control}
269
+ render={({ field }) => (
270
+ <RefToTgphRef>
271
+ <Input {...field} placeholder="Email" />
272
+ </RefToTgphRef>
273
+ )}
274
+ />
275
+ );
276
+ };
277
+ ```
278
+
279
+ ## React Hooks
280
+
281
+ ### `useDeterminateState`
282
+
283
+ Hook for managing state transitions with minimum duration guarantees.
284
+
285
+ ```tsx
286
+ import { useDeterminateState } from "@telegraph/helpers";
287
+
288
+ const useDeterminateState = <T>({
289
+ value: T; // Current value
290
+ determinateValue: T; // Value that should persist for minimum duration
291
+ minDurationMs?: number; // Minimum duration (default: 1000ms)
292
+ }): T
293
+ ```
294
+
295
+ #### Basic Usage
296
+
297
+ ```tsx
298
+ import { useDeterminateState } from "@telegraph/helpers";
299
+ import { useState } from "react";
300
+
301
+ const LoadingButton = () => {
302
+ const [isLoading, setIsLoading] = useState(false);
303
+
304
+ // Ensure loading state persists for at least 1 second
305
+ const buttonState = useDeterminateState({
306
+ value: isLoading ? "loading" : "idle",
307
+ determinateValue: "loading",
308
+ minDurationMs: 1000,
309
+ });
310
+
311
+ const handleClick = async () => {
312
+ setIsLoading(true);
313
+ try {
314
+ await someAsyncOperation();
315
+ } finally {
316
+ setIsLoading(false); // Will transition back after minimum duration
317
+ }
318
+ };
319
+
320
+ return (
321
+ <button onClick={handleClick} disabled={buttonState === "loading"}>
322
+ {buttonState === "loading" ? "Loading..." : "Click me"}
323
+ </button>
324
+ );
325
+ };
326
+ ```
327
+
328
+ #### Advanced Usage
329
+
330
+ ```tsx
331
+ import { useDeterminateState } from "@telegraph/helpers";
332
+
333
+ type FormState = "idle" | "submitting" | "success" | "error";
334
+
335
+ const FormWithFeedback = () => {
336
+ const [formState, setFormState] = useState<FormState>("idle");
337
+
338
+ // Ensure success/error states are visible for at least 2 seconds
339
+ const displayState = useDeterminateState({
340
+ value: formState,
341
+ determinateValue:
342
+ formState === "success" || formState === "error" ? formState : "idle",
343
+ minDurationMs: 2000,
344
+ });
345
+
346
+ const handleSubmit = async () => {
347
+ setFormState("submitting");
348
+ try {
349
+ await submitForm();
350
+ setFormState("success");
351
+ // Auto-reset after success is shown
352
+ setTimeout(() => setFormState("idle"), 2500);
353
+ } catch (error) {
354
+ setFormState("error");
355
+ setTimeout(() => setFormState("idle"), 2500);
356
+ }
357
+ };
358
+
359
+ return (
360
+ <form onSubmit={handleSubmit}>
361
+ <button disabled={displayState === "submitting"}>
362
+ {displayState === "submitting" && "Submitting..."}
363
+ {displayState === "success" && "✓ Saved!"}
364
+ {displayState === "error" && "✗ Error"}
365
+ {displayState === "idle" && "Save"}
366
+ </button>
367
+ </form>
368
+ );
369
+ };
370
+ ```
371
+
372
+ ## Advanced Usage
373
+
374
+ ### Creating Polymorphic Components
375
+
376
+ ```tsx
377
+ import { forwardRef } from "react";
378
+ import { PolymorphicPropsWithTgphRef, TgphElement } from "@telegraph/helpers";
379
+
380
+ type TextProps<T extends TgphElement> = PolymorphicPropsWithTgphRef<T, HTMLElement> & {
381
+ size?: "small" | "medium" | "large";
382
+ weight?: "normal" | "bold";
383
+ color?: string;
384
+ };
385
+
386
+ const Text = forwardRef<HTMLElement, TextProps<TgphElement>>(
387
+ <T extends TgphElement = "span">({
388
+ as,
389
+ size = "medium",
390
+ weight = "normal",
391
+ color,
392
+ tgphRef,
393
+ style,
394
+ ...props
395
+ }: TextProps<T>, ref) => {
396
+ const Component = as || "span";
397
+
398
+ return (
399
+ <Component
400
+ ref={tgphRef || ref}
401
+ style={{
402
+ fontSize: size === "small" ? "12px" : size === "large" ? "18px" : "14px",
403
+ fontWeight: weight === "bold" ? "bold" : "normal",
404
+ color,
405
+ ...style,
406
+ }}
407
+ {...props}
408
+ />
409
+ );
410
+ }
411
+ );
412
+
413
+ // Usage examples:
414
+ <Text>Default span text</Text>
415
+ <Text as="p" size="large" weight="bold">Paragraph text</Text>
416
+ <Text as="h1" color="blue">Heading text</Text>
417
+ <Text as={Link} href="/about">Link text</Text>
418
+ ```
419
+
420
+ ### Type-Safe Component APIs
421
+
422
+ ```tsx
423
+ import { Required, Optional, TgphComponentProps } from "@telegraph/helpers";
424
+
425
+ // Base props with all optional styling
426
+ type BaseCardProps = {
427
+ padding?: string;
428
+ shadow?: boolean;
429
+ rounded?: boolean;
430
+ background?: string;
431
+ };
432
+
433
+ // Card variant that requires certain props
434
+ type PrimaryCardProps = Required<BaseCardProps, "background"> & {
435
+ variant: "primary";
436
+ };
437
+
438
+ // Card variant with some optional props
439
+ type SecondaryCardProps = Optional<BaseCardProps, "padding"> & {
440
+ variant: "secondary";
441
+ };
442
+
443
+ type CardProps = (PrimaryCardProps | SecondaryCardProps) & {
444
+ children: React.ReactNode;
445
+ };
446
+
447
+ const Card = ({ variant, children, ...props }: CardProps) => {
448
+ if (variant === "primary") {
449
+ // TypeScript knows `background` is required here
450
+ return <div style={{ background: props.background }} />;
451
+ }
452
+
453
+ // Handle secondary variant
454
+ return <div>{children}</div>;
455
+ };
456
+
457
+ // Usage:
458
+ <Card variant="primary" background="white">Content</Card> // ✓ Valid
459
+ <Card variant="primary">Content</Card> // ✗ TypeScript error - missing background
460
+ ```
461
+
462
+ ### Integration with External Libraries
463
+
464
+ ```tsx
465
+ import * as RadixPopover from "@radix-ui/react-popover";
466
+ import { Button } from "@telegraph/button";
467
+ import { PolymorphicProps, RefToTgphRef } from "@telegraph/helpers";
468
+
469
+ type PopoverProps = PolymorphicProps<"div"> & {
470
+ trigger: React.ReactNode;
471
+ content: React.ReactNode;
472
+ };
473
+
474
+ const Popover = ({ trigger, content, ...props }: PopoverProps) => (
475
+ <RadixPopover.Root>
476
+ <RadixPopover.Trigger asChild>
477
+ <RefToTgphRef>{trigger}</RefToTgphRef>
478
+ </RadixPopover.Trigger>
479
+ <RadixPopover.Content {...props}>{content}</RadixPopover.Content>
480
+ </RadixPopover.Root>
481
+ );
482
+
483
+ // Usage:
484
+ <Popover
485
+ trigger={<Button>Open Menu</Button>}
486
+ content={<div>Popover content</div>}
487
+ />;
488
+ ```
489
+
490
+ ## TypeScript
491
+
492
+ ### Utility Type Examples
493
+
494
+ ```tsx
495
+ import { Optional, RemappedOmit, Required } from "@telegraph/helpers";
496
+
497
+ // API response type
498
+ type User = {
499
+ id: string;
500
+ name: string;
501
+ email: string;
502
+ avatar?: string;
503
+ createdAt: Date;
504
+ updatedAt: Date;
505
+ };
506
+
507
+ // Create user payload (remove server-generated fields)
508
+ type CreateUserPayload = RemappedOmit<User, "id" | "createdAt" | "updatedAt">;
509
+
510
+ // Update user payload (make all fields optional except id)
511
+ type UpdateUserPayload = Required<
512
+ Optional<User, "name" | "email" | "avatar">,
513
+ "id"
514
+ >;
515
+
516
+ // Public user (safe for client-side)
517
+ type PublicUser = RemappedOmit<User, "email">;
518
+ ```
519
+
520
+ ### Component Prop Patterns
521
+
522
+ ```tsx
523
+ import {
524
+ PolymorphicProps,
525
+ TgphComponentProps,
526
+ TgphElement,
527
+ } from "@telegraph/helpers";
528
+
529
+ // Pattern 1: Simple polymorphic component
530
+ type SimpleProps<T extends TgphElement> = PolymorphicProps<T> & {
531
+ variant?: string;
532
+ };
533
+
534
+ // Pattern 2: Component with ref forwarding
535
+ type WithRefProps<T extends TgphElement> = PolymorphicPropsWithTgphRef<
536
+ T,
537
+ HTMLElement
538
+ > & {
539
+ variant?: string;
540
+ };
541
+
542
+ // Pattern 3: Extending native element props
543
+ type NativeProps = TgphComponentProps<"button"> & {
544
+ loading?: boolean;
545
+ };
546
+
547
+ // Pattern 4: Conditional props based on variant
548
+ type ConditionalProps<T extends TgphElement> = PolymorphicProps<T> &
549
+ (
550
+ | { variant: "icon"; icon: React.ComponentType; label?: never }
551
+ | { variant: "text"; label: string; icon?: never }
552
+ );
553
+ ```
554
+
555
+ ## Best Practices
556
+
557
+ ### Type Utility Guidelines
558
+
559
+ 1. **Use `RemappedOmit` over `Omit`**: Ensures complete field removal
560
+ 2. **Prefer `Required`/`Optional` for partial updates**: More explicit than `Partial`
561
+ 3. **Use `PolymorphicProps` for flexible components**: Enables `as` prop pattern
562
+ 4. **Always provide default `TgphElement`**: Improves TypeScript inference
563
+
564
+ ### Component Development
565
+
566
+ 1. **Use `RefToTgphRef` with external libraries**: Ensures ref compatibility
567
+ 2. **Implement `useDeterminateState` for loading states**: Improves UX with minimum durations
568
+ 3. **Type polymorphic components properly**: Use appropriate helper types
569
+ 4. **Test type constraints**: Verify TypeScript catches errors correctly
570
+
571
+ ## References
572
+
573
+ - [TypeScript Handbook - Utility Types](https://www.typescriptlang.org/docs/handbook/utility-types.html)
574
+ - [React TypeScript Cheatsheet](https://react-typescript-cheatsheet.netlify.app/)
575
+
576
+ ## Contributing
4
577
 
5
- # @telegraph/helpers
6
- > Internal helpers for use in telegraph
578
+ See our [Contributing Guide](../../CONTRIBUTING.md) for more details.
7
579
 
580
+ ## License
8
581
 
582
+ MIT License - see [LICENSE](../../LICENSE) for details.
package/dist/cjs/index.js CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const c=require("react"),p=Symbol.for("react.forward_ref"),R=(e,o)=>{const r={...o};for(const s in o){const t=e[s],n=o[s];/^on[A-Z]/.test(s)?t&&n?r[s]=(...i)=>{n(...i),t(...i)}:t&&(r[s]=t):s==="style"?r[s]={...t,...n}:s==="className"&&(r[s]=[t,n].filter(Boolean).join(" "))}return{...e,...r}},m=({children:e,...o},r)=>e?c.Children.toArray(e).map(t=>{if(c.isValidElement(t)){const n=t,l=n.$$typeof,i=n.type.$$typeof,f=n.props,u=f.tgphRef;return l===p||i===p?c.cloneElement(n,{...R(o,f),tgphRef:u||r,ref:u||r}):c.cloneElement(n,{...R(o,f),tgphRef:u||r})}return t}):null,y=c.forwardRef(({children:e,...o},r)=>m({children:e,...o},r)),T=({value:e,determinateValue:o,minDurationMs:r=1e3})=>{const[s,t]=c.useState(e),n=c.useRef(null),l=c.useRef(null),i=()=>{n.current&&(clearTimeout(n.current),n.current=null)},f=c.useCallback(()=>{if(e===o)i(),t(o),l.current=Date.now();else if(l.current!==null){const u=Date.now()-l.current,a=r-u;a>0?(i(),n.current=setTimeout(()=>{t(e),l.current=null},a)):(t(e),l.current=null)}else t(e)},[e,o,r]);return c.useEffect(()=>(f(),i),[e,f]),s};exports.RefToTgphRef=y;exports.useDeterminateState=T;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const f=require("react"),p=Symbol.for("react.forward_ref"),R=(s,l)=>{const e={...l};for(const c in l){const t=s[c],r=l[c];/^on[A-Z]/.test(c)?t&&r?e[c]=(...o)=>{r(...o),t(...o)}:t&&(e[c]=t):c==="style"?e[c]={...t,...r}:c==="className"&&(e[c]=[t,r].filter(Boolean).join(" "))}return{...s,...e}},y=({children:s,...l},e)=>s?f.Children.toArray(s).map(t=>{if(f.isValidElement(t)){const r=t,n=r.$$typeof,o=r.type.$$typeof,u=r.props,i=u.tgphRef;return n===p||o===p?f.cloneElement(r,{...R(l,u),tgphRef:i||e,ref:i||e}):f.cloneElement(r,{...R(l,u),tgphRef:i||e})}return t}):null,m=f.forwardRef(({children:s,...l},e)=>{const c=f.useRef(e),t=f.useRef(null);f.useEffect(()=>{const n=c.current,o=t.current;n!==e&&o&&(typeof n=="function"?n(null):n&&(n.current=null),typeof e=="function"?e(o):e&&(e.current=o)),c.current=e});const r=f.useCallback(n=>{t.current=n;const o=c.current;typeof o=="function"?o(n):o&&(o.current=n)},[]);return y({children:s,...l},r)}),T=({value:s,determinateValue:l,minDurationMs:e=1e3})=>{const[c,t]=f.useState(s),r=f.useRef(null),n=f.useRef(null),o=()=>{r.current&&(clearTimeout(r.current),r.current=null)},u=f.useCallback(()=>{if(s===l)o(),t(l),n.current=Date.now();else if(n.current!==null){const i=Date.now()-n.current,a=e-i;a>0?(o(),r.current=setTimeout(()=>{t(s),n.current=null},a)):(t(s),n.current=null)}else t(s)},[s,l,e]);return f.useEffect(()=>(u(),o),[s,u]),c};exports.RefToTgphRef=m;exports.useDeterminateState=T;
2
2
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../../src/components/RefToTgphRef/RefToTgphRef.tsx","../../src/hooks/useDeterminateState.ts"],"sourcesContent":["//\n// When interacting with components like a Radix.Trigger, they assume that\n// our components accept a `ref` prop. This is not the case with our components\n// because we use the `tgphRef` prop instead. To work around this, we can create\n// a new component that accepts a `ref` prop and forwards it to the `tgphRef`\n// prop.\n//\nimport React from \"react\";\n\nconst FORWARD_REF_SYMBOL = Symbol.for(\"react.forward_ref\");\n\ntype ApplyRefPropsProps = {\n children: React.ReactNode;\n};\n\ntype Child = React.ReactElement & {\n $$typeof: symbol;\n type: { $$typeof: symbol };\n};\n\n// Merge props the same way that radix slot does\n// https://github.com/radix-ui/primitives/blob/main/packages/react/slot/src/Slot.tsx\nconst mergeProps = (\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n slotProps: Record<string, any>,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n childProps: Record<string, any>,\n) => {\n // all child props should override\n const overrideProps = { ...childProps };\n\n for (const propName in childProps) {\n const slotPropValue = slotProps[propName];\n const childPropValue = childProps[propName];\n\n const isHandler = /^on[A-Z]/.test(propName);\n if (isHandler) {\n // if the handler exists on both, we compose them\n if (slotPropValue && childPropValue) {\n overrideProps[propName] = (...args: unknown[]) => {\n childPropValue(...args);\n slotPropValue(...args);\n };\n }\n // but if it exists only on the slot, we use only this one\n else if (slotPropValue) {\n overrideProps[propName] = slotPropValue;\n }\n }\n // if it's `style`, we merge them\n else if (propName === \"style\") {\n overrideProps[propName] = { ...slotPropValue, ...childPropValue };\n } else if (propName === \"className\") {\n overrideProps[propName] = [slotPropValue, childPropValue]\n .filter(Boolean)\n .join(\" \");\n }\n }\n\n return { ...slotProps, ...overrideProps };\n};\n\nconst applyRefProps = (\n { children, ...props }: ApplyRefPropsProps,\n ref: React.Ref<unknown>,\n) => {\n if (!children) return null;\n const childrenArray = React.Children.toArray(children);\n return childrenArray.map((child) => {\n if (React.isValidElement(child)) {\n const validChild = child as Child;\n const $$typeof = validChild.$$typeof;\n const $$typeofType = validChild.type.$$typeof;\n const childProps = validChild.props;\n const tgphRef = childProps.tgphRef;\n\n // If we detect that the child is a forwardRef, we to pass the `ref` prop\n // to it so that components that exist outside of our library can still\n // receive the ref. We do it this way in order to avoid this warning:\n // \"Function components cannot be given refs. Attempts to access this ref will fail.\n // Did you mean to use React.forwardRef()\"\n if (\n $$typeof === FORWARD_REF_SYMBOL ||\n $$typeofType === FORWARD_REF_SYMBOL\n ) {\n return React.cloneElement(validChild, {\n ...mergeProps(props, childProps),\n tgphRef: tgphRef || ref,\n ref: tgphRef || ref,\n });\n }\n\n // Otherwise, we can just pass the `tgphRef` prop to the child.\n return React.cloneElement(validChild, {\n ...mergeProps(props, childProps),\n tgphRef: tgphRef || ref,\n });\n }\n\n // If the child is not a valid element, we can just return it.\n return child;\n });\n};\n\n// We can't generate the type of the ref because it's a forwardRef so any\n// works for this use case\n//\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst RefToTgphRef = React.forwardRef<any, any>(\n ({ children: childrenProp, ...props }, ref) => {\n return applyRefProps({ children: childrenProp, ...props }, ref);\n },\n);\n\nexport { RefToTgphRef };\n","/*\n * useDeterminateState\n *\n * A hook that returns a state transitioning to a determinate value after a minimum duration.\n * For example, you could use this hook with a button that transitions into a \"loading\" state,\n * ensuring it remains in the \"loading\" state for at least 1000ms. This provides clear feedback\n * to the user that the action is being processed.\n *\n */\nimport React from \"react\";\n\ntype UseDeterminateStateParams<T> = {\n value: T;\n determinateValue: T;\n minDurationMs?: number;\n};\n\nconst useDeterminateState = <T>({\n value,\n determinateValue,\n minDurationMs = 1000,\n}: UseDeterminateStateParams<T>): T => {\n const [state, setState] = React.useState<T>(value);\n const timeoutRef = React.useRef<NodeJS.Timeout | null>(null);\n const startTimeRef = React.useRef<number | null>(null);\n\n const clearExistingTimeout = () => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n timeoutRef.current = null;\n }\n };\n\n const handleTransition = React.useCallback(() => {\n if (value === determinateValue) {\n clearExistingTimeout();\n setState(determinateValue);\n startTimeRef.current = Date.now();\n } else if (startTimeRef.current !== null) {\n const elapsedTime = Date.now() - startTimeRef.current;\n const remainingTime = minDurationMs - elapsedTime;\n\n if (remainingTime > 0) {\n clearExistingTimeout();\n timeoutRef.current = setTimeout(() => {\n setState(value);\n startTimeRef.current = null;\n }, remainingTime);\n } else {\n setState(value);\n startTimeRef.current = null;\n }\n } else {\n setState(value);\n }\n }, [value, determinateValue, minDurationMs]);\n\n React.useEffect(() => {\n handleTransition();\n return clearExistingTimeout;\n }, [value, handleTransition]);\n\n return state;\n};\n\nexport { useDeterminateState };\n"],"names":["FORWARD_REF_SYMBOL","mergeProps","slotProps","childProps","overrideProps","propName","slotPropValue","childPropValue","args","applyRefProps","children","props","ref","React","child","validChild","$$typeof","$$typeofType","tgphRef","RefToTgphRef","childrenProp","useDeterminateState","value","determinateValue","minDurationMs","state","setState","timeoutRef","startTimeRef","clearExistingTimeout","handleTransition","elapsedTime","remainingTime"],"mappings":"yGASMA,EAAqB,OAAO,IAAI,mBAAmB,EAanDC,EAAa,CAEjBC,EAEAC,IACG,CAEG,MAAAC,EAAgB,CAAE,GAAGD,CAAW,EAEtC,UAAWE,KAAYF,EAAY,CAC3B,MAAAG,EAAgBJ,EAAUG,CAAQ,EAClCE,EAAiBJ,EAAWE,CAAQ,EAExB,WAAW,KAAKA,CAAQ,EAGpCC,GAAiBC,EACLH,EAAAC,CAAQ,EAAI,IAAIG,IAAoB,CAChDD,EAAe,GAAGC,CAAI,EACtBF,EAAc,GAAGE,CAAI,CACvB,EAGOF,IACPF,EAAcC,CAAQ,EAAIC,GAIrBD,IAAa,QACpBD,EAAcC,CAAQ,EAAI,CAAE,GAAGC,EAAe,GAAGC,CAAe,EACvDF,IAAa,cACRD,EAAAC,CAAQ,EAAI,CAACC,EAAeC,CAAc,EACrD,OAAO,OAAO,EACd,KAAK,GAAG,EACb,CAGF,MAAO,CAAE,GAAGL,EAAW,GAAGE,CAAc,CAC1C,EAEMK,EAAgB,CACpB,CAAE,SAAAC,EAAU,GAAGC,CAAA,EACfC,IAEKF,EACiBG,EAAM,SAAS,QAAQH,CAAQ,EAChC,IAAKI,GAAU,CAC9B,GAAAD,EAAM,eAAeC,CAAK,EAAG,CAC/B,MAAMC,EAAaD,EACbE,EAAWD,EAAW,SACtBE,EAAeF,EAAW,KAAK,SAC/BZ,EAAaY,EAAW,MACxBG,EAAUf,EAAW,QAQzB,OAAAa,IAAahB,GACbiB,IAAiBjB,EAEVa,EAAM,aAAaE,EAAY,CACpC,GAAGd,EAAWU,EAAOR,CAAU,EAC/B,QAASe,GAAWN,EACpB,IAAKM,GAAWN,CAAA,CACjB,EAIIC,EAAM,aAAaE,EAAY,CACpC,GAAGd,EAAWU,EAAOR,CAAU,EAC/B,QAASe,GAAWN,CAAA,CACrB,CAAA,CAII,OAAAE,CAAA,CACR,EAnCqB,KA0ClBK,EAAeN,EAAM,WACzB,CAAC,CAAE,SAAUO,EAAc,GAAGT,CAAA,EAASC,IAC9BH,EAAc,CAAE,SAAUW,EAAc,GAAGT,GAASC,CAAG,CAElE,EC/FMS,EAAsB,CAAI,CAC9B,MAAAC,EACA,iBAAAC,EACA,cAAAC,EAAgB,GAClB,IAAuC,CACrC,KAAM,CAACC,EAAOC,CAAQ,EAAIb,EAAM,SAAYS,CAAK,EAC3CK,EAAad,EAAM,OAA8B,IAAI,EACrDe,EAAef,EAAM,OAAsB,IAAI,EAE/CgB,EAAuB,IAAM,CAC7BF,EAAW,UACb,aAAaA,EAAW,OAAO,EAC/BA,EAAW,QAAU,KAEzB,EAEMG,EAAmBjB,EAAM,YAAY,IAAM,CAC/C,GAAIS,IAAUC,EACSM,EAAA,EACrBH,EAASH,CAAgB,EACZK,EAAA,QAAU,KAAK,IAAI,UACvBA,EAAa,UAAY,KAAM,CACxC,MAAMG,EAAc,KAAK,IAAI,EAAIH,EAAa,QACxCI,EAAgBR,EAAgBO,EAElCC,EAAgB,GACGH,EAAA,EACVF,EAAA,QAAU,WAAW,IAAM,CACpCD,EAASJ,CAAK,EACdM,EAAa,QAAU,MACtBI,CAAa,IAEhBN,EAASJ,CAAK,EACdM,EAAa,QAAU,KACzB,MAEAF,EAASJ,CAAK,CAEf,EAAA,CAACA,EAAOC,EAAkBC,CAAa,CAAC,EAE3C,OAAAX,EAAM,UAAU,KACGiB,EAAA,EACVD,GACN,CAACP,EAAOQ,CAAgB,CAAC,EAErBL,CACT"}
1
+ {"version":3,"file":"index.js","sources":["../../src/components/RefToTgphRef/RefToTgphRef.tsx","../../src/hooks/useDeterminateState.ts"],"sourcesContent":["/**\n * RefToTgphRef Component\n *\n * PURPOSE:\n * ========\n * This component bridges the gap between third-party libraries (like Radix UI) and\n * Telegraph components. Third-party libraries expect components to accept a standard\n * React `ref` prop, but Telegraph components use a custom `tgphRef` prop instead.\n *\n * Without this adapter, using Telegraph components with libraries like Radix would fail\n * because Radix would try to pass a `ref` that Telegraph components wouldn't receive.\n *\n * EXAMPLE USAGE:\n * ==============\n * ```tsx\n * <RadixTooltip.Trigger asChild>\n * <RefToTgphRef>\n * <Button>Hover me</Button> // Button uses tgphRef internally\n * </RefToTgphRef>\n * </RadixTooltip.Trigger>\n * ```\n *\n * WHAT IT DOES:\n * =============\n * 1. Receives a `ref` from the parent (e.g., Radix)\n * 2. Forwards it as both `ref` AND `tgphRef` to Telegraph children\n * 3. Merges any additional props from the parent with child props\n * 4. Handles both forwardRef components and regular components appropriately\n *\n * THE INFINITE LOOP PROBLEM:\n * ==========================\n * Radix and other libraries often pass ref callbacks that are recreated on every render\n * (new function references). When we pass these unstable refs to children via\n * React.cloneElement, it causes the child to re-render with \"new\" props even though\n * the ref functionality hasn't actually changed. This can trigger infinite render loops.\n *\n * THE SOLUTION:\n * =============\n * We create a STABLE ref callback using useCallback with an empty dependency array,\n * so the function reference never changes. We store the actual (unstable) ref in a\n * mutable ref (refStorage) and update it on every render. When our stable callback\n * is invoked, it reads from refStorage to get the latest ref and calls it.\n *\n * We also track the DOM node so that if the ref callback itself changes (rare but\n * possible), we can properly cleanup the old ref by calling it with null, and then\n * call the new ref with the current node. This matches React's standard ref behavior.\n */\nimport React from \"react\";\n\nconst FORWARD_REF_SYMBOL = Symbol.for(\"react.forward_ref\");\n\ntype ApplyRefPropsProps = {\n children: React.ReactNode;\n};\n\ntype Child = React.ReactElement & {\n $$typeof: symbol;\n type: { $$typeof: symbol };\n};\n\n/**\n * mergeProps\n *\n * Merges props from the slot (parent/wrapper) with props from the child component.\n * This follows the same approach as Radix's Slot component to ensure compatibility.\n *\n * MERGE STRATEGY:\n * - Event handlers (onX): Compose them so both parent and child handlers run\n * - style: Merge objects (child styles override parent styles with same keys)\n * - className: Concatenate both class strings\n * - Other props: Child props override parent props\n *\n * @see https://github.com/radix-ui/primitives/blob/main/packages/react/slot/src/Slot.tsx\n */\nconst mergeProps = (\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n slotProps: Record<string, any>,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n childProps: Record<string, any>,\n) => {\n // all child props should override\n const overrideProps = { ...childProps };\n\n for (const propName in childProps) {\n const slotPropValue = slotProps[propName];\n const childPropValue = childProps[propName];\n\n const isHandler = /^on[A-Z]/.test(propName);\n if (isHandler) {\n // if the handler exists on both, we compose them\n if (slotPropValue && childPropValue) {\n overrideProps[propName] = (...args: unknown[]) => {\n childPropValue(...args);\n slotPropValue(...args);\n };\n }\n // but if it exists only on the slot, we use only this one\n else if (slotPropValue) {\n overrideProps[propName] = slotPropValue;\n }\n }\n // if it's `style`, we merge them\n else if (propName === \"style\") {\n overrideProps[propName] = { ...slotPropValue, ...childPropValue };\n } else if (propName === \"className\") {\n overrideProps[propName] = [slotPropValue, childPropValue]\n .filter(Boolean)\n .join(\" \");\n }\n }\n\n return { ...slotProps, ...overrideProps };\n};\n\n/**\n * applyRefProps\n *\n * Clones child elements and applies the forwarded ref and any merged props to them.\n *\n * KEY DECISIONS:\n *\n * 1. ForwardRef Detection:\n * We check if a child is a forwardRef component by inspecting its $$typeof symbol.\n * This is necessary because forwardRef components EXPECT a `ref` prop, while\n * regular function components would throw a warning if given one.\n *\n * 2. Dual Ref Forwarding (forwardRef components):\n * For forwardRef components, we pass BOTH `ref` and `tgphRef` because:\n * - They might be third-party components that only understand `ref`\n * - They might be Telegraph components that need `tgphRef`\n * - Passing both ensures compatibility with all cases\n *\n * 3. Single Ref Forwarding (regular components):\n * For non-forwardRef components, we only pass `tgphRef` to avoid React warnings\n * about function components receiving refs.\n *\n * 4. Ref Priority:\n * If a child already has a `tgphRef`, we use that instead of the forwarded ref.\n * This allows child components to override ref behavior if needed.\n */\nconst applyRefProps = (\n { children, ...props }: ApplyRefPropsProps,\n ref: React.Ref<unknown>,\n) => {\n if (!children) return null;\n const childrenArray = React.Children.toArray(children);\n return childrenArray.map((child) => {\n if (React.isValidElement(child)) {\n const validChild = child as Child;\n const $$typeof = validChild.$$typeof;\n const $$typeofType = validChild.type.$$typeof;\n const childProps = validChild.props as Record<string, unknown>;\n const tgphRef = childProps.tgphRef;\n\n // CASE 1: ForwardRef Component\n // Pass both `ref` and `tgphRef` to ensure compatibility with both\n // Telegraph components and third-party forwardRef components.\n if (\n $$typeof === FORWARD_REF_SYMBOL ||\n $$typeofType === FORWARD_REF_SYMBOL\n ) {\n return React.cloneElement(validChild, {\n ...mergeProps(props, childProps as Record<string, unknown>),\n tgphRef: tgphRef || ref,\n ref: tgphRef || ref,\n } as Record<string, unknown>);\n }\n\n // CASE 2: Regular Component\n // Only pass `tgphRef` to avoid React warnings about function components\n // receiving refs (which would happen if we passed `ref`).\n return React.cloneElement(validChild, {\n ...mergeProps(props, childProps as Record<string, unknown>),\n tgphRef: tgphRef || ref,\n } as Record<string, unknown>);\n }\n\n // CASE 3: Non-element children (strings, numbers, etc.)\n // Return as-is since they can't receive refs or props.\n return child;\n });\n};\n\n/**\n * RefToTgphRef Component Implementation\n *\n * TYPE CONSTRAINTS:\n * We use `any` for the ref type because this component must accept refs of any type\n * (HTMLButtonElement, HTMLDivElement, custom component refs, etc.). Since we're\n * forwarding refs generically, there's no way to statically type this without\n * making the API cumbersome.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst RefToTgphRef = React.forwardRef<any, any>(\n ({ children: childrenProp, ...props }, ref) => {\n /**\n * REF STABILIZATION ARCHITECTURE\n *\n * PROBLEM:\n * Libraries like Radix create new ref callback functions on every render.\n * If we pass these unstable refs directly to children via React.cloneElement,\n * React sees the props object as changed (new function reference), causing\n * unnecessary re-renders and potential infinite loops.\n *\n * SOLUTION OVERVIEW:\n * Create a stable callback (stableRef) that never changes (empty deps array),\n * but internally reads from a mutable storage to get the latest ref. This way:\n * - Children receive the same function reference every render (no infinite loops)\n * - The function still forwards to the latest ref (functionality preserved)\n *\n */\n\n // Storage for the latest ref callback/object from parent (e.g., Radix)\n // This gets updated on every render but doesn't cause re-renders since it's\n // a mutable ref, not state.\n const refStorage = React.useRef(ref);\n\n // Storage for the current DOM node/component instance\n // We need this to handle ref changes properly (cleanup old, set new)\n const nodeStorage = React.useRef<unknown>(null);\n\n /**\n * REF CHANGE HANDLING\n *\n * When the parent ref changes (rare, but possible), we need to:\n * 1. Call the OLD ref with null (cleanup - standard React behavior)\n * 2. Call the NEW ref with the current node (re-attach)\n *\n * This matches React's native behavior when a ref prop changes.\n *\n * WHY IN useEffect:\n * We use useEffect (not direct assignment) because we need to detect when\n * the ref has actually changed between renders and perform cleanup/setup.\n */\n React.useEffect(() => {\n const prevRef = refStorage.current;\n const currentNode = nodeStorage.current;\n\n // Detect ref change\n if (prevRef !== ref && currentNode) {\n // Step 1: Cleanup old ref (call with null)\n if (typeof prevRef === \"function\") {\n prevRef(null);\n } else if (prevRef) {\n (prevRef as React.MutableRefObject<unknown>).current = null;\n }\n\n // Step 2: Set new ref with current node\n if (typeof ref === \"function\") {\n ref(currentNode);\n } else if (ref) {\n (ref as React.MutableRefObject<unknown>).current = currentNode;\n }\n }\n\n // Update storage with latest ref for next render\n refStorage.current = ref;\n });\n\n /**\n * STABLE REF CALLBACK\n *\n * This is the key to preventing infinite loops. The function reference\n * returned by useCallback with an empty dependency array NEVER changes.\n *\n * When called (by React when attaching/detaching from DOM):\n * 1. Store the node so we can handle ref changes\n * 2. Read the LATEST ref from refStorage\n * 3. Forward the call to that ref\n *\n * This indirection gives us stability (no infinite loops) while maintaining\n * correctness (always calls the latest ref).\n */\n const stableRef = React.useCallback((node: unknown) => {\n // Store node for ref change handling\n nodeStorage.current = node;\n\n // Get the current ref (might have been updated since last call)\n const currentRef = refStorage.current;\n\n // Forward to the actual ref (handle both callback refs and ref objects)\n if (typeof currentRef === \"function\") {\n currentRef(node);\n } else if (currentRef) {\n (currentRef as React.MutableRefObject<unknown>).current = node;\n }\n }, []); // Empty deps = stable function reference forever\n\n // Apply the stable ref and merged props to children\n return applyRefProps({ children: childrenProp, ...props }, stableRef);\n },\n);\n\nexport { RefToTgphRef };\n","/*\n * useDeterminateState\n *\n * A hook that returns a state transitioning to a determinate value after a minimum duration.\n * For example, you could use this hook with a button that transitions into a \"loading\" state,\n * ensuring it remains in the \"loading\" state for at least 1000ms. This provides clear feedback\n * to the user that the action is being processed.\n *\n */\nimport React from \"react\";\n\ntype UseDeterminateStateParams<T> = {\n value: T;\n determinateValue: T;\n minDurationMs?: number;\n};\n\nconst useDeterminateState = <T>({\n value,\n determinateValue,\n minDurationMs = 1000,\n}: UseDeterminateStateParams<T>): T => {\n const [state, setState] = React.useState<T>(value);\n const timeoutRef = React.useRef<NodeJS.Timeout | null>(null);\n const startTimeRef = React.useRef<number | null>(null);\n\n const clearExistingTimeout = () => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n timeoutRef.current = null;\n }\n };\n\n const handleTransition = React.useCallback(() => {\n if (value === determinateValue) {\n clearExistingTimeout();\n setState(determinateValue);\n startTimeRef.current = Date.now();\n } else if (startTimeRef.current !== null) {\n const elapsedTime = Date.now() - startTimeRef.current;\n const remainingTime = minDurationMs - elapsedTime;\n\n if (remainingTime > 0) {\n clearExistingTimeout();\n timeoutRef.current = setTimeout(() => {\n setState(value);\n startTimeRef.current = null;\n }, remainingTime);\n } else {\n setState(value);\n startTimeRef.current = null;\n }\n } else {\n setState(value);\n }\n }, [value, determinateValue, minDurationMs]);\n\n React.useEffect(() => {\n handleTransition();\n return clearExistingTimeout;\n }, [value, handleTransition]);\n\n return state;\n};\n\nexport { useDeterminateState };\n"],"names":["FORWARD_REF_SYMBOL","mergeProps","slotProps","childProps","overrideProps","propName","slotPropValue","childPropValue","args","applyRefProps","children","props","ref","React","child","validChild","$$typeof","$$typeofType","tgphRef","RefToTgphRef","childrenProp","refStorage","nodeStorage","prevRef","currentNode","stableRef","node","currentRef","useDeterminateState","value","determinateValue","minDurationMs","state","setState","timeoutRef","startTimeRef","clearExistingTimeout","handleTransition","elapsedTime","remainingTime"],"mappings":"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"}
@@ -1,47 +1,59 @@
1
- import c from "react";
2
- const p = Symbol.for("react.forward_ref"), m = (e, o) => {
3
- const r = { ...o };
4
- for (const s in o) {
5
- const t = e[s], n = o[s];
6
- /^on[A-Z]/.test(s) ? t && n ? r[s] = (...f) => {
7
- n(...f), t(...f);
8
- } : t && (r[s] = t) : s === "style" ? r[s] = { ...t, ...n } : s === "className" && (r[s] = [t, n].filter(Boolean).join(" "));
1
+ import f from "react";
2
+ const p = Symbol.for("react.forward_ref"), R = (s, l) => {
3
+ const e = { ...l };
4
+ for (const c in l) {
5
+ const t = s[c], r = l[c];
6
+ /^on[A-Z]/.test(c) ? t && r ? e[c] = (...o) => {
7
+ r(...o), t(...o);
8
+ } : t && (e[c] = t) : c === "style" ? e[c] = { ...t, ...r } : c === "className" && (e[c] = [t, r].filter(Boolean).join(" "));
9
9
  }
10
- return { ...e, ...r };
11
- }, R = ({ children: e, ...o }, r) => e ? c.Children.toArray(e).map((t) => {
12
- if (c.isValidElement(t)) {
13
- const n = t, l = n.$$typeof, f = n.type.$$typeof, i = n.props, u = i.tgphRef;
14
- return l === p || f === p ? c.cloneElement(n, {
15
- ...m(o, i),
16
- tgphRef: u || r,
17
- ref: u || r
18
- }) : c.cloneElement(n, {
19
- ...m(o, i),
20
- tgphRef: u || r
10
+ return { ...s, ...e };
11
+ }, m = ({ children: s, ...l }, e) => s ? f.Children.toArray(s).map((t) => {
12
+ if (f.isValidElement(t)) {
13
+ const r = t, n = r.$$typeof, o = r.type.$$typeof, u = r.props, i = u.tgphRef;
14
+ return n === p || o === p ? f.cloneElement(r, {
15
+ ...R(l, u),
16
+ tgphRef: i || e,
17
+ ref: i || e
18
+ }) : f.cloneElement(r, {
19
+ ...R(l, u),
20
+ tgphRef: i || e
21
21
  });
22
22
  }
23
23
  return t;
24
- }) : null, d = c.forwardRef(
25
- ({ children: e, ...o }, r) => R({ children: e, ...o }, r)
24
+ }) : null, d = f.forwardRef(
25
+ ({ children: s, ...l }, e) => {
26
+ const c = f.useRef(e), t = f.useRef(null);
27
+ f.useEffect(() => {
28
+ const n = c.current, o = t.current;
29
+ n !== e && o && (typeof n == "function" ? n(null) : n && (n.current = null), typeof e == "function" ? e(o) : e && (e.current = o)), c.current = e;
30
+ });
31
+ const r = f.useCallback((n) => {
32
+ t.current = n;
33
+ const o = c.current;
34
+ typeof o == "function" ? o(n) : o && (o.current = n);
35
+ }, []);
36
+ return m({ children: s, ...l }, r);
37
+ }
26
38
  ), T = ({
27
- value: e,
28
- determinateValue: o,
29
- minDurationMs: r = 1e3
39
+ value: s,
40
+ determinateValue: l,
41
+ minDurationMs: e = 1e3
30
42
  }) => {
31
- const [s, t] = c.useState(e), n = c.useRef(null), l = c.useRef(null), f = () => {
32
- n.current && (clearTimeout(n.current), n.current = null);
33
- }, i = c.useCallback(() => {
34
- if (e === o)
35
- f(), t(o), l.current = Date.now();
36
- else if (l.current !== null) {
37
- const u = Date.now() - l.current, a = r - u;
38
- a > 0 ? (f(), n.current = setTimeout(() => {
39
- t(e), l.current = null;
40
- }, a)) : (t(e), l.current = null);
43
+ const [c, t] = f.useState(s), r = f.useRef(null), n = f.useRef(null), o = () => {
44
+ r.current && (clearTimeout(r.current), r.current = null);
45
+ }, u = f.useCallback(() => {
46
+ if (s === l)
47
+ o(), t(l), n.current = Date.now();
48
+ else if (n.current !== null) {
49
+ const i = Date.now() - n.current, a = e - i;
50
+ a > 0 ? (o(), r.current = setTimeout(() => {
51
+ t(s), n.current = null;
52
+ }, a)) : (t(s), n.current = null);
41
53
  } else
42
- t(e);
43
- }, [e, o, r]);
44
- return c.useEffect(() => (i(), f), [e, i]), s;
54
+ t(s);
55
+ }, [s, l, e]);
56
+ return f.useEffect(() => (u(), o), [s, u]), c;
45
57
  };
46
58
  export {
47
59
  d as RefToTgphRef,
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","sources":["../../src/components/RefToTgphRef/RefToTgphRef.tsx","../../src/hooks/useDeterminateState.ts"],"sourcesContent":["//\n// When interacting with components like a Radix.Trigger, they assume that\n// our components accept a `ref` prop. This is not the case with our components\n// because we use the `tgphRef` prop instead. To work around this, we can create\n// a new component that accepts a `ref` prop and forwards it to the `tgphRef`\n// prop.\n//\nimport React from \"react\";\n\nconst FORWARD_REF_SYMBOL = Symbol.for(\"react.forward_ref\");\n\ntype ApplyRefPropsProps = {\n children: React.ReactNode;\n};\n\ntype Child = React.ReactElement & {\n $$typeof: symbol;\n type: { $$typeof: symbol };\n};\n\n// Merge props the same way that radix slot does\n// https://github.com/radix-ui/primitives/blob/main/packages/react/slot/src/Slot.tsx\nconst mergeProps = (\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n slotProps: Record<string, any>,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n childProps: Record<string, any>,\n) => {\n // all child props should override\n const overrideProps = { ...childProps };\n\n for (const propName in childProps) {\n const slotPropValue = slotProps[propName];\n const childPropValue = childProps[propName];\n\n const isHandler = /^on[A-Z]/.test(propName);\n if (isHandler) {\n // if the handler exists on both, we compose them\n if (slotPropValue && childPropValue) {\n overrideProps[propName] = (...args: unknown[]) => {\n childPropValue(...args);\n slotPropValue(...args);\n };\n }\n // but if it exists only on the slot, we use only this one\n else if (slotPropValue) {\n overrideProps[propName] = slotPropValue;\n }\n }\n // if it's `style`, we merge them\n else if (propName === \"style\") {\n overrideProps[propName] = { ...slotPropValue, ...childPropValue };\n } else if (propName === \"className\") {\n overrideProps[propName] = [slotPropValue, childPropValue]\n .filter(Boolean)\n .join(\" \");\n }\n }\n\n return { ...slotProps, ...overrideProps };\n};\n\nconst applyRefProps = (\n { children, ...props }: ApplyRefPropsProps,\n ref: React.Ref<unknown>,\n) => {\n if (!children) return null;\n const childrenArray = React.Children.toArray(children);\n return childrenArray.map((child) => {\n if (React.isValidElement(child)) {\n const validChild = child as Child;\n const $$typeof = validChild.$$typeof;\n const $$typeofType = validChild.type.$$typeof;\n const childProps = validChild.props;\n const tgphRef = childProps.tgphRef;\n\n // If we detect that the child is a forwardRef, we to pass the `ref` prop\n // to it so that components that exist outside of our library can still\n // receive the ref. We do it this way in order to avoid this warning:\n // \"Function components cannot be given refs. Attempts to access this ref will fail.\n // Did you mean to use React.forwardRef()\"\n if (\n $$typeof === FORWARD_REF_SYMBOL ||\n $$typeofType === FORWARD_REF_SYMBOL\n ) {\n return React.cloneElement(validChild, {\n ...mergeProps(props, childProps),\n tgphRef: tgphRef || ref,\n ref: tgphRef || ref,\n });\n }\n\n // Otherwise, we can just pass the `tgphRef` prop to the child.\n return React.cloneElement(validChild, {\n ...mergeProps(props, childProps),\n tgphRef: tgphRef || ref,\n });\n }\n\n // If the child is not a valid element, we can just return it.\n return child;\n });\n};\n\n// We can't generate the type of the ref because it's a forwardRef so any\n// works for this use case\n//\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst RefToTgphRef = React.forwardRef<any, any>(\n ({ children: childrenProp, ...props }, ref) => {\n return applyRefProps({ children: childrenProp, ...props }, ref);\n },\n);\n\nexport { RefToTgphRef };\n","/*\n * useDeterminateState\n *\n * A hook that returns a state transitioning to a determinate value after a minimum duration.\n * For example, you could use this hook with a button that transitions into a \"loading\" state,\n * ensuring it remains in the \"loading\" state for at least 1000ms. This provides clear feedback\n * to the user that the action is being processed.\n *\n */\nimport React from \"react\";\n\ntype UseDeterminateStateParams<T> = {\n value: T;\n determinateValue: T;\n minDurationMs?: number;\n};\n\nconst useDeterminateState = <T>({\n value,\n determinateValue,\n minDurationMs = 1000,\n}: UseDeterminateStateParams<T>): T => {\n const [state, setState] = React.useState<T>(value);\n const timeoutRef = React.useRef<NodeJS.Timeout | null>(null);\n const startTimeRef = React.useRef<number | null>(null);\n\n const clearExistingTimeout = () => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n timeoutRef.current = null;\n }\n };\n\n const handleTransition = React.useCallback(() => {\n if (value === determinateValue) {\n clearExistingTimeout();\n setState(determinateValue);\n startTimeRef.current = Date.now();\n } else if (startTimeRef.current !== null) {\n const elapsedTime = Date.now() - startTimeRef.current;\n const remainingTime = minDurationMs - elapsedTime;\n\n if (remainingTime > 0) {\n clearExistingTimeout();\n timeoutRef.current = setTimeout(() => {\n setState(value);\n startTimeRef.current = null;\n }, remainingTime);\n } else {\n setState(value);\n startTimeRef.current = null;\n }\n } else {\n setState(value);\n }\n }, [value, determinateValue, minDurationMs]);\n\n React.useEffect(() => {\n handleTransition();\n return clearExistingTimeout;\n }, [value, handleTransition]);\n\n return state;\n};\n\nexport { useDeterminateState };\n"],"names":["FORWARD_REF_SYMBOL","mergeProps","slotProps","childProps","overrideProps","propName","slotPropValue","childPropValue","args","applyRefProps","children","props","ref","React","child","validChild","$$typeof","$$typeofType","tgphRef","RefToTgphRef","childrenProp","useDeterminateState","value","determinateValue","minDurationMs","state","setState","timeoutRef","startTimeRef","clearExistingTimeout","handleTransition","elapsedTime","remainingTime"],"mappings":";AASA,MAAMA,IAAqB,OAAO,IAAI,mBAAmB,GAanDC,IAAa,CAEjBC,GAEAC,MACG;AAEG,QAAAC,IAAgB,EAAE,GAAGD,EAAW;AAEtC,aAAWE,KAAYF,GAAY;AAC3B,UAAAG,IAAgBJ,EAAUG,CAAQ,GAClCE,IAAiBJ,EAAWE,CAAQ;AAG1C,IADkB,WAAW,KAAKA,CAAQ,IAGpCC,KAAiBC,IACLH,EAAAC,CAAQ,IAAI,IAAIG,MAAoB;AAChD,MAAAD,EAAe,GAAGC,CAAI,GACtBF,EAAc,GAAGE,CAAI;AAAA,IACvB,IAGOF,MACPF,EAAcC,CAAQ,IAAIC,KAIrBD,MAAa,UACpBD,EAAcC,CAAQ,IAAI,EAAE,GAAGC,GAAe,GAAGC,EAAe,IACvDF,MAAa,gBACRD,EAAAC,CAAQ,IAAI,CAACC,GAAeC,CAAc,EACrD,OAAO,OAAO,EACd,KAAK,GAAG;AAAA,EACb;AAGF,SAAO,EAAE,GAAGL,GAAW,GAAGE,EAAc;AAC1C,GAEMK,IAAgB,CACpB,EAAE,UAAAC,GAAU,GAAGC,EAAA,GACfC,MAEKF,IACiBG,EAAM,SAAS,QAAQH,CAAQ,EAChC,IAAI,CAACI,MAAU;AAC9B,MAAAD,EAAM,eAAeC,CAAK,GAAG;AAC/B,UAAMC,IAAaD,GACbE,IAAWD,EAAW,UACtBE,IAAeF,EAAW,KAAK,UAC/BZ,IAAaY,EAAW,OACxBG,IAAUf,EAAW;AAQzB,WAAAa,MAAahB,KACbiB,MAAiBjB,IAEVa,EAAM,aAAaE,GAAY;AAAA,MACpC,GAAGd,EAAWU,GAAOR,CAAU;AAAA,MAC/B,SAASe,KAAWN;AAAA,MACpB,KAAKM,KAAWN;AAAA,IAAA,CACjB,IAIIC,EAAM,aAAaE,GAAY;AAAA,MACpC,GAAGd,EAAWU,GAAOR,CAAU;AAAA,MAC/B,SAASe,KAAWN;AAAA,IAAA,CACrB;AAAA,EAAA;AAII,SAAAE;AAAA,CACR,IAnCqB,MA0ClBK,IAAeN,EAAM;AAAA,EACzB,CAAC,EAAE,UAAUO,GAAc,GAAGT,EAAA,GAASC,MAC9BH,EAAc,EAAE,UAAUW,GAAc,GAAGT,KAASC,CAAG;AAElE,GC/FMS,IAAsB,CAAI;AAAA,EAC9B,OAAAC;AAAA,EACA,kBAAAC;AAAA,EACA,eAAAC,IAAgB;AAClB,MAAuC;AACrC,QAAM,CAACC,GAAOC,CAAQ,IAAIb,EAAM,SAAYS,CAAK,GAC3CK,IAAad,EAAM,OAA8B,IAAI,GACrDe,IAAef,EAAM,OAAsB,IAAI,GAE/CgB,IAAuB,MAAM;AACjC,IAAIF,EAAW,YACb,aAAaA,EAAW,OAAO,GAC/BA,EAAW,UAAU;AAAA,EAEzB,GAEMG,IAAmBjB,EAAM,YAAY,MAAM;AAC/C,QAAIS,MAAUC;AACS,MAAAM,EAAA,GACrBH,EAASH,CAAgB,GACZK,EAAA,UAAU,KAAK,IAAI;AAAA,aACvBA,EAAa,YAAY,MAAM;AACxC,YAAMG,IAAc,KAAK,IAAI,IAAIH,EAAa,SACxCI,IAAgBR,IAAgBO;AAEtC,MAAIC,IAAgB,KACGH,EAAA,GACVF,EAAA,UAAU,WAAW,MAAM;AACpC,QAAAD,EAASJ,CAAK,GACdM,EAAa,UAAU;AAAA,SACtBI,CAAa,MAEhBN,EAASJ,CAAK,GACdM,EAAa,UAAU;AAAA,IACzB;AAEA,MAAAF,EAASJ,CAAK;AAAA,EAEf,GAAA,CAACA,GAAOC,GAAkBC,CAAa,CAAC;AAE3C,SAAAX,EAAM,UAAU,OACGiB,EAAA,GACVD,IACN,CAACP,GAAOQ,CAAgB,CAAC,GAErBL;AACT;"}
1
+ {"version":3,"file":"index.mjs","sources":["../../src/components/RefToTgphRef/RefToTgphRef.tsx","../../src/hooks/useDeterminateState.ts"],"sourcesContent":["/**\n * RefToTgphRef Component\n *\n * PURPOSE:\n * ========\n * This component bridges the gap between third-party libraries (like Radix UI) and\n * Telegraph components. Third-party libraries expect components to accept a standard\n * React `ref` prop, but Telegraph components use a custom `tgphRef` prop instead.\n *\n * Without this adapter, using Telegraph components with libraries like Radix would fail\n * because Radix would try to pass a `ref` that Telegraph components wouldn't receive.\n *\n * EXAMPLE USAGE:\n * ==============\n * ```tsx\n * <RadixTooltip.Trigger asChild>\n * <RefToTgphRef>\n * <Button>Hover me</Button> // Button uses tgphRef internally\n * </RefToTgphRef>\n * </RadixTooltip.Trigger>\n * ```\n *\n * WHAT IT DOES:\n * =============\n * 1. Receives a `ref` from the parent (e.g., Radix)\n * 2. Forwards it as both `ref` AND `tgphRef` to Telegraph children\n * 3. Merges any additional props from the parent with child props\n * 4. Handles both forwardRef components and regular components appropriately\n *\n * THE INFINITE LOOP PROBLEM:\n * ==========================\n * Radix and other libraries often pass ref callbacks that are recreated on every render\n * (new function references). When we pass these unstable refs to children via\n * React.cloneElement, it causes the child to re-render with \"new\" props even though\n * the ref functionality hasn't actually changed. This can trigger infinite render loops.\n *\n * THE SOLUTION:\n * =============\n * We create a STABLE ref callback using useCallback with an empty dependency array,\n * so the function reference never changes. We store the actual (unstable) ref in a\n * mutable ref (refStorage) and update it on every render. When our stable callback\n * is invoked, it reads from refStorage to get the latest ref and calls it.\n *\n * We also track the DOM node so that if the ref callback itself changes (rare but\n * possible), we can properly cleanup the old ref by calling it with null, and then\n * call the new ref with the current node. This matches React's standard ref behavior.\n */\nimport React from \"react\";\n\nconst FORWARD_REF_SYMBOL = Symbol.for(\"react.forward_ref\");\n\ntype ApplyRefPropsProps = {\n children: React.ReactNode;\n};\n\ntype Child = React.ReactElement & {\n $$typeof: symbol;\n type: { $$typeof: symbol };\n};\n\n/**\n * mergeProps\n *\n * Merges props from the slot (parent/wrapper) with props from the child component.\n * This follows the same approach as Radix's Slot component to ensure compatibility.\n *\n * MERGE STRATEGY:\n * - Event handlers (onX): Compose them so both parent and child handlers run\n * - style: Merge objects (child styles override parent styles with same keys)\n * - className: Concatenate both class strings\n * - Other props: Child props override parent props\n *\n * @see https://github.com/radix-ui/primitives/blob/main/packages/react/slot/src/Slot.tsx\n */\nconst mergeProps = (\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n slotProps: Record<string, any>,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n childProps: Record<string, any>,\n) => {\n // all child props should override\n const overrideProps = { ...childProps };\n\n for (const propName in childProps) {\n const slotPropValue = slotProps[propName];\n const childPropValue = childProps[propName];\n\n const isHandler = /^on[A-Z]/.test(propName);\n if (isHandler) {\n // if the handler exists on both, we compose them\n if (slotPropValue && childPropValue) {\n overrideProps[propName] = (...args: unknown[]) => {\n childPropValue(...args);\n slotPropValue(...args);\n };\n }\n // but if it exists only on the slot, we use only this one\n else if (slotPropValue) {\n overrideProps[propName] = slotPropValue;\n }\n }\n // if it's `style`, we merge them\n else if (propName === \"style\") {\n overrideProps[propName] = { ...slotPropValue, ...childPropValue };\n } else if (propName === \"className\") {\n overrideProps[propName] = [slotPropValue, childPropValue]\n .filter(Boolean)\n .join(\" \");\n }\n }\n\n return { ...slotProps, ...overrideProps };\n};\n\n/**\n * applyRefProps\n *\n * Clones child elements and applies the forwarded ref and any merged props to them.\n *\n * KEY DECISIONS:\n *\n * 1. ForwardRef Detection:\n * We check if a child is a forwardRef component by inspecting its $$typeof symbol.\n * This is necessary because forwardRef components EXPECT a `ref` prop, while\n * regular function components would throw a warning if given one.\n *\n * 2. Dual Ref Forwarding (forwardRef components):\n * For forwardRef components, we pass BOTH `ref` and `tgphRef` because:\n * - They might be third-party components that only understand `ref`\n * - They might be Telegraph components that need `tgphRef`\n * - Passing both ensures compatibility with all cases\n *\n * 3. Single Ref Forwarding (regular components):\n * For non-forwardRef components, we only pass `tgphRef` to avoid React warnings\n * about function components receiving refs.\n *\n * 4. Ref Priority:\n * If a child already has a `tgphRef`, we use that instead of the forwarded ref.\n * This allows child components to override ref behavior if needed.\n */\nconst applyRefProps = (\n { children, ...props }: ApplyRefPropsProps,\n ref: React.Ref<unknown>,\n) => {\n if (!children) return null;\n const childrenArray = React.Children.toArray(children);\n return childrenArray.map((child) => {\n if (React.isValidElement(child)) {\n const validChild = child as Child;\n const $$typeof = validChild.$$typeof;\n const $$typeofType = validChild.type.$$typeof;\n const childProps = validChild.props as Record<string, unknown>;\n const tgphRef = childProps.tgphRef;\n\n // CASE 1: ForwardRef Component\n // Pass both `ref` and `tgphRef` to ensure compatibility with both\n // Telegraph components and third-party forwardRef components.\n if (\n $$typeof === FORWARD_REF_SYMBOL ||\n $$typeofType === FORWARD_REF_SYMBOL\n ) {\n return React.cloneElement(validChild, {\n ...mergeProps(props, childProps as Record<string, unknown>),\n tgphRef: tgphRef || ref,\n ref: tgphRef || ref,\n } as Record<string, unknown>);\n }\n\n // CASE 2: Regular Component\n // Only pass `tgphRef` to avoid React warnings about function components\n // receiving refs (which would happen if we passed `ref`).\n return React.cloneElement(validChild, {\n ...mergeProps(props, childProps as Record<string, unknown>),\n tgphRef: tgphRef || ref,\n } as Record<string, unknown>);\n }\n\n // CASE 3: Non-element children (strings, numbers, etc.)\n // Return as-is since they can't receive refs or props.\n return child;\n });\n};\n\n/**\n * RefToTgphRef Component Implementation\n *\n * TYPE CONSTRAINTS:\n * We use `any` for the ref type because this component must accept refs of any type\n * (HTMLButtonElement, HTMLDivElement, custom component refs, etc.). Since we're\n * forwarding refs generically, there's no way to statically type this without\n * making the API cumbersome.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst RefToTgphRef = React.forwardRef<any, any>(\n ({ children: childrenProp, ...props }, ref) => {\n /**\n * REF STABILIZATION ARCHITECTURE\n *\n * PROBLEM:\n * Libraries like Radix create new ref callback functions on every render.\n * If we pass these unstable refs directly to children via React.cloneElement,\n * React sees the props object as changed (new function reference), causing\n * unnecessary re-renders and potential infinite loops.\n *\n * SOLUTION OVERVIEW:\n * Create a stable callback (stableRef) that never changes (empty deps array),\n * but internally reads from a mutable storage to get the latest ref. This way:\n * - Children receive the same function reference every render (no infinite loops)\n * - The function still forwards to the latest ref (functionality preserved)\n *\n */\n\n // Storage for the latest ref callback/object from parent (e.g., Radix)\n // This gets updated on every render but doesn't cause re-renders since it's\n // a mutable ref, not state.\n const refStorage = React.useRef(ref);\n\n // Storage for the current DOM node/component instance\n // We need this to handle ref changes properly (cleanup old, set new)\n const nodeStorage = React.useRef<unknown>(null);\n\n /**\n * REF CHANGE HANDLING\n *\n * When the parent ref changes (rare, but possible), we need to:\n * 1. Call the OLD ref with null (cleanup - standard React behavior)\n * 2. Call the NEW ref with the current node (re-attach)\n *\n * This matches React's native behavior when a ref prop changes.\n *\n * WHY IN useEffect:\n * We use useEffect (not direct assignment) because we need to detect when\n * the ref has actually changed between renders and perform cleanup/setup.\n */\n React.useEffect(() => {\n const prevRef = refStorage.current;\n const currentNode = nodeStorage.current;\n\n // Detect ref change\n if (prevRef !== ref && currentNode) {\n // Step 1: Cleanup old ref (call with null)\n if (typeof prevRef === \"function\") {\n prevRef(null);\n } else if (prevRef) {\n (prevRef as React.MutableRefObject<unknown>).current = null;\n }\n\n // Step 2: Set new ref with current node\n if (typeof ref === \"function\") {\n ref(currentNode);\n } else if (ref) {\n (ref as React.MutableRefObject<unknown>).current = currentNode;\n }\n }\n\n // Update storage with latest ref for next render\n refStorage.current = ref;\n });\n\n /**\n * STABLE REF CALLBACK\n *\n * This is the key to preventing infinite loops. The function reference\n * returned by useCallback with an empty dependency array NEVER changes.\n *\n * When called (by React when attaching/detaching from DOM):\n * 1. Store the node so we can handle ref changes\n * 2. Read the LATEST ref from refStorage\n * 3. Forward the call to that ref\n *\n * This indirection gives us stability (no infinite loops) while maintaining\n * correctness (always calls the latest ref).\n */\n const stableRef = React.useCallback((node: unknown) => {\n // Store node for ref change handling\n nodeStorage.current = node;\n\n // Get the current ref (might have been updated since last call)\n const currentRef = refStorage.current;\n\n // Forward to the actual ref (handle both callback refs and ref objects)\n if (typeof currentRef === \"function\") {\n currentRef(node);\n } else if (currentRef) {\n (currentRef as React.MutableRefObject<unknown>).current = node;\n }\n }, []); // Empty deps = stable function reference forever\n\n // Apply the stable ref and merged props to children\n return applyRefProps({ children: childrenProp, ...props }, stableRef);\n },\n);\n\nexport { RefToTgphRef };\n","/*\n * useDeterminateState\n *\n * A hook that returns a state transitioning to a determinate value after a minimum duration.\n * For example, you could use this hook with a button that transitions into a \"loading\" state,\n * ensuring it remains in the \"loading\" state for at least 1000ms. This provides clear feedback\n * to the user that the action is being processed.\n *\n */\nimport React from \"react\";\n\ntype UseDeterminateStateParams<T> = {\n value: T;\n determinateValue: T;\n minDurationMs?: number;\n};\n\nconst useDeterminateState = <T>({\n value,\n determinateValue,\n minDurationMs = 1000,\n}: UseDeterminateStateParams<T>): T => {\n const [state, setState] = React.useState<T>(value);\n const timeoutRef = React.useRef<NodeJS.Timeout | null>(null);\n const startTimeRef = React.useRef<number | null>(null);\n\n const clearExistingTimeout = () => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n timeoutRef.current = null;\n }\n };\n\n const handleTransition = React.useCallback(() => {\n if (value === determinateValue) {\n clearExistingTimeout();\n setState(determinateValue);\n startTimeRef.current = Date.now();\n } else if (startTimeRef.current !== null) {\n const elapsedTime = Date.now() - startTimeRef.current;\n const remainingTime = minDurationMs - elapsedTime;\n\n if (remainingTime > 0) {\n clearExistingTimeout();\n timeoutRef.current = setTimeout(() => {\n setState(value);\n startTimeRef.current = null;\n }, remainingTime);\n } else {\n setState(value);\n startTimeRef.current = null;\n }\n } else {\n setState(value);\n }\n }, [value, determinateValue, minDurationMs]);\n\n React.useEffect(() => {\n handleTransition();\n return clearExistingTimeout;\n }, [value, handleTransition]);\n\n return state;\n};\n\nexport { useDeterminateState };\n"],"names":["FORWARD_REF_SYMBOL","mergeProps","slotProps","childProps","overrideProps","propName","slotPropValue","childPropValue","args","applyRefProps","children","props","ref","React","child","validChild","$$typeof","$$typeofType","tgphRef","RefToTgphRef","childrenProp","refStorage","nodeStorage","prevRef","currentNode","stableRef","node","currentRef","useDeterminateState","value","determinateValue","minDurationMs","state","setState","timeoutRef","startTimeRef","clearExistingTimeout","handleTransition","elapsedTime","remainingTime"],"mappings":"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"}
@@ -1,4 +1,13 @@
1
1
  import { default as React } from 'react';
2
+ /**
3
+ * RefToTgphRef Component Implementation
4
+ *
5
+ * TYPE CONSTRAINTS:
6
+ * We use `any` for the ref type because this component must accept refs of any type
7
+ * (HTMLButtonElement, HTMLDivElement, custom component refs, etc.). Since we're
8
+ * forwarding refs generically, there's no way to statically type this without
9
+ * making the API cumbersome.
10
+ */
2
11
  declare const RefToTgphRef: React.ForwardRefExoticComponent<Omit<any, "ref"> & React.RefAttributes<any>>;
3
12
  export { RefToTgphRef };
4
13
  //# sourceMappingURL=RefToTgphRef.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"RefToTgphRef.d.ts","sourceRoot":"","sources":["../../../../src/components/RefToTgphRef/RefToTgphRef.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,MAAM,OAAO,CAAC;AAqG1B,QAAA,MAAM,YAAY,8EAIjB,CAAC;AAEF,OAAO,EAAE,YAAY,EAAE,CAAC"}
1
+ {"version":3,"file":"RefToTgphRef.d.ts","sourceRoot":"","sources":["../../../../src/components/RefToTgphRef/RefToTgphRef.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,OAAO,KAAK,MAAM,OAAO,CAAC;AAwI1B;;;;;;;;GAQG;AAEH,QAAA,MAAM,YAAY,8EAkGjB,CAAC;AAEF,OAAO,EAAE,YAAY,EAAE,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"useDeterminateState.d.ts","sourceRoot":"","sources":["../../../src/hooks/useDeterminateState.ts"],"names":[],"mappings":"AAWA,KAAK,yBAAyB,CAAC,CAAC,IAAI;IAClC,KAAK,EAAE,CAAC,CAAC;IACT,gBAAgB,EAAE,CAAC,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,QAAA,MAAM,mBAAmB,GAAI,CAAC,+CAI3B,yBAAyB,CAAC,CAAC,CAAC,KAAG,CA0CjC,CAAC;AAEF,OAAO,EAAE,mBAAmB,EAAE,CAAC"}
1
+ {"version":3,"file":"useDeterminateState.d.ts","sourceRoot":"","sources":["../../../src/hooks/useDeterminateState.ts"],"names":[],"mappings":"AAWA,KAAK,yBAAyB,CAAC,CAAC,IAAI;IAClC,KAAK,EAAE,CAAC,CAAC;IACT,gBAAgB,EAAE,CAAC,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,QAAA,MAAM,mBAAmB,GAAI,CAAC,EAAE,6CAI7B,yBAAyB,CAAC,CAAC,CAAC,KAAG,CA0CjC,CAAC;AAEF,OAAO,EAAE,mBAAmB,EAAE,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telegraph/helpers",
3
- "version": "0.0.13",
3
+ "version": "0.0.15",
4
4
  "repository": "https://github.com/knocklabs/telegraph/tree/main/packages/helpers",
5
5
  "author": "@knocklabs",
6
6
  "license": "MIT",
@@ -29,16 +29,16 @@
29
29
  "preview": "vite preview"
30
30
  },
31
31
  "devDependencies": {
32
- "@knocklabs/eslint-config": "^0.0.4",
32
+ "@knocklabs/eslint-config": "^0.0.5",
33
33
  "@knocklabs/typescript-config": "^0.0.2",
34
34
  "@telegraph/prettier-config": "^0.0.7",
35
35
  "@telegraph/vite-config": "^0.0.15",
36
- "@types/react": "^18.3.18",
36
+ "@types/react": "^19.2.9",
37
37
  "eslint": "^8.56.0",
38
- "react": "^18.3.1",
39
- "react-dom": "^18.3.1",
40
- "typescript": "^5.7.3",
41
- "vite": "^6.0.11"
38
+ "react": "^19.2.3",
39
+ "react-dom": "^19.2.3",
40
+ "typescript": "^5.9.3",
41
+ "vite": "^6.4.1"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "react": "^18.0.0 || ^19.0.0",