From a10875ad17607988ba1d198a243f05087c861bd8 Mon Sep 17 00:00:00 2001 From: Laiba Sameer Date: Sat, 29 Aug 2026 14:51:47 +0000 Subject: [PATCH 1/2] dev container configuration --- .devcontainer/devcontainer-lock.json | 9 +++++++++ .devcontainer/devcontainer.json | 10 ++++++++++ 2 files changed, 19 insertions(+) create mode 100644 .devcontainer/devcontainer-lock.json create mode 100644 .devcontainer/devcontainer.json diff --git a/.devcontainer/devcontainer-lock.json b/.devcontainer/devcontainer-lock.json new file mode 100644 index 00000000..b23cef5d --- /dev/null +++ b/.devcontainer/devcontainer-lock.json @@ -0,0 +1,9 @@ +{ + "features": { + "ghcr.io/devcontainers/features/python:1": { + "version": "1.8.0", + "resolved": "ghcr.io/devcontainers/features/python@sha256:fbcad6955caeecc5ad3f7886baf652e25cba5225a6c4c2287c536de2e5607511", + "integrity": "sha256:fbcad6955caeecc5ad3f7886baf652e25cba5225a6c4c2287c536de2e5607511" + } + } +} diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..ff5c37b6 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,10 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/ubuntu +{ + "name": "Ubuntu", + "image": "mcr.microsoft.com/devcontainers/base:resolute", + "features": { + "ghcr.io/devcontainers/features/python:1": {} + }, + "postCreateCommand": "curl -fsSL https://bun.sh/install | bash && echo 'export PATH=\"$HOME/.bun/bin:$PATH\"' >> ~/.bashrc" +} \ No newline at end of file From 8d5782098b3e4bebd6d8eb9087939f7f474d95e3 Mon Sep 17 00:00:00 2001 From: Laiba Sameer Date: Fri, 4 Sep 2026 21:05:13 +0000 Subject: [PATCH 2/2] Refactor SelectV2 to reduce complexity --- .../ui/src/v2/components/select-v2-utils.ts | 5 + .../ui/src/v2/components/select-v2.test.tsx | 20 ++++ packages/ui/src/v2/components/select-v2.tsx | 96 +++++++++++-------- 3 files changed, 82 insertions(+), 39 deletions(-) create mode 100644 packages/ui/src/v2/components/select-v2-utils.ts create mode 100644 packages/ui/src/v2/components/select-v2.test.tsx diff --git a/packages/ui/src/v2/components/select-v2-utils.ts b/packages/ui/src/v2/components/select-v2-utils.ts new file mode 100644 index 00000000..46057b57 --- /dev/null +++ b/packages/ui/src/v2/components/select-v2-utils.ts @@ -0,0 +1,5 @@ +export function getSelectedValue(next: T | T[] | null): T | null { + if (next == null) return null + if (Array.isArray(next)) return next[0] ?? null + return next +} diff --git a/packages/ui/src/v2/components/select-v2.test.tsx b/packages/ui/src/v2/components/select-v2.test.tsx new file mode 100644 index 00000000..31784ef1 --- /dev/null +++ b/packages/ui/src/v2/components/select-v2.test.tsx @@ -0,0 +1,20 @@ +import { describe, expect, test } from "bun:test" +import { getSelectedValue } from "./select-v2-utils" + +describe("getSelectedValue", () => { + test("returns null when there is no selection", () => { + expect(getSelectedValue(null)).toBe(null) + }) + + test("returns the selected value", () => { + expect(getSelectedValue("apple")).toBe("apple") + }) + + test("returns the first value from an array", () => { + expect(getSelectedValue(["apple", "banana"])).toBe("apple") + }) + + test("returns null for an empty array", () => { + expect(getSelectedValue([])).toBe(null) + }) +}) diff --git a/packages/ui/src/v2/components/select-v2.tsx b/packages/ui/src/v2/components/select-v2.tsx index 57348ed3..78601ccb 100644 --- a/packages/ui/src/v2/components/select-v2.tsx +++ b/packages/ui/src/v2/components/select-v2.tsx @@ -1,6 +1,7 @@ import { Select as Kobalte } from "@kobalte/core/select" import { Show, createMemo, onCleanup, splitProps, type ComponentProps, type JSX } from "solid-js" import "./select-v2.css" +import { getSelectedValue } from "./select-v2-utils" function groupOptions(options: T[], groupBy?: (x: T) => string): { category: string; options: T[] }[] { if (!groupBy) { @@ -60,6 +61,38 @@ export type SelectV2Props = Omit< children?: (item: T) => JSX.Element valueClass?: string } +function createHighlightMover( + onHighlight: ((value: T | undefined) => void | (() => void)) | undefined, + keyFor: (item: T) => string, +) { + const state: { key?: string; cleanup?: void | (() => void) } = {} + + const stop = () => { + state.cleanup?.() + state.cleanup = undefined + state.key = undefined + } + + const move = (item: T | undefined) => { + if (!onHighlight) return + if (!item) { + stop() + return + } + const key = keyFor(item) + if (state.key === key) return + state.cleanup?.() + state.cleanup = onHighlight(item) + state.key = key + } + + return { move, stop } +} + +function handleOpenChange(open: boolean, onOpenChange: ((open: boolean) => void) | undefined, stop: () => void) { + onOpenChange?.(open) + if (!open) stop() +} export function SelectV2(props: SelectV2Props) { const [local, others] = splitProps(props, [ @@ -90,32 +123,25 @@ export function SelectV2(props: SelectV2Props) { const inline = () => (local.appearance ?? "base") === "inline" - const state: { key?: string; cleanup?: void | (() => void) } = {} - - const stop = () => { - state.cleanup?.() - state.cleanup = undefined - state.key = undefined - } - const keyFor = (item: T) => (local.value ? local.value(item) : String(item as string)) - - const move = (item: T | undefined) => { - if (!local.onHighlight) return - if (!item) { - stop() - return - } - const key = keyFor(item) - if (state.key === key) return - state.cleanup?.() - state.cleanup = local.onHighlight(item) - state.key = key - } + const { move, stop } = createHighlightMover(local.onHighlight, keyFor) onCleanup(stop) const grouped = createMemo(() => groupOptions(local.options, local.groupBy)) + const getOptionValue = (item: T) => (local.value ? local.value(item) : String(item as string)) + const getOptionLabel = (item: T) => (local.label ? local.label(item) : String(item as string)) + + const placement = local.placement ?? (inline() ? "bottom-end" : "bottom-start") + const sameWidth = local.sameWidth ?? !inline() + const flip = local.flip ?? true + const slide = local.slide ?? true + const fitViewport = local.fitViewport ?? false + + const renderSelectedValue = (selected: T | undefined) => { + if (local.label && selected != null) return local.label(selected) + return selected != null ? (selected as string) : "" + } return ( @@ -124,16 +150,16 @@ export function SelectV2(props: SelectV2Props) { allowDuplicateSelectionEvents={false} disabled={local.disabled} data-component="select-v2-root" - placement={local.placement ?? (inline() ? "bottom-end" : "bottom-start")} + placement={placement} gutter={local.gutter ?? 4} - sameWidth={local.sameWidth ?? !inline()} - flip={local.flip ?? true} - slide={local.slide ?? true} - fitViewport={local.fitViewport ?? false} + sameWidth={sameWidth} + flip={flip} + slide={slide} + fitViewport={fitViewport} value={local.current} options={grouped()} - optionValue={(x) => (local.value ? local.value(x) : String(x as string))} - optionTextValue={(x) => (local.label ? local.label(x) : String(x as string))} + optionValue={getOptionValue} + optionTextValue={getOptionLabel} optionGroupChildren="options" placeholder={local.placeholder} sectionComponent={(sectionProps) => ( @@ -164,14 +190,10 @@ export function SelectV2(props: SelectV2Props) { )} onChange={(next) => { - const v = next == null ? null : Array.isArray(next) ? ((next[0] as T) ?? null) : (next as T) - local.onSelect?.(v) + local.onSelect?.(getSelectedValue(next)) stop() }} - onOpenChange={(open) => { - local.onOpenChange?.(open) - if (!open) stop() - }} + onOpenChange={(open) => handleOpenChange(open, local.onOpenChange, stop)} > (props: SelectV2Props) { >
data-slot="select-v2-value-text" class={local.valueClass}> - {(st) => { - const selected = st.selectedOption() - if (local.label && selected != null) return local.label(selected) - return selected != null ? (selected as string) : "" - }} + {(st) => renderSelectedValue(st.selectedOption())}