@tellegen/svelte 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +269 -5
  2. package/dist/Sparkline.svelte +2 -2
  3. package/dist/TellegenMap.svelte +518 -202
  4. package/dist/TellegenMap.svelte.d.ts +45 -0
  5. package/dist/TellegenProvider.svelte +40 -4
  6. package/dist/TellegenShell.svelte +24 -46
  7. package/dist/TellegenViewer.svelte +12 -2
  8. package/dist/api.d.ts +18 -3
  9. package/dist/api.js +8 -2
  10. package/dist/basemap.d.ts +192 -0
  11. package/dist/basemap.js +259 -0
  12. package/dist/colors.d.ts +25 -7
  13. package/dist/colors.js +39 -13
  14. package/dist/components/AppFooter.svelte +3 -2
  15. package/dist/components/AppHeader.svelte +44 -19
  16. package/dist/components/BindingLines.svelte +4 -7
  17. package/dist/components/BusPicker.svelte +11 -5
  18. package/dist/components/ControlPanel.svelte +106 -343
  19. package/dist/components/DemandSlider.svelte +36 -29
  20. package/dist/components/DiagramCanvas.svelte +486 -0
  21. package/dist/components/DiagramCanvas.svelte.d.ts +13 -0
  22. package/dist/components/DisplayControls.svelte +8 -2
  23. package/dist/components/DropZone.svelte +1 -1
  24. package/dist/components/FormulationSelector.svelte +9 -4
  25. package/dist/components/LocalCaseDetails.svelte +61 -48
  26. package/dist/components/McResults.svelte +428 -0
  27. package/dist/components/McResults.svelte.d.ts +17 -0
  28. package/dist/components/ModelDetails.svelte +230 -0
  29. package/dist/components/ModelDetails.svelte.d.ts +7 -0
  30. package/dist/components/MulticonductorDetails.svelte +232 -83
  31. package/dist/components/NetworkStats.svelte +20 -18
  32. package/dist/components/NotificationCenter.svelte +245 -0
  33. package/dist/components/NotificationCenter.svelte.d.ts +3 -0
  34. package/dist/components/PanelCard.svelte +384 -0
  35. package/dist/components/PanelCard.svelte.d.ts +9 -0
  36. package/dist/components/PanelFrame.svelte +46 -0
  37. package/dist/components/PanelFrame.svelte.d.ts +16 -0
  38. package/dist/components/PanelHost.svelte +237 -0
  39. package/dist/components/PanelHost.svelte.d.ts +6 -0
  40. package/dist/components/PlacementCue.svelte +1 -1
  41. package/dist/components/PowerFlowDetails.svelte +99 -0
  42. package/dist/components/PowerFlowDetails.svelte.d.ts +3 -0
  43. package/dist/components/RatingSlider.svelte +19 -14
  44. package/dist/components/RestoreDefaultsButton.svelte +1 -1
  45. package/dist/components/SavedNetworkDetails.svelte +214 -0
  46. package/dist/components/SavedNetworkDetails.svelte.d.ts +3 -0
  47. package/dist/components/SensitivityReadout.svelte +26 -18
  48. package/dist/components/ShellEnvironment.svelte +51 -0
  49. package/dist/components/ShellEnvironment.svelte.d.ts +18 -0
  50. package/dist/components/SizeLegend.svelte +2 -2
  51. package/dist/components/SolveCard.svelte +48 -51
  52. package/dist/components/TopMovers.svelte +9 -4
  53. package/dist/components/TopMovers.svelte.d.ts +2 -17
  54. package/dist/components.d.ts +5 -0
  55. package/dist/components.js +5 -0
  56. package/dist/context.svelte.d.ts +8 -1
  57. package/dist/context.svelte.js +8 -3
  58. package/dist/controller.svelte.d.ts +117 -36
  59. package/dist/controller.svelte.js +1133 -253
  60. package/dist/diagram-labels.d.ts +9 -0
  61. package/dist/diagram-labels.js +40 -0
  62. package/dist/display.d.ts +3 -7
  63. package/dist/display.js +31 -27
  64. package/dist/format.d.ts +9 -0
  65. package/dist/format.js +17 -4
  66. package/dist/index.d.ts +15 -5
  67. package/dist/index.js +9 -2
  68. package/dist/map.d.ts +4 -2
  69. package/dist/map.js +2 -1
  70. package/dist/maplibre-worker.d.ts +5 -0
  71. package/dist/maplibre-worker.dev.d.ts +11 -0
  72. package/dist/maplibre-worker.dev.js +15 -0
  73. package/dist/maplibre-worker.js +8 -0
  74. package/dist/multiconductor.d.ts +11 -2
  75. package/dist/multiconductor.js +107 -14
  76. package/dist/notices.svelte.d.ts +29 -0
  77. package/dist/notices.svelte.js +86 -0
  78. package/dist/panels.svelte.d.ts +71 -0
  79. package/dist/panels.svelte.js +245 -0
  80. package/dist/slider.d.ts +12 -0
  81. package/dist/slider.js +31 -0
  82. package/dist/state.svelte.d.ts +107 -23
  83. package/dist/state.svelte.js +106 -20
  84. package/dist/styles.css +59 -6
  85. package/dist/synthetic-layout.js +1 -0
  86. package/package.json +29 -11
  87. package/dist/study-package.d.ts +0 -8
  88. package/dist/study-package.js +0 -17
@@ -0,0 +1,245 @@
1
+ <script lang="ts">
2
+ import { onDestroy, untrack } from 'svelte';
3
+ import { formatPowerIoDiagnostic } from '@tellegen/engine';
4
+ import { getAppState, getController } from '../context.svelte.js';
5
+ import { getNoticeCenter, errorNoticeTitle } from '../notices.svelte.js';
6
+ const app = getAppState();
7
+ const ctrl = getController();
8
+ const notices = getNoticeCenter();
9
+ let root = $state.raw<HTMLElement>();
10
+ let focused = $state(false);
11
+ let hovered = $state(false);
12
+ const observed = new WeakSet<object>();
13
+ $effect(() => {
14
+ const message = app.error;
15
+ const detail = app.errorDetail;
16
+ const revision = app.errorRevision;
17
+ const retry = app.errorRetry;
18
+ if (message)
19
+ untrack(() =>
20
+ notices.push({
21
+ kind: 'error',
22
+ // A message with its own detail is already written for people.
23
+ title: detail ? message : errorNoticeTitle(message),
24
+ details: detail ?? message,
25
+ retry: () => ctrl.retryError(),
26
+ canRetry: () =>
27
+ app.errorRevision === revision && app.error === message && app.errorRetry === retry
28
+ })
29
+ );
30
+ });
31
+ $effect(() => {
32
+ const local = app.activeLocal;
33
+ const multi = app.activeMulti;
34
+ const summary = local?.summary ?? multi?.summary;
35
+ const geo = local?.geoWarnings ?? multi?.geoWarnings;
36
+ for (const [identity, title, details] of [
37
+ [
38
+ summary,
39
+ 'Case import notes',
40
+ summary
41
+ ? [
42
+ ...summary.diagnostics.map(formatPowerIoDiagnostic),
43
+ ...('warnings' in summary ? summary.warnings : [])
44
+ ].join('\n\n')
45
+ : ''
46
+ ],
47
+ [geo, 'Coordinate import notes', geo?.join('\n\n') ?? '']
48
+ ] as Array<[object | undefined, string, string]>) {
49
+ if (identity && !observed.has(identity)) {
50
+ observed.add(identity);
51
+ if (details) untrack(() => notices.push({ kind: 'warning', title, details }));
52
+ }
53
+ }
54
+ });
55
+ $effect(() => {
56
+ notices.pause(focused || hovered || notices.expanded);
57
+ });
58
+ onDestroy(() => notices.dispose());
59
+ </script>
60
+
61
+ <div class="announcement" role="status" aria-live="polite" aria-atomic="true">
62
+ {notices.announcement}
63
+ </div>
64
+ {#if notices.entries.length}
65
+ <section
66
+ class="notifications"
67
+ aria-label="Notifications"
68
+ bind:this={root}
69
+ onfocusin={() => (focused = true)}
70
+ onfocusout={(event) => {
71
+ focused = root?.contains(event.relatedTarget as Node | null) ?? false;
72
+ }}
73
+ onpointerenter={() => (hovered = true)}
74
+ onpointerleave={() => (hovered = false)}
75
+ >
76
+ {#if notices.expanded}
77
+ <div class="recent" id="recent-notifications">
78
+ <header>
79
+ <strong>Recent messages</strong><button
80
+ onclick={() => {
81
+ focused = false;
82
+ hovered = false;
83
+ notices.clear();
84
+ }}>Clear</button
85
+ >
86
+ </header>
87
+ <ol>
88
+ {#each notices.entries as entry (entry.id)}
89
+ <li>
90
+ <details>
91
+ <summary
92
+ >{entry.title}{#if entry.count > 1}<span class="count">{entry.count}</span
93
+ >{/if}</summary
94
+ >
95
+ <pre data-testid="notification-details">{entry.details}</pre>
96
+ </details>
97
+ {#if entry.retry && entry.canRetry?.()}<button onclick={entry.retry}>Retry</button
98
+ >{/if}
99
+ </li>
100
+ {/each}
101
+ </ol>
102
+ </div>
103
+ {/if}
104
+ <div
105
+ class="notice"
106
+ data-testid="notification-toast"
107
+ data-kind={notices.active?.kind}
108
+ class:quiet={!notices.active}
109
+ >
110
+ {#if notices.active}
111
+ <span class="message">{notices.active.title}</span>
112
+ {#if notices.active.count > 1}<span
113
+ class="count"
114
+ aria-label={`${notices.active.count} occurrences`}>{notices.active.count}</span
115
+ >{/if}
116
+ {#if notices.active.retry && notices.active.canRetry?.()}<button
117
+ onclick={notices.active.retry}>Retry</button
118
+ >{/if}
119
+ {/if}
120
+ <button
121
+ class="history"
122
+ aria-expanded={notices.expanded}
123
+ aria-controls="recent-notifications"
124
+ onclick={() => (notices.expanded = !notices.expanded)}
125
+ >{notices.active ? 'Details' : 'Messages'}{#if !notices.active}<span class="count"
126
+ >{notices.entries.length}</span
127
+ >{/if}</button
128
+ >
129
+ {#if notices.active}<button
130
+ aria-label="Dismiss notification"
131
+ onclick={() => notices.dismiss()}>Close</button
132
+ >{/if}
133
+ </div>
134
+ </section>
135
+ {/if}
136
+
137
+ <style>
138
+ .announcement {
139
+ position: absolute;
140
+ width: 1px;
141
+ height: 1px;
142
+ overflow: hidden;
143
+ clip-path: inset(50%);
144
+ }
145
+ .notifications {
146
+ position: fixed;
147
+ z-index: 65;
148
+ bottom: 40px;
149
+ left: 50%;
150
+ transform: translateX(-50%);
151
+ width: min(540px, calc(100vw - 200px));
152
+ font: 12px/1.4 var(--font-display);
153
+ pointer-events: none;
154
+ }
155
+ .notice,
156
+ .recent {
157
+ pointer-events: auto;
158
+ background: var(--panel);
159
+ border: 1px solid var(--line);
160
+ border-radius: 5px;
161
+ box-shadow: var(--elev-2);
162
+ }
163
+ .notice {
164
+ display: flex;
165
+ align-items: center;
166
+ gap: 8px;
167
+ min-height: 38px;
168
+ padding: 3px 8px 3px 12px;
169
+ box-sizing: border-box;
170
+ }
171
+ .message {
172
+ min-width: 0;
173
+ flex: 1;
174
+ overflow: hidden;
175
+ text-overflow: ellipsis;
176
+ white-space: nowrap;
177
+ }
178
+ .quiet {
179
+ width: fit-content;
180
+ margin: auto;
181
+ padding-left: 5px;
182
+ background: var(--paper);
183
+ }
184
+ button {
185
+ font: inherit;
186
+ color: var(--ink);
187
+ border: 0;
188
+ background: transparent;
189
+ padding: 6px;
190
+ border-radius: 3px;
191
+ cursor: pointer;
192
+ flex-shrink: 0;
193
+ }
194
+ button:hover {
195
+ background: var(--accent-soft);
196
+ }
197
+ button:focus-visible,
198
+ summary:focus-visible {
199
+ outline: 2px solid var(--focus-ring);
200
+ outline-offset: 2px;
201
+ }
202
+ .count {
203
+ margin-left: 5px;
204
+ font: 10px var(--font-mono);
205
+ color: var(--text-secondary);
206
+ }
207
+ .recent {
208
+ margin-bottom: 6px;
209
+ max-height: min(320px, 40dvh);
210
+ overflow-y: auto;
211
+ }
212
+ header {
213
+ display: flex;
214
+ align-items: center;
215
+ justify-content: space-between;
216
+ padding: 8px 12px;
217
+ }
218
+ ol {
219
+ list-style: none;
220
+ padding: 0;
221
+ margin: 0;
222
+ }
223
+ li {
224
+ padding: 10px 12px;
225
+ border-top: 1px solid var(--line);
226
+ }
227
+ summary {
228
+ cursor: pointer;
229
+ }
230
+ pre {
231
+ font: 11px/1.5 var(--font-mono);
232
+ white-space: pre-wrap;
233
+ overflow-wrap: anywhere;
234
+ margin: 10px 0 0;
235
+ }
236
+ @media (max-width: 960px) {
237
+ .notifications {
238
+ width: calc(100vw - 24px);
239
+ bottom: 60px;
240
+ }
241
+ button {
242
+ min-height: 36px;
243
+ }
244
+ }
245
+ </style>
@@ -0,0 +1,3 @@
1
+ declare const NotificationCenter: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type NotificationCenter = ReturnType<typeof NotificationCenter>;
3
+ export default NotificationCenter;
@@ -0,0 +1,384 @@
1
+ <script lang="ts">
2
+ import { tick } from 'svelte';
3
+ import {
4
+ focusFirst,
5
+ getPanelLayout,
6
+ type PanelRegistration,
7
+ type PanelSide
8
+ } from '../panels.svelte.js';
9
+ let {
10
+ panel,
11
+ floating = false,
12
+ mobile = false
13
+ }: {
14
+ panel: PanelRegistration;
15
+ floating?: boolean;
16
+ mobile?: boolean;
17
+ } = $props();
18
+ const layout = getPanelLayout();
19
+ let menu = $state(false);
20
+ let menuAnchor: HTMLDivElement | undefined = $state();
21
+ let card: HTMLElement;
22
+ const bounds = $derived(layout.bounds(panel));
23
+ const preferred = $derived(!floating && !mobile && layout.preferred(panel));
24
+ const dockReserve = $derived.by(() => {
25
+ const side = layout.position(panel);
26
+ return side === 'floating'
27
+ ? 0
28
+ : Math.max(0, layout.ordered(side).filter((candidate) => candidate.open).length - 1) * 142;
29
+ });
30
+ function start(event: PointerEvent, kind: 'move' | 'resize') {
31
+ if (mobile || !event.isPrimary || event.button !== 0) return;
32
+ const rect = card.getBoundingClientRect();
33
+ layout.beginGesture(panel, kind, event.clientX, event.clientY, {
34
+ x: rect.x,
35
+ y: rect.y,
36
+ width: rect.width,
37
+ height: rect.height
38
+ });
39
+ (event.currentTarget as HTMLElement).setPointerCapture(event.pointerId);
40
+ event.preventDefault();
41
+ }
42
+ async function key(event: KeyboardEvent) {
43
+ if (mobile) return;
44
+ const panelId = panel.id;
45
+ const step = event.shiftKey ? 40 : 10;
46
+ const directions: Record<string, [number, number]> = {
47
+ ArrowLeft: [-step, 0],
48
+ ArrowRight: [step, 0],
49
+ ArrowUp: [0, -step],
50
+ ArrowDown: [0, step]
51
+ };
52
+ if (directions[event.key]) {
53
+ event.preventDefault();
54
+ const [x, y] = directions[event.key];
55
+ const rect = card.getBoundingClientRect();
56
+ layout.float(panel, {
57
+ x: rect.x + x,
58
+ y: rect.y + y,
59
+ width: rect.width,
60
+ height: rect.height
61
+ });
62
+ layout.save();
63
+ await tick();
64
+ document.querySelector<HTMLElement>(`[data-panel="${CSS.escape(panelId)}"] .title`)?.focus();
65
+ } else if (event.key === 'Escape') {
66
+ menu = false;
67
+ }
68
+ }
69
+ function resizeKey(event: KeyboardEvent) {
70
+ const step = event.shiftKey ? 40 : 10;
71
+ if (!['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(event.key)) return;
72
+ event.preventDefault();
73
+ layout.float(panel, {
74
+ ...bounds,
75
+ width:
76
+ bounds.width + (event.key === 'ArrowRight' ? step : event.key === 'ArrowLeft' ? -step : 0),
77
+ height:
78
+ bounds.height + (event.key === 'ArrowDown' ? step : event.key === 'ArrowUp' ? -step : 0)
79
+ });
80
+ layout.save();
81
+ }
82
+ async function position(side?: PanelSide) {
83
+ const panelId = panel.id;
84
+ menu = false;
85
+ if (side) layout.dock(panel, side);
86
+ else {
87
+ layout.float(panel);
88
+ layout.save();
89
+ }
90
+ await tick();
91
+ document.querySelector<HTMLElement>(`[data-panel="${CSS.escape(panelId)}"] .title`)?.focus();
92
+ }
93
+ async function close() {
94
+ const title = panel.title;
95
+ const position = layout.position(panel);
96
+ const dock = position === 'floating' ? panel.side : position;
97
+ const opener = layout.opener(panel);
98
+ layout.close(panel);
99
+ await tick();
100
+ // The toolbar button for the panel; a host without the toolbar gets the
101
+ // control that opened it, else the panel's dock (the drawer when narrow).
102
+ focusFirst([
103
+ document.querySelector<HTMLElement>(
104
+ `.panel-toolbar button[aria-label="${CSS.escape(title)}"]`
105
+ ),
106
+ opener,
107
+ document.querySelector<HTMLElement>(`[data-panel-dock="${dock}"], [data-panel-drawer]`)
108
+ ]);
109
+ }
110
+ // Keyboard users can scroll an overflowing panel only through focus, so the
111
+ // content becomes a labelled, focusable region exactly while it overflows.
112
+ let overflowing = $state(false);
113
+ function trackOverflow(content: HTMLElement) {
114
+ const sync = () => (overflowing = content.scrollHeight > content.clientHeight + 1);
115
+ const resize = new ResizeObserver(sync);
116
+ const observe = () => {
117
+ resize.disconnect();
118
+ resize.observe(content);
119
+ for (const child of content.children) resize.observe(child);
120
+ sync();
121
+ };
122
+ const children = new MutationObserver(observe);
123
+ children.observe(content, { childList: true });
124
+ observe();
125
+ return () => {
126
+ resize.disconnect();
127
+ children.disconnect();
128
+ };
129
+ }
130
+ </script>
131
+
132
+ <svelte:window
133
+ onkeydown={(event) => {
134
+ if (event.key === 'Escape') menu = false;
135
+ }}
136
+ onpointerdown={(event) => {
137
+ if (menu && !menuAnchor?.contains(event.target as Node)) menu = false;
138
+ }}
139
+ />
140
+ <aside
141
+ bind:this={card}
142
+ class="panel-frame"
143
+ class:floating
144
+ class:mobile
145
+ class:preferred
146
+ data-panel={panel.id}
147
+ aria-label={panel.title}
148
+ style:left={floating ? `${bounds.x}px` : undefined}
149
+ style:top={floating ? `${bounds.y}px` : undefined}
150
+ style:width={floating ? `${bounds.width}px` : undefined}
151
+ style:height={floating ? `${bounds.height}px` : undefined}
152
+ style:max-height={preferred ? `calc(100% - ${dockReserve}px)` : undefined}
153
+ >
154
+ <header>
155
+ {#if mobile}
156
+ <h2 class="title">{panel.title}</h2>
157
+ {:else}
158
+ <button
159
+ class="title"
160
+ aria-label={`Move ${panel.title} panel`}
161
+ title="Click to expand. Drag or use arrow keys to move. Open panel options to dock."
162
+ onpointerdown={(event) => start(event, 'move')}
163
+ onkeydown={key}
164
+ onclick={() => layout.activate(panel)}>{panel.title}</button
165
+ >
166
+ {/if}
167
+ <div class="header-actions">{@render panel.headerActions?.()}</div>
168
+ {#if !mobile}<div class="menu-anchor" bind:this={menuAnchor}>
169
+ <button
170
+ class="icon"
171
+ aria-label={`${panel.title} panel options`}
172
+ aria-expanded={menu}
173
+ onclick={() => (menu = !menu)}
174
+ title="Panel options"
175
+ >
176
+ <svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true"
177
+ ><circle cx="3" cy="8" r="1" /><circle cx="8" cy="8" r="1" /><circle
178
+ cx="13"
179
+ cy="8"
180
+ r="1"
181
+ /></svg
182
+ >
183
+ </button>
184
+ {#if menu}<div class="panel-menu">
185
+ <button onclick={() => position('left')}>Dock left</button>
186
+ <button onclick={() => position('right')}>Dock right</button>
187
+ <button onclick={() => position()}>Detach</button>
188
+ </div>{/if}
189
+ </div>{/if}
190
+ <button
191
+ class="icon close"
192
+ aria-label={`Close ${panel.title.toLowerCase()} panel`}
193
+ title="Close panel"
194
+ onclick={close}
195
+ >
196
+ <svg width="14" height="14" viewBox="0 0 16 16" aria-hidden="true"
197
+ ><path d="m4 4 8 8m0-8-8 8" stroke="currentColor" stroke-width="1.5" fill="none" /></svg
198
+ >
199
+ </button>
200
+ </header>
201
+ <!-- A scroll container must take focus for keyboard scrolling, and this one
202
+ takes it only while it overflows. -->
203
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
204
+ <div
205
+ class="panel-content"
206
+ {@attach trackOverflow}
207
+ tabindex={overflowing ? 0 : undefined}
208
+ role={overflowing ? 'region' : undefined}
209
+ aria-label={overflowing ? `${panel.title} panel content` : undefined}
210
+ >
211
+ {@render panel.children()}
212
+ </div>
213
+ {#if floating}<button
214
+ class="resize"
215
+ aria-label={`Resize ${panel.title} panel`}
216
+ title="Drag to resize, or use arrow keys"
217
+ onpointerdown={(event) => start(event, 'resize')}
218
+ onkeydown={resizeKey}
219
+ >
220
+ <svg width="12" height="12" viewBox="0 0 12 12" aria-hidden="true"
221
+ ><path d="m3 10 7-7m-3 7 3-3" stroke="currentColor" /></svg
222
+ >
223
+ </button>{/if}
224
+ </aside>
225
+
226
+ <style>
227
+ .panel-frame {
228
+ display: flex;
229
+ flex-direction: column;
230
+ min-height: 130px;
231
+ max-height: 100%;
232
+ flex: 0 1 auto;
233
+ pointer-events: auto;
234
+ background: var(--paper);
235
+ color: var(--ink);
236
+ border: 1px solid var(--line);
237
+ border-radius: 5px;
238
+ box-shadow: var(--elev-1);
239
+ font: 13px/1.5 var(--font-display);
240
+ position: relative;
241
+ }
242
+ header {
243
+ display: flex;
244
+ align-items: center;
245
+ gap: 5px;
246
+ padding: 8px 10px 8px 14px;
247
+ flex: none;
248
+ border-bottom: 1px solid var(--line);
249
+ }
250
+ button {
251
+ color: inherit;
252
+ font: inherit;
253
+ cursor: pointer;
254
+ border: 0;
255
+ background: transparent;
256
+ border-radius: 3px;
257
+ }
258
+ button:hover {
259
+ background: var(--accent-soft);
260
+ }
261
+ button:focus-visible {
262
+ outline: 2px solid var(--focus-ring);
263
+ outline-offset: 2px;
264
+ }
265
+ .title {
266
+ flex: 1;
267
+ min-width: 0;
268
+ text-align: left;
269
+ font-size: inherit;
270
+ font-weight: 600;
271
+ margin: 0;
272
+ padding: 3px 0;
273
+ cursor: grab;
274
+ touch-action: none;
275
+ }
276
+ .title:active {
277
+ cursor: grabbing;
278
+ }
279
+ .icon {
280
+ display: grid;
281
+ place-items: center;
282
+ width: 26px;
283
+ height: 28px;
284
+ padding: 0;
285
+ flex: none;
286
+ }
287
+ .icon svg {
288
+ fill: currentColor;
289
+ }
290
+ /* Coarse pointers: a 44px hit area around the 26x28 close button. The
291
+ pseudo-element reaches into the header padding and the gap, so nothing
292
+ moves. */
293
+ @media (pointer: coarse) {
294
+ .close {
295
+ position: relative;
296
+ }
297
+ .close::after {
298
+ content: '';
299
+ position: absolute;
300
+ inset: -8px -10px -8px -8px;
301
+ }
302
+ }
303
+ .header-actions {
304
+ display: flex;
305
+ align-items: center;
306
+ gap: 8px;
307
+ }
308
+ .header-actions :global(button) {
309
+ font: 11px/1.3 var(--font-display);
310
+ padding: 4px;
311
+ border: 0;
312
+ background: none;
313
+ color: var(--text-secondary);
314
+ }
315
+ .panel-content {
316
+ min-height: 0;
317
+ overflow: auto;
318
+ overscroll-behavior: contain;
319
+ display: flex;
320
+ flex-direction: column;
321
+ border-radius: 0 0 5px 5px;
322
+ }
323
+ .panel-content:focus-visible {
324
+ outline-offset: -2px;
325
+ }
326
+ .menu-anchor {
327
+ position: relative;
328
+ }
329
+ .panel-menu {
330
+ position: absolute;
331
+ z-index: 3;
332
+ top: 100%;
333
+ right: 0;
334
+ min-width: 130px;
335
+ padding: 5px;
336
+ background: var(--paper);
337
+ border: 1px solid var(--line);
338
+ border-radius: 4px;
339
+ box-shadow: var(--elev-2);
340
+ }
341
+ .panel-menu button {
342
+ display: block;
343
+ padding: 7px 10px;
344
+ width: 100%;
345
+ text-align: left;
346
+ white-space: nowrap;
347
+ font-size: 12px;
348
+ }
349
+ .preferred {
350
+ flex-shrink: 0;
351
+ }
352
+ .floating {
353
+ position: fixed;
354
+ min-height: 0;
355
+ z-index: 40;
356
+ max-height: none;
357
+ box-shadow: var(--elev-2);
358
+ }
359
+ .floating .panel-content {
360
+ flex: 1;
361
+ padding-bottom: 12px;
362
+ }
363
+ .resize {
364
+ position: absolute;
365
+ bottom: 0;
366
+ right: 0;
367
+ width: 20px;
368
+ height: 20px;
369
+ cursor: nwse-resize;
370
+ touch-action: none;
371
+ color: var(--text-secondary);
372
+ }
373
+ .mobile {
374
+ min-height: 0;
375
+ height: 100%;
376
+ border-radius: 10px 10px 0 0;
377
+ }
378
+ .mobile .panel-content {
379
+ flex: 1;
380
+ }
381
+ .mobile .title {
382
+ cursor: default;
383
+ }
384
+ </style>
@@ -0,0 +1,9 @@
1
+ import { type PanelRegistration } from '../panels.svelte.js';
2
+ type $$ComponentProps = {
3
+ panel: PanelRegistration;
4
+ floating?: boolean;
5
+ mobile?: boolean;
6
+ };
7
+ declare const PanelCard: import("svelte").Component<$$ComponentProps, {}, "">;
8
+ type PanelCard = ReturnType<typeof PanelCard>;
9
+ export default PanelCard;
@@ -0,0 +1,46 @@
1
+ <script lang="ts">
2
+ import { onMount, untrack, type Snippet } from 'svelte';
3
+ import { getPanelLayout, type PanelSide } from '../panels.svelte.js';
4
+ let {
5
+ id,
6
+ title,
7
+ side,
8
+ order = 0,
9
+ width = 340,
10
+ open = $bindable(false),
11
+ children,
12
+ headerActions,
13
+ onopen
14
+ }: {
15
+ id: string;
16
+ title: string;
17
+ side: PanelSide;
18
+ order?: number;
19
+ width?: number;
20
+ open?: boolean;
21
+ children: Snippet;
22
+ headerActions?: Snippet;
23
+ onopen?: () => void;
24
+ } = $props();
25
+ const layout = getPanelLayout();
26
+ onMount(() =>
27
+ layout.register({
28
+ id,
29
+ title,
30
+ side,
31
+ order,
32
+ width,
33
+ open,
34
+ children,
35
+ headerActions,
36
+ onopen,
37
+ setOpen: (value) => {
38
+ open = value;
39
+ }
40
+ })
41
+ );
42
+ $effect(() => {
43
+ const values = { id, open, title, width };
44
+ untrack(() => layout.update(values.id, values.open, values.title, values.width));
45
+ });
46
+ </script>
@@ -0,0 +1,16 @@
1
+ import { type Snippet } from 'svelte';
2
+ import { type PanelSide } from '../panels.svelte.js';
3
+ type $$ComponentProps = {
4
+ id: string;
5
+ title: string;
6
+ side: PanelSide;
7
+ order?: number;
8
+ width?: number;
9
+ open?: boolean;
10
+ children: Snippet;
11
+ headerActions?: Snippet;
12
+ onopen?: () => void;
13
+ };
14
+ declare const PanelFrame: import("svelte").Component<$$ComponentProps, {}, "open">;
15
+ type PanelFrame = ReturnType<typeof PanelFrame>;
16
+ export default PanelFrame;