@tellegen/svelte 0.4.3 → 0.4.4
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 +20 -0
- package/dist/TellegenMap.svelte +63 -22
- package/dist/TellegenProvider.svelte +7 -4
- package/dist/TellegenShell.svelte +10 -1
- package/dist/TellegenViewer.svelte +2 -0
- package/dist/basemap.d.ts +14 -0
- package/dist/basemap.js +27 -0
- package/dist/colors.d.ts +6 -1
- package/dist/colors.js +11 -4
- package/dist/components/FormulationSelector.svelte +1 -1
- package/dist/components/LocalCaseDetails.svelte +18 -3
- package/dist/components/McResults.svelte +18 -3
- package/dist/components/McWorkbench.svelte +688 -0
- package/dist/components/McWorkbench.svelte.d.ts +38 -0
- package/dist/components/MultiWire.svelte +136 -0
- package/dist/components/MultiWire.svelte.d.ts +7 -0
- package/dist/components/MulticonductorDetails.svelte +7 -0
- package/dist/components/OmniSearch.svelte +163 -0
- package/dist/components/OmniSearch.svelte.d.ts +14 -0
- package/dist/components/ResultsTable.svelte +259 -0
- package/dist/components/ResultsTable.svelte.d.ts +13 -0
- package/dist/components/SchematicCanvas.svelte +1896 -0
- package/dist/components/SchematicCanvas.svelte.d.ts +13 -0
- package/dist/components/SolveCard.svelte +6 -0
- package/dist/components/schematic/PickCard.svelte +126 -0
- package/dist/components/schematic/PickCard.svelte.d.ts +56 -0
- package/dist/components/schematic/export.d.ts +21 -0
- package/dist/components/schematic/export.js +110 -0
- package/dist/components/schematic/layers.d.ts +161 -0
- package/dist/components/schematic/layers.js +756 -0
- package/dist/components/schematic/paint.d.ts +28 -0
- package/dist/components/schematic/paint.js +69 -0
- package/dist/components/schematic/palette.d.ts +34 -0
- package/dist/components/schematic/palette.js +54 -0
- package/dist/components/schematic/text.d.ts +83 -0
- package/dist/components/schematic/text.js +190 -0
- package/dist/components/schematic/view-prefs.svelte.d.ts +30 -0
- package/dist/components/schematic/view-prefs.svelte.js +77 -0
- package/dist/components.d.ts +1 -0
- package/dist/components.js +1 -0
- package/dist/context.svelte.d.ts +2 -0
- package/dist/context.svelte.js +2 -0
- package/dist/controller.svelte.d.ts +33 -5
- package/dist/controller.svelte.js +173 -20
- package/dist/diagram/basemap.d.ts +28 -0
- package/dist/diagram/basemap.js +39 -0
- package/dist/diagram/bus-stacks.d.ts +19 -0
- package/dist/diagram/bus-stacks.js +99 -0
- package/dist/diagram/colour-modes.d.ts +18 -0
- package/dist/diagram/colour-modes.js +33 -0
- package/dist/diagram/connections.d.ts +30 -0
- package/dist/diagram/connections.js +129 -0
- package/dist/diagram/display-tree.d.ts +21 -0
- package/dist/diagram/display-tree.js +65 -0
- package/dist/diagram/energisation.d.ts +10 -0
- package/dist/diagram/energisation.js +31 -0
- package/dist/diagram/feeders.d.ts +30 -0
- package/dist/diagram/feeders.js +57 -0
- package/dist/diagram/galvanic-zones.d.ts +60 -0
- package/dist/diagram/galvanic-zones.js +210 -0
- package/dist/diagram/graph-inputs.d.ts +18 -0
- package/dist/diagram/graph-inputs.js +103 -0
- package/dist/diagram/index.d.ts +28 -0
- package/dist/diagram/index.js +28 -0
- package/dist/diagram/landmarks.d.ts +50 -0
- package/dist/diagram/landmarks.js +153 -0
- package/dist/diagram/layout-morph.d.ts +15 -0
- package/dist/diagram/layout-morph.js +32 -0
- package/dist/diagram/multi-wire.d.ts +44 -0
- package/dist/diagram/multi-wire.js +65 -0
- package/dist/diagram/phase-bars.d.ts +35 -0
- package/dist/diagram/phase-bars.js +44 -0
- package/dist/diagram/results-table.d.ts +47 -0
- package/dist/diagram/results-table.js +173 -0
- package/dist/diagram/schematic-equipment.d.ts +133 -0
- package/dist/diagram/schematic-equipment.js +645 -0
- package/dist/diagram/schematic-layout.d.ts +51 -0
- package/dist/diagram/schematic-layout.js +240 -0
- package/dist/diagram/search.d.ts +36 -0
- package/dist/diagram/search.js +48 -0
- package/dist/diagram/shared-routes.d.ts +21 -0
- package/dist/diagram/shared-routes.js +40 -0
- package/dist/diagram/short-names.d.ts +11 -0
- package/dist/diagram/short-names.js +54 -0
- package/dist/diagram/snapshot.d.ts +115 -0
- package/dist/diagram/snapshot.js +242 -0
- package/dist/diagram/summary.d.ts +65 -0
- package/dist/diagram/summary.js +144 -0
- package/dist/diagram/svg-export.d.ts +80 -0
- package/dist/diagram/svg-export.js +108 -0
- package/dist/diagram/tree-walk.d.ts +13 -0
- package/dist/diagram/tree-walk.js +83 -0
- package/dist/diagram/value-labels.d.ts +24 -0
- package/dist/diagram/value-labels.js +69 -0
- package/dist/diagram/view-state.d.ts +12 -0
- package/dist/diagram/view-state.js +22 -0
- package/dist/diagram/voltage-levels.d.ts +14 -0
- package/dist/diagram/voltage-levels.js +20 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/state.svelte.d.ts +37 -1
- package/dist/state.svelte.js +35 -0
- package/dist/transformer-icon.d.ts +22 -13
- package/dist/transformer-icon.js +23 -13
- package/package.json +2 -2
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { DistGraph } from '@tellegen/engine';
|
|
2
|
+
import { type GraphInputs, type ResultSnapshot, type SearchIndex } from '../diagram/index.js';
|
|
3
|
+
type $$ComponentProps = {
|
|
4
|
+
/** The operating point projected onto `inputs` (`snapshotFromMcPf`); `null` while it is being
|
|
5
|
+
* fetched. `MulticonductorCase.overviewSnapshot` holds a case's, shared with the diagram. */
|
|
6
|
+
snapshot: ResultSnapshot | null;
|
|
7
|
+
graph: DistGraph;
|
|
8
|
+
/** `graphInputs(graph)`; `MulticonductorCase.overviewInputs` holds a case's. */
|
|
9
|
+
inputs: GraphInputs;
|
|
10
|
+
/** `buildSearchIndex(inputs.entities)`; `MulticonductorCase.overviewSearch` holds a case's. */
|
|
11
|
+
search: SearchIndex;
|
|
12
|
+
loading?: boolean;
|
|
13
|
+
selectedBus: string | null;
|
|
14
|
+
selectedEdge: string | null;
|
|
15
|
+
onselectbus: (id: string) => void;
|
|
16
|
+
onselectedge: (id: string) => void;
|
|
17
|
+
/** Case name, used in exported file names and provenance. */
|
|
18
|
+
label?: string;
|
|
19
|
+
/** When set, the overview is computed on request rather than after every solve. */
|
|
20
|
+
onanalyse?: () => void;
|
|
21
|
+
/** Whether the case is shown as a schematic, and how to toggle it. */
|
|
22
|
+
schematic?: boolean;
|
|
23
|
+
onschematic?: (show: boolean) => void;
|
|
24
|
+
/** Bus or branch under the pointer in any linked view. */
|
|
25
|
+
hovered?: {
|
|
26
|
+
kind: 'bus' | 'branch';
|
|
27
|
+
id: string;
|
|
28
|
+
} | null;
|
|
29
|
+
onhover?: (h: {
|
|
30
|
+
kind: 'bus' | 'branch';
|
|
31
|
+
id: string;
|
|
32
|
+
} | null) => void;
|
|
33
|
+
/** Bring the current selection into view again, for an issue clicked while selected. */
|
|
34
|
+
onreveal?: () => void;
|
|
35
|
+
};
|
|
36
|
+
declare const McWorkbench: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
37
|
+
type McWorkbench = ReturnType<typeof McWorkbench>;
|
|
38
|
+
export default McWorkbench;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { rgbaCss } from '../format.js';
|
|
3
|
+
import { isPhaseTerminal, phaseColor } from '../multiconductor.js';
|
|
4
|
+
import type { MultiWireView } from '../diagram/index.js';
|
|
5
|
+
|
|
6
|
+
let { view }: { view: MultiWireView } = $props();
|
|
7
|
+
|
|
8
|
+
const colour = (terminal: string) =>
|
|
9
|
+
isPhaseTerminal(terminal) ? rgbaCss(phaseColor(terminal)) : 'var(--text-secondary)';
|
|
10
|
+
const amps = (a: number | null) => (a === null ? '' : `${a.toFixed(a >= 100 ? 0 : 1)} A`);
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<figure class="multi-wire">
|
|
14
|
+
<svg
|
|
15
|
+
viewBox="0 0 {view.width} {view.height}"
|
|
16
|
+
role="img"
|
|
17
|
+
aria-label="Multi-wire view of {view.kind} {view.id}: {view.wires
|
|
18
|
+
.length} conductor segments between {view.buses[0].id} and {view.buses[1].id}"
|
|
19
|
+
>
|
|
20
|
+
{#each view.buses as bus, side (side)}
|
|
21
|
+
<text class="bus" x={bus.x} y="12" text-anchor="middle">{bus.id}</text>
|
|
22
|
+
{#each bus.terminals as t, r (r)}
|
|
23
|
+
<g>
|
|
24
|
+
{#if t.attachments.length}<title>{t.name}: {t.attachments.join(', ')}</title>{/if}
|
|
25
|
+
<line
|
|
26
|
+
class="rail"
|
|
27
|
+
x1={bus.x - 8}
|
|
28
|
+
x2={bus.x + 8}
|
|
29
|
+
y1={t.y}
|
|
30
|
+
y2={t.y}
|
|
31
|
+
stroke={colour(t.name)}
|
|
32
|
+
/>
|
|
33
|
+
<text
|
|
34
|
+
class="terminal"
|
|
35
|
+
x={bus.x + (bus.x < view.width / 2 ? -12 : 12)}
|
|
36
|
+
y={t.y + 3}
|
|
37
|
+
text-anchor={bus.x < view.width / 2 ? 'end' : 'start'}>{t.name}</text
|
|
38
|
+
>
|
|
39
|
+
{#if t.grounded}
|
|
40
|
+
<!-- Earth symbol: three shrinking bars below the rail end. -->
|
|
41
|
+
<g
|
|
42
|
+
class="earth"
|
|
43
|
+
transform="translate({bus.x + (bus.x < view.width / 2 ? -26 : 26)} {t.y})"
|
|
44
|
+
>
|
|
45
|
+
<line x1="-4" x2="4" y1="4" y2="4" /><line
|
|
46
|
+
x1="-2.6"
|
|
47
|
+
x2="2.6"
|
|
48
|
+
y1="6.5"
|
|
49
|
+
y2="6.5"
|
|
50
|
+
/><line x1="-1.2" x2="1.2" y1="9" y2="9" />
|
|
51
|
+
</g>
|
|
52
|
+
{/if}
|
|
53
|
+
</g>
|
|
54
|
+
{/each}
|
|
55
|
+
{/each}
|
|
56
|
+
{#if view.winding}
|
|
57
|
+
<rect
|
|
58
|
+
class="winding"
|
|
59
|
+
x={view.winding.x}
|
|
60
|
+
y={view.winding.y}
|
|
61
|
+
width={view.winding.width}
|
|
62
|
+
height={view.winding.height}
|
|
63
|
+
rx="4"
|
|
64
|
+
/>
|
|
65
|
+
<text
|
|
66
|
+
class="terminal"
|
|
67
|
+
x={view.width / 2}
|
|
68
|
+
y={view.winding.y + view.winding.height + 12}
|
|
69
|
+
text-anchor="middle">windings</text
|
|
70
|
+
>
|
|
71
|
+
{/if}
|
|
72
|
+
{#each view.wires as w, i (i)}
|
|
73
|
+
<line
|
|
74
|
+
class="wire"
|
|
75
|
+
class:open={!view.closed}
|
|
76
|
+
x1={w.x1}
|
|
77
|
+
y1={w.y1}
|
|
78
|
+
x2={w.x2}
|
|
79
|
+
y2={w.y2}
|
|
80
|
+
stroke={colour(w.terminal)}
|
|
81
|
+
/>
|
|
82
|
+
{#if w.amps !== null}
|
|
83
|
+
<text class="amps" x={(w.x1 + w.x2) / 2} y={(w.y1 + w.y2) / 2 - 3} text-anchor="middle"
|
|
84
|
+
>{amps(w.amps)}</text
|
|
85
|
+
>
|
|
86
|
+
{/if}
|
|
87
|
+
{/each}
|
|
88
|
+
</svg>
|
|
89
|
+
<figcaption>
|
|
90
|
+
Every conductor between the two terminal sets{view.winding
|
|
91
|
+
? '; transformer windings couple magnetically, with no conductor continuity across them'
|
|
92
|
+
: ''}{view.closed ? '' : '; open'}. Currents are magnitudes at the from end{view.winding
|
|
93
|
+
? ' and the to end'
|
|
94
|
+
: ''}. Hover a terminal for its attachments.
|
|
95
|
+
</figcaption>
|
|
96
|
+
</figure>
|
|
97
|
+
|
|
98
|
+
<style>
|
|
99
|
+
.multi-wire {
|
|
100
|
+
margin: 8px 0 0;
|
|
101
|
+
}
|
|
102
|
+
svg {
|
|
103
|
+
display: block;
|
|
104
|
+
width: 100%;
|
|
105
|
+
height: auto;
|
|
106
|
+
}
|
|
107
|
+
svg text {
|
|
108
|
+
font: 9px var(--font-mono);
|
|
109
|
+
fill: var(--text-secondary);
|
|
110
|
+
}
|
|
111
|
+
svg text.bus {
|
|
112
|
+
fill: var(--ink);
|
|
113
|
+
}
|
|
114
|
+
.rail {
|
|
115
|
+
stroke-width: 3;
|
|
116
|
+
}
|
|
117
|
+
.wire {
|
|
118
|
+
stroke-width: 1.6;
|
|
119
|
+
}
|
|
120
|
+
.wire.open {
|
|
121
|
+
stroke-dasharray: 4 3;
|
|
122
|
+
}
|
|
123
|
+
.winding {
|
|
124
|
+
fill: var(--paper);
|
|
125
|
+
stroke: var(--ink);
|
|
126
|
+
stroke-width: 1;
|
|
127
|
+
}
|
|
128
|
+
.earth line {
|
|
129
|
+
stroke: var(--text-secondary);
|
|
130
|
+
stroke-width: 1;
|
|
131
|
+
}
|
|
132
|
+
figcaption {
|
|
133
|
+
color: var(--text-secondary);
|
|
134
|
+
font-size: 11px;
|
|
135
|
+
}
|
|
136
|
+
</style>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { MultiWireView } from '../diagram/index.js';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
view: MultiWireView;
|
|
4
|
+
};
|
|
5
|
+
declare const MultiWire: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
6
|
+
type MultiWire = ReturnType<typeof MultiWire>;
|
|
7
|
+
export default MultiWire;
|
|
@@ -262,6 +262,13 @@
|
|
|
262
262
|
</span>
|
|
263
263
|
</div>
|
|
264
264
|
|
|
265
|
+
{#if mc.graph}<button
|
|
266
|
+
class="reset"
|
|
267
|
+
aria-pressed={mc.schematic}
|
|
268
|
+
title="Symbols and the engine's schematic; results once solved"
|
|
269
|
+
onclick={() => (mc.schematic = !mc.schematic)}
|
|
270
|
+
>{mc.schematic ? 'Show map' : 'Show diagram'}</button
|
|
271
|
+
>{/if}
|
|
265
272
|
<button class="reset" onclick={() => app.requestFrame(mc.id)}>Fit case</button>
|
|
266
273
|
<button class="reset" onclick={() => ctrl.removeMultiCase(mc)}>Remove</button>
|
|
267
274
|
{/if}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { search, type SearchIndex, type SearchMatch } from '../diagram/index.js';
|
|
3
|
+
|
|
4
|
+
let {
|
|
5
|
+
index,
|
|
6
|
+
onselect,
|
|
7
|
+
hint = () => null,
|
|
8
|
+
placeholder = 'Find a bus or branch'
|
|
9
|
+
}: {
|
|
10
|
+
index: SearchIndex;
|
|
11
|
+
onselect: (match: SearchMatch) => void;
|
|
12
|
+
/** A short result value shown beside each match, such as a voltage or loading. */
|
|
13
|
+
hint?: (match: SearchMatch) => { text: string; state?: string } | null;
|
|
14
|
+
placeholder?: string;
|
|
15
|
+
} = $props();
|
|
16
|
+
|
|
17
|
+
const id = $props.id();
|
|
18
|
+
let query = $state('');
|
|
19
|
+
let active = $state(0);
|
|
20
|
+
let open = $state(false);
|
|
21
|
+
const found = $derived(search(index, query, { limit: 10 }));
|
|
22
|
+
|
|
23
|
+
function choose(match: SearchMatch | undefined) {
|
|
24
|
+
if (!match) return;
|
|
25
|
+
onselect(match);
|
|
26
|
+
query = '';
|
|
27
|
+
open = false;
|
|
28
|
+
}
|
|
29
|
+
function onkeydown(e: KeyboardEvent) {
|
|
30
|
+
const n = found.matches.length;
|
|
31
|
+
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
|
32
|
+
e.preventDefault();
|
|
33
|
+
if (n) active = (active + (e.key === 'ArrowDown' ? 1 : -1) + n) % n;
|
|
34
|
+
open = true;
|
|
35
|
+
} else if (e.key === 'Enter') {
|
|
36
|
+
e.preventDefault();
|
|
37
|
+
choose(found.matches[active]);
|
|
38
|
+
} else if (e.key === 'Escape') {
|
|
39
|
+
if (query) query = '';
|
|
40
|
+
else open = false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<div class="omni">
|
|
46
|
+
<input
|
|
47
|
+
type="search"
|
|
48
|
+
role="combobox"
|
|
49
|
+
aria-label={placeholder}
|
|
50
|
+
aria-expanded={open && !!query.trim()}
|
|
51
|
+
aria-controls="{id}-results"
|
|
52
|
+
aria-activedescendant={open && found.matches.length ? `${id}-${active}` : undefined}
|
|
53
|
+
autocomplete="off"
|
|
54
|
+
spellcheck="false"
|
|
55
|
+
{placeholder}
|
|
56
|
+
bind:value={query}
|
|
57
|
+
oninput={() => {
|
|
58
|
+
active = 0;
|
|
59
|
+
open = true;
|
|
60
|
+
}}
|
|
61
|
+
onfocus={() => (open = true)}
|
|
62
|
+
onblur={() => setTimeout(() => (open = false), 120)}
|
|
63
|
+
{onkeydown}
|
|
64
|
+
/>
|
|
65
|
+
{#if open && query.trim()}
|
|
66
|
+
<ul id="{id}-results" role="listbox" aria-label="Search results">
|
|
67
|
+
{#each found.matches as match, i (`${match.kind}:${match.index}`)}
|
|
68
|
+
{@const h = hint(match)}
|
|
69
|
+
<li
|
|
70
|
+
id="{id}-{i}"
|
|
71
|
+
role="option"
|
|
72
|
+
aria-selected={i === active}
|
|
73
|
+
onpointerdown={(e) => {
|
|
74
|
+
e.preventDefault();
|
|
75
|
+
choose(match);
|
|
76
|
+
}}
|
|
77
|
+
>
|
|
78
|
+
<span class="kind">{match.kind === 'bus' ? 'bus' : match.family}</span>
|
|
79
|
+
<span class="id">{match.id}</span>
|
|
80
|
+
{#if h}<span class="hint {h.state ?? ''}">{h.text}</span>{/if}
|
|
81
|
+
</li>
|
|
82
|
+
{:else}
|
|
83
|
+
<li class="empty" role="presentation">No bus or branch ID contains that text.</li>
|
|
84
|
+
{/each}
|
|
85
|
+
{#if found.total > found.matches.length}<li class="more" role="presentation">
|
|
86
|
+
{found.total.toLocaleString()} matches · keep typing to narrow
|
|
87
|
+
</li>{/if}
|
|
88
|
+
</ul>
|
|
89
|
+
{/if}
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<style>
|
|
93
|
+
.omni {
|
|
94
|
+
position: relative;
|
|
95
|
+
}
|
|
96
|
+
input {
|
|
97
|
+
width: 100%;
|
|
98
|
+
box-sizing: border-box;
|
|
99
|
+
font: 11px var(--font-mono);
|
|
100
|
+
color: var(--ink);
|
|
101
|
+
background: var(--paper);
|
|
102
|
+
border: 1px solid var(--line);
|
|
103
|
+
border-radius: 3px;
|
|
104
|
+
padding: 6px 8px;
|
|
105
|
+
}
|
|
106
|
+
ul {
|
|
107
|
+
position: absolute;
|
|
108
|
+
z-index: 5;
|
|
109
|
+
left: 0;
|
|
110
|
+
right: 0;
|
|
111
|
+
top: calc(100% + 2px);
|
|
112
|
+
margin: 0;
|
|
113
|
+
padding: 3px;
|
|
114
|
+
list-style: none;
|
|
115
|
+
max-height: 260px;
|
|
116
|
+
overflow: auto;
|
|
117
|
+
background: var(--paper);
|
|
118
|
+
border: 1px solid var(--line);
|
|
119
|
+
border-radius: 3px;
|
|
120
|
+
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.16);
|
|
121
|
+
}
|
|
122
|
+
li {
|
|
123
|
+
display: grid;
|
|
124
|
+
grid-template-columns: 70px 1fr auto;
|
|
125
|
+
gap: 6px;
|
|
126
|
+
align-items: baseline;
|
|
127
|
+
padding: 5px 6px;
|
|
128
|
+
border-radius: 2px;
|
|
129
|
+
cursor: pointer;
|
|
130
|
+
}
|
|
131
|
+
li[aria-selected='true'] {
|
|
132
|
+
background: var(--accent-soft);
|
|
133
|
+
}
|
|
134
|
+
.kind,
|
|
135
|
+
.empty,
|
|
136
|
+
.more {
|
|
137
|
+
color: var(--text-secondary);
|
|
138
|
+
}
|
|
139
|
+
.kind {
|
|
140
|
+
font: 9px var(--font-mono);
|
|
141
|
+
text-transform: uppercase;
|
|
142
|
+
}
|
|
143
|
+
.id,
|
|
144
|
+
.hint {
|
|
145
|
+
font-family: var(--font-mono);
|
|
146
|
+
white-space: nowrap;
|
|
147
|
+
}
|
|
148
|
+
.id {
|
|
149
|
+
overflow: hidden;
|
|
150
|
+
text-overflow: ellipsis;
|
|
151
|
+
}
|
|
152
|
+
.hint.violation {
|
|
153
|
+
color: var(--text-danger);
|
|
154
|
+
}
|
|
155
|
+
.hint.warning {
|
|
156
|
+
color: var(--text-accent);
|
|
157
|
+
}
|
|
158
|
+
.empty,
|
|
159
|
+
.more {
|
|
160
|
+
display: block;
|
|
161
|
+
cursor: default;
|
|
162
|
+
}
|
|
163
|
+
</style>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type SearchIndex, type SearchMatch } from '../diagram/index.js';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
index: SearchIndex;
|
|
4
|
+
onselect: (match: SearchMatch) => void;
|
|
5
|
+
/** A short result value shown beside each match, such as a voltage or loading. */
|
|
6
|
+
hint?: (match: SearchMatch) => {
|
|
7
|
+
text: string;
|
|
8
|
+
state?: string;
|
|
9
|
+
} | null;
|
|
10
|
+
placeholder?: string;
|
|
11
|
+
};
|
|
12
|
+
declare const OmniSearch: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
13
|
+
type OmniSearch = ReturnType<typeof OmniSearch>;
|
|
14
|
+
export default OmniSearch;
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { queryTable, type TableModel } from '../diagram/index.js';
|
|
3
|
+
|
|
4
|
+
let {
|
|
5
|
+
model,
|
|
6
|
+
selected,
|
|
7
|
+
onselect,
|
|
8
|
+
hovered = -1,
|
|
9
|
+
onhover
|
|
10
|
+
}: {
|
|
11
|
+
model: TableModel;
|
|
12
|
+
/** Row index (network order) of the current selection, or −1. */
|
|
13
|
+
selected: number;
|
|
14
|
+
onselect: (index: number) => void;
|
|
15
|
+
/** Row under the pointer here or in a linked view (−1 for none). */
|
|
16
|
+
hovered?: number;
|
|
17
|
+
onhover?: (index: number) => void;
|
|
18
|
+
} = $props();
|
|
19
|
+
|
|
20
|
+
const ROW = 22;
|
|
21
|
+
const HEIGHT = 240;
|
|
22
|
+
const OVERSCAN = 6;
|
|
23
|
+
let text = $state('');
|
|
24
|
+
let issuesOnly = $state(false);
|
|
25
|
+
let sortKey = $state<string | null>(null);
|
|
26
|
+
let descending = $state(false);
|
|
27
|
+
let scrollTop = $state(0);
|
|
28
|
+
let scroller = $state<HTMLDivElement>();
|
|
29
|
+
|
|
30
|
+
const rows = $derived(queryTable(model, { text, issuesOnly, sortKey, descending }));
|
|
31
|
+
const first = $derived(Math.max(0, Math.floor(scrollTop / ROW) - OVERSCAN));
|
|
32
|
+
const last = $derived(Math.min(rows.length, first + Math.ceil(HEIGHT / ROW) + 2 * OVERSCAN));
|
|
33
|
+
const visible = $derived(Array.from(rows.subarray(first, last)));
|
|
34
|
+
const template = $derived(
|
|
35
|
+
model.columns
|
|
36
|
+
.map((c) =>
|
|
37
|
+
c.numeric
|
|
38
|
+
? 'minmax(64px, 0.8fr)'
|
|
39
|
+
: c.key === 'id'
|
|
40
|
+
? 'minmax(110px, 1.5fr)'
|
|
41
|
+
: 'minmax(70px, 1fr)'
|
|
42
|
+
)
|
|
43
|
+
.join(' ')
|
|
44
|
+
);
|
|
45
|
+
const format = (v: unknown, digits = 2) =>
|
|
46
|
+
v === null || v === undefined
|
|
47
|
+
? '—'
|
|
48
|
+
: typeof v === 'number'
|
|
49
|
+
? v.toLocaleString(undefined, {
|
|
50
|
+
minimumFractionDigits: digits,
|
|
51
|
+
maximumFractionDigits: digits
|
|
52
|
+
})
|
|
53
|
+
: String(v);
|
|
54
|
+
|
|
55
|
+
function sort(key: string, numeric?: boolean) {
|
|
56
|
+
// Numbers start largest-first, text A–Z; a second click reverses.
|
|
57
|
+
descending = sortKey === key ? !descending : !!numeric;
|
|
58
|
+
sortKey = key;
|
|
59
|
+
}
|
|
60
|
+
// Bring the selection into view when it changes elsewhere (map, issues, search).
|
|
61
|
+
$effect(() => {
|
|
62
|
+
const k = rows.indexOf(selected);
|
|
63
|
+
if (k < 0 || !scroller) return;
|
|
64
|
+
const y = k * ROW;
|
|
65
|
+
if (y < scroller.scrollTop || y + ROW > scroller.scrollTop + HEIGHT)
|
|
66
|
+
scroller.scrollTop = y - HEIGHT / 2;
|
|
67
|
+
});
|
|
68
|
+
function onkeydown(e: KeyboardEvent) {
|
|
69
|
+
if (!['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(e.key) || !rows.length) return;
|
|
70
|
+
e.preventDefault();
|
|
71
|
+
const k = rows.indexOf(selected);
|
|
72
|
+
const next =
|
|
73
|
+
e.key === 'Home'
|
|
74
|
+
? 0
|
|
75
|
+
: e.key === 'End'
|
|
76
|
+
? rows.length - 1
|
|
77
|
+
: Math.max(
|
|
78
|
+
0,
|
|
79
|
+
Math.min(rows.length - 1, (k < 0 ? -1 : k) + (e.key === 'ArrowDown' ? 1 : -1))
|
|
80
|
+
);
|
|
81
|
+
onselect(rows[next]);
|
|
82
|
+
}
|
|
83
|
+
</script>
|
|
84
|
+
|
|
85
|
+
<div class="table">
|
|
86
|
+
<div class="toolbar">
|
|
87
|
+
<input
|
|
88
|
+
type="search"
|
|
89
|
+
placeholder="Filter by ID or endpoint"
|
|
90
|
+
aria-label="Filter rows"
|
|
91
|
+
bind:value={text}
|
|
92
|
+
/>
|
|
93
|
+
<label><input type="checkbox" bind:checked={issuesOnly} /> Issues only</label>
|
|
94
|
+
</div>
|
|
95
|
+
<!-- The grid is a keyboard target: arrow keys move the shared selection. -->
|
|
96
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
97
|
+
<div
|
|
98
|
+
class="scroll"
|
|
99
|
+
role="grid"
|
|
100
|
+
aria-label="Results by {model.kind === 'bus' ? 'bus' : 'branch'}"
|
|
101
|
+
aria-rowcount={rows.length}
|
|
102
|
+
tabindex="0"
|
|
103
|
+
style:height="{HEIGHT}px"
|
|
104
|
+
bind:this={scroller}
|
|
105
|
+
onscroll={(e) => (scrollTop = e.currentTarget.scrollTop)}
|
|
106
|
+
{onkeydown}
|
|
107
|
+
>
|
|
108
|
+
<div class="head" role="row" style:grid-template-columns={template}>
|
|
109
|
+
{#each model.columns as c (c.key)}
|
|
110
|
+
<div
|
|
111
|
+
role="columnheader"
|
|
112
|
+
class:num={c.numeric}
|
|
113
|
+
aria-sort={sortKey === c.key ? (descending ? 'descending' : 'ascending') : 'none'}
|
|
114
|
+
>
|
|
115
|
+
<button title={c.title ?? `Sort by ${c.label}`} onclick={() => sort(c.key, c.numeric)}
|
|
116
|
+
>{c.label}{#if c.unit}<small>{c.unit}</small>{/if}</button
|
|
117
|
+
>
|
|
118
|
+
</div>
|
|
119
|
+
{/each}
|
|
120
|
+
</div>
|
|
121
|
+
<div class="body" style:height="{rows.length * ROW}px">
|
|
122
|
+
{#each visible as i, n (i)}
|
|
123
|
+
{@const row = model.row(i)}
|
|
124
|
+
<!-- Arrow keys on the grid move the selection; a click is the pointer equivalent. -->
|
|
125
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
126
|
+
<div
|
|
127
|
+
class="row {row.state}"
|
|
128
|
+
role="row"
|
|
129
|
+
tabindex="-1"
|
|
130
|
+
aria-selected={i === selected}
|
|
131
|
+
class:hovered={i === hovered}
|
|
132
|
+
style:top="{(first + n) * ROW}px"
|
|
133
|
+
style:grid-template-columns={template}
|
|
134
|
+
onclick={() => onselect(i)}
|
|
135
|
+
onpointerenter={() => onhover?.(i)}
|
|
136
|
+
onpointerleave={() => onhover?.(-1)}
|
|
137
|
+
>
|
|
138
|
+
{#each model.columns as c (c.key)}
|
|
139
|
+
<span role="gridcell" class:num={c.numeric}>{format(row[c.key], c.digits)}</span>
|
|
140
|
+
{/each}
|
|
141
|
+
</div>
|
|
142
|
+
{/each}
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
<p class="count">{rows.length.toLocaleString()} of {model.count.toLocaleString()} rows</p>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<style>
|
|
149
|
+
.table {
|
|
150
|
+
font-size: 11px;
|
|
151
|
+
}
|
|
152
|
+
.toolbar {
|
|
153
|
+
display: flex;
|
|
154
|
+
gap: 8px;
|
|
155
|
+
align-items: center;
|
|
156
|
+
margin-bottom: 4px;
|
|
157
|
+
}
|
|
158
|
+
.toolbar input[type='search'] {
|
|
159
|
+
flex: 1;
|
|
160
|
+
min-width: 0;
|
|
161
|
+
font: 11px var(--font-mono);
|
|
162
|
+
color: var(--ink);
|
|
163
|
+
background: var(--paper);
|
|
164
|
+
border: 1px solid var(--line);
|
|
165
|
+
border-radius: 3px;
|
|
166
|
+
padding: 5px 7px;
|
|
167
|
+
}
|
|
168
|
+
.toolbar label,
|
|
169
|
+
.count,
|
|
170
|
+
small {
|
|
171
|
+
color: var(--text-secondary);
|
|
172
|
+
}
|
|
173
|
+
.scroll {
|
|
174
|
+
position: relative;
|
|
175
|
+
overflow: auto;
|
|
176
|
+
border: 1px solid var(--line);
|
|
177
|
+
border-radius: 3px;
|
|
178
|
+
font: 10px var(--font-mono);
|
|
179
|
+
}
|
|
180
|
+
.scroll:focus-visible {
|
|
181
|
+
outline: 2px solid var(--accent);
|
|
182
|
+
outline-offset: 2px;
|
|
183
|
+
}
|
|
184
|
+
.head {
|
|
185
|
+
position: sticky;
|
|
186
|
+
top: 0;
|
|
187
|
+
z-index: 1;
|
|
188
|
+
display: grid;
|
|
189
|
+
min-width: max-content;
|
|
190
|
+
background: var(--surface-control);
|
|
191
|
+
border-bottom: 1px solid var(--line);
|
|
192
|
+
}
|
|
193
|
+
.head button {
|
|
194
|
+
width: 100%;
|
|
195
|
+
padding: 4px 6px;
|
|
196
|
+
text-align: left;
|
|
197
|
+
font: inherit;
|
|
198
|
+
color: var(--text-secondary);
|
|
199
|
+
background: transparent;
|
|
200
|
+
border: 0;
|
|
201
|
+
cursor: pointer;
|
|
202
|
+
white-space: nowrap;
|
|
203
|
+
}
|
|
204
|
+
.head .num button {
|
|
205
|
+
text-align: right;
|
|
206
|
+
}
|
|
207
|
+
.head [aria-sort='ascending'] button,
|
|
208
|
+
.head [aria-sort='descending'] button {
|
|
209
|
+
color: var(--ink);
|
|
210
|
+
}
|
|
211
|
+
small {
|
|
212
|
+
display: block;
|
|
213
|
+
font-size: 9px;
|
|
214
|
+
}
|
|
215
|
+
.body {
|
|
216
|
+
position: relative;
|
|
217
|
+
min-width: max-content;
|
|
218
|
+
}
|
|
219
|
+
.row {
|
|
220
|
+
position: absolute;
|
|
221
|
+
left: 0;
|
|
222
|
+
right: 0;
|
|
223
|
+
height: 22px;
|
|
224
|
+
display: grid;
|
|
225
|
+
align-items: center;
|
|
226
|
+
cursor: pointer;
|
|
227
|
+
border-bottom: 1px solid color-mix(in srgb, var(--line) 50%, transparent);
|
|
228
|
+
}
|
|
229
|
+
.row span {
|
|
230
|
+
padding: 0 6px;
|
|
231
|
+
overflow: hidden;
|
|
232
|
+
text-overflow: ellipsis;
|
|
233
|
+
white-space: nowrap;
|
|
234
|
+
}
|
|
235
|
+
.num {
|
|
236
|
+
text-align: right;
|
|
237
|
+
}
|
|
238
|
+
.row.hovered {
|
|
239
|
+
box-shadow: inset 3px 0 var(--line-strong, var(--text-secondary));
|
|
240
|
+
}
|
|
241
|
+
.row[aria-selected='true'] {
|
|
242
|
+
background: var(--accent-soft);
|
|
243
|
+
box-shadow: inset 3px 0 var(--accent);
|
|
244
|
+
}
|
|
245
|
+
.violation span:last-child {
|
|
246
|
+
color: var(--text-danger);
|
|
247
|
+
}
|
|
248
|
+
.warning span:last-child {
|
|
249
|
+
color: var(--text-accent);
|
|
250
|
+
}
|
|
251
|
+
.inactive,
|
|
252
|
+
.missing,
|
|
253
|
+
.unbounded {
|
|
254
|
+
color: var(--text-secondary);
|
|
255
|
+
}
|
|
256
|
+
.count {
|
|
257
|
+
margin: 4px 0 0;
|
|
258
|
+
}
|
|
259
|
+
</style>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type TableModel } from '../diagram/index.js';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
model: TableModel;
|
|
4
|
+
/** Row index (network order) of the current selection, or −1. */
|
|
5
|
+
selected: number;
|
|
6
|
+
onselect: (index: number) => void;
|
|
7
|
+
/** Row under the pointer here or in a linked view (−1 for none). */
|
|
8
|
+
hovered?: number;
|
|
9
|
+
onhover?: (index: number) => void;
|
|
10
|
+
};
|
|
11
|
+
declare const ResultsTable: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
12
|
+
type ResultsTable = ReturnType<typeof ResultsTable>;
|
|
13
|
+
export default ResultsTable;
|