Skip to content

Commit 2a9af26

Browse files
committed
Artifact actions
1 parent 4ff5352 commit 2a9af26

2 files changed

Lines changed: 59 additions & 20 deletions

File tree

apps/example/src/components/canvas/artifact-canvas.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export function ArtifactCanvas() {
1818
// - undefined = not possible with nuqs parseAsString
1919
// - string = open to specific type
2020
value: selectedType,
21-
onChange: (v: string | null) => setSelectedType(v ?? null),
21+
onChange: (v) => setSelectedType(v ?? null),
2222
});
2323

2424
// Only render if there are available artifacts

packages/artifacts/src/hooks.ts

Lines changed: 58 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -347,12 +347,17 @@ export function useArtifacts(
347347
const hadArtifactsRef = useRef(false);
348348
// Track the previous latest artifact type to detect when a new artifact type appears
349349
const prevLatestTypeRef = useRef<string | null>(null);
350+
// Track if value has ever been set (to distinguish initial null from user-closed null)
351+
const valueWasSetRef = useRef(false);
350352

351353
// Internal dismissed types state (for uncontrolled mode)
352354
const [internalDismissed, setInternalDismissed] = useState<Set<string>>(
353355
new Set(),
354356
);
355357

358+
// Internal value state (for uncontrolled mode)
359+
const [internalValue, setInternalValue] = useState<string | null>(null);
360+
356361
// Use external dismissed if provided, otherwise use internal
357362
const dismissedSet = useMemo(() => {
358363
if (externalDismissed) {
@@ -361,9 +366,26 @@ export function useArtifacts(
361366
return internalDismissed;
362367
}, [externalDismissed, internalDismissed]);
363368

369+
// Use external value if provided (controlled), otherwise use internal (uncontrolled)
370+
const currentValue = useMemo(() => {
371+
if (externalValue !== undefined) {
372+
return externalValue;
373+
}
374+
return internalValue;
375+
}, [externalValue, internalValue]);
376+
364377
const setValue = useCallback(
365378
(value: string | null) => {
366-
onChange?.(value);
379+
// Mark that value has been set (to distinguish initial null from user-closed null)
380+
valueWasSetRef.current = true;
381+
382+
if (onChange) {
383+
// Controlled mode - notify parent
384+
onChange(value);
385+
} else {
386+
// Uncontrolled mode - update internal state
387+
setInternalValue(value);
388+
}
367389
},
368390
[onChange],
369391
);
@@ -479,17 +501,17 @@ export function useArtifacts(
479501
? latestArtifact.type
480502
: types[0] || null;
481503

482-
// Determine activeType - simple derivation from externalValue:
483-
// 1. If externalValue is a valid type string → use it (canvas open)
484-
// 2. If externalValue is null/undefined AND artifacts just appeared (first time) → auto-open to latest
504+
// Determine activeType - simple derivation from currentValue:
505+
// 1. If currentValue is a valid type string → use it (canvas open)
506+
// 2. If currentValue is null/undefined AND artifacts just appeared (first time) → auto-open to latest
485507
// 3. Otherwise → null (closed)
486508
let activeType: string | null = null;
487509

488-
if (externalValue && types.includes(externalValue)) {
510+
if (currentValue && types.includes(currentValue)) {
489511
// Valid type provided - use it
490-
activeType = externalValue;
512+
activeType = currentValue;
491513
} else if (
492-
(externalValue === null || externalValue === undefined) &&
514+
(currentValue === null || currentValue === undefined) &&
493515
hasArtifacts &&
494516
!hadArtifacts &&
495517
types.length > 0
@@ -518,7 +540,7 @@ export function useArtifacts(
518540
available,
519541
dismissed,
520542
};
521-
}, [messages, include, exclude, externalValue, dismissedSet]);
543+
}, [messages, include, exclude, currentValue, dismissedSet]);
522544

523545
// Auto-switch to latest artifact: when a new artifact appears, switch to it
524546
useEffect(() => {
@@ -528,7 +550,7 @@ export function useArtifacts(
528550
// Update ref for next render
529551
prevLatestTypeRef.current = currentLatestType;
530552

531-
if (!onChange || !currentLatestType) {
553+
if (!currentLatestType) {
532554
return;
533555
}
534556

@@ -541,21 +563,29 @@ export function useArtifacts(
541563
artifactsData.types.includes(currentLatestType)
542564
) {
543565
// A new artifact appeared - auto-switch to it
544-
onChange(currentLatestType);
566+
if (onChange) {
567+
onChange(currentLatestType);
568+
} else {
569+
setInternalValue(currentLatestType);
570+
}
545571
} else if (
546572
prevLatestType === null &&
547573
currentLatestType !== null &&
548-
(externalValue === null || externalValue === undefined) &&
574+
(currentValue === null || currentValue === undefined) &&
549575
artifactsData.activeType !== null
550576
) {
551577
// First artifact appeared and no query param - sync to open it
552-
onChange(currentLatestType);
578+
if (onChange) {
579+
onChange(currentLatestType);
580+
} else {
581+
setInternalValue(currentLatestType);
582+
}
553583
}
554584
}, [
555585
artifactsData.activeType,
556586
artifactsData.latestArtifactType,
557587
artifactsData.types,
558-
externalValue,
588+
currentValue,
559589
onChange,
560590
]);
561591

@@ -570,23 +600,32 @@ export function useArtifacts(
570600
}, [artifactsData.activeType, dismissedSet, restore]);
571601

572602
// Auto-activate first available tab when there's no valid activeType
573-
// But don't auto-activate if user explicitly closed the canvas (externalValue is null/undefined)
603+
// But don't auto-activate if user explicitly closed the canvas
574604
useEffect(() => {
605+
// Skip auto-activation if:
606+
// 1. Controlled mode: user explicitly set externalValue to null
607+
// 2. Uncontrolled mode: value was previously set and is now null (user closed it)
608+
const shouldSkipAutoActivate =
609+
(externalValue !== undefined && externalValue === null) ||
610+
(externalValue === undefined &&
611+
valueWasSetRef.current &&
612+
currentValue === null);
613+
575614
if (
576615
artifactsData.available.length > 0 &&
577616
(!artifactsData.activeType ||
578617
!artifactsData.available.includes(artifactsData.activeType)) &&
579-
onChange &&
580-
externalValue != null // Don't auto-activate if user explicitly closed (check for both null and undefined)
618+
!shouldSkipAutoActivate
581619
) {
582-
// Set the first available tab as active
583-
onChange(artifactsData.available[0]);
620+
// Set the first available tab as active (use setValue to mark ref)
621+
setValue(artifactsData.available[0]);
584622
}
585623
}, [
586624
artifactsData.available,
587625
artifactsData.activeType,
588-
onChange,
589626
externalValue,
627+
currentValue,
628+
setValue,
590629
]);
591630

592631
// Create actions

0 commit comments

Comments
 (0)