@a4ui/core 0.24.2 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -11
- package/dist/{NumberInput-DcJlXNKq.js → NumberInput-BQhVucw-.js} +4 -4
- package/dist/commerce.js +1 -1
- package/dist/elements.css +127 -7
- package/dist/elements.iife.js +1 -1
- package/dist/elements.js +4 -4
- package/dist/full.css +127 -7
- package/dist/index.d.ts +12 -1
- package/dist/index.js +3072 -2624
- package/dist/lib/createOptimistic.d.ts +17 -0
- package/dist/lib/viewTransition.d.ts +8 -0
- package/dist/ui/ArtifactPanel.d.ts +29 -0
- package/dist/ui/ChatThread.d.ts +24 -0
- package/dist/ui/Citation.d.ts +44 -0
- package/dist/ui/FloatingToolbar.d.ts +26 -0
- package/dist/ui/InlineSelect.d.ts +33 -0
- package/dist/ui/Message.d.ts +28 -0
- package/dist/ui/PageTransition.d.ts +25 -0
- package/dist/ui/PromptComposer.d.ts +34 -0
- package/dist/ui/StreamingText.d.ts +26 -0
- package/package.json +1 -1
- package/preset.js +6 -1
- package/src/index.ts +14 -1
- package/src/lib/createOptimistic.ts +43 -0
- package/src/lib/viewTransition.ts +32 -0
- package/src/ui/ArtifactPanel.tsx +88 -0
- package/src/ui/Badge.tsx +7 -4
- package/src/ui/ChatThread.tsx +59 -0
- package/src/ui/Citation.tsx +97 -0
- package/src/ui/FloatingToolbar.tsx +75 -0
- package/src/ui/InlineSelect.tsx +101 -0
- package/src/ui/Message.tsx +69 -0
- package/src/ui/PageTransition.tsx +90 -0
- package/src/ui/PromptComposer.tsx +138 -0
- package/src/ui/StreamingText.tsx +98 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Holds an optimistic value that's updated immediately on `run`, then
|
|
3
|
+
* reconciled with the resolved (or rolled back on rejected) async result.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* const list = createOptimistic<Todo[]>([])
|
|
8
|
+
* async function addTodo(todo: Todo) {
|
|
9
|
+
* await list.run([...list.value(), todo], () => api.addTodo(todo))
|
|
10
|
+
* }
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
export declare function createOptimistic<T>(initial: T): {
|
|
14
|
+
value: () => T;
|
|
15
|
+
pending: () => boolean;
|
|
16
|
+
run: (optimistic: T, commit: () => Promise<T>) => Promise<T>;
|
|
17
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run `update` inside a View Transition when the browser supports it, so DOM
|
|
3
|
+
* changes cross-fade automatically; otherwise just run `update` synchronously.
|
|
4
|
+
* Respects reduced-motion (skips the transition).
|
|
5
|
+
* @example
|
|
6
|
+
* startViewTransition(() => setRoute('details'))
|
|
7
|
+
*/
|
|
8
|
+
export declare function startViewTransition(update: () => void): void;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { JSX } from 'solid-js';
|
|
2
|
+
export interface ArtifactPanelProps {
|
|
3
|
+
open: boolean;
|
|
4
|
+
onClose?: () => void;
|
|
5
|
+
title?: string;
|
|
6
|
+
/** Panel width in px when open. @default 420 */
|
|
7
|
+
width?: number;
|
|
8
|
+
children: JSX.Element;
|
|
9
|
+
class?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Right-side split panel that holds generated output (code, preview, doc) next
|
|
13
|
+
* to a chat — for AI UIs that keep the artifact and the conversation visible at
|
|
14
|
+
* once. Inline, not an overlay: place it as the last child of a flex row next
|
|
15
|
+
* to the main content. Slides open/closed by animating width (and a matching
|
|
16
|
+
* fade/slide on its content), reduced-motion aware via `motionReduced()`.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```tsx
|
|
20
|
+
* const [open, setOpen] = createSignal(false)
|
|
21
|
+
* <div class="flex h-full">
|
|
22
|
+
* <main class="min-w-0 flex-1">…chat…</main>
|
|
23
|
+
* <ArtifactPanel open={open()} onClose={() => setOpen(false)} title="script.py">
|
|
24
|
+
* <pre class="p-4 text-sm">…generated code…</pre>
|
|
25
|
+
* </ArtifactPanel>
|
|
26
|
+
* </div>
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export declare function ArtifactPanel(props: ArtifactPanelProps): JSX.Element;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { JSX } from 'solid-js';
|
|
2
|
+
export interface ChatThreadProps {
|
|
3
|
+
children: JSX.Element;
|
|
4
|
+
/** Max width of the reading column, in px. Defaults to `768`. */
|
|
5
|
+
maxWidth?: number;
|
|
6
|
+
/** Auto-scroll to the newest message on mount and whenever children change. Defaults to `true`. */
|
|
7
|
+
stickToBottom?: boolean;
|
|
8
|
+
class?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Scrollable, centered reading column that stacks chat messages. Layout-only:
|
|
12
|
+
* it renders whatever is passed as `children`, so pair it with your own
|
|
13
|
+
* message bubbles/components.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```tsx
|
|
17
|
+
* <ChatThread maxWidth={640}>
|
|
18
|
+
* <div>Hi! Ask me anything about your itinerary.</div>
|
|
19
|
+
* <div>What's the weather like in Rivertown this weekend?</div>
|
|
20
|
+
* <div>Sunny and mild, 18–22°C, light breeze on Saturday.</div>
|
|
21
|
+
* </ChatThread>
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export declare function ChatThread(props: ChatThreadProps): JSX.Element;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { JSX } from 'solid-js';
|
|
2
|
+
export interface CitationProps {
|
|
3
|
+
/** The source number shown in the chip, e.g. `1`. */
|
|
4
|
+
index: number;
|
|
5
|
+
/** Links out to the source when present (opens in a new tab). */
|
|
6
|
+
href?: string;
|
|
7
|
+
/** Shown as a native tooltip and used as the accessible name. */
|
|
8
|
+
title?: string;
|
|
9
|
+
class?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Small inline chip citing a numbered source, meant to sit inside a sentence
|
|
13
|
+
* right after the claim it backs up.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```tsx
|
|
17
|
+
* <p>
|
|
18
|
+
* Glass surfaces use a translucent backdrop blur
|
|
19
|
+
* <Citation index={1} href="https://example.com/spec" title="Design spec" />.
|
|
20
|
+
* </p>
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export declare function Citation(props: CitationProps): JSX.Element;
|
|
24
|
+
export interface SourceListProps {
|
|
25
|
+
sources: {
|
|
26
|
+
title: string;
|
|
27
|
+
href?: string;
|
|
28
|
+
}[];
|
|
29
|
+
class?: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Compact numbered "Sources" block matching the indices used by {@link Citation}.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```tsx
|
|
36
|
+
* <SourceList
|
|
37
|
+
* sources={[
|
|
38
|
+
* { title: 'Design spec', href: 'https://example.com/spec' },
|
|
39
|
+
* { title: 'Internal style guide' },
|
|
40
|
+
* ]}
|
|
41
|
+
* />
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export declare function SourceList(props: SourceListProps): JSX.Element;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { JSX } from 'solid-js';
|
|
2
|
+
export interface FloatingToolbarProps {
|
|
3
|
+
children: JSX.Element;
|
|
4
|
+
/** Which edge of the viewport the bar is anchored to. Defaults to `'bottom'`. */
|
|
5
|
+
position?: 'top' | 'bottom';
|
|
6
|
+
/** Shrink padding/scale and intensify the glass blur once scrolled past ~24px. Defaults to `true`. */
|
|
7
|
+
condenseOnScroll?: boolean;
|
|
8
|
+
class?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Fixed, horizontally-centered glass toolbar that floats above the page
|
|
12
|
+
* content. When `condenseOnScroll` is on (the default), it tightens its
|
|
13
|
+
* padding and scales down slightly once the user scrolls past a small
|
|
14
|
+
* threshold, mimicking a "Liquid Glass" detached bar. Reduced-motion aware:
|
|
15
|
+
* the condense change applies instantly, without a transition, when
|
|
16
|
+
* {@link motionReduced} is true.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```tsx
|
|
20
|
+
* <FloatingToolbar position="bottom">
|
|
21
|
+
* <IconButton label="Undo"><UndoIcon /></IconButton>
|
|
22
|
+
* <IconButton label="Redo"><RedoIcon /></IconButton>
|
|
23
|
+
* </FloatingToolbar>
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare function FloatingToolbar(props: FloatingToolbarProps): JSX.Element;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { JSX } from 'solid-js';
|
|
2
|
+
export interface InlineSelectOption {
|
|
3
|
+
value: string;
|
|
4
|
+
label: string;
|
|
5
|
+
}
|
|
6
|
+
export interface InlineSelectProps {
|
|
7
|
+
value: string;
|
|
8
|
+
options: InlineSelectOption[];
|
|
9
|
+
onChange: (value: string) => void;
|
|
10
|
+
placeholder?: string;
|
|
11
|
+
class?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Edit-in-place select: renders the current value as a clickable chip until
|
|
15
|
+
* clicked, then swaps to a `Select` for picking a new value. Committing a
|
|
16
|
+
* choice returns it to text; blurring away or pressing Escape cancels back
|
|
17
|
+
* to the previous value. Built for status/priority-style editing inline in
|
|
18
|
+
* rows/tables.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```tsx
|
|
22
|
+
* <InlineSelect
|
|
23
|
+
* value={status()}
|
|
24
|
+
* options={[
|
|
25
|
+
* { value: 'todo', label: 'To do' },
|
|
26
|
+
* { value: 'doing', label: 'In progress' },
|
|
27
|
+
* { value: 'done', label: 'Done' },
|
|
28
|
+
* ]}
|
|
29
|
+
* onChange={setStatus}
|
|
30
|
+
* />
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export declare function InlineSelect(props: InlineSelectProps): JSX.Element;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { JSX } from 'solid-js';
|
|
2
|
+
/** Who sent a {@link Message}; drives its subtle visual treatment. */
|
|
3
|
+
export type ChatRole = 'user' | 'assistant' | 'system';
|
|
4
|
+
export interface MessageProps {
|
|
5
|
+
role: ChatRole;
|
|
6
|
+
children: JSX.Element;
|
|
7
|
+
/** Display name shown in the header line. Omitted for `system` messages. */
|
|
8
|
+
author?: string;
|
|
9
|
+
/** Small avatar node, e.g. an icon or `<Avatar/>`. Omitted for `system` messages. */
|
|
10
|
+
avatar?: JSX.Element;
|
|
11
|
+
/** Preformatted timestamp string, e.g. `'2:45 PM'`. */
|
|
12
|
+
timestamp?: string;
|
|
13
|
+
class?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Full-width chat message row with no bubble: an optional avatar/author
|
|
17
|
+
* header line followed by a prose content block. Role is differentiated with
|
|
18
|
+
* subtle, token-based treatment only — `assistant` gets a faint muted panel,
|
|
19
|
+
* `user` stays plain, `system` is a small centered note.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```tsx
|
|
23
|
+
* <Message role="assistant" author="Nova" timestamp="2:45 PM">
|
|
24
|
+
* <p>Here's the summary you asked for.</p>
|
|
25
|
+
* </Message>
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export declare function Message(props: MessageProps): JSX.Element;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { JSX } from 'solid-js';
|
|
2
|
+
/** Transition style for {@link PageTransition}. `slide` adds a small vertical shift. */
|
|
3
|
+
export type PageTransitionMode = 'fade' | 'slide';
|
|
4
|
+
export interface PageTransitionProps {
|
|
5
|
+
/** Change this value (e.g. a route or view id) to trigger a transition. */
|
|
6
|
+
key: string | number;
|
|
7
|
+
children: JSX.Element;
|
|
8
|
+
/** `fade` = opacity only; `slide` = opacity + small vertical shift. @default 'fade' */
|
|
9
|
+
mode?: PageTransitionMode;
|
|
10
|
+
class?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Cross-fades (or slides) its `children` out and in whenever `key` changes —
|
|
14
|
+
* for route/view transitions. Driven purely by the `key` prop; pair it with a
|
|
15
|
+
* router's current path or any view-switch signal. Swaps instantly under
|
|
16
|
+
* reduced motion.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```tsx
|
|
20
|
+
* <PageTransition key={activeTab()} mode="slide">
|
|
21
|
+
* <TabContent id={activeTab()} />
|
|
22
|
+
* </PageTransition>
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare function PageTransition(props: PageTransitionProps): JSX.Element;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { JSX } from 'solid-js';
|
|
2
|
+
export interface PromptComposerProps {
|
|
3
|
+
value: string;
|
|
4
|
+
onInput: (value: string) => void;
|
|
5
|
+
onSubmit: (value: string) => void;
|
|
6
|
+
placeholder?: string;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
/** Small helper text under the field, e.g. model name or a character count. */
|
|
9
|
+
hint?: JSX.Element;
|
|
10
|
+
/** Attachment labels/filenames shown as removable chips above the input. */
|
|
11
|
+
attachments?: string[];
|
|
12
|
+
onRemoveAttachment?: (index: number) => void;
|
|
13
|
+
/** When set, shows a paperclip button that calls this on click. */
|
|
14
|
+
onAttach?: () => void;
|
|
15
|
+
class?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The input area for a chat/AI UI: an auto-growing textarea inside a glass
|
|
19
|
+
* card, removable attachment chips, and a bottom bar with an optional attach
|
|
20
|
+
* button and a Send button. Fully controlled — the parent owns `value`.
|
|
21
|
+
* Submits on **Cmd/Ctrl+Enter** or the Send button click.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```tsx
|
|
25
|
+
* const [value, setValue] = createSignal('')
|
|
26
|
+
* <PromptComposer
|
|
27
|
+
* value={value()}
|
|
28
|
+
* onInput={setValue}
|
|
29
|
+
* onSubmit={(v) => { sendMessage(v); setValue('') }}
|
|
30
|
+
* placeholder="Ask anything…"
|
|
31
|
+
* />
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export declare function PromptComposer(props: PromptComposerProps): JSX.Element;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { JSX } from 'solid-js';
|
|
2
|
+
export interface StreamingTextProps {
|
|
3
|
+
/** The (possibly growing) full text so far. */
|
|
4
|
+
text: string;
|
|
5
|
+
/** Whether more text is still coming; shows the blinking caret while `true`. @default true */
|
|
6
|
+
streaming?: boolean;
|
|
7
|
+
/** Reveal speed, in characters per second, for newly appended text. @default 120 */
|
|
8
|
+
speed?: number;
|
|
9
|
+
class?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Reveals `text` as it "streams" in: when `text` grows, the newly appended
|
|
13
|
+
* characters ease in at `speed` chars/sec instead of snapping into place,
|
|
14
|
+
* with a blinking caret while `streaming`. Already-revealed characters never
|
|
15
|
+
* re-animate — only the new tail is eased in, so appending to `text` doesn't
|
|
16
|
+
* replay the whole reveal. Skips the reveal (shows the full text at once)
|
|
17
|
+
* under reduced motion — the caret still blinks while `streaming` in that
|
|
18
|
+
* case. `streaming={false}` shows the full text immediately with no caret,
|
|
19
|
+
* for the final/settled answer.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```tsx
|
|
23
|
+
* <StreamingText text={answer()} streaming={!done()} />
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare function StreamingText(props: StreamingTextProps): JSX.Element;
|
package/package.json
CHANGED
package/preset.js
CHANGED
|
@@ -21,7 +21,12 @@ import plugin from 'tailwindcss/plugin.js'
|
|
|
21
21
|
// Frosted "space glass" surfaces. addComponents so they tree-shake like any
|
|
22
22
|
// utility (emitted only when the class is found in scanned content — a4ui's own
|
|
23
23
|
// components use them, so scanning ./node_modules/@a4ui/core/dist covers it).
|
|
24
|
-
const glass = plugin(({ addComponents }) => {
|
|
24
|
+
const glass = plugin(({ addComponents, addVariant }) => {
|
|
25
|
+
// `light:` applies when an ancestor is in the light theme (`[data-theme='light']`),
|
|
26
|
+
// mirroring how dark is the base and light is the override throughout A4ui. Used
|
|
27
|
+
// for theme-aware tints (e.g. Badge tones) that need a lighter foreground on dark
|
|
28
|
+
// and a darker one on light to keep WCAG AA in both.
|
|
29
|
+
addVariant('light', "[data-theme='light'] &")
|
|
25
30
|
addComponents({
|
|
26
31
|
// ---- Primary glass surface ----
|
|
27
32
|
'.card': {
|
package/src/index.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// import '@a4ui/core/styles.css'
|
|
9
9
|
// import { Button, Card, Modal } from '@a4ui/core'
|
|
10
10
|
|
|
11
|
-
export const A4UI_VERSION = '0.
|
|
11
|
+
export const A4UI_VERSION = '0.25.0'
|
|
12
12
|
|
|
13
13
|
// Helpers (src/lib) — generic, framework-level utilities.
|
|
14
14
|
export { cn } from './lib/cn'
|
|
@@ -33,6 +33,8 @@ export {
|
|
|
33
33
|
} from './lib/motion'
|
|
34
34
|
export { useMediaQuery } from './lib/media'
|
|
35
35
|
export { remeasureAfterLayout } from './lib/virtual'
|
|
36
|
+
export { createOptimistic } from './lib/createOptimistic'
|
|
37
|
+
export { startViewTransition } from './lib/viewTransition'
|
|
36
38
|
|
|
37
39
|
// UI components (src/ui) — all 18 extracted. See CLAUDE.md.
|
|
38
40
|
export { Accordion, type AccordionItem } from './ui/Accordion'
|
|
@@ -145,6 +147,17 @@ export { Portal, type PortalProps } from './ui/Portal'
|
|
|
145
147
|
export { Sortable, type SortableProps } from './ui/Sortable'
|
|
146
148
|
export { Clock, type ClockProps } from './ui/Clock'
|
|
147
149
|
|
|
150
|
+
// Conversation / AI, transitions, and depth surfaces (roadmap phase 1).
|
|
151
|
+
export { ChatThread, type ChatThreadProps } from './ui/ChatThread'
|
|
152
|
+
export { Message, type MessageProps, type ChatRole } from './ui/Message'
|
|
153
|
+
export { StreamingText, type StreamingTextProps } from './ui/StreamingText'
|
|
154
|
+
export { Citation, type CitationProps, SourceList, type SourceListProps } from './ui/Citation'
|
|
155
|
+
export { PromptComposer, type PromptComposerProps } from './ui/PromptComposer'
|
|
156
|
+
export { ArtifactPanel, type ArtifactPanelProps } from './ui/ArtifactPanel'
|
|
157
|
+
export { FloatingToolbar, type FloatingToolbarProps } from './ui/FloatingToolbar'
|
|
158
|
+
export { PageTransition, type PageTransitionProps } from './ui/PageTransition'
|
|
159
|
+
export { InlineSelect, type InlineSelectProps, type InlineSelectOption } from './ui/InlineSelect'
|
|
160
|
+
|
|
148
161
|
// Motion components (src/ui) — animation primitives built on the `motion` engine
|
|
149
162
|
// (adapted from motion.dev examples). All tree-shakeable and reduced-motion aware;
|
|
150
163
|
// `motion` is external, so importing one of these is the only thing that pulls it.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Optimistic-update helper: show a value immediately, then reconcile with the
|
|
2
|
+
// real async result. On failure, rolls back to the previous settled value and
|
|
3
|
+
// rethrows so callers still see the error (e.g. to show a toast).
|
|
4
|
+
import { createSignal } from 'solid-js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Holds an optimistic value that's updated immediately on `run`, then
|
|
8
|
+
* reconciled with the resolved (or rolled back on rejected) async result.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* const list = createOptimistic<Todo[]>([])
|
|
13
|
+
* async function addTodo(todo: Todo) {
|
|
14
|
+
* await list.run([...list.value(), todo], () => api.addTodo(todo))
|
|
15
|
+
* }
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export function createOptimistic<T>(initial: T): {
|
|
19
|
+
value: () => T
|
|
20
|
+
pending: () => boolean
|
|
21
|
+
run: (optimistic: T, commit: () => Promise<T>) => Promise<T>
|
|
22
|
+
} {
|
|
23
|
+
const [value, setValue] = createSignal(initial)
|
|
24
|
+
const [pending, setPending] = createSignal(false)
|
|
25
|
+
|
|
26
|
+
async function run(optimistic: T, commit: () => Promise<T>): Promise<T> {
|
|
27
|
+
const previous = value()
|
|
28
|
+
setValue(() => optimistic)
|
|
29
|
+
setPending(true)
|
|
30
|
+
try {
|
|
31
|
+
const resolved = await commit()
|
|
32
|
+
setValue(() => resolved)
|
|
33
|
+
return resolved
|
|
34
|
+
} catch (err) {
|
|
35
|
+
setValue(() => previous)
|
|
36
|
+
throw err
|
|
37
|
+
} finally {
|
|
38
|
+
setPending(false)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { value, pending, run }
|
|
43
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Wrapper around the browser View Transitions API. Not every target browser
|
|
2
|
+
// implements `document.startViewTransition` yet, and even where it exists we
|
|
3
|
+
// want to skip it under reduced motion — this module centralizes both checks
|
|
4
|
+
// so callers don't have to feature-detect or re-import motionReduced themselves.
|
|
5
|
+
import { motionReduced } from './motion'
|
|
6
|
+
|
|
7
|
+
/** Minimal shape of `Document` augmented with the View Transitions API. */
|
|
8
|
+
interface DocumentWithViewTransitions {
|
|
9
|
+
startViewTransition: (callback: () => void) => unknown
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function supportsViewTransitions(doc: Document): doc is Document & DocumentWithViewTransitions {
|
|
13
|
+
return (
|
|
14
|
+
'startViewTransition' in doc &&
|
|
15
|
+
typeof (doc as unknown as DocumentWithViewTransitions).startViewTransition === 'function'
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Run `update` inside a View Transition when the browser supports it, so DOM
|
|
21
|
+
* changes cross-fade automatically; otherwise just run `update` synchronously.
|
|
22
|
+
* Respects reduced-motion (skips the transition).
|
|
23
|
+
* @example
|
|
24
|
+
* startViewTransition(() => setRoute('details'))
|
|
25
|
+
*/
|
|
26
|
+
export function startViewTransition(update: () => void): void {
|
|
27
|
+
if (typeof document === 'undefined' || motionReduced() || !supportsViewTransitions(document)) {
|
|
28
|
+
update()
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
document.startViewTransition(update)
|
|
32
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Inline (non-portalled) right-side panel for AI-generated output — code,
|
|
2
|
+
// preview, or doc — shown alongside a chat. Unlike `Drawer` (a portalled
|
|
3
|
+
// overlay on Kobalte's Dialog), the caller places this directly in a flex row
|
|
4
|
+
// next to the main content: it reserves no space when closed (width collapses
|
|
5
|
+
// to 0) and reveals by animating its own width, so the chat pane reflows next
|
|
6
|
+
// to it instead of being covered. We collapse to width 0 rather than toggling
|
|
7
|
+
// `display: none` because a `display` toggle jumps instantly — animating width
|
|
8
|
+
// is what makes it slide.
|
|
9
|
+
import { X } from 'lucide-solid'
|
|
10
|
+
import type { JSX } from 'solid-js'
|
|
11
|
+
|
|
12
|
+
import { cn } from '../lib/cn'
|
|
13
|
+
import { motionReduced } from '../lib/motion'
|
|
14
|
+
|
|
15
|
+
export interface ArtifactPanelProps {
|
|
16
|
+
open: boolean
|
|
17
|
+
onClose?: () => void
|
|
18
|
+
title?: string
|
|
19
|
+
/** Panel width in px when open. @default 420 */
|
|
20
|
+
width?: number
|
|
21
|
+
children: JSX.Element
|
|
22
|
+
class?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Right-side split panel that holds generated output (code, preview, doc) next
|
|
27
|
+
* to a chat — for AI UIs that keep the artifact and the conversation visible at
|
|
28
|
+
* once. Inline, not an overlay: place it as the last child of a flex row next
|
|
29
|
+
* to the main content. Slides open/closed by animating width (and a matching
|
|
30
|
+
* fade/slide on its content), reduced-motion aware via `motionReduced()`.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```tsx
|
|
34
|
+
* const [open, setOpen] = createSignal(false)
|
|
35
|
+
* <div class="flex h-full">
|
|
36
|
+
* <main class="min-w-0 flex-1">…chat…</main>
|
|
37
|
+
* <ArtifactPanel open={open()} onClose={() => setOpen(false)} title="script.py">
|
|
38
|
+
* <pre class="p-4 text-sm">…generated code…</pre>
|
|
39
|
+
* </ArtifactPanel>
|
|
40
|
+
* </div>
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export function ArtifactPanel(props: ArtifactPanelProps): JSX.Element {
|
|
44
|
+
const width = () => props.width ?? 420
|
|
45
|
+
const reduced = () => motionReduced()
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div
|
|
49
|
+
role="complementary"
|
|
50
|
+
aria-label={props.title ?? 'Artifact'}
|
|
51
|
+
aria-hidden={!props.open}
|
|
52
|
+
class={cn(
|
|
53
|
+
'card h-full shrink-0 overflow-hidden border-border bg-glass',
|
|
54
|
+
props.open ? 'border-l' : 'border-0',
|
|
55
|
+
reduced() ? undefined : 'transition-[width] duration-300 ease-in-out',
|
|
56
|
+
props.class,
|
|
57
|
+
)}
|
|
58
|
+
style={{ width: `${props.open ? width() : 0}px` }}
|
|
59
|
+
>
|
|
60
|
+
{/* Fixed-width inner wrapper: the content never reflows/wraps while the
|
|
61
|
+
outer element's width animates between 0 and `width()`. */}
|
|
62
|
+
<div
|
|
63
|
+
class={cn(
|
|
64
|
+
'flex h-full flex-col',
|
|
65
|
+
reduced() ? undefined : 'transition-[opacity,transform] duration-300 ease-in-out',
|
|
66
|
+
)}
|
|
67
|
+
style={{
|
|
68
|
+
width: `${width()}px`,
|
|
69
|
+
opacity: props.open ? 1 : 0,
|
|
70
|
+
transform: props.open ? 'translateX(0)' : 'translateX(16px)',
|
|
71
|
+
}}
|
|
72
|
+
>
|
|
73
|
+
<div class="sticky top-0 z-10 flex items-center justify-between border-b border-border bg-background/90 px-5 py-4 backdrop-blur">
|
|
74
|
+
<span class="truncate text-[16px] font-bold leading-tight">{props.title ?? 'Artifact'}</span>
|
|
75
|
+
<button
|
|
76
|
+
type="button"
|
|
77
|
+
onClick={() => props.onClose?.()}
|
|
78
|
+
class="grid h-9 w-9 shrink-0 place-items-center rounded-lg text-muted-foreground transition hover:bg-muted hover:text-foreground"
|
|
79
|
+
aria-label="Close"
|
|
80
|
+
>
|
|
81
|
+
<X class="h-5 w-5" />
|
|
82
|
+
</button>
|
|
83
|
+
</div>
|
|
84
|
+
<div class="min-h-0 flex-1 overflow-y-auto">{props.children}</div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
)
|
|
88
|
+
}
|
package/src/ui/Badge.tsx
CHANGED
|
@@ -9,12 +9,15 @@ import { cn } from '../lib/cn'
|
|
|
9
9
|
/** Semantic tone of a {@link Badge}; drives its background/text/ring color. */
|
|
10
10
|
export type BadgeTone = 'neutral' | 'success' | 'warning' | 'danger' | 'info'
|
|
11
11
|
|
|
12
|
+
// Tinted tones carry a lighter foreground for the dark (base) theme and a darker
|
|
13
|
+
// one under `[data-theme='light']`, so the text meets WCAG AA over the translucent
|
|
14
|
+
// tint in both — a single fixed shade can't clear 4.5:1 on both backgrounds.
|
|
12
15
|
const TONE_CLASSES: Record<BadgeTone, string> = {
|
|
13
16
|
neutral: 'bg-muted text-muted-foreground ring-border',
|
|
14
|
-
success: 'bg-emerald-500/15 text-emerald-
|
|
15
|
-
warning: 'bg-amber-500/15 text-amber-
|
|
16
|
-
danger: 'bg-rose-500/15 text-rose-
|
|
17
|
-
info: 'bg-sky-500/15 text-sky-
|
|
17
|
+
success: 'bg-emerald-500/15 text-emerald-300 ring-emerald-500/30 light:text-emerald-700',
|
|
18
|
+
warning: 'bg-amber-500/15 text-amber-300 ring-amber-500/30 light:text-amber-700',
|
|
19
|
+
danger: 'bg-rose-500/15 text-rose-300 ring-rose-500/30 light:text-rose-700',
|
|
20
|
+
info: 'bg-sky-500/15 text-sky-300 ring-sky-500/30 light:text-sky-700',
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
const BADGE_BASE =
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// ChatThread — layout shell for AI/conversation UIs: a centered, scrollable
|
|
2
|
+
// reading column that stacks chat messages. Purely a container: it has no
|
|
3
|
+
// idea what a "message" looks like, so any element can be stacked inside it.
|
|
4
|
+
import { createEffect, onMount, type JSX } from 'solid-js'
|
|
5
|
+
|
|
6
|
+
import { cn } from '../lib/cn'
|
|
7
|
+
import { motionReduced } from '../lib/motion'
|
|
8
|
+
|
|
9
|
+
export interface ChatThreadProps {
|
|
10
|
+
children: JSX.Element
|
|
11
|
+
/** Max width of the reading column, in px. Defaults to `768`. */
|
|
12
|
+
maxWidth?: number
|
|
13
|
+
/** Auto-scroll to the newest message on mount and whenever children change. Defaults to `true`. */
|
|
14
|
+
stickToBottom?: boolean
|
|
15
|
+
class?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Scrollable, centered reading column that stacks chat messages. Layout-only:
|
|
20
|
+
* it renders whatever is passed as `children`, so pair it with your own
|
|
21
|
+
* message bubbles/components.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```tsx
|
|
25
|
+
* <ChatThread maxWidth={640}>
|
|
26
|
+
* <div>Hi! Ask me anything about your itinerary.</div>
|
|
27
|
+
* <div>What's the weather like in Rivertown this weekend?</div>
|
|
28
|
+
* <div>Sunny and mild, 18–22°C, light breeze on Saturday.</div>
|
|
29
|
+
* </ChatThread>
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export function ChatThread(props: ChatThreadProps): JSX.Element {
|
|
33
|
+
let scrollEl: HTMLDivElement | undefined
|
|
34
|
+
|
|
35
|
+
const scrollToBottom = () => {
|
|
36
|
+
if (!scrollEl) return
|
|
37
|
+
scrollEl.scrollTo({ top: scrollEl.scrollHeight, behavior: motionReduced() ? 'auto' : 'smooth' })
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
onMount(() => {
|
|
41
|
+
if (props.stickToBottom ?? true) scrollToBottom()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
createEffect(() => {
|
|
45
|
+
// Track children so newly appended messages pull the thread down.
|
|
46
|
+
void props.children
|
|
47
|
+
if (props.stickToBottom ?? true) queueMicrotask(scrollToBottom)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div
|
|
52
|
+
ref={scrollEl}
|
|
53
|
+
class={cn('mx-auto flex w-full flex-col gap-4 overflow-y-auto', props.class)}
|
|
54
|
+
style={{ 'max-width': `${props.maxWidth ?? 768}px` }}
|
|
55
|
+
>
|
|
56
|
+
{props.children}
|
|
57
|
+
</div>
|
|
58
|
+
)
|
|
59
|
+
}
|