@joewinke/jatui 0.1.23 → 0.1.25

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 (124) hide show
  1. package/CLAUDE.md +244 -0
  2. package/package.json +23 -10
  3. package/src/lib/actions/collapse.ts +0 -136
  4. package/src/lib/actions/railNav.ts +0 -473
  5. package/src/lib/components/AnnotationLayer.svelte +0 -108
  6. package/src/lib/components/AnnotationPanel.svelte +0 -319
  7. package/src/lib/components/AudioWaveform.svelte +0 -698
  8. package/src/lib/components/AvailabilityModal.svelte +0 -177
  9. package/src/lib/components/AvatarUpload.svelte +0 -234
  10. package/src/lib/components/Badge.svelte +0 -38
  11. package/src/lib/components/BookingForm.svelte +0 -278
  12. package/src/lib/components/BurndownChart.svelte +0 -778
  13. package/src/lib/components/Button.svelte +0 -81
  14. package/src/lib/components/CalendarPicker.svelte +0 -284
  15. package/src/lib/components/Card.svelte +0 -67
  16. package/src/lib/components/CharacterCounter.svelte +0 -82
  17. package/src/lib/components/ChipInput.svelte +0 -609
  18. package/src/lib/components/Collapse.svelte +0 -82
  19. package/src/lib/components/ColorSelector.svelte +0 -167
  20. package/src/lib/components/CommentThread.svelte +0 -773
  21. package/src/lib/components/ConfirmDialog.svelte +0 -348
  22. package/src/lib/components/ConfirmModal.svelte +0 -142
  23. package/src/lib/components/ContextMenu.svelte +0 -188
  24. package/src/lib/components/CountdownTimer.svelte +0 -94
  25. package/src/lib/components/DateRangePicker.svelte +0 -194
  26. package/src/lib/components/Drawer.svelte +0 -143
  27. package/src/lib/components/EmojiPicker.svelte +0 -159
  28. package/src/lib/components/EntityPreviewCard.svelte +0 -104
  29. package/src/lib/components/FileDropzone.svelte +0 -493
  30. package/src/lib/components/FilePicker.svelte +0 -488
  31. package/src/lib/components/FileThumbnail.svelte +0 -80
  32. package/src/lib/components/FilterDropdown.svelte +0 -203
  33. package/src/lib/components/FloatingActionBar.svelte +0 -354
  34. package/src/lib/components/GPSTracker.svelte +0 -202
  35. package/src/lib/components/HunkDiffView.svelte +0 -348
  36. package/src/lib/components/ImageLightbox.svelte +0 -274
  37. package/src/lib/components/ImageUpload.svelte +0 -147
  38. package/src/lib/components/InlineEdit.svelte +0 -291
  39. package/src/lib/components/InputDialog.svelte +0 -327
  40. package/src/lib/components/KeyboardShortcutsOverlay.svelte +0 -296
  41. package/src/lib/components/LazyImage.svelte +0 -123
  42. package/src/lib/components/LinkShortener.svelte +0 -274
  43. package/src/lib/components/LoadingSpinner.svelte +0 -106
  44. package/src/lib/components/LocationMap.svelte +0 -186
  45. package/src/lib/components/MapView.svelte +0 -341
  46. package/src/lib/components/MarkupEditor.svelte +0 -485
  47. package/src/lib/components/MarkupOverlay.svelte +0 -55
  48. package/src/lib/components/MediaWorkbench.svelte +0 -871
  49. package/src/lib/components/MilestoneCard.svelte +0 -98
  50. package/src/lib/components/MilestoneTimeline.svelte +0 -91
  51. package/src/lib/components/Modal.svelte +0 -244
  52. package/src/lib/components/PDFViewer.svelte +0 -105
  53. package/src/lib/components/PdfThumbnail.svelte +0 -63
  54. package/src/lib/components/PhoneInput.svelte +0 -212
  55. package/src/lib/components/ResizableDivider.svelte +0 -307
  56. package/src/lib/components/ResizablePanel.svelte +0 -305
  57. package/src/lib/components/SearchDropdown.svelte +0 -421
  58. package/src/lib/components/SelectInput.svelte +0 -237
  59. package/src/lib/components/SidebarUserFooter.svelte +0 -34
  60. package/src/lib/components/SignaturePad.svelte +0 -175
  61. package/src/lib/components/SmartImageEditor.svelte +0 -720
  62. package/src/lib/components/SortDropdown.svelte +0 -156
  63. package/src/lib/components/Sparkline.svelte +0 -116
  64. package/src/lib/components/SpeechForm.svelte +0 -114
  65. package/src/lib/components/StatusBadge.svelte +0 -157
  66. package/src/lib/components/TaskTypeIcon.svelte +0 -54
  67. package/src/lib/components/TextArea.svelte +0 -162
  68. package/src/lib/components/TextInput.svelte +0 -131
  69. package/src/lib/components/ThemeSelector.svelte +0 -206
  70. package/src/lib/components/TimeSlotPicker.svelte +0 -162
  71. package/src/lib/components/UserAvatar.svelte +0 -72
  72. package/src/lib/components/VariablePicker.svelte +0 -170
  73. package/src/lib/components/VoicePlayer.svelte +0 -422
  74. package/src/lib/components/linked-columns/LinkedColumns.svelte +0 -526
  75. package/src/lib/components/markup.ts +0 -287
  76. package/src/lib/components/messaging/Avatar.svelte +0 -81
  77. package/src/lib/components/messaging/ChannelInfoModal.svelte +0 -163
  78. package/src/lib/components/messaging/ChannelList.svelte +0 -107
  79. package/src/lib/components/messaging/ChannelMemberAvatarStack.svelte +0 -69
  80. package/src/lib/components/messaging/ChannelMembersModal.svelte +0 -182
  81. package/src/lib/components/messaging/CreateChannelModal.svelte +0 -190
  82. package/src/lib/components/messaging/DirectMessageList.svelte +0 -145
  83. package/src/lib/components/messaging/EmojiSelector.svelte +0 -261
  84. package/src/lib/components/messaging/MentionAutocomplete.svelte +0 -193
  85. package/src/lib/components/messaging/MessageAttachment.svelte +0 -273
  86. package/src/lib/components/messaging/MessageAttachmentUpload.svelte +0 -243
  87. package/src/lib/components/messaging/MessageInput.svelte +0 -452
  88. package/src/lib/components/messaging/MessageItem.svelte +0 -341
  89. package/src/lib/components/messaging/MessageThread.svelte +0 -306
  90. package/src/lib/components/messaging/NotificationSettingsModal.svelte +0 -234
  91. package/src/lib/components/messaging/QuotedMessageDisplay.svelte +0 -123
  92. package/src/lib/components/messaging/StartDMModal.svelte +0 -100
  93. package/src/lib/components/messaging/ThreadPanel.svelte +0 -153
  94. package/src/lib/components/pipeline/DESIGN.md +0 -361
  95. package/src/lib/components/pipeline/Pipeline.svelte +0 -391
  96. package/src/lib/components/pipeline/PipelineCard.svelte +0 -94
  97. package/src/lib/components/pipeline/PipelineColumn.svelte +0 -163
  98. package/src/lib/components/replay/ChapterTimeline.svelte +0 -326
  99. package/src/lib/components/session-nav/transcriptModel.ts +0 -352
  100. package/src/lib/data/emojis.ts +0 -1208
  101. package/src/lib/index.ts +0 -405
  102. package/src/lib/stores/confirmDialog.svelte.ts +0 -48
  103. package/src/lib/stores/inputDialog.svelte.ts +0 -51
  104. package/src/lib/styles/rail.css +0 -63
  105. package/src/lib/types/annotation.ts +0 -38
  106. package/src/lib/types/booking.ts +0 -143
  107. package/src/lib/types/comments.ts +0 -97
  108. package/src/lib/types/entityPreview.ts +0 -45
  109. package/src/lib/types/filePicker.ts +0 -16
  110. package/src/lib/types/googleMaps.d.ts +0 -51
  111. package/src/lib/types/maps.ts +0 -43
  112. package/src/lib/types/messaging.ts +0 -459
  113. package/src/lib/types/milestone.ts +0 -22
  114. package/src/lib/types/pipeline.ts +0 -150
  115. package/src/lib/types/smartImageEditor.ts +0 -39
  116. package/src/lib/types/templateVars.ts +0 -36
  117. package/src/lib/utils/currency.ts +0 -20
  118. package/src/lib/utils/daisyuiColors.ts +0 -243
  119. package/src/lib/utils/dateFormatters.ts +0 -196
  120. package/src/lib/utils/googleMapsLoader.ts +0 -84
  121. package/src/lib/utils/mentionParser.ts +0 -188
  122. package/src/lib/utils/phone.ts +0 -80
  123. package/src/lib/utils/phoneFormat.ts +0 -74
  124. package/src/lib/utils/taskUtils.ts +0 -75
@@ -1,94 +0,0 @@
1
- <script lang="ts">
2
- import { fade } from "svelte/transition"
3
-
4
- type TimerProps = {
5
- targetDate?: string | null
6
- minutesToAdd?: number
7
- onComplete?: () => void
8
- size?: "sm" | "md" | "lg"
9
- completionMessage?: string
10
- }
11
-
12
- let {
13
- targetDate = null,
14
- minutesToAdd = 1,
15
- onComplete = () => {},
16
- size = "sm",
17
- completionMessage = "Complete",
18
- }: TimerProps = $props()
19
-
20
- let minutes = $state(0)
21
- let seconds = $state(0)
22
- let countdownInterval: number | null = null
23
- let startTime: Date | null = null
24
- let isComplete = $state(false)
25
-
26
- const sizeClasses: Record<"sm" | "md" | "lg", string> = {
27
- sm: "text-lg",
28
- md: "text-2xl",
29
- lg: "text-4xl",
30
- }
31
- let sizeClass = $derived(sizeClasses[size])
32
-
33
- function updateCountdown() {
34
- const now = new Date()
35
- let targetTime: Date
36
-
37
- if (targetDate) {
38
- const finishTime = new Date(targetDate)
39
- targetTime = new Date(finishTime.getTime() + minutesToAdd * 60 * 1000)
40
- } else {
41
- if (!startTime) {
42
- startTime = now
43
- }
44
- targetTime = new Date(startTime.getTime() + minutesToAdd * 60 * 1000)
45
- }
46
-
47
- const diff = targetTime.getTime() - now.getTime()
48
-
49
- if (diff <= 0) {
50
- if (countdownInterval) {
51
- clearInterval(countdownInterval)
52
- countdownInterval = null
53
- }
54
- minutes = 0
55
- seconds = 0
56
- isComplete = true
57
- onComplete()
58
- return
59
- }
60
-
61
- minutes = Math.floor(diff / (1000 * 60))
62
- seconds = Math.floor((diff % (1000 * 60)) / 1000)
63
- }
64
-
65
- $effect(() => {
66
- if (targetDate || minutesToAdd > 0) {
67
- updateCountdown()
68
- countdownInterval = setInterval(updateCountdown, 1000) as unknown as number
69
- }
70
-
71
- return () => {
72
- if (countdownInterval) {
73
- clearInterval(countdownInterval)
74
- countdownInterval = null
75
- }
76
- }
77
- })
78
- </script>
79
-
80
- <div class="text-info">
81
- {#if !isComplete}
82
- EST.&nbsp;
83
- <span class="countdown font-mono {sizeClass}" transition:fade>
84
- <span style="--value:{minutes};"></span>:
85
- <span style="--value:{seconds};"></span>
86
- &nbsp;REMAINING
87
- </span>
88
- {:else}
89
- <span class="countdown {sizeClass}" transition:fade>
90
- <span class="loading loading-dots loading-md mr-3"></span>
91
- {completionMessage}
92
- </span>
93
- {/if}
94
- </div>
@@ -1,194 +0,0 @@
1
- <script lang="ts">
2
- /**
3
- * DateRangePicker Component
4
- * DaisyUI dropdown for filtering by date range with quick presets
5
- */
6
-
7
- import { formatShortDate } from '../utils/dateFormatters';
8
-
9
- interface Props {
10
- selectedRange: string; // 'today' | 'yesterday' | 'week' | 'month' | 'all' | 'custom'
11
- customFrom?: string | null; // ISO date for custom range
12
- customTo?: string | null; // ISO date for custom range
13
- onRangeChange: (range: string, from?: string, to?: string) => void;
14
- compact?: boolean;
15
- }
16
-
17
- let {
18
- selectedRange = 'all',
19
- customFrom = null,
20
- customTo = null,
21
- onRangeChange,
22
- compact = false
23
- }: Props = $props();
24
-
25
- // Local state for custom date inputs
26
- // svelte-ignore state_referenced_locally
27
- let localFrom = $state(customFrom || '');
28
- // svelte-ignore state_referenced_locally
29
- let localTo = $state(customTo || '');
30
- let showCustom = $state(false);
31
-
32
- // Preset options
33
- const presets = [
34
- { value: 'today', label: 'Today', icon: '1' },
35
- { value: 'yesterday', label: 'Yesterday', icon: '-1' },
36
- { value: 'week', label: 'Last 7 days', icon: '7' },
37
- { value: 'month', label: 'Last 30 days', icon: '30' },
38
- { value: 'all', label: 'All time', icon: '*' }
39
- ];
40
-
41
- // Get display label for current selection
42
- const displayLabel = $derived.by(() => {
43
- if (selectedRange === 'custom' && customFrom && customTo) {
44
- return `${formatShortDate(customFrom)} - ${formatShortDate(customTo)}`;
45
- }
46
- if (selectedRange === 'custom' && customFrom) {
47
- return `From ${formatShortDate(customFrom)}`;
48
- }
49
- if (selectedRange === 'custom' && customTo) {
50
- return `Until ${formatShortDate(customTo)}`;
51
- }
52
- return presets.find(p => p.value === selectedRange)?.label || 'All time';
53
- });
54
-
55
- function handlePresetSelect(preset: string) {
56
- showCustom = false;
57
- localFrom = '';
58
- localTo = '';
59
- onRangeChange(preset);
60
-
61
- // Close dropdown
62
- if (document.activeElement instanceof HTMLElement) {
63
- document.activeElement.blur();
64
- }
65
- }
66
-
67
- function handleCustomApply() {
68
- if (localFrom || localTo) {
69
- onRangeChange('custom', localFrom || undefined, localTo || undefined);
70
-
71
- // Close dropdown
72
- if (document.activeElement instanceof HTMLElement) {
73
- document.activeElement.blur();
74
- }
75
- }
76
- }
77
-
78
- function toggleCustom() {
79
- showCustom = !showCustom;
80
- }
81
- </script>
82
-
83
- <!-- Date Range Picker -->
84
- <div class="dropdown dropdown-end">
85
- <!-- Trigger Button -->
86
- <div
87
- tabindex="0"
88
- role="button"
89
- class="{compact ? 'px-2 py-1 text-[10px]' : 'px-2.5 py-1.5 text-xs'} rounded cursor-pointer transition-all flex items-center gap-1.5 font-mono tracking-wider whitespace-nowrap bg-base-200 border border-base-300 text-base-content/60"
90
- >
91
- <!-- Calendar icon -->
92
- <svg
93
- xmlns="http://www.w3.org/2000/svg"
94
- fill="none"
95
- viewBox="0 0 24 24"
96
- stroke-width="1.5"
97
- stroke="currentColor"
98
- class="w-4 h-4 text-primary"
99
- >
100
- <path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5" />
101
- </svg>
102
- <span class="hidden sm:inline">{displayLabel}</span>
103
- <span class="sm:hidden">
104
- {#if selectedRange === 'today'}
105
- 1d
106
- {:else if selectedRange === 'yesterday'}
107
- -1d
108
- {:else if selectedRange === 'week'}
109
- 7d
110
- {:else if selectedRange === 'month'}
111
- 30d
112
- {:else if selectedRange === 'all'}
113
- All
114
- {:else}
115
- Custom
116
- {/if}
117
- </span>
118
- <svg
119
- xmlns="http://www.w3.org/2000/svg"
120
- fill="none"
121
- viewBox="0 0 24 24"
122
- stroke-width="1.5"
123
- stroke="currentColor"
124
- class="w-3 h-3 text-base-content/50"
125
- >
126
- <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
127
- </svg>
128
- </div>
129
-
130
- <!-- Dropdown Content -->
131
- <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
132
- <div
133
- tabindex="0"
134
- class="dropdown-content rounded-box z-40 w-72 p-3 mt-1 bg-base-200 border border-base-300"
135
- >
136
- <!-- Quick Presets -->
137
- <div class="flex flex-wrap gap-1.5 mb-3">
138
- {#each presets as preset}
139
- <button
140
- type="button"
141
- class="px-2 py-1 rounded cursor-pointer transition-all duration-200 font-mono text-xs border {selectedRange === preset.value ? 'bg-primary/20 border-primary text-primary' : 'bg-base-300 border-base-content/20 text-base-content opacity-60 hover:opacity-100'}"
142
- onclick={() => handlePresetSelect(preset.value)}
143
- >
144
- {preset.label}
145
- </button>
146
- {/each}
147
- </div>
148
-
149
- <!-- Divider -->
150
- <div class="my-2 flex items-center gap-2">
151
- <div class="flex-1 h-px bg-base-300"></div>
152
- <span class="text-[0.75rem] text-base-content/45">or custom</span>
153
- <div class="flex-1 h-px bg-base-300"></div>
154
- </div>
155
-
156
- <!-- Custom Range Section -->
157
- <div class="space-y-2">
158
- <div class="flex gap-2">
159
- <div class="form-control flex-1">
160
- <label class="label py-0.5" for="date-range-from">
161
- <span class="text-[0.8125rem] text-base-content/45">From</span>
162
- </label>
163
- <input
164
- id="date-range-from"
165
- type="date"
166
- class="input input-sm w-full font-mono bg-base-300 border border-base-300 text-base-content"
167
- bind:value={localFrom}
168
- max={localTo || undefined}
169
- />
170
- </div>
171
- <div class="form-control flex-1">
172
- <label class="label py-0.5" for="date-range-to">
173
- <span class="text-[0.8125rem] text-base-content/45">To</span>
174
- </label>
175
- <input
176
- id="date-range-to"
177
- type="date"
178
- class="input input-sm w-full font-mono bg-base-300 border border-base-300 text-base-content"
179
- bind:value={localTo}
180
- min={localFrom || undefined}
181
- />
182
- </div>
183
- </div>
184
- <button
185
- type="button"
186
- class="btn btn-sm btn-primary w-full font-mono"
187
- onclick={handleCustomApply}
188
- disabled={!localFrom && !localTo}
189
- >
190
- Apply Custom Range
191
- </button>
192
- </div>
193
- </div>
194
- </div>
@@ -1,143 +0,0 @@
1
- <script lang="ts">
2
- /**
3
- * Drawer Component
4
- *
5
- * Sliding panel overlay with backdrop, escape key, and header actions.
6
- */
7
-
8
- import type { Snippet } from 'svelte';
9
-
10
- interface Props {
11
- open?: boolean;
12
- title?: string;
13
- position?: 'left' | 'right';
14
- size?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | 'full';
15
- showCloseButton?: boolean;
16
- contentClass?: string;
17
- /**
18
- * When true (default), keydown events fired inside the drawer panel are
19
- * stopped from propagating to window — preventing page-level list/keyboard
20
- * navigation handlers (listNav-style) from receiving arrow keys, j/k, vim
21
- * chords, digit counts, etc. while the drawer is open.
22
- *
23
- * Free-text fields (INPUT, TEXTAREA, contentEditable) are intentionally
24
- * excluded so normal typing still works. Set to false only when the
25
- * consumer needs custom window-level key handling even while the drawer
26
- * is open.
27
- */
28
- trapKeys?: boolean;
29
- onclose?: () => void;
30
- class?: string;
31
- headerActions?: Snippet;
32
- children?: Snippet;
33
- }
34
-
35
- let {
36
- open = $bindable(false),
37
- title = '',
38
- position = 'right',
39
- size = 'md',
40
- showCloseButton = true,
41
- contentClass = '',
42
- trapKeys = true,
43
- onclose,
44
- class: className = '',
45
- headerActions,
46
- children
47
- }: Props = $props();
48
-
49
- const sizeClasses: Record<string, string> = {
50
- sm: 'w-80',
51
- md: 'w-96',
52
- lg: 'w-[640px]',
53
- xl: 'w-[800px]',
54
- '2xl': 'w-[960px]',
55
- '3xl': 'w-[1024px] max-w-[calc(100vw-2rem)]',
56
- full: 'w-full'
57
- };
58
-
59
- function handleClose() {
60
- open = false;
61
- onclose?.();
62
- }
63
-
64
- function handleKeydown(e: KeyboardEvent) {
65
- if (e.key === 'Escape' && open) {
66
- e.stopImmediatePropagation();
67
- handleClose();
68
- }
69
- }
70
-
71
- /**
72
- * Stop keydown events from bubbling to window while the drawer is open.
73
- * This prevents page-level keyboard navigation (listNav-style handlers on
74
- * window) from reacting to arrow keys, j/k, vim chords, digit counts, etc.
75
- * typed inside the drawer.
76
- *
77
- * Free-text fields are excluded: page-level handlers already ignore them,
78
- * and we must not interfere with normal typing or textarea shortcuts.
79
- */
80
- function handlePanelKeydown(e: KeyboardEvent) {
81
- if (!trapKeys) return;
82
- const el = document.activeElement as HTMLElement | null;
83
- const tag = el?.tagName;
84
- const isTextField =
85
- !!el && (tag === 'INPUT' || tag === 'TEXTAREA' || el.isContentEditable === true);
86
- if (!isTextField) e.stopPropagation();
87
- }
88
- </script>
89
-
90
- <svelte:window onkeydown={handleKeydown} />
91
-
92
- <div
93
- class="fixed inset-0 transition-opacity duration-300 {open
94
- ? 'z-[100] opacity-100'
95
- : 'opacity-0 pointer-events-none -z-10'}"
96
- style="background: color-mix(in oklch, var(--color-primary) 55%, transparent)"
97
- onclick={open ? handleClose : undefined}
98
- role={open ? 'presentation' : undefined}
99
- ></div>
100
-
101
- <div
102
- class="fixed top-0 {position === 'right'
103
- ? 'right-0'
104
- : 'left-0'} h-full bg-base-200 {position === 'right' ? 'border-l' : 'border-r'} border-base-300 {sizeClasses[size]} flex flex-col transition-all duration-300 {open
105
- ? 'z-[101] translate-x-0'
106
- : '-z-10 ' + (position === 'right' ? 'translate-x-full' : '-translate-x-full')} {className}"
107
- role="dialog"
108
- aria-modal={open}
109
- aria-label={title}
110
- aria-hidden={!open}
111
- onkeydown={handlePanelKeydown}
112
- >
113
- {#if title || showCloseButton || headerActions}
114
- <div class="flex items-center justify-between p-6 border-b border-base-content/10 flex-shrink-0">
115
- {#if title}
116
- <h2 class="text-xl font-semibold">{title}</h2>
117
- {/if}
118
- <div class="flex items-center gap-2 ml-auto">
119
- {#if headerActions}
120
- {@render headerActions()}
121
- {/if}
122
- {#if showCloseButton}
123
- <button
124
- type="button"
125
- class="btn btn-sm btn-circle btn-ghost"
126
- onclick={handleClose}
127
- aria-label="Close drawer"
128
- >
129
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
130
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
131
- </svg>
132
- </button>
133
- {/if}
134
- </div>
135
- </div>
136
- {/if}
137
-
138
- <div class="flex-1 overflow-y-auto {contentClass}">
139
- {#if open && children}
140
- {@render children()}
141
- {/if}
142
- </div>
143
- </div>
@@ -1,159 +0,0 @@
1
- <script lang="ts">
2
- /**
3
- * EmojiPicker — Full Unicode emoji selector with search-by-name.
4
- * Shows a small trigger button with the current emoji, opens a categorized grid dropdown.
5
- * For assigning emoji icons to items (bases, channels, categories, etc.).
6
- */
7
- import { EMOJI_DATA, searchEmojis } from '../data/emojis';
8
-
9
- let {
10
- selected = null,
11
- onSelect,
12
- size = 'sm',
13
- }: {
14
- selected: string | null;
15
- onSelect: (emoji: string | null) => void;
16
- size?: 'sm' | 'md';
17
- } = $props();
18
-
19
- let open = $state(false);
20
- let searchQuery = $state('');
21
- let triggerEl: HTMLButtonElement | undefined = $state(undefined);
22
- let searchInputEl: HTMLInputElement | undefined = $state(undefined);
23
-
24
- const searchResults = $derived.by(() => {
25
- if (!searchQuery.trim()) return null;
26
- return searchEmojis(searchQuery);
27
- });
28
-
29
- function handleSelect(emoji: string) {
30
- onSelect(emoji);
31
- open = false;
32
- searchQuery = '';
33
- }
34
-
35
- function handleClear(e: MouseEvent) {
36
- e.stopPropagation();
37
- onSelect(null);
38
- open = false;
39
- }
40
-
41
- function handleClickOutside(e: MouseEvent) {
42
- if (triggerEl && !triggerEl.contains(e.target as Node)) {
43
- const dropdown = document.querySelector('.emoji-picker-dropdown');
44
- if (dropdown && !dropdown.contains(e.target as Node)) {
45
- open = false;
46
- searchQuery = '';
47
- }
48
- }
49
- }
50
-
51
- function handleToggle(e: MouseEvent) {
52
- e.stopPropagation();
53
- open = !open;
54
- if (!open) {
55
- searchQuery = '';
56
- } else {
57
- requestAnimationFrame(() => searchInputEl?.focus());
58
- }
59
- }
60
-
61
- const btnSize = $derived(size === 'sm' ? 'w-6 h-6 text-sm' : 'w-8 h-8 text-base');
62
- </script>
63
-
64
- <svelte:window onclick={handleClickOutside} />
65
-
66
- <div class="relative inline-flex">
67
- <button
68
- bind:this={triggerEl}
69
- class="emoji-trigger {btnSize} flex items-center justify-center rounded transition-all duration-100 cursor-pointer"
70
- style="background: {selected ? 'oklch(0.25 0.02 250)' : 'oklch(0.20 0.01 250)'}; border: 1px solid {open ? 'oklch(0.45 0.10 240)' : 'oklch(0.28 0.02 250)'};"
71
- onclick={handleToggle}
72
- title={selected ? `Icon: ${selected} (click to change)` : 'Set icon'}
73
- >
74
- {#if selected}
75
- <span>{selected}</span>
76
- {:else}
77
- <span style="color: oklch(0.40 0.02 250); font-size: 0.65em;">+</span>
78
- {/if}
79
- </button>
80
-
81
- {#if open}
82
- <!-- svelte-ignore a11y_click_events_have_key_events -->
83
- <!-- svelte-ignore a11y_no_static_element_interactions -->
84
- <div
85
- class="emoji-picker-dropdown absolute left-0 top-full mt-1 z-50 rounded-lg overflow-hidden"
86
- style="background: oklch(0.18 0.02 250); border: 1px solid oklch(0.30 0.02 250); box-shadow: 0 8px 32px oklch(0 0 0 / 0.5); width: 280px;"
87
- onclick={(e) => e.stopPropagation()}
88
- >
89
- <!-- Search -->
90
- <div class="px-2 py-1.5" style="border-bottom: 1px solid oklch(0.24 0.01 250);">
91
- <input
92
- bind:this={searchInputEl}
93
- type="text"
94
- placeholder="Search emojis..."
95
- bind:value={searchQuery}
96
- class="w-full px-2 py-1 rounded text-xs border-0 outline-none"
97
- style="background: oklch(0.22 0.01 250); color: oklch(0.85 0.01 250);"
98
- />
99
- </div>
100
-
101
- <!-- Emoji grid -->
102
- <div class="max-h-64 overflow-y-auto px-1.5 py-1.5">
103
- {#if searchResults}
104
- {#if searchResults.length === 0}
105
- <div class="text-center py-4 text-xs" style="color: oklch(0.45 0.02 250);">
106
- No emojis found
107
- </div>
108
- {:else}
109
- <div class="grid grid-cols-8 gap-0.5">
110
- {#each searchResults as entry}
111
- <button
112
- class="w-7 h-7 flex items-center justify-center rounded text-sm cursor-pointer transition-all duration-75 hover:scale-110"
113
- style="background: {selected === entry.char ? 'oklch(0.45 0.12 240 / 0.3)' : 'transparent'};"
114
- onclick={() => handleSelect(entry.char)}
115
- title={entry.name}
116
- >
117
- {entry.char}
118
- </button>
119
- {/each}
120
- </div>
121
- {/if}
122
- {:else}
123
- {#each EMOJI_DATA as group}
124
- <div class="mb-1.5">
125
- <div class="text-[9px] font-mono uppercase tracking-wider px-1 py-0.5 sticky top-0" style="color: oklch(0.45 0.02 250); background: oklch(0.18 0.02 250);">
126
- {group.label}
127
- </div>
128
- <div class="grid grid-cols-8 gap-0.5">
129
- {#each group.emojis as entry}
130
- <button
131
- class="w-7 h-7 flex items-center justify-center rounded text-sm cursor-pointer transition-all duration-75 hover:scale-110"
132
- style="background: {selected === entry.char ? 'oklch(0.45 0.12 240 / 0.3)' : 'transparent'};"
133
- onclick={() => handleSelect(entry.char)}
134
- title={entry.name}
135
- >
136
- {entry.char}
137
- </button>
138
- {/each}
139
- </div>
140
- </div>
141
- {/each}
142
- {/if}
143
- </div>
144
-
145
- <!-- Clear button -->
146
- {#if selected}
147
- <div class="px-2 py-1.5" style="border-top: 1px solid oklch(0.24 0.01 250);">
148
- <button
149
- class="w-full text-center text-[10px] py-1 rounded cursor-pointer transition-colors duration-100"
150
- style="color: oklch(0.55 0.02 250); background: transparent;"
151
- onclick={handleClear}
152
- >
153
- Clear icon
154
- </button>
155
- </div>
156
- {/if}
157
- </div>
158
- {/if}
159
- </div>
@@ -1,104 +0,0 @@
1
- <script lang="ts">
2
- import type { EntityPreviewData, EntityPreviewBadge } from "../types/entityPreview"
3
-
4
- let {
5
- data,
6
- loading = false,
7
- error = false,
8
- }: {
9
- data: EntityPreviewData | null
10
- loading?: boolean
11
- error?: boolean
12
- } = $props()
13
-
14
- // First letter monogram fallback when there's no image.
15
- const monogram = $derived((data?.title ?? "?").trim().charAt(0).toUpperCase() || "?")
16
-
17
- function toneClass(tone: EntityPreviewBadge["tone"]): string {
18
- switch (tone) {
19
- case "primary":
20
- return "badge-primary"
21
- case "info":
22
- return "badge-info"
23
- case "success":
24
- return "badge-success"
25
- case "warning":
26
- return "badge-warning"
27
- case "error":
28
- return "badge-error"
29
- default:
30
- return "badge-ghost"
31
- }
32
- }
33
- </script>
34
-
35
- <div
36
- class="bg-base-100 border-base-content/10 rounded-box w-72 overflow-hidden border text-left"
37
- >
38
- {#if loading}
39
- <div class="space-y-2 p-4">
40
- <div class="bg-base-300 h-3 w-20 animate-pulse rounded"></div>
41
- <div class="bg-base-300 h-4 w-40 animate-pulse rounded"></div>
42
- <div class="bg-base-300 h-3 w-full animate-pulse rounded"></div>
43
- </div>
44
- {:else if error || !data}
45
- <div class="text-base-content/50 p-4 text-sm">Preview unavailable.</div>
46
- {:else}
47
- {#if data.imageUrl}
48
- <div class="bg-base-200 aspect-[16/9] w-full overflow-hidden">
49
- <img src={data.imageUrl} alt={data.title} class="size-full object-cover" />
50
- </div>
51
- {/if}
52
-
53
- <div class="space-y-2 p-4">
54
- <div class="flex items-start gap-3">
55
- {#if !data.imageUrl}
56
- <div
57
- class="bg-primary/10 text-primary rounded-box flex size-9 shrink-0 items-center justify-center text-sm font-bold"
58
- aria-hidden="true"
59
- >
60
- {monogram}
61
- </div>
62
- {/if}
63
- <div class="min-w-0 flex-1">
64
- <div class="text-base-content/45 text-[10px] font-medium uppercase tracking-wide">
65
- {data.typeLabel}
66
- </div>
67
- <div class="truncate text-sm font-semibold leading-tight">
68
- {#if data.href}
69
- <a href={data.href} class="hover:underline">{data.title}</a>
70
- {:else}
71
- {data.title}
72
- {/if}
73
- </div>
74
- {#if data.subtitle}
75
- <div class="text-base-content/55 truncate text-xs">{data.subtitle}</div>
76
- {/if}
77
- </div>
78
- </div>
79
-
80
- {#if data.summary}
81
- <p class="text-base-content/70 line-clamp-2 text-xs">{data.summary}</p>
82
- {/if}
83
-
84
- {#if data.badges && data.badges.length > 0}
85
- <div class="flex flex-wrap gap-1">
86
- {#each data.badges as badge}
87
- <span class="badge {toneClass(badge.tone)} badge-sm capitalize">{badge.label}</span>
88
- {/each}
89
- </div>
90
- {/if}
91
-
92
- {#if data.stats && data.stats.length > 0}
93
- <div class="border-base-content/10 grid grid-cols-2 gap-x-4 gap-y-1 border-t pt-2">
94
- {#each data.stats.slice(0, 4) as stat}
95
- <div>
96
- <div class="text-base-content/45 text-[10px] uppercase tracking-wide">{stat.label}</div>
97
- <div class="text-xs font-semibold tabular-nums">{stat.value}</div>
98
- </div>
99
- {/each}
100
- </div>
101
- {/if}
102
- </div>
103
- {/if}
104
- </div>