diff --git a/packages/app/src/components/status-popover.tsx b/packages/app/src/components/status-popover.tsx index 7048808c8..61facb84e 100644 --- a/packages/app/src/components/status-popover.tsx +++ b/packages/app/src/components/status-popover.tsx @@ -86,15 +86,17 @@ const useServerHealth = (servers: Accessor) => { const useDefaultServerKey = ( get: (() => string | Promise | null | undefined) | undefined, ) => { - const [url, setUrl] = createSignal() - const [tick, setTick] = createSignal(0) + const [state, setState] = createStore({ + url: undefined as string | undefined, + tick: 0, + }) createEffect(() => { - tick() + state.tick let dead = false const result = get?.() if (!result) { - setUrl(undefined) + setState("url", undefined) onCleanup(() => { dead = true }) @@ -104,7 +106,7 @@ const useDefaultServerKey = ( if (result instanceof Promise) { void result.then((next) => { if (dead) return - setUrl(next ? normalizeServerUrl(next) : undefined) + setState("url", next ? normalizeServerUrl(next) : undefined) }) onCleanup(() => { dead = true @@ -112,7 +114,7 @@ const useDefaultServerKey = ( return } - setUrl(normalizeServerUrl(result)) + setState("url", normalizeServerUrl(result)) onCleanup(() => { dead = true }) @@ -120,11 +122,11 @@ const useDefaultServerKey = ( return { key: () => { - const u = url() + const u = state.url if (!u) return return ServerConnection.key({ type: "http", http: { url: u } }) }, - refresh: () => setTick((value) => value + 1), + refresh: () => setState("tick", (value) => value + 1), } } diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 9b964b4b5..c1088622a 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -1,16 +1,4 @@ -import { - batch, - createEffect, - createMemo, - createSignal, - For, - on, - onCleanup, - onMount, - ParentProps, - Show, - untrack, -} from "solid-js" +import { batch, createEffect, createMemo, For, on, onCleanup, onMount, ParentProps, Show, untrack } from "solid-js" import { useNavigate, useParams } from "@solidjs/router" import { useLayout, LocalProject } from "@/context/layout" import { useGlobalSync } from "@/context/global-sync" @@ -145,6 +133,10 @@ export default function Layout(props: ParentProps) { hoverProject: undefined as string | undefined, scrollSessionKey: undefined as string | undefined, nav: undefined as HTMLElement | undefined, + sortNow: Date.now(), + sizing: false, + peek: undefined as LocalProject | undefined, + peeked: false, }) const editor = createInlineEditorController() @@ -163,14 +155,13 @@ export default function Layout(props: ParentProps) { } const isBusy = (directory: string) => !!state.busyWorkspaces[workspaceKey(directory)] const navLeave = { current: undefined as number | undefined } - const [sortNow, setSortNow] = createSignal(Date.now()) - const [sizing, setSizing] = createSignal(false) + const sortNow = () => state.sortNow let sizet: number | undefined let sortNowInterval: ReturnType | undefined const sortNowTimeout = setTimeout( () => { - setSortNow(Date.now()) - sortNowInterval = setInterval(() => setSortNow(Date.now()), 60_000) + setState("sortNow", Date.now()) + sortNowInterval = setInterval(() => setState("sortNow", Date.now()), 60_000) }, 60_000 - (Date.now() % 60_000), ) @@ -196,7 +187,7 @@ export default function Layout(props: ParentProps) { }) onMount(() => { - const stop = () => setSizing(false) + const stop = () => setState("sizing", false) window.addEventListener("pointerup", stop) window.addEventListener("pointercancel", stop) window.addEventListener("blur", stop) @@ -234,8 +225,6 @@ export default function Layout(props: ParentProps) { }, 300) } - const [peek, setPeek] = createSignal(undefined) - const [peeked, setPeeked] = createSignal(false) let peekt: number | undefined const hoverProjectData = createMemo(() => { @@ -251,17 +240,17 @@ export default function Layout(props: ParentProps) { clearTimeout(peekt) peekt = undefined } - setPeek(p) - setPeeked(true) + setState("peek", p) + setState("peeked", true) return } - setPeeked(false) - if (peek() === undefined) return + setState("peeked", false) + if (state.peek === undefined) return if (peekt !== undefined) clearTimeout(peekt) peekt = window.setTimeout(() => { peekt = undefined - setPeek(undefined) + setState("peek", undefined) }, 180) }) @@ -2245,7 +2234,7 @@ export default function Layout(props: ParentProps) { >
{sidebarContent()}
-
setSizing(true)}> +
setState("sizing", true)}> { - setSizing(true) + setState("sizing", true) if (sizet !== undefined) clearTimeout(sizet) - sizet = window.setTimeout(() => setSizing(false), 120) + sizet = window.setTimeout(() => setState("sizing", false), 120) layout.sidebar.resize(w) }} onCollapse={layout.sidebar.close} @@ -2300,7 +2289,7 @@ export default function Layout(props: ParentProps) { "xl:inset-y-0 xl:right-0 xl:left-[var(--main-left)]": true, "z-20": true, "transition-[left] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[left] motion-reduce:transition-none": - !sizing(), + !state.sizing, }} style={{ "--main-left": layout.sidebar.opened() ? `${Math.max(layout.sidebar.width(), 244)}px` : "4rem", @@ -2320,11 +2309,11 @@ export default function Layout(props: ParentProps) {
{ @@ -2336,19 +2325,19 @@ export default function Layout(props: ParentProps) { arm() }} > - - + +
diff --git a/packages/ui/src/components/animated-number.tsx b/packages/ui/src/components/animated-number.tsx index b5fceba25..28edd7b70 100644 --- a/packages/ui/src/components/animated-number.tsx +++ b/packages/ui/src/components/animated-number.tsx @@ -1,4 +1,5 @@ -import { For, Index, createEffect, createMemo, createSignal, on } from "solid-js" +import { For, Index, createEffect, createMemo, on } from "solid-js" +import { createStore } from "solid-js/store" const TRACK = Array.from({ length: 30 }, (_, index) => index % 10) const DURATION = 600 @@ -14,8 +15,12 @@ function spin(from: number, to: number, direction: 1 | -1) { } function Digit(props: { value: number; direction: 1 | -1 }) { - const [step, setStep] = createSignal(props.value + 10) - const [animating, setAnimating] = createSignal(false) + const [state, setState] = createStore({ + step: props.value + 10, + animating: false, + }) + const step = () => state.step + const animating = () => state.animating let last = props.value createEffect( @@ -25,13 +30,13 @@ function Digit(props: { value: number; direction: 1 | -1 }) { const delta = spin(last, next, props.direction) last = next if (!delta) { - setAnimating(false) - setStep(next + 10) + setState("animating", false) + setState("step", next + 10) return } - setAnimating(true) - setStep((value) => value + delta) + setState("animating", true) + setState("step", (value) => value + delta) }, { defer: true }, ), @@ -43,8 +48,8 @@ function Digit(props: { value: number; direction: 1 | -1 }) { data-slot="animated-number-strip" data-animating={animating() ? "true" : "false"} onTransitionEnd={() => { - setAnimating(false) - setStep((value) => normalize(value) + 10) + setState("animating", false) + setState("step", (value) => normalize(value) + 10) }} style={{ "--animated-number-offset": `${step()}`, @@ -63,8 +68,12 @@ export function AnimatedNumber(props: { value: number; class?: string }) { return Math.max(0, Math.round(props.value)) }) - const [value, setValue] = createSignal(target()) - const [direction, setDirection] = createSignal<1 | -1>(1) + const [state, setState] = createStore({ + value: target(), + direction: 1 as 1 | -1, + }) + const value = () => state.value + const direction = () => state.direction createEffect( on( @@ -73,8 +82,8 @@ export function AnimatedNumber(props: { value: number; class?: string }) { const current = value() if (next === current) return - setDirection(next > current ? 1 : -1) - setValue(next) + setState("direction", next > current ? 1 : -1) + setState("value", next) }, { defer: true }, ), diff --git a/packages/ui/src/components/basic-tool.tsx b/packages/ui/src/components/basic-tool.tsx index 4ad91824d..3f009f4e0 100644 --- a/packages/ui/src/components/basic-tool.tsx +++ b/packages/ui/src/components/basic-tool.tsx @@ -1,5 +1,6 @@ -import { createEffect, createSignal, For, Match, on, onCleanup, Show, Switch, type JSX } from "solid-js" +import { createEffect, For, Match, on, onCleanup, Show, Switch, type JSX } from "solid-js" import { animate, type AnimationPlaybackControls } from "motion" +import { createStore } from "solid-js/store" import { Collapsible } from "./collapsible" import type { IconProps } from "./icon" import { TextShimmer } from "./text-shimmer" @@ -37,8 +38,12 @@ export interface BasicToolProps { const SPRING = { type: "spring" as const, visualDuration: 0.35, bounce: 0 } export function BasicTool(props: BasicToolProps) { - const [open, setOpen] = createSignal(props.defaultOpen ?? false) - const [ready, setReady] = createSignal(open()) + const [state, setState] = createStore({ + open: props.defaultOpen ?? false, + ready: props.defaultOpen ?? false, + }) + const open = () => state.open + const ready = () => state.ready const pending = () => props.status === "pending" || props.status === "running" let frame: number | undefined @@ -52,7 +57,7 @@ export function BasicTool(props: BasicToolProps) { onCleanup(cancel) createEffect(() => { - if (props.forceOpen) setOpen(true) + if (props.forceOpen) setState("open", true) }) createEffect( @@ -62,7 +67,7 @@ export function BasicTool(props: BasicToolProps) { if (!props.defer) return if (!value) { cancel() - setReady(false) + setState("ready", false) return } @@ -70,7 +75,7 @@ export function BasicTool(props: BasicToolProps) { frame = requestAnimationFrame(() => { frame = undefined if (!open()) return - setReady(true) + setState("ready", true) }) }, { defer: true }, @@ -112,7 +117,7 @@ export function BasicTool(props: BasicToolProps) { const handleOpenChange = (value: boolean) => { if (pending()) return if (props.locked && !value) return - setOpen(value) + setState("open", value) } return ( diff --git a/packages/ui/src/components/line-comment-annotations.tsx b/packages/ui/src/components/line-comment-annotations.tsx index 6b072d9c5..3505487eb 100644 --- a/packages/ui/src/components/line-comment-annotations.tsx +++ b/packages/ui/src/components/line-comment-annotations.tsx @@ -1,5 +1,6 @@ import { type DiffLineAnnotation, type SelectedLineRange } from "@pierre/diffs" import { createEffect, createMemo, createSignal, onCleanup, Show, type Accessor, type JSX } from "solid-js" +import { createStore } from "solid-js/store" import { render as renderSolid } from "solid-js/web" import { createHoverCommentUtility } from "../pierre/comment-hover" import { cloneSelectedLineRange, formatSelectedLineLabel, lineInSelectedRange } from "../pierre/selection-bridge" @@ -200,8 +201,14 @@ export function createLineCommentAnnotationRenderer(props: { } export function createLineCommentState(props: LineCommentStateProps) { - const [draft, setDraft] = createSignal("") - const [editing, setEditing] = createSignal(null) + const [state, setState] = createStore({ + draft: "", + editing: null as T | null, + }) + const draft = () => state.draft + const setDraft = (value: string) => setState("draft", value) + const editing = () => state.editing + const setEditing = (value: T | null) => setState("editing", typeof value === "function" ? () => value : value) const toRange = (range: SelectedLineRange | null) => (range ? cloneSelectedLineRange(range) : null) const setSelected = (range: SelectedLineRange | null) => { @@ -261,7 +268,7 @@ export function createLineCommentState(props: LineCommentStateProps) { closeComment() setSelected(range) props.setCommenting(null) - setEditing(() => id) + setEditing(id) setDraft(value) } diff --git a/packages/ui/src/components/list.tsx b/packages/ui/src/components/list.tsx index aa2347037..8ce45bc5c 100644 --- a/packages/ui/src/components/list.tsx +++ b/packages/ui/src/components/list.tsx @@ -1,5 +1,5 @@ import { type FilteredListProps, useFilteredList } from "@opencode-ai/ui/hooks" -import { createEffect, createSignal, For, onCleanup, type JSX, on, Show } from "solid-js" +import { createEffect, For, onCleanup, type JSX, on, Show } from "solid-js" import { createStore } from "solid-js/store" import { useI18n } from "../context/i18n" import { Icon, type IconProps } from "./icon" @@ -56,12 +56,16 @@ export interface ListRef { export function List(props: ListProps & { ref?: (ref: ListRef) => void }) { const i18n = useI18n() - const [scrollRef, setScrollRef] = createSignal(undefined) - const [internalFilter, setInternalFilter] = createSignal("") let inputRef: HTMLInputElement | HTMLTextAreaElement | undefined const [store, setStore] = createStore({ mouseActive: false, + scrollRef: undefined as HTMLDivElement | undefined, + internalFilter: "", }) + const scrollRef = () => store.scrollRef + const setScrollRef = (el: HTMLDivElement | undefined) => setStore("scrollRef", el) + const internalFilter = () => store.internalFilter + const setInternalFilter = (value: string) => setStore("internalFilter", value) const scrollIntoView = (container: HTMLDivElement, node: HTMLElement, block: "center" | "nearest") => { const containerRect = container.getBoundingClientRect() @@ -208,18 +212,20 @@ export function List(props: ListProps & { ref?: (ref: ListRef) => void }) } function GroupHeader(groupProps: { group: { category: string; items: T[] } }): JSX.Element { - const [stuck, setStuck] = createSignal(false) - const [header, setHeader] = createSignal(undefined) + const [state, setState] = createStore({ + stuck: false, + header: undefined as HTMLDivElement | undefined, + }) createEffect(() => { const scroll = scrollRef() - const node = header() + const node = state.header if (!scroll || !node) return const handler = () => { const rect = node.getBoundingClientRect() const scrollRect = scroll.getBoundingClientRect() - setStuck(rect.top <= scrollRect.top + 1 && scroll.scrollTop > 0) + setState("stuck", rect.top <= scrollRect.top + 1 && scroll.scrollTop > 0) } scroll.addEventListener("scroll", handler, { passive: true }) @@ -228,7 +234,7 @@ export function List(props: ListProps & { ref?: (ref: ListRef) => void }) }) return ( -
+
setState("header", el)}> {props.groupHeader?.(groupProps.group) ?? groupProps.group.category}
) diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index ee83ffa15..a89d97272 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -12,6 +12,7 @@ import { Index, type JSX, } from "solid-js" +import { createStore } from "solid-js/store" import stripAnsi from "strip-ansi" import { Dynamic } from "solid-js/web" import { @@ -885,8 +886,12 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp const data = useData() const dialog = useDialog() const i18n = useI18n() - const [copied, setCopied] = createSignal(false) - const [busy, setBusy] = createSignal<"fork" | "revert" | undefined>() + const [state, setState] = createStore({ + copied: false, + busy: undefined as "fork" | "revert" | undefined, + }) + const copied = () => state.copied + const busy = () => state.busy const textPart = createMemo( () => props.parts?.find((p) => p.type === "text" && !(p as TextPart).synthetic) as TextPart | undefined, @@ -946,14 +951,14 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp const content = text() if (!content) return await navigator.clipboard.writeText(content) - setCopied(true) - setTimeout(() => setCopied(false), 2000) + setState("copied", true) + setTimeout(() => setState("copied", false), 2000) } const run = (kind: "fork" | "revert") => { const act = kind === "fork" ? props.actions?.fork : props.actions?.revert if (!act || busy()) return - setBusy(kind) + setState("busy", kind) void Promise.resolve() .then(() => act({ @@ -962,7 +967,7 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp }), ) .finally(() => { - if (busy() === kind) setBusy(undefined) + if (busy() === kind) setState("busy", undefined) }) } diff --git a/packages/ui/src/components/popover.tsx b/packages/ui/src/components/popover.tsx index fe09a98a8..9d3da4109 100644 --- a/packages/ui/src/components/popover.tsx +++ b/packages/ui/src/components/popover.tsx @@ -5,11 +5,11 @@ import { ParentProps, Show, createEffect, - createSignal, onCleanup, splitProps, ValidComponent, } from "solid-js" +import { createStore } from "solid-js/store" import { useI18n } from "../context/i18n" import { IconButton } from "./icon-button" @@ -46,23 +46,24 @@ export function Popover(props: PopoverProps "modal", ]) - const [contentRef, setContentRef] = createSignal(undefined) - const [triggerRef, setTriggerRef] = createSignal(undefined) - const [dismiss, setDismiss] = createSignal<"escape" | "outside" | null>(null) - - const [uncontrolledOpen, setUncontrolledOpen] = createSignal(local.defaultOpen ?? false) + const [state, setState] = createStore({ + contentRef: undefined as HTMLElement | undefined, + triggerRef: undefined as HTMLElement | undefined, + dismiss: null as "escape" | "outside" | null, + uncontrolledOpen: local.defaultOpen ?? false, + }) const controlled = () => local.open !== undefined const opened = () => { if (controlled()) return local.open ?? false - return uncontrolledOpen() + return state.uncontrolledOpen } const onOpenChange = (next: boolean) => { - if (next) setDismiss(null) + if (next) setState("dismiss", null) if (local.onOpenChange) local.onOpenChange(next) if (controlled()) return - setUncontrolledOpen(next) + setState("uncontrolledOpen", next) } createEffect(() => { @@ -70,15 +71,15 @@ export function Popover(props: PopoverProps const inside = (node: Node | null | undefined) => { if (!node) return false - const content = contentRef() + const content = state.contentRef if (content && content.contains(node)) return true - const trigger = triggerRef() + const trigger = state.triggerRef if (trigger && trigger.contains(node)) return true return false } const close = (reason: "escape" | "outside") => { - setDismiss(reason) + setState("dismiss", reason) onOpenChange(false) } @@ -116,7 +117,7 @@ export function Popover(props: PopoverProps const content = () => ( setContentRef(el)} + ref={(el: HTMLElement | undefined) => setState("contentRef", el)} data-component="popover-content" classList={{ ...(local.classList ?? {}), @@ -124,8 +125,8 @@ export function Popover(props: PopoverProps }} style={local.style} onCloseAutoFocus={(event: Event) => { - if (dismiss() === "outside") event.preventDefault() - setDismiss(null) + if (state.dismiss === "outside") event.preventDefault() + setState("dismiss", null) }} > {/* */} @@ -151,7 +152,7 @@ export function Popover(props: PopoverProps return ( setTriggerRef(el)} + ref={(el: HTMLElement) => setState("triggerRef", el)} as={local.triggerAs ?? "div"} data-slot="popover-trigger" {...(local.triggerProps as any)} diff --git a/packages/ui/src/components/resize-handle.stories.tsx b/packages/ui/src/components/resize-handle.stories.tsx index 474cf71e2..c0151ee70 100644 --- a/packages/ui/src/components/resize-handle.stories.tsx +++ b/packages/ui/src/components/resize-handle.stories.tsx @@ -1,5 +1,6 @@ // @ts-nocheck import { createSignal } from "solid-js" +import { createStore } from "solid-js/store" import * as mod from "./resize-handle" const docs = `### Overview @@ -94,8 +95,12 @@ export const Vertical = { export const Collapse = { render: () => { - const [size, setSize] = createSignal(200) - const [collapsed, setCollapsed] = createSignal(false) + const [state, setState] = createStore({ + size: 200, + collapsed: false, + }) + const size = () => state.size + const collapsed = () => state.collapsed return (
@@ -116,10 +121,10 @@ export const Collapse = { max={360} collapseThreshold={100} onResize={(next) => { - setCollapsed(false) - setSize(next) + setState("collapsed", false) + setState("size", next) }} - onCollapse={() => setCollapsed(true)} + onCollapse={() => setState("collapsed", true)} style="height:24px;border:1px dashed color-mix(in oklab, var(--text-base) 20%, transparent)" />
diff --git a/packages/ui/src/components/scroll-view.tsx b/packages/ui/src/components/scroll-view.tsx index c3d878af6..2b58300b9 100644 --- a/packages/ui/src/components/scroll-view.tsx +++ b/packages/ui/src/components/scroll-view.tsx @@ -1,4 +1,5 @@ -import { createSignal, onCleanup, onMount, splitProps, type ComponentProps, Show, mergeProps } from "solid-js" +import { onCleanup, onMount, splitProps, type ComponentProps, Show, mergeProps } from "solid-js" +import { createStore } from "solid-js/store" import { useI18n } from "../context/i18n" export interface ScrollViewProps extends ComponentProps<"div"> { @@ -48,23 +49,29 @@ export function ScrollView(props: ScrollViewProps) { let viewportRef!: HTMLDivElement let thumbRef!: HTMLDivElement - const [isHovered, setIsHovered] = createSignal(false) - const [isDragging, setIsDragging] = createSignal(false) - - const [thumbHeight, setThumbHeight] = createSignal(0) - const [thumbTop, setThumbTop] = createSignal(0) - const [showThumb, setShowThumb] = createSignal(false) + const [state, setState] = createStore({ + isHovered: false, + isDragging: false, + thumbHeight: 0, + thumbTop: 0, + showThumb: false, + }) + const isHovered = () => state.isHovered + const isDragging = () => state.isDragging + const thumbHeight = () => state.thumbHeight + const thumbTop = () => state.thumbTop + const showThumb = () => state.showThumb const updateThumb = () => { if (!viewportRef) return const { scrollTop, scrollHeight, clientHeight } = viewportRef if (scrollHeight <= clientHeight || scrollHeight === 0) { - setShowThumb(false) + setState("showThumb", false) return } - setShowThumb(true) + setState("showThumb", true) const trackPadding = 8 const trackHeight = clientHeight - trackPadding * 2 @@ -81,8 +88,8 @@ export function ScrollView(props: ScrollViewProps) { // Ensure thumb stays within bounds (shouldn't be necessary due to math above, but good for safety) const boundedTop = trackPadding + Math.max(0, Math.min(top, maxThumbTop)) - setThumbHeight(height) - setThumbTop(boundedTop) + setState("thumbHeight", height) + setState("thumbTop", boundedTop) } onMount(() => { @@ -113,7 +120,7 @@ export function ScrollView(props: ScrollViewProps) { const onThumbPointerDown = (e: PointerEvent) => { e.preventDefault() e.stopPropagation() - setIsDragging(true) + setState("isDragging", true) startY = e.clientY startScrollTop = viewportRef.scrollTop @@ -132,7 +139,7 @@ export function ScrollView(props: ScrollViewProps) { } const onPointerUp = (e: PointerEvent) => { - setIsDragging(false) + setState("isDragging", false) thumbRef.releasePointerCapture(e.pointerId) thumbRef.removeEventListener("pointermove", onPointerMove) thumbRef.removeEventListener("pointerup", onPointerUp) @@ -191,8 +198,8 @@ export function ScrollView(props: ScrollViewProps) { ref={rootRef} class={`scroll-view ${local.class || ""}`} style={local.style} - onPointerEnter={() => setIsHovered(true)} - onPointerLeave={() => setIsHovered(false)} + onPointerEnter={() => setState("isHovered", true)} + onPointerLeave={() => setState("isHovered", false)} {...rest} > {/* Viewport */} diff --git a/packages/ui/src/components/session-review.tsx b/packages/ui/src/components/session-review.tsx index 49c561e0b..83d2980f6 100644 --- a/packages/ui/src/components/session-review.tsx +++ b/packages/ui/src/components/session-review.tsx @@ -13,7 +13,7 @@ import { useFileComponent } from "../context/file" import { useI18n } from "../context/i18n" import { getDirectory, getFilename } from "@opencode-ai/util/path" import { checksum } from "@opencode-ai/util/encode" -import { createEffect, createMemo, createSignal, For, Match, Show, Switch, untrack, type JSX } from "solid-js" +import { createEffect, createMemo, For, Match, Show, Switch, untrack, type JSX } from "solid-js" import { onCleanup } from "solid-js" import { createStore } from "solid-js/store" import { type FileContent, type FileDiff } from "@opencode-ai/sdk/v2" @@ -138,14 +138,16 @@ export const SessionReview = (props: SessionReviewProps) => { const i18n = useI18n() const fileComponent = useFileComponent() const anchors = new Map() - const [store, setStore] = createStore<{ open: string[]; force: Record }>({ - open: [], - force: {}, + const [store, setStore] = createStore({ + open: [] as string[], + force: {} as Record, + selection: null as SessionReviewSelection | null, + commenting: null as SessionReviewSelection | null, + opened: null as SessionReviewFocus | null, }) - - const [selection, setSelection] = createSignal(null) - const [commenting, setCommenting] = createSignal(null) - const [opened, setOpened] = createSignal(null) + const selection = () => store.selection + const commenting = () => store.commenting + const opened = () => store.opened const open = () => props.open ?? store.open const files = createMemo(() => props.diffs.map((diff) => diff.file)) @@ -184,10 +186,10 @@ export const SessionReview = (props: SessionReviewProps) => { focusToken++ const token = focusToken - setOpened(focus) + setStore("opened", focus) const comment = (props.comments ?? []).find((c) => c.file === focus.file && c.id === focus.id) - if (comment) setSelection({ file: comment.file, range: cloneSelectedLineRange(comment.selection) }) + if (comment) setStore("selection", { file: comment.file, range: cloneSelectedLineRange(comment.selection) }) const current = open() if (!current.includes(focus.file)) { @@ -331,11 +333,11 @@ export const SessionReview = (props: SessionReviewProps) => { if (!current || current.file !== file) return null return current.id }, - setOpened: (id) => setOpened(id ? { file, id } : null), + setOpened: (id) => setStore("opened", id ? { file, id } : null), selected: selectedLines, - setSelected: (range) => setSelection(range ? { file, range } : null), + setSelected: (range) => setStore("selection", range ? { file, range } : null), commenting: draftRange, - setCommenting: (range) => setCommenting(range ? { file, range } : null), + setCommenting: (range) => setStore("commenting", range ? { file, range } : null), }, getSide: selectionSide, clearSelectionOnSelectionEndNull: false, diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx index 41e160d16..8c9c1ffe4 100644 --- a/packages/ui/src/components/session-turn.tsx +++ b/packages/ui/src/components/session-turn.tsx @@ -6,6 +6,7 @@ import { useFileComponent } from "../context/file" import { Binary } from "@opencode-ai/util/binary" import { getDirectory, getFilename } from "@opencode-ai/util/path" import { createEffect, createMemo, createSignal, For, on, ParentProps, Show } from "solid-js" +import { createStore } from "solid-js/store" import { Dynamic } from "solid-js/web" import { AssistantParts, Message, MessageDivider, PART_MAPPING, type UserActions } from "./message-part" import { Card } from "./card" @@ -240,14 +241,18 @@ export function SessionTurn( .reverse() }) const edited = createMemo(() => diffs().length) - const [open, setOpen] = createSignal(false) - const [expanded, setExpanded] = createSignal([]) + const [state, setState] = createStore({ + open: false, + expanded: [] as string[], + }) + const open = () => state.open + const expanded = () => state.expanded createEffect( on( open, (value, prev) => { - if (!value && prev) setExpanded([]) + if (!value && prev) setState("expanded", []) }, { defer: true }, ), @@ -425,7 +430,7 @@ export function SessionTurn( 0 && !working()}>
- + setState("open", value)} variant="ghost">
@@ -447,7 +452,9 @@ export function SessionTurn( multiple style={{ "--sticky-accordion-offset": "40px" }} value={expanded()} - onChange={(value) => setExpanded(Array.isArray(value) ? value : value ? [value] : [])} + onChange={(value) => + setState("expanded", Array.isArray(value) ? value : value ? [value] : []) + } > {(diff) => { diff --git a/packages/ui/src/components/shell-submessage-motion.stories.tsx b/packages/ui/src/components/shell-submessage-motion.stories.tsx index 1f53b6e4d..1780c83ba 100644 --- a/packages/ui/src/components/shell-submessage-motion.stories.tsx +++ b/packages/ui/src/components/shell-submessage-motion.stories.tsx @@ -1,5 +1,6 @@ // @ts-nocheck -import { createEffect, createSignal, onCleanup } from "solid-js" +import { createEffect, onCleanup } from "solid-js" +import { createStore } from "solid-js/store" import { BasicTool } from "./basic-tool" import { animate } from "motion" @@ -138,29 +139,39 @@ function SpringSubmessage(props: { text: string; visible: boolean; visualDuratio export const Playground = { render: () => { - const [text, setText] = createSignal("Prints five topic blocks between timed commands") - const [show, setShow] = createSignal(true) - const [visualDuration, setVisualDuration] = createSignal(0.35) - const [bounce, setBounce] = createSignal(0) - const [fadeMs, setFadeMs] = createSignal(320) - const [blur, setBlur] = createSignal(2) - const [fadeEase, setFadeEase] = createSignal("snappy") - const [auto, setAuto] = createSignal(false) + const [state, setState] = createStore({ + text: "Prints five topic blocks between timed commands", + show: true, + visualDuration: 0.35, + bounce: 0, + fadeMs: 320, + blur: 2, + fadeEase: "snappy", + auto: false, + }) + const text = () => state.text + const show = () => state.show + const visualDuration = () => state.visualDuration + const bounce = () => state.bounce + const fadeMs = () => state.fadeMs + const blur = () => state.blur + const fadeEase = () => state.fadeEase + const auto = () => state.auto let replayTimer let autoTimer const replay = () => { - setShow(false) + setState("show", false) if (replayTimer) clearTimeout(replayTimer) replayTimer = setTimeout(() => { - setShow(true) + setState("show", true) }, 50) } const stopAuto = () => { if (autoTimer) clearInterval(autoTimer) autoTimer = undefined - setAuto(false) + setState("auto", false) } const toggleAuto = () => { @@ -168,7 +179,7 @@ export const Playground = { stopAuto() return } - setAuto(true) + setState("auto", true) autoTimer = setInterval(replay, 2200) } @@ -224,7 +235,7 @@ export const Playground = { -
@@ -278,7 +289,7 @@ export const Playground = { max={0.5} step={0.01} value={bounce()} - onInput={(e) => setBounce(Number(e.currentTarget.value))} + onInput={(e) => setState("bounce", Number(e.currentTarget.value))} /> {bounce().toFixed(2)}
@@ -287,8 +298,14 @@ export const Playground = { fade ease
@@ -318,7 +335,7 @@ export const Playground = { max={14} step={0.5} value={blur()} - onInput={(e) => setBlur(Number(e.currentTarget.value))} + onInput={(e) => setState("blur", Number(e.currentTarget.value))} /> {blur()}px
diff --git a/packages/ui/src/components/text-reveal.stories.tsx b/packages/ui/src/components/text-reveal.stories.tsx index df514ca38..af25d190c 100644 --- a/packages/ui/src/components/text-reveal.stories.tsx +++ b/packages/ui/src/components/text-reveal.stories.tsx @@ -1,5 +1,6 @@ // @ts-nocheck -import { createSignal, onCleanup } from "solid-js" +import { onCleanup } from "solid-js" +import { createStore } from "solid-js/store" import { TextReveal } from "./text-reveal" export default { @@ -87,33 +88,42 @@ const headingSlot = { export const Playground = { render: () => { - const [index, setIndex] = createSignal(0) - const [cycling, setCycling] = createSignal(false) - const [growOnly, setGrowOnly] = createSignal(true) - - const [duration, setDuration] = createSignal(600) - const [bounce, setBounce] = createSignal(1.0) - const [bounceSoft, setBounceSoft] = createSignal(1.0) - - const [hybridTravel, setHybridTravel] = createSignal(25) - const [hybridEdge, setHybridEdge] = createSignal(17) - - const [edge, setEdge] = createSignal(17) - const [revealTravel, setRevealTravel] = createSignal(0) + const [state, setState] = createStore({ + index: 0, + cycling: false, + growOnly: true, + duration: 600, + bounce: 1.0, + bounceSoft: 1.0, + hybridTravel: 25, + hybridEdge: 17, + edge: 17, + revealTravel: 0, + }) + const index = () => state.index + const cycling = () => state.cycling + const growOnly = () => state.growOnly + const duration = () => state.duration + const bounce = () => state.bounce + const bounceSoft = () => state.bounceSoft + const hybridTravel = () => state.hybridTravel + const hybridEdge = () => state.hybridEdge + const edge = () => state.edge + const revealTravel = () => state.revealTravel let timer: number | undefined const text = () => TEXTS[index()] - const next = () => setIndex((i) => (i + 1) % TEXTS.length) - const prev = () => setIndex((i) => (i - 1 + TEXTS.length) % TEXTS.length) + const next = () => setState("index", (value) => (value + 1) % TEXTS.length) + const prev = () => setState("index", (value) => (value - 1 + TEXTS.length) % TEXTS.length) const toggleCycle = () => { if (cycling()) { if (timer) clearTimeout(timer) timer = undefined - setCycling(false) + setState("cycling", false) return } - setCycling(true) + setState("cycling", true) const tick = () => { next() timer = window.setTimeout(tick, 700 + Math.floor(Math.random() * 600)) @@ -172,7 +182,7 @@ export const Playground = {
{TEXTS.map((t, i) => ( - ))} @@ -188,7 +198,7 @@ export const Playground = { -
@@ -204,7 +214,7 @@ export const Playground = { max="40" step="1" value={hybridEdge()} - onInput={(e) => setHybridEdge(e.currentTarget.valueAsNumber)} + onInput={(e) => setState("hybridEdge", e.currentTarget.valueAsNumber)} style={{ flex: 1 }} /> {hybridEdge()}% @@ -218,7 +228,7 @@ export const Playground = { max="40" step="1" value={hybridTravel()} - onInput={(e) => setHybridTravel(e.currentTarget.valueAsNumber)} + onInput={(e) => setState("hybridTravel", e.currentTarget.valueAsNumber)} style={{ flex: 1 }} /> {hybridTravel()}px @@ -234,7 +244,7 @@ export const Playground = { max="1400" step="10" value={duration()} - onInput={(e) => setDuration(e.currentTarget.valueAsNumber)} + onInput={(e) => setState("duration", e.currentTarget.valueAsNumber)} style={{ flex: 1 }} /> {duration()}ms @@ -248,7 +258,7 @@ export const Playground = { max="2" step="0.01" value={bounce()} - onInput={(e) => setBounce(e.currentTarget.valueAsNumber)} + onInput={(e) => setState("bounce", e.currentTarget.valueAsNumber)} style={{ flex: 1 }} /> {bounce().toFixed(2)} @@ -262,7 +272,7 @@ export const Playground = { max="1.5" step="0.01" value={bounceSoft()} - onInput={(e) => setBounceSoft(e.currentTarget.valueAsNumber)} + onInput={(e) => setState("bounceSoft", e.currentTarget.valueAsNumber)} style={{ flex: 1 }} /> {bounceSoft().toFixed(2)} @@ -280,7 +290,7 @@ export const Playground = { max="40" step="1" value={edge()} - onInput={(e) => setEdge(e.currentTarget.valueAsNumber)} + onInput={(e) => setState("edge", e.currentTarget.valueAsNumber)} style={{ flex: 1 }} /> {edge()}% @@ -294,7 +304,7 @@ export const Playground = { max="16" step="1" value={revealTravel()} - onInput={(e) => setRevealTravel(e.currentTarget.valueAsNumber)} + onInput={(e) => setState("revealTravel", e.currentTarget.valueAsNumber)} style={{ flex: 1 }} /> {revealTravel()}px diff --git a/packages/ui/src/components/text-reveal.tsx b/packages/ui/src/components/text-reveal.tsx index c4fe1302f..02bf8084c 100644 --- a/packages/ui/src/components/text-reveal.tsx +++ b/packages/ui/src/components/text-reveal.tsx @@ -1,4 +1,5 @@ -import { createEffect, createSignal, on, onCleanup, onMount } from "solid-js" +import { createEffect, on, onCleanup, onMount } from "solid-js" +import { createStore } from "solid-js/store" const px = (value: number | string | undefined, fallback: number) => { if (typeof value === "number") return `${value}px` @@ -30,11 +31,18 @@ export function TextReveal(props: { growOnly?: boolean truncate?: boolean }) { - const [cur, setCur] = createSignal(props.text) - const [old, setOld] = createSignal() - const [width, setWidth] = createSignal("auto") - const [ready, setReady] = createSignal(false) - const [swapping, setSwapping] = createSignal(false) + const [state, setState] = createStore({ + cur: props.text, + old: undefined as string | undefined, + width: "auto", + ready: false, + swapping: false, + }) + const cur = () => state.cur + const old = () => state.old + const width = () => state.width + const ready = () => state.ready + const swapping = () => state.swapping let inRef: HTMLSpanElement | undefined let outRef: HTMLSpanElement | undefined let rootRef: HTMLSpanElement | undefined @@ -49,7 +57,7 @@ export function TextReveal(props: { const prev = Number.parseFloat(width()) if (Number.isFinite(prev) && next <= prev) return } - setWidth(`${next}px`) + setState("width", `${next}px`) } createEffect( @@ -58,25 +66,25 @@ export function TextReveal(props: { (next, prev) => { if (next === prev) return if (typeof next === "string" && typeof prev === "string" && next.startsWith(prev)) { - setCur(next) + setState("cur", next) widen(win()) return } - setSwapping(true) - setOld(prev) - setCur(next) + setState("swapping", true) + setState("old", prev) + setState("cur", next) if (typeof requestAnimationFrame !== "function") { widen(Math.max(win(), wout())) rootRef?.offsetHeight - setSwapping(false) + setState("swapping", false) return } if (frame !== undefined && typeof cancelAnimationFrame === "function") cancelAnimationFrame(frame) frame = requestAnimationFrame(() => { widen(Math.max(win(), wout())) rootRef?.offsetHeight - setSwapping(false) + setState("swapping", false) frame = undefined }) }, @@ -87,16 +95,16 @@ export function TextReveal(props: { widen(win()) const fonts = typeof document !== "undefined" ? document.fonts : undefined if (typeof requestAnimationFrame !== "function") { - setReady(true) + setState("ready", true) return } if (!fonts) { - requestAnimationFrame(() => setReady(true)) + requestAnimationFrame(() => setState("ready", true)) return } fonts.ready.finally(() => { widen(win()) - requestAnimationFrame(() => setReady(true)) + requestAnimationFrame(() => setState("ready", true)) }) }) diff --git a/packages/ui/src/components/text-strikethrough.stories.tsx b/packages/ui/src/components/text-strikethrough.stories.tsx index b07e74553..5e86413f9 100644 --- a/packages/ui/src/components/text-strikethrough.stories.tsx +++ b/packages/ui/src/components/text-strikethrough.stories.tsx @@ -1,5 +1,6 @@ // @ts-nocheck import { createEffect, createSignal, onCleanup, onMount } from "solid-js" +import { createStore } from "solid-js/store" import { useSpring } from "./motion-spring" import { TextStrikethrough } from "./text-strikethrough" @@ -130,12 +131,16 @@ function VariantF(props: { active: boolean; text: string }) { ) let baseRef: HTMLSpanElement | undefined let containerRef: HTMLSpanElement | undefined - const [textWidth, setTextWidth] = createSignal(0) - const [containerWidth, setContainerWidth] = createSignal(0) + const [state, setState] = createStore({ + textWidth: 0, + containerWidth: 0, + }) + const textWidth = () => state.textWidth + const containerWidth = () => state.containerWidth const measure = () => { - if (baseRef) setTextWidth(baseRef.scrollWidth) - if (containerRef) setContainerWidth(containerRef.offsetWidth) + if (baseRef) setState("textWidth", baseRef.scrollWidth) + if (containerRef) setState("containerWidth", containerRef.offsetWidth) } onMount(measure) diff --git a/packages/ui/src/components/text-strikethrough.tsx b/packages/ui/src/components/text-strikethrough.tsx index 211e7d44c..aee5e0cbd 100644 --- a/packages/ui/src/components/text-strikethrough.tsx +++ b/packages/ui/src/components/text-strikethrough.tsx @@ -1,5 +1,6 @@ import type { JSX } from "solid-js" -import { createEffect, createSignal, onCleanup, onMount } from "solid-js" +import { createEffect, onCleanup, onMount } from "solid-js" +import { createStore } from "solid-js/store" import { useSpring } from "./motion-spring" export function TextStrikethrough(props: { @@ -19,12 +20,16 @@ export function TextStrikethrough(props: { let baseRef: HTMLSpanElement | undefined let containerRef: HTMLSpanElement | undefined - const [textWidth, setTextWidth] = createSignal(0) - const [containerWidth, setContainerWidth] = createSignal(0) + const [state, setState] = createStore({ + textWidth: 0, + containerWidth: 0, + }) + const textWidth = () => state.textWidth + const containerWidth = () => state.containerWidth const measure = () => { - if (baseRef) setTextWidth(baseRef.scrollWidth) - if (containerRef) setContainerWidth(containerRef.offsetWidth) + if (baseRef) setState("textWidth", baseRef.scrollWidth) + if (containerRef) setState("containerWidth", containerRef.offsetWidth) } onMount(measure) diff --git a/packages/ui/src/components/thinking-heading.stories.tsx b/packages/ui/src/components/thinking-heading.stories.tsx index 90eb7ee31..3a65619ce 100644 --- a/packages/ui/src/components/thinking-heading.stories.tsx +++ b/packages/ui/src/components/thinking-heading.stories.tsx @@ -1,5 +1,6 @@ // @ts-nocheck -import { createSignal, createEffect, on, onMount, onCleanup } from "solid-js" +import { createEffect, on, onMount, onCleanup } from "solid-js" +import { createStore } from "solid-js/store" import { TextShimmer } from "./text-shimmer" import { TextReveal } from "./text-reveal" @@ -375,11 +376,18 @@ input[type="range"].heading-slider::-webkit-slider-thumb { // --------------------------------------------------------------------------- function AnimatedHeading(props) { - const [current, setCurrent] = createSignal(props.text) - const [leaving, setLeaving] = createSignal(undefined) - const [width, setWidth] = createSignal("auto") - const [ready, setReady] = createSignal(false) - const [swapping, setSwapping] = createSignal(false) + const [state, setState] = createStore({ + current: props.text, + leaving: undefined, + width: "auto", + ready: false, + swapping: false, + }) + const current = () => state.current + const leaving = () => state.leaving + const width = () => state.width + const ready = () => state.ready + const swapping = () => state.swapping let enterRef let leaveRef let containerRef @@ -391,16 +399,16 @@ function AnimatedHeading(props) { if (px <= 0) return const w = Number.parseFloat(width()) if (Number.isFinite(w) && px <= w) return - setWidth(`${px}px`) + setState("width", `${px}px`) } const measure = () => { if (!current()) { - setWidth("0px") + setState("width", "0px") return } const px = measureEnter() - if (px > 0) setWidth(`${px}px`) + if (px > 0) setState("width", `${px}px`) } createEffect( @@ -408,9 +416,9 @@ function AnimatedHeading(props) { () => props.text, (next, prev) => { if (next === prev) return - setSwapping(true) - setLeaving(prev) - setCurrent(next) + setState("swapping", true) + setState("leaving", prev) + setState("current", next) if (frame) cancelAnimationFrame(frame) frame = requestAnimationFrame(() => { @@ -420,10 +428,10 @@ function AnimatedHeading(props) { const leaveW = measureLeave() widen(Math.max(enterW, leaveW)) containerRef?.offsetHeight // reflow with max width + swap positions - setSwapping(false) + setState("swapping", false) } else { containerRef?.offsetHeight - setSwapping(false) + setState("swapping", false) measure() } frame = undefined @@ -436,7 +444,7 @@ function AnimatedHeading(props) { measure() document.fonts?.ready.finally(() => { measure() - requestAnimationFrame(() => setReady(true)) + requestAnimationFrame(() => setState("ready", true)) }) }) @@ -552,47 +560,56 @@ const VARIANTS: { key: string; label: string }[] = [] export const Playground = { render: () => { - const [heading, setHeading] = createSignal(HEADINGS[0]) - const [headingIndex, setHeadingIndex] = createSignal(0) - const [active, setActive] = createSignal(true) - const [cycling, setCycling] = createSignal(false) + const [state, setState] = createStore({ + heading: HEADINGS[0], + headingIndex: 0, + active: true, + cycling: false, + duration: 550, + blur: 2, + travel: 4, + bounce: 1.35, + maskSize: 12, + maskPad: 9, + maskHeight: 0, + debug: false, + odoBlur: false, + }) + const heading = () => state.heading + const headingIndex = () => state.headingIndex + const active = () => state.active + const cycling = () => state.cycling + const duration = () => state.duration + const blur = () => state.blur + const travel = () => state.travel + const bounce = () => state.bounce + const maskSize = () => state.maskSize + const maskPad = () => state.maskPad + const maskHeight = () => state.maskHeight + const debug = () => state.debug + const odoBlur = () => state.odoBlur let cycleTimer - // tunable params - const [duration, setDuration] = createSignal(550) - const [blur, setBlur] = createSignal(2) - const [travel, setTravel] = createSignal(4) - const [bounce, setBounce] = createSignal(1.35) - const [maskSize, setMaskSize] = createSignal(12) - const [maskPad, setMaskPad] = createSignal(9) - const [maskHeight, setMaskHeight] = createSignal(0) - const [debug, setDebug] = createSignal(false) - const [odoBlur, setOdoBlur] = createSignal(false) - const nextHeading = () => { - setHeadingIndex((i) => { - const next = (i + 1) % HEADINGS.length - setHeading(HEADINGS[next]) - return next - }) + const next = (headingIndex() + 1) % HEADINGS.length + setState("headingIndex", next) + setState("heading", HEADINGS[next]) } const prevHeading = () => { - setHeadingIndex((i) => { - const prev = (i - 1 + HEADINGS.length) % HEADINGS.length - setHeading(HEADINGS[prev]) - return prev - }) + const prev = (headingIndex() - 1 + HEADINGS.length) % HEADINGS.length + setState("headingIndex", prev) + setState("heading", HEADINGS[prev]) } const toggleCycling = () => { if (cycling()) { clearTimeout(cycleTimer) cycleTimer = undefined - setCycling(false) + setState("cycling", false) return } - setCycling(true) + setState("cycling", true) const tick = () => { if (!cycling()) return nextHeading() @@ -602,11 +619,11 @@ export const Playground = { } const clearHeading = () => { - setHeading(undefined) + setState("heading", undefined) if (cycling()) { clearTimeout(cycleTimer) cycleTimer = undefined - setCycling(false) + setState("cycling", false) } } @@ -686,7 +703,7 @@ export const Playground = { max={1400} step={50} value={duration()} - onInput={(e) => setDuration(Number(e.currentTarget.value))} + onInput={(e) => setState("duration", Number(e.currentTarget.value))} /> {duration()}ms
@@ -700,7 +717,7 @@ export const Playground = { max={16} step={0.5} value={blur()} - onInput={(e) => setBlur(Number(e.currentTarget.value))} + onInput={(e) => setState("blur", Number(e.currentTarget.value))} /> {blur()}px
@@ -714,7 +731,7 @@ export const Playground = { max={120} step={1} value={travel()} - onInput={(e) => setTravel(Number(e.currentTarget.value))} + onInput={(e) => setState("travel", Number(e.currentTarget.value))} /> {travel()}px
@@ -728,7 +745,7 @@ export const Playground = { max={2.2} step={0.05} value={bounce()} - onInput={(e) => setBounce(Number(e.currentTarget.value))} + onInput={(e) => setState("bounce", Number(e.currentTarget.value))} /> {bounce().toFixed(2)} {bounce() <= 1.05 ? "(none)" : bounce() >= 1.9 ? "(heavy)" : ""} @@ -744,7 +761,7 @@ export const Playground = { max={50} step={1} value={maskSize()} - onInput={(e) => setMaskSize(Number(e.currentTarget.value))} + onInput={(e) => setState("maskSize", Number(e.currentTarget.value))} /> {maskSize()}px {maskSize() === 0 ? "(hard)" : ""} @@ -760,7 +777,7 @@ export const Playground = { max={60} step={1} value={maskPad()} - onInput={(e) => setMaskPad(Number(e.currentTarget.value))} + onInput={(e) => setState("maskPad", Number(e.currentTarget.value))} /> {maskPad()}px
@@ -774,7 +791,7 @@ export const Playground = { max={80} step={1} value={maskHeight()} - onInput={(e) => setMaskHeight(Number(e.currentTarget.value))} + onInput={(e) => setState("maskHeight", Number(e.currentTarget.value))} /> {maskHeight()}px @@ -795,13 +812,13 @@ export const Playground = { - - - @@ -810,8 +827,8 @@ export const Playground = { {HEADINGS.map((h, i) => ( {[0, 1, 2, 3].map((value) => ( - ))} @@ -307,7 +328,7 @@ export const Playground = { max="1" step="0.01" value={dockOpenDuration()} - onInput={(event) => setDockOpenDuration(event.currentTarget.valueAsNumber)} + onInput={(event) => setCfg("dockOpenDuration", event.currentTarget.valueAsNumber)} style={{ flex: 1 }} /> @@ -324,7 +345,7 @@ export const Playground = { max="1" step="0.01" value={dockOpenBounce()} - onInput={(event) => setDockOpenBounce(event.currentTarget.valueAsNumber)} + onInput={(event) => setCfg("dockOpenBounce", event.currentTarget.valueAsNumber)} style={{ flex: 1 }} /> @@ -345,7 +366,7 @@ export const Playground = { max="1" step="0.01" value={dockCloseDuration()} - onInput={(event) => setDockCloseDuration(event.currentTarget.valueAsNumber)} + onInput={(event) => setCfg("dockCloseDuration", event.currentTarget.valueAsNumber)} style={{ flex: 1 }} /> @@ -362,7 +383,7 @@ export const Playground = { max="1" step="0.01" value={dockCloseBounce()} - onInput={(event) => setDockCloseBounce(event.currentTarget.valueAsNumber)} + onInput={(event) => setCfg("dockCloseBounce", event.currentTarget.valueAsNumber)} style={{ flex: 1 }} /> @@ -383,7 +404,7 @@ export const Playground = { max="1" step="0.01" value={drawerExpandDuration()} - onInput={(event) => setDrawerExpandDuration(event.currentTarget.valueAsNumber)} + onInput={(event) => setCfg("drawerExpandDuration", event.currentTarget.valueAsNumber)} style={{ flex: 1 }} /> @@ -400,7 +421,7 @@ export const Playground = { max="1" step="0.01" value={drawerExpandBounce()} - onInput={(event) => setDrawerExpandBounce(event.currentTarget.valueAsNumber)} + onInput={(event) => setCfg("drawerExpandBounce", event.currentTarget.valueAsNumber)} style={{ flex: 1 }} /> @@ -421,7 +442,7 @@ export const Playground = { max="1" step="0.01" value={drawerCollapseDuration()} - onInput={(event) => setDrawerCollapseDuration(event.currentTarget.valueAsNumber)} + onInput={(event) => setCfg("drawerCollapseDuration", event.currentTarget.valueAsNumber)} style={{ flex: 1 }} /> @@ -438,7 +459,7 @@ export const Playground = { max="1" step="0.01" value={drawerCollapseBounce()} - onInput={(event) => setDrawerCollapseBounce(event.currentTarget.valueAsNumber)} + onInput={(event) => setCfg("drawerCollapseBounce", event.currentTarget.valueAsNumber)} style={{ flex: 1 }} /> @@ -459,7 +480,7 @@ export const Playground = { max="1400" step="10" value={subtitleDuration()} - onInput={(event) => setSubtitleDuration(event.currentTarget.valueAsNumber)} + onInput={(event) => setCfg("subtitleDuration", event.currentTarget.valueAsNumber)} style={{ flex: 1 }} /> @@ -473,7 +494,7 @@ export const Playground = { setSubtitleAuto(event.currentTarget.checked)} + onInput={(event) => setCfg("subtitleAuto", event.currentTarget.checked)} /> {subtitleAuto() ? "on" : "off"} @@ -489,7 +510,7 @@ export const Playground = { max="40" step="1" value={subtitleTravel()} - onInput={(event) => setSubtitleTravel(event.currentTarget.valueAsNumber)} + onInput={(event) => setCfg("subtitleTravel", event.currentTarget.valueAsNumber)} style={{ flex: 1 }} /> {subtitleTravel()}px @@ -504,7 +525,7 @@ export const Playground = { max="40" step="1" value={subtitleEdge()} - onInput={(event) => setSubtitleEdge(event.currentTarget.valueAsNumber)} + onInput={(event) => setCfg("subtitleEdge", event.currentTarget.valueAsNumber)} style={{ flex: 1 }} /> {subtitleEdge()}% @@ -523,7 +544,7 @@ export const Playground = { max="1400" step="10" value={countDuration()} - onInput={(event) => setCountDuration(event.currentTarget.valueAsNumber)} + onInput={(event) => setCfg("countDuration", event.currentTarget.valueAsNumber)} style={{ flex: 1 }} /> @@ -540,7 +561,7 @@ export const Playground = { max="40" step="1" value={countMask()} - onInput={(event) => setCountMask(event.currentTarget.valueAsNumber)} + onInput={(event) => setCfg("countMask", event.currentTarget.valueAsNumber)} style={{ flex: 1 }} /> {countMask()}% @@ -555,7 +576,7 @@ export const Playground = { max="14" step="1" value={countMaskHeight()} - onInput={(event) => setCountMaskHeight(event.currentTarget.valueAsNumber)} + onInput={(event) => setCfg("countMaskHeight", event.currentTarget.valueAsNumber)} style={{ flex: 1 }} /> {countMaskHeight()}px @@ -570,7 +591,7 @@ export const Playground = { max="1200" step="10" value={countWidthDuration()} - onInput={(event) => setCountWidthDuration(event.currentTarget.valueAsNumber)} + onInput={(event) => setCfg("countWidthDuration", event.currentTarget.valueAsNumber)} style={{ flex: 1 }} /> diff --git a/packages/ui/src/components/tool-count-summary.stories.tsx b/packages/ui/src/components/tool-count-summary.stories.tsx index 4be3a02bb..cf160b188 100644 --- a/packages/ui/src/components/tool-count-summary.stories.tsx +++ b/packages/ui/src/components/tool-count-summary.stories.tsx @@ -1,5 +1,6 @@ // @ts-nocheck -import { createSignal, onCleanup } from "solid-js" +import { onCleanup } from "solid-js" +import { createStore } from "solid-js/store" import { AnimatedCountList, type CountItem } from "./tool-count-summary" import { ToolStatusTitle } from "./tool-status-title" @@ -57,11 +58,18 @@ const smallBtn = (active?: boolean) => export const Playground = { render: () => { - const [reads, setReads] = createSignal(0) - const [searches, setSearches] = createSignal(0) - const [lists, setLists] = createSignal(0) - const [active, setActive] = createSignal(false) - const [reducedMotion, setReducedMotion] = createSignal(false) + const [state, setState] = createStore({ + reads: 0, + searches: 0, + lists: 0, + active: false, + reducedMotion: false, + }) + const reads = () => state.reads + const searches = () => state.searches + const lists = () => state.lists + const active = () => state.active + const reducedMotion = () => state.reducedMotion let timeouts: ReturnType[] = [] @@ -74,10 +82,10 @@ export const Playground = { const startSim = () => { clearAll() - setReads(0) - setSearches(0) - setLists(0) - setActive(true) + setState("reads", 0) + setState("searches", 0) + setState("lists", 0) + setState("active", true) const steps = rand(3, 10) let elapsed = 0 @@ -86,27 +94,27 @@ export const Playground = { elapsed += delay const t = setTimeout(() => { const pick = rand(0, 2) - if (pick === 0) setReads((n) => n + 1) - else if (pick === 1) setSearches((n) => n + 1) - else setLists((n) => n + 1) + if (pick === 0) setState("reads", (value) => value + 1) + else if (pick === 1) setState("searches", (value) => value + 1) + else setState("lists", (value) => value + 1) }, elapsed) timeouts.push(t) } - const end = setTimeout(() => setActive(false), elapsed + 100) + const end = setTimeout(() => setState("active", false), elapsed + 100) timeouts.push(end) } const stopSim = () => { clearAll() - setActive(false) + setState("active", false) } const reset = () => { stopSim() - setReads(0) - setSearches(0) - setLists(0) + setState("reads", 0) + setState("searches", 0) + setState("lists", 0) } const items = (): CountItem[] => [ @@ -164,19 +172,19 @@ export const Playground = { -
- - -
diff --git a/packages/ui/src/components/tool-error-card.tsx b/packages/ui/src/components/tool-error-card.tsx index ba39ae586..0c99924de 100644 --- a/packages/ui/src/components/tool-error-card.tsx +++ b/packages/ui/src/components/tool-error-card.tsx @@ -1,4 +1,5 @@ -import { type ComponentProps, createMemo, createSignal, Show, splitProps } from "solid-js" +import { type ComponentProps, createMemo, Show, splitProps } from "solid-js" +import { createStore } from "solid-js/store" import { Card, CardDescription } from "./card" import { Collapsible } from "./collapsible" import { Icon } from "./icon" @@ -16,8 +17,12 @@ export interface ToolErrorCardProps extends Omit, "c export function ToolErrorCard(props: ToolErrorCardProps) { const i18n = useI18n() - const [open, setOpen] = createSignal(props.defaultOpen ?? false) - const [copied, setCopied] = createSignal(false) + const [state, setState] = createStore({ + open: props.defaultOpen ?? false, + copied: false, + }) + const open = () => state.open + const copied = () => state.copied const [split, rest] = splitProps(props, ["tool", "error", "defaultOpen", "subtitle", "href"]) const name = createMemo(() => { const map: Record = { @@ -65,13 +70,18 @@ export function ToolErrorCard(props: ToolErrorCardProps) { const text = cleaned() if (!text) return await navigator.clipboard.writeText(text) - setCopied(true) - setTimeout(() => setCopied(false), 2000) + setState("copied", true) + setTimeout(() => setState("copied", false), 2000) } return ( - + setState("open", value)} + >
diff --git a/packages/ui/src/components/tool-status-title.tsx b/packages/ui/src/components/tool-status-title.tsx index 68440b6c6..2a58e0e5b 100644 --- a/packages/ui/src/components/tool-status-title.tsx +++ b/packages/ui/src/components/tool-status-title.tsx @@ -1,4 +1,5 @@ -import { Show, createEffect, createMemo, createSignal, on, onCleanup, onMount } from "solid-js" +import { Show, createEffect, createMemo, on, onCleanup, onMount } from "solid-js" +import { createStore } from "solid-js/store" import { TextShimmer } from "./text-shimmer" function common(active: string, done: string) { @@ -35,8 +36,12 @@ export function ToolStatusTitle(props: { const activeTail = createMemo(() => (suffix() ? split().active : props.activeText)) const doneTail = createMemo(() => (suffix() ? split().done : props.doneText)) - const [width, setWidth] = createSignal("auto") - const [ready, setReady] = createSignal(false) + const [state, setState] = createStore({ + width: "auto", + ready: false, + }) + const width = () => state.width + const ready = () => state.ready let activeRef: HTMLSpanElement | undefined let doneRef: HTMLSpanElement | undefined let frame: number | undefined @@ -45,7 +50,7 @@ export function ToolStatusTitle(props: { const measure = () => { const target = props.active ? activeRef : doneRef const px = contentWidth(target) - if (px > 0) setWidth(`${px}px`) + if (px > 0) setState("width", `${px}px`) } const schedule = () => { @@ -62,13 +67,13 @@ export function ToolStatusTitle(props: { const finish = () => { if (typeof requestAnimationFrame !== "function") { - setReady(true) + setState("ready", true) return } if (readyFrame !== undefined) cancelAnimationFrame(readyFrame) readyFrame = requestAnimationFrame(() => { readyFrame = undefined - setReady(true) + setState("ready", true) }) } diff --git a/packages/ui/src/pierre/file-find.ts b/packages/ui/src/pierre/file-find.ts index ee608152d..692ab3167 100644 --- a/packages/ui/src/pierre/file-find.ts +++ b/packages/ui/src/pierre/file-find.ts @@ -1,4 +1,5 @@ -import { createEffect, createSignal, onCleanup, onMount } from "solid-js" +import { createEffect, onCleanup, onMount } from "solid-js" +import { createStore } from "solid-js/store" export type FindHost = { element: () => HTMLElement | undefined @@ -107,11 +108,18 @@ export function createFileFind(opts: CreateFileFindOptions) { let mode: "highlights" | "overlay" = "overlay" let hits: Range[] = [] - const [open, setOpen] = createSignal(false) - const [query, setQuery] = createSignal("") - const [index, setIndex] = createSignal(0) - const [count, setCount] = createSignal(0) - const [pos, setPos] = createSignal({ top: 8, right: 8 }) + const [state, setState] = createStore({ + open: false, + query: "", + index: 0, + count: 0, + pos: { top: 8, right: 8 }, + }) + const open = () => state.open + const query = () => state.query + const index = () => state.index + const count = () => state.count + const pos = () => state.pos const clearOverlayScroll = () => { for (const el of overlayScroll) el.removeEventListener("scroll", scheduleOverlay) @@ -200,8 +208,8 @@ export function createFileFind(opts: CreateFileFindOptions) { clearOverlay() clearOverlayScroll() hits = [] - setCount(0) - setIndex(0) + setState("count", 0) + setState("index", 0) } const positionBar = () => { @@ -214,7 +222,7 @@ export function createFileFind(opts: CreateFileFindOptions) { const title = parseFloat(getComputedStyle(root).getPropertyValue("--session-title-height")) const header = Number.isNaN(title) ? 0 : title - setPos({ + setState("pos", { top: Math.round(rect.top) + header - 4, right: Math.round(window.innerWidth - rect.right) + 8, }) @@ -318,8 +326,8 @@ export function createFileFind(opts: CreateFileFindOptions) { const currentIndex = total ? Math.min(desired, total - 1) : 0 hits = ranges - setCount(total) - setIndex(currentIndex) + setState("count", total) + setState("index", currentIndex) const active = ranges[currentIndex] if (mode === "highlights") { @@ -342,8 +350,8 @@ export function createFileFind(opts: CreateFileFindOptions) { } const close = () => { - setOpen(false) - setQuery("") + setState("open", false) + setState("query", "") clearFind() if (current === host) current = undefined } @@ -352,7 +360,7 @@ export function createFileFind(opts: CreateFileFindOptions) { if (current && current !== host) current.close() current = host target = host - if (!open()) setOpen(true) + if (!open()) setState("open", true) requestAnimationFrame(() => { apply({ scroll: true }) input?.focus() @@ -366,7 +374,7 @@ export function createFileFind(opts: CreateFileFindOptions) { if (total <= 0) return const currentIndex = (index() + dir + total) % total - setIndex(currentIndex) + setState("index", currentIndex) const active = hits[currentIndex] if (!active) return @@ -449,8 +457,8 @@ export function createFileFind(opts: CreateFileFindOptions) { input = el }, setQuery: (value: string) => { - setQuery(value) - setIndex(0) + setState("query", value) + setState("index", 0) apply({ reset: true, scroll: true }) }, focus,