chore: cleanup (#17284)

This commit is contained in:
Adam
2026-03-13 06:27:58 -05:00
committed by GitHub
parent 46ba9c8170
commit 270cb0b8b4
23 changed files with 516 additions and 357 deletions

View File

@@ -86,15 +86,17 @@ const useServerHealth = (servers: Accessor<ServerConnection.Any[]>) => {
const useDefaultServerKey = ( const useDefaultServerKey = (
get: (() => string | Promise<string | null | undefined> | null | undefined) | undefined, get: (() => string | Promise<string | null | undefined> | null | undefined) | undefined,
) => { ) => {
const [url, setUrl] = createSignal<string | undefined>() const [state, setState] = createStore({
const [tick, setTick] = createSignal(0) url: undefined as string | undefined,
tick: 0,
})
createEffect(() => { createEffect(() => {
tick() state.tick
let dead = false let dead = false
const result = get?.() const result = get?.()
if (!result) { if (!result) {
setUrl(undefined) setState("url", undefined)
onCleanup(() => { onCleanup(() => {
dead = true dead = true
}) })
@@ -104,7 +106,7 @@ const useDefaultServerKey = (
if (result instanceof Promise) { if (result instanceof Promise) {
void result.then((next) => { void result.then((next) => {
if (dead) return if (dead) return
setUrl(next ? normalizeServerUrl(next) : undefined) setState("url", next ? normalizeServerUrl(next) : undefined)
}) })
onCleanup(() => { onCleanup(() => {
dead = true dead = true
@@ -112,7 +114,7 @@ const useDefaultServerKey = (
return return
} }
setUrl(normalizeServerUrl(result)) setState("url", normalizeServerUrl(result))
onCleanup(() => { onCleanup(() => {
dead = true dead = true
}) })
@@ -120,11 +122,11 @@ const useDefaultServerKey = (
return { return {
key: () => { key: () => {
const u = url() const u = state.url
if (!u) return if (!u) return
return ServerConnection.key({ type: "http", http: { url: u } }) return ServerConnection.key({ type: "http", http: { url: u } })
}, },
refresh: () => setTick((value) => value + 1), refresh: () => setState("tick", (value) => value + 1),
} }
} }

View File

@@ -1,16 +1,4 @@
import { import { batch, createEffect, createMemo, For, on, onCleanup, onMount, ParentProps, Show, untrack } from "solid-js"
batch,
createEffect,
createMemo,
createSignal,
For,
on,
onCleanup,
onMount,
ParentProps,
Show,
untrack,
} from "solid-js"
import { useNavigate, useParams } from "@solidjs/router" import { useNavigate, useParams } from "@solidjs/router"
import { useLayout, LocalProject } from "@/context/layout" import { useLayout, LocalProject } from "@/context/layout"
import { useGlobalSync } from "@/context/global-sync" import { useGlobalSync } from "@/context/global-sync"
@@ -145,6 +133,10 @@ export default function Layout(props: ParentProps) {
hoverProject: undefined as string | undefined, hoverProject: undefined as string | undefined,
scrollSessionKey: undefined as string | undefined, scrollSessionKey: undefined as string | undefined,
nav: undefined as HTMLElement | undefined, nav: undefined as HTMLElement | undefined,
sortNow: Date.now(),
sizing: false,
peek: undefined as LocalProject | undefined,
peeked: false,
}) })
const editor = createInlineEditorController() const editor = createInlineEditorController()
@@ -163,14 +155,13 @@ export default function Layout(props: ParentProps) {
} }
const isBusy = (directory: string) => !!state.busyWorkspaces[workspaceKey(directory)] const isBusy = (directory: string) => !!state.busyWorkspaces[workspaceKey(directory)]
const navLeave = { current: undefined as number | undefined } const navLeave = { current: undefined as number | undefined }
const [sortNow, setSortNow] = createSignal(Date.now()) const sortNow = () => state.sortNow
const [sizing, setSizing] = createSignal(false)
let sizet: number | undefined let sizet: number | undefined
let sortNowInterval: ReturnType<typeof setInterval> | undefined let sortNowInterval: ReturnType<typeof setInterval> | undefined
const sortNowTimeout = setTimeout( const sortNowTimeout = setTimeout(
() => { () => {
setSortNow(Date.now()) setState("sortNow", Date.now())
sortNowInterval = setInterval(() => setSortNow(Date.now()), 60_000) sortNowInterval = setInterval(() => setState("sortNow", Date.now()), 60_000)
}, },
60_000 - (Date.now() % 60_000), 60_000 - (Date.now() % 60_000),
) )
@@ -196,7 +187,7 @@ export default function Layout(props: ParentProps) {
}) })
onMount(() => { onMount(() => {
const stop = () => setSizing(false) const stop = () => setState("sizing", false)
window.addEventListener("pointerup", stop) window.addEventListener("pointerup", stop)
window.addEventListener("pointercancel", stop) window.addEventListener("pointercancel", stop)
window.addEventListener("blur", stop) window.addEventListener("blur", stop)
@@ -234,8 +225,6 @@ export default function Layout(props: ParentProps) {
}, 300) }, 300)
} }
const [peek, setPeek] = createSignal<LocalProject | undefined>(undefined)
const [peeked, setPeeked] = createSignal(false)
let peekt: number | undefined let peekt: number | undefined
const hoverProjectData = createMemo(() => { const hoverProjectData = createMemo(() => {
@@ -251,17 +240,17 @@ export default function Layout(props: ParentProps) {
clearTimeout(peekt) clearTimeout(peekt)
peekt = undefined peekt = undefined
} }
setPeek(p) setState("peek", p)
setPeeked(true) setState("peeked", true)
return return
} }
setPeeked(false) setState("peeked", false)
if (peek() === undefined) return if (state.peek === undefined) return
if (peekt !== undefined) clearTimeout(peekt) if (peekt !== undefined) clearTimeout(peekt)
peekt = window.setTimeout(() => { peekt = window.setTimeout(() => {
peekt = undefined peekt = undefined
setPeek(undefined) setState("peek", undefined)
}, 180) }, 180)
}) })
@@ -2245,7 +2234,7 @@ export default function Layout(props: ParentProps) {
> >
<div class="@container w-full h-full contain-strict">{sidebarContent()}</div> <div class="@container w-full h-full contain-strict">{sidebarContent()}</div>
<Show when={layout.sidebar.opened()}> <Show when={layout.sidebar.opened()}>
<div onPointerDown={() => setSizing(true)}> <div onPointerDown={() => setState("sizing", true)}>
<ResizeHandle <ResizeHandle
direction="horizontal" direction="horizontal"
size={layout.sidebar.width()} size={layout.sidebar.width()}
@@ -2253,9 +2242,9 @@ export default function Layout(props: ParentProps) {
max={typeof window === "undefined" ? 1000 : window.innerWidth * 0.3 + 64} max={typeof window === "undefined" ? 1000 : window.innerWidth * 0.3 + 64}
collapseThreshold={244} collapseThreshold={244}
onResize={(w) => { onResize={(w) => {
setSizing(true) setState("sizing", true)
if (sizet !== undefined) clearTimeout(sizet) if (sizet !== undefined) clearTimeout(sizet)
sizet = window.setTimeout(() => setSizing(false), 120) sizet = window.setTimeout(() => setState("sizing", false), 120)
layout.sidebar.resize(w) layout.sidebar.resize(w)
}} }}
onCollapse={layout.sidebar.close} 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, "xl:inset-y-0 xl:right-0 xl:left-[var(--main-left)]": true,
"z-20": true, "z-20": true,
"transition-[left] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[left] motion-reduce:transition-none": "transition-[left] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[left] motion-reduce:transition-none":
!sizing(), !state.sizing,
}} }}
style={{ style={{
"--main-left": layout.sidebar.opened() ? `${Math.max(layout.sidebar.width(), 244)}px` : "4rem", "--main-left": layout.sidebar.opened() ? `${Math.max(layout.sidebar.width(), 244)}px` : "4rem",
@@ -2320,11 +2309,11 @@ export default function Layout(props: ParentProps) {
<div <div
classList={{ classList={{
"hidden xl:flex absolute inset-y-0 left-16 z-30": true, "hidden xl:flex absolute inset-y-0 left-16 z-30": true,
"opacity-100 translate-x-0 pointer-events-auto": peeked() && !layout.sidebar.opened(), "opacity-100 translate-x-0 pointer-events-auto": state.peeked && !layout.sidebar.opened(),
"opacity-0 -translate-x-2 pointer-events-none": !peeked() || layout.sidebar.opened(), "opacity-0 -translate-x-2 pointer-events-none": !state.peeked || layout.sidebar.opened(),
"transition-[opacity,transform] motion-reduce:transition-none": true, "transition-[opacity,transform] motion-reduce:transition-none": true,
"duration-180 ease-out": peeked() && !layout.sidebar.opened(), "duration-180 ease-out": state.peeked && !layout.sidebar.opened(),
"duration-120 ease-in": !peeked() || layout.sidebar.opened(), "duration-120 ease-in": !state.peeked || layout.sidebar.opened(),
}} }}
onMouseMove={disarm} onMouseMove={disarm}
onMouseEnter={() => { onMouseEnter={() => {
@@ -2336,19 +2325,19 @@ export default function Layout(props: ParentProps) {
arm() arm()
}} }}
> >
<Show when={peek()}> <Show when={state.peek}>
<SidebarPanel project={peek()} merged={false} /> <SidebarPanel project={state.peek} merged={false} />
</Show> </Show>
</div> </div>
<div <div
classList={{ classList={{
"hidden xl:block pointer-events-none absolute inset-y-0 right-0 z-25 overflow-hidden": true, "hidden xl:block pointer-events-none absolute inset-y-0 right-0 z-25 overflow-hidden": true,
"opacity-100 translate-x-0": peeked() && !layout.sidebar.opened(), "opacity-100 translate-x-0": state.peeked && !layout.sidebar.opened(),
"opacity-0 -translate-x-2": !peeked() || layout.sidebar.opened(), "opacity-0 -translate-x-2": !state.peeked || layout.sidebar.opened(),
"transition-[opacity,transform] motion-reduce:transition-none": true, "transition-[opacity,transform] motion-reduce:transition-none": true,
"duration-180 ease-out": peeked() && !layout.sidebar.opened(), "duration-180 ease-out": state.peeked && !layout.sidebar.opened(),
"duration-120 ease-in": !peeked() || layout.sidebar.opened(), "duration-120 ease-in": !state.peeked || layout.sidebar.opened(),
}} }}
style={{ left: `calc(4rem + ${Math.max(Math.max(layout.sidebar.width(), 244) - 64, 0)}px)` }} style={{ left: `calc(4rem + ${Math.max(Math.max(layout.sidebar.width(), 244) - 64, 0)}px)` }}
> >

View File

@@ -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 TRACK = Array.from({ length: 30 }, (_, index) => index % 10)
const DURATION = 600 const DURATION = 600
@@ -14,8 +15,12 @@ function spin(from: number, to: number, direction: 1 | -1) {
} }
function Digit(props: { value: number; direction: 1 | -1 }) { function Digit(props: { value: number; direction: 1 | -1 }) {
const [step, setStep] = createSignal(props.value + 10) const [state, setState] = createStore({
const [animating, setAnimating] = createSignal(false) step: props.value + 10,
animating: false,
})
const step = () => state.step
const animating = () => state.animating
let last = props.value let last = props.value
createEffect( createEffect(
@@ -25,13 +30,13 @@ function Digit(props: { value: number; direction: 1 | -1 }) {
const delta = spin(last, next, props.direction) const delta = spin(last, next, props.direction)
last = next last = next
if (!delta) { if (!delta) {
setAnimating(false) setState("animating", false)
setStep(next + 10) setState("step", next + 10)
return return
} }
setAnimating(true) setState("animating", true)
setStep((value) => value + delta) setState("step", (value) => value + delta)
}, },
{ defer: true }, { defer: true },
), ),
@@ -43,8 +48,8 @@ function Digit(props: { value: number; direction: 1 | -1 }) {
data-slot="animated-number-strip" data-slot="animated-number-strip"
data-animating={animating() ? "true" : "false"} data-animating={animating() ? "true" : "false"}
onTransitionEnd={() => { onTransitionEnd={() => {
setAnimating(false) setState("animating", false)
setStep((value) => normalize(value) + 10) setState("step", (value) => normalize(value) + 10)
}} }}
style={{ style={{
"--animated-number-offset": `${step()}`, "--animated-number-offset": `${step()}`,
@@ -63,8 +68,12 @@ export function AnimatedNumber(props: { value: number; class?: string }) {
return Math.max(0, Math.round(props.value)) return Math.max(0, Math.round(props.value))
}) })
const [value, setValue] = createSignal(target()) const [state, setState] = createStore({
const [direction, setDirection] = createSignal<1 | -1>(1) value: target(),
direction: 1 as 1 | -1,
})
const value = () => state.value
const direction = () => state.direction
createEffect( createEffect(
on( on(
@@ -73,8 +82,8 @@ export function AnimatedNumber(props: { value: number; class?: string }) {
const current = value() const current = value()
if (next === current) return if (next === current) return
setDirection(next > current ? 1 : -1) setState("direction", next > current ? 1 : -1)
setValue(next) setState("value", next)
}, },
{ defer: true }, { defer: true },
), ),

View File

@@ -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 { animate, type AnimationPlaybackControls } from "motion"
import { createStore } from "solid-js/store"
import { Collapsible } from "./collapsible" import { Collapsible } from "./collapsible"
import type { IconProps } from "./icon" import type { IconProps } from "./icon"
import { TextShimmer } from "./text-shimmer" import { TextShimmer } from "./text-shimmer"
@@ -37,8 +38,12 @@ export interface BasicToolProps {
const SPRING = { type: "spring" as const, visualDuration: 0.35, bounce: 0 } const SPRING = { type: "spring" as const, visualDuration: 0.35, bounce: 0 }
export function BasicTool(props: BasicToolProps) { export function BasicTool(props: BasicToolProps) {
const [open, setOpen] = createSignal(props.defaultOpen ?? false) const [state, setState] = createStore({
const [ready, setReady] = createSignal(open()) open: props.defaultOpen ?? false,
ready: props.defaultOpen ?? false,
})
const open = () => state.open
const ready = () => state.ready
const pending = () => props.status === "pending" || props.status === "running" const pending = () => props.status === "pending" || props.status === "running"
let frame: number | undefined let frame: number | undefined
@@ -52,7 +57,7 @@ export function BasicTool(props: BasicToolProps) {
onCleanup(cancel) onCleanup(cancel)
createEffect(() => { createEffect(() => {
if (props.forceOpen) setOpen(true) if (props.forceOpen) setState("open", true)
}) })
createEffect( createEffect(
@@ -62,7 +67,7 @@ export function BasicTool(props: BasicToolProps) {
if (!props.defer) return if (!props.defer) return
if (!value) { if (!value) {
cancel() cancel()
setReady(false) setState("ready", false)
return return
} }
@@ -70,7 +75,7 @@ export function BasicTool(props: BasicToolProps) {
frame = requestAnimationFrame(() => { frame = requestAnimationFrame(() => {
frame = undefined frame = undefined
if (!open()) return if (!open()) return
setReady(true) setState("ready", true)
}) })
}, },
{ defer: true }, { defer: true },
@@ -112,7 +117,7 @@ export function BasicTool(props: BasicToolProps) {
const handleOpenChange = (value: boolean) => { const handleOpenChange = (value: boolean) => {
if (pending()) return if (pending()) return
if (props.locked && !value) return if (props.locked && !value) return
setOpen(value) setState("open", value)
} }
return ( return (

View File

@@ -1,5 +1,6 @@
import { type DiffLineAnnotation, type SelectedLineRange } from "@pierre/diffs" import { type DiffLineAnnotation, type SelectedLineRange } from "@pierre/diffs"
import { createEffect, createMemo, createSignal, onCleanup, Show, type Accessor, type JSX } from "solid-js" 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 { render as renderSolid } from "solid-js/web"
import { createHoverCommentUtility } from "../pierre/comment-hover" import { createHoverCommentUtility } from "../pierre/comment-hover"
import { cloneSelectedLineRange, formatSelectedLineLabel, lineInSelectedRange } from "../pierre/selection-bridge" import { cloneSelectedLineRange, formatSelectedLineLabel, lineInSelectedRange } from "../pierre/selection-bridge"
@@ -200,8 +201,14 @@ export function createLineCommentAnnotationRenderer<T>(props: {
} }
export function createLineCommentState<T>(props: LineCommentStateProps<T>) { export function createLineCommentState<T>(props: LineCommentStateProps<T>) {
const [draft, setDraft] = createSignal("") const [state, setState] = createStore({
const [editing, setEditing] = createSignal<T | null>(null) 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 toRange = (range: SelectedLineRange | null) => (range ? cloneSelectedLineRange(range) : null)
const setSelected = (range: SelectedLineRange | null) => { const setSelected = (range: SelectedLineRange | null) => {
@@ -261,7 +268,7 @@ export function createLineCommentState<T>(props: LineCommentStateProps<T>) {
closeComment() closeComment()
setSelected(range) setSelected(range)
props.setCommenting(null) props.setCommenting(null)
setEditing(() => id) setEditing(id)
setDraft(value) setDraft(value)
} }

View File

@@ -1,5 +1,5 @@
import { type FilteredListProps, useFilteredList } from "@opencode-ai/ui/hooks" 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 { createStore } from "solid-js/store"
import { useI18n } from "../context/i18n" import { useI18n } from "../context/i18n"
import { Icon, type IconProps } from "./icon" import { Icon, type IconProps } from "./icon"
@@ -56,12 +56,16 @@ export interface ListRef {
export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void }) { export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void }) {
const i18n = useI18n() const i18n = useI18n()
const [scrollRef, setScrollRef] = createSignal<HTMLDivElement | undefined>(undefined)
const [internalFilter, setInternalFilter] = createSignal("")
let inputRef: HTMLInputElement | HTMLTextAreaElement | undefined let inputRef: HTMLInputElement | HTMLTextAreaElement | undefined
const [store, setStore] = createStore({ const [store, setStore] = createStore({
mouseActive: false, 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 scrollIntoView = (container: HTMLDivElement, node: HTMLElement, block: "center" | "nearest") => {
const containerRect = container.getBoundingClientRect() const containerRect = container.getBoundingClientRect()
@@ -208,18 +212,20 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
} }
function GroupHeader(groupProps: { group: { category: string; items: T[] } }): JSX.Element { function GroupHeader(groupProps: { group: { category: string; items: T[] } }): JSX.Element {
const [stuck, setStuck] = createSignal(false) const [state, setState] = createStore({
const [header, setHeader] = createSignal<HTMLDivElement | undefined>(undefined) stuck: false,
header: undefined as HTMLDivElement | undefined,
})
createEffect(() => { createEffect(() => {
const scroll = scrollRef() const scroll = scrollRef()
const node = header() const node = state.header
if (!scroll || !node) return if (!scroll || !node) return
const handler = () => { const handler = () => {
const rect = node.getBoundingClientRect() const rect = node.getBoundingClientRect()
const scrollRect = scroll.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 }) scroll.addEventListener("scroll", handler, { passive: true })
@@ -228,7 +234,7 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
}) })
return ( return (
<div data-slot="list-header" data-stuck={stuck()} ref={setHeader}> <div data-slot="list-header" data-stuck={state.stuck} ref={(el) => setState("header", el)}>
{props.groupHeader?.(groupProps.group) ?? groupProps.group.category} {props.groupHeader?.(groupProps.group) ?? groupProps.group.category}
</div> </div>
) )

View File

@@ -12,6 +12,7 @@ import {
Index, Index,
type JSX, type JSX,
} from "solid-js" } from "solid-js"
import { createStore } from "solid-js/store"
import stripAnsi from "strip-ansi" import stripAnsi from "strip-ansi"
import { Dynamic } from "solid-js/web" import { Dynamic } from "solid-js/web"
import { import {
@@ -885,8 +886,12 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
const data = useData() const data = useData()
const dialog = useDialog() const dialog = useDialog()
const i18n = useI18n() const i18n = useI18n()
const [copied, setCopied] = createSignal(false) const [state, setState] = createStore({
const [busy, setBusy] = createSignal<"fork" | "revert" | undefined>() copied: false,
busy: undefined as "fork" | "revert" | undefined,
})
const copied = () => state.copied
const busy = () => state.busy
const textPart = createMemo( const textPart = createMemo(
() => props.parts?.find((p) => p.type === "text" && !(p as TextPart).synthetic) as TextPart | undefined, () => 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() const content = text()
if (!content) return if (!content) return
await navigator.clipboard.writeText(content) await navigator.clipboard.writeText(content)
setCopied(true) setState("copied", true)
setTimeout(() => setCopied(false), 2000) setTimeout(() => setState("copied", false), 2000)
} }
const run = (kind: "fork" | "revert") => { const run = (kind: "fork" | "revert") => {
const act = kind === "fork" ? props.actions?.fork : props.actions?.revert const act = kind === "fork" ? props.actions?.fork : props.actions?.revert
if (!act || busy()) return if (!act || busy()) return
setBusy(kind) setState("busy", kind)
void Promise.resolve() void Promise.resolve()
.then(() => .then(() =>
act({ act({
@@ -962,7 +967,7 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
}), }),
) )
.finally(() => { .finally(() => {
if (busy() === kind) setBusy(undefined) if (busy() === kind) setState("busy", undefined)
}) })
} }

View File

@@ -5,11 +5,11 @@ import {
ParentProps, ParentProps,
Show, Show,
createEffect, createEffect,
createSignal,
onCleanup, onCleanup,
splitProps, splitProps,
ValidComponent, ValidComponent,
} from "solid-js" } from "solid-js"
import { createStore } from "solid-js/store"
import { useI18n } from "../context/i18n" import { useI18n } from "../context/i18n"
import { IconButton } from "./icon-button" import { IconButton } from "./icon-button"
@@ -46,23 +46,24 @@ export function Popover<T extends ValidComponent = "div">(props: PopoverProps<T>
"modal", "modal",
]) ])
const [contentRef, setContentRef] = createSignal<HTMLElement | undefined>(undefined) const [state, setState] = createStore({
const [triggerRef, setTriggerRef] = createSignal<HTMLElement | undefined>(undefined) contentRef: undefined as HTMLElement | undefined,
const [dismiss, setDismiss] = createSignal<"escape" | "outside" | null>(null) triggerRef: undefined as HTMLElement | undefined,
dismiss: null as "escape" | "outside" | null,
const [uncontrolledOpen, setUncontrolledOpen] = createSignal<boolean>(local.defaultOpen ?? false) uncontrolledOpen: local.defaultOpen ?? false,
})
const controlled = () => local.open !== undefined const controlled = () => local.open !== undefined
const opened = () => { const opened = () => {
if (controlled()) return local.open ?? false if (controlled()) return local.open ?? false
return uncontrolledOpen() return state.uncontrolledOpen
} }
const onOpenChange = (next: boolean) => { const onOpenChange = (next: boolean) => {
if (next) setDismiss(null) if (next) setState("dismiss", null)
if (local.onOpenChange) local.onOpenChange(next) if (local.onOpenChange) local.onOpenChange(next)
if (controlled()) return if (controlled()) return
setUncontrolledOpen(next) setState("uncontrolledOpen", next)
} }
createEffect(() => { createEffect(() => {
@@ -70,15 +71,15 @@ export function Popover<T extends ValidComponent = "div">(props: PopoverProps<T>
const inside = (node: Node | null | undefined) => { const inside = (node: Node | null | undefined) => {
if (!node) return false if (!node) return false
const content = contentRef() const content = state.contentRef
if (content && content.contains(node)) return true if (content && content.contains(node)) return true
const trigger = triggerRef() const trigger = state.triggerRef
if (trigger && trigger.contains(node)) return true if (trigger && trigger.contains(node)) return true
return false return false
} }
const close = (reason: "escape" | "outside") => { const close = (reason: "escape" | "outside") => {
setDismiss(reason) setState("dismiss", reason)
onOpenChange(false) onOpenChange(false)
} }
@@ -116,7 +117,7 @@ export function Popover<T extends ValidComponent = "div">(props: PopoverProps<T>
const content = () => ( const content = () => (
<Kobalte.Content <Kobalte.Content
ref={(el: HTMLElement | undefined) => setContentRef(el)} ref={(el: HTMLElement | undefined) => setState("contentRef", el)}
data-component="popover-content" data-component="popover-content"
classList={{ classList={{
...(local.classList ?? {}), ...(local.classList ?? {}),
@@ -124,8 +125,8 @@ export function Popover<T extends ValidComponent = "div">(props: PopoverProps<T>
}} }}
style={local.style} style={local.style}
onCloseAutoFocus={(event: Event) => { onCloseAutoFocus={(event: Event) => {
if (dismiss() === "outside") event.preventDefault() if (state.dismiss === "outside") event.preventDefault()
setDismiss(null) setState("dismiss", null)
}} }}
> >
{/* <Kobalte.Arrow data-slot="popover-arrow" /> */} {/* <Kobalte.Arrow data-slot="popover-arrow" /> */}
@@ -151,7 +152,7 @@ export function Popover<T extends ValidComponent = "div">(props: PopoverProps<T>
return ( return (
<Kobalte gutter={4} {...rest} open={opened()} onOpenChange={onOpenChange} modal={local.modal ?? false}> <Kobalte gutter={4} {...rest} open={opened()} onOpenChange={onOpenChange} modal={local.modal ?? false}>
<Kobalte.Trigger <Kobalte.Trigger
ref={(el: HTMLElement) => setTriggerRef(el)} ref={(el: HTMLElement) => setState("triggerRef", el)}
as={local.triggerAs ?? "div"} as={local.triggerAs ?? "div"}
data-slot="popover-trigger" data-slot="popover-trigger"
{...(local.triggerProps as any)} {...(local.triggerProps as any)}

View File

@@ -1,5 +1,6 @@
// @ts-nocheck // @ts-nocheck
import { createSignal } from "solid-js" import { createSignal } from "solid-js"
import { createStore } from "solid-js/store"
import * as mod from "./resize-handle" import * as mod from "./resize-handle"
const docs = `### Overview const docs = `### Overview
@@ -94,8 +95,12 @@ export const Vertical = {
export const Collapse = { export const Collapse = {
render: () => { render: () => {
const [size, setSize] = createSignal(200) const [state, setState] = createStore({
const [collapsed, setCollapsed] = createSignal(false) size: 200,
collapsed: false,
})
const size = () => state.size
const collapsed = () => state.collapsed
return ( return (
<div style={{ display: "grid", gap: "8px" }}> <div style={{ display: "grid", gap: "8px" }}>
<div style={{ color: "var(--text-weak)", "font-size": "12px" }}> <div style={{ color: "var(--text-weak)", "font-size": "12px" }}>
@@ -116,10 +121,10 @@ export const Collapse = {
max={360} max={360}
collapseThreshold={100} collapseThreshold={100}
onResize={(next) => { onResize={(next) => {
setCollapsed(false) setState("collapsed", false)
setSize(next) 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)" style="height:24px;border:1px dashed color-mix(in oklab, var(--text-base) 20%, transparent)"
/> />
</div> </div>

View File

@@ -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" import { useI18n } from "../context/i18n"
export interface ScrollViewProps extends ComponentProps<"div"> { export interface ScrollViewProps extends ComponentProps<"div"> {
@@ -48,23 +49,29 @@ export function ScrollView(props: ScrollViewProps) {
let viewportRef!: HTMLDivElement let viewportRef!: HTMLDivElement
let thumbRef!: HTMLDivElement let thumbRef!: HTMLDivElement
const [isHovered, setIsHovered] = createSignal(false) const [state, setState] = createStore({
const [isDragging, setIsDragging] = createSignal(false) isHovered: false,
isDragging: false,
const [thumbHeight, setThumbHeight] = createSignal(0) thumbHeight: 0,
const [thumbTop, setThumbTop] = createSignal(0) thumbTop: 0,
const [showThumb, setShowThumb] = createSignal(false) showThumb: false,
})
const isHovered = () => state.isHovered
const isDragging = () => state.isDragging
const thumbHeight = () => state.thumbHeight
const thumbTop = () => state.thumbTop
const showThumb = () => state.showThumb
const updateThumb = () => { const updateThumb = () => {
if (!viewportRef) return if (!viewportRef) return
const { scrollTop, scrollHeight, clientHeight } = viewportRef const { scrollTop, scrollHeight, clientHeight } = viewportRef
if (scrollHeight <= clientHeight || scrollHeight === 0) { if (scrollHeight <= clientHeight || scrollHeight === 0) {
setShowThumb(false) setState("showThumb", false)
return return
} }
setShowThumb(true) setState("showThumb", true)
const trackPadding = 8 const trackPadding = 8
const trackHeight = clientHeight - trackPadding * 2 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) // 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)) const boundedTop = trackPadding + Math.max(0, Math.min(top, maxThumbTop))
setThumbHeight(height) setState("thumbHeight", height)
setThumbTop(boundedTop) setState("thumbTop", boundedTop)
} }
onMount(() => { onMount(() => {
@@ -113,7 +120,7 @@ export function ScrollView(props: ScrollViewProps) {
const onThumbPointerDown = (e: PointerEvent) => { const onThumbPointerDown = (e: PointerEvent) => {
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
setIsDragging(true) setState("isDragging", true)
startY = e.clientY startY = e.clientY
startScrollTop = viewportRef.scrollTop startScrollTop = viewportRef.scrollTop
@@ -132,7 +139,7 @@ export function ScrollView(props: ScrollViewProps) {
} }
const onPointerUp = (e: PointerEvent) => { const onPointerUp = (e: PointerEvent) => {
setIsDragging(false) setState("isDragging", false)
thumbRef.releasePointerCapture(e.pointerId) thumbRef.releasePointerCapture(e.pointerId)
thumbRef.removeEventListener("pointermove", onPointerMove) thumbRef.removeEventListener("pointermove", onPointerMove)
thumbRef.removeEventListener("pointerup", onPointerUp) thumbRef.removeEventListener("pointerup", onPointerUp)
@@ -191,8 +198,8 @@ export function ScrollView(props: ScrollViewProps) {
ref={rootRef} ref={rootRef}
class={`scroll-view ${local.class || ""}`} class={`scroll-view ${local.class || ""}`}
style={local.style} style={local.style}
onPointerEnter={() => setIsHovered(true)} onPointerEnter={() => setState("isHovered", true)}
onPointerLeave={() => setIsHovered(false)} onPointerLeave={() => setState("isHovered", false)}
{...rest} {...rest}
> >
{/* Viewport */} {/* Viewport */}

View File

@@ -13,7 +13,7 @@ import { useFileComponent } from "../context/file"
import { useI18n } from "../context/i18n" import { useI18n } from "../context/i18n"
import { getDirectory, getFilename } from "@opencode-ai/util/path" import { getDirectory, getFilename } from "@opencode-ai/util/path"
import { checksum } from "@opencode-ai/util/encode" 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 { onCleanup } from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { type FileContent, type FileDiff } from "@opencode-ai/sdk/v2" import { type FileContent, type FileDiff } from "@opencode-ai/sdk/v2"
@@ -138,14 +138,16 @@ export const SessionReview = (props: SessionReviewProps) => {
const i18n = useI18n() const i18n = useI18n()
const fileComponent = useFileComponent() const fileComponent = useFileComponent()
const anchors = new Map<string, HTMLElement>() const anchors = new Map<string, HTMLElement>()
const [store, setStore] = createStore<{ open: string[]; force: Record<string, boolean> }>({ const [store, setStore] = createStore({
open: [], open: [] as string[],
force: {}, force: {} as Record<string, boolean>,
selection: null as SessionReviewSelection | null,
commenting: null as SessionReviewSelection | null,
opened: null as SessionReviewFocus | null,
}) })
const selection = () => store.selection
const [selection, setSelection] = createSignal<SessionReviewSelection | null>(null) const commenting = () => store.commenting
const [commenting, setCommenting] = createSignal<SessionReviewSelection | null>(null) const opened = () => store.opened
const [opened, setOpened] = createSignal<SessionReviewFocus | null>(null)
const open = () => props.open ?? store.open const open = () => props.open ?? store.open
const files = createMemo(() => props.diffs.map((diff) => diff.file)) const files = createMemo(() => props.diffs.map((diff) => diff.file))
@@ -184,10 +186,10 @@ export const SessionReview = (props: SessionReviewProps) => {
focusToken++ focusToken++
const token = focusToken const token = focusToken
setOpened(focus) setStore("opened", focus)
const comment = (props.comments ?? []).find((c) => c.file === focus.file && c.id === focus.id) 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() const current = open()
if (!current.includes(focus.file)) { if (!current.includes(focus.file)) {
@@ -331,11 +333,11 @@ export const SessionReview = (props: SessionReviewProps) => {
if (!current || current.file !== file) return null if (!current || current.file !== file) return null
return current.id return current.id
}, },
setOpened: (id) => setOpened(id ? { file, id } : null), setOpened: (id) => setStore("opened", id ? { file, id } : null),
selected: selectedLines, selected: selectedLines,
setSelected: (range) => setSelection(range ? { file, range } : null), setSelected: (range) => setStore("selection", range ? { file, range } : null),
commenting: draftRange, commenting: draftRange,
setCommenting: (range) => setCommenting(range ? { file, range } : null), setCommenting: (range) => setStore("commenting", range ? { file, range } : null),
}, },
getSide: selectionSide, getSide: selectionSide,
clearSelectionOnSelectionEndNull: false, clearSelectionOnSelectionEndNull: false,

View File

@@ -6,6 +6,7 @@ import { useFileComponent } from "../context/file"
import { Binary } from "@opencode-ai/util/binary" import { Binary } from "@opencode-ai/util/binary"
import { getDirectory, getFilename } from "@opencode-ai/util/path" import { getDirectory, getFilename } from "@opencode-ai/util/path"
import { createEffect, createMemo, createSignal, For, on, ParentProps, Show } from "solid-js" import { createEffect, createMemo, createSignal, For, on, ParentProps, Show } from "solid-js"
import { createStore } from "solid-js/store"
import { Dynamic } from "solid-js/web" import { Dynamic } from "solid-js/web"
import { AssistantParts, Message, MessageDivider, PART_MAPPING, type UserActions } from "./message-part" import { AssistantParts, Message, MessageDivider, PART_MAPPING, type UserActions } from "./message-part"
import { Card } from "./card" import { Card } from "./card"
@@ -240,14 +241,18 @@ export function SessionTurn(
.reverse() .reverse()
}) })
const edited = createMemo(() => diffs().length) const edited = createMemo(() => diffs().length)
const [open, setOpen] = createSignal(false) const [state, setState] = createStore({
const [expanded, setExpanded] = createSignal<string[]>([]) open: false,
expanded: [] as string[],
})
const open = () => state.open
const expanded = () => state.expanded
createEffect( createEffect(
on( on(
open, open,
(value, prev) => { (value, prev) => {
if (!value && prev) setExpanded([]) if (!value && prev) setState("expanded", [])
}, },
{ defer: true }, { defer: true },
), ),
@@ -425,7 +430,7 @@ export function SessionTurn(
<SessionRetry status={status()} show={active()} /> <SessionRetry status={status()} show={active()} />
<Show when={edited() > 0 && !working()}> <Show when={edited() > 0 && !working()}>
<div data-slot="session-turn-diffs"> <div data-slot="session-turn-diffs">
<Collapsible open={open()} onOpenChange={setOpen} variant="ghost"> <Collapsible open={open()} onOpenChange={(value) => setState("open", value)} variant="ghost">
<Collapsible.Trigger> <Collapsible.Trigger>
<div data-component="session-turn-diffs-trigger"> <div data-component="session-turn-diffs-trigger">
<div data-slot="session-turn-diffs-title"> <div data-slot="session-turn-diffs-title">
@@ -447,7 +452,9 @@ export function SessionTurn(
multiple multiple
style={{ "--sticky-accordion-offset": "40px" }} style={{ "--sticky-accordion-offset": "40px" }}
value={expanded()} value={expanded()}
onChange={(value) => setExpanded(Array.isArray(value) ? value : value ? [value] : [])} onChange={(value) =>
setState("expanded", Array.isArray(value) ? value : value ? [value] : [])
}
> >
<For each={diffs()}> <For each={diffs()}>
{(diff) => { {(diff) => {

View File

@@ -1,5 +1,6 @@
// @ts-nocheck // @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 { BasicTool } from "./basic-tool"
import { animate } from "motion" import { animate } from "motion"
@@ -138,29 +139,39 @@ function SpringSubmessage(props: { text: string; visible: boolean; visualDuratio
export const Playground = { export const Playground = {
render: () => { render: () => {
const [text, setText] = createSignal("Prints five topic blocks between timed commands") const [state, setState] = createStore({
const [show, setShow] = createSignal(true) text: "Prints five topic blocks between timed commands",
const [visualDuration, setVisualDuration] = createSignal(0.35) show: true,
const [bounce, setBounce] = createSignal(0) visualDuration: 0.35,
const [fadeMs, setFadeMs] = createSignal(320) bounce: 0,
const [blur, setBlur] = createSignal(2) fadeMs: 320,
const [fadeEase, setFadeEase] = createSignal<keyof typeof ease>("snappy") blur: 2,
const [auto, setAuto] = createSignal(false) 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 replayTimer
let autoTimer let autoTimer
const replay = () => { const replay = () => {
setShow(false) setState("show", false)
if (replayTimer) clearTimeout(replayTimer) if (replayTimer) clearTimeout(replayTimer)
replayTimer = setTimeout(() => { replayTimer = setTimeout(() => {
setShow(true) setState("show", true)
}, 50) }, 50)
} }
const stopAuto = () => { const stopAuto = () => {
if (autoTimer) clearInterval(autoTimer) if (autoTimer) clearInterval(autoTimer)
autoTimer = undefined autoTimer = undefined
setAuto(false) setState("auto", false)
} }
const toggleAuto = () => { const toggleAuto = () => {
@@ -168,7 +179,7 @@ export const Playground = {
stopAuto() stopAuto()
return return
} }
setAuto(true) setState("auto", true)
autoTimer = setInterval(replay, 2200) autoTimer = setInterval(replay, 2200)
} }
@@ -224,7 +235,7 @@ export const Playground = {
<button onClick={replay} style={btn()}> <button onClick={replay} style={btn()}>
Replay entry Replay entry
</button> </button>
<button onClick={() => setShow((v) => !v)} style={btn(show())}> <button onClick={() => setState("show", (value) => !value)} style={btn(show())}>
{show() ? "Hide subtitle" : "Show subtitle"} {show() ? "Hide subtitle" : "Show subtitle"}
</button> </button>
<button onClick={toggleAuto} style={btn(auto())}> <button onClick={toggleAuto} style={btn(auto())}>
@@ -244,7 +255,7 @@ export const Playground = {
<span style={sliderLabel}>subtitle</span> <span style={sliderLabel}>subtitle</span>
<input <input
value={text()} value={text()}
onInput={(e) => setText(e.currentTarget.value)} onInput={(e) => setState("text", e.currentTarget.value)}
style={{ style={{
width: "420px", width: "420px",
"max-width": "100%", "max-width": "100%",
@@ -265,7 +276,7 @@ export const Playground = {
max={1.5} max={1.5}
step={0.01} step={0.01}
value={visualDuration()} value={visualDuration()}
onInput={(e) => setVisualDuration(Number(e.currentTarget.value))} onInput={(e) => setState("visualDuration", Number(e.currentTarget.value))}
/> />
<span style={sliderValue}>{visualDuration().toFixed(2)}s</span> <span style={sliderValue}>{visualDuration().toFixed(2)}s</span>
</div> </div>
@@ -278,7 +289,7 @@ export const Playground = {
max={0.5} max={0.5}
step={0.01} step={0.01}
value={bounce()} value={bounce()}
onInput={(e) => setBounce(Number(e.currentTarget.value))} onInput={(e) => setState("bounce", Number(e.currentTarget.value))}
/> />
<span style={sliderValue}>{bounce().toFixed(2)}</span> <span style={sliderValue}>{bounce().toFixed(2)}</span>
</div> </div>
@@ -287,8 +298,14 @@ export const Playground = {
<span style={sliderLabel}>fade ease</span> <span style={sliderLabel}>fade ease</span>
<button <button
onClick={() => onClick={() =>
setFadeEase((v) => setState("fadeEase", (value) =>
v === "snappy" ? "smooth" : v === "smooth" ? "standard" : v === "standard" ? "linear" : "snappy", value === "snappy"
? "smooth"
: value === "smooth"
? "standard"
: value === "standard"
? "linear"
: "snappy",
) )
} }
style={btn()} style={btn()}
@@ -305,7 +322,7 @@ export const Playground = {
max={1400} max={1400}
step={10} step={10}
value={fadeMs()} value={fadeMs()}
onInput={(e) => setFadeMs(Number(e.currentTarget.value))} onInput={(e) => setState("fadeMs", Number(e.currentTarget.value))}
/> />
<span style={sliderValue}>{fadeMs()}ms</span> <span style={sliderValue}>{fadeMs()}ms</span>
</div> </div>
@@ -318,7 +335,7 @@ export const Playground = {
max={14} max={14}
step={0.5} step={0.5}
value={blur()} value={blur()}
onInput={(e) => setBlur(Number(e.currentTarget.value))} onInput={(e) => setState("blur", Number(e.currentTarget.value))}
/> />
<span style={sliderValue}>{blur()}px</span> <span style={sliderValue}>{blur()}px</span>
</div> </div>

View File

@@ -1,5 +1,6 @@
// @ts-nocheck // @ts-nocheck
import { createSignal, onCleanup } from "solid-js" import { onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
import { TextReveal } from "./text-reveal" import { TextReveal } from "./text-reveal"
export default { export default {
@@ -87,33 +88,42 @@ const headingSlot = {
export const Playground = { export const Playground = {
render: () => { render: () => {
const [index, setIndex] = createSignal(0) const [state, setState] = createStore({
const [cycling, setCycling] = createSignal(false) index: 0,
const [growOnly, setGrowOnly] = createSignal(true) cycling: false,
growOnly: true,
const [duration, setDuration] = createSignal(600) duration: 600,
const [bounce, setBounce] = createSignal(1.0) bounce: 1.0,
const [bounceSoft, setBounceSoft] = createSignal(1.0) bounceSoft: 1.0,
hybridTravel: 25,
const [hybridTravel, setHybridTravel] = createSignal(25) hybridEdge: 17,
const [hybridEdge, setHybridEdge] = createSignal(17) edge: 17,
revealTravel: 0,
const [edge, setEdge] = createSignal(17) })
const [revealTravel, setRevealTravel] = createSignal(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 let timer: number | undefined
const text = () => TEXTS[index()] const text = () => TEXTS[index()]
const next = () => setIndex((i) => (i + 1) % TEXTS.length) const next = () => setState("index", (value) => (value + 1) % TEXTS.length)
const prev = () => setIndex((i) => (i - 1 + TEXTS.length) % TEXTS.length) const prev = () => setState("index", (value) => (value - 1 + TEXTS.length) % TEXTS.length)
const toggleCycle = () => { const toggleCycle = () => {
if (cycling()) { if (cycling()) {
if (timer) clearTimeout(timer) if (timer) clearTimeout(timer)
timer = undefined timer = undefined
setCycling(false) setState("cycling", false)
return return
} }
setCycling(true) setState("cycling", true)
const tick = () => { const tick = () => {
next() next()
timer = window.setTimeout(tick, 700 + Math.floor(Math.random() * 600)) timer = window.setTimeout(tick, 700 + Math.floor(Math.random() * 600))
@@ -172,7 +182,7 @@ export const Playground = {
<div style={{ display: "flex", gap: "6px", "flex-wrap": "wrap" }}> <div style={{ display: "flex", gap: "6px", "flex-wrap": "wrap" }}>
{TEXTS.map((t, i) => ( {TEXTS.map((t, i) => (
<button onClick={() => setIndex(i)} style={btn(index() === i)}> <button onClick={() => setState("index", i)} style={btn(index() === i)}>
{t ?? "(none)"} {t ?? "(none)"}
</button> </button>
))} ))}
@@ -188,7 +198,7 @@ export const Playground = {
<button onClick={toggleCycle} style={btn(cycling())}> <button onClick={toggleCycle} style={btn(cycling())}>
{cycling() ? "Stop cycle" : "Auto cycle"} {cycling() ? "Stop cycle" : "Auto cycle"}
</button> </button>
<button onClick={() => setGrowOnly((v) => !v)} style={btn(growOnly())}> <button onClick={() => setState("growOnly", (value) => !value)} style={btn(growOnly())}>
{growOnly() ? "growOnly: on" : "growOnly: off"} {growOnly() ? "growOnly: on" : "growOnly: off"}
</button> </button>
</div> </div>
@@ -204,7 +214,7 @@ export const Playground = {
max="40" max="40"
step="1" step="1"
value={hybridEdge()} value={hybridEdge()}
onInput={(e) => setHybridEdge(e.currentTarget.valueAsNumber)} onInput={(e) => setState("hybridEdge", e.currentTarget.valueAsNumber)}
style={{ flex: 1 }} style={{ flex: 1 }}
/> />
<span style={{ width: "60px", "text-align": "right", "font-size": "12px" }}>{hybridEdge()}%</span> <span style={{ width: "60px", "text-align": "right", "font-size": "12px" }}>{hybridEdge()}%</span>
@@ -218,7 +228,7 @@ export const Playground = {
max="40" max="40"
step="1" step="1"
value={hybridTravel()} value={hybridTravel()}
onInput={(e) => setHybridTravel(e.currentTarget.valueAsNumber)} onInput={(e) => setState("hybridTravel", e.currentTarget.valueAsNumber)}
style={{ flex: 1 }} style={{ flex: 1 }}
/> />
<span style={{ width: "60px", "text-align": "right", "font-size": "12px" }}>{hybridTravel()}px</span> <span style={{ width: "60px", "text-align": "right", "font-size": "12px" }}>{hybridTravel()}px</span>
@@ -234,7 +244,7 @@ export const Playground = {
max="1400" max="1400"
step="10" step="10"
value={duration()} value={duration()}
onInput={(e) => setDuration(e.currentTarget.valueAsNumber)} onInput={(e) => setState("duration", e.currentTarget.valueAsNumber)}
style={{ flex: 1 }} style={{ flex: 1 }}
/> />
<span style={{ width: "60px", "text-align": "right", "font-size": "12px" }}>{duration()}ms</span> <span style={{ width: "60px", "text-align": "right", "font-size": "12px" }}>{duration()}ms</span>
@@ -248,7 +258,7 @@ export const Playground = {
max="2" max="2"
step="0.01" step="0.01"
value={bounce()} value={bounce()}
onInput={(e) => setBounce(e.currentTarget.valueAsNumber)} onInput={(e) => setState("bounce", e.currentTarget.valueAsNumber)}
style={{ flex: 1 }} style={{ flex: 1 }}
/> />
<span style={{ width: "60px", "text-align": "right", "font-size": "12px" }}>{bounce().toFixed(2)}</span> <span style={{ width: "60px", "text-align": "right", "font-size": "12px" }}>{bounce().toFixed(2)}</span>
@@ -262,7 +272,7 @@ export const Playground = {
max="1.5" max="1.5"
step="0.01" step="0.01"
value={bounceSoft()} value={bounceSoft()}
onInput={(e) => setBounceSoft(e.currentTarget.valueAsNumber)} onInput={(e) => setState("bounceSoft", e.currentTarget.valueAsNumber)}
style={{ flex: 1 }} style={{ flex: 1 }}
/> />
<span style={{ width: "60px", "text-align": "right", "font-size": "12px" }}>{bounceSoft().toFixed(2)}</span> <span style={{ width: "60px", "text-align": "right", "font-size": "12px" }}>{bounceSoft().toFixed(2)}</span>
@@ -280,7 +290,7 @@ export const Playground = {
max="40" max="40"
step="1" step="1"
value={edge()} value={edge()}
onInput={(e) => setEdge(e.currentTarget.valueAsNumber)} onInput={(e) => setState("edge", e.currentTarget.valueAsNumber)}
style={{ flex: 1 }} style={{ flex: 1 }}
/> />
<span style={{ width: "60px", "text-align": "right", "font-size": "12px" }}>{edge()}%</span> <span style={{ width: "60px", "text-align": "right", "font-size": "12px" }}>{edge()}%</span>
@@ -294,7 +304,7 @@ export const Playground = {
max="16" max="16"
step="1" step="1"
value={revealTravel()} value={revealTravel()}
onInput={(e) => setRevealTravel(e.currentTarget.valueAsNumber)} onInput={(e) => setState("revealTravel", e.currentTarget.valueAsNumber)}
style={{ flex: 1 }} style={{ flex: 1 }}
/> />
<span style={{ width: "60px", "text-align": "right", "font-size": "12px" }}>{revealTravel()}px</span> <span style={{ width: "60px", "text-align": "right", "font-size": "12px" }}>{revealTravel()}px</span>

View File

@@ -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) => { const px = (value: number | string | undefined, fallback: number) => {
if (typeof value === "number") return `${value}px` if (typeof value === "number") return `${value}px`
@@ -30,11 +31,18 @@ export function TextReveal(props: {
growOnly?: boolean growOnly?: boolean
truncate?: boolean truncate?: boolean
}) { }) {
const [cur, setCur] = createSignal(props.text) const [state, setState] = createStore({
const [old, setOld] = createSignal<string | undefined>() cur: props.text,
const [width, setWidth] = createSignal("auto") old: undefined as string | undefined,
const [ready, setReady] = createSignal(false) width: "auto",
const [swapping, setSwapping] = createSignal(false) 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 inRef: HTMLSpanElement | undefined
let outRef: HTMLSpanElement | undefined let outRef: HTMLSpanElement | undefined
let rootRef: HTMLSpanElement | undefined let rootRef: HTMLSpanElement | undefined
@@ -49,7 +57,7 @@ export function TextReveal(props: {
const prev = Number.parseFloat(width()) const prev = Number.parseFloat(width())
if (Number.isFinite(prev) && next <= prev) return if (Number.isFinite(prev) && next <= prev) return
} }
setWidth(`${next}px`) setState("width", `${next}px`)
} }
createEffect( createEffect(
@@ -58,25 +66,25 @@ export function TextReveal(props: {
(next, prev) => { (next, prev) => {
if (next === prev) return if (next === prev) return
if (typeof next === "string" && typeof prev === "string" && next.startsWith(prev)) { if (typeof next === "string" && typeof prev === "string" && next.startsWith(prev)) {
setCur(next) setState("cur", next)
widen(win()) widen(win())
return return
} }
setSwapping(true) setState("swapping", true)
setOld(prev) setState("old", prev)
setCur(next) setState("cur", next)
if (typeof requestAnimationFrame !== "function") { if (typeof requestAnimationFrame !== "function") {
widen(Math.max(win(), wout())) widen(Math.max(win(), wout()))
rootRef?.offsetHeight rootRef?.offsetHeight
setSwapping(false) setState("swapping", false)
return return
} }
if (frame !== undefined && typeof cancelAnimationFrame === "function") cancelAnimationFrame(frame) if (frame !== undefined && typeof cancelAnimationFrame === "function") cancelAnimationFrame(frame)
frame = requestAnimationFrame(() => { frame = requestAnimationFrame(() => {
widen(Math.max(win(), wout())) widen(Math.max(win(), wout()))
rootRef?.offsetHeight rootRef?.offsetHeight
setSwapping(false) setState("swapping", false)
frame = undefined frame = undefined
}) })
}, },
@@ -87,16 +95,16 @@ export function TextReveal(props: {
widen(win()) widen(win())
const fonts = typeof document !== "undefined" ? document.fonts : undefined const fonts = typeof document !== "undefined" ? document.fonts : undefined
if (typeof requestAnimationFrame !== "function") { if (typeof requestAnimationFrame !== "function") {
setReady(true) setState("ready", true)
return return
} }
if (!fonts) { if (!fonts) {
requestAnimationFrame(() => setReady(true)) requestAnimationFrame(() => setState("ready", true))
return return
} }
fonts.ready.finally(() => { fonts.ready.finally(() => {
widen(win()) widen(win())
requestAnimationFrame(() => setReady(true)) requestAnimationFrame(() => setState("ready", true))
}) })
}) })

View File

@@ -1,5 +1,6 @@
// @ts-nocheck // @ts-nocheck
import { createEffect, createSignal, onCleanup, onMount } from "solid-js" import { createEffect, createSignal, onCleanup, onMount } from "solid-js"
import { createStore } from "solid-js/store"
import { useSpring } from "./motion-spring" import { useSpring } from "./motion-spring"
import { TextStrikethrough } from "./text-strikethrough" import { TextStrikethrough } from "./text-strikethrough"
@@ -130,12 +131,16 @@ function VariantF(props: { active: boolean; text: string }) {
) )
let baseRef: HTMLSpanElement | undefined let baseRef: HTMLSpanElement | undefined
let containerRef: HTMLSpanElement | undefined let containerRef: HTMLSpanElement | undefined
const [textWidth, setTextWidth] = createSignal(0) const [state, setState] = createStore({
const [containerWidth, setContainerWidth] = createSignal(0) textWidth: 0,
containerWidth: 0,
})
const textWidth = () => state.textWidth
const containerWidth = () => state.containerWidth
const measure = () => { const measure = () => {
if (baseRef) setTextWidth(baseRef.scrollWidth) if (baseRef) setState("textWidth", baseRef.scrollWidth)
if (containerRef) setContainerWidth(containerRef.offsetWidth) if (containerRef) setState("containerWidth", containerRef.offsetWidth)
} }
onMount(measure) onMount(measure)

View File

@@ -1,5 +1,6 @@
import type { JSX } from "solid-js" 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" import { useSpring } from "./motion-spring"
export function TextStrikethrough(props: { export function TextStrikethrough(props: {
@@ -19,12 +20,16 @@ export function TextStrikethrough(props: {
let baseRef: HTMLSpanElement | undefined let baseRef: HTMLSpanElement | undefined
let containerRef: HTMLSpanElement | undefined let containerRef: HTMLSpanElement | undefined
const [textWidth, setTextWidth] = createSignal(0) const [state, setState] = createStore({
const [containerWidth, setContainerWidth] = createSignal(0) textWidth: 0,
containerWidth: 0,
})
const textWidth = () => state.textWidth
const containerWidth = () => state.containerWidth
const measure = () => { const measure = () => {
if (baseRef) setTextWidth(baseRef.scrollWidth) if (baseRef) setState("textWidth", baseRef.scrollWidth)
if (containerRef) setContainerWidth(containerRef.offsetWidth) if (containerRef) setState("containerWidth", containerRef.offsetWidth)
} }
onMount(measure) onMount(measure)

View File

@@ -1,5 +1,6 @@
// @ts-nocheck // @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 { TextShimmer } from "./text-shimmer"
import { TextReveal } from "./text-reveal" import { TextReveal } from "./text-reveal"
@@ -375,11 +376,18 @@ input[type="range"].heading-slider::-webkit-slider-thumb {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function AnimatedHeading(props) { function AnimatedHeading(props) {
const [current, setCurrent] = createSignal(props.text) const [state, setState] = createStore({
const [leaving, setLeaving] = createSignal(undefined) current: props.text,
const [width, setWidth] = createSignal("auto") leaving: undefined,
const [ready, setReady] = createSignal(false) width: "auto",
const [swapping, setSwapping] = createSignal(false) 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 enterRef
let leaveRef let leaveRef
let containerRef let containerRef
@@ -391,16 +399,16 @@ function AnimatedHeading(props) {
if (px <= 0) return if (px <= 0) return
const w = Number.parseFloat(width()) const w = Number.parseFloat(width())
if (Number.isFinite(w) && px <= w) return if (Number.isFinite(w) && px <= w) return
setWidth(`${px}px`) setState("width", `${px}px`)
} }
const measure = () => { const measure = () => {
if (!current()) { if (!current()) {
setWidth("0px") setState("width", "0px")
return return
} }
const px = measureEnter() const px = measureEnter()
if (px > 0) setWidth(`${px}px`) if (px > 0) setState("width", `${px}px`)
} }
createEffect( createEffect(
@@ -408,9 +416,9 @@ function AnimatedHeading(props) {
() => props.text, () => props.text,
(next, prev) => { (next, prev) => {
if (next === prev) return if (next === prev) return
setSwapping(true) setState("swapping", true)
setLeaving(prev) setState("leaving", prev)
setCurrent(next) setState("current", next)
if (frame) cancelAnimationFrame(frame) if (frame) cancelAnimationFrame(frame)
frame = requestAnimationFrame(() => { frame = requestAnimationFrame(() => {
@@ -420,10 +428,10 @@ function AnimatedHeading(props) {
const leaveW = measureLeave() const leaveW = measureLeave()
widen(Math.max(enterW, leaveW)) widen(Math.max(enterW, leaveW))
containerRef?.offsetHeight // reflow with max width + swap positions containerRef?.offsetHeight // reflow with max width + swap positions
setSwapping(false) setState("swapping", false)
} else { } else {
containerRef?.offsetHeight containerRef?.offsetHeight
setSwapping(false) setState("swapping", false)
measure() measure()
} }
frame = undefined frame = undefined
@@ -436,7 +444,7 @@ function AnimatedHeading(props) {
measure() measure()
document.fonts?.ready.finally(() => { document.fonts?.ready.finally(() => {
measure() measure()
requestAnimationFrame(() => setReady(true)) requestAnimationFrame(() => setState("ready", true))
}) })
}) })
@@ -552,47 +560,56 @@ const VARIANTS: { key: string; label: string }[] = []
export const Playground = { export const Playground = {
render: () => { render: () => {
const [heading, setHeading] = createSignal(HEADINGS[0]) const [state, setState] = createStore({
const [headingIndex, setHeadingIndex] = createSignal(0) heading: HEADINGS[0],
const [active, setActive] = createSignal(true) headingIndex: 0,
const [cycling, setCycling] = createSignal(false) 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 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 = () => { const nextHeading = () => {
setHeadingIndex((i) => { const next = (headingIndex() + 1) % HEADINGS.length
const next = (i + 1) % HEADINGS.length setState("headingIndex", next)
setHeading(HEADINGS[next]) setState("heading", HEADINGS[next])
return next
})
} }
const prevHeading = () => { const prevHeading = () => {
setHeadingIndex((i) => { const prev = (headingIndex() - 1 + HEADINGS.length) % HEADINGS.length
const prev = (i - 1 + HEADINGS.length) % HEADINGS.length setState("headingIndex", prev)
setHeading(HEADINGS[prev]) setState("heading", HEADINGS[prev])
return prev
})
} }
const toggleCycling = () => { const toggleCycling = () => {
if (cycling()) { if (cycling()) {
clearTimeout(cycleTimer) clearTimeout(cycleTimer)
cycleTimer = undefined cycleTimer = undefined
setCycling(false) setState("cycling", false)
return return
} }
setCycling(true) setState("cycling", true)
const tick = () => { const tick = () => {
if (!cycling()) return if (!cycling()) return
nextHeading() nextHeading()
@@ -602,11 +619,11 @@ export const Playground = {
} }
const clearHeading = () => { const clearHeading = () => {
setHeading(undefined) setState("heading", undefined)
if (cycling()) { if (cycling()) {
clearTimeout(cycleTimer) clearTimeout(cycleTimer)
cycleTimer = undefined cycleTimer = undefined
setCycling(false) setState("cycling", false)
} }
} }
@@ -686,7 +703,7 @@ export const Playground = {
max={1400} max={1400}
step={50} step={50}
value={duration()} value={duration()}
onInput={(e) => setDuration(Number(e.currentTarget.value))} onInput={(e) => setState("duration", Number(e.currentTarget.value))}
/> />
<span style={sliderValue}>{duration()}ms</span> <span style={sliderValue}>{duration()}ms</span>
</div> </div>
@@ -700,7 +717,7 @@ export const Playground = {
max={16} max={16}
step={0.5} step={0.5}
value={blur()} value={blur()}
onInput={(e) => setBlur(Number(e.currentTarget.value))} onInput={(e) => setState("blur", Number(e.currentTarget.value))}
/> />
<span style={sliderValue}>{blur()}px</span> <span style={sliderValue}>{blur()}px</span>
</div> </div>
@@ -714,7 +731,7 @@ export const Playground = {
max={120} max={120}
step={1} step={1}
value={travel()} value={travel()}
onInput={(e) => setTravel(Number(e.currentTarget.value))} onInput={(e) => setState("travel", Number(e.currentTarget.value))}
/> />
<span style={sliderValue}>{travel()}px</span> <span style={sliderValue}>{travel()}px</span>
</div> </div>
@@ -728,7 +745,7 @@ export const Playground = {
max={2.2} max={2.2}
step={0.05} step={0.05}
value={bounce()} value={bounce()}
onInput={(e) => setBounce(Number(e.currentTarget.value))} onInput={(e) => setState("bounce", Number(e.currentTarget.value))}
/> />
<span style={sliderValue}> <span style={sliderValue}>
{bounce().toFixed(2)} {bounce() <= 1.05 ? "(none)" : bounce() >= 1.9 ? "(heavy)" : ""} {bounce().toFixed(2)} {bounce() <= 1.05 ? "(none)" : bounce() >= 1.9 ? "(heavy)" : ""}
@@ -744,7 +761,7 @@ export const Playground = {
max={50} max={50}
step={1} step={1}
value={maskSize()} value={maskSize()}
onInput={(e) => setMaskSize(Number(e.currentTarget.value))} onInput={(e) => setState("maskSize", Number(e.currentTarget.value))}
/> />
<span style={sliderValue}> <span style={sliderValue}>
{maskSize()}px {maskSize() === 0 ? "(hard)" : ""} {maskSize()}px {maskSize() === 0 ? "(hard)" : ""}
@@ -760,7 +777,7 @@ export const Playground = {
max={60} max={60}
step={1} step={1}
value={maskPad()} value={maskPad()}
onInput={(e) => setMaskPad(Number(e.currentTarget.value))} onInput={(e) => setState("maskPad", Number(e.currentTarget.value))}
/> />
<span style={sliderValue}>{maskPad()}px</span> <span style={sliderValue}>{maskPad()}px</span>
</div> </div>
@@ -774,7 +791,7 @@ export const Playground = {
max={80} max={80}
step={1} step={1}
value={maskHeight()} value={maskHeight()}
onInput={(e) => setMaskHeight(Number(e.currentTarget.value))} onInput={(e) => setState("maskHeight", Number(e.currentTarget.value))}
/> />
<span style={sliderValue}>{maskHeight()}px</span> <span style={sliderValue}>{maskHeight()}px</span>
</div> </div>
@@ -795,13 +812,13 @@ export const Playground = {
<button onClick={clearHeading} style={btn()}> <button onClick={clearHeading} style={btn()}>
Clear Clear
</button> </button>
<button onClick={() => setActive((v) => !v)} style={smallBtn(active())}> <button onClick={() => setState("active", (value) => !value)} style={smallBtn(active())}>
{active() ? "Shimmer: on" : "Shimmer: off"} {active() ? "Shimmer: on" : "Shimmer: off"}
</button> </button>
<button onClick={() => setDebug((v) => !v)} style={smallBtn(debug())}> <button onClick={() => setState("debug", (value) => !value)} style={smallBtn(debug())}>
{debug() ? "Debug mask: on" : "Debug mask"} {debug() ? "Debug mask: on" : "Debug mask"}
</button> </button>
<button onClick={() => setOdoBlur((v) => !v)} style={smallBtn(odoBlur())}> <button onClick={() => setState("odoBlur", (value) => !value)} style={smallBtn(odoBlur())}>
{odoBlur() ? "Odo blur: on" : "Odo blur"} {odoBlur() ? "Odo blur: on" : "Odo blur"}
</button> </button>
</div> </div>
@@ -810,8 +827,8 @@ export const Playground = {
{HEADINGS.map((h, i) => ( {HEADINGS.map((h, i) => (
<button <button
onClick={() => { onClick={() => {
setHeadingIndex(i) setState("headingIndex", i)
setHeading(h) setState("heading", h)
}} }}
style={smallBtn(headingIndex() === i)} style={smallBtn(headingIndex() === i)}
> >

View File

@@ -1,5 +1,6 @@
// @ts-nocheck // @ts-nocheck
import { createEffect, createMemo, createSignal, onCleanup } from "solid-js" import { createEffect, createMemo, onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
import type { Todo } from "@opencode-ai/sdk/v2" import type { Todo } from "@opencode-ai/sdk/v2"
import { useGlobalSync } from "@/context/global-sync" import { useGlobalSync } from "@/context/global-sync"
import { SessionComposerRegion, createSessionComposerState } from "@/pages/session/composer" import { SessionComposerRegion, createSessionComposerState } from "@/pages/session/composer"
@@ -129,24 +130,44 @@ const css = `
export const Playground = { export const Playground = {
render: () => { render: () => {
const global = useGlobalSync() const global = useGlobalSync()
const [open, setOpen] = createSignal(true) const [cfg, setCfg] = createStore({
const [step, setStep] = createSignal(1) open: true,
const [dockOpenDuration, setDockOpenDuration] = createSignal(0.3) step: 1,
const [dockOpenBounce, setDockOpenBounce] = createSignal(0) dockOpenDuration: 0.3,
const [dockCloseDuration, setDockCloseDuration] = createSignal(0.3) dockOpenBounce: 0,
const [dockCloseBounce, setDockCloseBounce] = createSignal(0) dockCloseDuration: 0.3,
const [drawerExpandDuration, setDrawerExpandDuration] = createSignal(0.3) dockCloseBounce: 0,
const [drawerExpandBounce, setDrawerExpandBounce] = createSignal(0) drawerExpandDuration: 0.3,
const [drawerCollapseDuration, setDrawerCollapseDuration] = createSignal(0.3) drawerExpandBounce: 0,
const [drawerCollapseBounce, setDrawerCollapseBounce] = createSignal(0) drawerCollapseDuration: 0.3,
const [subtitleDuration, setSubtitleDuration] = createSignal(600) drawerCollapseBounce: 0,
const [subtitleAuto, setSubtitleAuto] = createSignal(true) subtitleDuration: 600,
const [subtitleTravel, setSubtitleTravel] = createSignal(25) subtitleAuto: true,
const [subtitleEdge, setSubtitleEdge] = createSignal(17) subtitleTravel: 25,
const [countDuration, setCountDuration] = createSignal(600) subtitleEdge: 17,
const [countMask, setCountMask] = createSignal(18) countDuration: 600,
const [countMaskHeight, setCountMaskHeight] = createSignal(0) countMask: 18,
const [countWidthDuration, setCountWidthDuration] = createSignal(560) countMaskHeight: 0,
countWidthDuration: 560,
})
const open = () => cfg.open
const step = () => cfg.step
const dockOpenDuration = () => cfg.dockOpenDuration
const dockOpenBounce = () => cfg.dockOpenBounce
const dockCloseDuration = () => cfg.dockCloseDuration
const dockCloseBounce = () => cfg.dockCloseBounce
const drawerExpandDuration = () => cfg.drawerExpandDuration
const drawerExpandBounce = () => cfg.drawerExpandBounce
const drawerCollapseDuration = () => cfg.drawerCollapseDuration
const drawerCollapseBounce = () => cfg.drawerCollapseBounce
const subtitleDuration = () => cfg.subtitleDuration
const subtitleAuto = () => cfg.subtitleAuto
const subtitleTravel = () => cfg.subtitleTravel
const subtitleEdge = () => cfg.subtitleEdge
const countDuration = () => cfg.countDuration
const countMask = () => cfg.countMask
const countMaskHeight = () => cfg.countMaskHeight
const countWidthDuration = () => cfg.countWidthDuration
const state = createSessionComposerState({ closeMs: () => Math.round(dockCloseDuration() * 1000) }) const state = createSessionComposerState({ closeMs: () => Math.round(dockCloseDuration() * 1000) })
let frame let frame
let composerRef let composerRef
@@ -187,7 +208,7 @@ export const Playground = {
const openDock = () => { const openDock = () => {
clear() clear()
setOpen(true) setCfg("open", true)
frame = requestAnimationFrame(() => { frame = requestAnimationFrame(() => {
pin() pin()
frame = undefined frame = undefined
@@ -196,7 +217,7 @@ export const Playground = {
const closeDock = () => { const closeDock = () => {
clear() clear()
setOpen(false) setCfg("open", false)
} }
const dockOpen = () => open() const dockOpen = () => open()
@@ -223,7 +244,7 @@ export const Playground = {
} }
const cycle = () => { const cycle = () => {
setStep((value) => (value + 1) % 4) setCfg("step", (value) => (value + 1) % 4)
} }
onCleanup(clear) onCleanup(clear)
@@ -289,7 +310,7 @@ export const Playground = {
Cycle progress ({step()}/3 done) Cycle progress ({step()}/3 done)
</button> </button>
{[0, 1, 2, 3].map((value) => ( {[0, 1, 2, 3].map((value) => (
<button onClick={() => setStep(value)} style={btn(step() === value)}> <button onClick={() => setCfg("step", value)} style={btn(step() === value)}>
{value} done {value} done
</button> </button>
))} ))}
@@ -307,7 +328,7 @@ export const Playground = {
max="1" max="1"
step="0.01" step="0.01"
value={dockOpenDuration()} value={dockOpenDuration()}
onInput={(event) => setDockOpenDuration(event.currentTarget.valueAsNumber)} onInput={(event) => setCfg("dockOpenDuration", event.currentTarget.valueAsNumber)}
style={{ flex: 1 }} style={{ flex: 1 }}
/> />
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}> <span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>
@@ -324,7 +345,7 @@ export const Playground = {
max="1" max="1"
step="0.01" step="0.01"
value={dockOpenBounce()} value={dockOpenBounce()}
onInput={(event) => setDockOpenBounce(event.currentTarget.valueAsNumber)} onInput={(event) => setCfg("dockOpenBounce", event.currentTarget.valueAsNumber)}
style={{ flex: 1 }} style={{ flex: 1 }}
/> />
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}> <span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>
@@ -345,7 +366,7 @@ export const Playground = {
max="1" max="1"
step="0.01" step="0.01"
value={dockCloseDuration()} value={dockCloseDuration()}
onInput={(event) => setDockCloseDuration(event.currentTarget.valueAsNumber)} onInput={(event) => setCfg("dockCloseDuration", event.currentTarget.valueAsNumber)}
style={{ flex: 1 }} style={{ flex: 1 }}
/> />
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}> <span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>
@@ -362,7 +383,7 @@ export const Playground = {
max="1" max="1"
step="0.01" step="0.01"
value={dockCloseBounce()} value={dockCloseBounce()}
onInput={(event) => setDockCloseBounce(event.currentTarget.valueAsNumber)} onInput={(event) => setCfg("dockCloseBounce", event.currentTarget.valueAsNumber)}
style={{ flex: 1 }} style={{ flex: 1 }}
/> />
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}> <span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>
@@ -383,7 +404,7 @@ export const Playground = {
max="1" max="1"
step="0.01" step="0.01"
value={drawerExpandDuration()} value={drawerExpandDuration()}
onInput={(event) => setDrawerExpandDuration(event.currentTarget.valueAsNumber)} onInput={(event) => setCfg("drawerExpandDuration", event.currentTarget.valueAsNumber)}
style={{ flex: 1 }} style={{ flex: 1 }}
/> />
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}> <span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>
@@ -400,7 +421,7 @@ export const Playground = {
max="1" max="1"
step="0.01" step="0.01"
value={drawerExpandBounce()} value={drawerExpandBounce()}
onInput={(event) => setDrawerExpandBounce(event.currentTarget.valueAsNumber)} onInput={(event) => setCfg("drawerExpandBounce", event.currentTarget.valueAsNumber)}
style={{ flex: 1 }} style={{ flex: 1 }}
/> />
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}> <span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>
@@ -421,7 +442,7 @@ export const Playground = {
max="1" max="1"
step="0.01" step="0.01"
value={drawerCollapseDuration()} value={drawerCollapseDuration()}
onInput={(event) => setDrawerCollapseDuration(event.currentTarget.valueAsNumber)} onInput={(event) => setCfg("drawerCollapseDuration", event.currentTarget.valueAsNumber)}
style={{ flex: 1 }} style={{ flex: 1 }}
/> />
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}> <span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>
@@ -438,7 +459,7 @@ export const Playground = {
max="1" max="1"
step="0.01" step="0.01"
value={drawerCollapseBounce()} value={drawerCollapseBounce()}
onInput={(event) => setDrawerCollapseBounce(event.currentTarget.valueAsNumber)} onInput={(event) => setCfg("drawerCollapseBounce", event.currentTarget.valueAsNumber)}
style={{ flex: 1 }} style={{ flex: 1 }}
/> />
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}> <span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>
@@ -459,7 +480,7 @@ export const Playground = {
max="1400" max="1400"
step="10" step="10"
value={subtitleDuration()} value={subtitleDuration()}
onInput={(event) => setSubtitleDuration(event.currentTarget.valueAsNumber)} onInput={(event) => setCfg("subtitleDuration", event.currentTarget.valueAsNumber)}
style={{ flex: 1 }} style={{ flex: 1 }}
/> />
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}> <span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>
@@ -473,7 +494,7 @@ export const Playground = {
<input <input
type="checkbox" type="checkbox"
checked={subtitleAuto()} checked={subtitleAuto()}
onInput={(event) => setSubtitleAuto(event.currentTarget.checked)} onInput={(event) => setCfg("subtitleAuto", event.currentTarget.checked)}
/> />
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}> <span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>
{subtitleAuto() ? "on" : "off"} {subtitleAuto() ? "on" : "off"}
@@ -489,7 +510,7 @@ export const Playground = {
max="40" max="40"
step="1" step="1"
value={subtitleTravel()} value={subtitleTravel()}
onInput={(event) => setSubtitleTravel(event.currentTarget.valueAsNumber)} onInput={(event) => setCfg("subtitleTravel", event.currentTarget.valueAsNumber)}
style={{ flex: 1 }} style={{ flex: 1 }}
/> />
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>{subtitleTravel()}px</span> <span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>{subtitleTravel()}px</span>
@@ -504,7 +525,7 @@ export const Playground = {
max="40" max="40"
step="1" step="1"
value={subtitleEdge()} value={subtitleEdge()}
onInput={(event) => setSubtitleEdge(event.currentTarget.valueAsNumber)} onInput={(event) => setCfg("subtitleEdge", event.currentTarget.valueAsNumber)}
style={{ flex: 1 }} style={{ flex: 1 }}
/> />
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>{subtitleEdge()}%</span> <span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>{subtitleEdge()}%</span>
@@ -523,7 +544,7 @@ export const Playground = {
max="1400" max="1400"
step="10" step="10"
value={countDuration()} value={countDuration()}
onInput={(event) => setCountDuration(event.currentTarget.valueAsNumber)} onInput={(event) => setCfg("countDuration", event.currentTarget.valueAsNumber)}
style={{ flex: 1 }} style={{ flex: 1 }}
/> />
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}> <span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>
@@ -540,7 +561,7 @@ export const Playground = {
max="40" max="40"
step="1" step="1"
value={countMask()} value={countMask()}
onInput={(event) => setCountMask(event.currentTarget.valueAsNumber)} onInput={(event) => setCfg("countMask", event.currentTarget.valueAsNumber)}
style={{ flex: 1 }} style={{ flex: 1 }}
/> />
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>{countMask()}%</span> <span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>{countMask()}%</span>
@@ -555,7 +576,7 @@ export const Playground = {
max="14" max="14"
step="1" step="1"
value={countMaskHeight()} value={countMaskHeight()}
onInput={(event) => setCountMaskHeight(event.currentTarget.valueAsNumber)} onInput={(event) => setCfg("countMaskHeight", event.currentTarget.valueAsNumber)}
style={{ flex: 1 }} style={{ flex: 1 }}
/> />
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>{countMaskHeight()}px</span> <span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>{countMaskHeight()}px</span>
@@ -570,7 +591,7 @@ export const Playground = {
max="1200" max="1200"
step="10" step="10"
value={countWidthDuration()} value={countWidthDuration()}
onInput={(event) => setCountWidthDuration(event.currentTarget.valueAsNumber)} onInput={(event) => setCfg("countWidthDuration", event.currentTarget.valueAsNumber)}
style={{ flex: 1 }} style={{ flex: 1 }}
/> />
<span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}> <span style={{ width: "64px", "text-align": "right", "font-size": "13px" }}>

View File

@@ -1,5 +1,6 @@
// @ts-nocheck // @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 { AnimatedCountList, type CountItem } from "./tool-count-summary"
import { ToolStatusTitle } from "./tool-status-title" import { ToolStatusTitle } from "./tool-status-title"
@@ -57,11 +58,18 @@ const smallBtn = (active?: boolean) =>
export const Playground = { export const Playground = {
render: () => { render: () => {
const [reads, setReads] = createSignal(0) const [state, setState] = createStore({
const [searches, setSearches] = createSignal(0) reads: 0,
const [lists, setLists] = createSignal(0) searches: 0,
const [active, setActive] = createSignal(false) lists: 0,
const [reducedMotion, setReducedMotion] = createSignal(false) 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<typeof setTimeout>[] = [] let timeouts: ReturnType<typeof setTimeout>[] = []
@@ -74,10 +82,10 @@ export const Playground = {
const startSim = () => { const startSim = () => {
clearAll() clearAll()
setReads(0) setState("reads", 0)
setSearches(0) setState("searches", 0)
setLists(0) setState("lists", 0)
setActive(true) setState("active", true)
const steps = rand(3, 10) const steps = rand(3, 10)
let elapsed = 0 let elapsed = 0
@@ -86,27 +94,27 @@ export const Playground = {
elapsed += delay elapsed += delay
const t = setTimeout(() => { const t = setTimeout(() => {
const pick = rand(0, 2) const pick = rand(0, 2)
if (pick === 0) setReads((n) => n + 1) if (pick === 0) setState("reads", (value) => value + 1)
else if (pick === 1) setSearches((n) => n + 1) else if (pick === 1) setState("searches", (value) => value + 1)
else setLists((n) => n + 1) else setState("lists", (value) => value + 1)
}, elapsed) }, elapsed)
timeouts.push(t) timeouts.push(t)
} }
const end = setTimeout(() => setActive(false), elapsed + 100) const end = setTimeout(() => setState("active", false), elapsed + 100)
timeouts.push(end) timeouts.push(end)
} }
const stopSim = () => { const stopSim = () => {
clearAll() clearAll()
setActive(false) setState("active", false)
} }
const reset = () => { const reset = () => {
stopSim() stopSim()
setReads(0) setState("reads", 0)
setSearches(0) setState("searches", 0)
setLists(0) setState("lists", 0)
} }
const items = (): CountItem[] => [ const items = (): CountItem[] => [
@@ -164,19 +172,19 @@ export const Playground = {
<button onClick={reset} style={btn()}> <button onClick={reset} style={btn()}>
Reset Reset
</button> </button>
<button onClick={() => setReducedMotion((v) => !v)} style={smallBtn(reducedMotion())}> <button onClick={() => setState("reducedMotion", (value) => !value)} style={smallBtn(reducedMotion())}>
{reducedMotion() ? "Motion: reduced" : "Motion: normal"} {reducedMotion() ? "Motion: reduced" : "Motion: normal"}
</button> </button>
</div> </div>
<div style={{ display: "flex", gap: "8px", "flex-wrap": "wrap" }}> <div style={{ display: "flex", gap: "8px", "flex-wrap": "wrap" }}>
<button onClick={() => setReads((n) => n + 1)} style={smallBtn()}> <button onClick={() => setState("reads", (value) => value + 1)} style={smallBtn()}>
+ read + read
</button> </button>
<button onClick={() => setSearches((n) => n + 1)} style={smallBtn()}> <button onClick={() => setState("searches", (value) => value + 1)} style={smallBtn()}>
+ search + search
</button> </button>
<button onClick={() => setLists((n) => n + 1)} style={smallBtn()}> <button onClick={() => setState("lists", (value) => value + 1)} style={smallBtn()}>
+ list + list
</button> </button>
</div> </div>

View File

@@ -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 { Card, CardDescription } from "./card"
import { Collapsible } from "./collapsible" import { Collapsible } from "./collapsible"
import { Icon } from "./icon" import { Icon } from "./icon"
@@ -16,8 +17,12 @@ export interface ToolErrorCardProps extends Omit<ComponentProps<typeof Card>, "c
export function ToolErrorCard(props: ToolErrorCardProps) { export function ToolErrorCard(props: ToolErrorCardProps) {
const i18n = useI18n() const i18n = useI18n()
const [open, setOpen] = createSignal(props.defaultOpen ?? false) const [state, setState] = createStore({
const [copied, setCopied] = createSignal(false) 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 [split, rest] = splitProps(props, ["tool", "error", "defaultOpen", "subtitle", "href"])
const name = createMemo(() => { const name = createMemo(() => {
const map: Record<string, string> = { const map: Record<string, string> = {
@@ -65,13 +70,18 @@ export function ToolErrorCard(props: ToolErrorCardProps) {
const text = cleaned() const text = cleaned()
if (!text) return if (!text) return
await navigator.clipboard.writeText(text) await navigator.clipboard.writeText(text)
setCopied(true) setState("copied", true)
setTimeout(() => setCopied(false), 2000) setTimeout(() => setState("copied", false), 2000)
} }
return ( return (
<Card {...rest} data-kind="tool-error-card" data-open={open() ? "true" : "false"} variant="error"> <Card {...rest} data-kind="tool-error-card" data-open={open() ? "true" : "false"} variant="error">
<Collapsible class="tool-collapsible" data-open={open() ? "true" : "false"} open={open()} onOpenChange={setOpen}> <Collapsible
class="tool-collapsible"
data-open={open() ? "true" : "false"}
open={open()}
onOpenChange={(value) => setState("open", value)}
>
<Collapsible.Trigger> <Collapsible.Trigger>
<div data-component="tool-trigger"> <div data-component="tool-trigger">
<div data-slot="basic-tool-tool-trigger-content"> <div data-slot="basic-tool-tool-trigger-content">

View File

@@ -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" import { TextShimmer } from "./text-shimmer"
function common(active: string, done: string) { function common(active: string, done: string) {
@@ -35,8 +36,12 @@ export function ToolStatusTitle(props: {
const activeTail = createMemo(() => (suffix() ? split().active : props.activeText)) const activeTail = createMemo(() => (suffix() ? split().active : props.activeText))
const doneTail = createMemo(() => (suffix() ? split().done : props.doneText)) const doneTail = createMemo(() => (suffix() ? split().done : props.doneText))
const [width, setWidth] = createSignal("auto") const [state, setState] = createStore({
const [ready, setReady] = createSignal(false) width: "auto",
ready: false,
})
const width = () => state.width
const ready = () => state.ready
let activeRef: HTMLSpanElement | undefined let activeRef: HTMLSpanElement | undefined
let doneRef: HTMLSpanElement | undefined let doneRef: HTMLSpanElement | undefined
let frame: number | undefined let frame: number | undefined
@@ -45,7 +50,7 @@ export function ToolStatusTitle(props: {
const measure = () => { const measure = () => {
const target = props.active ? activeRef : doneRef const target = props.active ? activeRef : doneRef
const px = contentWidth(target) const px = contentWidth(target)
if (px > 0) setWidth(`${px}px`) if (px > 0) setState("width", `${px}px`)
} }
const schedule = () => { const schedule = () => {
@@ -62,13 +67,13 @@ export function ToolStatusTitle(props: {
const finish = () => { const finish = () => {
if (typeof requestAnimationFrame !== "function") { if (typeof requestAnimationFrame !== "function") {
setReady(true) setState("ready", true)
return return
} }
if (readyFrame !== undefined) cancelAnimationFrame(readyFrame) if (readyFrame !== undefined) cancelAnimationFrame(readyFrame)
readyFrame = requestAnimationFrame(() => { readyFrame = requestAnimationFrame(() => {
readyFrame = undefined readyFrame = undefined
setReady(true) setState("ready", true)
}) })
} }

View File

@@ -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 = { export type FindHost = {
element: () => HTMLElement | undefined element: () => HTMLElement | undefined
@@ -107,11 +108,18 @@ export function createFileFind(opts: CreateFileFindOptions) {
let mode: "highlights" | "overlay" = "overlay" let mode: "highlights" | "overlay" = "overlay"
let hits: Range[] = [] let hits: Range[] = []
const [open, setOpen] = createSignal(false) const [state, setState] = createStore({
const [query, setQuery] = createSignal("") open: false,
const [index, setIndex] = createSignal(0) query: "",
const [count, setCount] = createSignal(0) index: 0,
const [pos, setPos] = createSignal({ top: 8, right: 8 }) 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 = () => { const clearOverlayScroll = () => {
for (const el of overlayScroll) el.removeEventListener("scroll", scheduleOverlay) for (const el of overlayScroll) el.removeEventListener("scroll", scheduleOverlay)
@@ -200,8 +208,8 @@ export function createFileFind(opts: CreateFileFindOptions) {
clearOverlay() clearOverlay()
clearOverlayScroll() clearOverlayScroll()
hits = [] hits = []
setCount(0) setState("count", 0)
setIndex(0) setState("index", 0)
} }
const positionBar = () => { const positionBar = () => {
@@ -214,7 +222,7 @@ export function createFileFind(opts: CreateFileFindOptions) {
const title = parseFloat(getComputedStyle(root).getPropertyValue("--session-title-height")) const title = parseFloat(getComputedStyle(root).getPropertyValue("--session-title-height"))
const header = Number.isNaN(title) ? 0 : title const header = Number.isNaN(title) ? 0 : title
setPos({ setState("pos", {
top: Math.round(rect.top) + header - 4, top: Math.round(rect.top) + header - 4,
right: Math.round(window.innerWidth - rect.right) + 8, 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 const currentIndex = total ? Math.min(desired, total - 1) : 0
hits = ranges hits = ranges
setCount(total) setState("count", total)
setIndex(currentIndex) setState("index", currentIndex)
const active = ranges[currentIndex] const active = ranges[currentIndex]
if (mode === "highlights") { if (mode === "highlights") {
@@ -342,8 +350,8 @@ export function createFileFind(opts: CreateFileFindOptions) {
} }
const close = () => { const close = () => {
setOpen(false) setState("open", false)
setQuery("") setState("query", "")
clearFind() clearFind()
if (current === host) current = undefined if (current === host) current = undefined
} }
@@ -352,7 +360,7 @@ export function createFileFind(opts: CreateFileFindOptions) {
if (current && current !== host) current.close() if (current && current !== host) current.close()
current = host current = host
target = host target = host
if (!open()) setOpen(true) if (!open()) setState("open", true)
requestAnimationFrame(() => { requestAnimationFrame(() => {
apply({ scroll: true }) apply({ scroll: true })
input?.focus() input?.focus()
@@ -366,7 +374,7 @@ export function createFileFind(opts: CreateFileFindOptions) {
if (total <= 0) return if (total <= 0) return
const currentIndex = (index() + dir + total) % total const currentIndex = (index() + dir + total) % total
setIndex(currentIndex) setState("index", currentIndex)
const active = hits[currentIndex] const active = hits[currentIndex]
if (!active) return if (!active) return
@@ -449,8 +457,8 @@ export function createFileFind(opts: CreateFileFindOptions) {
input = el input = el
}, },
setQuery: (value: string) => { setQuery: (value: string) => {
setQuery(value) setState("query", value)
setIndex(0) setState("index", 0)
apply({ reset: true, scroll: true }) apply({ reset: true, scroll: true })
}, },
focus, focus,