@joewinke/jatui 0.1.11 → 0.1.19

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 (90) hide show
  1. package/README.md +123 -0
  2. package/package.json +2 -1
  3. package/src/lib/actions/railNav.ts +473 -0
  4. package/src/lib/components/AnnotationLayer.svelte +108 -0
  5. package/src/lib/components/AnnotationPanel.svelte +319 -0
  6. package/src/lib/components/AudioWaveform.svelte +9 -5
  7. package/src/lib/components/AvailabilityModal.svelte +7 -3
  8. package/src/lib/components/AvatarUpload.svelte +27 -4
  9. package/src/lib/components/BookingForm.svelte +11 -9
  10. package/src/lib/components/BurndownChart.svelte +778 -0
  11. package/src/lib/components/Button.svelte +10 -1
  12. package/src/lib/components/CalendarPicker.svelte +3 -3
  13. package/src/lib/components/Card.svelte +2 -2
  14. package/src/lib/components/ChipInput.svelte +21 -15
  15. package/src/lib/components/ColorSelector.svelte +17 -13
  16. package/src/lib/components/CommentThread.svelte +773 -0
  17. package/src/lib/components/ConfirmDialog.svelte +348 -0
  18. package/src/lib/components/ConfirmModal.svelte +78 -11
  19. package/src/lib/components/ContextMenu.svelte +59 -19
  20. package/src/lib/components/CountdownTimer.svelte +1 -1
  21. package/src/lib/components/DateRangePicker.svelte +6 -4
  22. package/src/lib/components/Drawer.svelte +36 -3
  23. package/src/lib/components/EntityPreviewCard.svelte +104 -0
  24. package/src/lib/components/FileDropzone.svelte +493 -0
  25. package/src/lib/components/FilePicker.svelte +83 -14
  26. package/src/lib/components/FileThumbnail.svelte +80 -0
  27. package/src/lib/components/FilterDropdown.svelte +11 -11
  28. package/src/lib/components/HunkDiffView.svelte +348 -0
  29. package/src/lib/components/ImageLightbox.svelte +274 -0
  30. package/src/lib/components/ImageUpload.svelte +58 -9
  31. package/src/lib/components/InlineEdit.svelte +15 -9
  32. package/src/lib/components/InputDialog.svelte +327 -0
  33. package/src/lib/components/LazyImage.svelte +1 -0
  34. package/src/lib/components/LinkShortener.svelte +1 -1
  35. package/src/lib/components/LoadingSpinner.svelte +6 -2
  36. package/src/lib/components/MarkupEditor.svelte +485 -0
  37. package/src/lib/components/MarkupOverlay.svelte +55 -0
  38. package/src/lib/components/MediaWorkbench.svelte +871 -0
  39. package/src/lib/components/MilestoneCard.svelte +1 -1
  40. package/src/lib/components/MilestoneTimeline.svelte +1 -1
  41. package/src/lib/components/Modal.svelte +39 -4
  42. package/src/lib/components/PDFViewer.svelte +105 -0
  43. package/src/lib/components/PdfThumbnail.svelte +3 -1
  44. package/src/lib/components/PhoneInput.svelte +1 -1
  45. package/src/lib/components/ResizablePanel.svelte +4 -4
  46. package/src/lib/components/SearchDropdown.svelte +26 -13
  47. package/src/lib/components/SelectInput.svelte +26 -4
  48. package/src/lib/components/SidebarUserFooter.svelte +1 -1
  49. package/src/lib/components/SignaturePad.svelte +8 -4
  50. package/src/lib/components/SmartImageEditor.svelte +720 -0
  51. package/src/lib/components/SortDropdown.svelte +9 -3
  52. package/src/lib/components/Sparkline.svelte +9 -0
  53. package/src/lib/components/StatusBadge.svelte +20 -18
  54. package/src/lib/components/TextArea.svelte +24 -5
  55. package/src/lib/components/TextInput.svelte +29 -6
  56. package/src/lib/components/ThemeSelector.svelte +15 -4
  57. package/src/lib/components/TimeSlotPicker.svelte +7 -7
  58. package/src/lib/components/UserAvatar.svelte +14 -1
  59. package/src/lib/components/VariablePicker.svelte +170 -0
  60. package/src/lib/components/VoicePlayer.svelte +4 -3
  61. package/src/lib/components/markup.ts +287 -0
  62. package/src/lib/components/messaging/ChannelInfoModal.svelte +9 -9
  63. package/src/lib/components/messaging/ChannelList.svelte +1 -1
  64. package/src/lib/components/messaging/ChannelMembersModal.svelte +1 -1
  65. package/src/lib/components/messaging/CreateChannelModal.svelte +1 -1
  66. package/src/lib/components/messaging/DirectMessageList.svelte +1 -1
  67. package/src/lib/components/messaging/EmojiSelector.svelte +2 -1
  68. package/src/lib/components/messaging/MentionAutocomplete.svelte +1 -1
  69. package/src/lib/components/messaging/MessageAttachment.svelte +3 -3
  70. package/src/lib/components/messaging/MessageAttachmentUpload.svelte +3 -3
  71. package/src/lib/components/messaging/MessageInput.svelte +1 -1
  72. package/src/lib/components/messaging/MessageItem.svelte +6 -3
  73. package/src/lib/components/messaging/NotificationSettingsModal.svelte +1 -1
  74. package/src/lib/components/messaging/QuotedMessageDisplay.svelte +6 -1
  75. package/src/lib/components/messaging/StartDMModal.svelte +1 -1
  76. package/src/lib/components/pipeline/Pipeline.svelte +4 -4
  77. package/src/lib/components/pipeline/PipelineCard.svelte +1 -1
  78. package/src/lib/components/pipeline/PipelineColumn.svelte +8 -3
  79. package/src/lib/index.ts +91 -0
  80. package/src/lib/stores/confirmDialog.svelte.ts +48 -0
  81. package/src/lib/stores/inputDialog.svelte.ts +51 -0
  82. package/src/lib/styles/rail.css +63 -0
  83. package/src/lib/types/annotation.ts +38 -0
  84. package/src/lib/types/comments.ts +97 -0
  85. package/src/lib/types/entityPreview.ts +45 -0
  86. package/src/lib/types/filePicker.ts +2 -0
  87. package/src/lib/types/smartImageEditor.ts +39 -0
  88. package/src/lib/types/templateVars.ts +36 -0
  89. package/src/lib/utils/dateFormatters.ts +12 -10
  90. package/src/lib/utils/taskUtils.ts +21 -7
package/src/lib/index.ts CHANGED
@@ -1,9 +1,19 @@
1
+ // Actions
2
+ export { railNav, createRailNav, cycle } from './actions/railNav';
3
+ export type { RailNavOptions, RailNavController } from './actions/railNav';
4
+
1
5
  // Components — Universal
2
6
  export { default as UserAvatar } from './components/UserAvatar.svelte';
3
7
  export { default as SidebarUserFooter } from './components/SidebarUserFooter.svelte';
4
8
  export { default as TaskTypeIcon } from './components/TaskTypeIcon.svelte';
5
9
 
6
10
  // Components — from JAT IDE
11
+ export { default as ConfirmDialog } from './components/ConfirmDialog.svelte';
12
+ export { showConfirm, confirmDialog, cancelDialog, getPending } from './stores/confirmDialog.svelte';
13
+ export type { ConfirmOptions } from './stores/confirmDialog.svelte';
14
+ export { default as InputDialog } from './components/InputDialog.svelte';
15
+ export { showInput, submitInput, cancelInput, getPendingInput } from './stores/inputDialog.svelte';
16
+ export type { InputOptions } from './stores/inputDialog.svelte';
7
17
  export { default as SearchDropdown } from './components/SearchDropdown.svelte';
8
18
  export { default as FilterDropdown } from './components/FilterDropdown.svelte';
9
19
  export { default as ContextMenu } from './components/ContextMenu.svelte';
@@ -13,6 +23,7 @@ export { default as DateRangePicker } from './components/DateRangePicker.svelte'
13
23
  export { default as SortDropdown } from './components/SortDropdown.svelte';
14
24
  export { default as InlineEdit } from './components/InlineEdit.svelte';
15
25
  export { default as FloatingActionBar } from './components/FloatingActionBar.svelte';
26
+ export { default as HunkDiffView } from './components/HunkDiffView.svelte';
16
27
 
17
28
  // Components — from Flush
18
29
  export { default as Button } from './components/Button.svelte';
@@ -34,6 +45,7 @@ export { default as ColorSelector } from './components/ColorSelector.svelte';
34
45
  export { default as ConfirmModal } from './components/ConfirmModal.svelte';
35
46
  export { default as CountdownTimer } from './components/CountdownTimer.svelte';
36
47
  export { default as ImageUpload } from './components/ImageUpload.svelte';
48
+ export { default as SmartImageEditor } from './components/SmartImageEditor.svelte';
37
49
  export { default as LazyImage } from './components/LazyImage.svelte';
38
50
  export { default as ResizablePanel } from './components/ResizablePanel.svelte';
39
51
  export { default as VoicePlayer } from './components/VoicePlayer.svelte';
@@ -42,15 +54,31 @@ export { default as SpeechForm } from './components/SpeechForm.svelte';
42
54
  // Components — from Headcount
43
55
  export { default as ThemeSelector } from './components/ThemeSelector.svelte';
44
56
  export { default as Sparkline } from './components/Sparkline.svelte';
57
+ export { default as BurndownChart } from './components/BurndownChart.svelte';
45
58
 
46
59
  // Components — from JST
47
60
  export { default as AvatarUpload } from './components/AvatarUpload.svelte';
48
61
  export { default as PdfThumbnail } from './components/PdfThumbnail.svelte';
49
62
  export { default as FilePicker } from './components/FilePicker.svelte';
63
+ export { default as FileDropzone } from './components/FileDropzone.svelte';
64
+ export { default as FileThumbnail } from './components/FileThumbnail.svelte';
65
+ export { default as EntityPreviewCard } from './components/EntityPreviewCard.svelte';
66
+
67
+ // EntityPreviewCard types
68
+ export type { EntityPreviewData, EntityPreviewStat, EntityPreviewBadge } from './types/entityPreview';
50
69
 
51
70
  // FilePicker types
52
71
  export type { FilePickerFile, FilePickerSelection } from './types/filePicker';
53
72
 
73
+ // FileDropzone types
74
+ export type { FileDropzoneFile } from './components/FileDropzone.svelte';
75
+
76
+ // Components — Document Templates (JST)
77
+ export { default as VariablePicker } from './components/VariablePicker.svelte';
78
+
79
+ // Template variable types
80
+ export type { TemplateVar, TemplateVarGroup } from './types/templateVars';
81
+
54
82
  // Components — Emoji
55
83
  export { default as EmojiPicker } from './components/EmojiPicker.svelte';
56
84
 
@@ -61,6 +89,9 @@ export type { EmojiEntry, EmojiGroup } from './data/emojis';
61
89
  // Components — from Meadow
62
90
  export { default as LinkShortener } from './components/LinkShortener.svelte';
63
91
 
92
+ // Components — Media
93
+ export { default as MediaWorkbench } from './components/MediaWorkbench.svelte';
94
+
64
95
  // Components — from Marduk
65
96
  export { default as SignaturePad } from './components/SignaturePad.svelte';
66
97
  export { default as MilestoneCard } from './components/MilestoneCard.svelte';
@@ -110,6 +141,14 @@ export {
110
141
  LOCATION_ICONS
111
142
  } from './types/booking';
112
143
 
144
+ // SmartImageEditor types
145
+ export type {
146
+ ImageVariantSpec,
147
+ ImageProcessingSpec,
148
+ VariantResult,
149
+ VariantState,
150
+ } from './types/smartImageEditor';
151
+
113
152
  // Utilities — date formatting
114
153
  export {
115
154
  normalizeTimestamp,
@@ -262,3 +301,55 @@ export {
262
301
  countMentions,
263
302
  setMentionUrlGenerator
264
303
  } from './utils/mentionParser';
304
+
305
+ // Components — Comments (universal threaded comments)
306
+ export { default as CommentThread } from './components/CommentThread.svelte';
307
+
308
+ // Comment types
309
+ export type {
310
+ Comment,
311
+ CommentEntityType,
312
+ CommentVisibility,
313
+ CommentCreateInput,
314
+ CommentCallbacks
315
+ } from './types/comments';
316
+ export { COMMENT_ENTITY_TYPES, COMMENT_VISIBILITIES } from './types/comments';
317
+
318
+ // Components — PDF Viewer (server-side PDFium WASM rendering, no client pdf.js)
319
+ export { default as PDFViewer } from './components/PDFViewer.svelte';
320
+
321
+ // Components — Document annotation suite (from Steelbridge FileManager)
322
+ export { default as MarkupEditor } from './components/MarkupEditor.svelte';
323
+ export { default as MarkupOverlay } from './components/MarkupOverlay.svelte';
324
+ export { default as ImageLightbox } from './components/ImageLightbox.svelte';
325
+ export { default as AnnotationLayer } from './components/AnnotationLayer.svelte';
326
+ export { default as AnnotationPanel } from './components/AnnotationPanel.svelte';
327
+
328
+ // Markup drawing utilities
329
+ export {
330
+ MARKUP_COLORS,
331
+ DEFAULT_STROKE_WIDTH,
332
+ renderShape,
333
+ renderAllShapes,
334
+ nextShapeId,
335
+ } from './components/markup';
336
+
337
+ // Markup + annotation types
338
+ export type {
339
+ MarkupTool,
340
+ Point,
341
+ MarkupShape,
342
+ ArrowShape,
343
+ RectangleShape,
344
+ EllipseShape,
345
+ CloudShape,
346
+ FreehandShape,
347
+ TextShape,
348
+ } from './components/markup';
349
+
350
+ export type {
351
+ Annotation,
352
+ AnnotationProfile,
353
+ AnnotationThread,
354
+ AnnotationCallbacks,
355
+ } from './types/annotation';
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Global promise-based confirm dialog.
3
+ * Usage: const ok = await showConfirm({ title: '...', body: '...' })
4
+ */
5
+
6
+ export type ConfirmOptions = {
7
+ title: string;
8
+ body?: string;
9
+ confirmLabel?: string;
10
+ cancelLabel?: string;
11
+ danger?: boolean;
12
+ timeoutMs?: number; // auto-cancel after this ms; 0 = no timeout (default 8000)
13
+ };
14
+
15
+ type Pending = {
16
+ options: ConfirmOptions;
17
+ resolve: (result: boolean) => void;
18
+ };
19
+
20
+ let pending = $state<Pending | null>(null);
21
+
22
+ export function getPending(): Pending | null {
23
+ return pending;
24
+ }
25
+
26
+ export function showConfirm(options: ConfirmOptions): Promise<boolean> {
27
+ // If a dialog is already open, cancel it first.
28
+ if (pending) {
29
+ pending.resolve(false);
30
+ }
31
+ return new Promise<boolean>((resolve) => {
32
+ pending = { options, resolve };
33
+ });
34
+ }
35
+
36
+ export function confirmDialog(): void {
37
+ if (!pending) return;
38
+ const p = pending;
39
+ pending = null;
40
+ p.resolve(true);
41
+ }
42
+
43
+ export function cancelDialog(): void {
44
+ if (!pending) return;
45
+ const p = pending;
46
+ pending = null;
47
+ p.resolve(false);
48
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Global promise-based input dialog.
3
+ * Usage: const value = await showInput({ title: '...', placeholder: '...' })
4
+ * Returns the typed string if confirmed, null if cancelled.
5
+ */
6
+
7
+ export type InputOptions = {
8
+ title: string;
9
+ body?: string;
10
+ placeholder?: string;
11
+ confirmLabel?: string;
12
+ cancelLabel?: string;
13
+ timeoutMs?: number; // reserved; InputDialog never auto-cancels (user is typing)
14
+ validate?: (value: string) => string | null; // null = valid, string = error message
15
+ maxLength?: number;
16
+ };
17
+
18
+ type Pending = {
19
+ options: InputOptions;
20
+ resolve: (result: string | null) => void;
21
+ };
22
+
23
+ let pending = $state<Pending | null>(null);
24
+
25
+ export function getPendingInput(): Pending | null {
26
+ return pending;
27
+ }
28
+
29
+ export function showInput(options: InputOptions): Promise<string | null> {
30
+ // If a dialog is already open, cancel it first.
31
+ if (pending) {
32
+ pending.resolve(null);
33
+ }
34
+ return new Promise<string | null>((resolve) => {
35
+ pending = { options, resolve };
36
+ });
37
+ }
38
+
39
+ export function submitInput(value: string): void {
40
+ if (!pending) return;
41
+ const p = pending;
42
+ pending = null;
43
+ p.resolve(value);
44
+ }
45
+
46
+ export function cancelInput(): void {
47
+ if (!pending) return;
48
+ const p = pending;
49
+ pending = null;
50
+ p.resolve(null);
51
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * rail.css — keyboard rail-navigation primitives
3
+ *
4
+ * Provides focus-ring and value-change-pulse treatments for rail-nav
5
+ * components (vertical or horizontal keyboard-navigable lists where each
6
+ * rung carries a [data-rail-id] attribute).
7
+ *
8
+ * Usage: import once in your app's root layout or global stylesheet:
9
+ * @import '@joewinke/jatui/src/lib/styles/rail.css';
10
+ *
11
+ * Token: var(--color-primary) — DaisyUI v5 primary (resolves to the
12
+ * consuming app's active theme primary hue).
13
+ *
14
+ * Naming contract (must match the companion railNav Svelte action):
15
+ * - Active-rung focus treatment: [data-rail-id]:focus-visible selector
16
+ * AND .rail-focus utility class (both route to the same ring + tint).
17
+ * - Value-change one-shot pulse: .rail-cycled (added by the action,
18
+ * removed after the animation fires).
19
+ *
20
+ * Design rules enforced:
21
+ * - Ring + tint only — NO colored side-stripe (§6 Don't: border-l/r > 1px).
22
+ * - No box-shadow at rest; shadow only during the pulse keyframe.
23
+ * - Respects prefers-reduced-motion.
24
+ */
25
+
26
+ /* ─── Active-rung focus ring ───────────────────────────────────────────────
27
+ Applied via :focus-visible on the rung element (attribute-based, works
28
+ without adding a class) AND via .rail-focus (explicit class, same visual).
29
+ The ring is primary-tinted at 65% opacity; border-radius matches DaisyUI's
30
+ field radius token (falls back to 0.25rem). No side-stripe. No shadow at
31
+ rest (Flat-by-Default rule). ─────────────────────────────────────────── */
32
+
33
+ [data-rail-id]:focus-visible,
34
+ [data-rail-id]:has(:focus-visible),
35
+ .rail-focus {
36
+ outline: 2px solid color-mix(in oklch, var(--color-primary) 65%, transparent);
37
+ outline-offset: 2px;
38
+ border-radius: var(--radius-field, 0.25rem);
39
+ }
40
+
41
+ /* ─── Value-change pulse ────────────────────────────────────────────────────
42
+ One-shot animation applied by the railNav action when the rung's bound
43
+ value changes. Scales up 2.5% and blooms a brief primary-hued glow ring,
44
+ then returns to rest. Duration 260ms — fast enough to feel snappy,
45
+ long enough to register as intentional feedback.
46
+ Box-shadow (not outline) is used here so it can animate smoothly;
47
+ outline transitions are not interpolated by CSS. ─────────────────────── */
48
+
49
+ @keyframes rail-cycled {
50
+ 0% { transform: scale(1); box-shadow: 0 0 0 0 color-mix(in oklch, var(--color-primary) 0%, transparent); }
51
+ 35% { transform: scale(1.025); box-shadow: 0 0 0 3px color-mix(in oklch, var(--color-primary) 32%, transparent); }
52
+ 100% { transform: scale(1); box-shadow: 0 0 0 0 color-mix(in oklch, var(--color-primary) 0%, transparent); }
53
+ }
54
+
55
+ .rail-cycled {
56
+ animation: rail-cycled 0.26s cubic-bezier(0.22, 1, 0.36, 1);
57
+ }
58
+
59
+ @media (prefers-reduced-motion: reduce) {
60
+ .rail-cycled {
61
+ animation: none;
62
+ }
63
+ }
@@ -0,0 +1,38 @@
1
+ // Annotation types for document/image annotation suite
2
+ // Used by AnnotationLayer, AnnotationPanel, ImageLightbox
3
+
4
+ export interface Annotation {
5
+ id: string
6
+ user_id: string
7
+ x_percent: number
8
+ y_percent: number
9
+ content: string
10
+ parent_annotation_id: string | null
11
+ resolved: boolean
12
+ resolved_by: string | null
13
+ resolved_at: string | null
14
+ created_at: string
15
+ updated_at: string
16
+ }
17
+
18
+ export interface AnnotationProfile {
19
+ full_name: string | null
20
+ avatar_url: string | null
21
+ }
22
+
23
+ export interface AnnotationThread {
24
+ root: Annotation
25
+ replies: Annotation[]
26
+ }
27
+
28
+ /** Callbacks for AnnotationPanel async operations */
29
+ export interface AnnotationCallbacks {
30
+ /** Post a reply to a root annotation. Must return the new annotation or throw. */
31
+ onAddReply: (parentId: string, content: string) => Promise<void>
32
+ /** Toggle resolved state. */
33
+ onResolve: (annotationId: string, resolved: boolean) => Promise<void>
34
+ /** Delete an annotation (and its replies). */
35
+ onDelete: (annotationId: string) => Promise<void>
36
+ /** Edit annotation content. */
37
+ onEdit: (annotationId: string, content: string) => Promise<void>
38
+ }
@@ -0,0 +1,97 @@
1
+ // Threaded Comments Types
2
+ // Generic types for the CommentThread UI component — no framework or backend
3
+ // dependencies. Mirrors the JST `$lib/types/comments` projection so the
4
+ // component is a drop-in for any app whose server speaks the same shape.
5
+ //
6
+ // Threading is ONE level deep on the data side: a comment is either
7
+ // top-level (`parent_id === null`) or a single-level reply. The UI renders
8
+ // this as two visual levels (a comment and its replies).
9
+
10
+ /** Entity kinds a comment thread can hang off — mirrors the host DB CHECK. */
11
+ export type CommentEntityType =
12
+ | 'project'
13
+ | 'invoice'
14
+ | 'contract'
15
+ | 'asset'
16
+ | 'task'
17
+ | 'form_submission'
18
+ | 'milestone'
19
+ | 'contract_term'
20
+
21
+ export const COMMENT_ENTITY_TYPES: readonly CommentEntityType[] = [
22
+ 'project',
23
+ 'invoice',
24
+ 'contract',
25
+ 'asset',
26
+ 'task',
27
+ 'form_submission',
28
+ 'milestone',
29
+ 'contract_term'
30
+ ] as const
31
+
32
+ /** Internal = staff-only. Client = also visible in the customer portal. */
33
+ export type CommentVisibility = 'internal' | 'client'
34
+
35
+ export const COMMENT_VISIBILITIES: readonly CommentVisibility[] = [
36
+ 'internal',
37
+ 'client'
38
+ ] as const
39
+
40
+ export interface Comment {
41
+ id: string
42
+ team_id: string
43
+ entity_type: CommentEntityType
44
+ entity_id: string
45
+ author_id: string
46
+ /** Joined author display name (null if the profile is gone). */
47
+ author_name: string | null
48
+ /** Joined author avatar URL. */
49
+ author_avatar_url: string | null
50
+ body: string
51
+ /** null = top-level; otherwise the id of the top-level comment it replies to. */
52
+ parent_id: string | null
53
+ visibility: CommentVisibility
54
+ /** Set the first time the body is edited. */
55
+ edited_at: string | null
56
+ /** Set on soft-delete; body is replaced with "[deleted]". */
57
+ deleted_at: string | null
58
+ created_at: string
59
+ /** Number of replies (top-level rows only; always 0 for replies). */
60
+ reply_count: number
61
+ /**
62
+ * Replies are loaded on demand; this is [] in the paginated top-level
63
+ * listing and populated by the component when a thread is expanded.
64
+ */
65
+ replies: Comment[]
66
+ }
67
+
68
+ export interface CommentCreateInput {
69
+ entityType: CommentEntityType
70
+ entityId: string
71
+ body: string
72
+ /** When set, this comment is a reply to the given TOP-LEVEL comment. */
73
+ parentId?: string | null
74
+ /** Defaults to "internal". Clamped to the parent's visibility for replies. */
75
+ visibility?: CommentVisibility
76
+ }
77
+
78
+ /**
79
+ * Transport-agnostic data layer for `<CommentThread>`. The consuming app
80
+ * wires these to a server load / form action / fetch endpoint. The
81
+ * component never imports server code or knows how data is fetched.
82
+ */
83
+ export interface CommentCallbacks {
84
+ /** Persist a new comment (top-level or reply) and return the created row. */
85
+ submitComment: (input: CommentCreateInput) => Promise<Comment>
86
+ /** Edit an existing comment body; returns the updated row. */
87
+ editComment: (id: string, body: string) => Promise<Comment>
88
+ /** Soft-delete a comment. The row stays so replies remain anchored. */
89
+ deleteComment: (id: string) => Promise<void>
90
+ /** Fetch the replies for a top-level comment (oldest-first). */
91
+ loadReplies: (parentId: string) => Promise<Comment[]>
92
+ /**
93
+ * Optional: fetch the next page of top-level comments. When omitted, the
94
+ * "Load more" affordance is hidden.
95
+ */
96
+ loadMore?: (offset: number) => Promise<{ comments: Comment[]; total: number }>
97
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Entity preview card — the uniform shape an EntityPreviewCard renders and that
3
+ * a server-side resolver returns for any domain entity. One shape for every
4
+ * entity type so the card and hover-action stay generic and the same card backs
5
+ * hover-previews, @-mention unfurls, and link previews.
6
+ *
7
+ * Pure types — safe to import from both server (the resolver) and client (the
8
+ * card + action).
9
+ */
10
+
11
+ /** A small labelled stat shown in the card's footer grid (e.g. "Submissions" → 42). */
12
+ export interface EntityPreviewStat {
13
+ label: string
14
+ value: string | number
15
+ }
16
+
17
+ /** A coloured pill (status, type, …). `tone` maps to a DaisyUI semantic token. */
18
+ export interface EntityPreviewBadge {
19
+ label: string
20
+ tone?: "neutral" | "primary" | "info" | "success" | "warning" | "error"
21
+ }
22
+
23
+ export interface EntityPreviewData {
24
+ /** Canonical entity type slug (e.g. "base", "vendor", "form", "task"). */
25
+ type: string
26
+ id: string
27
+ /** Human label for the type, shown as the card's kicker (e.g. "Knowledge base"). */
28
+ typeLabel: string
29
+ /** Main title. */
30
+ title: string
31
+ /** Optional one-line subtitle under the title (e.g. a vendor's company). */
32
+ subtitle?: string | null
33
+ /** Optional longer summary / description (clamped in the card). */
34
+ summary?: string | null
35
+ /** og:image / avatar / thumbnail URL, when the entity has one. */
36
+ imageUrl?: string | null
37
+ /** A short emoji/text glyph fallback when there's no image. */
38
+ icon?: string | null
39
+ /** Up to ~4 labelled stats. */
40
+ stats?: EntityPreviewStat[]
41
+ /** Up to ~3 status/type pills. */
42
+ badges?: EntityPreviewBadge[]
43
+ /** The admin detail route for the entity (the card links here). */
44
+ href?: string | null
45
+ }
@@ -5,6 +5,8 @@ export interface FilePickerFile {
5
5
  updated_at: string
6
6
  metadata: Record<string, any>
7
7
  scope: "personal" | "team"
8
+ /** Publicly accessible URL (no auth required). When present, used instead of the /api/files proxy URL. */
9
+ publicUrl?: string
8
10
  }
9
11
 
10
12
  export interface FilePickerSelection {
@@ -0,0 +1,39 @@
1
+ export interface ImageVariantSpec {
2
+ name: string
3
+ width: number
4
+ height: number
5
+ fit?: "contain" | "cover" | "fill"
6
+ /** CSS hex color or "transparent". Transparent background when omitted. */
7
+ background?: string
8
+ /**
9
+ * Optional Gemini edit instruction run on the (background-removed) source
10
+ * before Sharp's deterministic resize. Used to intelligently re-frame the
11
+ * variant — e.g. crop a logo down to just the brand mark, or compose an OG
12
+ * card. Falls back to Sharp-only if the Gemini call fails.
13
+ */
14
+ aiPrompt?: string
15
+ }
16
+
17
+ export interface ImageProcessingSpec {
18
+ removeBackground?: boolean
19
+ aspectRatio?: string
20
+ variants: ImageVariantSpec[]
21
+ }
22
+
23
+ export interface VariantResult {
24
+ name: string
25
+ url: string
26
+ width: number
27
+ height: number
28
+ backgroundRemovalFailed?: boolean
29
+ /** True when this variant had an aiPrompt but the Gemini edit call failed
30
+ * and we fell back to Sharp-only. The result is still uploaded; this is a
31
+ * non-blocking signal that the smart re-frame didn't happen. */
32
+ aiEditFailed?: boolean
33
+ }
34
+
35
+ export type VariantState =
36
+ | { status: "idle" }
37
+ | { status: "loading" }
38
+ | { status: "done"; result: VariantResult }
39
+ | { status: "error"; message: string }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Template variable types — shared shape between the JST server-side registry
3
+ * (`src/lib/server/template-vars.ts`, `getVarsForCategory`) and the
4
+ * VariablePicker component.
5
+ *
6
+ * jatui cannot import the host app's `$lib/server` module (server-only), so
7
+ * these types are mirrored here. They are kept structurally identical to the
8
+ * registry's output so `getVarsForCategory(category, availableVars)` can be
9
+ * passed straight into `<VariablePicker groups={...} />` with no mapping —
10
+ * same jst↔jatui structural-compatibility pattern used by CommentThread.
11
+ */
12
+
13
+ /** A single insertable merge variable. */
14
+ export interface TemplateVar {
15
+ /**
16
+ * Dotted variable path inserted into the template as `{{name}}`.
17
+ * e.g. `contact.name`, `org.logo_url`, `total`.
18
+ */
19
+ name: string
20
+ /** Human label shown in the picker, e.g. "Contact name". */
21
+ label: string
22
+ /** Optional one-line help text describing the value. */
23
+ description?: string
24
+ /** Optional sample value shown as a hint, e.g. "Acme Inc.". */
25
+ example?: string
26
+ }
27
+
28
+ /** Variable context group, e.g. all client/org/financial/custom vars. */
29
+ export interface TemplateVarGroup {
30
+ /** Stable group id — matches VarGroupId in the host app's template-vars registry. */
31
+ id: string
32
+ /** Group heading shown in the picker, e.g. "Client". */
33
+ label: string
34
+ /** Variables in this group (never empty — empty groups are omitted). */
35
+ vars: TemplateVar[]
36
+ }
@@ -24,9 +24,11 @@ export function normalizeTimestamp(timestamp: string): string {
24
24
 
25
25
  /**
26
26
  * Parse a potentially non-standard timestamp into a Date object.
27
+ * Accepts a Date (returned by postgres.js), ISO string, or null/undefined.
27
28
  */
28
- export function parseTimestamp(timestamp: string | null | undefined): Date | null {
29
+ export function parseTimestamp(timestamp: Date | string | null | undefined): Date | null {
29
30
  if (!timestamp) return null;
31
+ if (timestamp instanceof Date) return isNaN(timestamp.getTime()) ? null : timestamp;
30
32
 
31
33
  const normalized = normalizeTimestamp(timestamp);
32
34
  const date = new Date(normalized);
@@ -38,7 +40,7 @@ export function parseTimestamp(timestamp: string | null | undefined): Date | nul
38
40
  * Format relative time (e.g., "2d", "3mo", "1y").
39
41
  * Compact format suitable for tables and compact UIs.
40
42
  */
41
- export function formatRelativeTime(dateStr: string | null | undefined): string {
43
+ export function formatRelativeTime(dateStr: Date | string | null | undefined): string {
42
44
  if (!dateStr) return '-';
43
45
 
44
46
  const date = parseTimestamp(dateStr);
@@ -65,7 +67,7 @@ export function formatRelativeTime(dateStr: string | null | undefined): string {
65
67
  /**
66
68
  * Format full date and time for tooltips and detailed views.
67
69
  */
68
- export function formatFullDate(dateStr: string | null | undefined): string {
70
+ export function formatFullDate(dateStr: Date | string | null | undefined): string {
69
71
  if (!dateStr) return '';
70
72
 
71
73
  const date = parseTimestamp(dateStr);
@@ -83,7 +85,7 @@ export function formatFullDate(dateStr: string | null | undefined): string {
83
85
  /**
84
86
  * Format short date for compact displays (e.g., date range picker).
85
87
  */
86
- export function formatShortDate(dateStr: string | null | undefined): string {
88
+ export function formatShortDate(dateStr: Date | string | null | undefined): string {
87
89
  if (!dateStr) return '';
88
90
 
89
91
  const date = parseTimestamp(dateStr);
@@ -96,7 +98,7 @@ export function formatShortDate(dateStr: string | null | undefined): string {
96
98
  * Format last activity time for agent cards.
97
99
  * Shows "Just now", "Xm ago", "Xh ago", or "Xd ago".
98
100
  */
99
- export function formatLastActivity(timestamp: string | null | undefined): string {
101
+ export function formatLastActivity(timestamp: Date | string | null | undefined): string {
100
102
  if (!timestamp) return 'Never';
101
103
 
102
104
  const date = parseTimestamp(timestamp);
@@ -117,7 +119,7 @@ export function formatLastActivity(timestamp: string | null | undefined): string
117
119
  /**
118
120
  * Format date for display (simple locale string).
119
121
  */
120
- export function formatDate(dateString: string | null | undefined): string {
122
+ export function formatDate(dateString: Date | string | null | undefined): string {
121
123
  if (!dateString) return 'N/A';
122
124
 
123
125
  const date = parseTimestamp(dateString);
@@ -129,7 +131,7 @@ export function formatDate(dateString: string | null | undefined): string {
129
131
  /**
130
132
  * Get milliseconds since timestamp (for calculations).
131
133
  */
132
- export function getTimeSinceMs(timestamp: string | null | undefined): number {
134
+ export function getTimeSinceMs(timestamp: Date | string | null | undefined): number {
133
135
  if (!timestamp) return Infinity;
134
136
 
135
137
  const date = parseTimestamp(timestamp);
@@ -141,14 +143,14 @@ export function getTimeSinceMs(timestamp: string | null | undefined): number {
141
143
  /**
142
144
  * Get minutes since timestamp.
143
145
  */
144
- export function getTimeSinceMinutes(timestamp: string | null | undefined): number {
146
+ export function getTimeSinceMinutes(timestamp: Date | string | null | undefined): number {
145
147
  return getTimeSinceMs(timestamp) / 60000;
146
148
  }
147
149
 
148
150
  /**
149
151
  * Check if timestamp is within a given number of minutes.
150
152
  */
151
- export function isWithinMinutes(timestamp: string | null | undefined, minutes: number): boolean {
153
+ export function isWithinMinutes(timestamp: Date | string | null | undefined, minutes: number): boolean {
152
154
  return getTimeSinceMinutes(timestamp) < minutes;
153
155
  }
154
156
 
@@ -156,7 +158,7 @@ export function isWithinMinutes(timestamp: string | null | undefined, minutes: n
156
158
  * Format date + time for detail views (e.g. "Jan 5, 2:30 PM").
157
159
  * No year — use formatFullDate when year is needed.
158
160
  */
159
- export function formatDateTime(dateStr: string | null | undefined): string {
161
+ export function formatDateTime(dateStr: Date | string | null | undefined): string {
160
162
  if (!dateStr) return '';
161
163
  const date = parseTimestamp(dateStr);
162
164
  if (!date) return '';