@svgrid/enterprise 2.6.0 → 2.6.2
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/dist/ExpressionEditorHarness.svelte +47 -0
- package/dist/ExpressionEditorHarness.svelte.d.ts +15 -0
- package/dist/SvAdvancedFilter.svelte +208 -0
- package/dist/SvAdvancedFilter.svelte.d.ts +37 -0
- package/dist/SvExpressionEditor.svelte +255 -124
- package/dist/SvGridEditPanel.svelte +1157 -1012
- package/dist/SvGridEditPanel.svelte.d.ts +2 -0
- package/dist/advanced-filter/expr-columns-from-grid.d.ts +30 -0
- package/dist/advanced-filter/expr-columns-from-grid.js +44 -0
- package/dist/advanced-filter-enable.d.ts +5 -0
- package/dist/advanced-filter-enable.js +35 -0
- package/dist/cdn/svgrid-enterprise.svelte-external.js +7663 -6482
- package/dist/designer/assets/GridMenus-Bjr19Tz_.js +7 -0
- package/dist/designer/assets/{SvListBox-BVzVfCG3.js → SvListBox-BB8rt4aP.js} +1 -1
- package/dist/designer/assets/index-BfzoL904.css +1 -0
- package/dist/designer/assets/{index-BF0UH638.js → index-xjfKckuH.js} +139 -139
- package/dist/designer/assets/{js-scroller.svelte-tHanKJx0.js → js-scroller.svelte-CGkUUXwV.js} +2 -2
- package/dist/designer/assets/src-BT3LjWJL.css +1 -0
- package/dist/designer/assets/src-DDhAK0WP.js +2893 -0
- package/dist/designer/index.html +6 -6
- package/dist/expressions/builder-tree.d.ts +62 -0
- package/dist/expressions/builder-tree.js +133 -0
- package/dist/expressions/compile.d.ts +14 -0
- package/dist/expressions/compile.js +286 -0
- package/dist/expressions/expression-columns.d.ts +1 -11
- package/dist/expressions/expression-columns.js +40 -11
- package/dist/index.d.ts +7 -3
- package/dist/index.js +7 -3
- package/dist/install.js +2 -0
- package/dist/node/studio.js +743 -25
- package/dist/schema.d.ts +39 -0
- package/dist/schema.js +9 -0
- package/dist/sources/rest.js +25 -0
- package/dist/sources/supabase.js +49 -3
- package/dist/studio/emit-project.js +85 -11
- package/dist/studio/index.d.ts +2 -2
- package/dist/studio/index.js +5 -2
- package/dist/studio/project.d.ts +124 -3
- package/dist/studio/project.js +393 -4
- package/dist/studio/ui-components.generated.js +213 -0
- package/dist/sveltekit/in-memory.js +96 -3
- package/dist/sveltekit/query-plan.d.ts +32 -1
- package/dist/sveltekit/query-plan.js +88 -1
- package/dist/sveltekit/sql.d.ts +20 -0
- package/dist/sveltekit/sql.js +26 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
- package/dist/designer/assets/GridMenus-CW-rnHS9.js +0 -7
- package/dist/designer/assets/index-oYok52zd.css +0 -1
- package/dist/designer/assets/src-BPJruwB9.js +0 -2873
- package/dist/designer/assets/src-D-O2F805.css +0 -1
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
SvTagsInput,
|
|
13
13
|
type ExcelFilterOperator,
|
|
14
14
|
} from '@svgrid/grid'
|
|
15
|
+
import { untrack } from 'svelte'
|
|
15
16
|
import { evaluatePredicate } from './expressions/evaluate'
|
|
16
17
|
import {
|
|
17
18
|
ExpressionParseError,
|
|
@@ -29,13 +30,15 @@
|
|
|
29
30
|
} from './expressions/expression-columns'
|
|
30
31
|
import type { PredicateExpr } from './expressions/expression-types'
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
import {
|
|
34
|
+
freshCondition,
|
|
35
|
+
freshGroup,
|
|
36
|
+
fromBuilderTree,
|
|
37
|
+
toBuilderTree,
|
|
38
|
+
MAX_BUILDER_DEPTH,
|
|
39
|
+
type BuilderGroup,
|
|
40
|
+
type BuilderNode,
|
|
41
|
+
} from './expressions/builder-tree'
|
|
39
42
|
|
|
40
43
|
type Props = {
|
|
41
44
|
columns: ReadonlyArray<ExprColumn>
|
|
@@ -54,55 +57,50 @@
|
|
|
54
57
|
}: Props = $props()
|
|
55
58
|
|
|
56
59
|
// --- Builder <-> predicate conversion ------------------------------------
|
|
60
|
+
// The conversion itself lives in expressions/builder-tree.ts, so the round
|
|
61
|
+
// trip can be tested without mounting this component.
|
|
57
62
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const conditions: Condition[] = []
|
|
63
|
-
for (const p of parts) {
|
|
64
|
-
if (p.kind !== 'cmp') return null // scalarCmp / nested -> text mode only
|
|
65
|
-
conditions.push({
|
|
66
|
-
column: p.column,
|
|
67
|
-
op: p.op,
|
|
68
|
-
value: p.value != null && !Array.isArray(p.value) ? String(p.value) : '',
|
|
69
|
-
valueTo: p.valueTo != null ? String(p.valueTo) : '',
|
|
70
|
-
values: Array.isArray(p.value) ? p.value.map(String) : [],
|
|
71
|
-
})
|
|
72
|
-
}
|
|
73
|
-
return { combinator, conditions }
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function fromConditions(combinator: 'and' | 'or', conditions: Condition[]): PredicateExpr {
|
|
77
|
-
const parts: PredicateExpr[] = conditions.map((c) => {
|
|
78
|
-
const base = { kind: 'cmp' as const, column: c.column, op: c.op }
|
|
79
|
-
if (isValueless(c.op)) return base
|
|
80
|
-
if (isSetOperator(c.op)) return { ...base, value: c.values }
|
|
81
|
-
if (isRangeOperator(c.op)) return { ...base, value: c.value, valueTo: c.valueTo }
|
|
82
|
-
return { ...base, value: c.value }
|
|
83
|
-
})
|
|
84
|
-
if (parts.length === 0) return { kind: 'const', value: true }
|
|
85
|
-
if (parts.length === 1) return parts[0]
|
|
86
|
-
return { kind: combinator, parts }
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const initial = toConditions(value)
|
|
90
|
-
let combinator = $state<'and' | 'or'>(initial?.combinator ?? 'and')
|
|
91
|
-
let conditions = $state<Condition[]>(
|
|
92
|
-
initial?.conditions ?? [freshCondition()],
|
|
93
|
-
)
|
|
94
|
-
// If the incoming value is not representable as a flat condition list, start
|
|
95
|
-
// in text mode so nothing is lost.
|
|
63
|
+
const initial = toBuilderTree(value, columns)
|
|
64
|
+
let root = $state<BuilderGroup>(initial ?? freshGroup(columns))
|
|
65
|
+
// If the incoming value cannot be shown as a tree, start in text mode so
|
|
66
|
+
// nothing is lost.
|
|
96
67
|
if (!initial && mode === 'builder') mode = 'text'
|
|
97
68
|
|
|
98
69
|
let text = $state(stringifyPredicate(value, columns))
|
|
99
70
|
let textError = $state<string | null>(null)
|
|
100
71
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
72
|
+
/**
|
|
73
|
+
* The last expression WE produced, serialized. Everything below is seeded
|
|
74
|
+
* once at init, so without this the editor would ignore a `value` assigned
|
|
75
|
+
* from outside - loading a saved filter or clicking a preset would update the
|
|
76
|
+
* grid while the editor kept showing the old conditions.
|
|
77
|
+
*
|
|
78
|
+
* Compared by VALUE, not by reference. Reference comparison looks right and
|
|
79
|
+
* is cheaper, but `value` is $bindable: the object that comes back in is not
|
|
80
|
+
* the object we assigned, so the check never matched and the effect re-seeded
|
|
81
|
+
* the tree after every one of our own edits. Flat lists survived that because
|
|
82
|
+
* they round-trip exactly; a nested group does not, since a group holding one
|
|
83
|
+
* condition serializes to the bare condition, so adding a group appeared to
|
|
84
|
+
* do nothing at all.
|
|
85
|
+
*/
|
|
86
|
+
let lastEmittedJson = $state<string>(JSON.stringify(value))
|
|
87
|
+
|
|
88
|
+
$effect(() => {
|
|
89
|
+
const incoming = value
|
|
90
|
+
if (JSON.stringify(incoming) === untrack(() => lastEmittedJson)) return
|
|
91
|
+
untrack(() => {
|
|
92
|
+
const next = toBuilderTree(incoming, columns)
|
|
93
|
+
if (next) {
|
|
94
|
+
root = next
|
|
95
|
+
} else if (mode === 'builder') {
|
|
96
|
+
// Not representable as a tree; text mode keeps it intact.
|
|
97
|
+
mode = 'text'
|
|
98
|
+
}
|
|
99
|
+
text = stringifyPredicate(incoming, columns)
|
|
100
|
+
textError = null
|
|
101
|
+
lastEmittedJson = JSON.stringify(incoming)
|
|
102
|
+
})
|
|
103
|
+
})
|
|
106
104
|
|
|
107
105
|
const columnOptions = $derived(columns.map((c) => ({ value: c.id, label: c.name ?? c.id })))
|
|
108
106
|
|
|
@@ -114,7 +112,8 @@
|
|
|
114
112
|
// --- Emit ----------------------------------------------------------------
|
|
115
113
|
|
|
116
114
|
function emitBuilder() {
|
|
117
|
-
const expr =
|
|
115
|
+
const expr = fromBuilderTree(root)
|
|
116
|
+
lastEmittedJson = JSON.stringify(expr)
|
|
118
117
|
value = expr
|
|
119
118
|
text = stringifyPredicate(expr, columns)
|
|
120
119
|
textError = null
|
|
@@ -131,6 +130,7 @@
|
|
|
131
130
|
const problems = validateExpression(expr, columns)
|
|
132
131
|
textError = problems.length ? problems.join('; ') : null
|
|
133
132
|
if (!problems.length) {
|
|
133
|
+
lastEmittedJson = JSON.stringify(expr)
|
|
134
134
|
value = expr
|
|
135
135
|
onChange?.(expr)
|
|
136
136
|
}
|
|
@@ -139,22 +139,60 @@
|
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
|
|
143
|
-
|
|
142
|
+
// Mutations take the parent group and an index rather than a path, because
|
|
143
|
+
// the recursive markup already holds the parent it is rendering.
|
|
144
|
+
|
|
145
|
+
function addCondition(group: BuilderGroup) {
|
|
146
|
+
group.children.push(freshCondition(columns))
|
|
144
147
|
emitBuilder()
|
|
145
148
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
+
|
|
150
|
+
function addGroup(group: BuilderGroup) {
|
|
151
|
+
// A nested group defaults to the OPPOSITE combinator. Nesting an "all"
|
|
152
|
+
// inside an "all" is a no-op the user would then have to correct, so the
|
|
153
|
+
// default is the one that makes the new group mean something.
|
|
154
|
+
group.children.push(
|
|
155
|
+
freshGroup(columns, group.combinator === 'and' ? 'or' : 'and'),
|
|
156
|
+
)
|
|
149
157
|
emitBuilder()
|
|
150
158
|
}
|
|
151
|
-
|
|
159
|
+
|
|
160
|
+
function removeChild(group: BuilderGroup, i: number) {
|
|
161
|
+
group.children.splice(i, 1)
|
|
162
|
+
// The root must always offer somewhere to type. A nested group is allowed
|
|
163
|
+
// to empty out, because removing its last child is how you delete it.
|
|
164
|
+
if (group === root && group.children.length === 0) {
|
|
165
|
+
group.children.push(freshCondition(columns))
|
|
166
|
+
}
|
|
167
|
+
emitBuilder()
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function setColumn(cond: Extract<BuilderNode, { kind: 'cond' }>, columnId: string) {
|
|
152
171
|
const ops = opsFor(columnId)
|
|
153
|
-
|
|
154
|
-
if (!ops.some((o) => o.value ===
|
|
172
|
+
cond.column = columnId
|
|
173
|
+
if (!ops.some((o) => o.value === cond.op)) cond.op = ops[0]?.value ?? 'equals'
|
|
155
174
|
emitBuilder()
|
|
156
175
|
}
|
|
157
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Whether the text currently in the box could be shown in the builder.
|
|
179
|
+
*
|
|
180
|
+
* Drives the Builder tab's disabled state. Letting the user click a tab that
|
|
181
|
+
* then refuses with a red error is worse than showing up front that this
|
|
182
|
+
* particular expression has to be edited as text - the refusal is a property
|
|
183
|
+
* of the expression, not a mistake the user made.
|
|
184
|
+
*/
|
|
185
|
+
const builderAvailable = $derived.by(() => {
|
|
186
|
+
if (mode === 'builder') return true
|
|
187
|
+
try {
|
|
188
|
+
return toBuilderTree(parsePredicate(text, columns), columns) != null
|
|
189
|
+
} catch {
|
|
190
|
+
// Half-typed text is not a reason to lock the tab; switchMode reports
|
|
191
|
+
// the parse error properly if they do click.
|
|
192
|
+
return true
|
|
193
|
+
}
|
|
194
|
+
})
|
|
195
|
+
|
|
158
196
|
function switchMode(next: 'builder' | 'text') {
|
|
159
197
|
if (next === mode) return
|
|
160
198
|
if (next === 'text') {
|
|
@@ -164,13 +202,13 @@
|
|
|
164
202
|
// Re-seed the builder from the parsed text, if representable.
|
|
165
203
|
try {
|
|
166
204
|
const expr = parsePredicate(text, columns)
|
|
167
|
-
const
|
|
168
|
-
if (!
|
|
169
|
-
textError = 'This expression uses maths
|
|
205
|
+
const tree = toBuilderTree(expr, columns)
|
|
206
|
+
if (!tree) {
|
|
207
|
+
textError = 'This expression uses column maths - stay in text mode'
|
|
170
208
|
return
|
|
171
209
|
}
|
|
172
|
-
|
|
173
|
-
|
|
210
|
+
root = tree
|
|
211
|
+
lastEmittedJson = JSON.stringify(expr)
|
|
174
212
|
value = expr
|
|
175
213
|
} catch {
|
|
176
214
|
textError = 'Fix the expression before switching'
|
|
@@ -213,6 +251,10 @@
|
|
|
213
251
|
role="tab"
|
|
214
252
|
aria-selected={mode === 'builder'}
|
|
215
253
|
class:sx-active={mode === 'builder'}
|
|
254
|
+
disabled={!builderAvailable}
|
|
255
|
+
title={builderAvailable
|
|
256
|
+
? undefined
|
|
257
|
+
: 'The builder shows a flat list of conditions, so it cannot represent column maths, aggregates or nested groups. Edit this one as text.'}
|
|
216
258
|
onclick={() => switchMode('builder')}>Builder</button>
|
|
217
259
|
<button
|
|
218
260
|
type="button"
|
|
@@ -233,65 +275,7 @@
|
|
|
233
275
|
</div>
|
|
234
276
|
|
|
235
277
|
{#if mode === 'builder'}
|
|
236
|
-
|
|
237
|
-
Match
|
|
238
|
-
<SvDropDownList
|
|
239
|
-
size="sm"
|
|
240
|
-
options={[{ value: 'and', label: 'all' }, { value: 'or', label: 'any' }]}
|
|
241
|
-
value={combinator}
|
|
242
|
-
onChange={(v) => { combinator = v as 'and' | 'or'; emitBuilder() }}
|
|
243
|
-
ariaLabel="Combine conditions" />
|
|
244
|
-
of the following:
|
|
245
|
-
</div>
|
|
246
|
-
<div class="sx-rows">
|
|
247
|
-
{#each conditions as cond, i (i)}
|
|
248
|
-
<div class="sx-row">
|
|
249
|
-
<SvDropDownList
|
|
250
|
-
size="sm"
|
|
251
|
-
options={columnOptions}
|
|
252
|
-
value={cond.column}
|
|
253
|
-
onChange={(v) => setColumn(i, String(v))}
|
|
254
|
-
ariaLabel="Column" />
|
|
255
|
-
<SvDropDownList
|
|
256
|
-
size="sm"
|
|
257
|
-
options={opsFor(cond.column)}
|
|
258
|
-
value={cond.op}
|
|
259
|
-
onChange={(v) => { conditions[i].op = v as ExcelFilterOperator; emitBuilder() }}
|
|
260
|
-
ariaLabel="Operator" />
|
|
261
|
-
{#if isValueless(cond.op)}
|
|
262
|
-
<span class="sx-noval">-</span>
|
|
263
|
-
{:else if isSetOperator(cond.op)}
|
|
264
|
-
<SvTagsInput
|
|
265
|
-
value={cond.values}
|
|
266
|
-
onChange={(vals) => { conditions[i].values = vals; emitBuilder() }}
|
|
267
|
-
placeholder="Add value…"
|
|
268
|
-
ariaLabel="Values" />
|
|
269
|
-
{:else if isRangeOperator(cond.op)}
|
|
270
|
-
<SvTextInput
|
|
271
|
-
size="sm"
|
|
272
|
-
value={cond.value}
|
|
273
|
-
onChange={(v) => { conditions[i].value = v; emitBuilder() }}
|
|
274
|
-
placeholder="From"
|
|
275
|
-
ariaLabel="From" />
|
|
276
|
-
<SvTextInput
|
|
277
|
-
size="sm"
|
|
278
|
-
value={cond.valueTo}
|
|
279
|
-
onChange={(v) => { conditions[i].valueTo = v; emitBuilder() }}
|
|
280
|
-
placeholder="To"
|
|
281
|
-
ariaLabel="To" />
|
|
282
|
-
{:else}
|
|
283
|
-
<SvTextInput
|
|
284
|
-
size="sm"
|
|
285
|
-
value={cond.value}
|
|
286
|
-
onChange={(v) => { conditions[i].value = v; emitBuilder() }}
|
|
287
|
-
placeholder="Value"
|
|
288
|
-
ariaLabel="Value" />
|
|
289
|
-
{/if}
|
|
290
|
-
<button type="button" class="sx-del" onclick={() => removeCondition(i)} aria-label="Remove condition">✕</button>
|
|
291
|
-
</div>
|
|
292
|
-
{/each}
|
|
293
|
-
</div>
|
|
294
|
-
<button type="button" class="sx-add" onclick={addCondition}>+ Add condition</button>
|
|
278
|
+
{@render groupBody(root, 0)}
|
|
295
279
|
{:else}
|
|
296
280
|
<SvTextArea
|
|
297
281
|
value={text}
|
|
@@ -307,6 +291,122 @@
|
|
|
307
291
|
{/if}
|
|
308
292
|
</div>
|
|
309
293
|
|
|
294
|
+
<!--
|
|
295
|
+
One condition row: column, operator, and whichever value control the operator
|
|
296
|
+
needs. `cond` is the live state object, so writing to it mutates the tree in
|
|
297
|
+
place and `emitBuilder` re-reads the whole thing.
|
|
298
|
+
-->
|
|
299
|
+
{#snippet conditionRow(cond: Extract<BuilderNode, { kind: 'cond' }>, parent: BuilderGroup, i: number)}
|
|
300
|
+
<div class="sx-row">
|
|
301
|
+
<SvDropDownList
|
|
302
|
+
size="sm"
|
|
303
|
+
options={columnOptions}
|
|
304
|
+
value={cond.column}
|
|
305
|
+
onChange={(v) => setColumn(cond, String(v))}
|
|
306
|
+
ariaLabel="Column" />
|
|
307
|
+
<SvDropDownList
|
|
308
|
+
size="sm"
|
|
309
|
+
options={opsFor(cond.column)}
|
|
310
|
+
value={cond.op}
|
|
311
|
+
onChange={(v) => { cond.op = v as ExcelFilterOperator; emitBuilder() }}
|
|
312
|
+
ariaLabel="Operator" />
|
|
313
|
+
{#if isValueless(cond.op)}
|
|
314
|
+
<span class="sx-noval">-</span>
|
|
315
|
+
{:else if isSetOperator(cond.op)}
|
|
316
|
+
<SvTagsInput
|
|
317
|
+
value={cond.values}
|
|
318
|
+
onChange={(vals) => { cond.values = vals; emitBuilder() }}
|
|
319
|
+
placeholder="Add value…"
|
|
320
|
+
ariaLabel="Values" />
|
|
321
|
+
{:else if isRangeOperator(cond.op)}
|
|
322
|
+
<SvTextInput
|
|
323
|
+
size="sm"
|
|
324
|
+
value={cond.value}
|
|
325
|
+
onChange={(v) => { cond.value = v; emitBuilder() }}
|
|
326
|
+
placeholder="From"
|
|
327
|
+
ariaLabel="From" />
|
|
328
|
+
<SvTextInput
|
|
329
|
+
size="sm"
|
|
330
|
+
value={cond.valueTo}
|
|
331
|
+
onChange={(v) => { cond.valueTo = v; emitBuilder() }}
|
|
332
|
+
placeholder="To"
|
|
333
|
+
ariaLabel="To" />
|
|
334
|
+
{:else}
|
|
335
|
+
<SvTextInput
|
|
336
|
+
size="sm"
|
|
337
|
+
value={cond.value}
|
|
338
|
+
onChange={(v) => { cond.value = v; emitBuilder() }}
|
|
339
|
+
placeholder="Value"
|
|
340
|
+
ariaLabel="Value" />
|
|
341
|
+
{/if}
|
|
342
|
+
<button
|
|
343
|
+
type="button"
|
|
344
|
+
class="sx-del"
|
|
345
|
+
onclick={() => removeChild(parent, i)}
|
|
346
|
+
aria-label="Remove condition">✕</button>
|
|
347
|
+
</div>
|
|
348
|
+
{/snippet}
|
|
349
|
+
|
|
350
|
+
<!--
|
|
351
|
+
A group and everything under it. Recursive: a snippet may render itself, which
|
|
352
|
+
is what lets one definition cover arbitrary nesting.
|
|
353
|
+
|
|
354
|
+
`depth` only drives presentation and whether another level may be added.
|
|
355
|
+
toBuilderTree enforces the same limit on the way in, so an expression too deep
|
|
356
|
+
to render never reaches here in the first place.
|
|
357
|
+
-->
|
|
358
|
+
{#snippet groupBody(group: BuilderGroup, depth: number)}
|
|
359
|
+
<div class="sx-combinator">
|
|
360
|
+
{#if group.negated}
|
|
361
|
+
<span class="sx-not">NOT</span>
|
|
362
|
+
{/if}
|
|
363
|
+
Match
|
|
364
|
+
<SvDropDownList
|
|
365
|
+
size="sm"
|
|
366
|
+
options={[{ value: 'and', label: 'all' }, { value: 'or', label: 'any' }]}
|
|
367
|
+
value={group.combinator}
|
|
368
|
+
onChange={(v) => { group.combinator = v as 'and' | 'or'; emitBuilder() }}
|
|
369
|
+
ariaLabel="Combine conditions" />
|
|
370
|
+
of the following:
|
|
371
|
+
</div>
|
|
372
|
+
<div class="sx-rows">
|
|
373
|
+
{#each group.children as child, i (i)}
|
|
374
|
+
{#if child.kind === 'cond'}
|
|
375
|
+
{@render conditionRow(child, group, i)}
|
|
376
|
+
{:else}
|
|
377
|
+
<div class="sx-group">
|
|
378
|
+
<div class="sx-group-head">
|
|
379
|
+
<button
|
|
380
|
+
type="button"
|
|
381
|
+
class="sx-neg"
|
|
382
|
+
class:sx-neg-on={child.negated}
|
|
383
|
+
aria-pressed={child.negated}
|
|
384
|
+
title="Negate this group"
|
|
385
|
+
onclick={() => { child.negated = !child.negated; emitBuilder() }}>NOT</button>
|
|
386
|
+
<span class="sx-group-spacer"></span>
|
|
387
|
+
<button
|
|
388
|
+
type="button"
|
|
389
|
+
class="sx-del"
|
|
390
|
+
onclick={() => removeChild(group, i)}
|
|
391
|
+
aria-label="Remove group">✕</button>
|
|
392
|
+
</div>
|
|
393
|
+
{@render groupBody(child, depth + 1)}
|
|
394
|
+
</div>
|
|
395
|
+
{/if}
|
|
396
|
+
{/each}
|
|
397
|
+
</div>
|
|
398
|
+
<div class="sx-actions">
|
|
399
|
+
<button type="button" class="sx-add" onclick={() => addCondition(group)}>
|
|
400
|
+
+ Add condition
|
|
401
|
+
</button>
|
|
402
|
+
{#if depth < MAX_BUILDER_DEPTH - 1}
|
|
403
|
+
<button type="button" class="sx-add" onclick={() => addGroup(group)}>
|
|
404
|
+
+ Add group
|
|
405
|
+
</button>
|
|
406
|
+
{/if}
|
|
407
|
+
</div>
|
|
408
|
+
{/snippet}
|
|
409
|
+
|
|
310
410
|
<style>
|
|
311
411
|
/* Theme-agnostic surfaces (see SvGridAlerts): currentColor + inherit. */
|
|
312
412
|
.sx-editor {
|
|
@@ -320,6 +420,9 @@
|
|
|
320
420
|
.sx-modes button { border: 0; background: transparent; color: inherit; opacity: 0.65; padding: 4px 12px; font-size: 12px; font-weight: 500; border-radius: 6px; cursor: pointer; transition: background 0.13s ease, opacity 0.13s ease; }
|
|
321
421
|
.sx-modes button:hover { opacity: 1; }
|
|
322
422
|
.sx-modes button.sx-active { background: var(--sg-accent, #4f46e5); color: #fff; opacity: 1; }
|
|
423
|
+
/* Not representable as a flat condition list - the tooltip says why. */
|
|
424
|
+
.sx-modes button:disabled { opacity: 0.35; cursor: not-allowed; }
|
|
425
|
+
.sx-modes button:disabled:hover { opacity: 0.35; }
|
|
323
426
|
.sx-match {
|
|
324
427
|
font-size: 11.5px; font-weight: 600; color: var(--sg-accent, #4f46e5);
|
|
325
428
|
background: color-mix(in srgb, var(--sg-accent, #4f46e5) 14%, transparent);
|
|
@@ -328,14 +431,42 @@
|
|
|
328
431
|
.sx-combinator { display: flex; align-items: center; gap: 6px; font-size: 12px; opacity: 0.7; }
|
|
329
432
|
.sx-rows { display: flex; flex-direction: column; gap: 7px; }
|
|
330
433
|
.sx-row {
|
|
331
|
-
|
|
434
|
+
/* minmax(0, 2fr), not 2fr: a grid track sizes to its content by default, so
|
|
435
|
+
the value input refused to shrink and pushed the row out of any narrow
|
|
436
|
+
panel (the form builder's rules pane, a docked alerts panel). */
|
|
437
|
+
display: grid; grid-template-columns: minmax(90px, 1fr) minmax(90px, 1fr) minmax(0, 2fr) auto; gap: 7px; align-items: center;
|
|
332
438
|
border: 1px solid color-mix(in srgb, currentColor 14%, transparent); border-radius: 9px; padding: 8px;
|
|
333
439
|
}
|
|
334
440
|
.sx-noval { text-align: center; font-size: 13px; opacity: 0.5; }
|
|
335
441
|
.sx-del { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; border: 0; background: transparent; color: inherit; opacity: 0.55; cursor: pointer; font-size: 13px; border-radius: 7px; transition: background 0.13s ease, color 0.13s ease, opacity 0.13s ease; }
|
|
336
442
|
.sx-del:hover { color: var(--sg-danger, #dc2626); opacity: 1; background: color-mix(in srgb, var(--sg-danger, #dc2626) 12%, transparent); }
|
|
443
|
+
.sx-actions { display: flex; gap: 7px; flex-wrap: wrap; }
|
|
337
444
|
.sx-add { align-self: flex-start; border: 1px dashed color-mix(in srgb, var(--sg-accent, #4f46e5) 45%, currentColor); background: transparent; color: var(--sg-accent, #4f46e5); border-radius: 8px; padding: 6px 12px; font-size: 12px; font-weight: 600; cursor: pointer; transition: background 0.13s ease; }
|
|
338
445
|
.sx-add:hover { background: color-mix(in srgb, var(--sg-accent, #4f46e5) 10%, transparent); }
|
|
446
|
+
/* A nested group. The left rule is what makes the nesting readable at a
|
|
447
|
+
glance - without it the rows of a subgroup look like siblings of the rows
|
|
448
|
+
above them, which is exactly the ambiguity the group exists to remove. */
|
|
449
|
+
.sx-group {
|
|
450
|
+
display: flex; flex-direction: column; gap: 8px;
|
|
451
|
+
border: 1px solid color-mix(in srgb, currentColor 14%, transparent);
|
|
452
|
+
border-left: 3px solid color-mix(in srgb, var(--sg-accent, #4f46e5) 55%, transparent);
|
|
453
|
+
border-radius: 9px; padding: 9px; margin-left: 2px;
|
|
454
|
+
background: color-mix(in srgb, currentColor 3%, transparent);
|
|
455
|
+
}
|
|
456
|
+
.sx-group-head { display: flex; align-items: center; gap: 6px; }
|
|
457
|
+
.sx-group-spacer { flex: 1; }
|
|
458
|
+
.sx-neg {
|
|
459
|
+
border: 1px solid color-mix(in srgb, currentColor 20%, transparent);
|
|
460
|
+
background: transparent; color: inherit; opacity: 0.6;
|
|
461
|
+
border-radius: 6px; padding: 2px 8px; font-size: 10.5px; font-weight: 700;
|
|
462
|
+
letter-spacing: 0.04em; cursor: pointer; transition: background 0.13s ease, opacity 0.13s ease;
|
|
463
|
+
}
|
|
464
|
+
.sx-neg:hover { opacity: 1; }
|
|
465
|
+
.sx-neg-on {
|
|
466
|
+
background: var(--sg-danger, #dc2626); border-color: var(--sg-danger, #dc2626);
|
|
467
|
+
color: #fff; opacity: 1;
|
|
468
|
+
}
|
|
469
|
+
.sx-not { font-size: 10.5px; font-weight: 700; letter-spacing: 0.04em; color: var(--sg-danger, #dc2626); }
|
|
339
470
|
.sx-error { margin: 0; font-size: 12px; color: var(--sg-danger, #dc2626); }
|
|
340
471
|
.sx-hint { margin: 0; font-size: 12px; opacity: 0.55; }
|
|
341
472
|
</style>
|