snapshot of https://github.com/shadcn-ui/ui.git (main, 2026-09-15; blobs > 131072B omitted)
This commit is contained in:
142
apps/v4/app/(app)/(create)/hooks/use-action-menu.ts
Normal file
142
apps/v4/app/(app)/(create)/hooks/use-action-menu.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { type RegistryItem } from "shadcn/schema"
|
||||
import useSWR from "swr"
|
||||
|
||||
import { useDesignSystemSearchParams } from "@/app/(app)/(create)/lib/search-params"
|
||||
import { groupItemsByType } from "@/app/(app)/(create)/lib/utils"
|
||||
|
||||
const ACTION_MENU_OPEN_KEY = "create:action-menu-open"
|
||||
|
||||
type ActionMenuItem = {
|
||||
id: string
|
||||
type: string
|
||||
label: string
|
||||
registryName: string
|
||||
}
|
||||
|
||||
type ActionMenuGroup = {
|
||||
type: string
|
||||
title: string
|
||||
items: ActionMenuItem[]
|
||||
}
|
||||
|
||||
type ActionMenuSourceItem = Pick<RegistryItem, "name" | "title" | "type">
|
||||
|
||||
const SEARCH_KEYWORDS: Record<string, string> = {
|
||||
"registry:block": "block blocks component components",
|
||||
"registry:item": "item items component components",
|
||||
}
|
||||
|
||||
function sortRegistryGroups(groups: ReturnType<typeof groupItemsByType>) {
|
||||
return [...groups].sort((a, b) => {
|
||||
if (a.type === b.type) {
|
||||
return a.title.localeCompare(b.title)
|
||||
}
|
||||
if (a.type === "registry:block") {
|
||||
return -1
|
||||
}
|
||||
if (b.type === "registry:block") {
|
||||
return 1
|
||||
}
|
||||
return a.title.localeCompare(b.title)
|
||||
})
|
||||
}
|
||||
|
||||
export function useActionMenu(
|
||||
itemsByBase: Record<string, ActionMenuSourceItem[]>
|
||||
) {
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
const { data: open = false, mutate: setOpenData } = useSWR<boolean>(
|
||||
ACTION_MENU_OPEN_KEY,
|
||||
{
|
||||
fallbackData: false,
|
||||
revalidateOnFocus: false,
|
||||
revalidateIfStale: false,
|
||||
revalidateOnReconnect: false,
|
||||
}
|
||||
)
|
||||
|
||||
const groups = React.useMemo<ActionMenuGroup[]>(() => {
|
||||
const currentBaseItems = itemsByBase?.[params.base] ?? []
|
||||
const sortedRegistryGroups = sortRegistryGroups(
|
||||
groupItemsByType(currentBaseItems)
|
||||
)
|
||||
|
||||
return sortedRegistryGroups.map((group) => ({
|
||||
type: group.type,
|
||||
title: group.title,
|
||||
items: group.items.map((item) => ({
|
||||
id: `${group.type}:${item.name}`,
|
||||
type: group.type,
|
||||
label: item.title ?? item.name,
|
||||
registryName: item.name,
|
||||
})),
|
||||
}))
|
||||
}, [itemsByBase, params.base])
|
||||
|
||||
const activeRegistryName = params.item
|
||||
|
||||
const handleSelect = React.useCallback(
|
||||
(registryName: string) => {
|
||||
setParams({ item: registryName })
|
||||
void setOpenData(false, { revalidate: false })
|
||||
},
|
||||
[setOpenData, setParams]
|
||||
)
|
||||
|
||||
const handleOpenChange = React.useCallback(
|
||||
(nextOpen: boolean) => {
|
||||
void setOpenData(nextOpen, { revalidate: false })
|
||||
},
|
||||
[setOpenData]
|
||||
)
|
||||
|
||||
const getCommandValue = React.useCallback((item: ActionMenuItem) => {
|
||||
const keywords = SEARCH_KEYWORDS[item.type] ?? item.type.replace(":", " ")
|
||||
return `${item.label ?? ""} ${keywords}`.trim()
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
const down = (e: KeyboardEvent) => {
|
||||
if (e.key === "p" && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault()
|
||||
void setOpenData((currentOpen = false) => !currentOpen, {
|
||||
revalidate: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("keydown", down)
|
||||
return () => {
|
||||
document.removeEventListener("keydown", down)
|
||||
}
|
||||
}, [setOpenData])
|
||||
|
||||
return {
|
||||
activeRegistryName,
|
||||
getCommandValue,
|
||||
groups,
|
||||
handleSelect,
|
||||
open,
|
||||
setOpen: handleOpenChange,
|
||||
}
|
||||
}
|
||||
|
||||
export function useActionMenuTrigger() {
|
||||
const { mutate: setOpenData } = useSWR<boolean>(ACTION_MENU_OPEN_KEY, {
|
||||
fallbackData: false,
|
||||
revalidateOnFocus: false,
|
||||
revalidateIfStale: false,
|
||||
revalidateOnReconnect: false,
|
||||
})
|
||||
|
||||
const openActionMenu = React.useCallback(() => {
|
||||
void setOpenData(true, { revalidate: false })
|
||||
}, [setOpenData])
|
||||
|
||||
return {
|
||||
openActionMenu,
|
||||
}
|
||||
}
|
||||
11
apps/v4/app/(app)/(create)/hooks/use-design-system.ts
Normal file
11
apps/v4/app/(app)/(create)/hooks/use-design-system.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
"use client"
|
||||
|
||||
import { getPresetCode } from "@/app/(app)/(create)/lib/preset-code"
|
||||
import { useDesignSystemSearchParams } from "@/app/(app)/(create)/lib/search-params"
|
||||
|
||||
// Returns the canonical preset code derived from the current search params.
|
||||
export function usePresetCode() {
|
||||
const [params] = useDesignSystemSearchParams()
|
||||
|
||||
return getPresetCode(params)
|
||||
}
|
||||
185
apps/v4/app/(app)/(create)/hooks/use-history.tsx
Normal file
185
apps/v4/app/(app)/(create)/hooks/use-history.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Suspense } from "react"
|
||||
import { useSearchParams } from "next/navigation"
|
||||
|
||||
import {
|
||||
useDesignSystemSearchParams,
|
||||
type DesignSystemSearchParams,
|
||||
} from "@/app/(app)/(create)/lib/search-params"
|
||||
|
||||
type HistoryContextValue = {
|
||||
canGoBack: boolean
|
||||
canGoForward: boolean
|
||||
goBack: () => void
|
||||
goForward: () => void
|
||||
}
|
||||
|
||||
const HistoryContext = React.createContext<HistoryContextValue | null>(null)
|
||||
|
||||
// Reads useSearchParams() in its own Suspense boundary so the
|
||||
// provider never blanks out children while search params resolve.
|
||||
// useSearchParams reflects the *settled* preset, which coalesces the
|
||||
// transient values nuqs emits mid-update — reading nuqs state directly
|
||||
// here would record those transients as phantom history entries.
|
||||
function PresetSync({
|
||||
onPresetChange,
|
||||
}: {
|
||||
onPresetChange: (preset: string) => void
|
||||
}) {
|
||||
const searchParams = useSearchParams()
|
||||
const preset = searchParams.get("preset") ?? ""
|
||||
|
||||
React.useEffect(() => {
|
||||
onPresetChange(preset)
|
||||
}, [preset, onPresetChange])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function HistoryProvider({ children }: { children: React.ReactNode }) {
|
||||
// Write through the shared search-params hook (shallow) instead of
|
||||
// router.replace. router.replace triggers a full server navigation that
|
||||
// resets the preset on prod — the same failure the customizer avoids by
|
||||
// going shallow (#11060).
|
||||
const [, setParams] = useDesignSystemSearchParams()
|
||||
|
||||
const [preset, setPreset] = React.useState("")
|
||||
|
||||
const entriesRef = React.useRef<string[]>([preset])
|
||||
const indexRef = React.useRef(0)
|
||||
const maxIndexRef = React.useRef(0)
|
||||
const isNavigatingRef = React.useRef(false)
|
||||
|
||||
const [index, setIndex] = React.useState(0)
|
||||
const [maxIndex, setMaxIndex] = React.useState(0)
|
||||
|
||||
const onPresetChange = React.useCallback((nextPreset: string) => {
|
||||
setPreset(nextPreset)
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isNavigatingRef.current) {
|
||||
isNavigatingRef.current = false
|
||||
return
|
||||
}
|
||||
|
||||
if (preset === entriesRef.current[indexRef.current]) {
|
||||
return
|
||||
}
|
||||
|
||||
const nextEntries = entriesRef.current.slice(0, indexRef.current + 1)
|
||||
nextEntries.push(preset)
|
||||
entriesRef.current = nextEntries
|
||||
|
||||
const nextIndex = nextEntries.length - 1
|
||||
indexRef.current = nextIndex
|
||||
maxIndexRef.current = nextIndex
|
||||
setIndex(nextIndex)
|
||||
setMaxIndex(nextIndex)
|
||||
}, [preset])
|
||||
|
||||
const canGoBack = index > 0
|
||||
const canGoForward = index < maxIndex
|
||||
|
||||
const goBack = React.useCallback(() => {
|
||||
if (indexRef.current <= 0) {
|
||||
return
|
||||
}
|
||||
|
||||
isNavigatingRef.current = true
|
||||
const nextIndex = indexRef.current - 1
|
||||
indexRef.current = nextIndex
|
||||
setIndex(nextIndex)
|
||||
|
||||
// The first history entry is "" (no preset in the URL); null clears the
|
||||
// param to restore that state. nuqs accepts null to clear a key, which the
|
||||
// wrapper's input type does not model.
|
||||
setParams(
|
||||
{
|
||||
preset: entriesRef.current[nextIndex] || null,
|
||||
} as Partial<DesignSystemSearchParams>,
|
||||
{ history: "replace" }
|
||||
)
|
||||
}, [setParams])
|
||||
|
||||
const goForward = React.useCallback(() => {
|
||||
if (indexRef.current >= maxIndexRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
isNavigatingRef.current = true
|
||||
const nextIndex = indexRef.current + 1
|
||||
indexRef.current = nextIndex
|
||||
setIndex(nextIndex)
|
||||
|
||||
// The first history entry is "" (no preset in the URL); null clears the
|
||||
// param to restore that state. nuqs accepts null to clear a key, which the
|
||||
// wrapper's input type does not model.
|
||||
setParams(
|
||||
{
|
||||
preset: entriesRef.current[nextIndex] || null,
|
||||
} as Partial<DesignSystemSearchParams>,
|
||||
{ history: "replace" }
|
||||
)
|
||||
}, [setParams])
|
||||
|
||||
React.useEffect(() => {
|
||||
const down = (e: KeyboardEvent) => {
|
||||
if (!e.metaKey && !e.ctrlKey) {
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
(e.target instanceof HTMLElement && e.target.isContentEditable) ||
|
||||
e.target instanceof HTMLInputElement ||
|
||||
e.target instanceof HTMLTextAreaElement ||
|
||||
e.target instanceof HTMLSelectElement
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const key = e.key.toLowerCase()
|
||||
|
||||
if ((key === "z" && e.shiftKey) || (key === "y" && e.ctrlKey)) {
|
||||
e.preventDefault()
|
||||
goForward()
|
||||
return
|
||||
}
|
||||
|
||||
if (key === "z") {
|
||||
e.preventDefault()
|
||||
goBack()
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("keydown", down)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keydown", down)
|
||||
}
|
||||
}, [goBack, goForward])
|
||||
|
||||
const value = React.useMemo(
|
||||
() => ({ canGoBack, canGoForward, goBack, goForward }),
|
||||
[canGoBack, canGoForward, goBack, goForward]
|
||||
)
|
||||
|
||||
return (
|
||||
<HistoryContext value={value}>
|
||||
<Suspense>
|
||||
<PresetSync onPresetChange={onPresetChange} />
|
||||
</Suspense>
|
||||
{children}
|
||||
</HistoryContext>
|
||||
)
|
||||
}
|
||||
|
||||
export function useHistory() {
|
||||
const context = React.useContext(HistoryContext)
|
||||
if (!context) {
|
||||
throw new Error("useHistory must be used within HistoryProvider")
|
||||
}
|
||||
return context
|
||||
}
|
||||
69
apps/v4/app/(app)/(create)/hooks/use-iframe-sync.tsx
Normal file
69
apps/v4/app/(app)/(create)/hooks/use-iframe-sync.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import type { DesignSystemSearchParams } from "@/app/(app)/(create)/lib/search-params"
|
||||
|
||||
type ParentToIframeMessage = {
|
||||
type: "design-system-params"
|
||||
data: DesignSystemSearchParams
|
||||
}
|
||||
|
||||
export const isInIframe = () => {
|
||||
if (typeof window === "undefined") {
|
||||
return false
|
||||
}
|
||||
return window.self !== window.top
|
||||
}
|
||||
|
||||
export function useIframeMessageListener<
|
||||
Message extends ParentToIframeMessage,
|
||||
MessageType extends Message["type"],
|
||||
>(
|
||||
messageType: MessageType,
|
||||
onMessage: (data: Extract<Message, { type: MessageType }>["data"]) => void
|
||||
) {
|
||||
const onMessageRef = React.useRef(onMessage)
|
||||
|
||||
React.useEffect(() => {
|
||||
onMessageRef.current = onMessage
|
||||
}, [onMessage])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isInIframe()) {
|
||||
return
|
||||
}
|
||||
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
if (event.data.type === messageType) {
|
||||
onMessageRef.current(event.data.data)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("message", handleMessage)
|
||||
return () => {
|
||||
window.removeEventListener("message", handleMessage)
|
||||
}
|
||||
}, [messageType])
|
||||
}
|
||||
|
||||
export function sendToIframe<
|
||||
Message extends ParentToIframeMessage,
|
||||
MessageType extends Message["type"],
|
||||
>(
|
||||
iframe: HTMLIFrameElement | null,
|
||||
messageType: MessageType,
|
||||
data: Extract<Message, { type: MessageType }>["data"]
|
||||
) {
|
||||
if (!iframe?.contentWindow) {
|
||||
return
|
||||
}
|
||||
|
||||
iframe.contentWindow.postMessage(
|
||||
{
|
||||
type: messageType,
|
||||
data,
|
||||
},
|
||||
"*"
|
||||
)
|
||||
}
|
||||
59
apps/v4/app/(app)/(create)/hooks/use-locks.tsx
Normal file
59
apps/v4/app/(app)/(create)/hooks/use-locks.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
export type LockableParam =
|
||||
| "style"
|
||||
| "baseColor"
|
||||
| "theme"
|
||||
| "chartColor"
|
||||
| "iconLibrary"
|
||||
| "font"
|
||||
| "fontHeading"
|
||||
| "menuAccent"
|
||||
| "menuColor"
|
||||
| "radius"
|
||||
|
||||
type LocksContextValue = {
|
||||
locks: Set<LockableParam>
|
||||
isLocked: (param: LockableParam) => boolean
|
||||
toggleLock: (param: LockableParam) => void
|
||||
}
|
||||
|
||||
const LocksContext = React.createContext<LocksContextValue | null>(null)
|
||||
|
||||
export function LocksProvider({ children }: { children: React.ReactNode }) {
|
||||
const [locks, setLocks] = React.useState<Set<LockableParam>>(new Set())
|
||||
|
||||
const isLocked = React.useCallback(
|
||||
(param: LockableParam) => locks.has(param),
|
||||
[locks]
|
||||
)
|
||||
|
||||
const toggleLock = React.useCallback((param: LockableParam) => {
|
||||
setLocks((prev) => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(param)) {
|
||||
next.delete(param)
|
||||
} else {
|
||||
next.add(param)
|
||||
}
|
||||
return next
|
||||
})
|
||||
}, [])
|
||||
|
||||
const value = React.useMemo(
|
||||
() => ({ locks, isLocked, toggleLock }),
|
||||
[locks, isLocked, toggleLock]
|
||||
)
|
||||
|
||||
return <LocksContext value={value}>{children}</LocksContext>
|
||||
}
|
||||
|
||||
export function useLocks() {
|
||||
const context = React.useContext(LocksContext)
|
||||
if (!context) {
|
||||
throw new Error("useLocks must be used within LocksProvider")
|
||||
}
|
||||
return context
|
||||
}
|
||||
81
apps/v4/app/(app)/(create)/hooks/use-open-preset.tsx
Normal file
81
apps/v4/app/(app)/(create)/hooks/use-open-preset.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import useSWR from "swr"
|
||||
|
||||
const OPEN_PRESET_KEY = "create:open-preset-open"
|
||||
export const OPEN_PRESET_FORWARD_TYPE = "open-preset-forward"
|
||||
|
||||
function isEditableTarget(target: EventTarget | null) {
|
||||
return (
|
||||
(target instanceof HTMLElement && target.isContentEditable) ||
|
||||
target instanceof HTMLInputElement ||
|
||||
target instanceof HTMLTextAreaElement ||
|
||||
target instanceof HTMLSelectElement
|
||||
)
|
||||
}
|
||||
|
||||
export function useOpenPreset() {
|
||||
const { data: open = false, mutate: setOpenData } = useSWR<boolean>(
|
||||
OPEN_PRESET_KEY,
|
||||
{
|
||||
fallbackData: false,
|
||||
revalidateOnFocus: false,
|
||||
revalidateIfStale: false,
|
||||
revalidateOnReconnect: false,
|
||||
}
|
||||
)
|
||||
|
||||
const handleOpenChange = React.useCallback(
|
||||
(nextOpen: boolean) => {
|
||||
void setOpenData(nextOpen, { revalidate: false })
|
||||
},
|
||||
[setOpenData]
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
const down = (e: KeyboardEvent) => {
|
||||
if (
|
||||
e.key === "o" &&
|
||||
!e.shiftKey &&
|
||||
!e.metaKey &&
|
||||
!e.ctrlKey &&
|
||||
!e.altKey
|
||||
) {
|
||||
if (isEditableTarget(e.target)) {
|
||||
return
|
||||
}
|
||||
|
||||
e.preventDefault()
|
||||
void setOpenData(true, { revalidate: false })
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("keydown", down)
|
||||
return () => {
|
||||
document.removeEventListener("keydown", down)
|
||||
}
|
||||
}, [setOpenData])
|
||||
|
||||
return {
|
||||
open,
|
||||
setOpen: handleOpenChange,
|
||||
}
|
||||
}
|
||||
|
||||
export function useOpenPresetTrigger() {
|
||||
const { mutate: setOpenData } = useSWR<boolean>(OPEN_PRESET_KEY, {
|
||||
fallbackData: false,
|
||||
revalidateOnFocus: false,
|
||||
revalidateIfStale: false,
|
||||
revalidateOnReconnect: false,
|
||||
})
|
||||
|
||||
const openPreset = React.useCallback(() => {
|
||||
void setOpenData(true, { revalidate: false })
|
||||
}, [setOpenData])
|
||||
|
||||
return {
|
||||
openPreset,
|
||||
}
|
||||
}
|
||||
249
apps/v4/app/(app)/(create)/hooks/use-random.tsx
Normal file
249
apps/v4/app/(app)/(create)/hooks/use-random.tsx
Normal file
@@ -0,0 +1,249 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { decodePreset } from "shadcn/preset"
|
||||
|
||||
import {
|
||||
BASE_COLORS,
|
||||
getThemesForBaseColor,
|
||||
iconLibraries,
|
||||
MENU_ACCENTS,
|
||||
MENU_COLORS,
|
||||
RADII,
|
||||
STYLES,
|
||||
type BaseColorName,
|
||||
type ChartColorName,
|
||||
type FontHeadingValue,
|
||||
type FontValue,
|
||||
type IconLibraryName,
|
||||
type MenuAccentValue,
|
||||
type MenuColorValue,
|
||||
type RadiusValue,
|
||||
type StyleName,
|
||||
type ThemeName,
|
||||
} from "@/registry/config"
|
||||
import { useLocks } from "@/app/(app)/(create)/hooks/use-locks"
|
||||
import { FONTS } from "@/app/(app)/(create)/lib/fonts"
|
||||
import { getPresetCode } from "@/app/(app)/(create)/lib/preset-code"
|
||||
import {
|
||||
applyBias,
|
||||
RANDOMIZE_BIASES,
|
||||
type RandomizeContext,
|
||||
} from "@/app/(app)/(create)/lib/randomize-biases"
|
||||
import {
|
||||
isTranslucentMenuColor,
|
||||
useDesignSystemSearchParams,
|
||||
} from "@/app/(app)/(create)/lib/search-params"
|
||||
import { SHUFFLE_PRESETS } from "@/app/(app)/(create)/lib/shuffle-presets"
|
||||
|
||||
function randomItem<T>(array: readonly T[]): T {
|
||||
return array[Math.floor(Math.random() * array.length)]
|
||||
}
|
||||
|
||||
export function useRandom() {
|
||||
const { locks } = useLocks()
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
|
||||
const paramsRef = React.useRef(params)
|
||||
React.useEffect(() => {
|
||||
paramsRef.current = params
|
||||
}, [params])
|
||||
|
||||
// True-random implementation. Kept, but shuffle is now wired to the
|
||||
// curated preset list below.
|
||||
const randomizeTrueRandom = React.useCallback(() => {
|
||||
const selectedStyle = locks.has("style")
|
||||
? paramsRef.current.style
|
||||
: randomItem(STYLES).name
|
||||
|
||||
const context: RandomizeContext = {
|
||||
style: selectedStyle,
|
||||
}
|
||||
|
||||
const availableBaseColors = applyBias(
|
||||
BASE_COLORS,
|
||||
context,
|
||||
RANDOMIZE_BIASES.baseColors
|
||||
)
|
||||
const baseColor = locks.has("baseColor")
|
||||
? paramsRef.current.baseColor
|
||||
: randomItem(availableBaseColors).name
|
||||
context.baseColor = baseColor
|
||||
|
||||
const availableThemes = getThemesForBaseColor(baseColor)
|
||||
const availableFonts = applyBias(FONTS, context, RANDOMIZE_BIASES.fonts)
|
||||
const availableRadii = applyBias(RADII, context, RANDOMIZE_BIASES.radius)
|
||||
|
||||
const selectedTheme = locks.has("theme")
|
||||
? paramsRef.current.theme
|
||||
: randomItem(availableThemes).name
|
||||
context.theme = selectedTheme
|
||||
|
||||
const availableChartColors = applyBias(
|
||||
getThemesForBaseColor(baseColor),
|
||||
context,
|
||||
RANDOMIZE_BIASES.chartColors
|
||||
)
|
||||
const selectedChartColor = locks.has("chartColor")
|
||||
? paramsRef.current.chartColor
|
||||
: randomItem(availableChartColors).name
|
||||
context.chartColor = selectedChartColor
|
||||
const selectedFont = locks.has("font")
|
||||
? paramsRef.current.font
|
||||
: randomItem(availableFonts).value
|
||||
context.font = selectedFont
|
||||
|
||||
// Pick heading font: ~70% inherit, ~30% distinct with cross-category contrast.
|
||||
let selectedFontHeading: FontHeadingValue
|
||||
if (locks.has("fontHeading")) {
|
||||
selectedFontHeading = paramsRef.current.fontHeading
|
||||
} else if (Math.random() < 0.7) {
|
||||
selectedFontHeading = "inherit"
|
||||
} else {
|
||||
const bodyType = availableFonts.find(
|
||||
(f) => f.value === selectedFont
|
||||
)?.type
|
||||
const contrastFonts = availableFonts.filter(
|
||||
(f) => f.type !== bodyType && f.value !== selectedFont
|
||||
)
|
||||
selectedFontHeading = (
|
||||
contrastFonts.length > 0
|
||||
? randomItem(contrastFonts)
|
||||
: randomItem(availableFonts)
|
||||
).value as FontHeadingValue
|
||||
}
|
||||
const selectedRadius = locks.has("radius")
|
||||
? paramsRef.current.radius
|
||||
: randomItem(availableRadii).name
|
||||
const selectedIconLibrary = locks.has("iconLibrary")
|
||||
? paramsRef.current.iconLibrary
|
||||
: randomItem(Object.values(iconLibraries)).name
|
||||
const lockedMenuAccent = locks.has("menuAccent")
|
||||
? paramsRef.current.menuAccent
|
||||
: undefined
|
||||
const availableMenuColors =
|
||||
!locks.has("menuColor") && lockedMenuAccent === "bold"
|
||||
? MENU_COLORS.filter((menuColor) => {
|
||||
return !isTranslucentMenuColor(menuColor.value)
|
||||
})
|
||||
: MENU_COLORS
|
||||
const selectedMenuColor = locks.has("menuColor")
|
||||
? paramsRef.current.menuColor
|
||||
: randomItem(availableMenuColors).value
|
||||
const selectedMenuAccent =
|
||||
locks.has("menuAccent") || isTranslucentMenuColor(selectedMenuColor)
|
||||
? paramsRef.current.menuAccent === "bold" &&
|
||||
isTranslucentMenuColor(selectedMenuColor)
|
||||
? "subtle"
|
||||
: paramsRef.current.menuAccent
|
||||
: randomItem(MENU_ACCENTS).value
|
||||
|
||||
context.radius = selectedRadius
|
||||
|
||||
const nextParams = {
|
||||
style: selectedStyle,
|
||||
baseColor,
|
||||
theme: selectedTheme,
|
||||
chartColor: selectedChartColor,
|
||||
iconLibrary: selectedIconLibrary,
|
||||
font: selectedFont,
|
||||
fontHeading: selectedFontHeading,
|
||||
menuAccent: selectedMenuAccent,
|
||||
menuColor: selectedMenuColor,
|
||||
radius: selectedRadius,
|
||||
}
|
||||
|
||||
// Keep the ref in sync so rapid repeats use the latest randomized state
|
||||
// even before the URL state finishes committing.
|
||||
paramsRef.current = {
|
||||
...paramsRef.current,
|
||||
...nextParams,
|
||||
}
|
||||
|
||||
setParams(nextParams)
|
||||
}, [setParams, locks])
|
||||
|
||||
// Picks a random preset from the curated list instead of true random.
|
||||
// Locked params are preserved over the decoded preset values.
|
||||
const randomize = React.useCallback(() => {
|
||||
// Avoid re-picking the preset that is currently applied.
|
||||
const currentCode = getPresetCode(paramsRef.current)
|
||||
const availableCodes = SHUFFLE_PRESETS.filter(
|
||||
(code) => code !== currentCode
|
||||
)
|
||||
const decoded = decodePreset(
|
||||
randomItem(availableCodes.length > 0 ? availableCodes : SHUFFLE_PRESETS)
|
||||
)
|
||||
|
||||
if (!decoded) {
|
||||
return
|
||||
}
|
||||
|
||||
const current = paramsRef.current
|
||||
const nextParams = {
|
||||
style: locks.has("style") ? current.style : (decoded.style as StyleName),
|
||||
baseColor: locks.has("baseColor")
|
||||
? current.baseColor
|
||||
: (decoded.baseColor as BaseColorName),
|
||||
theme: locks.has("theme") ? current.theme : (decoded.theme as ThemeName),
|
||||
chartColor: locks.has("chartColor")
|
||||
? current.chartColor
|
||||
: ((decoded.chartColor ?? decoded.theme) as ChartColorName),
|
||||
iconLibrary: locks.has("iconLibrary")
|
||||
? current.iconLibrary
|
||||
: (decoded.iconLibrary as IconLibraryName),
|
||||
font: locks.has("font") ? current.font : (decoded.font as FontValue),
|
||||
fontHeading: locks.has("fontHeading")
|
||||
? current.fontHeading
|
||||
: (decoded.fontHeading as FontHeadingValue),
|
||||
menuAccent: locks.has("menuAccent")
|
||||
? current.menuAccent
|
||||
: (decoded.menuAccent as MenuAccentValue),
|
||||
menuColor: locks.has("menuColor")
|
||||
? current.menuColor
|
||||
: (decoded.menuColor as MenuColorValue),
|
||||
radius: locks.has("radius")
|
||||
? current.radius
|
||||
: (decoded.radius as RadiusValue),
|
||||
}
|
||||
|
||||
// Keep the ref in sync so rapid repeats use the latest randomized state
|
||||
// even before the URL state finishes committing.
|
||||
paramsRef.current = {
|
||||
...paramsRef.current,
|
||||
...nextParams,
|
||||
}
|
||||
|
||||
setParams(nextParams)
|
||||
}, [setParams, locks])
|
||||
|
||||
const randomizeRef = React.useRef(randomize)
|
||||
React.useEffect(() => {
|
||||
randomizeRef.current = randomize
|
||||
}, [randomize])
|
||||
|
||||
React.useEffect(() => {
|
||||
const down = (e: KeyboardEvent) => {
|
||||
if (e.key === "r" && !e.shiftKey && !e.metaKey && !e.ctrlKey) {
|
||||
if (
|
||||
(e.target instanceof HTMLElement && e.target.isContentEditable) ||
|
||||
e.target instanceof HTMLInputElement ||
|
||||
e.target instanceof HTMLTextAreaElement ||
|
||||
e.target instanceof HTMLSelectElement
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
e.preventDefault()
|
||||
randomizeRef.current()
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("keydown", down)
|
||||
return () => {
|
||||
document.removeEventListener("keydown", down)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return { randomize, randomizeTrueRandom }
|
||||
}
|
||||
103
apps/v4/app/(app)/(create)/hooks/use-reset.tsx
Normal file
103
apps/v4/app/(app)/(create)/hooks/use-reset.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import useSWR from "swr"
|
||||
|
||||
import { DEFAULT_CONFIG, PRESETS } from "@/registry/config"
|
||||
import { useDesignSystemSearchParams } from "@/app/(app)/(create)/lib/search-params"
|
||||
|
||||
const RESET_DIALOG_KEY = "create:reset-dialog-open"
|
||||
export const RESET_FORWARD_TYPE = "reset-forward"
|
||||
|
||||
export function useReset() {
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
const { data: showResetDialog = false, mutate: setShowResetDialogData } =
|
||||
useSWR<boolean>(RESET_DIALOG_KEY, {
|
||||
fallbackData: false,
|
||||
revalidateOnFocus: false,
|
||||
revalidateIfStale: false,
|
||||
revalidateOnReconnect: false,
|
||||
})
|
||||
|
||||
const reset = React.useCallback(() => {
|
||||
const preset =
|
||||
PRESETS.find(
|
||||
(preset) => preset.base === params.base && preset.style === params.style
|
||||
) ?? DEFAULT_CONFIG
|
||||
|
||||
setParams({
|
||||
base: params.base,
|
||||
style: params.style,
|
||||
baseColor: preset.baseColor,
|
||||
theme: preset.theme,
|
||||
chartColor: preset.chartColor,
|
||||
iconLibrary: preset.iconLibrary,
|
||||
font: preset.font,
|
||||
fontHeading: preset.fontHeading,
|
||||
menuAccent: preset.menuAccent,
|
||||
menuColor: preset.menuColor,
|
||||
radius: preset.radius,
|
||||
template: DEFAULT_CONFIG.template,
|
||||
item: params.item,
|
||||
})
|
||||
}, [setParams, params.base, params.style, params.item])
|
||||
|
||||
const handleShowResetDialogChange = React.useCallback(
|
||||
(open: boolean) => {
|
||||
void setShowResetDialogData(open, { revalidate: false })
|
||||
},
|
||||
[setShowResetDialogData]
|
||||
)
|
||||
|
||||
const confirmReset = React.useCallback(() => {
|
||||
reset()
|
||||
void setShowResetDialogData(false, { revalidate: false })
|
||||
}, [reset, setShowResetDialogData])
|
||||
|
||||
const showResetDialogRef = React.useRef(showResetDialog)
|
||||
React.useEffect(() => {
|
||||
showResetDialogRef.current = showResetDialog
|
||||
}, [showResetDialog])
|
||||
|
||||
const confirmResetRef = React.useRef(confirmReset)
|
||||
React.useEffect(() => {
|
||||
confirmResetRef.current = confirmReset
|
||||
}, [confirmReset])
|
||||
|
||||
React.useEffect(() => {
|
||||
const down = (e: KeyboardEvent) => {
|
||||
if (e.key === "R" && e.shiftKey && !e.metaKey && !e.ctrlKey) {
|
||||
if (
|
||||
(e.target instanceof HTMLElement && e.target.isContentEditable) ||
|
||||
e.target instanceof HTMLInputElement ||
|
||||
e.target instanceof HTMLTextAreaElement ||
|
||||
e.target instanceof HTMLSelectElement
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
e.preventDefault()
|
||||
|
||||
// If the dialog is already open, confirm the reset.
|
||||
if (showResetDialogRef.current) {
|
||||
confirmResetRef.current()
|
||||
return
|
||||
}
|
||||
|
||||
handleShowResetDialogChange(true)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("keydown", down)
|
||||
return () => {
|
||||
document.removeEventListener("keydown", down)
|
||||
}
|
||||
}, [handleShowResetDialogChange])
|
||||
|
||||
return {
|
||||
reset,
|
||||
showResetDialog,
|
||||
setShowResetDialog: handleShowResetDialogChange,
|
||||
confirmReset,
|
||||
}
|
||||
}
|
||||
48
apps/v4/app/(app)/(create)/hooks/use-theme-toggle.tsx
Normal file
48
apps/v4/app/(app)/(create)/hooks/use-theme-toggle.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { useTheme } from "next-themes"
|
||||
|
||||
import { useMetaColor } from "@/hooks/use-meta-color"
|
||||
|
||||
export function useThemeToggle() {
|
||||
const { setTheme, resolvedTheme } = useTheme()
|
||||
const { setMetaColor, metaColor } = useMetaColor()
|
||||
|
||||
React.useEffect(() => {
|
||||
setMetaColor(metaColor)
|
||||
}, [metaColor, setMetaColor])
|
||||
|
||||
const toggleTheme = React.useCallback(() => {
|
||||
setTheme(resolvedTheme === "dark" ? "light" : "dark")
|
||||
}, [resolvedTheme, setTheme])
|
||||
|
||||
// Listen for the D key to toggle theme.
|
||||
React.useEffect(() => {
|
||||
const down = (e: KeyboardEvent) => {
|
||||
if (
|
||||
(e.key === "d" || e.key === "D") &&
|
||||
!e.metaKey &&
|
||||
!e.ctrlKey &&
|
||||
!e.altKey
|
||||
) {
|
||||
if (
|
||||
(e.target instanceof HTMLElement && e.target.isContentEditable) ||
|
||||
e.target instanceof HTMLInputElement ||
|
||||
e.target instanceof HTMLTextAreaElement ||
|
||||
e.target instanceof HTMLSelectElement
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
e.preventDefault()
|
||||
toggleTheme()
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("keydown", down)
|
||||
return () => document.removeEventListener("keydown", down)
|
||||
}, [toggleTheme])
|
||||
|
||||
return { toggleTheme }
|
||||
}
|
||||
Reference in New Issue
Block a user