@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.
- package/COMPONENTS.md +7 -26
- package/README.md +12 -5
- package/package.json +14 -16
- package/scripts/sync-shadcn.mjs +12 -3
- package/src/agents/agent-avatar.svelte +639 -0
- package/src/agents/agent-cast.svelte +173 -0
- package/src/agents/demo.svelte +78 -0
- package/src/agents/index.ts +11 -0
- package/src/agents/types.ts +72 -0
- package/{LICENSE → src/agents/vendor/LICENSE} +1 -1
- package/src/agents/vendor/color.ts +155 -0
- package/src/agents/vendor/draw.ts +840 -0
- package/src/agents/vendor/engine.ts +845 -0
- package/src/agents/vendor/mesh.ts +838 -0
- package/src/agents/vendor/plastic.ts +4958 -0
- package/src/agents/vendor/presets.ts +103 -0
- package/src/agents/vendor/shapes.ts +41 -0
- package/src/agents/vendor/ticker.ts +67 -0
- package/src/agents/vendor/types.ts +261 -0
- package/src/agents/vendor/wear.ts +1699 -0
- package/src/ai/index.ts +0 -18
- package/src/animations/README.md +3 -3
- package/src/lib/sliding-pill.ts +68 -0
- package/src/lib/theme-transition.ts +4 -2
- package/src/shadcn/code-block/code-block-actions.svelte +23 -0
- package/src/shadcn/code-block/code-block-content.svelte +55 -0
- package/src/shadcn/code-block/code-block-copy-button.svelte +59 -0
- package/src/shadcn/code-block/code-block-footer.svelte +20 -0
- package/src/shadcn/code-block/code-block-header.svelte +23 -0
- package/src/shadcn/code-block/code-block-line.svelte +71 -0
- package/src/shadcn/code-block/code-block-title.svelte +23 -0
- package/src/shadcn/code-block/code-block.svelte +47 -0
- package/src/shadcn/code-block/index.ts +42 -0
- package/src/shadcn/hooks/highlighter.ts +132 -0
- package/src/shadcn/hooks/use-highlighted-code.svelte.ts +98 -0
- package/src/shadcn/kbd/index.ts +10 -0
- package/src/shadcn/kbd/kbd-group.svelte +20 -0
- package/src/shadcn/kbd/kbd.svelte +23 -0
- package/src/shadcn/message-scroller/index.ts +28 -0
- package/src/shadcn/message-scroller/message-scroller-button.svelte +52 -0
- package/src/shadcn/message-scroller/message-scroller-content.svelte +17 -0
- package/src/shadcn/message-scroller/message-scroller-item.svelte +22 -0
- package/src/shadcn/message-scroller/message-scroller-provider.svelte +7 -0
- package/src/shadcn/message-scroller/message-scroller-viewport.svelte +20 -0
- package/src/shadcn/message-scroller/message-scroller.svelte +20 -0
- package/src/shadcn/tabs/tabs-list.svelte +13 -2
- package/src/shadcn/tabs/tabs-trigger.svelte +1 -1
- package/src/shadcn/trace/index.ts +29 -0
- package/src/shadcn/trace/trace-collapsible-content.svelte +29 -0
- package/src/shadcn/trace/trace-collapsible-trigger.svelte +29 -0
- package/src/shadcn/trace/trace-collapsible.svelte +20 -0
- package/src/shadcn/trace/trace-description.svelte +20 -0
- package/src/shadcn/trace/trace-icon.svelte +24 -0
- package/src/shadcn/trace/trace-line.svelte +31 -0
- package/src/shadcn/trace/trace-title.svelte +20 -0
- package/src/shadcn/trace/trace.svelte +23 -0
- package/src/showcase/AiSection.svelte +0 -7
- package/src/showcase/demos/shadcn/code-block.svelte +80 -0
- package/src/showcase/demos/shadcn/index.ts +24 -0
- package/src/showcase/demos/shadcn/message-scroller.svelte +44 -0
- package/src/showcase/demos/shadcn/trace.svelte +51 -0
- package/src/showcase/kit.ts +1 -2
- package/src/ai/conversation/Conversation.svelte +0 -28
- package/src/ai/conversation/ConversationContent.svelte +0 -35
- package/src/ai/conversation/ConversationEmptyState.svelte +0 -46
- package/src/ai/conversation/ConversationScrollButton.svelte +0 -51
- package/src/ai/conversation/index.ts +0 -33
- package/src/ai/conversation/stick-to-bottom-context.svelte.ts +0 -131
- package/src/charts/animated-value.css +0 -7
- package/src/charts/animated-value.svelte +0 -110
- package/src/charts/area-chart.css +0 -29
- package/src/charts/area-chart.svelte +0 -195
- package/src/charts/bar-chart.css +0 -119
- package/src/charts/bar-chart.svelte +0 -269
- package/src/charts/charts.css +0 -201
- package/src/charts/crosshair.svelte +0 -35
- package/src/charts/donut-chart.css +0 -56
- package/src/charts/donut-chart.svelte +0 -158
- package/src/charts/edge-fade.svelte +0 -43
- package/src/charts/edge-fade.ts +0 -16
- package/src/charts/figure.css +0 -100
- package/src/charts/figure.svelte +0 -122
- package/src/charts/gauge-chart.css +0 -94
- package/src/charts/gauge-chart.svelte +0 -303
- package/src/charts/hover-band.test.ts +0 -30
- package/src/charts/hover-band.ts +0 -21
- package/src/charts/hover-focus.svelte +0 -85
- package/src/charts/index.ts +0 -30
- package/src/charts/legend.css +0 -131
- package/src/charts/legend.svelte +0 -112
- package/src/charts/line-chart.css +0 -26
- package/src/charts/line-chart.svelte +0 -256
- package/src/charts/motion.ts +0 -19
- package/src/charts/number-chart.css +0 -168
- package/src/charts/number-chart.svelte +0 -177
- package/src/charts/primitives/chart-container.svelte +0 -57
- package/src/charts/primitives/chart-style.svelte +0 -36
- package/src/charts/primitives/chart-tooltip.svelte +0 -161
- package/src/charts/primitives/chart-utils.ts +0 -54
- package/src/charts/primitives/index.ts +0 -11
- package/src/charts/reveal.svelte +0 -61
- package/src/charts/ring-chart.css +0 -96
- package/src/charts/ring-chart.svelte +0 -158
- package/src/charts/sparkline.css +0 -45
- package/src/charts/sparkline.svelte +0 -140
- package/src/charts/stat-card.css +0 -103
- package/src/charts/stat-card.svelte +0 -267
- package/src/charts/time-series-chart.css +0 -28
- package/src/charts/time-series-chart.svelte +0 -179
- package/src/charts/trend-pill.css +0 -38
- package/src/charts/trend-pill.svelte +0 -48
- package/src/lib/hooks/is-mobile.svelte.ts +0 -11
- package/src/showcase/ChartsSection.svelte +0 -458
- package/src/showcase/demos/ai/conversation.svelte +0 -59
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
|
// =========================
|
package/src/animations/README.md
CHANGED
|
@@ -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.
|
|
57
|
-
| `checkbox-check` | `shadcn/checkbox/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
|
|
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
|
|
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>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import Actions from "./code-block-actions.svelte";
|
|
2
|
+
import Content from "./code-block-content.svelte";
|
|
3
|
+
import CopyButton from "./code-block-copy-button.svelte";
|
|
4
|
+
import Footer from "./code-block-footer.svelte";
|
|
5
|
+
import Header from "./code-block-header.svelte";
|
|
6
|
+
import Title from "./code-block-title.svelte";
|
|
7
|
+
import Line, {
|
|
8
|
+
codeBlockLineVariants,
|
|
9
|
+
type CodeBlockLineToken,
|
|
10
|
+
type CodeBlockLineVariant,
|
|
11
|
+
} from "./code-block-line.svelte";
|
|
12
|
+
import Root, {
|
|
13
|
+
codeBlockVariants,
|
|
14
|
+
type CodeBlockSize,
|
|
15
|
+
type CodeBlockVariant,
|
|
16
|
+
} from "./code-block.svelte";
|
|
17
|
+
|
|
18
|
+
export {
|
|
19
|
+
Root,
|
|
20
|
+
Header,
|
|
21
|
+
Title,
|
|
22
|
+
Actions,
|
|
23
|
+
CopyButton,
|
|
24
|
+
Content,
|
|
25
|
+
Line,
|
|
26
|
+
Footer,
|
|
27
|
+
codeBlockVariants,
|
|
28
|
+
codeBlockLineVariants,
|
|
29
|
+
type CodeBlockVariant,
|
|
30
|
+
type CodeBlockSize,
|
|
31
|
+
type CodeBlockLineVariant,
|
|
32
|
+
type CodeBlockLineToken,
|
|
33
|
+
//
|
|
34
|
+
Root as CodeBlock,
|
|
35
|
+
Header as CodeBlockHeader,
|
|
36
|
+
Title as CodeBlockTitle,
|
|
37
|
+
Actions as CodeBlockActions,
|
|
38
|
+
CopyButton as CodeBlockCopyButton,
|
|
39
|
+
Content as CodeBlockContent,
|
|
40
|
+
Line as CodeBlockLine,
|
|
41
|
+
Footer as CodeBlockFooter,
|
|
42
|
+
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createHighlighterCore,
|
|
3
|
+
type GrammarState,
|
|
4
|
+
type HighlighterCore,
|
|
5
|
+
type ThemedToken,
|
|
6
|
+
} from "shiki/core";
|
|
7
|
+
import { createJavaScriptRegexEngine } from "shiki/engine/javascript";
|
|
8
|
+
|
|
9
|
+
// Add the languages you need. Each one loads the first time it is used.
|
|
10
|
+
const languages = {
|
|
11
|
+
bash: () => import("shiki/langs/shellscript.mjs"),
|
|
12
|
+
css: () => import("shiki/langs/css.mjs"),
|
|
13
|
+
diff: () => import("shiki/langs/diff.mjs"),
|
|
14
|
+
html: () => import("shiki/langs/html.mjs"),
|
|
15
|
+
javascript: () => import("shiki/langs/javascript.mjs"),
|
|
16
|
+
json: () => import("shiki/langs/json.mjs"),
|
|
17
|
+
jsx: () => import("shiki/langs/jsx.mjs"),
|
|
18
|
+
markdown: () => import("shiki/langs/markdown.mjs"),
|
|
19
|
+
python: () => import("shiki/langs/python.mjs"),
|
|
20
|
+
sql: () => import("shiki/langs/sql.mjs"),
|
|
21
|
+
svelte: () => import("shiki/langs/svelte.mjs"),
|
|
22
|
+
tsx: () => import("shiki/langs/tsx.mjs"),
|
|
23
|
+
typescript: () => import("shiki/langs/typescript.mjs"),
|
|
24
|
+
yaml: () => import("shiki/langs/yaml.mjs"),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const aliases: Record<string, keyof typeof languages> = {
|
|
28
|
+
js: "javascript",
|
|
29
|
+
md: "markdown",
|
|
30
|
+
py: "python",
|
|
31
|
+
sh: "bash",
|
|
32
|
+
shell: "bash",
|
|
33
|
+
shellscript: "bash",
|
|
34
|
+
ts: "typescript",
|
|
35
|
+
yml: "yaml",
|
|
36
|
+
zsh: "bash",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
let highlighter: Promise<HighlighterCore> | undefined;
|
|
40
|
+
|
|
41
|
+
function getHighlighter() {
|
|
42
|
+
highlighter ??= createHighlighterCore({
|
|
43
|
+
themes: [import("shiki/themes/github-light.mjs"), import("shiki/themes/github-dark.mjs")],
|
|
44
|
+
langs: [],
|
|
45
|
+
engine: createJavaScriptRegexEngine({ forgiving: true }),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return highlighter;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getLanguage(language: string) {
|
|
52
|
+
const name = language.toLowerCase();
|
|
53
|
+
|
|
54
|
+
if (name === "ansi") {
|
|
55
|
+
return name;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (name in languages) {
|
|
59
|
+
return name as keyof typeof languages;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return aliases[name] ?? null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// One highlighted token. Render it as a `<span>` with this style and class.
|
|
66
|
+
export type HighlightedToken = {
|
|
67
|
+
content: string;
|
|
68
|
+
style?: string;
|
|
69
|
+
class?: string;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// A line of plain text, or the highlighted tokens of a line.
|
|
73
|
+
export type HighlightedLine = string | HighlightedToken[];
|
|
74
|
+
|
|
75
|
+
function toHighlightedLine(tokens: ThemedToken[]): HighlightedToken[] {
|
|
76
|
+
return tokens.map((token) => ({
|
|
77
|
+
content: token.content,
|
|
78
|
+
style: Object.entries(token.htmlStyle ?? {})
|
|
79
|
+
.map(([property, value]) => `${property}:${value}`)
|
|
80
|
+
.join(";"),
|
|
81
|
+
class: "text-(--shiki-light) dark:text-(--shiki-dark)",
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export type Highlighter = NonNullable<Awaited<ReturnType<typeof loadHighlighter>>>;
|
|
86
|
+
|
|
87
|
+
// Loads a language and returns a highlighter for it, or null for a language
|
|
88
|
+
// that is not listed.
|
|
89
|
+
export async function loadHighlighter(language: string) {
|
|
90
|
+
const lang = getLanguage(language);
|
|
91
|
+
|
|
92
|
+
if (!lang) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const shiki = await getHighlighter();
|
|
97
|
+
|
|
98
|
+
if (lang !== "ansi" && !shiki.getLoadedLanguages().includes(lang)) {
|
|
99
|
+
await shiki.loadLanguage(languages[lang]);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
// Returns the tokens of each line, and the grammar state after the last
|
|
104
|
+
// line. Pass that state back to continue where the code left off.
|
|
105
|
+
highlight(code: string, state?: GrammarState) {
|
|
106
|
+
const tokenize = (text: string) =>
|
|
107
|
+
shiki.codeToTokens(text, {
|
|
108
|
+
lang,
|
|
109
|
+
themes: { light: "github-light", dark: "github-dark" },
|
|
110
|
+
defaultColor: false,
|
|
111
|
+
grammarState: state,
|
|
112
|
+
}).tokens;
|
|
113
|
+
|
|
114
|
+
// ANSI has no grammar state, so each line is tokenized on its own and
|
|
115
|
+
// gets the same colors whether it arrives in one piece or streams.
|
|
116
|
+
const tokens =
|
|
117
|
+
lang === "ansi" ? code.split("\n").flatMap((line) => tokenize(line)) : tokenize(code);
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
lines: tokens.map((line) => toHighlightedLine(line)),
|
|
121
|
+
state: shiki.getLastGrammarState(tokens),
|
|
122
|
+
};
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Returns the tokens of each line, or null for a language that is not listed.
|
|
128
|
+
export async function highlightCode(code: string, language: string) {
|
|
129
|
+
const highlighter = await loadHighlighter(language);
|
|
130
|
+
|
|
131
|
+
return highlighter ? highlighter.highlight(code).lines : null;
|
|
132
|
+
}
|