@rokkit/app 1.15.0 → 1.15.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/components/ScreenInfo.svelte +11 -3
- package/dist/components/ScreenInfo.svelte.d.ts +2 -1
- package/dist/components/ScreenShell.svelte +40 -34
- package/dist/types/screen-shell.d.ts +16 -1
- package/dist/utils/screen-shell.d.ts +3 -1
- package/dist/utils/screen-shell.js +12 -3
- package/package.json +6 -6
|
@@ -7,16 +7,24 @@
|
|
|
7
7
|
* the role.
|
|
8
8
|
*/
|
|
9
9
|
import type { Snippet } from 'svelte'
|
|
10
|
+
import type { HTMLAttributes } from 'svelte/elements'
|
|
10
11
|
import type { GlyphMode, InfoColumn, InfoRole, InfoSlot } from '../types/screen-shell.js'
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
// `role` is the band's wash, not the ARIA role, so it stays out of the passthrough.
|
|
14
|
+
interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'role'> {
|
|
13
15
|
columns: InfoSlot[]
|
|
14
16
|
role?: InfoRole
|
|
15
17
|
glyphs?: GlyphMode
|
|
16
18
|
class?: string
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
const {
|
|
21
|
+
const {
|
|
22
|
+
columns,
|
|
23
|
+
role = 'accent',
|
|
24
|
+
glyphs = 'text',
|
|
25
|
+
class: className = '',
|
|
26
|
+
...rest
|
|
27
|
+
}: Props = $props()
|
|
20
28
|
|
|
21
29
|
const isSnippet = (slot: InfoSlot): slot is Snippet => typeof slot === 'function'
|
|
22
30
|
const percent = (progress: number) => `${Math.round(Math.max(0, Math.min(1, progress)) * 100)}%`
|
|
@@ -72,7 +80,7 @@
|
|
|
72
80
|
{#if info.text}<p data-screen-info-text>{info.text}</p>{/if}
|
|
73
81
|
{/snippet}
|
|
74
82
|
|
|
75
|
-
<div data-screen-info data-role={role} class={className || undefined}>
|
|
83
|
+
<div {...rest} data-screen-info data-role={role} class={className || undefined}>
|
|
76
84
|
{#each columns.slice(0, 3) as slot, index (index)}
|
|
77
85
|
<div data-screen-info-column>
|
|
78
86
|
{#if isSnippet(slot)}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
1
2
|
import type { GlyphMode, InfoRole, InfoSlot } from '../types/screen-shell.js';
|
|
2
|
-
interface Props {
|
|
3
|
+
interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'role'> {
|
|
3
4
|
columns: InfoSlot[];
|
|
4
5
|
role?: InfoRole;
|
|
5
6
|
glyphs?: GlyphMode;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
/**
|
|
3
|
-
* One screen, laid out from data: a title bar (mark, title, context, then
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* One screen, laid out from data: a title bar (mark, title, context, then the screen's own
|
|
4
|
+
* buttons and the band toggles), a description, the info / stats / filters bands (search is
|
|
5
|
+
* the filters band's first control), a body and a sidebar that each scroll on their own, and
|
|
6
|
+
* a footer. The shell fills
|
|
6
7
|
* its container; nothing but the body and the sidebar scrolls.
|
|
7
8
|
*
|
|
8
9
|
* Layout only, and it persists nothing: `panels` and `filter` are bindable (and emitted
|
|
@@ -69,7 +70,10 @@
|
|
|
69
70
|
glyphs = 'text',
|
|
70
71
|
icons,
|
|
71
72
|
labels,
|
|
72
|
-
class: className = ''
|
|
73
|
+
class: className = '',
|
|
74
|
+
toggleAttrs,
|
|
75
|
+
bandAttrs,
|
|
76
|
+
...rest
|
|
73
77
|
}: ScreenShellProps = $props()
|
|
74
78
|
|
|
75
79
|
const text = $derived({ ...messages.screen, ...labels })
|
|
@@ -81,7 +85,9 @@
|
|
|
81
85
|
const has = $derived<Record<PanelId, boolean>>({
|
|
82
86
|
info: Boolean(info?.length),
|
|
83
87
|
stats: stats !== undefined,
|
|
84
|
-
filters
|
|
88
|
+
// Search lives in the filters band, so a screen with search has the band even when it
|
|
89
|
+
// passes no filters of its own.
|
|
90
|
+
filters: Boolean(filters) || search !== false,
|
|
85
91
|
side: Boolean(sidebar)
|
|
86
92
|
})
|
|
87
93
|
const present = $derived(presentPanels(has))
|
|
@@ -101,7 +107,8 @@
|
|
|
101
107
|
panels,
|
|
102
108
|
labels: text,
|
|
103
109
|
icons: { ...DEFAULT_ICONS, ...icons },
|
|
104
|
-
busy: text.busy
|
|
110
|
+
busy: text.busy,
|
|
111
|
+
toggleAttrs
|
|
105
112
|
})
|
|
106
113
|
)
|
|
107
114
|
|
|
@@ -209,55 +216,38 @@
|
|
|
209
216
|
|
|
210
217
|
{#snippet side()}
|
|
211
218
|
{#if sidebar && shown('side')}
|
|
212
|
-
<aside data-screen-sidebar data-side={sidebarSide} aria-label={text.side}>
|
|
219
|
+
<aside {...bandAttrs?.side} data-screen-sidebar data-side={sidebarSide} aria-label={text.side}>
|
|
213
220
|
{@render sidebar()}
|
|
214
221
|
</aside>
|
|
215
222
|
{/if}
|
|
216
223
|
{/snippet}
|
|
217
224
|
|
|
218
|
-
<div data-screen-shell data-view={viewId} class={className || undefined}>
|
|
225
|
+
<div {...rest} data-screen-shell data-view={viewId} class={className || undefined}>
|
|
219
226
|
<header data-screen-header>
|
|
220
227
|
<div data-screen-titlebar>
|
|
221
228
|
{@render glyph(mark.kanji, mark.icon)}
|
|
222
229
|
<h1 data-screen-title>{title}</h1>
|
|
223
230
|
{#if context}<span data-screen-context>{context}</span>{/if}
|
|
224
|
-
{#if
|
|
231
|
+
{#if items.length}
|
|
225
232
|
<div data-screen-tools>
|
|
226
|
-
{
|
|
227
|
-
<label data-screen-search>
|
|
228
|
-
<span
|
|
229
|
-
data-screen-search-icon
|
|
230
|
-
class={DEFAULT_STATE_ICONS.action.search}
|
|
231
|
-
aria-hidden="true"
|
|
232
|
-
></span>
|
|
233
|
-
<input
|
|
234
|
-
type="search"
|
|
235
|
-
bind:value={query}
|
|
236
|
-
oninput={onsearch}
|
|
237
|
-
{placeholder}
|
|
238
|
-
aria-label={placeholder}
|
|
239
|
-
/>
|
|
240
|
-
</label>
|
|
241
|
-
{/if}
|
|
242
|
-
{#if items.length}
|
|
243
|
-
<Toolbar {items} label={title} width="fit" showDividers={false} onclick={runItem} />
|
|
244
|
-
{/if}
|
|
233
|
+
<Toolbar {items} label={title} width="fit" showDividers={false} onclick={runItem} />
|
|
245
234
|
</div>
|
|
246
235
|
{/if}
|
|
247
236
|
</div>
|
|
248
237
|
{#if description}<p data-screen-description>{description}</p>{/if}
|
|
249
238
|
|
|
250
239
|
{#if info?.length && shown('info')}
|
|
251
|
-
<ScreenInfo columns={info} role={infoRole} {glyphs} />
|
|
240
|
+
<ScreenInfo columns={info} role={infoRole} {glyphs} {...bandAttrs?.info} />
|
|
252
241
|
{/if}
|
|
253
242
|
|
|
254
243
|
{#if stats !== undefined && shown('stats')}
|
|
255
|
-
<div data-screen-stats aria-label={text.stats} role="group">
|
|
244
|
+
<div {...bandAttrs?.stats} data-screen-stats aria-label={text.stats} role="group">
|
|
256
245
|
{#if stats === null}
|
|
257
246
|
<p data-screen-stats-reading>{text.reading}</p>
|
|
258
247
|
{:else}
|
|
259
248
|
{#each stats as stat (stat.id)}
|
|
260
249
|
<StatCard
|
|
250
|
+
{...stat.attrs}
|
|
261
251
|
label={stat.title}
|
|
262
252
|
value={stat.value}
|
|
263
253
|
tone={stat.tone}
|
|
@@ -276,8 +266,24 @@
|
|
|
276
266
|
</div>
|
|
277
267
|
{/if}
|
|
278
268
|
|
|
279
|
-
{#if
|
|
280
|
-
<div data-screen-filters role="group" aria-label={text.filters}>
|
|
269
|
+
{#if shown('filters')}
|
|
270
|
+
<div {...bandAttrs?.filters} data-screen-filters role="group" aria-label={text.filters}>
|
|
271
|
+
{#if searchOn}
|
|
272
|
+
<label data-screen-search>
|
|
273
|
+
<span
|
|
274
|
+
data-screen-search-icon
|
|
275
|
+
class={DEFAULT_STATE_ICONS.action.search}
|
|
276
|
+
aria-hidden="true"
|
|
277
|
+
></span>
|
|
278
|
+
<input
|
|
279
|
+
type="search"
|
|
280
|
+
bind:value={query}
|
|
281
|
+
oninput={onsearch}
|
|
282
|
+
{placeholder}
|
|
283
|
+
aria-label={placeholder}
|
|
284
|
+
/>
|
|
285
|
+
</label>
|
|
286
|
+
{/if}
|
|
281
287
|
{#if filterSnippet}
|
|
282
288
|
{@render filterSnippet(api)}
|
|
283
289
|
{:else if filterSchema}
|
|
@@ -312,9 +318,9 @@
|
|
|
312
318
|
</div>
|
|
313
319
|
|
|
314
320
|
{#if footerSnippet}
|
|
315
|
-
<footer data-screen-footer>{@render footerSnippet()}</footer>
|
|
321
|
+
<footer {...bandAttrs?.footer} data-screen-footer>{@render footerSnippet()}</footer>
|
|
316
322
|
{:else if footerParts}
|
|
317
|
-
<footer data-screen-footer>
|
|
323
|
+
<footer {...bandAttrs?.footer} data-screen-footer>
|
|
318
324
|
{#each FOOTER_PARTS as part (part)}
|
|
319
325
|
{#if footerParts[part]}<span data-part={part}>{footerParts[part]}</span>{/if}
|
|
320
326
|
{/each}
|
|
@@ -7,7 +7,14 @@
|
|
|
7
7
|
* `filter` are bindable and the host stores them.
|
|
8
8
|
*/
|
|
9
9
|
import type { Snippet } from 'svelte';
|
|
10
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
10
11
|
import type { ToolbarItem } from '@rokkit/ui';
|
|
12
|
+
/**
|
|
13
|
+
* Attributes a host hangs on a shell part for its own hooks (`data-testid`) or extra
|
|
14
|
+
* semantics (`aria-*`): components take them as rest props, data (actions, stats, toolbar
|
|
15
|
+
* items) carries them in an `attrs` field, rendered on the element.
|
|
16
|
+
*/
|
|
17
|
+
export type ShellAttrs = Record<string, string>;
|
|
11
18
|
/** The bands a screen can open and close from its toolbar, in toolbar order. */
|
|
12
19
|
export type PanelId = 'info' | 'stats' | 'filters' | 'side';
|
|
13
20
|
/** Which bands are open. A missing key is open, so a first visit opens everything. */
|
|
@@ -33,6 +40,8 @@ export interface ShellAction {
|
|
|
33
40
|
href?: string;
|
|
34
41
|
busy?: boolean;
|
|
35
42
|
disabled?: string | boolean;
|
|
43
|
+
/** Attributes rendered on the action's button or link (e.g. `data-testid`). */
|
|
44
|
+
attrs?: ShellAttrs;
|
|
36
45
|
[key: string]: unknown;
|
|
37
46
|
}
|
|
38
47
|
/**
|
|
@@ -89,6 +98,8 @@ export interface ShellStat {
|
|
|
89
98
|
value: unknown;
|
|
90
99
|
};
|
|
91
100
|
href?: string;
|
|
101
|
+
/** Attributes rendered on the stat's card (e.g. `data-testid`). */
|
|
102
|
+
attrs?: ShellAttrs;
|
|
92
103
|
}
|
|
93
104
|
/** The filter values, the one source of truth, and the active stat's id. */
|
|
94
105
|
export interface FilterState {
|
|
@@ -127,7 +138,7 @@ export interface ScreenShellLabels extends PanelLabels {
|
|
|
127
138
|
reading: string;
|
|
128
139
|
busy: string;
|
|
129
140
|
}
|
|
130
|
-
export interface ScreenShellProps {
|
|
141
|
+
export interface ScreenShellProps extends HTMLAttributes<HTMLDivElement> {
|
|
131
142
|
/** `<surface>.<area>[.<view>]` — the host keys its stored panels by it. */
|
|
132
143
|
viewId?: string;
|
|
133
144
|
mark: ShellMark;
|
|
@@ -162,4 +173,8 @@ export interface ScreenShellProps {
|
|
|
162
173
|
/** String overrides; defaults come from `messages.screen`. */
|
|
163
174
|
labels?: Partial<ScreenShellLabels>;
|
|
164
175
|
class?: string;
|
|
176
|
+
/** Attributes per band toggle, rendered on its toolbar button (e.g. `data-testid`). */
|
|
177
|
+
toggleAttrs?: Partial<Record<PanelId, ShellAttrs>>;
|
|
178
|
+
/** Attributes per band, rendered on the band's element (e.g. `data-testid`). */
|
|
179
|
+
bandAttrs?: Partial<Record<PanelId | 'footer', ShellAttrs>>;
|
|
165
180
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* how wide a stat card is, and how a stat's filter merges into the filter values.
|
|
4
4
|
*/
|
|
5
5
|
import type { ToolbarItem } from '@rokkit/ui';
|
|
6
|
-
import type { PanelId, PanelState, PanelLabels, ToolbarSpec, ShellStat, FilterState, StatMemory } from '../types/screen-shell.js';
|
|
6
|
+
import type { PanelId, PanelState, PanelLabels, ToolbarSpec, ShellAttrs, ShellStat, FilterState, StatMemory } from '../types/screen-shell.js';
|
|
7
7
|
/** The bands a screen can toggle, in toolbar order. */
|
|
8
8
|
export declare const PANEL_IDS: readonly PanelId[];
|
|
9
9
|
/** Whether a band is open: a missing key is open (first visit: everything open). */
|
|
@@ -22,6 +22,8 @@ interface ToolbarContext {
|
|
|
22
22
|
icons: PanelLabels;
|
|
23
23
|
/** Appended to a busy action's label. */
|
|
24
24
|
busy?: string;
|
|
25
|
+
/** Attributes per band toggle, rendered on its toolbar button. */
|
|
26
|
+
toggleAttrs?: Partial<Record<PanelId, ShellAttrs>>;
|
|
25
27
|
}
|
|
26
28
|
/**
|
|
27
29
|
* The toolbar's items: the screen's own buttons, a separator, then one toggle per band the
|
|
@@ -38,23 +38,32 @@ function toToolbarItem(item, busy = '') {
|
|
|
38
38
|
disabled: Boolean(action.disabled || action.busy)
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
|
+
/** A panel toggle with the band's attrs under its own (its own keys win). */
|
|
42
|
+
function withToggleAttrs(item, band) {
|
|
43
|
+
if (!band && item.attrs === undefined)
|
|
44
|
+
return item;
|
|
45
|
+
return { ...item, attrs: { ...band, ...(item.attrs ?? {}) } };
|
|
46
|
+
}
|
|
41
47
|
/** One toggle per band, pressed while the band is open. */
|
|
42
48
|
function panelToggle(id, ctx) {
|
|
43
|
-
return {
|
|
49
|
+
return withToggleAttrs({
|
|
44
50
|
id,
|
|
45
51
|
value: id,
|
|
46
52
|
type: 'toggle',
|
|
47
53
|
icon: ctx.icons[id],
|
|
48
54
|
label: ctx.labels[id],
|
|
49
55
|
active: isOpen(ctx.panels, id)
|
|
50
|
-
};
|
|
56
|
+
}, ctx.toggleAttrs?.[id]);
|
|
51
57
|
}
|
|
52
58
|
/** A full list: kept as is, except a band toggle follows the band (dropped when it is absent). */
|
|
53
59
|
function resolveFullList(spec, ctx) {
|
|
54
60
|
return spec
|
|
55
61
|
.filter((item) => item.type !== 'toggle' || !isPanelId(item.value) || ctx.present.includes(item.value))
|
|
56
62
|
.map((item) => item.type === 'toggle' && isPanelId(item.value)
|
|
57
|
-
? {
|
|
63
|
+
? {
|
|
64
|
+
...withToggleAttrs(item, ctx.toggleAttrs?.[item.value]),
|
|
65
|
+
active: isOpen(ctx.panels, item.value)
|
|
66
|
+
}
|
|
58
67
|
: item.type === 'separator' || item.type === 'spacer'
|
|
59
68
|
? item
|
|
60
69
|
: toToolbarItem(item, ctx.busy));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rokkit/app",
|
|
3
|
-
"version": "1.15.
|
|
3
|
+
"version": "1.15.2",
|
|
4
4
|
"description": "App-level controls for Rokkit applications - theme management and UI chrome",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -38,11 +38,11 @@
|
|
|
38
38
|
"build": "svelte-package -i src -o dist"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@rokkit/actions": "1.15.
|
|
42
|
-
"@rokkit/core": "1.15.
|
|
43
|
-
"@rokkit/forms": "1.15.
|
|
44
|
-
"@rokkit/states": "1.15.
|
|
45
|
-
"@rokkit/ui": "1.15.
|
|
41
|
+
"@rokkit/actions": "1.15.2",
|
|
42
|
+
"@rokkit/core": "1.15.2",
|
|
43
|
+
"@rokkit/forms": "1.15.2",
|
|
44
|
+
"@rokkit/states": "1.15.2",
|
|
45
|
+
"@rokkit/ui": "1.15.2"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
48
|
"svelte": "^5.0.0"
|