@r2digisolutions/components 0.1.5 → 0.1.7
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/README.md +6 -1
- package/dist/components/atoms/Checkbox/Checkbox.svelte +10 -3
- package/dist/components/atoms/Checkbox/Checkbox.svelte.d.ts +3 -0
- package/dist/components/atoms/Input/Input.svelte +17 -2
- package/dist/components/atoms/Input/Input.svelte.d.ts +6 -1
- package/dist/components/atoms/MarqueeRect/MarqueeRect.svelte +21 -0
- package/dist/components/atoms/MarqueeRect/MarqueeRect.svelte.d.ts +8 -0
- package/dist/components/atoms/Textarea/Textarea.svelte +4 -1
- package/dist/components/atoms/Textarea/Textarea.svelte.d.ts +1 -0
- package/dist/components/atoms/Toggle/Toggle.svelte +1 -1
- package/dist/components/molecules/BulkActionBar/BulkActionBar.svelte +19 -5
- package/dist/components/molecules/CodeEditor/CodeEditor.svelte +13 -10
- package/dist/components/molecules/ColorPicker/ColorPicker.svelte +166 -96
- package/dist/components/molecules/ConditionalFormatPanel/ConditionalFormatPanel.stories.d.ts +9 -0
- package/dist/components/molecules/ConditionalFormatPanel/ConditionalFormatPanel.stories.js +8 -0
- package/dist/components/molecules/ConditionalFormatPanel/ConditionalFormatPanel.svelte +412 -0
- package/dist/components/molecules/ConditionalFormatPanel/ConditionalFormatPanel.svelte.d.ts +14 -0
- package/dist/components/molecules/ConditionalFormatPanel/ConditionalFormatPanelStory.svelte +27 -0
- package/dist/components/molecules/ConditionalFormatPanel/ConditionalFormatPanelStory.svelte.d.ts +3 -0
- package/dist/components/molecules/ContextMenu/ContextMenu.svelte +73 -21
- package/dist/components/molecules/ContextMenu/ContextMenu.svelte.d.ts +8 -1
- package/dist/components/molecules/DateTimePicker/DateTimePicker.svelte.d.ts +1 -1
- package/dist/components/molecules/FormDateTimePicker/FormDateTimePicker.svelte.d.ts +1 -1
- package/dist/components/molecules/Gauge/Gauge.stories.d.ts +1 -1
- package/dist/components/molecules/InfiniteScroll/InfiniteScroll.stories.d.ts +1 -1
- package/dist/components/organisms/AssetPickerPage/AssetPickerPage.svelte.d.ts +1 -1
- package/dist/components/organisms/BacklogPage/BacklogPage.svelte.d.ts +1 -1
- package/dist/components/organisms/CalendarApp/CalendarApp.svelte.d.ts +1 -1
- package/dist/components/organisms/DataGrid/DataGrid.stories.d.ts +65 -0
- package/dist/components/organisms/DataGrid/DataGrid.stories.js +95 -0
- package/dist/components/organisms/DataGrid/DataGrid.svelte +2264 -0
- package/dist/components/organisms/DataGrid/DataGrid.svelte.d.ts +99 -0
- package/dist/components/organisms/DataGrid/DataGridStory.svelte +659 -0
- package/dist/components/organisms/DataGrid/DataGridStory.svelte.d.ts +11 -0
- package/dist/components/organisms/DataGrid/breakpoints.d.ts +10 -0
- package/dist/components/organisms/DataGrid/breakpoints.js +20 -0
- package/dist/components/organisms/DataGrid/cellEditors.d.ts +8 -0
- package/dist/components/organisms/DataGrid/cellEditors.js +59 -0
- package/dist/components/organisms/DataGrid/conditionalFormat.d.ts +11 -0
- package/dist/components/organisms/DataGrid/conditionalFormat.js +98 -0
- package/dist/components/organisms/DataGrid/filterRows.d.ts +5 -0
- package/dist/components/organisms/DataGrid/filterRows.js +60 -0
- package/dist/components/organisms/DataGrid/marks.d.ts +9 -0
- package/dist/components/organisms/DataGrid/marks.js +91 -0
- package/dist/components/organisms/DataGrid/types.d.ts +126 -0
- package/dist/components/organisms/DataGrid/types.js +21 -0
- package/dist/components/organisms/ResourceStudio/ResourceStudio.stories.d.ts +33 -0
- package/dist/components/organisms/ResourceStudio/ResourceStudio.stories.js +56 -0
- package/dist/components/organisms/ResourceStudio/ResourceStudio.svelte +830 -0
- package/dist/components/organisms/ResourceStudio/ResourceStudio.svelte.d.ts +68 -0
- package/dist/components/organisms/ResourceStudio/ResourceStudioStory.svelte +600 -0
- package/dist/components/organisms/ResourceStudio/ResourceStudioStory.svelte.d.ts +7 -0
- package/dist/components/organisms/ResourceStudio/fieldsToColumns.d.ts +4 -0
- package/dist/components/organisms/ResourceStudio/fieldsToColumns.js +20 -0
- package/dist/components/organisms/ResourceStudio/fieldsToSchema.d.ts +6 -0
- package/dist/components/organisms/ResourceStudio/fieldsToSchema.js +53 -0
- package/dist/components/organisms/ResourceStudio/types.d.ts +69 -0
- package/dist/components/organisms/ResourceStudio/types.js +15 -0
- package/dist/components/organisms/Scheduler/Scheduler.svelte.d.ts +1 -1
- package/dist/components/organisms/SchemaForm/SchemaForm.svelte +10 -3
- package/dist/components/organisms/SchemaForm/SchemaForm.svelte.d.ts +1 -0
- package/dist/components/templates/ResourceStudioTemplate/ResourceStudioTemplate.stories.d.ts +25 -0
- package/dist/components/templates/ResourceStudioTemplate/ResourceStudioTemplate.stories.js +18 -0
- package/dist/components/templates/ResourceStudioTemplate/ResourceStudioTemplate.svelte +192 -0
- package/dist/components/templates/ResourceStudioTemplate/ResourceStudioTemplate.svelte.d.ts +62 -0
- package/dist/components/templates/ResourceStudioTemplate/ResourceStudioTemplateStory.svelte +129 -0
- package/dist/components/templates/ResourceStudioTemplate/ResourceStudioTemplateStory.svelte.d.ts +7 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +8 -0
- package/dist/utils/marqueeSelect.d.ts +48 -0
- package/dist/utils/marqueeSelect.js +189 -0
- package/package.json +1 -1
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Button from '../../atoms/Button/Button.svelte';
|
|
3
|
+
import Badge from '../../atoms/Badge/Badge.svelte';
|
|
4
|
+
import IconButton from '../../atoms/IconButton/IconButton.svelte';
|
|
5
|
+
import Text from '../../atoms/Text/Text.svelte';
|
|
6
|
+
import Divider from '../../atoms/Divider/Divider.svelte';
|
|
7
|
+
import ColorSwatch from '../../atoms/ColorSwatch/ColorSwatch.svelte';
|
|
8
|
+
import Toggle from '../../atoms/Toggle/Toggle.svelte';
|
|
9
|
+
import Input from '../../atoms/Input/Input.svelte';
|
|
10
|
+
import Card from '../Card/Card.svelte';
|
|
11
|
+
import Select from '../Select/Select.svelte';
|
|
12
|
+
import ColorPicker from '../ColorPicker/ColorPicker.svelte';
|
|
13
|
+
import EmptyState from '../EmptyState/EmptyState.svelte';
|
|
14
|
+
import type {
|
|
15
|
+
ConditionalFormatRule,
|
|
16
|
+
FormatCondition,
|
|
17
|
+
FormatTarget
|
|
18
|
+
} from '../../organisms/DataGrid/types.js';
|
|
19
|
+
import ArrowUp from '@lucide/svelte/icons/arrow-up';
|
|
20
|
+
import ArrowDown from '@lucide/svelte/icons/arrow-down';
|
|
21
|
+
import Pencil from '@lucide/svelte/icons/pencil';
|
|
22
|
+
import Trash2 from '@lucide/svelte/icons/trash-2';
|
|
23
|
+
import Palette from '@lucide/svelte/icons/palette';
|
|
24
|
+
|
|
25
|
+
interface ColumnOption {
|
|
26
|
+
id: string;
|
|
27
|
+
label: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface ConditionalFormatPanelProps {
|
|
31
|
+
rules?: ConditionalFormatRule[];
|
|
32
|
+
columns?: ColumnOption[];
|
|
33
|
+
class?: string;
|
|
34
|
+
onchange?: (rules: ConditionalFormatRule[]) => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let {
|
|
38
|
+
rules = $bindable<ConditionalFormatRule[]>([]),
|
|
39
|
+
columns = [],
|
|
40
|
+
class: className = '',
|
|
41
|
+
onchange
|
|
42
|
+
}: ConditionalFormatPanelProps = $props();
|
|
43
|
+
|
|
44
|
+
let editingId = $state<string | null>(null);
|
|
45
|
+
let draftName = $state('');
|
|
46
|
+
let draftTarget = $state<string>('row');
|
|
47
|
+
let draftColumnId = $state('');
|
|
48
|
+
let draftOp = $state<string>('eq');
|
|
49
|
+
let draftValue = $state('');
|
|
50
|
+
let draftMin = $state('0');
|
|
51
|
+
let draftMax = $state('100');
|
|
52
|
+
let draftBg = $state('#fef3c7');
|
|
53
|
+
let draftFg = $state('#92400e');
|
|
54
|
+
|
|
55
|
+
const isEditing = $derived(editingId !== null);
|
|
56
|
+
|
|
57
|
+
const targetOptions = [
|
|
58
|
+
{ value: 'row', label: 'Entire row' },
|
|
59
|
+
{ value: 'cell', label: 'Cell only' }
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
const ops = [
|
|
63
|
+
{ value: 'eq', label: 'Equals' },
|
|
64
|
+
{ value: 'neq', label: 'Not equals' },
|
|
65
|
+
{ value: 'gt', label: 'Greater than' },
|
|
66
|
+
{ value: 'gte', label: '≥ Greater or equal' },
|
|
67
|
+
{ value: 'lt', label: 'Less than' },
|
|
68
|
+
{ value: 'lte', label: '≤ Less or equal' },
|
|
69
|
+
{ value: 'contains', label: 'Contains' },
|
|
70
|
+
{ value: 'startsWith', label: 'Starts with' },
|
|
71
|
+
{ value: 'empty', label: 'Is empty' },
|
|
72
|
+
{ value: 'notEmpty', label: 'Is not empty' },
|
|
73
|
+
{ value: 'between', label: 'Between' }
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
const columnOptions = $derived(columns.map((c) => ({ value: c.id, label: c.label })));
|
|
77
|
+
|
|
78
|
+
const formatSwatches = [
|
|
79
|
+
'#fef3c7',
|
|
80
|
+
'#92400e',
|
|
81
|
+
'#fee2e2',
|
|
82
|
+
'#991b1b',
|
|
83
|
+
'#dcfce7',
|
|
84
|
+
'#166534',
|
|
85
|
+
'#dbeafe',
|
|
86
|
+
'#1e40af',
|
|
87
|
+
'#f3e8ff',
|
|
88
|
+
'#6b21a8',
|
|
89
|
+
'#f1f5f9',
|
|
90
|
+
'#0f172a'
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
$effect(() => {
|
|
94
|
+
if (!draftColumnId && columns[0]) draftColumnId = columns[0].id;
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
function emit(next: ConditionalFormatRule[]) {
|
|
98
|
+
rules = next;
|
|
99
|
+
onchange?.(next);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function buildWhen(): FormatCondition | null {
|
|
103
|
+
const columnId = draftColumnId || columns[0]?.id;
|
|
104
|
+
if (!columnId) return null;
|
|
105
|
+
const op = draftOp as FormatCondition['op'];
|
|
106
|
+
if (op === 'empty' || op === 'notEmpty') return { op, columnId };
|
|
107
|
+
if (op === 'between') {
|
|
108
|
+
return { op: 'between', columnId, min: Number(draftMin), max: Number(draftMax) };
|
|
109
|
+
}
|
|
110
|
+
if (op === 'contains' || op === 'startsWith') {
|
|
111
|
+
return { op, columnId, value: draftValue };
|
|
112
|
+
}
|
|
113
|
+
const num = Number(draftValue);
|
|
114
|
+
const value =
|
|
115
|
+
draftValue !== '' && !Number.isNaN(num) && String(num) === draftValue.trim()
|
|
116
|
+
? num
|
|
117
|
+
: draftValue;
|
|
118
|
+
return { op: op as 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte', columnId, value };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function resetDraft() {
|
|
122
|
+
editingId = null;
|
|
123
|
+
draftName = '';
|
|
124
|
+
draftTarget = 'row';
|
|
125
|
+
draftOp = 'eq';
|
|
126
|
+
draftValue = '';
|
|
127
|
+
draftMin = '0';
|
|
128
|
+
draftMax = '100';
|
|
129
|
+
draftBg = '#fef3c7';
|
|
130
|
+
draftFg = '#92400e';
|
|
131
|
+
draftColumnId = columns[0]?.id ?? '';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function loadRule(rule: ConditionalFormatRule) {
|
|
135
|
+
editingId = rule.id;
|
|
136
|
+
draftName = rule.name ?? '';
|
|
137
|
+
draftTarget = rule.target;
|
|
138
|
+
draftBg = rule.style.background ?? '#fef3c7';
|
|
139
|
+
draftFg = rule.style.color ?? '#92400e';
|
|
140
|
+
const w = rule.when;
|
|
141
|
+
draftColumnId = w.columnId;
|
|
142
|
+
draftOp = w.op;
|
|
143
|
+
draftValue = '';
|
|
144
|
+
draftMin = '0';
|
|
145
|
+
draftMax = '100';
|
|
146
|
+
if (w.op === 'between') {
|
|
147
|
+
draftMin = String(w.min);
|
|
148
|
+
draftMax = String(w.max);
|
|
149
|
+
} else if (w.op !== 'empty' && w.op !== 'notEmpty' && 'value' in w) {
|
|
150
|
+
draftValue = String(w.value);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function saveRule() {
|
|
155
|
+
const when = buildWhen();
|
|
156
|
+
if (!when) return;
|
|
157
|
+
const target = draftTarget as FormatTarget;
|
|
158
|
+
const style = { background: draftBg, color: draftFg };
|
|
159
|
+
|
|
160
|
+
if (editingId) {
|
|
161
|
+
emit(
|
|
162
|
+
rules.map((r) =>
|
|
163
|
+
r.id === editingId
|
|
164
|
+
? {
|
|
165
|
+
...r,
|
|
166
|
+
name: draftName || undefined,
|
|
167
|
+
target,
|
|
168
|
+
columnId: target === 'cell' ? when.columnId : undefined,
|
|
169
|
+
when,
|
|
170
|
+
style: { ...r.style, ...style }
|
|
171
|
+
}
|
|
172
|
+
: r
|
|
173
|
+
)
|
|
174
|
+
);
|
|
175
|
+
resetDraft();
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const maxPriority = rules.reduce((m, r) => Math.max(m, r.priority), 0);
|
|
180
|
+
const rule: ConditionalFormatRule = {
|
|
181
|
+
id: `rule-${Math.random().toString(36).slice(2, 9)}`,
|
|
182
|
+
name: draftName || undefined,
|
|
183
|
+
enabled: true,
|
|
184
|
+
target,
|
|
185
|
+
columnId: target === 'cell' ? when.columnId : undefined,
|
|
186
|
+
when,
|
|
187
|
+
style,
|
|
188
|
+
priority: maxPriority + 1
|
|
189
|
+
};
|
|
190
|
+
emit([...rules, rule]);
|
|
191
|
+
resetDraft();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function setEnabled(id: string, enabled: boolean) {
|
|
195
|
+
emit(rules.map((r) => (r.id === id ? { ...r, enabled } : r)));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function remove(id: string) {
|
|
199
|
+
if (editingId === id) resetDraft();
|
|
200
|
+
emit(rules.filter((r) => r.id !== id));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function move(id: string, dir: -1 | 1) {
|
|
204
|
+
const idx = rules.findIndex((r) => r.id === id);
|
|
205
|
+
if (idx < 0) return;
|
|
206
|
+
const next = [...rules];
|
|
207
|
+
const swap = idx + dir;
|
|
208
|
+
if (swap < 0 || swap >= next.length) return;
|
|
209
|
+
[next[idx], next[swap]] = [next[swap], next[idx]];
|
|
210
|
+
emit(next.map((r, i) => ({ ...r, priority: i + 1 })));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function describe(rule: ConditionalFormatRule): string {
|
|
214
|
+
const w = rule.when;
|
|
215
|
+
const col = columns.find((c) => c.id === w.columnId)?.label ?? w.columnId;
|
|
216
|
+
if (w.op === 'empty') return `${col} is empty`;
|
|
217
|
+
if (w.op === 'notEmpty') return `${col} is not empty`;
|
|
218
|
+
if (w.op === 'between') return `${col} between ${w.min}–${w.max}`;
|
|
219
|
+
if ('value' in w) return `${col} ${w.op} ${String(w.value)}`;
|
|
220
|
+
return `${col} ${w.op}`;
|
|
221
|
+
}
|
|
222
|
+
</script>
|
|
223
|
+
|
|
224
|
+
<Card variant="elevated" padding="md" class={className}>
|
|
225
|
+
{#snippet header()}
|
|
226
|
+
<div class="flex items-start justify-between gap-3">
|
|
227
|
+
<div class="min-w-0 space-y-0.5">
|
|
228
|
+
<p class="text-sm font-semibold text-primary">Conditional formatting</p>
|
|
229
|
+
<Text size="xs" tone="muted">Paint rows or cells when a condition matches.</Text>
|
|
230
|
+
</div>
|
|
231
|
+
<Badge variant="secondary">{rules.length}</Badge>
|
|
232
|
+
</div>
|
|
233
|
+
{/snippet}
|
|
234
|
+
|
|
235
|
+
<div class="space-y-3">
|
|
236
|
+
{#if rules.length === 0}
|
|
237
|
+
<EmptyState
|
|
238
|
+
title="No rules yet"
|
|
239
|
+
description="Add a rule below to highlight matching rows or cells."
|
|
240
|
+
class="!py-6"
|
|
241
|
+
>
|
|
242
|
+
{#snippet icon()}
|
|
243
|
+
<Palette class="h-6 w-6" strokeWidth={1.75} />
|
|
244
|
+
{/snippet}
|
|
245
|
+
</EmptyState>
|
|
246
|
+
{:else}
|
|
247
|
+
<ul class="divide-y divide-border overflow-hidden rounded-xl border border-border bg-surface">
|
|
248
|
+
{#each rules as rule (rule.id)}
|
|
249
|
+
<li
|
|
250
|
+
class={[
|
|
251
|
+
'flex items-center gap-3 px-3 py-2.5 transition-opacity',
|
|
252
|
+
!rule.enabled && 'opacity-55',
|
|
253
|
+
editingId === rule.id && 'bg-brand-500/5'
|
|
254
|
+
]}
|
|
255
|
+
>
|
|
256
|
+
<span
|
|
257
|
+
class="h-10 w-1.5 shrink-0 rounded-full"
|
|
258
|
+
style={`background:${rule.style.background ?? '#94a3b8'}`}
|
|
259
|
+
aria-hidden="true"
|
|
260
|
+
></span>
|
|
261
|
+
|
|
262
|
+
<div class="min-w-0 flex-1">
|
|
263
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
264
|
+
<span class="truncate text-sm font-medium text-primary">
|
|
265
|
+
{rule.name ?? describe(rule)}
|
|
266
|
+
</span>
|
|
267
|
+
<Badge size="sm" variant={rule.target === 'row' ? 'info' : 'primary'}>
|
|
268
|
+
{rule.target === 'row' ? 'Row' : 'Cell'}
|
|
269
|
+
</Badge>
|
|
270
|
+
</div>
|
|
271
|
+
<div class="mt-1 flex flex-wrap items-center gap-2">
|
|
272
|
+
<span
|
|
273
|
+
class="inline-flex items-center rounded-md px-2 py-0.5 text-[11px] font-medium"
|
|
274
|
+
style={`background-color:${rule.style.background ?? 'transparent'};color:${rule.style.color ?? 'inherit'};font-weight:${rule.style.fontWeight ?? 'medium'}`}
|
|
275
|
+
>
|
|
276
|
+
Aa preview
|
|
277
|
+
</span>
|
|
278
|
+
<Text as="span" size="xs" tone="muted">{describe(rule)}</Text>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
|
|
282
|
+
<div class="flex shrink-0 items-center gap-1">
|
|
283
|
+
<Toggle
|
|
284
|
+
size="sm"
|
|
285
|
+
checked={rule.enabled}
|
|
286
|
+
label=""
|
|
287
|
+
onchange={(checked) => setEnabled(rule.id, checked)}
|
|
288
|
+
/>
|
|
289
|
+
<IconButton
|
|
290
|
+
size="xs"
|
|
291
|
+
variant="ghost"
|
|
292
|
+
label="Edit rule"
|
|
293
|
+
onclick={() => loadRule(rule)}
|
|
294
|
+
>
|
|
295
|
+
<Pencil class="h-3.5 w-3.5" strokeWidth={2} />
|
|
296
|
+
</IconButton>
|
|
297
|
+
<IconButton
|
|
298
|
+
size="xs"
|
|
299
|
+
variant="ghost"
|
|
300
|
+
label="Move up"
|
|
301
|
+
disabled={rules[0]?.id === rule.id}
|
|
302
|
+
onclick={() => move(rule.id, -1)}
|
|
303
|
+
>
|
|
304
|
+
<ArrowUp class="h-3.5 w-3.5" strokeWidth={2} />
|
|
305
|
+
</IconButton>
|
|
306
|
+
<IconButton
|
|
307
|
+
size="xs"
|
|
308
|
+
variant="ghost"
|
|
309
|
+
label="Move down"
|
|
310
|
+
disabled={rules.at(-1)?.id === rule.id}
|
|
311
|
+
onclick={() => move(rule.id, 1)}
|
|
312
|
+
>
|
|
313
|
+
<ArrowDown class="h-3.5 w-3.5" strokeWidth={2} />
|
|
314
|
+
</IconButton>
|
|
315
|
+
<IconButton
|
|
316
|
+
size="xs"
|
|
317
|
+
variant="ghost"
|
|
318
|
+
label="Delete rule"
|
|
319
|
+
onclick={() => remove(rule.id)}
|
|
320
|
+
>
|
|
321
|
+
<Trash2 class="h-3.5 w-3.5" strokeWidth={2} />
|
|
322
|
+
</IconButton>
|
|
323
|
+
</div>
|
|
324
|
+
</li>
|
|
325
|
+
{/each}
|
|
326
|
+
</ul>
|
|
327
|
+
{/if}
|
|
328
|
+
</div>
|
|
329
|
+
|
|
330
|
+
{#snippet footer()}
|
|
331
|
+
<div class="space-y-4">
|
|
332
|
+
<Divider label={isEditing ? 'Edit rule' : 'New rule'} />
|
|
333
|
+
|
|
334
|
+
<div class="grid gap-3 sm:grid-cols-2">
|
|
335
|
+
<Input
|
|
336
|
+
label="Name"
|
|
337
|
+
placeholder="Optional"
|
|
338
|
+
size="sm"
|
|
339
|
+
bind:value={draftName}
|
|
340
|
+
/>
|
|
341
|
+
<Select
|
|
342
|
+
label="Target"
|
|
343
|
+
size="sm"
|
|
344
|
+
options={targetOptions}
|
|
345
|
+
bind:value={draftTarget}
|
|
346
|
+
/>
|
|
347
|
+
<Select
|
|
348
|
+
label="Column"
|
|
349
|
+
size="sm"
|
|
350
|
+
options={columnOptions}
|
|
351
|
+
bind:value={draftColumnId}
|
|
352
|
+
placeholder="Select column"
|
|
353
|
+
/>
|
|
354
|
+
<Select
|
|
355
|
+
label="Operator"
|
|
356
|
+
size="sm"
|
|
357
|
+
options={ops}
|
|
358
|
+
bind:value={draftOp}
|
|
359
|
+
/>
|
|
360
|
+
{#if draftOp === 'between'}
|
|
361
|
+
<Input label="Min" size="sm" type="number" bind:value={draftMin} />
|
|
362
|
+
<Input label="Max" size="sm" type="number" bind:value={draftMax} />
|
|
363
|
+
{:else if draftOp !== 'empty' && draftOp !== 'notEmpty'}
|
|
364
|
+
<div class="sm:col-span-2">
|
|
365
|
+
<Input
|
|
366
|
+
label="Value"
|
|
367
|
+
size="sm"
|
|
368
|
+
placeholder="Compare value"
|
|
369
|
+
bind:value={draftValue}
|
|
370
|
+
/>
|
|
371
|
+
</div>
|
|
372
|
+
{/if}
|
|
373
|
+
</div>
|
|
374
|
+
|
|
375
|
+
<div class="grid gap-3 sm:grid-cols-2">
|
|
376
|
+
<ColorPicker
|
|
377
|
+
label="Background"
|
|
378
|
+
bind:value={draftBg}
|
|
379
|
+
swatches={formatSwatches}
|
|
380
|
+
format="hex"
|
|
381
|
+
/>
|
|
382
|
+
<ColorPicker
|
|
383
|
+
label="Text"
|
|
384
|
+
bind:value={draftFg}
|
|
385
|
+
swatches={formatSwatches}
|
|
386
|
+
format="hex"
|
|
387
|
+
/>
|
|
388
|
+
</div>
|
|
389
|
+
|
|
390
|
+
<div class="flex items-center justify-between gap-3">
|
|
391
|
+
<div class="flex items-center gap-2">
|
|
392
|
+
<ColorSwatch color={draftBg} size="sm" label="Background preview" />
|
|
393
|
+
<ColorSwatch color={draftFg} size="sm" label="Text preview" />
|
|
394
|
+
<span
|
|
395
|
+
class="rounded-md px-2 py-1 text-xs font-medium"
|
|
396
|
+
style={`background-color:${draftBg};color:${draftFg}`}
|
|
397
|
+
>
|
|
398
|
+
Preview
|
|
399
|
+
</span>
|
|
400
|
+
</div>
|
|
401
|
+
<div class="flex items-center gap-2">
|
|
402
|
+
{#if isEditing}
|
|
403
|
+
<Button size="sm" variant="ghost" onclick={resetDraft}>Cancel</Button>
|
|
404
|
+
{/if}
|
|
405
|
+
<Button size="sm" variant="primary" onclick={saveRule}>
|
|
406
|
+
{isEditing ? 'Update rule' : 'Add rule'}
|
|
407
|
+
</Button>
|
|
408
|
+
</div>
|
|
409
|
+
</div>
|
|
410
|
+
</div>
|
|
411
|
+
{/snippet}
|
|
412
|
+
</Card>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ConditionalFormatRule } from '../../organisms/DataGrid/types.js';
|
|
2
|
+
interface ColumnOption {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
}
|
|
6
|
+
interface ConditionalFormatPanelProps {
|
|
7
|
+
rules?: ConditionalFormatRule[];
|
|
8
|
+
columns?: ColumnOption[];
|
|
9
|
+
class?: string;
|
|
10
|
+
onchange?: (rules: ConditionalFormatRule[]) => void;
|
|
11
|
+
}
|
|
12
|
+
declare const ConditionalFormatPanel: import("svelte").Component<ConditionalFormatPanelProps, {}, "rules">;
|
|
13
|
+
type ConditionalFormatPanel = ReturnType<typeof ConditionalFormatPanel>;
|
|
14
|
+
export default ConditionalFormatPanel;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import ConditionalFormatPanel from './ConditionalFormatPanel.svelte';
|
|
3
|
+
import type { ConditionalFormatRule } from '../../organisms/DataGrid/types.js';
|
|
4
|
+
|
|
5
|
+
let rules = $state<ConditionalFormatRule[]>([
|
|
6
|
+
{
|
|
7
|
+
id: 'r1',
|
|
8
|
+
name: 'Overdue',
|
|
9
|
+
enabled: true,
|
|
10
|
+
target: 'row',
|
|
11
|
+
when: { op: 'eq', columnId: 'status', value: 'overdue' },
|
|
12
|
+
style: { background: '#fee2e2', color: '#991b1b' },
|
|
13
|
+
priority: 1
|
|
14
|
+
}
|
|
15
|
+
]);
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<div class="max-w-lg">
|
|
19
|
+
<ConditionalFormatPanel
|
|
20
|
+
bind:rules
|
|
21
|
+
columns={[
|
|
22
|
+
{ id: 'status', label: 'Status' },
|
|
23
|
+
{ id: 'amount', label: 'Amount' },
|
|
24
|
+
{ id: 'name', label: 'Name' }
|
|
25
|
+
]}
|
|
26
|
+
/>
|
|
27
|
+
</div>
|
|
@@ -10,9 +10,17 @@
|
|
|
10
10
|
shortcut?: string;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
export interface ContextMenuAnchor {
|
|
14
|
+
x: number;
|
|
15
|
+
y: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
13
18
|
interface ContextMenuProps {
|
|
14
19
|
items?: ContextMenuItem[];
|
|
15
20
|
open?: boolean;
|
|
21
|
+
/** Screen coordinates for the menu (clientX / clientY). */
|
|
22
|
+
anchor?: ContextMenuAnchor | null;
|
|
23
|
+
class?: string;
|
|
16
24
|
children?: Snippet;
|
|
17
25
|
onselect?: (id: string, item: ContextMenuItem) => void;
|
|
18
26
|
}
|
|
@@ -20,6 +28,8 @@
|
|
|
20
28
|
let {
|
|
21
29
|
items = [],
|
|
22
30
|
open = $bindable(false),
|
|
31
|
+
anchor = $bindable<ContextMenuAnchor | null>(null),
|
|
32
|
+
class: className = '',
|
|
23
33
|
children,
|
|
24
34
|
onselect
|
|
25
35
|
}: ContextMenuProps = $props();
|
|
@@ -27,18 +37,17 @@
|
|
|
27
37
|
let menuX = $state(0);
|
|
28
38
|
let menuY = $state(0);
|
|
29
39
|
let menuEl = $state<HTMLDivElement | null>(null);
|
|
30
|
-
let containerEl = $state<HTMLDivElement | null>(null);
|
|
31
40
|
let highlighted = $state(-1);
|
|
41
|
+
/** Ignore the click/pointer that browsers fire right after `contextmenu`. */
|
|
42
|
+
let suppressOutsideUntil = $state(0);
|
|
32
43
|
|
|
33
44
|
const enabledIndexes = $derived(
|
|
34
45
|
items.map((item, i) => (item.separator || item.disabled ? -1 : i)).filter((i) => i !== -1)
|
|
35
46
|
);
|
|
36
47
|
|
|
37
|
-
function
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
// Defer to measure menu dimensions after render
|
|
48
|
+
function placeMenu(x: number, y: number) {
|
|
49
|
+
menuX = x;
|
|
50
|
+
menuY = y;
|
|
42
51
|
requestAnimationFrame(() => {
|
|
43
52
|
if (!menuEl) return;
|
|
44
53
|
const vw = window.innerWidth;
|
|
@@ -47,14 +56,19 @@
|
|
|
47
56
|
menuX = x + mw > vw - 8 ? Math.max(8, x - mw) : x;
|
|
48
57
|
menuY = y + mh > vh - 8 ? Math.max(8, y - mh) : y;
|
|
49
58
|
});
|
|
59
|
+
}
|
|
50
60
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
61
|
+
function openAt(x: number, y: number) {
|
|
62
|
+
anchor = { x, y };
|
|
63
|
+
open = true;
|
|
64
|
+
highlighted = -1;
|
|
65
|
+
suppressOutsideUntil = performance.now() + 500;
|
|
66
|
+
placeMenu(x, y);
|
|
54
67
|
}
|
|
55
68
|
|
|
56
69
|
function close() {
|
|
57
70
|
open = false;
|
|
71
|
+
anchor = null;
|
|
58
72
|
highlighted = -1;
|
|
59
73
|
}
|
|
60
74
|
|
|
@@ -66,12 +80,27 @@
|
|
|
66
80
|
|
|
67
81
|
function handleContextMenu(event: MouseEvent) {
|
|
68
82
|
event.preventDefault();
|
|
83
|
+
event.stopPropagation();
|
|
69
84
|
openAt(event.clientX, event.clientY);
|
|
70
85
|
}
|
|
71
86
|
|
|
87
|
+
function isOutsideEvent(event: Event) {
|
|
88
|
+
if (!open) return false;
|
|
89
|
+
if (performance.now() < suppressOutsideUntil) return false;
|
|
90
|
+
const target = event.target as Node | null;
|
|
91
|
+
if (menuEl && target && menuEl.contains(target)) return false;
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function handleOutsidePointer(event: PointerEvent) {
|
|
96
|
+
if (event.button === 2) return;
|
|
97
|
+
if (!isOutsideEvent(event)) return;
|
|
98
|
+
close();
|
|
99
|
+
}
|
|
100
|
+
|
|
72
101
|
function handleOutsideClick(event: MouseEvent) {
|
|
73
|
-
if (
|
|
74
|
-
if (
|
|
102
|
+
if (event.button === 2) return;
|
|
103
|
+
if (!isOutsideEvent(event)) return;
|
|
75
104
|
close();
|
|
76
105
|
}
|
|
77
106
|
|
|
@@ -109,16 +138,32 @@
|
|
|
109
138
|
highlighted = enabledIndexes[next];
|
|
110
139
|
}
|
|
111
140
|
}
|
|
141
|
+
|
|
142
|
+
/** Render menu in document.body so overflow/transform ancestors cannot clip it. */
|
|
143
|
+
function portal(node: HTMLElement) {
|
|
144
|
+
document.body.appendChild(node);
|
|
145
|
+
return {
|
|
146
|
+
destroy() {
|
|
147
|
+
node.remove();
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
$effect(() => {
|
|
153
|
+
if (!open || !anchor) return;
|
|
154
|
+
suppressOutsideUntil = performance.now() + 500;
|
|
155
|
+
placeMenu(anchor.x, anchor.y);
|
|
156
|
+
});
|
|
112
157
|
</script>
|
|
113
158
|
|
|
114
|
-
<svelte:window
|
|
159
|
+
<svelte:window
|
|
160
|
+
onpointerdown={handleOutsidePointer}
|
|
161
|
+
onclick={handleOutsideClick}
|
|
162
|
+
onkeydown={handleKeydown}
|
|
163
|
+
/>
|
|
115
164
|
|
|
116
165
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
117
|
-
<div
|
|
118
|
-
bind:this={containerEl}
|
|
119
|
-
class="contents"
|
|
120
|
-
oncontextmenu={handleContextMenu}
|
|
121
|
-
>
|
|
166
|
+
<div class={['h-full min-h-0 w-full', className]} oncontextmenu={handleContextMenu}>
|
|
122
167
|
{#if children}
|
|
123
168
|
{@render children()}
|
|
124
169
|
{/if}
|
|
@@ -126,12 +171,13 @@
|
|
|
126
171
|
|
|
127
172
|
{#if open}
|
|
128
173
|
<div
|
|
174
|
+
use:portal
|
|
129
175
|
bind:this={menuEl}
|
|
130
176
|
role="menu"
|
|
131
177
|
tabindex={-1}
|
|
132
178
|
aria-label="Context menu"
|
|
133
179
|
style="position:fixed;top:{menuY}px;left:{menuX}px;z-index:9999;"
|
|
134
|
-
class="min-w-44 rounded-xl border border-border bg-surface-elevated p-1.5 shadow-xl outline-none
|
|
180
|
+
class="min-w-44 rounded-xl border border-border bg-surface-elevated p-1.5 shadow-xl outline-none"
|
|
135
181
|
>
|
|
136
182
|
{#each items as item, index (item.id)}
|
|
137
183
|
{#if item.separator}
|
|
@@ -144,8 +190,12 @@
|
|
|
144
190
|
tabindex={-1}
|
|
145
191
|
aria-disabled={item.disabled || undefined}
|
|
146
192
|
onclick={() => selectItem(item)}
|
|
147
|
-
onpointerenter={() => {
|
|
148
|
-
|
|
193
|
+
onpointerenter={() => {
|
|
194
|
+
if (!item.disabled) highlighted = index;
|
|
195
|
+
}}
|
|
196
|
+
onpointerleave={() => {
|
|
197
|
+
highlighted = -1;
|
|
198
|
+
}}
|
|
149
199
|
class={[
|
|
150
200
|
'flex cursor-pointer items-center justify-between gap-4 rounded-lg px-2.5 py-2 text-sm font-medium transition-colors',
|
|
151
201
|
item.disabled && 'cursor-not-allowed opacity-40',
|
|
@@ -155,7 +205,9 @@
|
|
|
155
205
|
>
|
|
156
206
|
<span class="truncate">{item.label}</span>
|
|
157
207
|
{#if item.shortcut}
|
|
158
|
-
<kbd
|
|
208
|
+
<kbd
|
|
209
|
+
class="shrink-0 rounded border border-border bg-surface-overlay px-1.5 py-0.5 font-mono text-[10px] text-secondary"
|
|
210
|
+
>
|
|
159
211
|
{item.shortcut}
|
|
160
212
|
</kbd>
|
|
161
213
|
{/if}
|
|
@@ -7,12 +7,19 @@ export interface ContextMenuItem {
|
|
|
7
7
|
separator?: boolean;
|
|
8
8
|
shortcut?: string;
|
|
9
9
|
}
|
|
10
|
+
export interface ContextMenuAnchor {
|
|
11
|
+
x: number;
|
|
12
|
+
y: number;
|
|
13
|
+
}
|
|
10
14
|
interface ContextMenuProps {
|
|
11
15
|
items?: ContextMenuItem[];
|
|
12
16
|
open?: boolean;
|
|
17
|
+
/** Screen coordinates for the menu (clientX / clientY). */
|
|
18
|
+
anchor?: ContextMenuAnchor | null;
|
|
19
|
+
class?: string;
|
|
13
20
|
children?: Snippet;
|
|
14
21
|
onselect?: (id: string, item: ContextMenuItem) => void;
|
|
15
22
|
}
|
|
16
|
-
declare const ContextMenu: import("svelte").Component<ContextMenuProps, {}, "open">;
|
|
23
|
+
declare const ContextMenu: import("svelte").Component<ContextMenuProps, {}, "open" | "anchor">;
|
|
17
24
|
type ContextMenu = ReturnType<typeof ContextMenu>;
|
|
18
25
|
export default ContextMenu;
|
|
@@ -25,6 +25,6 @@ interface DateTimePickerProps {
|
|
|
25
25
|
value: string;
|
|
26
26
|
}) => void;
|
|
27
27
|
}
|
|
28
|
-
declare const DateTimePicker: import("svelte").Component<DateTimePickerProps, {}, "
|
|
28
|
+
declare const DateTimePicker: import("svelte").Component<DateTimePickerProps, {}, "date" | "value" | "time" | "open">;
|
|
29
29
|
type DateTimePicker = ReturnType<typeof DateTimePicker>;
|
|
30
30
|
export default DateTimePicker;
|
|
@@ -18,6 +18,6 @@ interface FormDateTimePickerProps {
|
|
|
18
18
|
value: string;
|
|
19
19
|
}) => void;
|
|
20
20
|
}
|
|
21
|
-
declare const FormDateTimePicker: import("svelte").Component<FormDateTimePickerProps, {}, "
|
|
21
|
+
declare const FormDateTimePicker: import("svelte").Component<FormDateTimePickerProps, {}, "date" | "value" | "time">;
|
|
22
22
|
type FormDateTimePicker = ReturnType<typeof FormDateTimePicker>;
|
|
23
23
|
export default FormDateTimePicker;
|
|
@@ -2,7 +2,7 @@ import type { StoryObj } from '@storybook/svelte';
|
|
|
2
2
|
declare const meta: {
|
|
3
3
|
title: string;
|
|
4
4
|
component: import("svelte").Component<{
|
|
5
|
-
example?: "
|
|
5
|
+
example?: "storage" | "grid" | "ring" | "score" | "horseshoe" | "cpu" | "health" | "battery";
|
|
6
6
|
value?: number;
|
|
7
7
|
min?: number;
|
|
8
8
|
max?: number;
|