@heyramzi/ui 6.0.1 → 7.0.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.
Files changed (114) hide show
  1. package/COMPONENTS.md +7 -26
  2. package/README.md +12 -5
  3. package/package.json +14 -16
  4. package/scripts/sync-shadcn.mjs +12 -3
  5. package/src/agents/agent-avatar.svelte +639 -0
  6. package/src/agents/agent-cast.svelte +173 -0
  7. package/src/agents/demo.svelte +78 -0
  8. package/src/agents/index.ts +11 -0
  9. package/src/agents/types.ts +72 -0
  10. package/{LICENSE → src/agents/vendor/LICENSE} +1 -1
  11. package/src/agents/vendor/color.ts +155 -0
  12. package/src/agents/vendor/draw.ts +840 -0
  13. package/src/agents/vendor/engine.ts +845 -0
  14. package/src/agents/vendor/mesh.ts +838 -0
  15. package/src/agents/vendor/plastic.ts +4958 -0
  16. package/src/agents/vendor/presets.ts +103 -0
  17. package/src/agents/vendor/shapes.ts +41 -0
  18. package/src/agents/vendor/ticker.ts +67 -0
  19. package/src/agents/vendor/types.ts +261 -0
  20. package/src/agents/vendor/wear.ts +1699 -0
  21. package/src/ai/index.ts +0 -18
  22. package/src/animations/README.md +3 -3
  23. package/src/lib/sliding-pill.ts +68 -0
  24. package/src/lib/theme-transition.ts +4 -2
  25. package/src/shadcn/code-block/code-block-actions.svelte +23 -0
  26. package/src/shadcn/code-block/code-block-content.svelte +55 -0
  27. package/src/shadcn/code-block/code-block-copy-button.svelte +59 -0
  28. package/src/shadcn/code-block/code-block-footer.svelte +20 -0
  29. package/src/shadcn/code-block/code-block-header.svelte +23 -0
  30. package/src/shadcn/code-block/code-block-line.svelte +71 -0
  31. package/src/shadcn/code-block/code-block-title.svelte +23 -0
  32. package/src/shadcn/code-block/code-block.svelte +47 -0
  33. package/src/shadcn/code-block/index.ts +42 -0
  34. package/src/shadcn/hooks/highlighter.ts +132 -0
  35. package/src/shadcn/hooks/use-highlighted-code.svelte.ts +98 -0
  36. package/src/shadcn/kbd/index.ts +10 -0
  37. package/src/shadcn/kbd/kbd-group.svelte +20 -0
  38. package/src/shadcn/kbd/kbd.svelte +23 -0
  39. package/src/shadcn/message-scroller/index.ts +28 -0
  40. package/src/shadcn/message-scroller/message-scroller-button.svelte +52 -0
  41. package/src/shadcn/message-scroller/message-scroller-content.svelte +17 -0
  42. package/src/shadcn/message-scroller/message-scroller-item.svelte +22 -0
  43. package/src/shadcn/message-scroller/message-scroller-provider.svelte +7 -0
  44. package/src/shadcn/message-scroller/message-scroller-viewport.svelte +20 -0
  45. package/src/shadcn/message-scroller/message-scroller.svelte +20 -0
  46. package/src/shadcn/tabs/tabs-list.svelte +13 -2
  47. package/src/shadcn/tabs/tabs-trigger.svelte +1 -1
  48. package/src/shadcn/trace/index.ts +29 -0
  49. package/src/shadcn/trace/trace-collapsible-content.svelte +29 -0
  50. package/src/shadcn/trace/trace-collapsible-trigger.svelte +29 -0
  51. package/src/shadcn/trace/trace-collapsible.svelte +20 -0
  52. package/src/shadcn/trace/trace-description.svelte +20 -0
  53. package/src/shadcn/trace/trace-icon.svelte +24 -0
  54. package/src/shadcn/trace/trace-line.svelte +31 -0
  55. package/src/shadcn/trace/trace-title.svelte +20 -0
  56. package/src/shadcn/trace/trace.svelte +23 -0
  57. package/src/showcase/AiSection.svelte +0 -7
  58. package/src/showcase/demos/shadcn/code-block.svelte +80 -0
  59. package/src/showcase/demos/shadcn/index.ts +24 -0
  60. package/src/showcase/demos/shadcn/message-scroller.svelte +44 -0
  61. package/src/showcase/demos/shadcn/trace.svelte +51 -0
  62. package/src/showcase/kit.ts +1 -2
  63. package/src/ai/conversation/Conversation.svelte +0 -28
  64. package/src/ai/conversation/ConversationContent.svelte +0 -35
  65. package/src/ai/conversation/ConversationEmptyState.svelte +0 -46
  66. package/src/ai/conversation/ConversationScrollButton.svelte +0 -51
  67. package/src/ai/conversation/index.ts +0 -33
  68. package/src/ai/conversation/stick-to-bottom-context.svelte.ts +0 -131
  69. package/src/charts/animated-value.css +0 -7
  70. package/src/charts/animated-value.svelte +0 -110
  71. package/src/charts/area-chart.css +0 -29
  72. package/src/charts/area-chart.svelte +0 -195
  73. package/src/charts/bar-chart.css +0 -119
  74. package/src/charts/bar-chart.svelte +0 -269
  75. package/src/charts/charts.css +0 -201
  76. package/src/charts/crosshair.svelte +0 -35
  77. package/src/charts/donut-chart.css +0 -56
  78. package/src/charts/donut-chart.svelte +0 -158
  79. package/src/charts/edge-fade.svelte +0 -43
  80. package/src/charts/edge-fade.ts +0 -16
  81. package/src/charts/figure.css +0 -100
  82. package/src/charts/figure.svelte +0 -122
  83. package/src/charts/gauge-chart.css +0 -94
  84. package/src/charts/gauge-chart.svelte +0 -303
  85. package/src/charts/hover-band.test.ts +0 -30
  86. package/src/charts/hover-band.ts +0 -21
  87. package/src/charts/hover-focus.svelte +0 -85
  88. package/src/charts/index.ts +0 -30
  89. package/src/charts/legend.css +0 -131
  90. package/src/charts/legend.svelte +0 -112
  91. package/src/charts/line-chart.css +0 -26
  92. package/src/charts/line-chart.svelte +0 -256
  93. package/src/charts/motion.ts +0 -19
  94. package/src/charts/number-chart.css +0 -168
  95. package/src/charts/number-chart.svelte +0 -177
  96. package/src/charts/primitives/chart-container.svelte +0 -57
  97. package/src/charts/primitives/chart-style.svelte +0 -36
  98. package/src/charts/primitives/chart-tooltip.svelte +0 -161
  99. package/src/charts/primitives/chart-utils.ts +0 -54
  100. package/src/charts/primitives/index.ts +0 -11
  101. package/src/charts/reveal.svelte +0 -61
  102. package/src/charts/ring-chart.css +0 -96
  103. package/src/charts/ring-chart.svelte +0 -158
  104. package/src/charts/sparkline.css +0 -45
  105. package/src/charts/sparkline.svelte +0 -140
  106. package/src/charts/stat-card.css +0 -103
  107. package/src/charts/stat-card.svelte +0 -267
  108. package/src/charts/time-series-chart.css +0 -28
  109. package/src/charts/time-series-chart.svelte +0 -179
  110. package/src/charts/trend-pill.css +0 -38
  111. package/src/charts/trend-pill.svelte +0 -48
  112. package/src/lib/hooks/is-mobile.svelte.ts +0 -11
  113. package/src/showcase/ChartsSection.svelte +0 -458
  114. package/src/showcase/demos/ai/conversation.svelte +0 -59
@@ -0,0 +1,80 @@
1
+ <script lang="ts">
2
+ import DownloadIcon from "@lucide/svelte/icons/download";
3
+ import FileCodeIcon from "@lucide/svelte/icons/file-code";
4
+ import WrapTextIcon from "@lucide/svelte/icons/text-wrap";
5
+ import * as CodeBlock from "../../../shadcn/code-block/index.js";
6
+ import { useHighlightedCode } from "../../../shadcn/hooks/use-highlighted-code.svelte.js";
7
+ import { Button } from "../../../shadcn/button/index.js";
8
+
9
+ const code = `import { streamText } from "ai"
10
+
11
+ export async function POST(request: Request) {
12
+ const { messages } = await request.json()
13
+
14
+ const result = streamText({
15
+ model: "anthropic/claude-sonnet-4.5",
16
+ system: "You are a helpful assistant. Answer in one short paragraph.",
17
+ messages,
18
+ })
19
+
20
+ return result.toUIMessageStreamResponse()
21
+ }`;
22
+
23
+ function download(value: string, filename: string) {
24
+ const url = URL.createObjectURL(new Blob([value], { type: "text/plain" }));
25
+ const link = document.createElement("a");
26
+ link.href = url;
27
+ link.download = filename;
28
+ link.click();
29
+ URL.revokeObjectURL(url);
30
+ }
31
+
32
+ let wrap = $state(false);
33
+
34
+ const lines = useHighlightedCode(
35
+ () => code,
36
+ () => "ts"
37
+ );
38
+ </script>
39
+
40
+ <div class="w-full max-w-lg">
41
+ <CodeBlock.Root>
42
+ <CodeBlock.Header>
43
+ <CodeBlock.Title>
44
+ <FileCodeIcon />
45
+ app/api/chat/route.ts
46
+ </CodeBlock.Title>
47
+ <CodeBlock.Actions>
48
+ <Button
49
+ variant="ghost"
50
+ size="sm"
51
+ icon
52
+ aria-label="Wrap lines"
53
+ aria-pressed={wrap}
54
+ class="aria-pressed:text-foreground"
55
+ onclick={() => (wrap = !wrap)}
56
+ >
57
+ <WrapTextIcon />
58
+ </Button>
59
+ <Button
60
+ variant="ghost"
61
+ size="sm"
62
+ icon
63
+ aria-label="Download"
64
+ onclick={() => download(code, "route.ts")}
65
+ >
66
+ <DownloadIcon />
67
+ </Button>
68
+ <CodeBlock.CopyButton value={code} />
69
+ </CodeBlock.Actions>
70
+ </CodeBlock.Header>
71
+ <CodeBlock.Content {wrap} lineNumbers>
72
+ {#each lines.current as line, index (index)}
73
+ <CodeBlock.Line
74
+ variant={index >= 5 && index <= 9 ? "highlighted" : "default"}
75
+ code={line}
76
+ />
77
+ {/each}
78
+ </CodeBlock.Content>
79
+ </CodeBlock.Root>
80
+ </div>
@@ -13,6 +13,7 @@ import ButtonDemo from "./button.svelte";
13
13
  import CalendarDemo from "./calendar.svelte";
14
14
  import CardDemo from "./card.svelte";
15
15
  import CheckboxDemo from "./checkbox.svelte";
16
+ import CodeBlockDemo from "./code-block.svelte";
16
17
  import CollapsibleDemo from "./collapsible.svelte";
17
18
  import CommandDemo from "./command.svelte";
18
19
  import ContextMenuDemo from "./context-menu.svelte";
@@ -26,6 +27,7 @@ import InputOtpDemo from "./input-otp.svelte";
26
27
  import LabelDemo from "./label.svelte";
27
28
  import MarkerDemo from "./marker.svelte";
28
29
  import MessageDemo from "./message.svelte";
30
+ import MessageScrollerDemo from "./message-scroller.svelte";
29
31
  import PopoverDemo from "./popover.svelte";
30
32
  import ProgressDemo from "./progress.svelte";
31
33
  import RadioGroupDemo from "./radio-group.svelte";
@@ -44,6 +46,7 @@ import TextareaDemo from "./textarea.svelte";
44
46
  import ToggleDemo from "./toggle.svelte";
45
47
  import ToggleGroupDemo from "./toggle-group.svelte";
46
48
  import TooltipDemo from "./tooltip.svelte";
49
+ import TraceDemo from "./trace.svelte";
47
50
 
48
51
  export type ShadcnDemo = {
49
52
  name: string;
@@ -138,6 +141,13 @@ export const SHADCN_DEMOS: ShadcnDemo[] = [
138
141
  path: 'import { Checkbox } from "@heyramzi/ui/shadcn/checkbox";\nimport { Label } from "@heyramzi/ui/shadcn/label";',
139
142
  Comp: CheckboxDemo,
140
143
  },
144
+ {
145
+ name: "code-block",
146
+ title: "Code Block",
147
+ note: "Displays code, terminal output and diffs with an optional header, line numbers and a copy button.",
148
+ path: 'import * as CodeBlock from "@heyramzi/ui/shadcn/code-block";\nimport { useHighlightedCode } from "@heyramzi/ui/shadcn/hooks/use-highlighted-code.svelte.js";\nimport { Button } from "@heyramzi/ui/shadcn/button";',
149
+ Comp: CodeBlockDemo,
150
+ },
141
151
  {
142
152
  name: "collapsible",
143
153
  title: "Collapsible",
@@ -229,6 +239,13 @@ export const SHADCN_DEMOS: ShadcnDemo[] = [
229
239
  path: 'import * as Avatar from "@heyramzi/ui/shadcn/avatar";\nimport * as Bubble from "@heyramzi/ui/shadcn/bubble";\nimport * as Marker from "@heyramzi/ui/shadcn/marker";\nimport * as Message from "@heyramzi/ui/shadcn/message";',
230
240
  Comp: MessageDemo,
231
241
  },
242
+ {
243
+ name: "message-scroller",
244
+ title: "Message Scroller",
245
+ note: "A chat scroll container that anchors turns, opens saved transcripts, follows streamed responses, loads history without jumping, and jumps to any message.",
246
+ path: 'import * as Card from "@heyramzi/ui/shadcn/card";\nimport * as DropdownMenu from "@heyramzi/ui/shadcn/dropdown-menu";\nimport * as Empty from "@heyramzi/ui/shadcn/empty";\nimport * as InputGroup from "@heyramzi/ui/shadcn/input-group";\nimport * as MessageScroller from "@heyramzi/ui/shadcn/message-scroller";\nimport * as Tooltip from "@heyramzi/ui/shadcn/tooltip";\nimport { Button } from "@heyramzi/ui/shadcn/button";',
247
+ Comp: MessageScrollerDemo,
248
+ },
232
249
  {
233
250
  name: "popover",
234
251
  title: "Popover",
@@ -355,4 +372,11 @@ export const SHADCN_DEMOS: ShadcnDemo[] = [
355
372
  path: 'import * as Tooltip from "@heyramzi/ui/shadcn/tooltip";\nimport { buttonVariants } from "@heyramzi/ui/shadcn/button";',
356
373
  Comp: TooltipDemo,
357
374
  },
375
+ {
376
+ name: "trace",
377
+ title: "Trace",
378
+ note: "The building block for reasoning, tool calls, sources and more.",
379
+ path: 'import * as Bubble from "@heyramzi/ui/shadcn/bubble";\nimport * as CodeBlock from "@heyramzi/ui/shadcn/code-block";\nimport * as Message from "@heyramzi/ui/shadcn/message";\nimport * as Trace from "@heyramzi/ui/shadcn/trace";\nimport { Button } from "@heyramzi/ui/shadcn/button";',
380
+ Comp: TraceDemo,
381
+ },
358
382
  ];
@@ -0,0 +1,44 @@
1
+ <!-- Ours, not upstream's: theirs streams through @ai-sdk/svelte and docs-only helpers this package doesn't ship. -->
2
+ <script lang="ts">
3
+ import * as Bubble from "../../../shadcn/bubble/index.js";
4
+ import * as Message from "../../../shadcn/message/index.js";
5
+ import * as MessageScroller from "../../../shadcn/message-scroller/index.js";
6
+ import { Response } from "../../../ai/index.js";
7
+
8
+ const turns = [
9
+ { id: "q1", role: "user", text: "How should I clean up our beam effects?" },
10
+ {
11
+ id: "a1",
12
+ role: "assistant",
13
+ text: "Here is the plan:\n\n1. **Extract** the beam into the shared package\n2. **Showcase** it on the Effects page\n3. **Migrate** both apps onto the shared copy",
14
+ },
15
+ { id: "q2", role: "user", text: "And the old copies?" },
16
+ { id: "a2", role: "assistant", text: "Delete them in the same change, so nothing imports the stale one." },
17
+ ] as const;
18
+ </script>
19
+
20
+ <div class="h-68 w-full max-w-md">
21
+ <MessageScroller.Provider>
22
+ <MessageScroller.Root>
23
+ <MessageScroller.Viewport>
24
+ <MessageScroller.Content class="gap-4 p-1">
25
+ {#each turns as turn (turn.id)}
26
+ {@const user = turn.role === "user"}
27
+ <MessageScroller.Item messageId={turn.id} scrollAnchor={user}>
28
+ <Message.Root align={user ? "end" : "start"}>
29
+ <Message.Content>
30
+ <Bubble.Root variant={user ? "default" : "muted"} align={user ? "end" : "start"}>
31
+ <Bubble.Content>
32
+ {#if user}{turn.text}{:else}<Response content={turn.text} />{/if}
33
+ </Bubble.Content>
34
+ </Bubble.Root>
35
+ </Message.Content>
36
+ </Message.Root>
37
+ </MessageScroller.Item>
38
+ {/each}
39
+ </MessageScroller.Content>
40
+ </MessageScroller.Viewport>
41
+ <MessageScroller.Button />
42
+ </MessageScroller.Root>
43
+ </MessageScroller.Provider>
44
+ </div>
@@ -0,0 +1,51 @@
1
+ <!-- Ours, not upstream's: theirs streams through @ai-sdk/svelte and docs-only helpers this package doesn't ship. -->
2
+ <script lang="ts">
3
+ import CircleAlertIcon from "@lucide/svelte/icons/circle-alert";
4
+ import CircleCheckIcon from "@lucide/svelte/icons/circle-check";
5
+ import LoaderCircleIcon from "@lucide/svelte/icons/loader-circle";
6
+ import * as CodeBlock from "../../../shadcn/code-block/index.js";
7
+ import * as Trace from "../../../shadcn/trace/index.js";
8
+
9
+ const steps = [
10
+ { command: "pnpm --version", output: "12.10.1" },
11
+ {
12
+ command: "git diff --stat HEAD~1 -- package.json pnpm-lock.yaml",
13
+ output: " package.json | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)",
14
+ },
15
+ {
16
+ command: "pnpm install --frozen-lockfile",
17
+ error: "ERR_PNPM_OUTDATED_LOCKFILE pnpm-lock.yaml is not up to date with package.json",
18
+ },
19
+ ];
20
+ </script>
21
+
22
+ <div class="w-full max-w-md">
23
+ <Trace.Root>
24
+ <Trace.Line>
25
+ <Trace.Icon><CircleCheckIcon /></Trace.Icon>
26
+ <Trace.Title>Thought for 5s</Trace.Title>
27
+ </Trace.Line>
28
+ {#each steps as step (step.command)}
29
+ <Trace.Collapsible>
30
+ <Trace.CollapsibleTrigger>
31
+ <Trace.Line status={step.error ? "error" : undefined}>
32
+ <Trace.Icon>
33
+ {#if step.error}<CircleAlertIcon />{:else}<CircleCheckIcon />{/if}
34
+ </Trace.Icon>
35
+ <Trace.Title>{step.error ? "Failed to run" : "Ran"} {step.command}</Trace.Title>
36
+ </Trace.Line>
37
+ </Trace.CollapsibleTrigger>
38
+ <Trace.CollapsibleContent>
39
+ <CodeBlock.Root size="sm" variant={step.error ? "destructive" : "default"}>
40
+ <CodeBlock.Content wrap class="max-h-64" code={step.error ?? step.output} />
41
+ </CodeBlock.Root>
42
+ </Trace.CollapsibleContent>
43
+ </Trace.Collapsible>
44
+ {/each}
45
+ <Trace.Line status="running">
46
+ <Trace.Icon><LoaderCircleIcon class="animate-spin" /></Trace.Icon>
47
+ <Trace.Title>Writing the fix</Trace.Title>
48
+ <Trace.Description>2 sources</Trace.Description>
49
+ </Trace.Line>
50
+ </Trace.Root>
51
+ </div>
@@ -1,5 +1,5 @@
1
1
  // The showcase of @heyramzi/ui itself: its shadcn primitives,
2
- // its motion vocabulary, its visual effects, its chart set and its AI surface.
2
+ // its motion vocabulary, its visual effects and its AI surface. The chart set's own section ships with `@heyramzi/charts/showcase`.
3
3
 
4
4
  // WHY they ship with the package and not with the app showing them: they demo
5
5
 
@@ -13,5 +13,4 @@
13
13
  export { default as AnimationsSection } from "./AnimationsSection.svelte";
14
14
  export { default as ShadcnSection } from "./ShadcnSection.svelte";
15
15
  export { default as EffectsSection } from "./EffectsSection.svelte";
16
- export { default as ChartsSection } from "./ChartsSection.svelte";
17
16
  export { default as AiSection } from "./AiSection.svelte";
@@ -1,28 +0,0 @@
1
- <script lang="ts" module>
2
- import type { Snippet } from "svelte";
3
- import type { HTMLAttributes } from "svelte/elements";
4
-
5
- export interface ConversationProps extends HTMLAttributes<HTMLDivElement> {
6
- children?: Snippet;
7
- class?: string;
8
- }
9
- </script>
10
-
11
- <script lang="ts">
12
- import { cn } from "@heyramzi/utils";
13
- import { setStickToBottomContext } from "./stick-to-bottom-context.svelte.js";
14
-
15
- let { class: className, children, ...rest }: ConversationProps = $props();
16
-
17
- setStickToBottomContext();
18
- </script>
19
-
20
- <div
21
- class={cn("relative flex h-full flex-col overflow-hidden", className)}
22
- role="log"
23
- aria-live="polite"
24
- aria-atomic="false"
25
- {...rest}
26
- >
27
- {@render children?.()}
28
- </div>
@@ -1,35 +0,0 @@
1
- <script lang="ts" module>
2
- import type { Snippet } from "svelte";
3
- import type { HTMLAttributes } from "svelte/elements";
4
-
5
- export interface ConversationContentProps extends HTMLAttributes<HTMLDivElement> {
6
- children?: Snippet;
7
- class?: string;
8
- }
9
- </script>
10
-
11
- <script lang="ts">
12
- import { cn } from "@heyramzi/utils";
13
- import { onDestroy } from "svelte";
14
- import { getStickToBottomContext } from "./stick-to-bottom-context.svelte.js";
15
-
16
- let { class: className, children, ...rest }: ConversationContentProps = $props();
17
-
18
- const context = getStickToBottomContext();
19
- let element = $state<HTMLDivElement | null>(null);
20
-
21
- $effect(() => {
22
- if (element) {
23
- context.setElement(element);
24
- context.scrollToBottom("smooth");
25
- }
26
- });
27
-
28
- onDestroy(() => {
29
- context.destroy();
30
- });
31
- </script>
32
-
33
- <div bind:this={element} class={cn("flex-1 overflow-y-auto", className)} {...rest}>
34
- {@render children?.()}
35
- </div>
@@ -1,46 +0,0 @@
1
- <script lang="ts" module>
2
- import type { Snippet } from "svelte";
3
- import type { HTMLAttributes } from "svelte/elements";
4
-
5
- export interface ConversationEmptyStateProps extends HTMLAttributes<HTMLDivElement> {
6
- title?: string;
7
- description?: string;
8
- icon?: Snippet;
9
- children?: Snippet;
10
- class?: string;
11
- }
12
- </script>
13
-
14
- <script lang="ts">
15
- import { cn } from "@heyramzi/utils";
16
-
17
- let {
18
- class: className,
19
- title,
20
- description,
21
- icon,
22
- children,
23
- ...rest
24
- }: ConversationEmptyStateProps = $props();
25
- </script>
26
-
27
- <div
28
- class={cn("flex size-full flex-col items-center justify-center gap-3 p-6 text-center", className)}
29
- {...rest}
30
- >
31
- {#if children}
32
- {@render children()}
33
- {:else}
34
- {#if icon}
35
- <div class="text-[color:var(--muted-foreground,inherit)]">
36
- {@render icon()}
37
- </div>
38
- {/if}
39
- <div class="space-y-1">
40
- {#if title}<h3 class="text-sm font-medium">{title}</h3>{/if}
41
- {#if description}
42
- <p class="text-[color:var(--muted-foreground,inherit)] text-sm">{description}</p>
43
- {/if}
44
- </div>
45
- {/if}
46
- </div>
@@ -1,51 +0,0 @@
1
- <script lang="ts" module>
2
- import type { ButtonProps } from "../../shadcn/button/index.js";
3
-
4
- export interface ConversationScrollButtonProps extends Omit<ButtonProps, "onclick"> {
5
- class?: string;
6
- onclick?: (event: MouseEvent) => void;
7
- }
8
- </script>
9
-
10
- <script lang="ts">
11
- import { cn } from "@heyramzi/utils";
12
- import { Button } from "../../shadcn/button/index.js";
13
- import ArrowDown from "phosphor-svelte/lib/ArrowDown";
14
- import { fly } from "svelte/transition";
15
- import { backOut } from "svelte/easing";
16
- import { getStickToBottomContext } from "./stick-to-bottom-context.svelte.js";
17
-
18
- let { class: className, onclick, ...rest }: ConversationScrollButtonProps = $props();
19
-
20
- const context = getStickToBottomContext();
21
-
22
- function handleClick(event: MouseEvent) {
23
- context.scrollToBottom();
24
- onclick?.(event);
25
- }
26
- </script>
27
-
28
- {#if !context.isAtBottom}
29
- <div
30
- in:fly={{ duration: 300, y: 10, easing: backOut }}
31
- out:fly={{ duration: 200, y: 10, easing: backOut }}
32
- class="pointer-events-auto absolute bottom-3 left-1/2 -translate-x-1/2"
33
- >
34
- <Button
35
- class={cn(
36
- "rounded-full shadow-lg backdrop-blur-sm",
37
- "bg-[color:var(--background,white)]/80 hover:bg-[color:var(--background,white)]/95",
38
- "border-[color:var(--border,rgba(0,0,0,0.1))]",
39
- className,
40
- )}
41
- onclick={handleClick}
42
- icon
43
- type="button"
44
- variant="outline"
45
- aria-label="Scroll to latest"
46
- {...rest}
47
- >
48
- <ArrowDown class="size-4" />
49
- </Button>
50
- </div>
51
- {/if}
@@ -1,33 +0,0 @@
1
- import Conversation from "./Conversation.svelte";
2
- import ConversationContent from "./ConversationContent.svelte";
3
- import ConversationEmptyState from "./ConversationEmptyState.svelte";
4
- import ConversationScrollButton from "./ConversationScrollButton.svelte";
5
- import {
6
- StickToBottomContext,
7
- getStickToBottomContext,
8
- setStickToBottomContext,
9
- } from "./stick-to-bottom-context.svelte.js";
10
-
11
- import type { ConversationProps } from "./Conversation.svelte";
12
- import type { ConversationContentProps } from "./ConversationContent.svelte";
13
- import type { ConversationEmptyStateProps } from "./ConversationEmptyState.svelte";
14
- import type { ConversationScrollButtonProps } from "./ConversationScrollButton.svelte";
15
-
16
- export {
17
- Conversation,
18
- ConversationContent,
19
- ConversationEmptyState,
20
- ConversationScrollButton,
21
- StickToBottomContext,
22
- getStickToBottomContext,
23
- setStickToBottomContext,
24
- type ConversationProps,
25
- type ConversationContentProps,
26
- type ConversationEmptyStateProps,
27
- type ConversationScrollButtonProps,
28
- //
29
- Conversation as Root,
30
- ConversationContent as Content,
31
- ConversationEmptyState as EmptyState,
32
- ConversationScrollButton as ScrollButton,
33
- };
@@ -1,131 +0,0 @@
1
- import { setContext, getContext } from "svelte";
2
-
3
- const STICK_TO_BOTTOM_CONTEXT_KEY = Symbol("stick-to-bottom-context");
4
-
5
- const SCROLL_THRESHOLD_PX = 200;
6
-
7
- export class StickToBottomContext {
8
- #element: HTMLElement | null = $state(null);
9
- #isAtBottom = $state(true);
10
- #userHasScrolled = $state(false);
11
- #resizeObserver: ResizeObserver | null = null;
12
- #mutationObserver: MutationObserver | null = null;
13
- #intersectionObserver: IntersectionObserver | null = null;
14
- #sentinel: HTMLElement | null = null;
15
-
16
- isAtBottom = $derived(this.#isAtBottom);
17
-
18
- setElement(element: HTMLElement) {
19
- this.#cleanup();
20
- this.#element = element;
21
- if (element) this.#setupObservers();
22
- }
23
-
24
- scrollToBottom = (behavior: ScrollBehavior = "smooth") => {
25
- if (!this.#element) return;
26
- this.#userHasScrolled = false;
27
- this.#element.scrollTo({ top: this.#element.scrollHeight, behavior });
28
- };
29
-
30
- destroy() {
31
- this.#cleanup();
32
- this.#element = null;
33
- }
34
-
35
- #handleScroll = () => {
36
- if (!this.#element) return;
37
- const { scrollTop, scrollHeight, clientHeight } = this.#element;
38
- const isAtBottom = scrollTop + clientHeight >= scrollHeight - SCROLL_THRESHOLD_PX;
39
- this.#isAtBottom = isAtBottom;
40
- if (!isAtBottom) {
41
- this.#userHasScrolled = true;
42
- } else if (this.#userHasScrolled) {
43
- this.#userHasScrolled = false;
44
- }
45
- };
46
-
47
- #setupObservers() {
48
- if (!this.#element) return;
49
-
50
- this.#sentinel = document.createElement("div");
51
- this.#sentinel.style.height = "1px";
52
- this.#sentinel.style.width = "100%";
53
- this.#sentinel.style.pointerEvents = "none";
54
- this.#sentinel.style.opacity = "0";
55
- this.#sentinel.setAttribute("data-stick-to-bottom-sentinel", "");
56
- this.#element.appendChild(this.#sentinel);
57
-
58
- this.#intersectionObserver = new IntersectionObserver(
59
- (entries) => {
60
- const entry = entries[0];
61
- if (entry?.isIntersecting) {
62
- this.#isAtBottom = true;
63
- this.#userHasScrolled = false;
64
- }
65
- },
66
- { threshold: 0, root: this.#element },
67
- );
68
- this.#intersectionObserver.observe(this.#sentinel);
69
-
70
- this.#element.addEventListener("scroll", this.#handleScroll, { passive: true });
71
-
72
- this.#resizeObserver = new ResizeObserver(() => {
73
- this.#checkPosition();
74
- if (this.#isAtBottom && !this.#userHasScrolled) {
75
- this.scrollToBottom("auto");
76
- }
77
- });
78
- this.#resizeObserver.observe(this.#element);
79
-
80
- this.#mutationObserver = new MutationObserver(() => {
81
- requestAnimationFrame(() => {
82
- const shouldAutoScroll = this.#isAtBottom && !this.#userHasScrolled;
83
- this.#checkPosition();
84
- if (shouldAutoScroll) this.scrollToBottom("smooth");
85
- });
86
- });
87
- this.#mutationObserver.observe(this.#element, {
88
- childList: true,
89
- subtree: true,
90
- characterData: true,
91
- });
92
-
93
- this.#checkPosition();
94
- }
95
-
96
- #checkPosition() {
97
- if (!this.#element) return;
98
- const { scrollTop, scrollHeight, clientHeight } = this.#element;
99
- this.#isAtBottom = scrollTop + clientHeight >= scrollHeight - SCROLL_THRESHOLD_PX;
100
- }
101
-
102
- #cleanup() {
103
- this.#resizeObserver?.disconnect();
104
- this.#mutationObserver?.disconnect();
105
- this.#intersectionObserver?.disconnect();
106
- if (this.#element) {
107
- this.#element.removeEventListener("scroll", this.#handleScroll);
108
- }
109
- if (this.#sentinel && this.#element?.contains(this.#sentinel)) {
110
- this.#element.removeChild(this.#sentinel);
111
- }
112
- this.#resizeObserver = null;
113
- this.#mutationObserver = null;
114
- this.#intersectionObserver = null;
115
- this.#sentinel = null;
116
- }
117
- }
118
-
119
- export function setStickToBottomContext(): StickToBottomContext {
120
- const context = new StickToBottomContext();
121
- setContext(STICK_TO_BOTTOM_CONTEXT_KEY, context);
122
- return context;
123
- }
124
-
125
- export function getStickToBottomContext(): StickToBottomContext {
126
- const context = getContext<StickToBottomContext>(STICK_TO_BOTTOM_CONTEXT_KEY);
127
- if (!context) {
128
- throw new Error("Conversation parts must be used inside a <Conversation>.");
129
- }
130
- return context;
131
- }
@@ -1,7 +0,0 @@
1
- /* Styles for AnimatedValue. Plain stylesheet (see area-chart.css for why).
2
- `.hr-animated-value` is the component's own namespaced fallback for tabular
3
- figures (it also carries the `tabular-nums` utility class). */
4
-
5
- .hr-animated-value {
6
- font-variant-numeric: tabular-nums;
7
- }