@heyramzi/ui 6.1.0 → 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 (98) hide show
  1. package/COMPONENTS.md +6 -25
  2. package/README.md +6 -4
  3. package/package.json +7 -15
  4. package/scripts/sync-shadcn.mjs +12 -3
  5. package/src/ai/index.ts +0 -18
  6. package/src/animations/README.md +3 -3
  7. package/src/lib/sliding-pill.ts +68 -0
  8. package/src/lib/theme-transition.ts +4 -2
  9. package/src/shadcn/code-block/code-block-actions.svelte +23 -0
  10. package/src/shadcn/code-block/code-block-content.svelte +55 -0
  11. package/src/shadcn/code-block/code-block-copy-button.svelte +59 -0
  12. package/src/shadcn/code-block/code-block-footer.svelte +20 -0
  13. package/src/shadcn/code-block/code-block-header.svelte +23 -0
  14. package/src/shadcn/code-block/code-block-line.svelte +71 -0
  15. package/src/shadcn/code-block/code-block-title.svelte +23 -0
  16. package/src/shadcn/code-block/code-block.svelte +47 -0
  17. package/src/shadcn/code-block/index.ts +42 -0
  18. package/src/shadcn/hooks/highlighter.ts +132 -0
  19. package/src/shadcn/hooks/use-highlighted-code.svelte.ts +98 -0
  20. package/src/shadcn/kbd/index.ts +10 -0
  21. package/src/shadcn/kbd/kbd-group.svelte +20 -0
  22. package/src/shadcn/kbd/kbd.svelte +23 -0
  23. package/src/shadcn/message-scroller/index.ts +28 -0
  24. package/src/shadcn/message-scroller/message-scroller-button.svelte +52 -0
  25. package/src/shadcn/message-scroller/message-scroller-content.svelte +17 -0
  26. package/src/shadcn/message-scroller/message-scroller-item.svelte +22 -0
  27. package/src/shadcn/message-scroller/message-scroller-provider.svelte +7 -0
  28. package/src/shadcn/message-scroller/message-scroller-viewport.svelte +20 -0
  29. package/src/shadcn/message-scroller/message-scroller.svelte +20 -0
  30. package/src/shadcn/tabs/tabs-list.svelte +13 -2
  31. package/src/shadcn/tabs/tabs-trigger.svelte +1 -1
  32. package/src/shadcn/trace/index.ts +29 -0
  33. package/src/shadcn/trace/trace-collapsible-content.svelte +29 -0
  34. package/src/shadcn/trace/trace-collapsible-trigger.svelte +29 -0
  35. package/src/shadcn/trace/trace-collapsible.svelte +20 -0
  36. package/src/shadcn/trace/trace-description.svelte +20 -0
  37. package/src/shadcn/trace/trace-icon.svelte +24 -0
  38. package/src/shadcn/trace/trace-line.svelte +31 -0
  39. package/src/shadcn/trace/trace-title.svelte +20 -0
  40. package/src/shadcn/trace/trace.svelte +23 -0
  41. package/src/showcase/AiSection.svelte +0 -7
  42. package/src/showcase/demos/shadcn/code-block.svelte +80 -0
  43. package/src/showcase/demos/shadcn/index.ts +24 -0
  44. package/src/showcase/demos/shadcn/message-scroller.svelte +44 -0
  45. package/src/showcase/demos/shadcn/trace.svelte +51 -0
  46. package/src/showcase/kit.ts +1 -2
  47. package/src/ai/conversation/Conversation.svelte +0 -28
  48. package/src/ai/conversation/ConversationContent.svelte +0 -35
  49. package/src/ai/conversation/ConversationEmptyState.svelte +0 -46
  50. package/src/ai/conversation/ConversationScrollButton.svelte +0 -51
  51. package/src/ai/conversation/index.ts +0 -33
  52. package/src/ai/conversation/stick-to-bottom-context.svelte.ts +0 -131
  53. package/src/charts/animated-value.css +0 -7
  54. package/src/charts/animated-value.svelte +0 -110
  55. package/src/charts/area-chart.css +0 -29
  56. package/src/charts/area-chart.svelte +0 -195
  57. package/src/charts/bar-chart.css +0 -119
  58. package/src/charts/bar-chart.svelte +0 -269
  59. package/src/charts/charts.css +0 -201
  60. package/src/charts/crosshair.svelte +0 -35
  61. package/src/charts/donut-chart.css +0 -56
  62. package/src/charts/donut-chart.svelte +0 -158
  63. package/src/charts/edge-fade.svelte +0 -43
  64. package/src/charts/edge-fade.ts +0 -16
  65. package/src/charts/figure.css +0 -100
  66. package/src/charts/figure.svelte +0 -122
  67. package/src/charts/gauge-chart.css +0 -94
  68. package/src/charts/gauge-chart.svelte +0 -303
  69. package/src/charts/hover-band.test.ts +0 -30
  70. package/src/charts/hover-band.ts +0 -21
  71. package/src/charts/hover-focus.svelte +0 -85
  72. package/src/charts/index.ts +0 -30
  73. package/src/charts/legend.css +0 -131
  74. package/src/charts/legend.svelte +0 -112
  75. package/src/charts/line-chart.css +0 -26
  76. package/src/charts/line-chart.svelte +0 -256
  77. package/src/charts/motion.ts +0 -19
  78. package/src/charts/number-chart.css +0 -168
  79. package/src/charts/number-chart.svelte +0 -177
  80. package/src/charts/primitives/chart-container.svelte +0 -57
  81. package/src/charts/primitives/chart-style.svelte +0 -36
  82. package/src/charts/primitives/chart-tooltip.svelte +0 -161
  83. package/src/charts/primitives/chart-utils.ts +0 -54
  84. package/src/charts/primitives/index.ts +0 -11
  85. package/src/charts/reveal.svelte +0 -61
  86. package/src/charts/ring-chart.css +0 -96
  87. package/src/charts/ring-chart.svelte +0 -158
  88. package/src/charts/sparkline.css +0 -45
  89. package/src/charts/sparkline.svelte +0 -140
  90. package/src/charts/stat-card.css +0 -103
  91. package/src/charts/stat-card.svelte +0 -267
  92. package/src/charts/time-series-chart.css +0 -28
  93. package/src/charts/time-series-chart.svelte +0 -179
  94. package/src/charts/trend-pill.css +0 -38
  95. package/src/charts/trend-pill.svelte +0 -48
  96. package/src/lib/hooks/is-mobile.svelte.ts +0 -11
  97. package/src/showcase/ChartsSection.svelte +0 -458
  98. package/src/showcase/demos/ai/conversation.svelte +0 -59
package/COMPONENTS.md CHANGED
@@ -21,6 +21,7 @@ The style layer lives in [`src/tokens.css`](./src/tokens.css) and the motion CSS
21
21
  - `calendar` — Calendar
22
22
  - `card` — Card
23
23
  - `checkbox` — Checkbox
24
+ - `code-block` — CodeBlock. Highlight with `useHighlightedCode` from `@heyramzi/ui/shadcn/hooks/use-highlighted-code.svelte` (Shiki, ANSI too)
24
25
  - `collapsible` — Collapsible
25
26
  - `command` — Command
26
27
  - `context-menu` — ContextMenu
@@ -34,6 +35,7 @@ The style layer lives in [`src/tokens.css`](./src/tokens.css) and the motion CSS
34
35
  - `label` — Label
35
36
  - `marker` — Marker
36
37
  - `message` — Message
38
+ - `message-scroller` — MessageScroller, the chat transcript scroller: follows a streamed reply, anchors each turn, keeps place on prepend
37
39
  - `popover` — Popover
38
40
  - `progress` — Progress
39
41
  - `radio-group` — RadioGroup
@@ -53,6 +55,7 @@ The style layer lives in [`src/tokens.css`](./src/tokens.css) and the motion CSS
53
55
  - `toggle` — toggleVariants, ToggleSize, ToggleVariant, ToggleVariants
54
56
  - `toggle-group` — ToggleGroup
55
57
  - `tooltip` — Tooltip
58
+ - `trace` — Trace, an agent's steps: reasoning, tool calls and sources as status rows that can expand
56
59
 
57
60
  ## Effects (`src/effects`)
58
61
 
@@ -66,7 +69,7 @@ The style layer lives in [`src/tokens.css`](./src/tokens.css) and the motion CSS
66
69
  - `reveal` — InView, AnimatedGroup, TextEffect, observeInView, RevealPreset, RevealPer
67
70
  - `shader-gradient` — ShaderGradient, ShaderGradientProps, ShaderGradientUniforms
68
71
  - `spotlight` — Spotlight
69
- - `theme-toggle` — ThemeToggle, revealTheme. Props: `isDark`, `mode` (`light`, `dark`, `system`; swaps in the Monitor icon), `onToggle`, `variant` (`outline`, `ghost`, `inverted`; `inverted` boolean is an alias), `size`, `label`, `title`, `skipTransition`, `class`. `revealTheme(event, apply, { skip, duration })` is the circular wipe on its own: `import { revealTheme } from "@heyramzi/ui"` (the `./lib/*` subpath points at `.js` and the source is `.ts`).
72
+ - `theme-toggle` — ThemeToggle, revealTheme. Props: `isDark`, `mode` (`light`, `dark`, `system`; swaps in the Monitor icon), `onToggle`, `variant` (`outline`, `ghost`, `inverted`; `inverted` boolean is an alias), `size`, `label`, `title`, `skipTransition`, `class`. `revealTheme(event, apply, { skip, duration, easing })` is the circular wipe on its own: `import { revealTheme } from "@heyramzi/ui"` (the `./lib/*` subpath points at `.js` and the source is `.ts`).
70
73
 
71
74
  ## Animations (`src/animations`)
72
75
 
@@ -102,7 +105,6 @@ The style layer lives in [`src/tokens.css`](./src/tokens.css) and the motion CSS
102
105
 
103
106
  - `composer` — Composer, ComposerProps
104
107
  - `confirmation` — Confirmation, ConfirmationTitle, ConfirmationRequest, ConfirmationAccepted, ConfirmationRejected, ConfirmationActions, ConfirmationAction
105
- - `conversation` — Conversation, ConversationContent, ConversationEmptyState, ConversationScrollButton
106
108
  - `loading-state` — PixelLoader
107
109
  - `prompt-input` — PromptInput, PromptInputBody, PromptInputToolbar, PromptInputTextarea, PromptInputSubmit
108
110
  - `reasoning` — Reasoning, ReasoningTrigger, ReasoningContent
@@ -112,30 +114,9 @@ The style layer lives in [`src/tokens.css`](./src/tokens.css) and the motion CSS
112
114
  - `typing-dots` — TypingDots
113
115
  - `voice-waveform` — VoiceWaveform
114
116
 
115
- ## Charts (`src/charts`)
116
-
117
- - `animated-value` — AnimatedValue
118
- - `area-chart` — AreaChart
119
- - `bar-chart` — BarChart
120
- - `crosshair` — Crosshair
121
- - `donut-chart` — DonutChart
122
- - `edge-fade` — EdgeFade
123
- - `figure` — Figure
124
- - `gauge-chart` — GaugeChart
125
- - `hover-focus` — HoverFocus
126
- - `legend` — Legend
127
- - `line-chart` — LineChart
128
- - `number-chart` — NumberChart
129
- - `reveal` — Reveal
130
- - `ring-chart` — RingChart
131
- - `sparkline` — Sparkline
132
- - `stat-card` — StatCard
133
- - `time-series-chart` — TimeSeriesChart
134
- - `trend-pill` — TrendPill
135
- - `primitives` — ChartContainer, ChartTooltip, ChartStyle, ChartConfig, TooltipSeriesItem, getPayloadConfigFromPayload
136
-
137
117
  ---
138
118
 
139
119
  Skipped on purpose: brand assets (logos, marks, brand renderer, language flags,
140
- locale switch), kanban and status (they live in `@heyramzi/board`). The bot avatars
120
+ locale switch), kanban, list, gantt and status (they live in `@heyramzi/tasks`), and charts
121
+ (they live in `@heyramzi/charts`). The bot avatars
141
122
  are in `src/agents`, imported from `@heyramzi/ui/agents`.
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # @heyramzi/ui
2
2
 
3
3
  Svelte 5 UI primitives you copy into your project and own. Components, visual
4
- effects, CSS animations, charts and a small set of chat and AI surfaces. No build step, no runtime wrapper,
4
+ effects, CSS animations and a small set of chat and AI surfaces. No build step, no runtime wrapper,
5
5
  no framework lock-in.
6
6
 
7
7
  The package is a source registry. Every component is a folder under
@@ -15,9 +15,7 @@ only a convenience.
15
15
  image comparison, shimmer gradients and a theme toggle.
16
16
  - `src/animations` keyframe CSS for dropdowns, modals, tooltips, text swaps,
17
17
  success checks and the rest of the motion vocabulary.
18
- - `src/ai` chat and AI surfaces: conversation, composer, prompt input, response, reasoning, confirmation and loaders.
19
- - `src/charts` SVG charts: line, area, bar, donut, ring, gauge, sparkline,
20
- time series, and stat cards.
18
+ - `src/ai` chat and AI surfaces: composer, prompt input, response, reasoning, confirmation and loaders.
21
19
  - `src/agents` bot avatars and a cast grid, imported from `@heyramzi/ui/agents`:
22
20
  `AgentAvatar` draws an eyes-only face from a name, role, shape and colour, and
23
21
  `AgentCast` lays a list of agents out as cards you can pick from.
@@ -43,6 +41,10 @@ Retheme by overriding any variable on `:root`, for example
43
41
  `--color-brand-indigo` or `--radius-lg`. Dark mode keys off a `.dark`
44
42
  class on `html`.
45
43
 
44
+ Charts live in their own package now,
45
+ [`@heyramzi/charts`](https://github.com/heyramzi/ui/tree/main/packages/charts). It reads
46
+ the same tokens, so import `tokens.css` once and both work.
47
+
46
48
  React and other framework ports are out of scope. The source is Svelte 5
47
49
  runes only.
48
50
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heyramzi/ui",
3
- "version": "6.1.0",
3
+ "version": "7.0.0",
4
4
  "description": "Svelte 5 UI primitives: effects, animations, and shared component CSS.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -115,11 +115,6 @@
115
115
  "./animations/*": {
116
116
  "default": "./src/animations/*"
117
117
  },
118
- "./charts": {
119
- "svelte": "./src/charts/index.ts",
120
- "types": "./src/charts/index.ts",
121
- "default": "./src/charts/index.ts"
122
- },
123
118
  "./showcase": {
124
119
  "svelte": "./src/showcase/index.ts",
125
120
  "types": "./src/showcase/index.ts",
@@ -130,10 +125,10 @@
130
125
  "types": "./src/showcase/kit.ts",
131
126
  "default": "./src/showcase/kit.ts"
132
127
  },
133
- "./shadcn/hooks/is-mobile.svelte": {
134
- "svelte": "./src/shadcn/hooks/is-mobile.svelte.ts",
135
- "types": "./src/shadcn/hooks/is-mobile.svelte.ts",
136
- "default": "./src/shadcn/hooks/is-mobile.svelte.ts"
128
+ "./shadcn/hooks/*": {
129
+ "svelte": "./src/shadcn/hooks/*.ts",
130
+ "types": "./src/shadcn/hooks/*.ts",
131
+ "default": "./src/shadcn/hooks/*.ts"
137
132
  },
138
133
  "./shadcn/*": {
139
134
  "svelte": "./src/shadcn/*/index.ts",
@@ -164,20 +159,17 @@
164
159
  "dependencies": {
165
160
  "@heyramzi/utils": "^1.3.3",
166
161
  "@lucide/svelte": "^1.52.0",
162
+ "@shadcn-svelte/primitives": "^0.0.1",
167
163
  "clsx": "^2.1.1",
168
- "d3-scale": "^4.0.2",
169
- "d3-shape": "^3.2.0",
170
- "layerchart": "^2.5.1",
171
164
  "marked": "^18.1.0",
172
165
  "phosphor-svelte": "^3.1.0",
166
+ "shiki": "^4.5.0",
173
167
  "tailwind-merge": "^3.7.0",
174
168
  "tailwind-variants": "^3.3.1"
175
169
  },
176
170
  "devDependencies": {
177
171
  "@internationalized/date": "^3.12.4",
178
172
  "@sveltejs/vite-plugin-svelte": "^7.3.1",
179
- "@types/d3-scale": "^4.0.9",
180
- "@types/d3-shape": "^3.2.0",
181
173
  "bits-ui": "^2.19.5",
182
174
  "jsdom": "^30.1.2",
183
175
  "mode-watcher": "^1.1.0",
@@ -42,12 +42,16 @@ const ALIASES = [
42
42
  ["$UTILS$.js", "@heyramzi/utils"],
43
43
  ["$UI$/", "../src/shadcn"],
44
44
  ["$HOOKS$/", "../src/shadcn/hooks"],
45
+ ["$LIB$/", "../src/shadcn/hooks"],
45
46
  ];
46
47
 
47
48
  /* Upstream target path -> ours, where the two differ. Everything else lands at
48
49
  * src/shadcn/<target>. */
49
50
  const REROUTE = {
50
51
  "is-mobile/is-mobile.svelte.ts": "src/shadcn/hooks/is-mobile.svelte.ts",
52
+ "highlighter/highlighter.ts": "src/shadcn/hooks/highlighter.ts",
53
+ "use-highlighted-code/use-highlighted-code.svelte.ts":
54
+ "src/shadcn/hooks/use-highlighted-code.svelte.ts",
51
55
  };
52
56
 
53
57
  const DOCS = "https://raw.githubusercontent.com/huntabyte/shadcn-svelte/main/docs";
@@ -90,12 +94,17 @@ const NOT_SHIPPED = new Set([
90
94
  // `cn` ships from @heyramzi/utils in vibe-kit, so a copy here would be a stray.
91
95
  "utils",
92
96
  "questionnaire",
93
- // Every app chat runs on our ai/conversation, so a second scroller would be two answers to one job.
94
- "message-scroller",
95
97
  ]);
96
98
 
97
99
  /* No demo to take: `hooks`, `is-mobile` and `utils` are not components, and `sidebar` demos as a page block. */
98
- const NO_DEMO = new Set(["hooks", "is-mobile", "sidebar", "utils"]);
100
+ const NO_DEMO = new Set([
101
+ "hooks",
102
+ "is-mobile",
103
+ "highlighter",
104
+ "use-highlighted-code",
105
+ "sidebar",
106
+ "utils",
107
+ ]);
99
108
 
100
109
  const adapt = (text) => ALIASES.reduce((acc, [from, to]) => acc.replaceAll(from, to), text);
101
110
  const adaptDemo = (text) =>
package/src/ai/index.ts CHANGED
@@ -1,21 +1,3 @@
1
- // =========================
2
- // CONVERSATION (scroll container)
3
- // =========================
4
-
5
- export {
6
- Conversation,
7
- ConversationContent,
8
- ConversationEmptyState,
9
- ConversationScrollButton,
10
- StickToBottomContext,
11
- getStickToBottomContext,
12
- setStickToBottomContext,
13
- type ConversationProps,
14
- type ConversationContentProps,
15
- type ConversationEmptyStateProps,
16
- type ConversationScrollButtonProps,
17
- } from "./conversation/index.js";
18
-
19
1
  // =========================
20
2
  // COMPOSER
21
3
  // =========================
@@ -53,8 +53,8 @@ One behaviour, one implementation. Where a component already did the job, the up
53
53
 
54
54
  | Upstream | Where it lives instead |
55
55
  | --- | --- |
56
- | `toggle` | `shadcn/switch/switch.css` — the thumb slide, on our switch. |
57
- | `checkbox-check` | `shadcn/checkbox/checkbox.css` — the mark draw, on our checkbox. |
56
+ | `toggle` | `shadcn/switch/switch.svelte` — the thumb slide, on our switch. |
57
+ | `checkbox-check` | `shadcn/checkbox/checkbox.svelte` — the fill and border, on our checkbox. |
58
58
  | `toast-open-close` | `shadcn/sonner` — svelte-sonner owns toast motion. |
59
59
  | `banner-stacking` | `shadcn/sonner` — stacking and hover spread are what it does. |
60
60
  | `matrix-dot-loader` | `ai/loading-state` — `PixelLoader` is the dot-grid loader. |
@@ -81,4 +81,4 @@ Every transition reads semantic CSS variables (`--modal-open-dur`, `--tilt-follo
81
81
 
82
82
  So the scale is single-sourced when the design layer is imported, and the transition still runs at the shipped value when it is not. A value the scale has no token for — a 1000ms tilt return, a 90ms reel stagger — stays a literal rather than being rounded onto the nearest rung. Override any of them at `:root` or per-scope to retune without forking the CSS.
83
83
 
84
- See the [transitions.dev SKILL](../../../../ai-doc/skills/design/motion/transitions-dev/SKILL.md) for the full token list and the per-transition variables.
84
+ See the `transitions-dev` skill in vibe-kit (`ai-doc/skills/design/motion/transitions-dev/SKILL.md`) for the full token list and the per-transition variables.
@@ -0,0 +1,68 @@
1
+ import type { Attachment } from "svelte/attachments";
2
+
3
+ /** The travelling highlight behind the active tab. Attach it to a span inside the list. */
4
+
5
+ // WHY attach to the pill, not the list: the pill finds its list via
6
+ // `parentElement`, so any strip that renders one span gets the motion.
7
+
8
+ // WHY measure: the active item's width is its label. The selector covers the
9
+ // three ways a strip marks it: a tab, a nav link, a toggle.
10
+
11
+ // Per transitions.dev `tabs-sliding`: first paint and resizes write the
12
+ // position with the transition off, so the pill snaps in instead of flying.
13
+ const ACTIVE = '[data-state="active"],[aria-current="page"],[aria-pressed="true"]';
14
+
15
+ export const slidingPill: Attachment<HTMLElement> = (pill) => {
16
+ const list = pill.parentElement;
17
+ if (!list) return;
18
+
19
+ let settled = false;
20
+
21
+ function move(animate: boolean) {
22
+ const active = list!.querySelector<HTMLElement>(ACTIVE);
23
+ if (!active) {
24
+ pill.style.opacity = "0";
25
+ return;
26
+ }
27
+ pill.style.opacity = "1";
28
+ if (animate) {
29
+ pill.style.transform = `translateX(${active.offsetLeft}px)`;
30
+ pill.style.width = `${active.offsetWidth}px`;
31
+ return;
32
+ }
33
+ // Suspend, write, reflow, restore. Without the reflow the browser
34
+ // coalesces the two writes and the snap animates anyway.
35
+ pill.style.transition = "none";
36
+ pill.style.transform = `translateX(${active.offsetLeft}px)`;
37
+ pill.style.width = `${active.offsetWidth}px`;
38
+ void pill.offsetWidth;
39
+ pill.style.transition = "";
40
+ }
41
+
42
+ // Position first, THEN hand the highlight over. Swapped, one frame draws both
43
+ // the trigger's own background and a zero-width pill: a flicker on hydration.
44
+ move(false);
45
+ list.dataset.pill = "on";
46
+ settled = true;
47
+
48
+ // The selection lives in an attribute bits-ui owns, and a badge arriving
49
+ // changes a trigger's width without any attribute changing.
50
+ const attributes = new MutationObserver(() => move(settled));
51
+ attributes.observe(list, {
52
+ subtree: true,
53
+ childList: true,
54
+ attributes: true,
55
+ attributeFilter: ["data-state", "aria-current", "aria-pressed"],
56
+ });
57
+
58
+ // A list that reflows (a rail collapsing, a font landing) re-measures
59
+ // without motion: nothing was chosen, so nothing should travel.
60
+ const resize = new ResizeObserver(() => move(false));
61
+ resize.observe(list);
62
+
63
+ return () => {
64
+ attributes.disconnect();
65
+ resize.disconnect();
66
+ delete list.dataset.pill;
67
+ };
68
+ };
@@ -3,6 +3,8 @@ export interface RevealThemeOptions {
3
3
  skip?: boolean;
4
4
  /** Wipe length in ms. */
5
5
  duration?: number;
6
+ /** Wipe easing, any CSS easing value. */
7
+ easing?: string;
6
8
  }
7
9
 
8
10
  const DEFAULT_DURATION = 400;
@@ -20,7 +22,7 @@ export function revealTheme(
20
22
  apply: () => void,
21
23
  options: RevealThemeOptions = {},
22
24
  ): void {
23
- const { skip = false, duration = DEFAULT_DURATION } = options;
25
+ const { skip = false, duration = DEFAULT_DURATION, easing = "ease-in-out" } = options;
24
26
  const origin = event.currentTarget;
25
27
 
26
28
  const prefersReduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
@@ -45,7 +47,7 @@ export function revealTheme(
45
47
  transition.ready.then(() => {
46
48
  document.documentElement.animate(
47
49
  { clipPath: [`circle(0px at ${x}px ${y}px)`, `circle(${endRadius}px at ${x}px ${y}px)`] },
48
- { duration, easing: "ease-in-out", pseudoElement: "::view-transition-new(root)" },
50
+ { duration, easing, pseudoElement: "::view-transition-new(root)" },
49
51
  );
50
52
  });
51
53
  }
@@ -0,0 +1,23 @@
1
+ <script lang="ts">
2
+ import { cn, type WithElementRef } from "@heyramzi/utils";
3
+ import type { HTMLAttributes } from "svelte/elements";
4
+
5
+ let {
6
+ ref = $bindable(null),
7
+ class: className,
8
+ children,
9
+ ...restProps
10
+ }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
11
+ </script>
12
+
13
+ <div
14
+ bind:this={ref}
15
+ data-slot="code-block-actions"
16
+ class={cn(
17
+ "-my-1 -me-1.5 gap-1 [[data-slot=code-block]>&]:m-0 [[data-slot=code-block]>&]:top-3 [[data-slot=code-block]>&]:end-3 group-data-[size=sm]/code-block:[[data-slot=code-block]>&]:top-2 group-data-[size=sm]/code-block:[[data-slot=code-block]>&]:end-2 [[data-slot=code-block]>&]:*:[button]:bg-background [[data-slot=code-block]>&]:*:[button]:ring-1 [[data-slot=code-block]>&]:*:[button]:ring-foreground/10 ms-auto flex shrink-0 items-center [[data-slot=code-block]>&]:absolute [[data-slot=code-block]>&]:z-10 [[data-slot=code-block]>&]:opacity-0 [[data-slot=code-block]>&]:transition-opacity [[data-slot=code-block]>&]:group-focus-within/code-block:opacity-100 [[data-slot=code-block]>&]:group-hover/code-block:opacity-100 [[data-slot=code-block]>&]:pointer-coarse:opacity-100",
18
+ className
19
+ )}
20
+ {...restProps}
21
+ >
22
+ {@render children?.()}
23
+ </div>
@@ -0,0 +1,55 @@
1
+ <script lang="ts">
2
+ import { cn, type WithElementRef } from "@heyramzi/utils";
3
+ import CodeBlockLine from "./code-block-line.svelte";
4
+ import type { HTMLAttributes } from "svelte/elements";
5
+
6
+ let {
7
+ ref = $bindable(null),
8
+ class: className,
9
+ style,
10
+ code,
11
+ wrap = false,
12
+ lineNumbers = false,
13
+ startLine = 1,
14
+ children,
15
+ ...restProps
16
+ }: WithElementRef<HTMLAttributes<HTMLPreElement>> & {
17
+ /** Plain text to render. Split into lines when `lineNumbers` is set. */
18
+ code?: string;
19
+ wrap?: boolean;
20
+ lineNumbers?: boolean;
21
+ startLine?: number;
22
+ } = $props();
23
+
24
+ // A `code` string is split into lines so each one can be numbered.
25
+ const lines = $derived(
26
+ code !== undefined && lineNumbers ? code.replace(/\n$/, "").split("\n") : null
27
+ );
28
+ </script>
29
+
30
+ {#snippet content()}
31
+ <code
32
+ data-slot="code-block-code"class=" grid min-h-lh min-w-max content-start in-data-numbered:grid-cols-[auto_1fr] in-data-wrap:min-w-0">
33
+ {#if lines}
34
+ {#each lines as line, index (index)}
35
+ <CodeBlockLine code={line} />
36
+ {/each}
37
+ {:else if code !== undefined}
38
+ {code}
39
+ {:else}
40
+ {@render children?.()}
41
+ {/if}
42
+ </code>
43
+ {/snippet}
44
+
45
+ <!-- Whitespace inside a <pre> is rendered, so its content is a snippet defined outside it, where Svelte trims whitespace as usual. -->
46
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
47
+ <pre
48
+ bind:this={ref}
49
+ data-slot="code-block-content"data-wrap={wrap || undefined}
50
+ data-numbered={lineNumbers || undefined}
51
+ dir="ltr"tabindex={0}
52
+ style="counter-reset: line {startLine - 1};{style ?? ''}"class={cn("px-4 py-4 leading-relaxed group-has-data-[slot=code-block-header]/code-block:pt-2 group-has-data-[slot=code-block-footer]/code-block:pb-2 group-data-[size=sm]/code-block:px-3 group-data-[size=sm]/code-block:py-3 group-data-[size=sm]/code-block:group-has-data-[slot=code-block-header]/code-block:pt-1.5 min-h-0 min-w-0 scroll-fade-x scrollbar-thin overflow-auto text-start font-mono outline-none data-wrap:scroll-fade-y data-wrap:wrap-break-word data-wrap:whitespace-pre-wrap",
53
+ className
54
+ )}
55
+ {...restProps}>{@render content()}</pre>
@@ -0,0 +1,59 @@
1
+ <script lang="ts">
2
+ import { onDestroy } from "svelte";
3
+ import CheckIcon from "@lucide/svelte/icons/check";
4
+ import CopyIcon from "@lucide/svelte/icons/copy";
5
+ import { Button, type ButtonProps } from "../button/index.js";
6
+ import { cn } from "@heyramzi/utils";
7
+
8
+ let {
9
+ ref = $bindable(null),
10
+ class: className,
11
+ value,
12
+ variant = "ghost",
13
+ size = "sm",
14
+ onclick,
15
+ ...restProps
16
+ }: Omit<ButtonProps, "onclick"> & {
17
+ value: string;
18
+ onclick?: (e: MouseEvent) => void;
19
+ } = $props();
20
+
21
+ let copied = $state(false);
22
+ let timeout: ReturnType<typeof setTimeout> | undefined;
23
+
24
+ function copy() {
25
+ navigator.clipboard
26
+ .writeText(value)
27
+ .then(() => {
28
+ copied = true;
29
+ clearTimeout(timeout);
30
+ timeout = setTimeout(() => (copied = false), 2000);
31
+ })
32
+ .catch(() => {});
33
+ }
34
+
35
+ onDestroy(() => clearTimeout(timeout));
36
+ </script>
37
+
38
+ <Button
39
+ bind:ref
40
+ data-slot="code-block-copy-button"
41
+ data-copied={copied || undefined}
42
+ {variant}
43
+ {size}
44
+ icon
45
+ aria-label={copied ? "Copied" : "Copy"}
46
+ class={cn(className)}
47
+ onclick={(e) => {
48
+ onclick?.(e);
49
+ copy();
50
+ }}
51
+ {...restProps}
52
+ >
53
+ {#if copied}
54
+ <CheckIcon />
55
+ {:else}
56
+ <CopyIcon />
57
+ {/if}
58
+ </Button>
59
+ <span aria-live="polite" class="sr-only">{copied ? "Copied" : ""}</span>
@@ -0,0 +1,20 @@
1
+ <script lang="ts">
2
+ import { cn, type WithElementRef } from "@heyramzi/utils";
3
+ import type { HTMLAttributes } from "svelte/elements";
4
+
5
+ let {
6
+ ref = $bindable(null),
7
+ class: className,
8
+ children,
9
+ ...restProps
10
+ }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
11
+ </script>
12
+
13
+ <div
14
+ bind:this={ref}
15
+ data-slot="code-block-footer"
16
+ class={cn("gap-2 px-4 pb-4 text-muted-foreground group-data-[size=sm]/code-block:px-3 group-data-[size=sm]/code-block:pb-3 flex min-w-0 shrink-0 items-center", className)}
17
+ {...restProps}
18
+ >
19
+ {@render children?.()}
20
+ </div>
@@ -0,0 +1,23 @@
1
+ <script lang="ts">
2
+ import { cn, type WithElementRef } from "@heyramzi/utils";
3
+ import type { HTMLAttributes } from "svelte/elements";
4
+
5
+ let {
6
+ ref = $bindable(null),
7
+ class: className,
8
+ children,
9
+ ...restProps
10
+ }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
11
+ </script>
12
+
13
+ <div
14
+ bind:this={ref}
15
+ data-slot="code-block-header"
16
+ class={cn(
17
+ "gap-2 px-4 pt-4 text-muted-foreground group-data-[variant=destructive]/code-block:text-destructive group-data-[size=sm]/code-block:px-3 group-data-[size=sm]/code-block:pt-3 flex min-w-0 shrink-0 items-center *:has-[>[data-slot=code-block-title]]:min-w-0",
18
+ className
19
+ )}
20
+ {...restProps}
21
+ >
22
+ {@render children?.()}
23
+ </div>
@@ -0,0 +1,71 @@
1
+ <script lang="ts" module>
2
+ import { tv, type VariantProps } from "tailwind-variants";
3
+
4
+ export const codeBlockLineVariants = tv({
5
+ base: "-mx-4 px-4 in-data-numbered:gap-x-3 before:min-w-6 before:text-end before:text-muted-foreground after:start-1 after:text-muted-foreground group-data-[size=sm]/code-block:-mx-3 group-data-[size=sm]/code-block:px-3 relative block min-h-lh after:absolute after:top-0 after:select-none in-data-numbered:col-span-2 in-data-numbered:grid in-data-numbered:grid-cols-subgrid in-data-numbered:[counter-increment:line] in-data-numbered:before:content-[counter(line)] in-data-numbered:before:select-none in-data-numbered:supports-[content:counter(line)_/_'']:before:content-[counter(line)_/_'']",
6
+ variants: {
7
+ variant: {
8
+ default: "",
9
+ added: "bg-muted after:content-['+']",
10
+ removed: "bg-destructive/10 after:text-destructive after:content-['−']",
11
+ highlighted: "bg-muted",
12
+ },
13
+ },
14
+ defaultVariants: {
15
+ variant: "default",
16
+ },
17
+ });
18
+
19
+ export type CodeBlockLineVariant = VariantProps<typeof codeBlockLineVariants>["variant"];
20
+
21
+ // One highlighted token, such as one returned by `useHighlightedCode`.
22
+ export type CodeBlockLineToken = {
23
+ content: string;
24
+ style?: string;
25
+ class?: string;
26
+ };
27
+ </script>
28
+
29
+ <script lang="ts">
30
+ import { cn, type WithElementRef } from "@heyramzi/utils";
31
+ import type { HTMLAttributes } from "svelte/elements";
32
+
33
+ let {
34
+ ref = $bindable(null),
35
+ class: className,
36
+ variant = "default",
37
+ code,
38
+ children,
39
+ ...restProps
40
+ }: WithElementRef<HTMLAttributes<HTMLSpanElement>> & {
41
+ variant?: CodeBlockLineVariant;
42
+ /** The line as plain text, or as highlighted tokens. When it is set, `children` is ignored. */
43
+ code?: string | CodeBlockLineToken[];
44
+ } = $props();
45
+
46
+ const newline = "\n";
47
+ </script>
48
+
49
+ <!-- An empty line holds a newline so copying a selection keeps blank lines. -->
50
+ <span
51
+ bind:this={ref}
52
+ data-slot="code-block-line"data-variant={variant}
53
+ class={cn(codeBlockLineVariants({ variant }), className)}
54
+ {...restProps}
55
+ >
56
+ <span data-slot="code-block-line-content"class="min-w-0">
57
+ {#if typeof code ==="string"}
58
+ {code || newline}
59
+ {:else if code?.length}
60
+ {#each code as token, index (index)}
61
+ <span style={token.style} class={token.class}>{token.content}</span>
62
+ {/each}
63
+ {:else if code}
64
+ {newline}
65
+ {:else if children}
66
+ {@render children()}
67
+ {:else}
68
+ {newline}
69
+ {/if}
70
+ </span>
71
+ </span>
@@ -0,0 +1,23 @@
1
+ <script lang="ts">
2
+ import { cn, type WithElementRef } from "@heyramzi/utils";
3
+ import type { HTMLAttributes } from "svelte/elements";
4
+
5
+ let {
6
+ ref = $bindable(null),
7
+ class: className,
8
+ children,
9
+ ...restProps
10
+ }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
11
+ </script>
12
+
13
+ <div
14
+ bind:this={ref}
15
+ data-slot="code-block-title"
16
+ class={cn(
17
+ "font-sans text-sm font-medium text-foreground group-data-[variant=destructive]/code-block:text-destructive *:[svg]:me-1.5 [&_svg:not([class*='size-'])]:size-4 group-data-[size=sm]/code-block:text-xs group-data-[size=sm]/code-block:[&_svg:not([class*='size-'])]:size-3.5 min-w-0 truncate [&_svg]:pointer-events-none [&_svg]:inline-block [&_svg]:align-middle",
18
+ className
19
+ )}
20
+ {...restProps}
21
+ >
22
+ {@render children?.()}
23
+ </div>
@@ -0,0 +1,47 @@
1
+ <script lang="ts" module>
2
+ import { tv, type VariantProps } from "tailwind-variants";
3
+
4
+ export const codeBlockVariants = tv({
5
+ base: "rounded-lg text-sm data-[size=sm]:text-xs has-[>[data-slot=code-block-content]:focus-visible]:ring-3 has-[>[data-slot=code-block-content]:focus-visible]:ring-ring/50 group/code-block relative flex w-full min-w-0 flex-col overflow-hidden",
6
+ variants: {
7
+ variant: {
8
+ default: "border border-input text-foreground dark:bg-input/30 has-[>[data-slot=code-block-content]:focus-visible]:border-ring",
9
+ destructive: "border border-destructive/30 text-destructive dark:border-destructive/40",
10
+ },
11
+ },
12
+ defaultVariants: {
13
+ variant: "default",
14
+ },
15
+ });
16
+
17
+ export type CodeBlockVariant = VariantProps<typeof codeBlockVariants>["variant"];
18
+ export type CodeBlockSize = "default" | "sm";
19
+ </script>
20
+
21
+ <script lang="ts">
22
+ import { cn, type WithElementRef } from "@heyramzi/utils";
23
+ import type { HTMLAttributes } from "svelte/elements";
24
+
25
+ let {
26
+ ref = $bindable(null),
27
+ class: className,
28
+ variant = "default",
29
+ size = "default",
30
+ children,
31
+ ...restProps
32
+ }: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
33
+ variant?: CodeBlockVariant;
34
+ size?: CodeBlockSize;
35
+ } = $props();
36
+ </script>
37
+
38
+ <div
39
+ bind:this={ref}
40
+ data-slot="code-block"
41
+ data-variant={variant}
42
+ data-size={size}
43
+ class={cn(codeBlockVariants({ variant }), className)}
44
+ {...restProps}
45
+ >
46
+ {@render children?.()}
47
+ </div>