@joewinke/jatui 0.1.33 → 0.2.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/CLAUDE.md +22 -0
- package/dist/actions/attention.d.ts +24 -0
- package/dist/actions/attention.js +94 -0
- package/dist/actions/floating.d.ts +53 -0
- package/dist/actions/floating.js +158 -0
- package/dist/actions/staggerEntrance.d.ts +28 -0
- package/dist/actions/staggerEntrance.js +71 -0
- package/dist/actions/tooltip.d.ts +28 -0
- package/dist/actions/tooltip.js +186 -0
- package/dist/assets/sounds/README.md +47 -0
- package/dist/assets/sounds/error.d.ts +1 -0
- package/dist/assets/sounds/error.js +4 -0
- package/dist/assets/sounds/notify.d.ts +1 -0
- package/dist/assets/sounds/notify.js +4 -0
- package/dist/assets/sounds/pop.d.ts +1 -0
- package/dist/assets/sounds/pop.js +4 -0
- package/dist/assets/sounds/send.d.ts +1 -0
- package/dist/assets/sounds/send.js +4 -0
- package/dist/assets/sounds/success.d.ts +1 -0
- package/dist/assets/sounds/success.js +4 -0
- package/dist/assets/sounds/toggle.d.ts +1 -0
- package/dist/assets/sounds/toggle.js +4 -0
- package/dist/components/AnimatedNumber.svelte +112 -0
- package/dist/components/AnimatedNumber.svelte.d.ts +8 -0
- package/dist/components/Badge.svelte +75 -4
- package/dist/components/Badge.svelte.d.ts +6 -1
- package/dist/components/Button.svelte +196 -21
- package/dist/components/Button.svelte.d.ts +10 -1
- package/dist/components/Carousel.svelte +265 -0
- package/dist/components/Carousel.svelte.d.ts +21 -0
- package/dist/components/ChipInput.svelte +63 -3
- package/dist/components/Collapse.svelte +70 -14
- package/dist/components/Collapse.svelte.d.ts +11 -0
- package/dist/components/ColorSelector.svelte +21 -24
- package/dist/components/CommandPalette.svelte +475 -0
- package/dist/components/CommandPalette.svelte.d.ts +3 -0
- package/dist/components/ContextMenu.svelte +9 -73
- package/dist/components/CopyButton.svelte +156 -0
- package/dist/components/CopyButton.svelte.d.ts +12 -0
- package/dist/components/HoldToConfirm.svelte +183 -0
- package/dist/components/HoldToConfirm.svelte.d.ts +11 -0
- package/dist/components/KeyboardShortcutsOverlay.svelte +54 -8
- package/dist/components/KeyboardShortcutsOverlay.svelte.d.ts +3 -0
- package/dist/components/PasswordMeter.svelte +87 -0
- package/dist/components/PasswordMeter.svelte.d.ts +8 -0
- package/dist/components/PinInput.svelte +186 -0
- package/dist/components/PinInput.svelte.d.ts +13 -0
- package/dist/components/QuantityStepper.svelte +185 -0
- package/dist/components/QuantityStepper.svelte.d.ts +12 -0
- package/dist/components/Skeleton.svelte +123 -0
- package/dist/components/Skeleton.svelte.d.ts +12 -0
- package/dist/components/SpeechForm.svelte +37 -0
- package/dist/components/StatusBadge.svelte +31 -3
- package/dist/components/StatusBadge.svelte.d.ts +0 -5
- package/dist/components/Tabs.svelte +270 -0
- package/dist/components/Tabs.svelte.d.ts +15 -0
- package/dist/components/TextInput.svelte +138 -38
- package/dist/components/TextInput.svelte.d.ts +9 -1
- package/dist/components/VoicePlayer.svelte +51 -0
- package/dist/components/basic/Alert.svelte +36 -0
- package/dist/components/basic/Alert.svelte.d.ts +9 -0
- package/dist/components/basic/Breadcrumb.svelte +27 -0
- package/dist/components/basic/Breadcrumb.svelte.d.ts +11 -0
- package/dist/components/basic/ButtonGroup.svelte +15 -0
- package/dist/components/basic/ButtonGroup.svelte.d.ts +8 -0
- package/dist/components/basic/Checkbox.svelte +85 -0
- package/dist/components/basic/Checkbox.svelte.d.ts +15 -0
- package/dist/components/basic/Empty.svelte +17 -0
- package/dist/components/basic/Empty.svelte.d.ts +8 -0
- package/dist/components/basic/Kbd.svelte +16 -0
- package/dist/components/basic/Kbd.svelte.d.ts +8 -0
- package/dist/components/basic/Pagination.svelte +104 -0
- package/dist/components/basic/Pagination.svelte.d.ts +12 -0
- package/dist/components/basic/RadioGroup.svelte +71 -0
- package/dist/components/basic/RadioGroup.svelte.d.ts +18 -0
- package/dist/components/basic/Separator.svelte +19 -0
- package/dist/components/basic/Separator.svelte.d.ts +8 -0
- package/dist/components/basic/Slider.svelte +72 -0
- package/dist/components/basic/Slider.svelte.d.ts +17 -0
- package/dist/components/basic/Switch.svelte +88 -0
- package/dist/components/basic/Switch.svelte.d.ts +14 -0
- package/dist/components/basic/Toggle.svelte +43 -0
- package/dist/components/basic/Toggle.svelte.d.ts +14 -0
- package/dist/components/graph/GraphCanvas.svelte +103 -0
- package/dist/components/graph/GraphCanvas.svelte.d.ts +22 -0
- package/dist/components/graph/JatuiNode.svelte +128 -0
- package/dist/components/graph/JatuiNode.svelte.d.ts +14 -0
- package/dist/components/graph/theme.d.ts +27 -0
- package/dist/components/graph/theme.js +136 -0
- package/dist/components/messaging/MessageItem.svelte +92 -0
- package/dist/components/messaging/MessageItem.svelte.d.ts +5 -0
- package/dist/components/messaging/TypingIndicator.svelte +63 -0
- package/dist/components/messaging/TypingIndicator.svelte.d.ts +7 -0
- package/dist/components/pipeline/PipelineColumn.svelte +44 -3
- package/dist/components/progress/ProgressBar.svelte +189 -0
- package/dist/components/progress/ProgressBar.svelte.d.ts +17 -0
- package/dist/components/progress/ProgressRing.svelte +138 -0
- package/dist/components/progress/ProgressRing.svelte.d.ts +12 -0
- package/dist/components/progress/Steps.svelte +195 -0
- package/dist/components/progress/Steps.svelte.d.ts +14 -0
- package/dist/components/shell/NavigationMenu.svelte +422 -0
- package/dist/components/shell/NavigationMenu.svelte.d.ts +43 -0
- package/dist/components/shell/Sidebar.svelte +276 -0
- package/dist/components/shell/Sidebar.svelte.d.ts +32 -0
- package/dist/components/shell/SidebarGroup.svelte +180 -0
- package/dist/components/shell/SidebarGroup.svelte.d.ts +19 -0
- package/dist/components/shell/SidebarItem.svelte +170 -0
- package/dist/components/shell/SidebarItem.svelte.d.ts +24 -0
- package/dist/components/shell/shell.d.ts +30 -0
- package/dist/components/shell/shell.js +65 -0
- package/dist/components/table/DataTable.svelte +364 -0
- package/dist/components/table/DataTable.svelte.d.ts +4 -0
- package/dist/components/table/flexRender.d.ts +31 -0
- package/dist/components/table/flexRender.js +32 -0
- package/dist/components/table/types.d.ts +28 -0
- package/dist/components/table/types.js +1 -0
- package/dist/components/toast/ToastHost.svelte +21 -0
- package/dist/components/toast/ToastHost.svelte.d.ts +3 -0
- package/dist/components/toast/ToastItem.svelte +287 -0
- package/dist/components/toast/ToastItem.svelte.d.ts +9 -0
- package/dist/index.d.ts +59 -0
- package/dist/index.js +67 -0
- package/dist/showcase/AnimatedBeam.svelte +183 -0
- package/dist/showcase/AnimatedBeam.svelte.d.ts +26 -0
- package/dist/showcase/AuroraDrift.svelte +74 -0
- package/dist/showcase/AuroraDrift.svelte.d.ts +9 -0
- package/dist/showcase/BentoCard.svelte +50 -0
- package/dist/showcase/BentoCard.svelte.d.ts +14 -0
- package/dist/showcase/BentoGrid.svelte +43 -0
- package/dist/showcase/BentoGrid.svelte.d.ts +12 -0
- package/dist/showcase/BorderBeam.svelte +79 -0
- package/dist/showcase/BorderBeam.svelte.d.ts +12 -0
- package/dist/showcase/ClipWipe.svelte +82 -0
- package/dist/showcase/ClipWipe.svelte.d.ts +12 -0
- package/dist/showcase/Confetti.svelte +71 -0
- package/dist/showcase/Confetti.svelte.d.ts +10 -0
- package/dist/showcase/CursorSpotlight.svelte +85 -0
- package/dist/showcase/CursorSpotlight.svelte.d.ts +9 -0
- package/dist/showcase/DeviceMock.svelte +186 -0
- package/dist/showcase/DeviceMock.svelte.d.ts +11 -0
- package/dist/showcase/Dock.svelte +123 -0
- package/dist/showcase/Dock.svelte.d.ts +17 -0
- package/dist/showcase/FlipCard.svelte +103 -0
- package/dist/showcase/FlipCard.svelte.d.ts +10 -0
- package/dist/showcase/FocusReveal.svelte +83 -0
- package/dist/showcase/FocusReveal.svelte.d.ts +11 -0
- package/dist/showcase/Globe.svelte +202 -0
- package/dist/showcase/Globe.svelte.d.ts +16 -0
- package/dist/showcase/KenBurns.svelte +105 -0
- package/dist/showcase/KenBurns.svelte.d.ts +13 -0
- package/dist/showcase/Marquee.svelte +89 -0
- package/dist/showcase/Marquee.svelte.d.ts +9 -0
- package/dist/showcase/ParallaxTilt.svelte +98 -0
- package/dist/showcase/ParallaxTilt.svelte.d.ts +10 -0
- package/dist/showcase/ScrambleText.svelte +98 -0
- package/dist/showcase/ScrambleText.svelte.d.ts +11 -0
- package/dist/showcase/ShineSweep.svelte +81 -0
- package/dist/showcase/ShineSweep.svelte.d.ts +9 -0
- package/dist/showcase/Terminal.svelte +230 -0
- package/dist/showcase/Terminal.svelte.d.ts +18 -0
- package/dist/showcase/TextSplitReveal.svelte +146 -0
- package/dist/showcase/TextSplitReveal.svelte.d.ts +13 -0
- package/dist/showcase/TextWave.svelte +55 -0
- package/dist/showcase/TextWave.svelte.d.ts +8 -0
- package/dist/showcase/TypewriterText.svelte +130 -0
- package/dist/showcase/TypewriterText.svelte.d.ts +9 -0
- package/dist/showcase/backdrop/GrainGradient.svelte +126 -0
- package/dist/showcase/backdrop/GrainGradient.svelte.d.ts +23 -0
- package/dist/showcase/backdrop/MeshGradient.svelte +104 -0
- package/dist/showcase/backdrop/MeshGradient.svelte.d.ts +20 -0
- package/dist/showcase/backdrop/Metaballs.svelte +116 -0
- package/dist/showcase/backdrop/Metaballs.svelte.d.ts +18 -0
- package/dist/showcase/backdrop/NeuroNoise.svelte +115 -0
- package/dist/showcase/backdrop/NeuroNoise.svelte.d.ts +20 -0
- package/dist/showcase/backdrop/ShowcaseBackdrop.svelte +48 -0
- package/dist/showcase/backdrop/ShowcaseBackdrop.svelte.d.ts +12 -0
- package/dist/showcase/generative/GenerativeBackdrop.svelte +234 -0
- package/dist/showcase/generative/GenerativeBackdrop.svelte.d.ts +9 -0
- package/dist/showcase/generative/intent.d.ts +7 -0
- package/dist/showcase/generative/intent.js +67 -0
- package/dist/showcase/generative/manifest.d.ts +9 -0
- package/dist/showcase/generative/manifest.js +97 -0
- package/dist/showcase/generative/types.d.ts +48 -0
- package/dist/showcase/generative/types.js +8 -0
- package/dist/showcase/index.d.ts +36 -0
- package/dist/showcase/index.js +46 -0
- package/dist/showcase/scroll/SmoothScroll.svelte +49 -0
- package/dist/showcase/scroll/SmoothScroll.svelte.d.ts +12 -0
- package/dist/showcase/scroll/scrollProgress.d.ts +24 -0
- package/dist/showcase/scroll/scrollProgress.js +65 -0
- package/dist/showcase/scroll/scrollReveal.d.ts +30 -0
- package/dist/showcase/scroll/scrollReveal.js +70 -0
- package/dist/stores/commandPalette.svelte.d.ts +24 -0
- package/dist/stores/commandPalette.svelte.js +82 -0
- package/dist/stores/toast.svelte.d.ts +28 -0
- package/dist/stores/toast.svelte.js +33 -0
- package/dist/styles/motion.css +46 -0
- package/dist/styles/tokens.css +31 -0
- package/dist/transitions/glide.d.ts +31 -0
- package/dist/transitions/glide.js +58 -0
- package/dist/utils/sound.d.ts +6 -0
- package/dist/utils/sound.js +113 -0
- package/dist/utils/splitText.d.ts +25 -0
- package/dist/utils/splitText.js +94 -0
- package/dist/utils/themeWipe.d.ts +21 -0
- package/dist/utils/themeWipe.js +94 -0
- package/dist/utils/viewTransition.d.ts +23 -0
- package/dist/utils/viewTransition.js +75 -0
- package/package.json +50 -3
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import '@xyflow/svelte/dist/style.css'
|
|
3
|
+
|
|
4
|
+
import { SvelteFlow, Background, Controls, MiniMap, type Node, type Edge } from '@xyflow/svelte'
|
|
5
|
+
import JatuiNode from './JatuiNode.svelte'
|
|
6
|
+
import { watchAndApplyTheme } from './theme.js'
|
|
7
|
+
|
|
8
|
+
import type { NodeEventWithPointer } from '@xyflow/svelte'
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
/** Flow nodes — bindable for interactive graphs. */
|
|
12
|
+
nodes: Node[]
|
|
13
|
+
/** Flow edges — bindable for interactive graphs. */
|
|
14
|
+
edges: Edge[]
|
|
15
|
+
/** When true, disables pan/zoom/selection interactions. */
|
|
16
|
+
readonly?: boolean
|
|
17
|
+
/** Fit the graph to the viewport on first render. */
|
|
18
|
+
fitView?: boolean
|
|
19
|
+
/** Show the minimap panel. */
|
|
20
|
+
minimap?: boolean
|
|
21
|
+
/** Fired when the user clicks a node. */
|
|
22
|
+
onNodeClick?: NodeEventWithPointer<MouseEvent | TouchEvent>
|
|
23
|
+
/** Height of the canvas container (CSS value). Defaults to 100%. */
|
|
24
|
+
height?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let {
|
|
28
|
+
nodes = $bindable([]),
|
|
29
|
+
edges = $bindable([]),
|
|
30
|
+
readonly = false,
|
|
31
|
+
fitView = true,
|
|
32
|
+
minimap = false,
|
|
33
|
+
onNodeClick,
|
|
34
|
+
height = '100%',
|
|
35
|
+
}: Props = $props()
|
|
36
|
+
|
|
37
|
+
const nodeTypes = { jatuiNode: JatuiNode }
|
|
38
|
+
|
|
39
|
+
let flowEl: HTMLDivElement | null = $state(null)
|
|
40
|
+
let stopWatch: (() => void) | null = null
|
|
41
|
+
|
|
42
|
+
$effect(() => {
|
|
43
|
+
if (!flowEl) return
|
|
44
|
+
stopWatch?.()
|
|
45
|
+
stopWatch = watchAndApplyTheme(flowEl)
|
|
46
|
+
return () => {
|
|
47
|
+
stopWatch?.()
|
|
48
|
+
stopWatch = null
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<div class="graph-canvas" style:height bind:this={flowEl}>
|
|
54
|
+
<SvelteFlow
|
|
55
|
+
{nodes}
|
|
56
|
+
{edges}
|
|
57
|
+
{nodeTypes}
|
|
58
|
+
{fitView}
|
|
59
|
+
nodesDraggable={!readonly}
|
|
60
|
+
nodesConnectable={!readonly}
|
|
61
|
+
elementsSelectable={!readonly}
|
|
62
|
+
panOnDrag={!readonly}
|
|
63
|
+
zoomOnScroll={!readonly}
|
|
64
|
+
onnodeclick={onNodeClick as NodeEventWithPointer<MouseEvent | TouchEvent> | undefined}
|
|
65
|
+
fitViewOptions={{ padding: 0.2 }}
|
|
66
|
+
attributionPosition="bottom-right"
|
|
67
|
+
>
|
|
68
|
+
<Background gap={16} size={1} />
|
|
69
|
+
<Controls showLock={false} />
|
|
70
|
+
{#if minimap}
|
|
71
|
+
<MiniMap />
|
|
72
|
+
{/if}
|
|
73
|
+
</SvelteFlow>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<style>
|
|
77
|
+
.graph-canvas {
|
|
78
|
+
width: 100%;
|
|
79
|
+
position: relative;
|
|
80
|
+
/* Contain the xyflow absolute-positioned layers */
|
|
81
|
+
overflow: hidden;
|
|
82
|
+
border-radius: var(--jatui-radius-box);
|
|
83
|
+
border: var(--jatui-border-w) solid var(--color-base-300, oklch(0.9 0 0));
|
|
84
|
+
background: var(--color-base-100, oklch(0.98 0 0));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* Strip xyflow's built-in node box-shadow (house rule: no at-rest shadows) */
|
|
88
|
+
:global(.svelte-flow .svelte-flow__node) {
|
|
89
|
+
box-shadow: none !important;
|
|
90
|
+
filter: none !important;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* Edge animation uses opacity+stroke transitions, not layout — allowed */
|
|
94
|
+
:global(.svelte-flow .svelte-flow__edge path) {
|
|
95
|
+
transition: stroke var(--motion-fast) var(--motion-glide);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@media (prefers-reduced-motion: reduce) {
|
|
99
|
+
:global(.svelte-flow .svelte-flow__edge path) {
|
|
100
|
+
transition: none;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
</style>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import '@xyflow/svelte/dist/style.css';
|
|
2
|
+
import { type Node, type Edge } from '@xyflow/svelte';
|
|
3
|
+
import type { NodeEventWithPointer } from '@xyflow/svelte';
|
|
4
|
+
interface Props {
|
|
5
|
+
/** Flow nodes — bindable for interactive graphs. */
|
|
6
|
+
nodes: Node[];
|
|
7
|
+
/** Flow edges — bindable for interactive graphs. */
|
|
8
|
+
edges: Edge[];
|
|
9
|
+
/** When true, disables pan/zoom/selection interactions. */
|
|
10
|
+
readonly?: boolean;
|
|
11
|
+
/** Fit the graph to the viewport on first render. */
|
|
12
|
+
fitView?: boolean;
|
|
13
|
+
/** Show the minimap panel. */
|
|
14
|
+
minimap?: boolean;
|
|
15
|
+
/** Fired when the user clicks a node. */
|
|
16
|
+
onNodeClick?: NodeEventWithPointer<MouseEvent | TouchEvent>;
|
|
17
|
+
/** Height of the canvas container (CSS value). Defaults to 100%. */
|
|
18
|
+
height?: string;
|
|
19
|
+
}
|
|
20
|
+
declare const GraphCanvas: import("svelte").Component<Props, {}, "nodes" | "edges">;
|
|
21
|
+
type GraphCanvas = ReturnType<typeof GraphCanvas>;
|
|
22
|
+
export default GraphCanvas;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Handle, Position } from '@xyflow/svelte'
|
|
3
|
+
import type { NodeProps } from '@xyflow/svelte'
|
|
4
|
+
import type { NodeStatus } from './theme.js'
|
|
5
|
+
|
|
6
|
+
interface JatuiNodeData {
|
|
7
|
+
label?: string
|
|
8
|
+
status?: NodeStatus
|
|
9
|
+
/** Extra text below the label — e.g. type or description */
|
|
10
|
+
sublabel?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type Props = NodeProps & { data: JatuiNodeData }
|
|
14
|
+
|
|
15
|
+
let { data, selected = false, sourcePosition = Position.Bottom, targetPosition = Position.Top }: Props = $props()
|
|
16
|
+
|
|
17
|
+
const STATUS_TINT: Record<NodeStatus, string> = {
|
|
18
|
+
default: '',
|
|
19
|
+
primary: 'bg-primary/10 border-primary text-primary',
|
|
20
|
+
secondary: 'bg-secondary/10 border-secondary text-secondary',
|
|
21
|
+
info: 'bg-info/10 border-info text-info',
|
|
22
|
+
success: 'bg-success/10 border-success text-success',
|
|
23
|
+
warning: 'bg-warning/10 border-warning text-warning',
|
|
24
|
+
error: 'bg-error/10 border-error text-error',
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const STATUS_DOT: Record<NodeStatus, string> = {
|
|
28
|
+
default: '',
|
|
29
|
+
primary: 'bg-primary',
|
|
30
|
+
secondary: 'bg-secondary',
|
|
31
|
+
info: 'bg-info',
|
|
32
|
+
success: 'bg-success',
|
|
33
|
+
warning: 'bg-warning',
|
|
34
|
+
error: 'bg-error',
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const status = $derived(data?.status ?? 'default')
|
|
38
|
+
const tintClass = $derived(STATUS_TINT[status])
|
|
39
|
+
const dotClass = $derived(STATUS_DOT[status])
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<Handle type="target" position={targetPosition} />
|
|
43
|
+
|
|
44
|
+
<div
|
|
45
|
+
class="jatui-node"
|
|
46
|
+
class:jatui-node--selected={selected}
|
|
47
|
+
class:jatui-node--tinted={status !== 'default'}
|
|
48
|
+
>
|
|
49
|
+
<div class="jatui-node__inner {tintClass}">
|
|
50
|
+
{#if status !== 'default'}
|
|
51
|
+
<span class="jatui-node__dot {dotClass}" aria-hidden="true"></span>
|
|
52
|
+
{/if}
|
|
53
|
+
<span class="jatui-node__label">{data?.label ?? ''}</span>
|
|
54
|
+
{#if data?.sublabel}
|
|
55
|
+
<span class="jatui-node__sublabel">{data.sublabel}</span>
|
|
56
|
+
{/if}
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<Handle type="source" position={sourcePosition} />
|
|
61
|
+
|
|
62
|
+
<style>
|
|
63
|
+
.jatui-node {
|
|
64
|
+
min-width: 120px;
|
|
65
|
+
max-width: 240px;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.jatui-node__inner {
|
|
69
|
+
display: flex;
|
|
70
|
+
align-items: center;
|
|
71
|
+
gap: var(--jatui-space-sm);
|
|
72
|
+
padding: var(--jatui-space-base) var(--jatui-space-lg);
|
|
73
|
+
background: var(--color-base-200, oklch(0.96 0 0));
|
|
74
|
+
border: var(--jatui-border-w) solid var(--color-base-300, oklch(0.9 0 0));
|
|
75
|
+
border-radius: var(--jatui-radius-box);
|
|
76
|
+
transition:
|
|
77
|
+
border-color var(--motion-fast) var(--motion-glide),
|
|
78
|
+
background var(--motion-fast) var(--motion-glide);
|
|
79
|
+
cursor: default;
|
|
80
|
+
user-select: none;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* Tinted nodes: bg-X/10 and border-X classes applied via class binding */
|
|
84
|
+
.jatui-node--tinted .jatui-node__inner {
|
|
85
|
+
/* colors come from DaisyUI utility classes in the markup */
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* Selected ring — uses primary without shadows (house rule) */
|
|
89
|
+
.jatui-node--selected .jatui-node__inner {
|
|
90
|
+
border-color: var(--color-primary, oklch(0.6 0.2 264));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.jatui-node__dot {
|
|
94
|
+
flex-shrink: 0;
|
|
95
|
+
width: 6px;
|
|
96
|
+
height: 6px;
|
|
97
|
+
border-radius: 50%;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.jatui-node__label {
|
|
101
|
+
font-size: 0.8125rem; /* 13px */
|
|
102
|
+
font-weight: 500;
|
|
103
|
+
color: var(--color-base-content, currentColor);
|
|
104
|
+
line-height: 1.3;
|
|
105
|
+
word-break: break-word;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/* Tinted label inherits color from the tint utility class text-* */
|
|
109
|
+
.jatui-node--tinted .jatui-node__inner .jatui-node__label {
|
|
110
|
+
color: inherit;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.jatui-node__sublabel {
|
|
114
|
+
display: block;
|
|
115
|
+
font-size: 0.6875rem; /* 11px */
|
|
116
|
+
color: var(--color-base-content, currentColor);
|
|
117
|
+
opacity: 0.55;
|
|
118
|
+
line-height: 1.3;
|
|
119
|
+
margin-top: 2px;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/* Reduced motion: strip transitions */
|
|
123
|
+
@media (prefers-reduced-motion: reduce) {
|
|
124
|
+
.jatui-node__inner {
|
|
125
|
+
transition: none;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
</style>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { NodeProps } from '@xyflow/svelte';
|
|
2
|
+
import type { NodeStatus } from './theme.js';
|
|
3
|
+
interface JatuiNodeData {
|
|
4
|
+
label?: string;
|
|
5
|
+
status?: NodeStatus;
|
|
6
|
+
/** Extra text below the label — e.g. type or description */
|
|
7
|
+
sublabel?: string;
|
|
8
|
+
}
|
|
9
|
+
type Props = NodeProps & {
|
|
10
|
+
data: JatuiNodeData;
|
|
11
|
+
};
|
|
12
|
+
declare const JatuiNode: import("svelte").Component<Props, {}, "">;
|
|
13
|
+
type JatuiNode = ReturnType<typeof JatuiNode>;
|
|
14
|
+
export default JatuiNode;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GraphCanvas theme bridge — maps DaisyUI --color-* custom properties onto
|
|
3
|
+
* xyflow CSS vars so the graph follows the active DaisyUI theme at runtime.
|
|
4
|
+
*
|
|
5
|
+
* Call applyXyflowTheme(el) after mount and whenever the theme changes.
|
|
6
|
+
* el should be the .svelte-flow wrapper element.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Status colors available for JatuiNode status-tinting.
|
|
10
|
+
* Returns a map of status → resolved CSS color string.
|
|
11
|
+
*/
|
|
12
|
+
export type NodeStatus = 'default' | 'info' | 'success' | 'warning' | 'error' | 'primary' | 'secondary';
|
|
13
|
+
export declare function getStatusColor(status: NodeStatus): {
|
|
14
|
+
bg: string;
|
|
15
|
+
border: string;
|
|
16
|
+
text: string;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Apply DaisyUI-derived xyflow CSS vars to the flow container element.
|
|
20
|
+
* Safe to call multiple times — just re-sets the vars.
|
|
21
|
+
*/
|
|
22
|
+
export declare function applyXyflowTheme(el: HTMLElement): void;
|
|
23
|
+
/**
|
|
24
|
+
* Setup watcher: re-applies theme whenever DaisyUI theme changes.
|
|
25
|
+
* Returns cleanup fn.
|
|
26
|
+
*/
|
|
27
|
+
export declare function watchAndApplyTheme(el: HTMLElement): () => void;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GraphCanvas theme bridge — maps DaisyUI --color-* custom properties onto
|
|
3
|
+
* xyflow CSS vars so the graph follows the active DaisyUI theme at runtime.
|
|
4
|
+
*
|
|
5
|
+
* Call applyXyflowTheme(el) after mount and whenever the theme changes.
|
|
6
|
+
* el should be the .svelte-flow wrapper element.
|
|
7
|
+
*/
|
|
8
|
+
import { getDaisyUIColor, watchThemeChanges } from '../../utils/daisyuiColors.js';
|
|
9
|
+
/** All xyflow CSS vars we override, plus the fallback xyflow var they replace. */
|
|
10
|
+
var THEME_MAP = [
|
|
11
|
+
// Edges
|
|
12
|
+
['--xy-edge-stroke', 'var(--color-base-content, #b1b1b7)'],
|
|
13
|
+
['--xy-edge-stroke-selected', 'var(--color-primary, #555)'],
|
|
14
|
+
['--xy-connectionline-stroke', 'var(--color-base-content, #b1b1b7)'],
|
|
15
|
+
// Background canvas
|
|
16
|
+
['--xy-background-color', 'var(--color-base-100, transparent)'],
|
|
17
|
+
['--xy-background-pattern-dots-color', 'var(--color-base-300, #91919a)'],
|
|
18
|
+
['--xy-background-pattern-lines-color', 'var(--color-base-200, #eee)'],
|
|
19
|
+
['--xy-background-pattern-cross-color', 'var(--color-base-300, #e2e2e2)'],
|
|
20
|
+
// Node defaults
|
|
21
|
+
['--xy-node-color', 'var(--color-base-content, #333)'],
|
|
22
|
+
['--xy-node-border', '1px solid var(--color-base-300, #bbb)'],
|
|
23
|
+
['--xy-node-border-selected', '1px solid var(--color-primary, #555)'],
|
|
24
|
+
['--xy-node-background-color', 'var(--color-base-200, #fff)'],
|
|
25
|
+
// Handle
|
|
26
|
+
['--xy-handle-background-color', 'var(--color-base-content, #333)'],
|
|
27
|
+
['--xy-handle-border-color', 'var(--color-base-100, #fff)'],
|
|
28
|
+
// Minimap
|
|
29
|
+
['--xy-minimap-background-color', 'var(--color-base-100, #fff)'],
|
|
30
|
+
['--xy-minimap-mask-background-color', 'color-mix(in oklch, var(--color-base-200, #f0f0f0) 60%, transparent)'],
|
|
31
|
+
['--xy-minimap-node-background-color', 'var(--color-base-200, #e2e2e2)'],
|
|
32
|
+
// Selection box
|
|
33
|
+
['--xy-selection-background-color', 'color-mix(in oklch, var(--color-primary, #6366f1) 10%, transparent)'],
|
|
34
|
+
['--xy-selection-border', '1px dotted color-mix(in oklch, var(--color-primary, #6366f1) 50%, transparent)'],
|
|
35
|
+
// Attribution
|
|
36
|
+
['--xy-attribution-background-color', 'color-mix(in oklch, var(--color-base-100, #fff) 80%, transparent)'],
|
|
37
|
+
];
|
|
38
|
+
export function getStatusColor(status) {
|
|
39
|
+
switch (status) {
|
|
40
|
+
case 'primary':
|
|
41
|
+
return {
|
|
42
|
+
bg: getDaisyUIColor('primary'),
|
|
43
|
+
border: getDaisyUIColor('primary'),
|
|
44
|
+
text: getDaisyUIColor('primary-content'),
|
|
45
|
+
};
|
|
46
|
+
case 'secondary':
|
|
47
|
+
return {
|
|
48
|
+
bg: getDaisyUIColor('secondary'),
|
|
49
|
+
border: getDaisyUIColor('secondary'),
|
|
50
|
+
text: getDaisyUIColor('secondary-content'),
|
|
51
|
+
};
|
|
52
|
+
case 'info':
|
|
53
|
+
return {
|
|
54
|
+
bg: getDaisyUIColor('info'),
|
|
55
|
+
border: getDaisyUIColor('info'),
|
|
56
|
+
text: getDaisyUIColor('info-content'),
|
|
57
|
+
};
|
|
58
|
+
case 'success':
|
|
59
|
+
return {
|
|
60
|
+
bg: getDaisyUIColor('success'),
|
|
61
|
+
border: getDaisyUIColor('success'),
|
|
62
|
+
text: getDaisyUIColor('success-content'),
|
|
63
|
+
};
|
|
64
|
+
case 'warning':
|
|
65
|
+
return {
|
|
66
|
+
bg: getDaisyUIColor('warning'),
|
|
67
|
+
border: getDaisyUIColor('warning'),
|
|
68
|
+
text: getDaisyUIColor('warning-content'),
|
|
69
|
+
};
|
|
70
|
+
case 'error':
|
|
71
|
+
return {
|
|
72
|
+
bg: getDaisyUIColor('error'),
|
|
73
|
+
border: getDaisyUIColor('error'),
|
|
74
|
+
text: getDaisyUIColor('error-content'),
|
|
75
|
+
};
|
|
76
|
+
default:
|
|
77
|
+
return { bg: '', border: '', text: '' };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Apply DaisyUI-derived xyflow CSS vars to the flow container element.
|
|
82
|
+
* Safe to call multiple times — just re-sets the vars.
|
|
83
|
+
*/
|
|
84
|
+
export function applyXyflowTheme(el) {
|
|
85
|
+
for (var _i = 0, THEME_MAP_1 = THEME_MAP; _i < THEME_MAP_1.length; _i++) {
|
|
86
|
+
var prop = THEME_MAP_1[_i][0];
|
|
87
|
+
// We set the vars as CSS custom properties on the element itself.
|
|
88
|
+
// xyflow reads them from .svelte-flow context, so setting on the wrapper is correct.
|
|
89
|
+
var cssVarName = prop.replace('--xy-', '--xy-'); // identity — keep readable
|
|
90
|
+
// Read the DaisyUI value dynamically then set
|
|
91
|
+
var mappedValue = resolveThemeValue(prop);
|
|
92
|
+
el.style.setProperty(cssVarName, mappedValue);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function resolveThemeValue(prop) {
|
|
96
|
+
var _a, _b;
|
|
97
|
+
// Map the xyflow prop name to the DaisyUI color resolution
|
|
98
|
+
var propToColor = {
|
|
99
|
+
'--xy-edge-stroke': function () { return getDaisyUIColor('base-content'); },
|
|
100
|
+
'--xy-edge-stroke-selected': function () { return getDaisyUIColor('primary'); },
|
|
101
|
+
'--xy-connectionline-stroke': function () { return getDaisyUIColor('base-content'); },
|
|
102
|
+
'--xy-background-color': function () { return getDaisyUIColor('base-100'); },
|
|
103
|
+
'--xy-background-pattern-dots-color': function () { return getDaisyUIColor('base-300'); },
|
|
104
|
+
'--xy-background-pattern-lines-color': function () { return getDaisyUIColor('base-200'); },
|
|
105
|
+
'--xy-background-pattern-cross-color': function () { return getDaisyUIColor('base-300'); },
|
|
106
|
+
'--xy-node-color': function () { return getDaisyUIColor('base-content'); },
|
|
107
|
+
'--xy-node-border': function () { return "1px solid ".concat(getDaisyUIColor('base-300')); },
|
|
108
|
+
'--xy-node-border-selected': function () { return "1px solid ".concat(getDaisyUIColor('primary')); },
|
|
109
|
+
'--xy-node-background-color': function () { return getDaisyUIColor('base-200'); },
|
|
110
|
+
'--xy-handle-background-color': function () { return getDaisyUIColor('base-content'); },
|
|
111
|
+
'--xy-handle-border-color': function () { return getDaisyUIColor('base-100'); },
|
|
112
|
+
'--xy-minimap-background-color': function () { return getDaisyUIColor('base-100'); },
|
|
113
|
+
'--xy-minimap-mask-background-color': function () {
|
|
114
|
+
return "color-mix(in oklch, ".concat(getDaisyUIColor('base-200'), " 60%, transparent)");
|
|
115
|
+
},
|
|
116
|
+
'--xy-minimap-node-background-color': function () { return getDaisyUIColor('base-200'); },
|
|
117
|
+
'--xy-selection-background-color': function () {
|
|
118
|
+
return "color-mix(in oklch, ".concat(getDaisyUIColor('primary'), " 10%, transparent)");
|
|
119
|
+
},
|
|
120
|
+
'--xy-selection-border': function () {
|
|
121
|
+
return "1px dotted color-mix(in oklch, ".concat(getDaisyUIColor('primary'), " 50%, transparent)");
|
|
122
|
+
},
|
|
123
|
+
'--xy-attribution-background-color': function () {
|
|
124
|
+
return "color-mix(in oklch, ".concat(getDaisyUIColor('base-100'), " 80%, transparent)");
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
return (_b = (_a = propToColor[prop]) === null || _a === void 0 ? void 0 : _a.call(propToColor)) !== null && _b !== void 0 ? _b : '';
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Setup watcher: re-applies theme whenever DaisyUI theme changes.
|
|
131
|
+
* Returns cleanup fn.
|
|
132
|
+
*/
|
|
133
|
+
export function watchAndApplyTheme(el) {
|
|
134
|
+
applyXyflowTheme(el);
|
|
135
|
+
return watchThemeChanges(function () { return applyXyflowTheme(el); });
|
|
136
|
+
}
|
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
import MessageAttachment from './MessageAttachment.svelte'
|
|
22
22
|
import EmojiSelector from './EmojiSelector.svelte'
|
|
23
23
|
|
|
24
|
+
import type { Snippet } from 'svelte'
|
|
25
|
+
|
|
24
26
|
interface Props {
|
|
25
27
|
message: Message
|
|
26
28
|
isOwn?: boolean
|
|
@@ -43,6 +45,10 @@
|
|
|
43
45
|
onopenthread?: (data: { messageId: string }) => void
|
|
44
46
|
onquotereply?: (data: { messageId: string }) => void
|
|
45
47
|
onreactionchange?: () => void
|
|
48
|
+
/** Enable swipe-to-reveal on coarse pointers (touch). Defaults to false. */
|
|
49
|
+
swipeActions?: boolean
|
|
50
|
+
/** Snippet rendered in the revealed action zone (right side, behind the item). */
|
|
51
|
+
swipeContent?: Snippet
|
|
46
52
|
}
|
|
47
53
|
|
|
48
54
|
let {
|
|
@@ -64,6 +70,8 @@
|
|
|
64
70
|
onopenthread,
|
|
65
71
|
onquotereply,
|
|
66
72
|
onreactionchange,
|
|
73
|
+
swipeActions = false,
|
|
74
|
+
swipeContent,
|
|
67
75
|
}: Props = $props()
|
|
68
76
|
|
|
69
77
|
let isEditing = $state(false)
|
|
@@ -72,6 +80,66 @@
|
|
|
72
80
|
let showEmojiSelector = $state(false)
|
|
73
81
|
let emojiSelectorPosition = $state({ x: 0, y: 0 })
|
|
74
82
|
|
|
83
|
+
// Swipe-to-reveal state (coarse pointer / touch only)
|
|
84
|
+
const SWIPE_THRESHOLD = 96
|
|
85
|
+
const RUBBER_BAND_LIMIT = 130
|
|
86
|
+
let swipeOpen = $state(false)
|
|
87
|
+
let swipeDragging = $state(false)
|
|
88
|
+
let swipeOffset = $state(0)
|
|
89
|
+
let swipeStartX = 0
|
|
90
|
+
let swipeStartOpen = false
|
|
91
|
+
|
|
92
|
+
function isCoarsePointer(e: PointerEvent) {
|
|
93
|
+
return e.pointerType === 'touch' || e.pointerType === 'pen'
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function swipeRubberBand(raw: number): number {
|
|
97
|
+
if (raw >= 0) return 0
|
|
98
|
+
const abs = Math.abs(raw)
|
|
99
|
+
if (abs <= SWIPE_THRESHOLD) return raw
|
|
100
|
+
const over = abs - SWIPE_THRESHOLD
|
|
101
|
+
const damped = SWIPE_THRESHOLD + over * (1 - over / (RUBBER_BAND_LIMIT * 2))
|
|
102
|
+
return -Math.min(damped, RUBBER_BAND_LIMIT)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function onSwipePointerDown(e: PointerEvent) {
|
|
106
|
+
if (!swipeActions || !swipeContent || !isCoarsePointer(e)) return
|
|
107
|
+
swipeStartX = e.clientX
|
|
108
|
+
swipeStartOpen = swipeOpen
|
|
109
|
+
swipeDragging = true
|
|
110
|
+
;(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function onSwipePointerMove(e: PointerEvent) {
|
|
114
|
+
if (!swipeDragging) return
|
|
115
|
+
const baseOffset = swipeStartOpen ? -SWIPE_THRESHOLD : 0
|
|
116
|
+
const raw = baseOffset + (e.clientX - swipeStartX)
|
|
117
|
+
swipeOffset = swipeRubberBand(raw)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function onSwipePointerUp(e: PointerEvent) {
|
|
121
|
+
if (!swipeDragging) return
|
|
122
|
+
swipeDragging = false
|
|
123
|
+
const baseOffset = swipeStartOpen ? -SWIPE_THRESHOLD : 0
|
|
124
|
+
const delta = baseOffset + (e.clientX - swipeStartX)
|
|
125
|
+
// Velocity hint: fast leftward swipe counts even without crossing threshold
|
|
126
|
+
const velocity = e.clientX - swipeStartX
|
|
127
|
+
if (delta < -SWIPE_THRESHOLD / 2 || velocity < -30) {
|
|
128
|
+
swipeOpen = true
|
|
129
|
+
} else {
|
|
130
|
+
swipeOpen = false
|
|
131
|
+
}
|
|
132
|
+
swipeOffset = 0
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const swipeTranslateX = $derived(
|
|
136
|
+
swipeDragging
|
|
137
|
+
? swipeOffset
|
|
138
|
+
: swipeOpen
|
|
139
|
+
? -SWIPE_THRESHOLD
|
|
140
|
+
: 0
|
|
141
|
+
)
|
|
142
|
+
|
|
75
143
|
function formatTime(dateString: string): string {
|
|
76
144
|
const date = new Date(dateString)
|
|
77
145
|
const now = new Date()
|
|
@@ -182,6 +250,27 @@
|
|
|
182
250
|
}
|
|
183
251
|
</script>
|
|
184
252
|
|
|
253
|
+
<!-- Swipe wrapper: always rendered; overflow-hidden clips the action zone -->
|
|
254
|
+
<div
|
|
255
|
+
class="swipe-wrapper relative overflow-hidden"
|
|
256
|
+
class:rounded={swipeActions && !!swipeContent}
|
|
257
|
+
>
|
|
258
|
+
<!-- Action zone — only present when swipe is enabled -->
|
|
259
|
+
{#if swipeActions && swipeContent}
|
|
260
|
+
<div class="swipe-actions absolute inset-y-0 right-0 flex items-center pr-2 gap-1" aria-hidden={!swipeOpen}>
|
|
261
|
+
{@render swipeContent()}
|
|
262
|
+
</div>
|
|
263
|
+
{/if}
|
|
264
|
+
|
|
265
|
+
<!-- Draggable layer (swipe) or passthrough div -->
|
|
266
|
+
<div
|
|
267
|
+
style={swipeActions && swipeContent ? `transform: translateX(${swipeTranslateX}px); transition: ${swipeDragging ? 'none' : 'transform var(--motion-move) var(--motion-spring)'};` : undefined}
|
|
268
|
+
onpointerdown={swipeActions && swipeContent ? onSwipePointerDown : undefined}
|
|
269
|
+
onpointermove={swipeActions && swipeContent ? onSwipePointerMove : undefined}
|
|
270
|
+
onpointerup={swipeActions && swipeContent ? onSwipePointerUp : undefined}
|
|
271
|
+
onpointercancel={swipeActions && swipeContent ? onSwipePointerUp : undefined}
|
|
272
|
+
>
|
|
273
|
+
|
|
185
274
|
<div
|
|
186
275
|
class="group flex gap-3 px-2 py-1 rounded relative transition-colors w-full text-left cursor-pointer {message.isPinned ? 'bg-warning/5 hover:bg-warning/10' : 'hover:bg-base-200'}"
|
|
187
276
|
class:pt-2={!isConsecutive}
|
|
@@ -328,6 +417,9 @@
|
|
|
328
417
|
</div>
|
|
329
418
|
</div>
|
|
330
419
|
|
|
420
|
+
</div><!-- /draggable layer -->
|
|
421
|
+
</div><!-- /swipe-wrapper -->
|
|
422
|
+
|
|
331
423
|
<!-- Emoji Selector -->
|
|
332
424
|
{#if showEmojiSelector && currentUserId}
|
|
333
425
|
<EmojiSelector
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Message, ReactionSummary, StandardEmoji, CustomEmoji, ReactionCallbacks, AttachmentCallbacks } from '../../types/messaging';
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
2
3
|
interface Props {
|
|
3
4
|
message: Message;
|
|
4
5
|
isOwn?: boolean;
|
|
@@ -35,6 +36,10 @@ interface Props {
|
|
|
35
36
|
messageId: string;
|
|
36
37
|
}) => void;
|
|
37
38
|
onreactionchange?: () => void;
|
|
39
|
+
/** Enable swipe-to-reveal on coarse pointers (touch). Defaults to false. */
|
|
40
|
+
swipeActions?: boolean;
|
|
41
|
+
/** Snippet rendered in the revealed action zone (right side, behind the item). */
|
|
42
|
+
swipeContent?: Snippet;
|
|
38
43
|
}
|
|
39
44
|
declare const MessageItem: import("svelte").Component<Props, {}, "">;
|
|
40
45
|
type MessageItem = ReturnType<typeof MessageItem>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
TypingIndicator — three-dot phase-offset bounce (kinetics: Typing Indicator).
|
|
3
|
+
Infinite loop is sanctioned for functional status indicators.
|
|
4
|
+
Reduced-motion: static dots + label remain visible.
|
|
5
|
+
-->
|
|
6
|
+
|
|
7
|
+
<script lang="ts">
|
|
8
|
+
interface Props {
|
|
9
|
+
users?: string[]
|
|
10
|
+
class?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let { users = [], class: cls = '' }: Props = $props()
|
|
14
|
+
|
|
15
|
+
const label = $derived(
|
|
16
|
+
users.length === 0
|
|
17
|
+
? 'typing…'
|
|
18
|
+
: users.length === 1
|
|
19
|
+
? `${users[0]} is typing…`
|
|
20
|
+
: users.length === 2
|
|
21
|
+
? `${users[0]} and ${users[1]} are typing…`
|
|
22
|
+
: `${users[0]} and ${users.length - 1} others are typing…`
|
|
23
|
+
)
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<div class="typing-indicator inline-flex items-center gap-1.5 {cls}" aria-label={label} role="status">
|
|
27
|
+
<span class="dot" aria-hidden="true"></span>
|
|
28
|
+
<span class="dot" style="animation-delay: 0.16s" aria-hidden="true"></span>
|
|
29
|
+
<span class="dot" style="animation-delay: 0.32s" aria-hidden="true"></span>
|
|
30
|
+
{#if users.length > 0}
|
|
31
|
+
<span class="text-xs text-base-content/60 ml-0.5">{label}</span>
|
|
32
|
+
{/if}
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<style>
|
|
36
|
+
.dot {
|
|
37
|
+
width: 4px;
|
|
38
|
+
height: 4px;
|
|
39
|
+
border-radius: 50%;
|
|
40
|
+
background-color: oklch(from var(--color-base-content, currentColor) l c h / 0.6);
|
|
41
|
+
animation: typing-bounce 1.2s ease-in-out infinite;
|
|
42
|
+
display: inline-block;
|
|
43
|
+
flex-shrink: 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@keyframes typing-bounce {
|
|
47
|
+
0%, 60%, 100% {
|
|
48
|
+
transform: translateY(0);
|
|
49
|
+
opacity: 0.4;
|
|
50
|
+
}
|
|
51
|
+
30% {
|
|
52
|
+
transform: translateY(-6px);
|
|
53
|
+
opacity: 1;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@media (prefers-reduced-motion: reduce) {
|
|
58
|
+
.dot {
|
|
59
|
+
animation: none;
|
|
60
|
+
opacity: 0.6;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
</style>
|