| author | |
| committer | |
| log | 466d8ee5b0a003b3f735dd25e21273539b515316 |
| tree | 7eac4914d8a5fdc0506875d1504bd3466afa7a75 |
| parent | 11f40556de8a37c7f16486c23fb6bce058275557 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Julius Marminge <julius0216@outlook.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: Julius Marminge <jmarminge@gmail.com>11 files changed, 308 insertions(+), 33 deletions(-)
.plans/19-remote-endpoints-hosted-static.md-1| ... | ... | @@ -347,4 +347,3 @@ Each implementation PR should run: |
| 347 | 347 | - `bun typecheck` |
| 348 | 348 | - focused tests for changed backend/web behavior |
| 349 | 349 | - backend tests for any server-side endpoint discovery or auth changes using `bun run test`, never `bun test` |
| 350 |
apps/desktop/src/clientPersistence.test.ts+1| ... | ... | @@ -63,6 +63,7 @@ const clientSettings: ClientSettings = { |
| 63 | 63 | }, |
| 64 | 64 | sidebarProjectSortOrder: "manual", |
| 65 | 65 | sidebarThreadSortOrder: "created_at", |
| 66 | sidebarThreadPreviewCount: 6, | |
| 66 | 67 | timestampFormat: "24-hour", |
| 67 | 68 | }; |
| 68 | 69 |
apps/web/package.json+1-1| ... | ... | @@ -13,7 +13,7 @@ |
| 13 | 13 | "test:browser:install": "playwright install --with-deps chromium" |
| 14 | 14 | }, |
| 15 | 15 | "dependencies": { |
| 16 | "@base-ui/react": "^1.2.0", | |
| 16 | "@base-ui/react": "^1.4.1", | |
| 17 | 17 | "@dnd-kit/core": "^6.3.1", |
| 18 | 18 | "@dnd-kit/modifiers": "^9.0.0", |
| 19 | 19 | "@dnd-kit/sortable": "^10.0.0", |
apps/web/src/components/Sidebar.tsx+94-7| ... | ... | @@ -54,7 +54,10 @@ import { |
| 54 | 54 | } from "@t3tools/client-runtime"; |
| 55 | 55 | import { Link, useLocation, useNavigate, useParams, useRouter } from "@tanstack/react-router"; |
| 56 | 56 | import { |
| 57 | MAX_SIDEBAR_THREAD_PREVIEW_COUNT, | |
| 58 | MIN_SIDEBAR_THREAD_PREVIEW_COUNT, | |
| 57 | 59 | type SidebarProjectSortOrder, |
| 60 | type SidebarThreadPreviewCount, | |
| 58 | 61 | type SidebarThreadSortOrder, |
| 59 | 62 | } from "@t3tools/contracts/settings"; |
| 60 | 63 | import { usePrimaryEnvironmentId } from "../environments/primary"; |
| ... | ... | @@ -128,6 +131,13 @@ import { |
| 128 | 131 | MenuSeparator, |
| 129 | 132 | MenuTrigger, |
| 130 | 133 | } from "./ui/menu"; |
| 134 | import { | |
| 135 | NumberField, | |
| 136 | NumberFieldDecrement, | |
| 137 | NumberFieldGroup, | |
| 138 | NumberFieldIncrement, | |
| 139 | NumberFieldInput, | |
| 140 | } from "./ui/number-field"; | |
| 131 | 141 | import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "./ui/select"; |
| 132 | 142 | import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; |
| 133 | 143 | import { |
| ... | ... | @@ -186,7 +196,6 @@ import { |
| 186 | 196 | type SidebarProjectSnapshot, |
| 187 | 197 | } from "../sidebarProjectGrouping"; |
| 188 | 198 | import { SidebarProviderUpdatePill } from "./sidebar/SidebarProviderUpdatePill"; |
| 189 | const THREAD_PREVIEW_LIMIT = 6; | |
| 190 | 199 | const SIDEBAR_SORT_LABELS: Record<SidebarProjectSortOrder, string> = { |
| 191 | 200 | updated_at: "Last user message", |
| 192 | 201 | created_at: "Created at", |
| ... | ... | @@ -207,6 +216,13 @@ const PROJECT_GROUPING_MODE_LABELS: Record<SidebarProjectGroupingMode, string> = |
| 207 | 216 | separate: "Keep separate", |
| 208 | 217 | }; |
| 209 | 218 | |
| 219 | function clampSidebarThreadPreviewCount(value: number): SidebarThreadPreviewCount { | |
| 220 | return Math.min( | |
| 221 | MAX_SIDEBAR_THREAD_PREVIEW_COUNT, | |
| 222 | Math.max(MIN_SIDEBAR_THREAD_PREVIEW_COUNT, value), | |
| 223 | ) as SidebarThreadPreviewCount; | |
| 224 | } | |
| 225 | ||
| 210 | 226 | function formatProjectMemberActionLabel( |
| 211 | 227 | member: SidebarProjectGroupMember, |
| 212 | 228 | groupedProjectCount: number, |
| ... | ... | @@ -936,6 +952,9 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec |
| 936 | 952 | sidebarProjectGroupingOverrides: settings.sidebarProjectGroupingOverrides, |
| 937 | 953 | })); |
| 938 | 954 | const { updateSettings } = useUpdateSettings(); |
| 955 | const sidebarThreadPreviewCount = useSettings<SidebarThreadPreviewCount>( | |
| 956 | (settings) => settings.sidebarThreadPreviewCount, | |
| 957 | ); | |
| 939 | 958 | const router = useRouter(); |
| 940 | 959 | const { isMobile, setOpenMobile } = useSidebar(); |
| 941 | 960 | const markThreadUnread = useUiStateStore((state) => state.markThreadUnread); |
| ... | ... | @@ -1159,11 +1178,11 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec |
| 1159 | 1178 | }, |
| 1160 | 1179 | }); |
| 1161 | 1180 | }; |
| 1162 | const hasOverflowingThreads = visibleProjectThreads.length > THREAD_PREVIEW_LIMIT; | |
| 1181 | const hasOverflowingThreads = visibleProjectThreads.length > sidebarThreadPreviewCount; | |
| 1163 | 1182 | const previewThreads = |
| 1164 | 1183 | isThreadListExpanded || !hasOverflowingThreads |
| 1165 | 1184 | ? visibleProjectThreads |
| 1166 | : visibleProjectThreads.slice(0, THREAD_PREVIEW_LIMIT); | |
| 1185 | : visibleProjectThreads.slice(0, sidebarThreadPreviewCount); | |
| 1167 | 1186 | const visibleThreadKeys = new Set( |
| 1168 | 1187 | [...previewThreads, ...(pinnedCollapsedThread ? [pinnedCollapsedThread] : [])].map((thread) => |
| 1169 | 1188 | scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id)), |
| ... | ... | @@ -1192,6 +1211,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec |
| 1192 | 1211 | pinnedCollapsedThread, |
| 1193 | 1212 | projectExpanded, |
| 1194 | 1213 | projectThreads, |
| 1214 | sidebarThreadPreviewCount, | |
| 1195 | 1215 | threadLastVisitedAts, |
| 1196 | 1216 | visibleProjectThreads, |
| 1197 | 1217 | ]); |
| ... | ... | @@ -2253,17 +2273,35 @@ function ProjectSortMenu({ |
| 2253 | 2273 | projectSortOrder, |
| 2254 | 2274 | threadSortOrder, |
| 2255 | 2275 | projectGroupingMode, |
| 2276 | threadPreviewCount, | |
| 2256 | 2277 | onProjectSortOrderChange, |
| 2257 | 2278 | onThreadSortOrderChange, |
| 2258 | 2279 | onProjectGroupingModeChange, |
| 2280 | onThreadPreviewCountChange, | |
| 2259 | 2281 | }: { |
| 2260 | 2282 | projectSortOrder: SidebarProjectSortOrder; |
| 2261 | 2283 | threadSortOrder: SidebarThreadSortOrder; |
| 2262 | 2284 | projectGroupingMode: SidebarProjectGroupingMode; |
| 2285 | threadPreviewCount: SidebarThreadPreviewCount; | |
| 2263 | 2286 | onProjectSortOrderChange: (sortOrder: SidebarProjectSortOrder) => void; |
| 2264 | 2287 | onThreadSortOrderChange: (sortOrder: SidebarThreadSortOrder) => void; |
| 2265 | 2288 | onProjectGroupingModeChange: (mode: SidebarProjectGroupingMode) => void; |
| 2289 | onThreadPreviewCountChange: (count: SidebarThreadPreviewCount) => void; | |
| 2266 | 2290 | }) { |
| 2291 | const handleThreadPreviewCountChange = useCallback( | |
| 2292 | (nextValue: number | null) => { | |
| 2293 | if (nextValue === null) { | |
| 2294 | return; | |
| 2295 | } | |
| 2296 | ||
| 2297 | const clampedValue = clampSidebarThreadPreviewCount(nextValue); | |
| 2298 | if (clampedValue !== threadPreviewCount) { | |
| 2299 | onThreadPreviewCountChange(clampedValue); | |
| 2300 | } | |
| 2301 | }, | |
| 2302 | [onThreadPreviewCountChange, threadPreviewCount], | |
| 2303 | ); | |
| 2304 | ||
| 2267 | 2305 | return ( |
| 2268 | 2306 | <Menu> |
| 2269 | 2307 | <Tooltip> |
| ... | ... | @@ -2274,9 +2312,9 @@ function ProjectSortMenu({ |
| 2274 | 2312 | > |
| 2275 | 2313 | <ArrowUpDownIcon className="size-3.5" /> |
| 2276 | 2314 | </TooltipTrigger> |
| 2277 | <TooltipPopup side="right">Sort projects</TooltipPopup> | |
| 2315 | <TooltipPopup side="right">Sidebar options</TooltipPopup> | |
| 2278 | 2316 | </Tooltip> |
| 2279 | <MenuPopup align="end" side="bottom" className="min-w-44"> | |
| 2317 | <MenuPopup align="end" side="bottom" className="min-w-52"> | |
| 2280 | 2318 | <MenuGroup> |
| 2281 | 2319 | <div className="px-2 py-1 sm:text-xs font-medium text-muted-foreground"> |
| 2282 | 2320 | Sort projects |
| ... | ... | @@ -2315,6 +2353,42 @@ function ProjectSortMenu({ |
| 2315 | 2353 | ))} |
| 2316 | 2354 | </MenuRadioGroup> |
| 2317 | 2355 | </MenuGroup> |
| 2356 | <MenuGroup> | |
| 2357 | <div className="px-2 pt-2 pb-1 text-muted-foreground sm:text-xs font-medium"> | |
| 2358 | Visible threads | |
| 2359 | </div> | |
| 2360 | <div className="px-2 py-1"> | |
| 2361 | <NumberField | |
| 2362 | aria-label="Visible thread count" | |
| 2363 | className="w-28 gap-0" | |
| 2364 | max={MAX_SIDEBAR_THREAD_PREVIEW_COUNT} | |
| 2365 | min={MIN_SIDEBAR_THREAD_PREVIEW_COUNT} | |
| 2366 | onValueChange={handleThreadPreviewCountChange} | |
| 2367 | size="sm" | |
| 2368 | step={1} | |
| 2369 | value={threadPreviewCount} | |
| 2370 | > | |
| 2371 | <NumberFieldGroup className="h-7 rounded-md sm:h-6.5"> | |
| 2372 | <NumberFieldDecrement | |
| 2373 | aria-label="Decrease visible thread count" | |
| 2374 | className="px-2 sm:px-2 [&_svg]:size-3.5" | |
| 2375 | /> | |
| 2376 | <NumberFieldInput | |
| 2377 | aria-label="Visible thread count" | |
| 2378 | className="h-7 w-9 grow-0 px-0 text-xs leading-7 sm:h-6.5 sm:leading-6.5" | |
| 2379 | inputMode="numeric" | |
| 2380 | onKeyDownCapture={(event) => { | |
| 2381 | event.stopPropagation(); | |
| 2382 | }} | |
| 2383 | /> | |
| 2384 | <NumberFieldIncrement | |
| 2385 | aria-label="Increase visible thread count" | |
| 2386 | className="px-2 sm:px-2 [&_svg]:size-3.5" | |
| 2387 | /> | |
| 2388 | </NumberFieldGroup> | |
| 2389 | </NumberField> | |
| 2390 | </div> | |
| 2391 | </MenuGroup> | |
| 2318 | 2392 | <MenuSeparator /> |
| 2319 | 2393 | <MenuGroup> |
| 2320 | 2394 | <div className="px-2 pt-2 pb-1 font-medium text-muted-foreground sm:text-xs"> |
| ... | ... | @@ -2462,6 +2536,7 @@ interface SidebarProjectsContentProps { |
| 2462 | 2536 | projectSortOrder: SidebarProjectSortOrder; |
| 2463 | 2537 | threadSortOrder: SidebarThreadSortOrder; |
| 2464 | 2538 | projectGroupingMode: SidebarProjectGroupingMode; |
| 2539 | threadPreviewCount: SidebarThreadPreviewCount; | |
| 2465 | 2540 | updateSettings: ReturnType<typeof useUpdateSettings>["updateSettings"]; |
| 2466 | 2541 | openAddProject: () => void; |
| 2467 | 2542 | isManualProjectSorting: boolean; |
| ... | ... | @@ -2502,6 +2577,7 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent( |
| 2502 | 2577 | projectSortOrder, |
| 2503 | 2578 | threadSortOrder, |
| 2504 | 2579 | projectGroupingMode, |
| 2580 | threadPreviewCount, | |
| 2505 | 2581 | updateSettings, |
| 2506 | 2582 | openAddProject, |
| 2507 | 2583 | isManualProjectSorting, |
| ... | ... | @@ -2548,6 +2624,12 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent( |
| 2548 | 2624 | }, |
| 2549 | 2625 | [updateSettings], |
| 2550 | 2626 | ); |
| 2627 | const handleThreadPreviewCountChange = useCallback( | |
| 2628 | (count: SidebarThreadPreviewCount) => { | |
| 2629 | updateSettings({ sidebarThreadPreviewCount: count }); | |
| 2630 | }, | |
| 2631 | [updateSettings], | |
| 2632 | ); | |
| 2551 | 2633 | |
| 2552 | 2634 | return ( |
| 2553 | 2635 | <SidebarContent className="gap-0"> |
| ... | ... | @@ -2607,9 +2689,11 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent( |
| 2607 | 2689 | projectSortOrder={projectSortOrder} |
| 2608 | 2690 | threadSortOrder={threadSortOrder} |
| 2609 | 2691 | projectGroupingMode={projectGroupingMode} |
| 2692 | threadPreviewCount={threadPreviewCount} | |
| 2610 | 2693 | onProjectSortOrderChange={handleProjectSortOrderChange} |
| 2611 | 2694 | onThreadSortOrderChange={handleThreadSortOrderChange} |
| 2612 | 2695 | onProjectGroupingModeChange={handleProjectGroupingModeChange} |
| 2696 | onThreadPreviewCountChange={handleThreadPreviewCountChange} | |
| 2613 | 2697 | /> |
| 2614 | 2698 | <Tooltip> |
| 2615 | 2699 | <TooltipTrigger |
| ... | ... | @@ -2729,6 +2813,7 @@ export default function Sidebar() { |
| 2729 | 2813 | sidebarProjectGroupingMode: settings.sidebarProjectGroupingMode, |
| 2730 | 2814 | sidebarProjectGroupingOverrides: settings.sidebarProjectGroupingOverrides, |
| 2731 | 2815 | })); |
| 2816 | const sidebarThreadPreviewCount = useSettings((s) => s.sidebarThreadPreviewCount); | |
| 2732 | 2817 | const { updateSettings } = useUpdateSettings(); |
| 2733 | 2818 | const { handleNewThread } = useNewThreadHandler(); |
| 2734 | 2819 | const { archiveThread, deleteThread } = useThreadActions(); |
| ... | ... | @@ -3024,11 +3109,11 @@ export default function Sidebar() { |
| 3024 | 3109 | return []; |
| 3025 | 3110 | } |
| 3026 | 3111 | const isThreadListExpanded = expandedThreadListsByProject.has(project.projectKey); |
| 3027 | const hasOverflowingThreads = projectThreads.length > THREAD_PREVIEW_LIMIT; | |
| 3112 | const hasOverflowingThreads = projectThreads.length > sidebarThreadPreviewCount; | |
| 3028 | 3113 | const previewThreads = |
| 3029 | 3114 | isThreadListExpanded || !hasOverflowingThreads |
| 3030 | 3115 | ? projectThreads |
| 3031 | : projectThreads.slice(0, THREAD_PREVIEW_LIMIT); | |
| 3116 | : projectThreads.slice(0, sidebarThreadPreviewCount); | |
| 3032 | 3117 | const renderedThreads = pinnedCollapsedThread ? [pinnedCollapsedThread] : previewThreads; |
| 3033 | 3118 | return renderedThreads.map((thread) => |
| 3034 | 3119 | scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id)), |
| ... | ... | @@ -3036,6 +3121,7 @@ export default function Sidebar() { |
| 3036 | 3121 | }), |
| 3037 | 3122 | [ |
| 3038 | 3123 | sidebarThreadSortOrder, |
| 3124 | sidebarThreadPreviewCount, | |
| 3039 | 3125 | expandedThreadListsByProject, |
| 3040 | 3126 | projectExpandedById, |
| 3041 | 3127 | routeThreadKey, |
| ... | ... | @@ -3358,6 +3444,7 @@ export default function Sidebar() { |
| 3358 | 3444 | projectSortOrder={sidebarProjectSortOrder} |
| 3359 | 3445 | threadSortOrder={sidebarThreadSortOrder} |
| 3360 | 3446 | projectGroupingMode={sidebarProjectGroupingMode} |
| 3447 | threadPreviewCount={sidebarThreadPreviewCount} | |
| 3361 | 3448 | updateSettings={updateSettings} |
| 3362 | 3449 | openAddProject={openAddProjectCommandPalette} |
| 3363 | 3450 | isManualProjectSorting={isManualProjectSorting} |
apps/web/src/components/settings/SettingsPanels.tsx+4| ... | ... | @@ -364,6 +364,9 @@ export function useSettingsRestore(onRestored?: () => void) { |
| 364 | 364 | ...(settings.timestampFormat !== DEFAULT_UNIFIED_SETTINGS.timestampFormat |
| 365 | 365 | ? ["Time format"] |
| 366 | 366 | : []), |
| 367 | ...(settings.sidebarThreadPreviewCount !== DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount | |
| 368 | ? ["Visible threads"] | |
| 369 | : []), | |
| 367 | 370 | ...(settings.diffWordWrap !== DEFAULT_UNIFIED_SETTINGS.diffWordWrap |
| 368 | 371 | ? ["Diff line wrapping"] |
| 369 | 372 | : []), |
| ... | ... | @@ -400,6 +403,7 @@ export function useSettingsRestore(onRestored?: () => void) { |
| 400 | 403 | settings.diffIgnoreWhitespace, |
| 401 | 404 | settings.diffWordWrap, |
| 402 | 405 | settings.enableAssistantStreaming, |
| 406 | settings.sidebarThreadPreviewCount, | |
| 403 | 407 | settings.timestampFormat, |
| 404 | 408 | theme, |
| 405 | 409 | ], |
apps/web/src/components/ui/input.tsx+26-15| ... | ... | @@ -27,6 +27,31 @@ function Input({ |
| 27 | 27 | props.type === "file" && |
| 28 | 28 | "text-muted-foreground file:me-3 file:bg-transparent file:font-medium file:text-foreground file:text-sm", |
| 29 | 29 | ); |
| 30 | let inputElement: React.ReactElement; | |
| 31 | ||
| 32 | if (nativeInput) { | |
| 33 | const { style, onValueChange: _onValueChange, ...nativeInputProps } = props; | |
| 34 | const nativeStyle = typeof style === "function" ? undefined : style; | |
| 35 | ||
| 36 | inputElement = ( | |
| 37 | <input | |
| 38 | className={inputClassName} | |
| 39 | data-slot="input" | |
| 40 | size={typeof size === "number" ? size : undefined} | |
| 41 | style={nativeStyle} | |
| 42 | {...(nativeInputProps as React.ComponentProps<"input">)} | |
| 43 | /> | |
| 44 | ); | |
| 45 | } else { | |
| 46 | inputElement = ( | |
| 47 | <InputPrimitive | |
| 48 | className={inputClassName} | |
| 49 | data-slot="input" | |
| 50 | size={typeof size === "number" ? size : undefined} | |
| 51 | {...props} | |
| 52 | /> | |
| 53 | ); | |
| 54 | } | |
| 30 | 55 | |
| 31 | 56 | return ( |
| 32 | 57 | <span |
| ... | ... | @@ -40,21 +65,7 @@ function Input({ |
| 40 | 65 | data-size={size} |
| 41 | 66 | data-slot="input-control" |
| 42 | 67 | > |
| 43 | {nativeInput ? ( | |
| 44 | <input | |
| 45 | className={inputClassName} | |
| 46 | data-slot="input" | |
| 47 | size={typeof size === "number" ? size : undefined} | |
| 48 | {...props} | |
| 49 | /> | |
| 50 | ) : ( | |
| 51 | <InputPrimitive | |
| 52 | className={inputClassName} | |
| 53 | data-slot="input" | |
| 54 | size={typeof size === "number" ? size : undefined} | |
| 55 | {...props} | |
| 56 | /> | |
| 57 | )} | |
| 68 | {inputElement} | |
| 58 | 69 | </span> |
| 59 | 70 | ); |
| 60 | 71 | } |
apps/web/src/components/ui/number-field.tsx created+156| ... | ... | @@ -0,0 +1,156 @@ |
| 1 | "use client"; | |
| 2 | ||
| 3 | import { NumberField as NumberFieldPrimitive } from "@base-ui/react/number-field"; | |
| 4 | import { MinusIcon, PlusIcon } from "lucide-react"; | |
| 5 | import * as React from "react"; | |
| 6 | import { cn } from "~/lib/utils"; | |
| 7 | import { Label } from "~/components/ui/label"; | |
| 8 | ||
| 9 | export const NumberFieldContext: React.Context<{ | |
| 10 | fieldId: string; | |
| 11 | } | null> = React.createContext<{ | |
| 12 | fieldId: string; | |
| 13 | } | null>(null); | |
| 14 | ||
| 15 | export function NumberField({ | |
| 16 | id, | |
| 17 | className, | |
| 18 | size = "default", | |
| 19 | ...props | |
| 20 | }: NumberFieldPrimitive.Root.Props & { | |
| 21 | size?: "sm" | "default" | "lg"; | |
| 22 | }): React.ReactElement { | |
| 23 | const generatedId = React.useId(); | |
| 24 | const fieldId = id ?? generatedId; | |
| 25 | const contextValue = React.useMemo(() => ({ fieldId }), [fieldId]); | |
| 26 | ||
| 27 | return ( | |
| 28 | <NumberFieldContext.Provider value={contextValue}> | |
| 29 | <NumberFieldPrimitive.Root | |
| 30 | className={cn("flex w-full flex-col items-start gap-2", className)} | |
| 31 | data-size={size} | |
| 32 | data-slot="number-field" | |
| 33 | id={fieldId} | |
| 34 | {...props} | |
| 35 | /> | |
| 36 | </NumberFieldContext.Provider> | |
| 37 | ); | |
| 38 | } | |
| 39 | ||
| 40 | export function NumberFieldGroup({ | |
| 41 | className, | |
| 42 | ...props | |
| 43 | }: NumberFieldPrimitive.Group.Props): React.ReactElement { | |
| 44 | return ( | |
| 45 | <NumberFieldPrimitive.Group | |
| 46 | className={cn( | |
| 47 | "relative flex w-full justify-between rounded-lg border border-input bg-background not-dark:bg-clip-padding text-base text-foreground shadow-xs/5 ring-ring/24 transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] not-data-disabled:not-focus-within:not-aria-invalid:before:shadow-[0_1px_--theme(--color-black/4%)] focus-within:border-ring focus-within:ring-[3px] has-aria-invalid:border-destructive/36 has-autofill:bg-foreground/4 focus-within:has-aria-invalid:border-destructive/64 focus-within:has-aria-invalid:ring-destructive/48 data-disabled:pointer-events-none data-disabled:opacity-64 sm:text-sm dark:bg-input/32 dark:has-autofill:bg-foreground/8 dark:has-aria-invalid:ring-destructive/24 dark:not-data-disabled:not-focus-within:not-aria-invalid:before:shadow-[0_-1px_--theme(--color-white/6%)] [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0 [[data-disabled],:focus-within,[aria-invalid]]:shadow-none", | |
| 48 | className, | |
| 49 | )} | |
| 50 | data-slot="number-field-group" | |
| 51 | {...props} | |
| 52 | /> | |
| 53 | ); | |
| 54 | } | |
| 55 | ||
| 56 | export function NumberFieldDecrement({ | |
| 57 | className, | |
| 58 | ...props | |
| 59 | }: NumberFieldPrimitive.Decrement.Props): React.ReactElement { | |
| 60 | return ( | |
| 61 | <NumberFieldPrimitive.Decrement | |
| 62 | className={cn( | |
| 63 | "relative flex shrink-0 cursor-pointer items-center justify-center rounded-s-[calc(var(--radius-lg)-1px)] in-data-[size=sm]:px-[calc(--spacing(2.5)-1px)] px-[calc(--spacing(3)-1px)] transition-colors pointer-coarse:after:absolute pointer-coarse:after:size-full pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 hover:bg-accent", | |
| 64 | className, | |
| 65 | )} | |
| 66 | data-slot="number-field-decrement" | |
| 67 | {...props} | |
| 68 | > | |
| 69 | <MinusIcon /> | |
| 70 | </NumberFieldPrimitive.Decrement> | |
| 71 | ); | |
| 72 | } | |
| 73 | ||
| 74 | export function NumberFieldIncrement({ | |
| 75 | className, | |
| 76 | ...props | |
| 77 | }: NumberFieldPrimitive.Increment.Props): React.ReactElement { | |
| 78 | return ( | |
| 79 | <NumberFieldPrimitive.Increment | |
| 80 | className={cn( | |
| 81 | "relative flex shrink-0 cursor-pointer items-center justify-center rounded-e-[calc(var(--radius-lg)-1px)] in-data-[size=sm]:px-[calc(--spacing(2.5)-1px)] px-[calc(--spacing(3)-1px)] transition-colors pointer-coarse:after:absolute pointer-coarse:after:size-full pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 hover:bg-accent", | |
| 82 | className, | |
| 83 | )} | |
| 84 | data-slot="number-field-increment" | |
| 85 | {...props} | |
| 86 | > | |
| 87 | <PlusIcon /> | |
| 88 | </NumberFieldPrimitive.Increment> | |
| 89 | ); | |
| 90 | } | |
| 91 | ||
| 92 | export function NumberFieldInput({ | |
| 93 | className, | |
| 94 | ...props | |
| 95 | }: NumberFieldPrimitive.Input.Props): React.ReactElement { | |
| 96 | return ( | |
| 97 | <NumberFieldPrimitive.Input | |
| 98 | className={cn( | |
| 99 | "h-8.5 in-data-[size=lg]:h-9.5 in-data-[size=sm]:h-7.5 w-full min-w-0 grow bg-transparent in-data-[size=sm]:px-[calc(--spacing(2.5)-1px)] px-[calc(--spacing(3)-1px)] text-center tabular-nums in-data-[size=lg]:leading-9.5 in-data-[size=sm]:leading-7.5 leading-8.5 outline-none [transition:background-color_5000000s_ease-in-out_0s] sm:h-7.5 sm:in-data-[size=lg]:h-8.5 sm:in-data-[size=sm]:h-6.5 sm:in-data-[size=lg]:leading-8.5 sm:in-data-[size=sm]:leading-8.5 sm:leading-7.5", | |
| 100 | className, | |
| 101 | )} | |
| 102 | data-slot="number-field-input" | |
| 103 | {...props} | |
| 104 | /> | |
| 105 | ); | |
| 106 | } | |
| 107 | ||
| 108 | export function NumberFieldScrubArea({ | |
| 109 | className, | |
| 110 | label, | |
| 111 | ...props | |
| 112 | }: NumberFieldPrimitive.ScrubArea.Props & { | |
| 113 | label: string; | |
| 114 | }): React.ReactElement { | |
| 115 | const context = React.useContext(NumberFieldContext); | |
| 116 | ||
| 117 | if (!context) { | |
| 118 | throw new Error( | |
| 119 | "NumberFieldScrubArea must be used within a NumberField component for accessibility.", | |
| 120 | ); | |
| 121 | } | |
| 122 | ||
| 123 | return ( | |
| 124 | <NumberFieldPrimitive.ScrubArea | |
| 125 | className={cn("flex cursor-ew-resize", className)} | |
| 126 | data-slot="number-field-scrub-area" | |
| 127 | {...props} | |
| 128 | > | |
| 129 | <Label className="cursor-ew-resize" htmlFor={context.fieldId}> | |
| 130 | {label} | |
| 131 | </Label> | |
| 132 | <NumberFieldPrimitive.ScrubAreaCursor className="drop-shadow-[0_1px_1px_#0008] filter"> | |
| 133 | <CursorGrowIcon /> | |
| 134 | </NumberFieldPrimitive.ScrubAreaCursor> | |
| 135 | </NumberFieldPrimitive.ScrubArea> | |
| 136 | ); | |
| 137 | } | |
| 138 | ||
| 139 | export function CursorGrowIcon(props: React.ComponentProps<"svg">): React.ReactElement { | |
| 140 | return ( | |
| 141 | <svg | |
| 142 | aria-hidden="true" | |
| 143 | fill="black" | |
| 144 | height="14" | |
| 145 | stroke="white" | |
| 146 | viewBox="0 0 24 14" | |
| 147 | width="26" | |
| 148 | xmlns="http://www.w3.org/2000/svg" | |
| 149 | {...props} | |
| 150 | > | |
| 151 | <path d="M19.5 5.5L6.49737 5.51844V2L1 6.9999L6.5 12L6.49737 8.5L19.5 8.5V12L25 6.9999L19.5 2V5.5Z" /> | |
| 152 | </svg> | |
| 153 | ); | |
| 154 | } | |
| 155 | ||
| 156 | export { NumberFieldPrimitive }; |
apps/web/src/localApi.test.ts+2| ... | ... | @@ -613,6 +613,7 @@ describe("wsApi", () => { |
| 613 | 613 | }, |
| 614 | 614 | sidebarProjectSortOrder: "manual" as const, |
| 615 | 615 | sidebarThreadSortOrder: "created_at" as const, |
| 616 | sidebarThreadPreviewCount: 6, | |
| 616 | 617 | timestampFormat: "24-hour" as const, |
| 617 | 618 | }; |
| 618 | 619 | const getClientSettings = vi.fn().mockResolvedValue({ |
| ... | ... | @@ -675,6 +676,7 @@ describe("wsApi", () => { |
| 675 | 676 | }, |
| 676 | 677 | sidebarProjectSortOrder: "manual" as const, |
| 677 | 678 | sidebarThreadSortOrder: "created_at" as const, |
| 679 | sidebarThreadPreviewCount: 6, | |
| 678 | 680 | timestampFormat: "24-hour" as const, |
| 679 | 681 | }; |
| 680 | 682 |
apps/web/vite.config.ts-2| ... | ... | @@ -7,7 +7,6 @@ import pkg from "./package.json" with { type: "json" }; |
| 7 | 7 | |
| 8 | 8 | const port = Number(process.env.PORT ?? 5733); |
| 9 | 9 | const host = process.env.HOST?.trim() || "localhost"; |
| 10 | const configuredHttpUrl = process.env.VITE_HTTP_URL?.trim(); | |
| 11 | 10 | const configuredWsUrl = process.env.VITE_WS_URL?.trim(); |
| 12 | 11 | const configuredHostedAppUrl = (() => { |
| 13 | 12 | if (process.env.VERCEL_ENV === "production" && process.env.VERCEL_PROJECT_PRODUCTION_URL) { |
| ... | ... | @@ -74,7 +73,6 @@ export default defineConfig({ |
| 74 | 73 | ], |
| 75 | 74 | }, |
| 76 | 75 | define: { |
| 77 | "import.meta.env.VITE_HTTP_URL": JSON.stringify(configuredHttpUrl ?? ""), | |
| 78 | 76 | // In dev mode, tell the web app where the WebSocket server lives |
| 79 | 77 | "import.meta.env.VITE_WS_URL": JSON.stringify(configuredWsUrl ?? ""), |
| 80 | 78 | "import.meta.env.VITE_HOSTED_APP_URL": JSON.stringify(configuredHostedAppUrl ?? ""), |
bun.lock+11-7| ... | ... | @@ -16,7 +16,7 @@ |
| 16 | 16 | }, |
| 17 | 17 | "apps/desktop": { |
| 18 | 18 | "name": "@t3tools/desktop", |
| 19 | "version": "0.0.21", | |
| 19 | "version": "0.0.22", | |
| 20 | 20 | "dependencies": { |
| 21 | 21 | "@effect/platform-node": "catalog:", |
| 22 | 22 | "effect": "catalog:", |
| ... | ... | @@ -49,7 +49,7 @@ |
| 49 | 49 | }, |
| 50 | 50 | "apps/server": { |
| 51 | 51 | "name": "t3", |
| 52 | "version": "0.0.21", | |
| 52 | "version": "0.0.22", | |
| 53 | 53 | "bin": { |
| 54 | 54 | "t3": "./dist/bin.mjs", |
| 55 | 55 | }, |
| ... | ... | @@ -82,9 +82,9 @@ |
| 82 | 82 | }, |
| 83 | 83 | "apps/web": { |
| 84 | 84 | "name": "@t3tools/web", |
| 85 | "version": "0.0.21", | |
| 85 | "version": "0.0.22", | |
| 86 | 86 | "dependencies": { |
| 87 | "@base-ui/react": "^1.2.0", | |
| 87 | "@base-ui/react": "^1.4.1", | |
| 88 | 88 | "@dnd-kit/core": "^6.3.1", |
| 89 | 89 | "@dnd-kit/modifiers": "^9.0.0", |
| 90 | 90 | "@dnd-kit/sortable": "^10.0.0", |
| ... | ... | @@ -148,7 +148,7 @@ |
| 148 | 148 | }, |
| 149 | 149 | "packages/contracts": { |
| 150 | 150 | "name": "@t3tools/contracts", |
| 151 | "version": "0.0.21", | |
| 151 | "version": "0.0.22", | |
| 152 | 152 | "dependencies": { |
| 153 | 153 | "effect": "catalog:", |
| 154 | 154 | }, |
| ... | ... | @@ -350,9 +350,9 @@ |
| 350 | 350 | |
| 351 | 351 | "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], |
| 352 | 352 | |
| 353 | "@base-ui/react": ["@base-ui/react@1.3.0", "", { "dependencies": { "@babel/runtime": "^7.28.6", "@base-ui/utils": "0.2.6", "@floating-ui/react-dom": "^2.1.8", "@floating-ui/utils": "^0.2.11", "tabbable": "^6.4.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-FwpKqZbPz14AITp1CVgf4AjhKPe1OeeVKSBMdgD10zbFlj3QSWelmtCMLi2+/PFZZcIm3l87G7rwtCZJwHyXWA=="], | |
| 353 | "@base-ui/react": ["@base-ui/react@1.4.1", "", { "dependencies": { "@babel/runtime": "^7.29.2", "@base-ui/utils": "0.2.8", "@floating-ui/react-dom": "^2.1.8", "@floating-ui/utils": "^0.2.11", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@date-fns/tz": "^1.2.0", "@types/react": "^17 || ^18 || ^19", "date-fns": "^4.0.0", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@date-fns/tz", "@types/react", "date-fns"] }, "sha512-Ab5/LIhcmL8BQcsBUYiOfkSDRdLpvgUBzMK30cu684JPcLclYlztharvCZyNNgzJtbAiREzI9q0pI5erHCMgCw=="], | |
| 354 | 354 | |
| 355 | "@base-ui/utils": ["@base-ui/utils@0.2.6", "", { "dependencies": { "@babel/runtime": "^7.28.6", "@floating-ui/utils": "^0.2.11", "reselect": "^5.1.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-yQ+qeuqohwhsNpoYDqqXaLllYAkPCP4vYdDrVo8FQXaAPfHWm1pG/Vm+jmGTA5JFS0BAIjookyapuJFY8F9PIw=="], | |
| 355 | "@base-ui/utils": ["@base-ui/utils@0.2.8", "", { "dependencies": { "@babel/runtime": "^7.29.2", "@floating-ui/utils": "^0.2.11", "reselect": "^5.1.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-jvOi+c+ftGlGotNcKnzPVg2IhCaDTB6/6R3JeqdjdXktuAJi3wKH9T7+svuaKh1mmfVU11UWzUZVH74JDfi/wQ=="], | |
| 356 | 356 | |
| 357 | 357 | "@blazediff/core": ["@blazediff/core@1.9.1", "", {}, "sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA=="], |
| 358 | 358 | |
| ... | ... | @@ -2122,6 +2122,10 @@ |
| 2122 | 2122 | |
| 2123 | 2123 | "@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], |
| 2124 | 2124 | |
| 2125 | "@base-ui/react/@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="], | |
| 2126 | ||
| 2127 | "@base-ui/utils/@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="], | |
| 2128 | ||
| 2125 | 2129 | "@electron/get/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], |
| 2126 | 2130 | |
| 2127 | 2131 | "@electron/get/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], |
packages/contracts/src/settings.ts+13| ... | ... | @@ -27,6 +27,16 @@ export const SidebarProjectGroupingMode = Schema.Literals([ |
| 27 | 27 | ]); |
| 28 | 28 | export type SidebarProjectGroupingMode = typeof SidebarProjectGroupingMode.Type; |
| 29 | 29 | export const DEFAULT_SIDEBAR_PROJECT_GROUPING_MODE: SidebarProjectGroupingMode = "repository"; |
| 30 | export const MIN_SIDEBAR_THREAD_PREVIEW_COUNT = 1; | |
| 31 | export const MAX_SIDEBAR_THREAD_PREVIEW_COUNT = 15; | |
| 32 | export const SidebarThreadPreviewCount = Schema.Int.check( | |
| 33 | Schema.isBetween({ | |
| 34 | minimum: MIN_SIDEBAR_THREAD_PREVIEW_COUNT, | |
| 35 | maximum: MAX_SIDEBAR_THREAD_PREVIEW_COUNT, | |
| 36 | }), | |
| 37 | ); | |
| 38 | export type SidebarThreadPreviewCount = typeof SidebarThreadPreviewCount.Type; | |
| 39 | export const DEFAULT_SIDEBAR_THREAD_PREVIEW_COUNT: SidebarThreadPreviewCount = 6; | |
| 30 | 40 | |
| 31 | 41 | export const ClientSettingsSchema = Schema.Struct({ |
| 32 | 42 | autoOpenPlanSidebar: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), |
| ... | ... | @@ -75,6 +85,9 @@ export const ClientSettingsSchema = Schema.Struct({ |
| 75 | 85 | sidebarThreadSortOrder: SidebarThreadSortOrder.pipe( |
| 76 | 86 | Schema.withDecodingDefault(Effect.succeed(DEFAULT_SIDEBAR_THREAD_SORT_ORDER)), |
| 77 | 87 | ), |
| 88 | sidebarThreadPreviewCount: SidebarThreadPreviewCount.pipe( | |
| 89 | Schema.withDecodingDefault(Effect.succeed(DEFAULT_SIDEBAR_THREAD_PREVIEW_COUNT)), | |
| 90 | ), | |
| 78 | 91 | timestampFormat: TimestampFormat.pipe( |
| 79 | 92 | Schema.withDecodingDefault(Effect.succeed(DEFAULT_TIMESTAMP_FORMAT)), |
| 80 | 93 | ), |