@joewinke/jatui 0.1.7 → 0.1.10
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/package.json +1 -1
- package/src/lib/components/ChipInput.svelte +1 -0
- package/src/lib/components/ConfirmModal.svelte +1 -1
- package/src/lib/components/FilterDropdown.svelte +1 -0
- package/src/lib/components/FloatingActionBar.svelte +354 -0
- package/src/lib/components/ImageUpload.svelte +1 -0
- package/src/lib/components/MilestoneCard.svelte +19 -1
- package/src/lib/components/MilestoneTimeline.svelte +9 -1
- package/src/lib/components/ResizableDivider.svelte +3 -1
- package/src/lib/components/SearchDropdown.svelte +44 -3
- package/src/lib/components/SignaturePad.svelte +2 -2
- package/src/lib/components/TaskTypeIcon.svelte +54 -0
- package/src/lib/components/messaging/ChannelInfoModal.svelte +4 -4
- package/src/lib/components/messaging/CreateChannelModal.svelte +7 -7
- package/src/lib/components/messaging/NotificationSettingsModal.svelte +6 -6
- package/src/lib/index.ts +3 -0
- package/src/lib/utils/taskUtils.ts +12 -8
package/package.json
CHANGED
|
@@ -396,6 +396,7 @@
|
|
|
396
396
|
bind:this={editableRef}
|
|
397
397
|
contenteditable={disabled ? 'false' : 'true'}
|
|
398
398
|
role="textbox"
|
|
399
|
+
tabindex={disabled ? -1 : 0}
|
|
399
400
|
aria-multiline="true"
|
|
400
401
|
class="chip-input-editable w-full rounded-md text-sm"
|
|
401
402
|
class:chip-input-monospace={monospace}
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
<div class="modal-box max-w-md">
|
|
35
35
|
<div class="flex justify-between items-center mb-4">
|
|
36
36
|
<h3 class="font-bold text-lg">{title}</h3>
|
|
37
|
-
<button class="btn btn-ghost btn-sm" onclick={cancel}>
|
|
37
|
+
<button class="btn btn-ghost btn-sm" onclick={cancel} aria-label="Close">
|
|
38
38
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
|
39
39
|
</button>
|
|
40
40
|
</div>
|
|
@@ -155,6 +155,7 @@
|
|
|
155
155
|
<!-- Dropdown Content -->
|
|
156
156
|
{#if style === 'checkbox'}
|
|
157
157
|
<!-- Checkbox style: Menu with checkboxes -->
|
|
158
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
158
159
|
<ul
|
|
159
160
|
tabindex="0"
|
|
160
161
|
class="dropdown-content z-40 menu p-2 shadow rounded-box {menuWidth} {maxHeight ? maxHeight + ' overflow-y-auto' : ''} bg-base-200 border border-base-300"
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* FloatingActionBar Component
|
|
4
|
+
* Fixed bottom-center bar for bulk selection actions on tables/lists.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* <FloatingActionBar
|
|
8
|
+
* count={selectedTasks.size}
|
|
9
|
+
* {actions}
|
|
10
|
+
* {dropdowns}
|
|
11
|
+
* loading={bulkActionLoading}
|
|
12
|
+
* onClear={clearSelection}
|
|
13
|
+
* />
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { fade } from 'svelte/transition';
|
|
17
|
+
|
|
18
|
+
export interface ActionButton {
|
|
19
|
+
/** Unique key */
|
|
20
|
+
id: string;
|
|
21
|
+
/** Button label */
|
|
22
|
+
label: string;
|
|
23
|
+
/** SVG path string for a 24×24 icon (optional) */
|
|
24
|
+
icon?: string;
|
|
25
|
+
/** Color variant */
|
|
26
|
+
variant?: 'default' | 'primary' | 'danger' | 'warning' | 'success';
|
|
27
|
+
/** Called when clicked */
|
|
28
|
+
onclick: () => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface DropdownItem {
|
|
32
|
+
/** Display label */
|
|
33
|
+
label: string;
|
|
34
|
+
/** Optional colored dot (oklch string) */
|
|
35
|
+
dotColor?: string;
|
|
36
|
+
/** Optional SVG snippet rendered before the label */
|
|
37
|
+
iconSnippet?: import('svelte').Snippet;
|
|
38
|
+
/** Called when this item is clicked */
|
|
39
|
+
onclick: () => void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ActionDropdown {
|
|
43
|
+
/** Unique key */
|
|
44
|
+
id: string;
|
|
45
|
+
/** Button label */
|
|
46
|
+
label: string;
|
|
47
|
+
/** SVG path string for a 24×24 icon (optional) */
|
|
48
|
+
icon?: string;
|
|
49
|
+
/** Color variant */
|
|
50
|
+
variant?: 'default' | 'primary' | 'info' | 'purple';
|
|
51
|
+
/** Items in the dropdown */
|
|
52
|
+
items: DropdownItem[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface Props {
|
|
56
|
+
/** Number of selected items — bar is hidden when 0 */
|
|
57
|
+
count: number;
|
|
58
|
+
/** Primary action buttons */
|
|
59
|
+
actions?: ActionButton[];
|
|
60
|
+
/** Dropdown menus (open upward from the bar) */
|
|
61
|
+
dropdowns?: ActionDropdown[];
|
|
62
|
+
/** Disables all buttons while true and shows a spinner */
|
|
63
|
+
loading?: boolean;
|
|
64
|
+
/** Called when the "Clear" button is clicked */
|
|
65
|
+
onClear?: () => void;
|
|
66
|
+
/** Label for the selection count (default: "selected") */
|
|
67
|
+
countLabel?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let {
|
|
71
|
+
count,
|
|
72
|
+
actions = [],
|
|
73
|
+
dropdowns = [],
|
|
74
|
+
loading = false,
|
|
75
|
+
onClear,
|
|
76
|
+
countLabel = 'selected',
|
|
77
|
+
}: Props = $props();
|
|
78
|
+
|
|
79
|
+
let openDropdownId = $state<string | null>(null);
|
|
80
|
+
|
|
81
|
+
// Close dropdown on outside click
|
|
82
|
+
$effect(() => {
|
|
83
|
+
if (!openDropdownId) return;
|
|
84
|
+
function handleClick(e: MouseEvent) {
|
|
85
|
+
if (!(e.target as HTMLElement).closest('.fab-dropdown-wrapper')) {
|
|
86
|
+
openDropdownId = null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const timer = setTimeout(() => document.addEventListener('click', handleClick), 0);
|
|
90
|
+
return () => {
|
|
91
|
+
clearTimeout(timer);
|
|
92
|
+
document.removeEventListener('click', handleClick);
|
|
93
|
+
};
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
function variantClass(variant: ActionButton['variant'] | ActionDropdown['variant']): string {
|
|
97
|
+
switch (variant) {
|
|
98
|
+
case 'primary': return 'fab-btn-primary';
|
|
99
|
+
case 'danger': return 'fab-btn-danger';
|
|
100
|
+
case 'warning': return 'fab-btn-warning';
|
|
101
|
+
case 'success': return 'fab-btn-success';
|
|
102
|
+
case 'info': return 'fab-btn-info';
|
|
103
|
+
case 'purple': return 'fab-btn-purple';
|
|
104
|
+
default: return 'fab-btn-default';
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
</script>
|
|
108
|
+
|
|
109
|
+
{#if count > 0}
|
|
110
|
+
<div class="fab" transition:fade={{ duration: 150 }}>
|
|
111
|
+
<span class="fab-count">{count} {countLabel}</span>
|
|
112
|
+
|
|
113
|
+
{#if actions.length > 0}
|
|
114
|
+
<div class="fab-divider"></div>
|
|
115
|
+
{#each actions as action}
|
|
116
|
+
<button
|
|
117
|
+
type="button"
|
|
118
|
+
class="fab-btn {variantClass(action.variant)}"
|
|
119
|
+
onclick={action.onclick}
|
|
120
|
+
disabled={loading}
|
|
121
|
+
>
|
|
122
|
+
{#if action.icon}
|
|
123
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
|
|
124
|
+
<!-- svelte-ignore a11y_consider_explicit_label -->
|
|
125
|
+
{@html action.icon}
|
|
126
|
+
</svg>
|
|
127
|
+
{/if}
|
|
128
|
+
{action.label}
|
|
129
|
+
</button>
|
|
130
|
+
{/each}
|
|
131
|
+
{/if}
|
|
132
|
+
|
|
133
|
+
{#if dropdowns.length > 0}
|
|
134
|
+
<div class="fab-divider"></div>
|
|
135
|
+
{#each dropdowns as dropdown}
|
|
136
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
137
|
+
<div
|
|
138
|
+
class="fab-dropdown-wrapper"
|
|
139
|
+
role="group"
|
|
140
|
+
onclick={(e) => e.stopPropagation()}
|
|
141
|
+
onkeydown={(e) => e.stopPropagation()}
|
|
142
|
+
>
|
|
143
|
+
<button
|
|
144
|
+
type="button"
|
|
145
|
+
class="fab-btn {variantClass(dropdown.variant)}"
|
|
146
|
+
onclick={() => openDropdownId = openDropdownId === dropdown.id ? null : dropdown.id}
|
|
147
|
+
disabled={loading}
|
|
148
|
+
>
|
|
149
|
+
{#if dropdown.icon}
|
|
150
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
|
|
151
|
+
<!-- svelte-ignore a11y_consider_explicit_label -->
|
|
152
|
+
{@html dropdown.icon}
|
|
153
|
+
</svg>
|
|
154
|
+
{/if}
|
|
155
|
+
{dropdown.label}
|
|
156
|
+
</button>
|
|
157
|
+
{#if openDropdownId === dropdown.id}
|
|
158
|
+
<div class="fab-dropdown" transition:fade={{ duration: 100 }}>
|
|
159
|
+
{#each dropdown.items as item}
|
|
160
|
+
<button
|
|
161
|
+
class="fab-dropdown-item"
|
|
162
|
+
onclick={() => { item.onclick(); openDropdownId = null; }}
|
|
163
|
+
>
|
|
164
|
+
{#if item.dotColor}
|
|
165
|
+
<span class="fab-dot" style="background: {item.dotColor};"></span>
|
|
166
|
+
{/if}
|
|
167
|
+
{#if item.iconSnippet}
|
|
168
|
+
{@render item.iconSnippet()}
|
|
169
|
+
{/if}
|
|
170
|
+
{item.label}
|
|
171
|
+
</button>
|
|
172
|
+
{/each}
|
|
173
|
+
</div>
|
|
174
|
+
{/if}
|
|
175
|
+
</div>
|
|
176
|
+
{/each}
|
|
177
|
+
{/if}
|
|
178
|
+
|
|
179
|
+
{#if onClear}
|
|
180
|
+
<div class="fab-divider"></div>
|
|
181
|
+
<button type="button" class="fab-btn fab-btn-clear" onclick={onClear} disabled={loading}>
|
|
182
|
+
Clear
|
|
183
|
+
</button>
|
|
184
|
+
{/if}
|
|
185
|
+
|
|
186
|
+
{#if loading}
|
|
187
|
+
<div class="fab-spinner"></div>
|
|
188
|
+
{/if}
|
|
189
|
+
</div>
|
|
190
|
+
{/if}
|
|
191
|
+
|
|
192
|
+
<style>
|
|
193
|
+
.fab {
|
|
194
|
+
position: fixed;
|
|
195
|
+
bottom: 1.5rem;
|
|
196
|
+
left: 50%;
|
|
197
|
+
transform: translateX(-50%);
|
|
198
|
+
z-index: 50;
|
|
199
|
+
display: flex;
|
|
200
|
+
align-items: center;
|
|
201
|
+
gap: 0.5rem;
|
|
202
|
+
padding: 0.5rem 0.75rem;
|
|
203
|
+
background: oklch(0.16 0.02 250);
|
|
204
|
+
border: 1px solid oklch(0.30 0.02 250);
|
|
205
|
+
border-radius: 0.75rem;
|
|
206
|
+
box-shadow: 0 8px 32px oklch(0.05 0 0 / 0.6), 0 2px 8px oklch(0.05 0 0 / 0.3);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.fab-count {
|
|
210
|
+
font-size: 0.8125rem;
|
|
211
|
+
font-weight: 600;
|
|
212
|
+
color: oklch(0.85 0.02 250);
|
|
213
|
+
white-space: nowrap;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.fab-divider {
|
|
217
|
+
width: 1px;
|
|
218
|
+
height: 1.25rem;
|
|
219
|
+
background: oklch(0.30 0.02 250);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.fab-btn {
|
|
223
|
+
display: flex;
|
|
224
|
+
align-items: center;
|
|
225
|
+
gap: 0.375rem;
|
|
226
|
+
padding: 0.375rem 0.625rem;
|
|
227
|
+
border: 1px solid transparent;
|
|
228
|
+
border-radius: 0.375rem;
|
|
229
|
+
font-size: 0.75rem;
|
|
230
|
+
font-weight: 500;
|
|
231
|
+
cursor: pointer;
|
|
232
|
+
transition: all 0.15s;
|
|
233
|
+
white-space: nowrap;
|
|
234
|
+
}
|
|
235
|
+
.fab-btn:disabled {
|
|
236
|
+
opacity: 0.5;
|
|
237
|
+
pointer-events: none;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/* Variants */
|
|
241
|
+
.fab-btn-default {
|
|
242
|
+
background: oklch(0.25 0.02 250 / 0.5);
|
|
243
|
+
color: oklch(0.75 0.02 250);
|
|
244
|
+
border-color: oklch(0.35 0.02 250);
|
|
245
|
+
}
|
|
246
|
+
.fab-btn-default:hover { background: oklch(0.30 0.02 250); }
|
|
247
|
+
|
|
248
|
+
.fab-btn-primary {
|
|
249
|
+
background: oklch(0.65 0.18 250 / 0.15);
|
|
250
|
+
color: oklch(0.82 0.14 250);
|
|
251
|
+
border-color: oklch(0.65 0.18 250 / 0.3);
|
|
252
|
+
}
|
|
253
|
+
.fab-btn-primary:hover { background: oklch(0.65 0.18 250 / 0.25); }
|
|
254
|
+
|
|
255
|
+
.fab-btn-danger {
|
|
256
|
+
background: oklch(0.55 0.18 30 / 0.15);
|
|
257
|
+
color: oklch(0.80 0.15 30);
|
|
258
|
+
border-color: oklch(0.55 0.18 30 / 0.3);
|
|
259
|
+
}
|
|
260
|
+
.fab-btn-danger:hover { background: oklch(0.55 0.18 30 / 0.25); }
|
|
261
|
+
|
|
262
|
+
.fab-btn-warning {
|
|
263
|
+
background: oklch(0.75 0.15 85 / 0.12);
|
|
264
|
+
color: oklch(0.80 0.12 85);
|
|
265
|
+
border-color: oklch(0.75 0.15 85 / 0.25);
|
|
266
|
+
}
|
|
267
|
+
.fab-btn-warning:hover { background: oklch(0.75 0.15 85 / 0.22); }
|
|
268
|
+
|
|
269
|
+
.fab-btn-success {
|
|
270
|
+
background: oklch(0.55 0.15 145 / 0.15);
|
|
271
|
+
color: oklch(0.80 0.12 145);
|
|
272
|
+
border-color: oklch(0.55 0.15 145 / 0.3);
|
|
273
|
+
}
|
|
274
|
+
.fab-btn-success:hover { background: oklch(0.55 0.15 145 / 0.25); }
|
|
275
|
+
|
|
276
|
+
.fab-btn-info {
|
|
277
|
+
background: oklch(0.55 0.15 200 / 0.15);
|
|
278
|
+
color: oklch(0.80 0.12 200);
|
|
279
|
+
border-color: oklch(0.55 0.15 200 / 0.3);
|
|
280
|
+
}
|
|
281
|
+
.fab-btn-info:hover { background: oklch(0.55 0.15 200 / 0.25); }
|
|
282
|
+
|
|
283
|
+
.fab-btn-purple {
|
|
284
|
+
background: oklch(0.55 0.15 300 / 0.15);
|
|
285
|
+
color: oklch(0.80 0.12 300);
|
|
286
|
+
border-color: oklch(0.55 0.15 300 / 0.3);
|
|
287
|
+
}
|
|
288
|
+
.fab-btn-purple:hover { background: oklch(0.55 0.15 300 / 0.25); }
|
|
289
|
+
|
|
290
|
+
.fab-btn-clear {
|
|
291
|
+
background: transparent;
|
|
292
|
+
color: oklch(0.65 0.02 250);
|
|
293
|
+
border-color: transparent;
|
|
294
|
+
}
|
|
295
|
+
.fab-btn-clear:hover {
|
|
296
|
+
background: oklch(0.25 0.02 250);
|
|
297
|
+
color: oklch(0.80 0.02 250);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.fab-spinner {
|
|
301
|
+
width: 14px;
|
|
302
|
+
height: 14px;
|
|
303
|
+
border: 2px solid oklch(0.35 0.02 250);
|
|
304
|
+
border-top-color: oklch(0.70 0.15 240);
|
|
305
|
+
border-radius: 50%;
|
|
306
|
+
animation: fab-spin 0.6s linear infinite;
|
|
307
|
+
flex-shrink: 0;
|
|
308
|
+
}
|
|
309
|
+
@keyframes fab-spin {
|
|
310
|
+
to { transform: rotate(360deg); }
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/* Dropdown */
|
|
314
|
+
.fab-dropdown-wrapper {
|
|
315
|
+
position: relative;
|
|
316
|
+
}
|
|
317
|
+
.fab-dropdown {
|
|
318
|
+
position: absolute;
|
|
319
|
+
bottom: calc(100% + 8px);
|
|
320
|
+
left: 50%;
|
|
321
|
+
transform: translateX(-50%);
|
|
322
|
+
min-width: 160px;
|
|
323
|
+
background: oklch(0.18 0.02 250);
|
|
324
|
+
border: 1px solid oklch(0.30 0.02 250);
|
|
325
|
+
border-radius: 0.5rem;
|
|
326
|
+
padding: 0.375rem;
|
|
327
|
+
box-shadow: 0 8px 24px oklch(0.05 0 0 / 0.5);
|
|
328
|
+
z-index: 60;
|
|
329
|
+
}
|
|
330
|
+
.fab-dropdown-item {
|
|
331
|
+
display: flex;
|
|
332
|
+
align-items: center;
|
|
333
|
+
gap: 0.5rem;
|
|
334
|
+
width: 100%;
|
|
335
|
+
padding: 0.375rem 0.625rem;
|
|
336
|
+
border: none;
|
|
337
|
+
background: transparent;
|
|
338
|
+
color: oklch(0.80 0.02 250);
|
|
339
|
+
font-size: 0.75rem;
|
|
340
|
+
text-align: left;
|
|
341
|
+
border-radius: 0.375rem;
|
|
342
|
+
cursor: pointer;
|
|
343
|
+
transition: background 0.1s;
|
|
344
|
+
white-space: nowrap;
|
|
345
|
+
}
|
|
346
|
+
.fab-dropdown-item:hover { background: oklch(0.25 0.02 250); }
|
|
347
|
+
|
|
348
|
+
.fab-dot {
|
|
349
|
+
width: 8px;
|
|
350
|
+
height: 8px;
|
|
351
|
+
border-radius: 50%;
|
|
352
|
+
flex-shrink: 0;
|
|
353
|
+
}
|
|
354
|
+
</style>
|
|
@@ -65,6 +65,7 @@
|
|
|
65
65
|
class="btn btn-sm btn-circle absolute top-2 right-2 bg-base-100/80"
|
|
66
66
|
onclick={clearImage}
|
|
67
67
|
{disabled}
|
|
68
|
+
aria-label="Remove image"
|
|
68
69
|
>
|
|
69
70
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
|
70
71
|
</button>
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Milestone, MilestoneStatus } from '../types/milestone'
|
|
3
3
|
|
|
4
|
+
interface LinkedTask {
|
|
5
|
+
id: string
|
|
6
|
+
title: string
|
|
7
|
+
status: string
|
|
8
|
+
}
|
|
9
|
+
|
|
4
10
|
interface Props {
|
|
5
11
|
milestone: Milestone
|
|
12
|
+
tasks?: LinkedTask[]
|
|
6
13
|
isCurrent?: boolean
|
|
7
14
|
currency?: string
|
|
8
15
|
onAcceptAndPay?: (milestone: Milestone) => void
|
|
9
16
|
}
|
|
10
17
|
|
|
11
|
-
let { milestone, isCurrent = false, currency = 'USD', onAcceptAndPay }: Props = $props()
|
|
18
|
+
let { milestone, tasks = [], isCurrent = false, currency = 'USD', onAcceptAndPay }: Props = $props()
|
|
12
19
|
|
|
13
20
|
const statusConfig: Record<MilestoneStatus, { label: string; class: string; icon: string }> = {
|
|
14
21
|
pending: { label: 'Pending', class: 'badge-ghost', icon: '○' },
|
|
@@ -59,6 +66,17 @@
|
|
|
59
66
|
</div>
|
|
60
67
|
{/if}
|
|
61
68
|
|
|
69
|
+
{#if tasks.length > 0}
|
|
70
|
+
<div class="flex flex-wrap gap-1 mt-2">
|
|
71
|
+
{#each tasks as task}
|
|
72
|
+
<span class="badge badge-xs badge-outline gap-1">
|
|
73
|
+
<span class="w-1.5 h-1.5 rounded-full {task.status === 'deployed' || task.status === 'accepted' || task.status === 'closed' ? 'bg-success' : task.status === 'in_progress' || task.status === 'submitted' ? 'bg-warning' : 'bg-base-300'}"></span>
|
|
74
|
+
{task.title}
|
|
75
|
+
</span>
|
|
76
|
+
{/each}
|
|
77
|
+
</div>
|
|
78
|
+
{/if}
|
|
79
|
+
|
|
62
80
|
<div class="flex items-center gap-3 text-xs text-base-content/40 mt-1">
|
|
63
81
|
<span>{milestone.percentage}% of total</span>
|
|
64
82
|
{#if milestone.delivered_at}
|
|
@@ -2,13 +2,20 @@
|
|
|
2
2
|
import type { Milestone } from '../types/milestone'
|
|
3
3
|
import MilestoneCard from './MilestoneCard.svelte'
|
|
4
4
|
|
|
5
|
+
interface LinkedTask {
|
|
6
|
+
id: string
|
|
7
|
+
title: string
|
|
8
|
+
status: string
|
|
9
|
+
}
|
|
10
|
+
|
|
5
11
|
interface Props {
|
|
6
12
|
milestones: Milestone[]
|
|
13
|
+
linkedTasks?: Record<string, LinkedTask[]>
|
|
7
14
|
currency?: string
|
|
8
15
|
onAcceptAndPay?: (milestone: Milestone) => void
|
|
9
16
|
}
|
|
10
17
|
|
|
11
|
-
let { milestones, currency = 'USD', onAcceptAndPay }: Props = $props()
|
|
18
|
+
let { milestones, linkedTasks = {}, currency = 'USD', onAcceptAndPay }: Props = $props()
|
|
12
19
|
|
|
13
20
|
const sorted = $derived([...milestones].sort((a, b) => a.sort_order - b.sort_order))
|
|
14
21
|
|
|
@@ -72,6 +79,7 @@
|
|
|
72
79
|
<div class="flex-1 pb-4">
|
|
73
80
|
<MilestoneCard
|
|
74
81
|
{milestone}
|
|
82
|
+
tasks={linkedTasks[milestone.id] || []}
|
|
75
83
|
isCurrent={i === currentIndex}
|
|
76
84
|
{currency}
|
|
77
85
|
{onAcceptAndPay}
|
|
@@ -121,6 +121,8 @@
|
|
|
121
121
|
const isExpanded = $derived(isDragging || isHovering || isNearby);
|
|
122
122
|
</script>
|
|
123
123
|
|
|
124
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
125
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
124
126
|
<div
|
|
125
127
|
bind:this={dividerElement}
|
|
126
128
|
class="divider-container flex items-center justify-center select-none {className} {isCollapsed ? 'cursor-pointer divider-collapsed' : 'cursor-row-resize'} {isDragging ? 'bg-primary/25' : isExpanded && isCollapsed ? 'bg-primary/40' : isExpanded ? 'bg-primary/15' : ''}"
|
|
@@ -128,7 +130,7 @@
|
|
|
128
130
|
class:dragging={isDragging}
|
|
129
131
|
role="separator"
|
|
130
132
|
aria-orientation="horizontal"
|
|
131
|
-
aria-
|
|
133
|
+
aria-label={isCollapsed ? 'Expand panel' : 'Resize panel'}
|
|
132
134
|
tabindex="0"
|
|
133
135
|
onmousedown={handleMouseDown}
|
|
134
136
|
onmouseenter={handleMouseEnter}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
value: string;
|
|
4
4
|
label: string;
|
|
5
5
|
icon?: string;
|
|
6
|
+
image?: string; // URL for avatar/thumbnail image
|
|
6
7
|
};
|
|
7
8
|
|
|
8
9
|
export type SearchDropdownGroup = {
|
|
@@ -53,6 +54,7 @@
|
|
|
53
54
|
|
|
54
55
|
const triggerLabel = $derived(displayValue || selectedOption?.label || value || placeholder);
|
|
55
56
|
const triggerIcon = $derived(selectedOption?.icon || '');
|
|
57
|
+
const triggerImage = $derived(selectedOption?.image || '');
|
|
56
58
|
const activeColor = $derived(colorFn ? colorFn(value) : undefined);
|
|
57
59
|
|
|
58
60
|
const filteredGroups = $derived.by(() => {
|
|
@@ -68,6 +70,13 @@
|
|
|
68
70
|
return result;
|
|
69
71
|
});
|
|
70
72
|
|
|
73
|
+
// Generate a deterministic hue from a string for initials avatars
|
|
74
|
+
function nameHue(s: string): number {
|
|
75
|
+
let h = 0;
|
|
76
|
+
for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) & 0xffff;
|
|
77
|
+
return h % 360;
|
|
78
|
+
}
|
|
79
|
+
|
|
71
80
|
function select(optionValue: string) {
|
|
72
81
|
open = false;
|
|
73
82
|
searchQuery = '';
|
|
@@ -100,7 +109,15 @@
|
|
|
100
109
|
{disabled}
|
|
101
110
|
>
|
|
102
111
|
<span class="sd-trigger-label">
|
|
103
|
-
{#if
|
|
112
|
+
{#if triggerImage}
|
|
113
|
+
<img src={triggerImage} alt="" class="sd-avatar" />
|
|
114
|
+
{:else if triggerIcon}
|
|
115
|
+
<span class="sd-trigger-icon">{triggerIcon}</span>
|
|
116
|
+
{:else if selectedOption && selectedOption.value}
|
|
117
|
+
<span class="sd-avatar sd-avatar-initials" style="background:oklch(0.55 0.15 {nameHue(selectedOption.label)})">
|
|
118
|
+
{selectedOption.label.charAt(0).toUpperCase()}
|
|
119
|
+
</span>
|
|
120
|
+
{/if}
|
|
104
121
|
<span class="truncate">{triggerLabel}</span>
|
|
105
122
|
</span>
|
|
106
123
|
<svg class="sd-chevron" class:sd-chevron-open={open} fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
@@ -131,7 +148,7 @@
|
|
|
131
148
|
autocomplete="off"
|
|
132
149
|
/>
|
|
133
150
|
{#if searchQuery}
|
|
134
|
-
<button type="button" onclick={() => { searchQuery = ''; searchInput?.focus(); }} class="sd-search-clear">
|
|
151
|
+
<button type="button" onclick={() => { searchQuery = ''; searchInput?.focus(); }} class="sd-search-clear" aria-label="Clear search">
|
|
135
152
|
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
|
|
136
153
|
</button>
|
|
137
154
|
{/if}
|
|
@@ -154,7 +171,15 @@
|
|
|
154
171
|
class:sd-option-selected={value === option.value}
|
|
155
172
|
style={colorFn && colorFn(option.value) ? `border-left-color: ${colorFn(option.value)}; color: ${colorFn(option.value)};` : ''}
|
|
156
173
|
>
|
|
157
|
-
{#if option.
|
|
174
|
+
{#if option.image}
|
|
175
|
+
<img src={option.image} alt="" class="sd-avatar" />
|
|
176
|
+
{:else if option.icon}
|
|
177
|
+
<span class="sd-option-icon">{option.icon}</span>
|
|
178
|
+
{:else if option.value}
|
|
179
|
+
<span class="sd-avatar sd-avatar-initials" style="background:oklch(0.55 0.15 {nameHue(option.label)})">
|
|
180
|
+
{option.label.charAt(0).toUpperCase()}
|
|
181
|
+
</span>
|
|
182
|
+
{/if}
|
|
158
183
|
<span class="truncate">{option.label}</span>
|
|
159
184
|
{#if value === option.value}
|
|
160
185
|
<svg class="sd-check" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" /></svg>
|
|
@@ -227,6 +252,22 @@
|
|
|
227
252
|
flex-shrink: 0;
|
|
228
253
|
font-size: 0.75rem;
|
|
229
254
|
}
|
|
255
|
+
.sd-avatar {
|
|
256
|
+
width: 1.25rem;
|
|
257
|
+
height: 1.25rem;
|
|
258
|
+
border-radius: 50%;
|
|
259
|
+
object-fit: cover;
|
|
260
|
+
flex-shrink: 0;
|
|
261
|
+
}
|
|
262
|
+
.sd-avatar-initials {
|
|
263
|
+
display: inline-flex;
|
|
264
|
+
align-items: center;
|
|
265
|
+
justify-content: center;
|
|
266
|
+
font-size: 0.6rem;
|
|
267
|
+
font-weight: 700;
|
|
268
|
+
color: #fff;
|
|
269
|
+
line-height: 1;
|
|
270
|
+
}
|
|
230
271
|
|
|
231
272
|
.sd-chevron {
|
|
232
273
|
width: 0.75rem;
|
|
@@ -136,12 +136,12 @@
|
|
|
136
136
|
<div class="signature-pad-container">
|
|
137
137
|
{#if label}
|
|
138
138
|
<div class="flex justify-between items-center mb-2">
|
|
139
|
-
<
|
|
139
|
+
<span class="text-sm text-base-content/70">
|
|
140
140
|
{label}
|
|
141
141
|
{#if required}
|
|
142
142
|
<span class="text-error">*</span>
|
|
143
143
|
{/if}
|
|
144
|
-
</
|
|
144
|
+
</span>
|
|
145
145
|
<button type="button" onclick={() => clear()} class="btn btn-ghost btn-xs">
|
|
146
146
|
Clear
|
|
147
147
|
</button>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Stroke icon for a task issue_type value.
|
|
4
|
+
* Replaces the emoji returned by `typeIcon()`.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* <TaskTypeIcon type={task.issue_type} />
|
|
8
|
+
* <TaskTypeIcon type="bug" size={14} class="text-base-content/60" />
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
type: string | null | undefined
|
|
13
|
+
size?: number
|
|
14
|
+
class?: string
|
|
15
|
+
title?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let { type, size = 14, class: className = "", title }: Props = $props()
|
|
19
|
+
|
|
20
|
+
// Path data per type. Single-path lucide-style strokes so they inherit
|
|
21
|
+
// currentColor and size cleanly inside dense tables.
|
|
22
|
+
const paths: Record<string, string> = {
|
|
23
|
+
// Bug: body + antennae/legs
|
|
24
|
+
bug: "M12 4v2M8 6l1 2M16 6l-1 2M6 12H4M20 12h-2M6 16l-2 1M18 16l2 1M9 9h6v5a3 3 0 11-6 0V9z",
|
|
25
|
+
// Feature (spark): 4-point star
|
|
26
|
+
feature: "M12 3v4M12 17v4M3 12h4M17 12h4M5.5 5.5l2.5 2.5M16 16l2.5 2.5M5.5 18.5L8 16M16 8l2.5-2.5",
|
|
27
|
+
// Task: checklist
|
|
28
|
+
task: "M9 4h9a1 1 0 011 1v14a1 1 0 01-1 1H6a1 1 0 01-1-1V5a1 1 0 011-1h1M9 3h6v2H9zM8.5 11.5l1.5 1.5 3-3M8.5 16.5l1.5 1.5 3-3",
|
|
29
|
+
// Epic: mountain peaks
|
|
30
|
+
epic: "M3 19l5-8 4 6 3-4 6 6H3z",
|
|
31
|
+
// Default: document
|
|
32
|
+
document: "M13 3H6a1 1 0 00-1 1v16a1 1 0 001 1h12a1 1 0 001-1V9l-6-6zM13 3v6h6",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const d = $derived(paths[type ?? ""] ?? paths.document)
|
|
36
|
+
const label = $derived(title ?? (type ? `${type[0].toUpperCase()}${type.slice(1)}` : "Document"))
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<svg
|
|
40
|
+
viewBox="0 0 24 24"
|
|
41
|
+
fill="none"
|
|
42
|
+
stroke="currentColor"
|
|
43
|
+
stroke-width="2"
|
|
44
|
+
stroke-linecap="round"
|
|
45
|
+
stroke-linejoin="round"
|
|
46
|
+
width={size}
|
|
47
|
+
height={size}
|
|
48
|
+
class={className}
|
|
49
|
+
role="img"
|
|
50
|
+
aria-label={label}
|
|
51
|
+
>
|
|
52
|
+
<title>{label}</title>
|
|
53
|
+
<path d={d} />
|
|
54
|
+
</svg>
|
|
@@ -96,12 +96,12 @@
|
|
|
96
96
|
{#if isEditing}
|
|
97
97
|
<div class="space-y-4">
|
|
98
98
|
<div>
|
|
99
|
-
<label class="label"><span class="label-text font-medium">Name</span></label>
|
|
100
|
-
<input type="text" bind:value={editName} class="input input-bordered w-full" maxlength="80" />
|
|
99
|
+
<label for="channel-info-name" class="label"><span class="label-text font-medium">Name</span></label>
|
|
100
|
+
<input id="channel-info-name" type="text" bind:value={editName} class="input input-bordered w-full" maxlength="80" />
|
|
101
101
|
</div>
|
|
102
102
|
<div>
|
|
103
|
-
<label class="label"><span class="label-text font-medium">Description</span></label>
|
|
104
|
-
<textarea bind:value={editDescription} class="textarea textarea-bordered w-full" rows="3" maxlength="250"></textarea>
|
|
103
|
+
<label for="channel-info-description" class="label"><span class="label-text font-medium">Description</span></label>
|
|
104
|
+
<textarea id="channel-info-description" bind:value={editDescription} class="textarea textarea-bordered w-full" rows="3" maxlength="250"></textarea>
|
|
105
105
|
</div>
|
|
106
106
|
<div class="flex gap-2">
|
|
107
107
|
<button class="btn btn-primary btn-sm" onclick={saveEdit} disabled={saving || !editName.trim()}>
|
|
@@ -126,20 +126,20 @@
|
|
|
126
126
|
{selectedEmoji}
|
|
127
127
|
</button>
|
|
128
128
|
<div class="flex-1">
|
|
129
|
-
<label class="label"><span class="label-text font-medium">Channel Name</span></label>
|
|
130
|
-
<input type="text" bind:value={channelName} class="input input-bordered w-full" placeholder="e.g., general, announcements" maxlength="80" />
|
|
129
|
+
<label for="create-channel-name" class="label"><span class="label-text font-medium">Channel Name</span></label>
|
|
130
|
+
<input id="create-channel-name" type="text" bind:value={channelName} class="input input-bordered w-full" placeholder="e.g., general, announcements" maxlength="80" />
|
|
131
131
|
</div>
|
|
132
132
|
</div>
|
|
133
133
|
|
|
134
134
|
<!-- Description -->
|
|
135
135
|
<div>
|
|
136
|
-
<label class="label"><span class="label-text font-medium">Description</span></label>
|
|
137
|
-
<textarea bind:value={description} class="textarea textarea-bordered w-full" placeholder="What's this channel about?" rows="2" maxlength="250"></textarea>
|
|
136
|
+
<label for="create-channel-description" class="label"><span class="label-text font-medium">Description</span></label>
|
|
137
|
+
<textarea id="create-channel-description" bind:value={description} class="textarea textarea-bordered w-full" placeholder="What's this channel about?" rows="2" maxlength="250"></textarea>
|
|
138
138
|
</div>
|
|
139
139
|
|
|
140
140
|
<!-- Channel type -->
|
|
141
141
|
<div>
|
|
142
|
-
<
|
|
142
|
+
<span class="label"><span class="label-text font-medium">Channel Type</span></span>
|
|
143
143
|
<div class="flex gap-3">
|
|
144
144
|
<label class="cursor-pointer flex items-center gap-2">
|
|
145
145
|
<input type="radio" name="channelType" class="radio radio-primary" bind:group={channelType} value="public" />
|
|
@@ -154,8 +154,8 @@
|
|
|
154
154
|
|
|
155
155
|
<!-- Member selection -->
|
|
156
156
|
<div>
|
|
157
|
-
<label class="label"><span class="label-text font-medium">Add Members ({selectedMembers.length})</span></label>
|
|
158
|
-
<input type="text" bind:value={searchQuery} class="input input-bordered input-sm w-full mb-2" placeholder="Search members..." />
|
|
157
|
+
<label for="create-channel-member-search" class="label"><span class="label-text font-medium">Add Members ({selectedMembers.length})</span></label>
|
|
158
|
+
<input id="create-channel-member-search" type="text" bind:value={searchQuery} class="input input-bordered input-sm w-full mb-2" placeholder="Search members..." />
|
|
159
159
|
<div class="max-h-40 overflow-y-auto space-y-1">
|
|
160
160
|
{#each filteredMembers as member}
|
|
161
161
|
<button type="button" class="w-full flex items-center gap-3 px-2 py-1.5 rounded hover:bg-base-200 transition-colors" onclick={() => toggleMember(member.id)}>
|
|
@@ -165,12 +165,12 @@
|
|
|
165
165
|
{#if globalSettings.quiet_hours_enabled}
|
|
166
166
|
<div class="flex gap-3">
|
|
167
167
|
<div class="flex-1">
|
|
168
|
-
<label class="label"><span class="label-text text-xs">Start</span></label>
|
|
169
|
-
<input type="time" bind:value={globalSettings.quiet_hours_start} class="input input-bordered input-sm w-full" />
|
|
168
|
+
<label for="notif-quiet-hours-start" class="label"><span class="label-text text-xs">Start</span></label>
|
|
169
|
+
<input id="notif-quiet-hours-start" type="time" bind:value={globalSettings.quiet_hours_start} class="input input-bordered input-sm w-full" />
|
|
170
170
|
</div>
|
|
171
171
|
<div class="flex-1">
|
|
172
|
-
<label class="label"><span class="label-text text-xs">End</span></label>
|
|
173
|
-
<input type="time" bind:value={globalSettings.quiet_hours_end} class="input input-bordered input-sm w-full" />
|
|
172
|
+
<label for="notif-quiet-hours-end" class="label"><span class="label-text text-xs">End</span></label>
|
|
173
|
+
<input id="notif-quiet-hours-end" type="time" bind:value={globalSettings.quiet_hours_end} class="input input-bordered input-sm w-full" />
|
|
174
174
|
</div>
|
|
175
175
|
</div>
|
|
176
176
|
{/if}
|
|
@@ -197,8 +197,8 @@
|
|
|
197
197
|
</label>
|
|
198
198
|
</div>
|
|
199
199
|
<div>
|
|
200
|
-
<label class="label"><span class="label-text font-medium">Notification Level</span></label>
|
|
201
|
-
<select bind:value={channelPrefs.notification_level} class="select select-bordered w-full">
|
|
200
|
+
<label for="notif-channel-level" class="label"><span class="label-text font-medium">Notification Level</span></label>
|
|
201
|
+
<select id="notif-channel-level" bind:value={channelPrefs.notification_level} class="select select-bordered w-full">
|
|
202
202
|
<option value="all">All Messages</option>
|
|
203
203
|
<option value="mentions">Mentions Only</option>
|
|
204
204
|
<option value="none">None</option>
|
package/src/lib/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Components — Universal
|
|
2
2
|
export { default as UserAvatar } from './components/UserAvatar.svelte';
|
|
3
3
|
export { default as SidebarUserFooter } from './components/SidebarUserFooter.svelte';
|
|
4
|
+
export { default as TaskTypeIcon } from './components/TaskTypeIcon.svelte';
|
|
4
5
|
|
|
5
6
|
// Components — from JAT IDE
|
|
6
7
|
export { default as SearchDropdown } from './components/SearchDropdown.svelte';
|
|
@@ -10,6 +11,7 @@ export { default as ResizableDivider } from './components/ResizableDivider.svelt
|
|
|
10
11
|
export { default as DateRangePicker } from './components/DateRangePicker.svelte';
|
|
11
12
|
export { default as SortDropdown } from './components/SortDropdown.svelte';
|
|
12
13
|
export { default as InlineEdit } from './components/InlineEdit.svelte';
|
|
14
|
+
export { default as FloatingActionBar } from './components/FloatingActionBar.svelte';
|
|
13
15
|
|
|
14
16
|
// Components — from Flush
|
|
15
17
|
export { default as Button } from './components/Button.svelte';
|
|
@@ -75,6 +77,7 @@ export type { SearchDropdownOption, SearchDropdownGroup } from './components/Sea
|
|
|
75
77
|
export type { ChipSuggestion, SuggestionGroup, ChipInfo } from './components/ChipInput.svelte';
|
|
76
78
|
export type { SortOption } from './components/SortDropdown.svelte';
|
|
77
79
|
export type { DisplaySegment } from './components/InlineEdit.svelte';
|
|
80
|
+
export type { ActionButton, ActionDropdown, DropdownItem } from './components/FloatingActionBar.svelte';
|
|
78
81
|
|
|
79
82
|
// Component types — Flush
|
|
80
83
|
export type { SelectOption } from './components/SelectInput.svelte';
|
|
@@ -9,14 +9,16 @@
|
|
|
9
9
|
*/
|
|
10
10
|
export function statusColor(status: string): string {
|
|
11
11
|
switch (status) {
|
|
12
|
-
case '
|
|
12
|
+
case 'open': return 'badge-ghost';
|
|
13
13
|
case 'in_progress': return 'badge-warning';
|
|
14
|
-
case '
|
|
15
|
-
case '
|
|
16
|
-
case '
|
|
17
|
-
case '
|
|
18
|
-
case '
|
|
19
|
-
|
|
14
|
+
case 'waiting': return 'badge-warning badge-outline';
|
|
15
|
+
case 'blocked': return 'badge-error badge-outline';
|
|
16
|
+
case 'submitted': return 'badge-info';
|
|
17
|
+
case 'accepted': return 'badge-success';
|
|
18
|
+
case 'deployed': return 'badge-success badge-outline';
|
|
19
|
+
case 'closed': return 'badge-neutral';
|
|
20
|
+
case 'dev': return 'badge-ghost badge-outline';
|
|
21
|
+
default: return 'badge-ghost';
|
|
20
22
|
}
|
|
21
23
|
}
|
|
22
24
|
|
|
@@ -35,7 +37,9 @@ export function priorityColor(priority: string | null | undefined): string {
|
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
/**
|
|
38
|
-
*
|
|
40
|
+
* @deprecated Prefer the `<TaskTypeIcon type={…} />` component — it renders a
|
|
41
|
+
* stroke SVG that inherits `currentColor`, scales precisely, and avoids
|
|
42
|
+
* cross-OS emoji rendering drift. Kept only for backwards compatibility.
|
|
39
43
|
* Usage: {typeIcon(task.issue_type)} {task.title}
|
|
40
44
|
*/
|
|
41
45
|
export function typeIcon(type: string): string {
|