diff --git a/DESIGN.md b/DESIGN.md index 1655ce8..9194852 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -51,6 +51,28 @@ Only after that translation should the idea appear in product UI. This is how visual taste becomes a maintainable system instead of a sequence of one-off page edits. +## 2.1 Density and scale + +`mini-stim` should default to a compact work-surface density. + +The desired proportion is closer to a browser surface viewed around 75% than to +large default web-app chrome. This is a system scale decision, not a user zoom +workaround. + +When density changes, scale these together: + +- typography tokens +- spacing tokens +- icon and avatar sizes +- badge and control heights +- shell, panel, composer, rail, and Inspect geometry +- inspection overlay labels + +Do not shrink only spacing while leaving type and symbols large. That creates a +compressed but disproportionate surface. Do not shrink only font size while +leaving chrome large either. The target is a coherent ratio: readable text in a +denser tool surface. + ## 3. Design pairing workflow Design work in `mini-stim` should treat the current local product surface as a diff --git a/apps/client/soma/web/src/App.tsx b/apps/client/soma/web/src/App.tsx index 62e7166..eb34370 100644 --- a/apps/client/soma/web/src/App.tsx +++ b/apps/client/soma/web/src/App.tsx @@ -13,14 +13,30 @@ import { useSessionTurnTimeline, } from "@mini-stim/hooks"; import { uiStorage } from "@mini-stim/storage"; -import { type PointerEvent as ReactPointerEvent, useEffect, useMemo, useState } from "react"; +import { + type PointerEvent as ReactPointerEvent, + useCallback, + useEffect, + useMemo, + useState, +} from "react"; import { STIM_APP_NAMESPACE } from "./appNamespace"; import { ChatShell } from "./components/ChatShell"; import { InspectPanel } from "./components/InspectPanel"; import { NavigationDock } from "./components/NavigationDock"; import { SessionRail } from "./components/SessionRail"; -import { type InspectTarget, subscribeInspectTarget } from "./events/inspect"; +import { + type InspectEvent, + type InspectTarget, + subscribeInspect, + toggleInspectPanel, +} from "./events/inspect"; +import { + type NavigationEvent, + subscribeNavigation, + toggleNavigationPanel, +} from "./events/navigation"; import type { NavigationMode } from "./navigationMode"; const DEFAULT_RAIL_WIDTH = 304; @@ -45,7 +61,29 @@ export function App() { const [error, setError] = useState(null); const [inspecting, setInspecting] = useState(preferences.inspect.open); const [inspectTarget, setInspectTarget] = useState(null); - const [navigationMode, setNavigationMode] = useState("sessions"); + const [navigation, setNavigation] = useState(preferences.navigation); + + const commitNavigation = useCallback((updater: (current: NavigationState) => NavigationState) => { + setNavigation((current) => { + const next = updater(current); + uiStorage.update((stored) => ({ + ...stored, + navigation: next, + })); + return next; + }); + }, []); + + const commitInspectOpen = useCallback((open: boolean) => { + setInspecting(open); + uiStorage.update((current) => ({ + ...current, + inspect: { + ...current.inspect, + open, + }, + })); + }, []); useEffect(() => { if (!selectedSessionId && sessions.length) { @@ -64,51 +102,51 @@ export function App() { useEffect( () => - subscribeInspectTarget(({ target }) => { - setError(null); - setInspectTarget(target); - setInspecting(true); - if (target.sessionId !== selectedSessionId) { - actions.selectAndGet(target.sessionId); - } else { - actions.refreshRuntime(target.sessionId); - } - uiStorage.update((current) => ({ - ...current, - inspect: { - ...current.inspect, - open: true, - }, - })); + subscribeInspect((event) => { + handleInspectEvent(event, { + actions, + commitInspectOpen, + inspecting, + selectedSessionId, + setError, + setInspectTarget, + }); }), - [actions, selectedSessionId], + [actions, commitInspectOpen, inspecting, selectedSessionId], ); - function openInspect() { - setInspectTarget(null); - if (selectedSessionId) { - actions.refreshRuntime(selectedSessionId); + useEffect( + () => + subscribeNavigation((event) => { + commitNavigation((current) => nextNavigationState(current, event)); + }), + [commitNavigation], + ); + + useEffect(() => { + function handleKeyDown(event: KeyboardEvent) { + if (event.defaultPrevented || event.altKey || event.shiftKey) { + return; + } + if (!event.metaKey && !event.ctrlKey) { + return; + } + + switch (event.key.toLowerCase()) { + case "b": + event.preventDefault(); + toggleNavigationPanel(); + return; + case "i": + event.preventDefault(); + toggleInspectPanel(); + return; + } } - setInspecting(true); - uiStorage.update((current) => ({ - ...current, - inspect: { - ...current.inspect, - open: true, - }, - })); - } - function closeInspect() { - setInspecting(false); - uiStorage.update((current) => ({ - ...current, - inspect: { - ...current.inspect, - open: false, - }, - })); - } + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, []); const busy = pending > 0; const debouncedBusy = useDebouncedValue(busy, { debounceMs: 150 }); @@ -255,36 +293,38 @@ export function App() { } return ( - + - + - - - - + {navigation.open ? ( + + + + + ) : null} @@ -334,3 +372,81 @@ function clampWidth(width: number, target: "inspect" | "rail") { const range = target === "rail" ? RAIL_WIDTH_RANGE : INSPECT_WIDTH_RANGE; return Math.round(Math.min(range.max, Math.max(range.min, width))); } + +type NavigationState = { + mode: NavigationMode; + open: boolean; +}; + +function nextNavigationState(current: NavigationState, event: NavigationEvent) { + switch (event.type) { + case "navigation:mode-selected": + if (current.mode === event.mode) { + return { + ...current, + open: !current.open, + }; + } + return { + mode: event.mode, + open: true, + }; + case "navigation:panel-toggled": + return { + ...current, + open: !current.open, + }; + } +} + +function handleInspectEvent( + event: InspectEvent, + context: { + actions: ReturnType; + commitInspectOpen: (open: boolean) => void; + inspecting: boolean; + selectedSessionId: string | null; + setError: (value: string | null) => void; + setInspectTarget: (value: InspectTarget | null) => void; + }, +) { + switch (event.type) { + case "inspect:target:selected": + context.setError(null); + context.setInspectTarget(event.target); + context.commitInspectOpen(true); + if (event.target.sessionId !== context.selectedSessionId) { + context.actions.selectAndGet(event.target.sessionId); + } else { + context.actions.refreshRuntime(event.target.sessionId); + } + return; + case "inspect:panel:opened": + context.setError(null); + context.commitInspectOpen(true); + if (context.selectedSessionId) { + context.actions.refreshRuntime(context.selectedSessionId); + } + return; + case "inspect:panel:closed": + context.commitInspectOpen(false); + return; + case "inspect:panel:toggled": { + const nextOpen = !context.inspecting; + context.commitInspectOpen(nextOpen); + if (nextOpen && context.selectedSessionId) { + context.actions.refreshRuntime(context.selectedSessionId); + } + } + } +} + +function gridTemplate(navigationOpen: boolean, inspecting: boolean) { + if (navigationOpen && inspecting) { + return "sidebar-main-inspect"; + } + if (navigationOpen) { + return "sidebar-main"; + } + return inspecting ? "main-inspect" : "main"; +} diff --git a/apps/client/soma/web/src/components/ChatHeader.tsx b/apps/client/soma/web/src/components/ChatHeader.tsx index f35833b..d36c2c1 100644 --- a/apps/client/soma/web/src/components/ChatHeader.tsx +++ b/apps/client/soma/web/src/components/ChatHeader.tsx @@ -10,13 +10,13 @@ import { import { useEffect, useState } from "react"; import { STIM_APP_NAMESPACE } from "../appNamespace"; +import { openInspectPanel } from "../events/inspect"; export function ChatHeader(props: { activity: string; busy: boolean; connection: string; inspecting: boolean; - onOpenInspect: () => void; onTitleCommit: (title: string | null) => void; selectedSessionId: string | null; title: string; @@ -113,7 +113,7 @@ export function ChatHeader(props: { variant="ghost" disabled={!props.selectedSessionId} type="button" - onClick={props.onOpenInspect} + onClick={openInspectPanel} > Inspect diff --git a/apps/client/soma/web/src/components/ChatShell.tsx b/apps/client/soma/web/src/components/ChatShell.tsx index fb33988..0674936 100644 --- a/apps/client/soma/web/src/components/ChatShell.tsx +++ b/apps/client/soma/web/src/components/ChatShell.tsx @@ -12,7 +12,6 @@ export function ChatShell(props: { error: string | null; inspecting: boolean; onDraftChange: (value: string) => void; - onOpenInspect: () => void; onSend: () => void; onTitleCommit: (title: string | null) => void; selectedSessionId: string | null; @@ -40,7 +39,6 @@ export function ChatShell(props: { busy={props.busy} connection={props.connection} inspecting={props.inspecting} - onOpenInspect={props.onOpenInspect} onTitleCommit={props.onTitleCommit} selectedSessionId={props.selectedSessionId} title={props.title} diff --git a/apps/client/soma/web/src/components/InspectPanel.tsx b/apps/client/soma/web/src/components/InspectPanel.tsx index ba160df..0cfb744 100644 --- a/apps/client/soma/web/src/components/InspectPanel.tsx +++ b/apps/client/soma/web/src/components/InspectPanel.tsx @@ -1,9 +1,9 @@ import { - Button, + ChevronRightIcon, + IconButton, Inline, Pane, Panel, - Stack, Text, useAppComponentRef, } from "@mini-stim/components"; @@ -11,7 +11,7 @@ import type { SessionRuntimeSnapshot } from "@mini-stim/hooks"; import type { ReactNode } from "react"; import { STIM_APP_NAMESPACE } from "../appNamespace"; -import type { InspectTarget } from "../events/inspect"; +import { closeInspectPanel, type InspectTarget } from "../events/inspect"; import { MessageInspectPanel } from "./domains/message/InspectPanel/MessageInspectPanel"; import { SessionInspectPanel } from "./domains/session/InspectPanel/SessionInspectPanel"; import { ToolCallInspectPanel } from "./domains/tool-call/InspectPanel/ToolCallInspectPanel"; @@ -32,7 +32,6 @@ const inspectDomainRegistry = { } satisfies Record; export function InspectPanel(props: { - onClose: () => void; runtime: SessionRuntimeSnapshot | null; target: InspectTarget | null; }) { @@ -54,17 +53,17 @@ export function InspectPanel(props: { - - - INSPECT + + + Inspect - + {inspectDomainTitle(domain)} - - + + + + diff --git a/apps/client/soma/web/src/components/NavigationDock.tsx b/apps/client/soma/web/src/components/NavigationDock.tsx index f4e752b..973e1e1 100644 --- a/apps/client/soma/web/src/components/NavigationDock.tsx +++ b/apps/client/soma/web/src/components/NavigationDock.tsx @@ -7,12 +7,10 @@ import { } from "@mini-stim/components"; import { STIM_APP_NAMESPACE } from "../appNamespace"; +import { selectNavigationMode } from "../events/navigation"; import type { NavigationMode } from "../navigationMode"; -export function NavigationDock(props: { - mode: NavigationMode; - onModeChange: (mode: NavigationMode) => void; -}) { +export function NavigationDock(props: { mode: NavigationMode; open: boolean }) { const dockRef = useAppComponentRef({ domain: "navigation", id: "navigation-dock", @@ -49,9 +47,9 @@ export function NavigationDock(props: { ref={sessionsRef} label="Sessions" size="sm" - tone={props.mode === "sessions" ? "accent" : "neutral"} - aria-pressed={props.mode === "sessions"} - onClick={() => props.onModeChange("sessions")} + tone={props.open && props.mode === "sessions" ? "accent" : "neutral"} + aria-pressed={pressedState(props, "sessions")} + onClick={() => selectNavigationMode("sessions")} > @@ -59,12 +57,20 @@ export function NavigationDock(props: { ref={soulsRef} label="Souls" size="sm" - tone={props.mode === "souls" ? "accent" : "neutral"} - aria-pressed={props.mode === "souls"} - onClick={() => props.onModeChange("souls")} + tone={props.open && props.mode === "souls" ? "accent" : "neutral"} + aria-pressed={pressedState(props, "souls")} + onClick={() => selectNavigationMode("souls")} > ); } + +function pressedState(navigation: { mode: NavigationMode; open: boolean }, mode: NavigationMode) { + if (navigation.mode !== mode) { + return false; + } + + return navigation.open ? true : "mixed"; +} diff --git a/apps/client/soma/web/src/components/SessionRail.tsx b/apps/client/soma/web/src/components/SessionRail.tsx index 9d9783c..df4ff60 100644 --- a/apps/client/soma/web/src/components/SessionRail.tsx +++ b/apps/client/soma/web/src/components/SessionRail.tsx @@ -95,7 +95,7 @@ export function SessionRail(props: { {props.mode === "sessions" ? ( - + @@ -150,7 +150,7 @@ export function SessionRail(props: { ) : null} ) : ( - + diff --git a/apps/client/soma/web/src/components/TimelineItemView.tsx b/apps/client/soma/web/src/components/TimelineItemView.tsx index 0d0e912..6881dcd 100644 --- a/apps/client/soma/web/src/components/TimelineItemView.tsx +++ b/apps/client/soma/web/src/components/TimelineItemView.tsx @@ -1,12 +1,7 @@ import { - Badge, - IdentityLine, - Inline, - Pressable, - Stack, - Surface, + AnchoredContentGroupItem, + MarkdownText, Text, - Timestamp, useAppComponentRef, } from "@mini-stim/components"; import type { TimelineItem } from "@mini-stim/hooks"; @@ -20,86 +15,66 @@ import { truncateMiddle, } from "./domains/tool-call/model/toolCallModel"; import { ToolCallWhisper, ToolResultWhisper } from "./domains/tool-call/transcript/ToolCallWhisper"; -import type { SoulIdentity } from "./Transcript"; export function TimelineItemView(props: { + align?: "start" | "center" | "end"; item: TimelineItem; - showIdentity: boolean; - soulIdentity: SoulIdentity; }) { - const { item, showIdentity, soulIdentity } = props; + const { align = "start", item } = props; const target = targetForItem(item); const itemRef = useAppComponentRef(registrationForItem(item)); switch (item.kind) { case "message": - return ( - - ); + return ; case "tool_call": return ( - selectInspectTarget(target)}> - - + selectInspectTarget(target)} + > + + ); case "tool_result": return ( - selectInspectTarget(target)}> - - + selectInspectTarget(target)} + > + + ); } } function MessageTimelineItem(props: { + align: "start" | "center" | "end"; item: Extract; itemRef: (element: HTMLElement | null) => void; - showIdentity: boolean; - soulIdentity: SoulIdentity; target: ReturnType; }) { const role = props.item.message.message.actor_type; const pending = props.item.message.message.state === "pending"; - const identity = identityForMessage( - role, - props.item.message.message.actor_id, - props.soulIdentity, - ); - const showHeader = props.showIdentity || pending; return ( - selectInspectTarget(props.target)}> - - - {showHeader ? ( - - {props.showIdentity ? ( - - ) : null} - {pending ? ( - - generating… - - ) : ( - - )} - - ) : null} - {props.item.message.content_text} - - - + selectInspectTarget(props.target)} + > + + {props.item.message.content_text} + + {pending ? ( + + generating… + + ) : null} + ); } @@ -185,53 +160,6 @@ function targetForItem(item: TimelineItem) { } } -function roleTone(role: string) { - switch (role) { - case "account": - return "accent"; - case "system": - return "warning"; - default: - return "muted"; - } -} - -function roleAlign(role: string) { - switch (role) { - case "account": - return "end"; - case "system": - return "center"; - default: - return "start"; - } -} - -function identityForMessage(role: string, actorId: string, soulIdentity: SoulIdentity) { - switch (role) { - case "account": - return { - avatarSeed: `account:${actorId}`, - name: "You", - }; - case "system": - return { - avatarSeed: "system", - name: "System", - }; - default: - return { - avatarSeed: soulIdentity.avatarSeed, - marker: ( - - verified - - ), - name: soulIdentity.name, - }; - } -} - function toolRunLabel(toolName: string) { return isShellTool(toolName) ? "Shell Run" : "Tool Run"; } diff --git a/apps/client/soma/web/src/components/Transcript.tsx b/apps/client/soma/web/src/components/Transcript.tsx index 20e5a5e..87d197c 100644 --- a/apps/client/soma/web/src/components/Transcript.tsx +++ b/apps/client/soma/web/src/components/Transcript.tsx @@ -1,9 +1,18 @@ -import { Notice, Stack, Text, useAppComponentRef } from "@mini-stim/components"; +import { + AnchoredContentGroup, + AnchoredContentGroupDivider, + Notice, + Stack, + Text, + Timestamp, + useAppComponentRef, +} from "@mini-stim/components"; import type { TurnGroup } from "@mini-stim/hooks"; +import { Fragment } from "react"; import { STIM_APP_NAMESPACE } from "../appNamespace"; -import type { AnnotatedTurnGroup } from "../projections/identityGrouping"; -import { annotateIdentityGroups } from "../projections/identityGrouping"; +import type { TranscriptGroup } from "../projections/transcriptGrouping"; +import { buildTranscriptGroups, flattenTimelineItems } from "../projections/transcriptGrouping"; import { TimelineItemView } from "./TimelineItemView"; export interface SoulIdentity { @@ -12,7 +21,8 @@ export interface SoulIdentity { } export function Transcript(props: { soulIdentity: SoulIdentity; timeline: TurnGroup[] }) { - const groups = annotateIdentityGroups(props.timeline); + const items = flattenTimelineItems(props.timeline); + const groups = buildTranscriptGroups(items); const empty = !props.timeline.some((group) => group.items.length || group.turn); const transcriptRef = useAppComponentRef({ domain: "message", @@ -25,10 +35,16 @@ export function Transcript(props: { soulIdentity: SoulIdentity; timeline: TurnGr }); return ( - + {groups.map((group) => ( - + ))} + {empty ? : null} ); @@ -44,34 +60,103 @@ function TranscriptEmpty() { ); } -function TurnGroupView(props: { group: AnnotatedTurnGroup; soulIdentity: SoulIdentity }) { - const { group, soulIdentity } = props; - const turn = group.turn; +function TranscriptGroupView(props: { + group: TranscriptGroup; + items: ReturnType; + soulIdentity: SoulIdentity; +}) { + const { group, items, soulIdentity } = props; + const identity = identityForGroup(group, items, soulIdentity); + const groupItems = items.slice(group.start, group.end); + let breakCursor = 0; + return ( - - {group.items.map(({ item, showIdentity }) => ( - - ))} - {turn?.status === "failed" ? ( - - - - TURN FAILED + } + side={group.side} + > + {groupItems.map((item, offset) => { + const absoluteIndex = group.start + offset; + const nextBreak = group.breaks[breakCursor]; + const divider = + nextBreak?.before === absoluteIndex ? ( + + + + ) : null; + if (divider) { + breakCursor += 1; + } + return ( + + {divider} + + + ); + })} + + ); +} + +function TurnStateNotices(props: { timeline: TurnGroup[] }) { + return ( + <> + {props.timeline.map((group) => { + const turn = group.turn; + if (turn?.status === "failed") { + return ( + + + + TURN FAILED + + {turn.error_text ?? "The turn failed without an error message."} + + + ); + } + if (turn?.status === "running" && !group.items.length) { + return ( + + Working… - {turn.error_text ?? "The turn failed without an error message."} - - - ) : null} - {turn?.status === "running" && !group.items.length ? ( - - Working… - - ) : null} - + ); + } + return null; + })} + ); } + +function identityForGroup( + group: TranscriptGroup, + items: ReturnType, + soulIdentity: SoulIdentity, +) { + if (group.actor === "account") { + const first = items[group.start]; + const actorId = first?.kind === "message" ? first.message.message.actor_id : "account"; + return { + avatarSeed: `account:${actorId}`, + name: "You", + }; + } + + if (group.actor === "system") { + return { + avatarSeed: "system", + name: "System", + }; + } + + return { + avatarSeed: soulIdentity.avatarSeed, + name: soulIdentity.name, + }; +} + +function alignForSide(side: TranscriptGroup["side"]) { + return side === "end" ? "end" : "start"; +} diff --git a/apps/client/soma/web/src/components/domains/message/InspectPanel/MessageInspectPanel.tsx b/apps/client/soma/web/src/components/domains/message/InspectPanel/MessageInspectPanel.tsx index 98e353d..030f64c 100644 --- a/apps/client/soma/web/src/components/domains/message/InspectPanel/MessageInspectPanel.tsx +++ b/apps/client/soma/web/src/components/domains/message/InspectPanel/MessageInspectPanel.tsx @@ -26,7 +26,7 @@ export function MessageInspectPanel(props: { messageId: string; runtime: Session props.runtime.messages.find((item) => item.message.id === props.messageId) ?? null; return ( - + MESSAGE diff --git a/apps/client/soma/web/src/components/domains/session/InspectPanel/SessionInspectPanel.tsx b/apps/client/soma/web/src/components/domains/session/InspectPanel/SessionInspectPanel.tsx index 0724802..41c8bd4 100644 --- a/apps/client/soma/web/src/components/domains/session/InspectPanel/SessionInspectPanel.tsx +++ b/apps/client/soma/web/src/components/domains/session/InspectPanel/SessionInspectPanel.tsx @@ -19,7 +19,7 @@ export function SessionInspectPanel(props: { runtime: SessionRuntimeSnapshot }) const memory = runtime.soul_session?.session_memory.trim() ?? ""; return ( - + } diff --git a/apps/client/soma/web/src/components/domains/tool-call/InspectPanel/ToolCallInspectPanel.tsx b/apps/client/soma/web/src/components/domains/tool-call/InspectPanel/ToolCallInspectPanel.tsx index d4f341d..50bf883 100644 --- a/apps/client/soma/web/src/components/domains/tool-call/InspectPanel/ToolCallInspectPanel.tsx +++ b/apps/client/soma/web/src/components/domains/tool-call/InspectPanel/ToolCallInspectPanel.tsx @@ -74,7 +74,7 @@ export function ToolCallInspectPanel(props: ToolCallInspectProps) { const ModePanel = inspectModeRegistry[mode]; return ( - + TOOL CALL diff --git a/apps/client/soma/web/src/components/domains/tool-call/transcript/ToolCallWhisper.tsx b/apps/client/soma/web/src/components/domains/tool-call/transcript/ToolCallWhisper.tsx index e4158ed..e43ed4c 100644 --- a/apps/client/soma/web/src/components/domains/tool-call/transcript/ToolCallWhisper.tsx +++ b/apps/client/soma/web/src/components/domains/tool-call/transcript/ToolCallWhisper.tsx @@ -17,20 +17,22 @@ type ToolResultItem = Extract; const COMMAND_LIMIT = 92; const OUTPUT_LIMIT = 120; -export function ToolCallWhisper(props: { item: ToolCallItem }) { +export function ToolCallWhisper(props: { item: ToolCallItem; showTimestamp?: boolean }) { return ( ); } -export function ToolResultWhisper(props: { item: ToolResultItem }) { +export function ToolResultWhisper(props: { item: ToolResultItem; showTimestamp?: boolean }) { return ( @@ -39,6 +41,7 @@ export function ToolResultWhisper(props: { item: ToolResultItem }) { function ToolWhisperBody(props: { createdAt: string; + showTimestamp?: boolean; toolCall: ToolCallRecord | null; toolResult: ToolResultRecord | null; }) { @@ -59,7 +62,9 @@ function ToolWhisperBody(props: { {failed ? "failed" : "completed"} - + {props.showTimestamp === false ? null : ( + + )} ); @@ -72,6 +77,7 @@ function ToolWhisperBody(props: { return ( @@ -80,6 +86,7 @@ function ToolWhisperBody(props: { return ( @@ -89,6 +96,7 @@ function ToolWhisperBody(props: { function ShellToolWhisper(props: { createdAt: string; + showTimestamp?: boolean; toolCall: ToolCallRecord; toolResult: ToolResultRecord | null; }) { @@ -118,7 +126,9 @@ function ShellToolWhisper(props: { {outputLabel}: {truncateMiddle(output, OUTPUT_LIMIT)} ) : null} - + {props.showTimestamp === false ? null : ( + + )} ); @@ -126,6 +136,7 @@ function ShellToolWhisper(props: { function UnknownToolWhisper(props: { createdAt: string; + showTimestamp?: boolean; toolCall: ToolCallRecord; toolResult: ToolResultRecord | null; }) { @@ -144,7 +155,9 @@ function UnknownToolWhisper(props: { {status} - + {props.showTimestamp === false ? null : ( + + )} ); diff --git a/apps/client/soma/web/src/events/inspect.ts b/apps/client/soma/web/src/events/inspect.ts index f585409..ad887b4 100644 --- a/apps/client/soma/web/src/events/inspect.ts +++ b/apps/client/soma/web/src/events/inspect.ts @@ -10,9 +10,28 @@ export interface InspectTargetSelectedEvent { target: InspectTarget; } -const INSPECT_TARGET_SELECTED = "mini-stim:inspect-target-selected"; +export interface InspectPanelOpenedEvent { + type: "inspect:panel:opened"; +} + +export interface InspectPanelClosedEvent { + type: "inspect:panel:closed"; +} + +export interface InspectPanelToggledEvent { + type: "inspect:panel:toggled"; +} + +export type InspectEvent = + | InspectPanelClosedEvent + | InspectPanelOpenedEvent + | InspectPanelToggledEvent + | InspectTargetSelectedEvent; + +const INSPECT_EVENT = "mini-stim:inspect"; type InspectListener = (event: InspectTargetSelectedEvent) => void; +type InspectEventListener = (event: InspectEvent) => void; export function selectInspectTarget(target: InspectTarget) { dispatchInspectEvent({ @@ -21,18 +40,86 @@ export function selectInspectTarget(target: InspectTarget) { }); } +export function openInspectPanel() { + dispatchInspectEvent({ + type: "inspect:panel:opened", + }); +} + +export function closeInspectPanel() { + dispatchInspectEvent({ + type: "inspect:panel:closed", + }); +} + +export function toggleInspectPanel() { + dispatchInspectEvent({ + type: "inspect:panel:toggled", + }); +} + +export function subscribeInspect(listener: InspectEventListener) { + function handleEvent(event: Event) { + const detail = (event as CustomEvent).detail; + if (isInspectEvent(detail)) { + listener(detail); + } + } + + window.addEventListener(INSPECT_EVENT, handleEvent); + return () => window.removeEventListener(INSPECT_EVENT, handleEvent); +} + export function subscribeInspectTarget(listener: InspectListener) { function handleEvent(event: Event) { - const detail = (event as CustomEvent).detail; + const detail = (event as CustomEvent).detail; if (detail?.type === "inspect:target:selected") { listener(detail); } } - window.addEventListener(INSPECT_TARGET_SELECTED, handleEvent); - return () => window.removeEventListener(INSPECT_TARGET_SELECTED, handleEvent); + window.addEventListener(INSPECT_EVENT, handleEvent); + return () => window.removeEventListener(INSPECT_EVENT, handleEvent); } -function dispatchInspectEvent(event: InspectTargetSelectedEvent) { - window.dispatchEvent(new CustomEvent(INSPECT_TARGET_SELECTED, { detail: event })); +function dispatchInspectEvent(event: InspectEvent) { + window.dispatchEvent(new CustomEvent(INSPECT_EVENT, { detail: event })); +} + +function isInspectEvent(value: unknown): value is InspectEvent { + if (typeof value !== "object" || value === null || !("type" in value)) { + return false; + } + + const event = value as Partial; + return ( + event.type === "inspect:panel:closed" || + event.type === "inspect:panel:opened" || + event.type === "inspect:panel:toggled" || + (event.type === "inspect:target:selected" && isInspectTarget(event.target)) + ); +} + +function isInspectTarget(value: unknown): value is InspectTarget { + if (typeof value !== "object" || value === null || !("kind" in value)) { + return false; + } + + const target = value as Record; + switch (target.kind) { + case "session": + return typeof target.sessionId === "string"; + case "turn": + return typeof target.sessionId === "string" && typeof target.turnId === "string"; + case "message": + return typeof target.sessionId === "string" && typeof target.messageId === "string"; + case "tool_call": + return typeof target.sessionId === "string" && typeof target.toolCallId === "string"; + case "tool_result": + return typeof target.sessionId === "string" && typeof target.toolResultId === "string"; + case undefined: + return false; + default: + return false; + } } diff --git a/apps/client/soma/web/src/events/navigation.ts b/apps/client/soma/web/src/events/navigation.ts new file mode 100644 index 0000000..6cf157d --- /dev/null +++ b/apps/client/soma/web/src/events/navigation.ts @@ -0,0 +1,58 @@ +import type { NavigationMode } from "../navigationMode"; + +export interface NavigationModeSelectedEvent { + mode: NavigationMode; + type: "navigation:mode-selected"; +} + +export interface NavigationPanelToggledEvent { + type: "navigation:panel-toggled"; +} + +export type NavigationEvent = NavigationModeSelectedEvent | NavigationPanelToggledEvent; + +const NAVIGATION_EVENT = "mini-stim:navigation"; + +type NavigationListener = (event: NavigationEvent) => void; + +export function selectNavigationMode(mode: NavigationMode) { + dispatchNavigationEvent({ + mode, + type: "navigation:mode-selected", + }); +} + +export function toggleNavigationPanel() { + dispatchNavigationEvent({ + type: "navigation:panel-toggled", + }); +} + +export function subscribeNavigation(listener: NavigationListener) { + function handleEvent(event: Event) { + const detail = (event as CustomEvent).detail; + if (isNavigationEvent(detail)) { + listener(detail); + } + } + + window.addEventListener(NAVIGATION_EVENT, handleEvent); + return () => window.removeEventListener(NAVIGATION_EVENT, handleEvent); +} + +function dispatchNavigationEvent(event: NavigationEvent) { + window.dispatchEvent(new CustomEvent(NAVIGATION_EVENT, { detail: event })); +} + +function isNavigationEvent(value: unknown): value is NavigationEvent { + if (typeof value !== "object" || value === null || !("type" in value)) { + return false; + } + + const event = value as Partial; + return ( + event.type === "navigation:panel-toggled" || + (event.type === "navigation:mode-selected" && + (event.mode === "sessions" || event.mode === "souls")) + ); +} diff --git a/apps/client/soma/web/src/projections/identityGrouping.ts b/apps/client/soma/web/src/projections/identityGrouping.ts deleted file mode 100644 index 6b05dcb..0000000 --- a/apps/client/soma/web/src/projections/identityGrouping.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { TimelineItem, TurnGroup } from "@mini-stim/hooks"; - -export interface AnnotatedTimelineItem { - item: TimelineItem; - showIdentity: boolean; -} - -export type AnnotatedTurnGroup = Omit & { - items: AnnotatedTimelineItem[]; -}; - -export function annotateIdentityGroups(groups: TurnGroup[]): AnnotatedTurnGroup[] { - let previousIdentityKey: string | null = null; - return groups.map((group) => ({ - ...group, - items: group.items.map((item) => { - const identityKey = messageIdentityKey(item); - const showIdentity = identityKey === null || identityKey !== previousIdentityKey; - previousIdentityKey = identityKey; - return { - item, - showIdentity, - }; - }), - })); -} - -function messageIdentityKey(item: TimelineItem) { - if (item.kind !== "message") { - return null; - } - const message = item.message.message; - return `${message.actor_type}:${message.actor_id}`; -} diff --git a/apps/client/soma/web/src/projections/transcriptGrouping.ts b/apps/client/soma/web/src/projections/transcriptGrouping.ts new file mode 100644 index 0000000..c23fa4f --- /dev/null +++ b/apps/client/soma/web/src/projections/transcriptGrouping.ts @@ -0,0 +1,108 @@ +import type { TimelineItem, TurnGroup } from "@mini-stim/hooks"; + +export type TranscriptActor = "account" | "soul" | "system"; +export type TranscriptSide = "start" | "end"; + +export interface TranscriptBreak { + before: number; + timestamp: string; +} + +export interface TranscriptGroup { + actor: TranscriptActor; + breaks: TranscriptBreak[]; + end: number; + id: string; + side: TranscriptSide; + start: number; + startedAt: string; +} + +type MutableTranscriptGroup = TranscriptGroup & { + lastSeenAt: string; +}; + +const TRANSCRIPT_GROUP_GAP_MS = 5 * 60 * 1000; + +export function flattenTimelineItems(groups: TurnGroup[]): TimelineItem[] { + const items: TimelineItem[] = []; + for (const group of groups) { + for (const item of group.items) { + items.push(item); + } + } + return items; +} + +export function buildTranscriptGroups(items: TimelineItem[]): TranscriptGroup[] { + const groups: MutableTranscriptGroup[] = []; + let current: MutableTranscriptGroup | undefined; + + for (let index = 0; index < items.length; index += 1) { + const item = items[index]; + const actor = actorForItem(item); + const timestamp = item.createdAt; + + if (!current || current.actor !== actor) { + current = acquireTranscriptGroup(groups, item, index, actor); + continue; + } + + if (isLongGap(current.lastSeenAt, timestamp)) { + current.breaks.push({ before: index, timestamp }); + } + + current.end = index + 1; + current.lastSeenAt = timestamp; + } + + return groups.map(({ lastSeenAt: _lastSeenAt, ...group }) => group); +} + +function acquireTranscriptGroup( + groups: MutableTranscriptGroup[], + item: TimelineItem, + index: number, + actor: TranscriptActor, +): MutableTranscriptGroup { + const group: MutableTranscriptGroup = { + actor, + breaks: [], + end: index + 1, + id: `${actor}:${index}:${item.id}`, + side: sideForActor(actor), + start: index, + startedAt: item.createdAt, + lastSeenAt: item.createdAt, + }; + groups.push(group); + return group; +} + +function actorForItem(item: TimelineItem): TranscriptActor { + if (item.kind !== "message") { + return "soul"; + } + + switch (item.message.message.actor_type) { + case "account": + return "account"; + case "system": + return "system"; + default: + return "soul"; + } +} + +function sideForActor(actor: TranscriptActor): TranscriptSide { + return actor === "account" ? "end" : "start"; +} + +function isLongGap(previous: string, next: string): boolean { + const previousMs = Date.parse(previous); + const nextMs = Date.parse(next); + if (!Number.isFinite(previousMs) || !Number.isFinite(nextMs)) { + return false; + } + return nextMs - previousMs > TRANSCRIPT_GROUP_GAP_MS; +} diff --git a/packages/components/package.json b/packages/components/package.json index d5bff83..f82bcfc 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -14,7 +14,9 @@ "dayjs": "1.11.21", "lucide-react": "1.18.0", "prismjs": "1.30.0", - "react": "19.2.7" + "react": "19.2.7", + "react-markdown": "10.1.0", + "remark-gfm": "4.0.1" }, "devDependencies": { "@biomejs/biome": "2.5.0", diff --git a/packages/components/src/atoms/AppRoot/AppRoot.tsx b/packages/components/src/atoms/AppRoot/AppRoot.tsx index 323f65b..c899f51 100644 --- a/packages/components/src/atoms/AppRoot/AppRoot.tsx +++ b/packages/components/src/atoms/AppRoot/AppRoot.tsx @@ -414,12 +414,12 @@ const inspectionShadowCss = ` color: rgba(31, 82, 120, 0.72); cursor: pointer; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; - font-size: 10px; + font-size: 8px; font-weight: 620; line-height: 1.25; - max-width: 18rem; + max-width: 13.5rem; overflow: hidden; - padding: 1px 6px; + padding: 1px 4px; pointer-events: auto; position: fixed; text-overflow: ellipsis; @@ -451,12 +451,12 @@ const inspectionShadowCss = ` 0 10px 28px -18px rgba(25, 42, 59, 0.38); color: rgba(31, 82, 120, 0.92); font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; - font-size: 11px; + font-size: 9px; font-weight: 640; line-height: 1.25; - max-width: min(22rem, calc(100vw - 2rem)); + max-width: min(16.5rem, calc(100vw - 2rem)); overflow: hidden; - padding: 5px 10px; + padding: 4px 8px; text-overflow: ellipsis; user-select: none; white-space: nowrap; diff --git a/packages/components/src/atoms/Avatar/Avatar.scss b/packages/components/src/atoms/Avatar/Avatar.scss index 5fe6568..d96ebd9 100644 --- a/packages/components/src/atoms/Avatar/Avatar.scss +++ b/packages/components/src/atoms/Avatar/Avatar.scss @@ -38,20 +38,20 @@ } &--size-sm { - width: 1.5rem; - height: 1.5rem; + width: 1.125rem; + height: 1.125rem; font-size: var(--ms-font-size-xs); } &--size-md { - width: 2rem; - height: 2rem; + width: 1.5rem; + height: 1.5rem; font-size: var(--ms-font-size-sm); } &--size-lg { - width: 2.5rem; - height: 2.5rem; + width: 1.875rem; + height: 1.875rem; font-size: var(--ms-font-size-md); } } diff --git a/packages/components/src/atoms/Badge/Badge.scss b/packages/components/src/atoms/Badge/Badge.scss index 18a32d4..49e93e8 100644 --- a/packages/components/src/atoms/Badge/Badge.scss +++ b/packages/components/src/atoms/Badge/Badge.scss @@ -7,7 +7,7 @@ align-items: center; justify-content: center; min-width: 0; - min-height: 1.5rem; + min-height: 1.125rem; border: 1px solid var(--ms-badge-border); border-radius: 999px; background: var(--ms-badge-bg); @@ -18,12 +18,12 @@ white-space: nowrap; &--size-sm { - padding: 0.125rem 0.5rem; + padding: 0.0625rem 0.375rem; font-size: var(--ms-font-size-xs); } &--size-md { - padding: 0.1875rem 0.625rem; + padding: 0.125rem 0.4375rem; font-size: var(--ms-font-size-sm); } diff --git a/packages/components/src/atoms/Button/Button.scss b/packages/components/src/atoms/Button/Button.scss index 4e08db9..5a3ad85 100644 --- a/packages/components/src/atoms/Button/Button.scss +++ b/packages/components/src/atoms/Button/Button.scss @@ -3,17 +3,17 @@ $button-sizes: ( "sm": ( - min-height: 2rem, + min-height: 1.75rem, font-size: var(--ms-font-size-sm), padding-inline: var(--ms-space-3), ), "md": ( - min-height: 2.5rem, + min-height: 2.125rem, font-size: var(--ms-font-size-md), padding-inline: var(--ms-space-4), ), "lg": ( - min-height: 3.25rem, + min-height: 3rem, font-size: var(--ms-font-size-md), padding-inline: var(--ms-space-4), ), diff --git a/packages/components/src/atoms/DockShell/DockShell.scss b/packages/components/src/atoms/DockShell/DockShell.scss index 27552a5..27b163b 100644 --- a/packages/components/src/atoms/DockShell/DockShell.scss +++ b/packages/components/src/atoms/DockShell/DockShell.scss @@ -1,5 +1,5 @@ .msDockShell { - --ms-dock-shell-width: 3rem; + --ms-dock-shell-width: 2.5rem; --ms-dock-shell-dock-padding-block-start: var(--ms-space-2); --ms-dock-shell-dock-padding-block-end: var(--ms-space-1); --ms-dock-shell-dock-padding-inline: calc(var(--ms-space-1) / 2); diff --git a/packages/components/src/atoms/Grid/Grid.scss b/packages/components/src/atoms/Grid/Grid.scss index ba54961..c9e3a37 100644 --- a/packages/components/src/atoms/Grid/Grid.scss +++ b/packages/components/src/atoms/Grid/Grid.scss @@ -6,8 +6,8 @@ $grid-gaps: ( ); .msGrid { - --ms-grid-sidebar-width: 19rem; - --ms-grid-inspect-width: 22rem; + --ms-grid-sidebar-width: 17rem; + --ms-grid-inspect-width: 20rem; display: grid; width: 100%; @@ -21,7 +21,7 @@ $grid-gaps: ( } &--template-sidebar-main { - grid-template-columns: minmax(16.5rem, 19rem) minmax(0, 1fr); + grid-template-columns: minmax(15rem, 17rem) minmax(0, 1fr); grid-template-areas: "sidebar main"; @media (max-width: 48rem) { @@ -33,11 +33,31 @@ $grid-gaps: ( } } + &--template-main { + grid-template-columns: minmax(0, 1fr); + grid-template-areas: "main"; + } + + &--template-main-inspect { + grid-template-columns: + minmax(22rem, 1fr) + minmax(16rem, var(--ms-grid-inspect-width)); + grid-template-areas: "main inspect"; + + @media (max-width: 48rem) { + grid-template-columns: 1fr; + grid-template-rows: minmax(0, 1fr) minmax(12rem, 32vh); + grid-template-areas: + "main" + "inspect"; + } + } + &--template-sidebar-main-inspect { grid-template-columns: - minmax(16.5rem, var(--ms-grid-sidebar-width)) - minmax(24rem, 1fr) - minmax(18rem, var(--ms-grid-inspect-width)); + minmax(15rem, var(--ms-grid-sidebar-width)) + minmax(22rem, 1fr) + minmax(16rem, var(--ms-grid-inspect-width)); grid-template-areas: "sidebar main inspect"; @media (max-width: 48rem) { diff --git a/packages/components/src/atoms/Grid/Grid.tsx b/packages/components/src/atoms/Grid/Grid.tsx index 57295e0..0ead713 100644 --- a/packages/components/src/atoms/Grid/Grid.tsx +++ b/packages/components/src/atoms/Grid/Grid.tsx @@ -8,7 +8,7 @@ type GridProps = ComponentPropsWithoutRef<"div"> & { grow?: boolean; inspectWidthPx?: number | null; sidebarWidthPx?: number | null; - template?: "sidebar-main" | "sidebar-main-inspect"; + template?: "main" | "main-inspect" | "sidebar-main" | "sidebar-main-inspect"; }; type GridItemProps = ComponentPropsWithoutRef<"div"> & { diff --git a/packages/components/src/atoms/IconButton/IconButton.scss b/packages/components/src/atoms/IconButton/IconButton.scss index d692d7b..8ddd71c 100644 --- a/packages/components/src/atoms/IconButton/IconButton.scss +++ b/packages/components/src/atoms/IconButton/IconButton.scss @@ -8,8 +8,8 @@ --ms-icon-button-hover-border: var(--ms-color-border-strong); display: inline-grid; - width: 2.625rem; - height: 2.625rem; + width: 2.25rem; + height: 2.25rem; place-items: center; border: 1px solid var(--ms-icon-button-border); border-radius: var(--ms-radius-md); @@ -29,14 +29,23 @@ @include focus-ring; + &--variant-ghost { + --ms-icon-button-bg: transparent; + --ms-icon-button-border: transparent; + --ms-icon-button-text: var(--ms-color-text-muted); + --ms-icon-button-hover-bg: var(--ms-color-ghost-hover-bg); + --ms-icon-button-hover-border: transparent; + box-shadow: none; + } + &--size-sm { - width: 2rem; - height: 2rem; + width: 1.75rem; + height: 1.75rem; } &--size-md { - width: 2.625rem; - height: 2.625rem; + width: 2.25rem; + height: 2.25rem; } &--tone-accent { @@ -46,4 +55,24 @@ --ms-icon-button-hover-bg: var(--ms-color-accent-hover); --ms-icon-button-hover-border: var(--ms-color-accent-hover); } + + &--variant-ghost.msIconButton--tone-accent { + --ms-icon-button-bg: transparent; + --ms-icon-button-border: transparent; + --ms-icon-button-text: var(--ms-color-accent); + --ms-icon-button-hover-bg: var(--ms-color-ghost-hover-bg); + --ms-icon-button-hover-border: transparent; + box-shadow: none; + } + + &[aria-pressed="mixed"] { + --ms-icon-button-bg: color-mix(in srgb, var(--ms-color-selected-bg) 42%, transparent); + --ms-icon-button-border: color-mix(in srgb, var(--ms-color-selected-border) 58%, transparent); + --ms-icon-button-text: var(--ms-color-accent); + --ms-icon-button-hover-bg: var(--ms-color-selected-bg); + --ms-icon-button-hover-border: var(--ms-color-selected-border); + box-shadow: + inset 0 0 0 1px rgba(255, 255, 255, 0.34), + 0 1px 0 rgba(35, 67, 88, 0.06); + } } diff --git a/packages/components/src/atoms/IconButton/IconButton.tsx b/packages/components/src/atoms/IconButton/IconButton.tsx index 747fc4e..9fa7e8c 100644 --- a/packages/components/src/atoms/IconButton/IconButton.tsx +++ b/packages/components/src/atoms/IconButton/IconButton.tsx @@ -8,6 +8,7 @@ type IconButtonProps = ComponentPropsWithRef<"button"> & { label: string; size?: "sm" | "md"; tone?: "neutral" | "accent"; + variant?: "solid" | "ghost"; }; export function IconButton({ @@ -17,6 +18,7 @@ export function IconButton({ size = "md", tone = "neutral", type = "button", + variant = "solid", ...props }: IconButtonProps) { return ( @@ -27,6 +29,7 @@ export function IconButton({ "msIconButton", `msIconButton--size-${size}`, `msIconButton--tone-${tone}`, + `msIconButton--variant-${variant}`, className, )} aria-label={label} diff --git a/packages/components/src/atoms/MarkdownText/MarkdownText.scss b/packages/components/src/atoms/MarkdownText/MarkdownText.scss new file mode 100644 index 0000000..cfef0cd --- /dev/null +++ b/packages/components/src/atoms/MarkdownText/MarkdownText.scss @@ -0,0 +1,133 @@ +.msMarkdownText { + min-width: 0; + color: var(--ms-color-text); + font-size: var(--ms-font-size-md); + line-height: 1.45; + overflow-wrap: anywhere; + + &--tone-strong { + color: var(--ms-color-text-strong); + } + + > :first-child { + margin-block-start: 0; + } + + > :last-child { + margin-block-end: 0; + } + + p { + margin-block: 0; + } + + p + p, + p + ul, + p + ol, + p + blockquote, + p + .msCodeBlock, + ul + p, + ol + p, + blockquote + p, + .msCodeBlock + p, + .msMarkdownText__tableScroll + p { + margin-block-start: var(--ms-space-2); + } + + ul, + ol { + margin-block: var(--ms-space-1) 0; + padding-inline-start: 1.25em; + } + + li { + margin-block: 0; + padding-inline-start: 0.2em; + } + + li + li { + margin-block-start: var(--ms-space-1); + } + + li > p { + margin-block: 0; + } + + li > ul, + li > ol { + margin-block-start: var(--ms-space-1); + padding-inline-start: 1.15em; + } + + li::marker { + color: var(--ms-color-text-subtle); + } + + blockquote { + margin: var(--ms-space-2) 0; + border-inline-start: 2px solid color-mix(in srgb, var(--ms-color-border) 84%, transparent); + padding-inline-start: var(--ms-space-3); + color: var(--ms-color-text-muted); + } + + a { + color: var(--ms-color-surface-accent-text); + text-decoration-color: color-mix(in srgb, currentColor 44%, transparent); + text-decoration-line: underline; + text-underline-offset: 0.14em; + } + + a:hover { + text-decoration-color: currentColor; + } + + .msCodeBlock { + margin-block: var(--ms-space-2); + max-height: min(18rem, 46vh); + } + + .msCodeText { + display: inline; + border: 0; + background: color-mix(in srgb, var(--ms-color-code-bg) 42%, transparent); + padding: 0 0.18em; + font-size: 0.94em; + line-height: inherit; + vertical-align: baseline; + white-space: normal; + } + + &__tableScroll { + max-width: 100%; + overflow-x: auto; + margin-block: var(--ms-space-2); + } + + table { + width: max-content; + min-width: min(100%, 24rem); + border-collapse: collapse; + font-size: var(--ms-font-size-sm); + } + + th, + td { + border: 1px solid color-mix(in srgb, var(--ms-color-border) 70%, transparent); + padding: var(--ms-space-1) var(--ms-space-2); + text-align: start; + vertical-align: top; + } + + th { + background: color-mix(in srgb, var(--ms-color-surface-muted-bg) 54%, transparent); + color: var(--ms-color-text-strong); + font-weight: 680; + } + + input[type="checkbox"] { + width: 0.85em; + height: 0.85em; + margin: 0 0.35em 0 0; + vertical-align: -0.08em; + } +} diff --git a/packages/components/src/atoms/MarkdownText/MarkdownText.tsx b/packages/components/src/atoms/MarkdownText/MarkdownText.tsx new file mode 100644 index 0000000..f0d26a5 --- /dev/null +++ b/packages/components/src/atoms/MarkdownText/MarkdownText.tsx @@ -0,0 +1,107 @@ +import type { ComponentPropsWithoutRef, MouseEvent, ReactNode } from "react"; +import ReactMarkdown, { type Components } from "react-markdown"; +import remarkGfm from "remark-gfm"; + +import { cx } from "../../internal/cx"; +import { CodeBlock } from "../CodeBlock/CodeBlock"; +import type { CodeLanguage } from "../CodeBlock/codeHighlight"; +import { CodeText } from "../CodeText/CodeText"; +import "./MarkdownText.scss"; + +type MarkdownTextProps = Omit, "children"> & { + children: string; + tone?: "default" | "strong"; +}; + +export function MarkdownText({ + children, + className, + tone = "default", + ...props +}: MarkdownTextProps) { + return ( +
+ + {children} + +
+ ); +} + +const markdownComponents: Components = { + a({ children, href }) { + return ( + + {children} + + ); + }, + blockquote({ children }) { + return
{children}
; + }, + code({ children, className }) { + const source = textFromChildren(children).replace(/\n$/, ""); + const language = languageFromClassName(className); + if (className?.startsWith("language-")) { + return {source}; + } + return {children}; + }, + img({ alt, src }) { + if (src) { + return ( + + {alt || src} + + ); + } + return {alt}; + }, + input({ checked, type }) { + if (type !== "checkbox") { + return null; + } + return ; + }, + pre({ children }) { + return <>{children}; + }, + table({ children }) { + return ( +
+ {children}
+
+ ); + }, +}; + +function stopMessageActivation(event: MouseEvent) { + event.stopPropagation(); +} + +function languageFromClassName(className?: string): CodeLanguage | undefined { + const match = className?.match(/(?:^|\s)language-(\S+)/); + switch (match?.[1]) { + case "bash": + case "sh": + case "shell": + return "shell"; + default: + return undefined; + } +} + +function textFromChildren(children: ReactNode): string { + if (typeof children === "string" || typeof children === "number") { + return String(children); + } + if (Array.isArray(children)) { + return children.map(textFromChildren).join(""); + } + return ""; +} diff --git a/packages/components/src/atoms/Panel/Panel.scss b/packages/components/src/atoms/Panel/Panel.scss index 1d7ce9c..e28f361 100644 --- a/packages/components/src/atoms/Panel/Panel.scss +++ b/packages/components/src/atoms/Panel/Panel.scss @@ -63,6 +63,7 @@ &__body { position: relative; display: flex; + flex-direction: column; grid-row: 2; min-height: 0; overflow: hidden; @@ -70,22 +71,13 @@ } &__body--tone-inset { - background: transparent; - - &::before { - content: ""; - position: absolute; - inset: 0; - background: - linear-gradient(180deg, var(--ms-color-panel-inset-feather), rgba(240, 237, 230, 0) 1.75rem), - linear-gradient(0deg, var(--ms-color-panel-inset-feather), rgba(240, 237, 230, 0) 1.75rem), - linear-gradient(180deg, rgba(115, 97, 74, 0.06), rgba(115, 97, 74, 0) 7rem), - radial-gradient(circle at top, rgba(255, 255, 255, 0.18), transparent 22rem), - color-mix(in srgb, var(--ms-color-surface-inset-bg) 62%, transparent); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.28); - pointer-events: none; - z-index: 0; - } + background: + linear-gradient(180deg, var(--ms-color-panel-inset-feather), rgba(240, 237, 230, 0) 1.75rem), + linear-gradient(0deg, var(--ms-color-panel-inset-feather), rgba(240, 237, 230, 0) 1.75rem), + linear-gradient(180deg, rgba(115, 97, 74, 0.06), rgba(115, 97, 74, 0) 7rem), + radial-gradient(circle at top, rgba(255, 255, 255, 0.18), transparent 22rem), + color-mix(in srgb, var(--ms-color-surface-inset-bg) 62%, transparent); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.28); } &__body--scroll { diff --git a/packages/components/src/atoms/TextArea/TextArea.scss b/packages/components/src/atoms/TextArea/TextArea.scss index 286931f..9b84090 100644 --- a/packages/components/src/atoms/TextArea/TextArea.scss +++ b/packages/components/src/atoms/TextArea/TextArea.scss @@ -34,7 +34,7 @@ .msTextArea { display: block; width: 100%; - min-height: 5.25rem; + min-height: 4.5rem; overscroll-behavior: contain; scrollbar-gutter: stable; border: 1px solid var(--ms-color-input-border); @@ -42,6 +42,7 @@ background: var(--ms-color-input-bg); color: var(--ms-color-text); padding: var(--ms-space-3); + font-size: var(--ms-font-size-md); line-height: 1.45; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.75); @include transition(background-color, border-color, box-shadow); @@ -65,13 +66,21 @@ &--variant-composer { --ms-textarea-frame-border: var(--ms-color-dock-border); --ms-textarea-frame-bg: var(--ms-color-input-dock-bg); + --ms-textarea-border-width: 1px; + --ms-textarea-optical-center-offset: 1px; + --ms-textarea-single-line-block-space: calc( + (3rem - (var(--ms-textarea-border-width) * 2) - (var(--ms-font-size-md) * 1.5)) / 2 + ); --ms-textarea-frame-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.9), 0 1px 0 var(--ms-color-border-faint); - min-height: 3.5rem; + min-height: 3rem; border-color: var(--ms-color-dock-border); background: var(--ms-color-input-dock-bg); - padding: var(--ms-space-4); + padding-block: + calc(var(--ms-textarea-single-line-block-space) + var(--ms-textarea-optical-center-offset)) + calc(var(--ms-textarea-single-line-block-space) - var(--ms-textarea-optical-center-offset)); + padding-inline: var(--ms-space-3); line-height: 1.5; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.9), diff --git a/packages/components/src/icons/ChevronRightIcon.tsx b/packages/components/src/icons/ChevronRightIcon.tsx new file mode 100644 index 0000000..b5b67d5 --- /dev/null +++ b/packages/components/src/icons/ChevronRightIcon.tsx @@ -0,0 +1,7 @@ +import { ChevronRight } from "lucide-react"; + +import { Icon, type IconProps } from "./Icon/Icon"; + +export function ChevronRightIcon(props: Omit) { + return ; +} diff --git a/packages/components/src/icons/Icon/Icon.scss b/packages/components/src/icons/Icon/Icon.scss index decd9fb..f4f4f66 100644 --- a/packages/components/src/icons/Icon/Icon.scss +++ b/packages/components/src/icons/Icon/Icon.scss @@ -4,17 +4,17 @@ color: currentColor; &--size-sm { - width: 1rem; - height: 1rem; + width: 0.75rem; + height: 0.75rem; } &--size-md { - width: 1.125rem; - height: 1.125rem; + width: 0.875rem; + height: 0.875rem; } &--size-lg { - width: 1.25rem; - height: 1.25rem; + width: 1rem; + height: 1rem; } } diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index a5452de..71f7f47 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -18,6 +18,7 @@ export { Heading } from "./atoms/Heading/Heading"; export { IconButton } from "./atoms/IconButton/IconButton"; export { Inline } from "./atoms/Inline/Inline"; export { Input } from "./atoms/Input/Input"; +export { MarkdownText } from "./atoms/MarkdownText/MarkdownText"; export { Notice } from "./atoms/Notice/Notice"; export { Pane } from "./atoms/Pane/Pane"; export { Panel } from "./atoms/Panel/Panel"; @@ -29,10 +30,16 @@ export { Surface } from "./atoms/Surface/Surface"; export { Text } from "./atoms/Text/Text"; export { TextArea } from "./atoms/TextArea/TextArea"; export { Timestamp } from "./atoms/Timestamp/Timestamp"; +export { ChevronRightIcon } from "./icons/ChevronRightIcon"; export { Icon } from "./icons/Icon/Icon"; export { PlusIcon } from "./icons/PlusIcon"; export { SendIcon } from "./icons/SendIcon"; export { SessionsIcon } from "./icons/SessionsIcon"; export { SoulsIcon } from "./icons/SoulsIcon"; +export { + AnchoredContentGroup, + AnchoredContentGroupDivider, + AnchoredContentGroupItem, +} from "./patterns/AnchoredContentGroup/AnchoredContentGroup"; export { FieldActionLayout } from "./patterns/FieldActionLayout/FieldActionLayout"; export { IdentityLine } from "./patterns/IdentityLine/IdentityLine"; diff --git a/packages/components/src/patterns/AnchoredContentGroup/AnchoredContentGroup.scss b/packages/components/src/patterns/AnchoredContentGroup/AnchoredContentGroup.scss new file mode 100644 index 0000000..68c7602 --- /dev/null +++ b/packages/components/src/patterns/AnchoredContentGroup/AnchoredContentGroup.scss @@ -0,0 +1,184 @@ +@use "../../theme/mixins" as *; + +.msAnchoredContentGroup { + --ms-anchored-group-avatar-size: 1.5rem; + --ms-anchored-group-avatar-intrusion: calc(var(--ms-anchored-group-avatar-size) / 2); + --ms-anchored-group-content-inset: calc( + var(--ms-anchored-group-avatar-intrusion) + 0.125rem + ); + --ms-anchored-group-side-padding: var(--ms-space-3); + --ms-anchored-group-item-max-width: min(42rem, 100%); + + position: relative; + display: flex; + min-width: 0; + flex-direction: column; + margin-inline: var(--ms-space-5); + padding-block: var(--ms-space-5) var(--ms-space-3); + + &:first-child { + margin-block-start: var(--ms-space-6); + } + + &__topDivider, + &__breakDivider { + display: flex; + align-items: center; + gap: var(--ms-space-2); + min-width: 0; + color: var(--ms-color-text-subtle); + } + + &__topDivider { + position: absolute; + z-index: 0; + top: 0; + right: 0; + left: 0; + transform: translateY(-50%); + } + + &__rule, + &__breakRule { + height: 1px; + min-width: var(--ms-space-5); + flex: 1 1 auto; + background: color-mix(in srgb, var(--ms-color-border) 46%, transparent); + } + + &__dividerLabel, + &__breakLabel { + position: relative; + z-index: 1; + flex: 0 0 auto; + line-height: 1; + } + + &__anchor { + position: absolute; + z-index: 2; + top: calc(-1 * var(--ms-anchored-group-avatar-intrusion)); + width: var(--ms-anchored-group-avatar-size); + height: var(--ms-anchored-group-avatar-size); + } + + &__content { + position: relative; + z-index: 1; + display: flex; + min-width: 0; + flex-direction: column; + } + + &__item { + position: relative; + display: flex; + width: 100%; + min-width: 0; + padding: var(--ms-space-1) 0; + border: 0; + background: transparent; + color: inherit; + cursor: pointer; + font: inherit; + text-align: left; + + @include control-disabled; + @include focus-ring; + + &::before { + content: ""; + position: absolute; + top: 0; + height: 1px; + opacity: 0; + background: color-mix(in srgb, var(--ms-color-border) 42%, transparent); + transition: opacity var(--ms-motion-fast) ease; + } + + &:first-child::before { + display: none; + } + } + + &:hover &__item::before, + &:focus-within &__item::before { + opacity: 1; + } + + &__itemBody { + display: flex; + width: fit-content; + max-width: var(--ms-anchored-group-item-max-width); + min-width: 0; + box-sizing: border-box; + flex-direction: column; + gap: var(--ms-space-1); + padding: var(--ms-space-1) 0; + border-radius: var(--ms-radius-md); + outline: 1px solid transparent; + outline-offset: var(--ms-space-1); + transition: outline-color var(--ms-motion-fast) ease; + } + + &__item:hover &__itemBody, + &__item:focus-visible &__itemBody { + outline-color: color-mix(in srgb, var(--ms-color-border) 48%, transparent); + } + + &__item--align-start { + justify-content: flex-start; + text-align: left; + } + + &__item--align-center { + justify-content: center; + text-align: center; + } + + &__item--align-end { + justify-content: flex-end; + text-align: right; + } + + &__breakDivider { + padding-block: var(--ms-space-3) var(--ms-space-2); + } + + &__breakRule { + flex-grow: 0; + flex: 1 1 min(6rem, 28%); + max-width: 6rem; + background: color-mix(in srgb, var(--ms-color-border) 36%, transparent); + } + + &__breakLabel { + color: var(--ms-color-text-subtle); + } + + &--side-start { + padding-inline: var(--ms-anchored-group-content-inset) var(--ms-anchored-group-side-padding); + + .msAnchoredContentGroup__anchor { + left: calc(-1 * var(--ms-anchored-group-avatar-intrusion)); + } + + .msAnchoredContentGroup__item::before { + right: 0; + left: 0; + } + } + + &--side-end { + padding-inline: var(--ms-anchored-group-side-padding) var(--ms-anchored-group-content-inset); + + .msAnchoredContentGroup__anchor { + right: calc(-1 * var(--ms-anchored-group-avatar-intrusion)); + } + + .msAnchoredContentGroup__item::before { + right: 0; + left: 0; + } + } +} diff --git a/packages/components/src/patterns/AnchoredContentGroup/AnchoredContentGroup.tsx b/packages/components/src/patterns/AnchoredContentGroup/AnchoredContentGroup.tsx new file mode 100644 index 0000000..0fa7d01 --- /dev/null +++ b/packages/components/src/patterns/AnchoredContentGroup/AnchoredContentGroup.tsx @@ -0,0 +1,135 @@ +import type { + ComponentPropsWithoutRef, + KeyboardEventHandler, + MouseEventHandler, + ReactNode, + Ref, +} from "react"; + +import { Avatar } from "../../atoms/Avatar/Avatar"; +import { cx } from "../../internal/cx"; +import "./AnchoredContentGroup.scss"; + +type AnchoredContentGroupSide = "start" | "end"; + +type AnchoredContentGroupProps = ComponentPropsWithoutRef<"section"> & { + anchorLabel?: string; + anchorSeed: string; + children: ReactNode; + divider: ReactNode; + side?: AnchoredContentGroupSide; +}; + +type AnchoredContentGroupItemProps = ComponentPropsWithoutRef<"div"> & { + align?: "start" | "center" | "end"; + element?: "button" | "div"; + innerRef?: Ref; + type?: ComponentPropsWithoutRef<"button">["type"]; +}; + +type AnchoredContentGroupDividerProps = ComponentPropsWithoutRef<"div"> & { + children: ReactNode; +}; + +export function AnchoredContentGroup({ + anchorLabel, + anchorSeed, + children, + className, + divider, + side = "start", + ...props +}: AnchoredContentGroupProps) { + return ( +
+
+ + {divider} + +
+ +
{children}
+
+ ); +} + +export function AnchoredContentGroupItem({ + align = "start", + element = "button", + children, + className, + innerRef, + onClick, + onKeyDown, + tabIndex, + type = "button", + ...props +}: AnchoredContentGroupItemProps) { + const itemClassName = cx( + "msAnchoredContentGroup__item", + `msAnchoredContentGroup__item--align-${align}`, + className, + ); + const itemBody = {children}; + + if (element === "div") { + return ( + // biome-ignore lint/a11y/useSemanticElements: Markdown message bodies can contain anchors, so this cannot be a native button. +
{ + onKeyDown?.(event); + if (event.defaultPrevented) { + return; + } + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + event.currentTarget.click(); + } + }} + ref={innerRef as Ref} + role="button" + tabIndex={tabIndex ?? 0} + > + {itemBody} +
+ ); + } + + return ( + + ); +} + +export function AnchoredContentGroupDivider({ + children, + className, + ...props +}: AnchoredContentGroupDividerProps) { + return ( +
+ + {children} + +
+ ); +} diff --git a/packages/components/src/patterns/FieldActionLayout/FieldActionLayout.scss b/packages/components/src/patterns/FieldActionLayout/FieldActionLayout.scss index 7c474d2..fd6eec6 100644 --- a/packages/components/src/patterns/FieldActionLayout/FieldActionLayout.scss +++ b/packages/components/src/patterns/FieldActionLayout/FieldActionLayout.scss @@ -1,5 +1,5 @@ .msFieldActionLayout { - --ms-field-action-layout-action-width: 5.5rem; + --ms-field-action-layout-action-width: 5rem; display: grid; grid-template-columns: minmax(0, 1fr) var(--ms-field-action-layout-action-width); @@ -35,6 +35,6 @@ } &--action-width-lg { - --ms-field-action-layout-action-width: 5.5rem; + --ms-field-action-layout-action-width: 5rem; } } diff --git a/packages/components/src/theme/tokens.space.scss b/packages/components/src/theme/tokens.space.scss index 77ad4ce..d194d0e 100644 --- a/packages/components/src/theme/tokens.space.scss +++ b/packages/components/src/theme/tokens.space.scss @@ -1,8 +1,9 @@ :root { - --ms-space-1: 0.25rem; - --ms-space-2: 0.5rem; - --ms-space-3: 0.75rem; - --ms-space-4: 1rem; - --ms-space-5: 1.25rem; - --ms-space-6: 1.5rem; + --ms-density-scale: 0.75; + --ms-space-1: calc(0.25rem * var(--ms-density-scale)); + --ms-space-2: calc(0.5rem * var(--ms-density-scale)); + --ms-space-3: calc(0.75rem * var(--ms-density-scale)); + --ms-space-4: calc(1rem * var(--ms-density-scale)); + --ms-space-5: calc(1.25rem * var(--ms-density-scale)); + --ms-space-6: calc(1.5rem * var(--ms-density-scale)); } diff --git a/packages/components/src/theme/tokens.typography.scss b/packages/components/src/theme/tokens.typography.scss index d42a8e1..7201b1f 100644 --- a/packages/components/src/theme/tokens.typography.scss +++ b/packages/components/src/theme/tokens.typography.scss @@ -4,8 +4,10 @@ sans-serif; --ms-font-family-mono: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; - --ms-font-size-xs: 0.75rem; - --ms-font-size-sm: 0.8125rem; - --ms-font-size-md: 0.9375rem; - --ms-font-size-lg: 1rem; + --ms-font-scale: 0.8; + --ms-font-size-md: calc(0.9375rem * var(--ms-font-scale)); + --ms-font-step: 1px; + --ms-font-size-xs: calc(var(--ms-font-size-md) - (var(--ms-font-step) * 2)); + --ms-font-size-sm: calc(var(--ms-font-size-md) - var(--ms-font-step)); + --ms-font-size-lg: calc(var(--ms-font-size-md) + var(--ms-font-step)); } diff --git a/packages/storage/src/index.ts b/packages/storage/src/index.ts index 088c238..2951186 100644 --- a/packages/storage/src/index.ts +++ b/packages/storage/src/index.ts @@ -2,6 +2,11 @@ export interface InspectPanePreference { open: boolean; } +export interface NavigationPanePreference { + mode: "sessions" | "souls"; + open: boolean; +} + export interface DesktopLayoutPreference { railWidthPx: number | null; inspectWidthPx: number | null; @@ -9,6 +14,7 @@ export interface DesktopLayoutPreference { export interface UiPreferences { inspect: InspectPanePreference; + navigation: NavigationPanePreference; desktopLayout: DesktopLayoutPreference; } @@ -32,6 +38,10 @@ const DEFAULT_UI_PREFERENCES: UiPreferences = { inspect: { open: false, }, + navigation: { + mode: "sessions", + open: true, + }, desktopLayout: { railWidthPx: null, inspectWidthPx: null, @@ -68,6 +78,10 @@ export function defaultPreferences(): UiPreferences { inspect: { open: DEFAULT_UI_PREFERENCES.inspect.open, }, + navigation: { + mode: DEFAULT_UI_PREFERENCES.navigation.mode, + open: DEFAULT_UI_PREFERENCES.navigation.open, + }, desktopLayout: { railWidthPx: DEFAULT_UI_PREFERENCES.desktopLayout.railWidthPx, inspectWidthPx: DEFAULT_UI_PREFERENCES.desktopLayout.inspectWidthPx, @@ -115,11 +129,22 @@ function removePreferences(driver: StorageDriver | null) { function normalizePreferences(value: unknown): UiPreferences { const source = isRecord(value) ? value : {}; const inspect = isRecord(source.inspect) ? source.inspect : {}; + const navigation = isRecord(source.navigation) ? source.navigation : {}; const desktopLayout = isRecord(source.desktopLayout) ? source.desktopLayout : {}; return { inspect: { open: inspect.open === true, }, + navigation: { + mode: + navigation.mode === "sessions" || navigation.mode === "souls" + ? navigation.mode + : DEFAULT_UI_PREFERENCES.navigation.mode, + open: + typeof navigation.open === "boolean" + ? navigation.open + : DEFAULT_UI_PREFERENCES.navigation.open, + }, desktopLayout: { railWidthPx: nullablePositiveNumber(desktopLayout.railWidthPx), inspectWidthPx: nullablePositiveNumber(desktopLayout.inspectWidthPx), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4aee7b0..46859b2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -90,6 +90,12 @@ importers: react: specifier: 19.2.7 version: 19.2.7 + react-markdown: + specifier: 10.1.0 + version: 10.1.0(@types/react@19.2.17)(react@19.2.7) + remark-gfm: + specifier: 4.0.1 + version: 4.0.1 devDependencies: '@biomejs/biome': specifier: 2.5.0 @@ -690,15 +696,27 @@ packages: '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/debug@4.1.13': + resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} + '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node@25.9.3': resolution: {integrity: sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==} @@ -713,9 +731,15 @@ packages: '@types/react@19.2.17': resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@ungap/structured-clone@1.3.1': + resolution: {integrity: sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==} + '@vitejs/plugin-react@6.0.2': resolution: {integrity: sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -797,6 +821,9 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + balanced-match@4.0.4: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} @@ -805,14 +832,32 @@ packages: resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} engines: {node: 18 || 20 || >=22} + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + chokidar@5.0.0: resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} engines: {node: '>= 20.19.0'} + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + commander@14.0.3: resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} engines: {node: '>=20'} @@ -842,10 +887,20 @@ packages: supports-color: optional: true + decode-named-character-reference@1.3.0: + resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + enquirer@2.4.1: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} engines: {node: '>=8.6'} @@ -862,6 +917,13 @@ packages: engines: {node: '>=18'} hasBin: true + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} @@ -877,6 +939,9 @@ packages: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -924,6 +989,15 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + hast-util-to-jsx-runtime@2.3.6: + resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + html-url-attributes@3.0.1: + resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + human-signals@8.0.1: resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} engines: {node: '>=18.18.0'} @@ -931,6 +1005,18 @@ packages: immutable@5.1.6: resolution: {integrity: sha512-q1swsS8K7L8usSHuOqF2TAoCCkonYz0SG38wLAggaa4Wml70zixIvt2ql4coQ2C2B3hTjltJry4r6bULwgAXLQ==} + inline-style-parser@0.2.7: + resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -939,6 +1025,9 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + is-plain-obj@4.1.0: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} @@ -1057,6 +1146,9 @@ packages: resolution: {integrity: sha512-XT9ewWAC43tiAV7xDAPflMkG0qOPn2QjHqlgX8FOqmWa/rxnyYDulF9T0F7tRy1u+TVTmK/M//6VIOye+2zDXg==} engines: {node: '>=20'} + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + lucide-react@1.18.0: resolution: {integrity: sha512-LZDb7H/0YfM+RJncD0hDQRCAu+vSGODqpe35TuVI8EuXaRjkczbsx7p8dY4J87F/MUSj6bpYqeI8nw8qXaAdmA==} peerDependencies: @@ -1072,9 +1164,141 @@ packages: resolution: {integrity: sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ==} hasBin: true + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + + mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + + mdast-util-from-markdown@2.0.3: + resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==} + + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.1.0: + resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + + mdast-util-mdx-expression@2.0.1: + resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} + + mdast-util-mdx-jsx@3.2.0: + resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} + + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + mdurl@2.0.0: resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@2.1.1: + resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + minimatch@10.2.5: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} @@ -1116,6 +1340,9 @@ packages: resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + parse-ms@4.0.0: resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} engines: {node: '>=18'} @@ -1165,6 +1392,9 @@ packages: resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} engines: {node: '>=6'} + property-information@7.2.0: + resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==} + punycode.js@2.3.1: resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} engines: {node: '>=6'} @@ -1174,6 +1404,12 @@ packages: peerDependencies: react: ^19.2.7 + react-markdown@10.1.0: + resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==} + peerDependencies: + '@types/react': '>=18' + react: '>=18' + react@19.2.7: resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} engines: {node: '>=0.10.0'} @@ -1182,6 +1418,18 @@ packages: resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} engines: {node: '>= 20.19.0'} + remark-gfm@4.0.1: + resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + remeda@2.39.0: resolution: {integrity: sha512-3Ki8dU1o3OVu4dwIQ2Pj+yiuP7OnEbmWAGmJ3yDRqopily5jsj8NWzPvbS89H85d6UdONKEcUnrfuHY6jN9vyw==} engines: {node: '>=18.0.0'} @@ -1225,6 +1473,9 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -1235,6 +1486,9 @@ packages: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -1243,6 +1497,12 @@ packages: resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} engines: {node: '>=18'} + style-to-js@1.1.21: + resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} + + style-to-object@1.0.14: + resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -1258,6 +1518,12 @@ packages: resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -1295,10 +1561,34 @@ packages: resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} engines: {node: '>=18'} + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + vite@8.0.16: resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1406,6 +1696,9 @@ packages: resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} engines: {node: '>=18'} + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + snapshots: '@biomejs/biome@2.5.0': @@ -1860,14 +2153,28 @@ snapshots: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 + '@types/debug@4.1.13': + dependencies: + '@types/ms': 2.1.0 + '@types/deep-eql@4.0.2': {} + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.9 + '@types/estree@1.0.9': {} '@types/hast@3.0.4': dependencies: '@types/unist': 3.0.3 + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/ms@2.1.0': {} + '@types/node@25.9.3': dependencies: undici-types: 7.24.6 @@ -1882,8 +2189,12 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/unist@2.0.11': {} + '@types/unist@3.0.3': {} + '@ungap/structured-clone@1.3.1': {} + '@vitejs/plugin-react@6.0.2(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(yaml@2.9.0))': dependencies: '@rolldown/pluginutils': 1.0.1 @@ -1955,18 +2266,32 @@ snapshots: assertion-error@2.0.1: {} + bail@2.0.2: {} + balanced-match@4.0.4: {} brace-expansion@5.0.6: dependencies: balanced-match: 4.0.4 + ccount@2.0.1: {} + chai@6.2.2: {} + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + character-reference-invalid@2.0.1: {} + chokidar@5.0.0: dependencies: readdirp: 5.0.0 + comma-separated-tokens@2.0.3: {} + commander@14.0.3: {} compare-versions@6.1.1: {} @@ -1987,8 +2312,18 @@ snapshots: dependencies: ms: 2.1.3 + decode-named-character-reference@1.3.0: + dependencies: + character-entities: 2.0.2 + + dequal@2.0.3: {} + detect-libc@2.1.2: {} + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + enquirer@2.4.1: dependencies: ansi-colors: 4.1.3 @@ -2027,6 +2362,10 @@ snapshots: '@esbuild/win32-ia32': 0.28.1 '@esbuild/win32-x64': 0.28.1 + escape-string-regexp@5.0.0: {} + + estree-util-is-identifier-name@3.0.0: {} + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.9 @@ -2050,6 +2389,8 @@ snapshots: expect-type@1.3.0: {} + extend@3.0.2: {} + fast-deep-equal@3.1.3: {} fast-uri@3.1.2: {} @@ -2090,10 +2431,47 @@ snapshots: graceful-fs@4.2.11: {} + hast-util-to-jsx-runtime@2.3.6: + dependencies: + '@types/estree': 1.0.9 + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.2.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.21 + unist-util-position: 5.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + html-url-attributes@3.0.1: {} + human-signals@8.0.1: {} immutable@5.1.6: {} + inline-style-parser@0.2.7: {} + + is-alphabetical@2.0.1: {} + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + + is-decimal@2.0.1: {} + is-extglob@2.1.1: optional: true @@ -2102,6 +2480,8 @@ snapshots: is-extglob: 2.1.1 optional: true + is-hexadecimal@2.0.1: {} + is-plain-obj@4.1.0: {} is-stream@4.0.1: {} @@ -2185,6 +2565,8 @@ snapshots: dependencies: p-locate: 6.0.0 + longest-streak@3.1.0: {} + lucide-react@1.18.0(react@19.2.7): dependencies: react: 19.2.7 @@ -2204,8 +2586,354 @@ snapshots: punycode.js: 2.3.1 uc.micro: 2.1.0 + markdown-table@3.0.4: {} + + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + mdast-util-from-markdown@2.0.3: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.2 + micromark-util-character: 2.1.1 + + mdast-util-gfm-footnote@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.1.0: + dependencies: + mdast-util-from-markdown: 2.0.3 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.1.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-expression@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@3.2.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + mdast-util-mdxjs-esm@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.1 + + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.1 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.1.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdurl@2.0.0: {} + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-table@2.1.1: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.1 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.3.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.13 + debug: 4.4.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + minimatch@10.2.5: dependencies: brace-expansion: 5.0.6 @@ -2271,6 +2999,16 @@ snapshots: dependencies: p-limit: 4.0.0 + parse-entities@4.0.2: + dependencies: + '@types/unist': 2.0.11 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.3.0 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + parse-ms@4.0.0: {} path-key@3.1.1: {} @@ -2305,6 +3043,8 @@ snapshots: prismjs@1.30.0: {} + property-information@7.2.0: {} + punycode.js@2.3.1: {} react-dom@19.2.7(react@19.2.7): @@ -2312,10 +3052,62 @@ snapshots: react: 19.2.7 scheduler: 0.27.0 + react-markdown@10.1.0(@types/react@19.2.17)(react@19.2.7): + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/react': 19.2.17 + devlop: 1.1.0 + hast-util-to-jsx-runtime: 2.3.6 + html-url-attributes: 3.0.1 + mdast-util-to-hast: 13.2.1 + react: 19.2.7 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + unified: 11.0.5 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + react@19.2.7: {} readdirp@5.0.0: {} + remark-gfm@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.1.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.1 + unified: 11.0.5 + vfile: 6.0.3 + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + remeda@2.39.0: {} require-from-string@2.0.2: {} @@ -2365,18 +3157,33 @@ snapshots: source-map-js@1.2.1: {} + space-separated-tokens@2.0.2: {} + stackback@0.0.2: {} std-env@4.1.0: {} string-argv@0.3.2: {} + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 strip-final-newline@4.0.0: {} + style-to-js@1.1.21: + dependencies: + style-to-object: 1.0.14 + + style-to-object@1.0.14: + dependencies: + inline-style-parser: 0.2.7 + tinybench@2.9.0: {} tinyexec@1.2.4: {} @@ -2388,6 +3195,10 @@ snapshots: tinyrainbow@3.1.0: {} + trim-lines@3.0.1: {} + + trough@2.2.0: {} + tslib@2.8.1: optional: true @@ -2416,8 +3227,51 @@ snapshots: unicorn-magic@0.3.0: {} + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + universalify@2.0.1: {} + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(yaml@2.9.0): dependencies: lightningcss: 1.32.0 @@ -2474,3 +3328,5 @@ snapshots: yocto-queue@1.2.2: {} yoctocolors@2.1.2: {} + + zwitch@2.0.4: {}