Download frontend/src/lib/shape-context.tsx from turtle89431/Moxie-Multimedia: direct link, hf CLI and curl.
- Browser
- Download file 4.12 kB
-
https://huggingface.co/turtle89431/Moxie-Multimedia/resolve/main/frontend/src/lib/shape-context.tsx
- Command line
-
hf download hf://turtle89431/Moxie-Multimedia/frontend/src/lib/shape-context.tsx
-
curl -L -o shape-context.tsx https://huggingface.co/turtle89431/Moxie-Multimedia/resolve/main/frontend/src/lib/shape-context.tsx
4.12 kB
| "use client"; | |
| import { | |
| createContext, | |
| useContext, | |
| useState, | |
| useEffect, | |
| useRef, | |
| useCallback, | |
| useMemo, | |
| type ReactNode, | |
| } from "react"; | |
| type ShapeVariant = "pill" | "rounded"; | |
| const shapeOrder: ShapeVariant[] = ["rounded", "pill"]; | |
| interface ShapeClasses { | |
| item: string; | |
| bg: string; | |
| focusRing: string; | |
| mergedBg: string; | |
| container: string; | |
| button: string; | |
| input: string; | |
| // Numeric counterparts of `bg` / `mergedBg`, in px. Needed where individual | |
| // corners are animated (e.g. the selected-background merge/split animation), | |
| // which requires per-corner numeric border-radii rather than a class. | |
| bgRadius: number; | |
| mergedRadius: number; | |
| } | |
| const shapeMap: Record<ShapeVariant, ShapeClasses> = { | |
| pill: { | |
| item: "rounded-[20px]", | |
| bg: "rounded-[20px]", | |
| // +2px over `item` because the focus ring sits 2px outside the element | |
| // (top/left -2, width/height +4); this keeps the corners concentric so a | |
| // pill element gets a pill ring (matches the rounded-mode 8px→10px bump). | |
| focusRing: "rounded-[22px]", | |
| mergedBg: "rounded-2xl", | |
| container: "rounded-3xl", | |
| button: "rounded-[20px]", | |
| input: "rounded-[20px]", | |
| bgRadius: 20, | |
| mergedRadius: 16, | |
| }, | |
| rounded: { | |
| item: "rounded-lg", | |
| bg: "rounded-lg", | |
| focusRing: "rounded-[10px]", | |
| mergedBg: "rounded-lg", | |
| container: "rounded-xl", | |
| button: "rounded-lg", | |
| input: "rounded-lg", | |
| bgRadius: 8, | |
| mergedRadius: 8, | |
| }, | |
| }; | |
| interface ShapeContextValue { | |
| shape: ShapeVariant; | |
| setShape: (shape: ShapeVariant) => void; | |
| classes: ShapeClasses; | |
| } | |
| const ShapeContext = createContext<ShapeContextValue | null>(null); | |
| function useShape(): ShapeClasses { | |
| const ctx = useContext(ShapeContext); | |
| if (!ctx) return shapeMap.pill; | |
| return ctx.classes; | |
| } | |
| function useShapeContext() { | |
| const ctx = useContext(ShapeContext); | |
| if (!ctx) throw new Error("useShapeContext must be used within a ShapeProvider"); | |
| return ctx; | |
| } | |
| function ShapeProvider({ | |
| children, | |
| defaultShape = "pill", | |
| }: { | |
| children: ReactNode; | |
| defaultShape?: ShapeVariant; | |
| }) { | |
| const [shape, setShapeState] = useState<ShapeVariant>(defaultShape); | |
| const transitionTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null); | |
| // Run a state change under the `.transitioning` guard (added + reflow-flushed | |
| // first so the 180ms border-radius cross-fade applies). Clearing the previous | |
| // timeout first keeps a double-press from removing the class mid-fade. | |
| const transitionShape = useCallback((callback: () => void) => { | |
| const root = document.documentElement; | |
| root.classList.add("transitioning"); | |
| void root.offsetHeight; | |
| callback(); | |
| if (transitionTimeoutRef.current) clearTimeout(transitionTimeoutRef.current); | |
| transitionTimeoutRef.current = setTimeout( | |
| () => root.classList.remove("transitioning"), | |
| 200 | |
| ); | |
| }, []); | |
| const setShape = useCallback( | |
| (next: ShapeVariant) => { | |
| transitionShape(() => setShapeState(next)); | |
| }, | |
| [transitionShape] | |
| ); | |
| // Global keyboard shortcut: R to cycle radius | |
| useEffect(() => { | |
| const onKeyDown = (e: KeyboardEvent) => { | |
| if (e.key !== "r" && e.key !== "R") return; | |
| if (e.metaKey || e.ctrlKey || e.altKey) return; | |
| const tag = (e.target as HTMLElement)?.tagName; | |
| if (tag === "INPUT" || tag === "TEXTAREA" || (e.target as HTMLElement)?.isContentEditable) return; | |
| e.preventDefault(); | |
| transitionShape(() => { | |
| setShapeState((prev) => { | |
| const idx = shapeOrder.indexOf(prev); | |
| return shapeOrder[(idx + 1) % shapeOrder.length]; | |
| }); | |
| }); | |
| }; | |
| document.addEventListener("keydown", onKeyDown); | |
| return () => document.removeEventListener("keydown", onKeyDown); | |
| }, [transitionShape]); | |
| const value = useMemo( | |
| () => ({ shape, setShape, classes: shapeMap[shape] }), | |
| [shape, setShape] | |
| ); | |
| return ( | |
| <ShapeContext.Provider value={value}> | |
| {children} | |
| </ShapeContext.Provider> | |
| ); | |
| } | |
| export { ShapeProvider, useShape, useShapeContext, shapeMap }; | |
| export type { ShapeVariant, ShapeClasses }; | |