@aiaiai-pt/design-system 0.41.0 → 0.43.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.
- package/components/MegaMenu.svelte +247 -0
- package/components/MegaMenu.svelte.d.ts +49 -0
- package/components/ServiceNavigation.svelte +5 -0
- package/components/index.d.ts +1 -0
- package/components/index.js +1 -0
- package/components/renderer/MetabaseEmbedWidget.svelte +76 -0
- package/components/renderer/MetabaseEmbedWidget.svelte.d.ts +4 -0
- package/components/renderer/StatGridWidget.svelte +17 -1
- package/components/renderer/registry.ts +4 -0
- package/components/renderer/resolve-data.ts +3 -0
- package/components/renderer/types.d.ts +1 -1
- package/components/renderer/types.ts +6 -0
- package/package.json +1 -1
- package/tokens/components.css +5 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component MegaMenu
|
|
3
|
+
|
|
4
|
+
Category mega-menu for many-section portals (#507): a single "Services ▾"
|
|
5
|
+
disclosure in the primary band that opens a full-width panel of category
|
|
6
|
+
columns, so ~20 verticals collapse into one trigger instead of overflowing
|
|
7
|
+
the bar. Modelled on the GOV.UK "Menu" / Designers Italia mega-nav pattern.
|
|
8
|
+
|
|
9
|
+
Semantics: a disclosure (`button[aria-expanded][aria-controls]` + panel),
|
|
10
|
+
NOT a `menu` role — the panel holds plain links in labelled groups, which
|
|
11
|
+
screen readers navigate as ordinary lists. Escape closes and restores focus
|
|
12
|
+
to the trigger; clicking outside or navigating closes. The trigger reads as
|
|
13
|
+
current (underline bar) when any contained link is current.
|
|
14
|
+
|
|
15
|
+
Desktop-only affordance: below `--breakpoint` it renders nothing — small
|
|
16
|
+
screens keep their native menu (SiteHeader's unified disclosure), which the
|
|
17
|
+
consumer feeds the same category groups.
|
|
18
|
+
|
|
19
|
+
Layout note: the panel is absolutely positioned against the NEAREST
|
|
20
|
+
POSITIONED ANCESTOR — inside `SiteHeader` (position: sticky) it spans the
|
|
21
|
+
full header band, which is the intended composition:
|
|
22
|
+
|
|
23
|
+
@example
|
|
24
|
+
<SiteHeader>
|
|
25
|
+
{#snippet nav()}
|
|
26
|
+
<ServiceNavigation label="Primary" items={primary} />
|
|
27
|
+
<MegaMenu label="Serviços" categories={buckets} />
|
|
28
|
+
{/snippet}
|
|
29
|
+
</SiteHeader>
|
|
30
|
+
-->
|
|
31
|
+
<script>
|
|
32
|
+
let {
|
|
33
|
+
/** @type {string} Visible trigger label (localize it). */
|
|
34
|
+
label = "Services",
|
|
35
|
+
/** @type {Array<{ label: string, items: Array<{ href: string, label: string, current?: boolean }> }>} */
|
|
36
|
+
categories = [],
|
|
37
|
+
/** @type {string} id linking the trigger to the panel (unique per page). */
|
|
38
|
+
menuId = "mega-menu",
|
|
39
|
+
/** @type {string} */
|
|
40
|
+
class: className = "",
|
|
41
|
+
/** @type {import('svelte').Snippet | undefined} Trigger icon (chevron). */
|
|
42
|
+
icon = undefined,
|
|
43
|
+
...rest
|
|
44
|
+
} = $props();
|
|
45
|
+
|
|
46
|
+
let open = $state(false);
|
|
47
|
+
/** @type {HTMLElement | undefined} */
|
|
48
|
+
let root = $state(undefined);
|
|
49
|
+
/** @type {HTMLButtonElement | undefined} */
|
|
50
|
+
let trigger = $state(undefined);
|
|
51
|
+
|
|
52
|
+
const hasCurrent = $derived(
|
|
53
|
+
categories.some((c) => (c.items ?? []).some((i) => i.current)),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
function onDocumentClick(event) {
|
|
57
|
+
if (open && root && !root.contains(event.target)) open = false;
|
|
58
|
+
}
|
|
59
|
+
function onDocumentKeydown(event) {
|
|
60
|
+
if (event.key === "Escape" && open) {
|
|
61
|
+
open = false;
|
|
62
|
+
trigger?.focus();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
</script>
|
|
66
|
+
|
|
67
|
+
{#if categories.length > 0}
|
|
68
|
+
<div class="mega-menu {className}" bind:this={root} {...rest}>
|
|
69
|
+
<button
|
|
70
|
+
type="button"
|
|
71
|
+
class="mega-menu-trigger"
|
|
72
|
+
class:current={hasCurrent}
|
|
73
|
+
aria-expanded={open}
|
|
74
|
+
aria-controls={menuId}
|
|
75
|
+
bind:this={trigger}
|
|
76
|
+
onclick={() => (open = !open)}
|
|
77
|
+
>
|
|
78
|
+
{label}
|
|
79
|
+
{#if icon}{@render icon()}{:else}
|
|
80
|
+
<svg
|
|
81
|
+
class="mega-menu-chevron"
|
|
82
|
+
class:open
|
|
83
|
+
viewBox="0 0 16 16"
|
|
84
|
+
aria-hidden="true"
|
|
85
|
+
>
|
|
86
|
+
<path
|
|
87
|
+
d="M4 6l4 4 4-4"
|
|
88
|
+
fill="none"
|
|
89
|
+
stroke="currentColor"
|
|
90
|
+
stroke-width="1.5"
|
|
91
|
+
stroke-linecap="round"
|
|
92
|
+
stroke-linejoin="round"
|
|
93
|
+
/>
|
|
94
|
+
</svg>
|
|
95
|
+
{/if}
|
|
96
|
+
</button>
|
|
97
|
+
|
|
98
|
+
{#if open}
|
|
99
|
+
<div id={menuId} class="mega-menu-panel">
|
|
100
|
+
{#each categories as category (category.label)}
|
|
101
|
+
<section class="mega-menu-category" aria-label={category.label}>
|
|
102
|
+
<h3 class="mega-menu-heading">{category.label}</h3>
|
|
103
|
+
<ul class="mega-menu-list">
|
|
104
|
+
{#each category.items ?? [] as item (item.href)}
|
|
105
|
+
<li class="mega-menu-item">
|
|
106
|
+
<a
|
|
107
|
+
href={item.href}
|
|
108
|
+
class="mega-menu-link"
|
|
109
|
+
aria-current={item.current ? "page" : undefined}
|
|
110
|
+
onclick={() => (open = false)}
|
|
111
|
+
>
|
|
112
|
+
{item.label}
|
|
113
|
+
</a>
|
|
114
|
+
</li>
|
|
115
|
+
{/each}
|
|
116
|
+
</ul>
|
|
117
|
+
</section>
|
|
118
|
+
{/each}
|
|
119
|
+
</div>
|
|
120
|
+
{/if}
|
|
121
|
+
</div>
|
|
122
|
+
{/if}
|
|
123
|
+
|
|
124
|
+
<svelte:document onclick={onDocumentClick} onkeydown={onDocumentKeydown} />
|
|
125
|
+
|
|
126
|
+
<style>
|
|
127
|
+
.mega-menu {
|
|
128
|
+
display: flex;
|
|
129
|
+
align-items: stretch;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* The trigger reads as a peer of ServiceNavigation's links: same type ramp,
|
|
133
|
+
full band height, and the same accent underline bar when a contained link
|
|
134
|
+
is the current page. */
|
|
135
|
+
.mega-menu-trigger {
|
|
136
|
+
display: inline-flex;
|
|
137
|
+
align-items: center;
|
|
138
|
+
gap: var(--space-2xs);
|
|
139
|
+
height: 100%;
|
|
140
|
+
padding-inline: var(--space-md);
|
|
141
|
+
border: none;
|
|
142
|
+
background: none;
|
|
143
|
+
color: var(--color-text-secondary);
|
|
144
|
+
font-family: var(--type-label-font);
|
|
145
|
+
font-size: var(--nav-service-link-size);
|
|
146
|
+
font-weight: var(--raw-font-weight-medium);
|
|
147
|
+
cursor: pointer;
|
|
148
|
+
transition: color var(--duration-fast) var(--easing-default);
|
|
149
|
+
}
|
|
150
|
+
.mega-menu-trigger:hover,
|
|
151
|
+
.mega-menu-trigger[aria-expanded="true"] {
|
|
152
|
+
color: var(--color-text);
|
|
153
|
+
}
|
|
154
|
+
.mega-menu-trigger:focus-visible {
|
|
155
|
+
outline: var(--focus-ring-width) solid var(--focus-ring-color);
|
|
156
|
+
outline-offset: var(--focus-ring-offset);
|
|
157
|
+
}
|
|
158
|
+
.mega-menu-trigger.current {
|
|
159
|
+
color: var(--color-text);
|
|
160
|
+
font-weight: var(--raw-font-weight-semibold);
|
|
161
|
+
box-shadow: inset 0 calc(-1 * var(--nav-service-underline)) 0 0
|
|
162
|
+
var(--color-accent);
|
|
163
|
+
}
|
|
164
|
+
.mega-menu-chevron {
|
|
165
|
+
width: var(--icon-size-sm);
|
|
166
|
+
height: var(--icon-size-sm);
|
|
167
|
+
transition: transform var(--duration-fast) var(--easing-default);
|
|
168
|
+
}
|
|
169
|
+
.mega-menu-chevron.open {
|
|
170
|
+
transform: rotate(180deg);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/* Full-width panel anchored under the band (the nearest positioned ancestor
|
|
174
|
+
— SiteHeader's sticky banner). Category columns auto-flow; the panel
|
|
175
|
+
scrolls internally rather than covering the page (#507 overflow fix). */
|
|
176
|
+
.mega-menu-panel {
|
|
177
|
+
position: absolute;
|
|
178
|
+
top: 100%;
|
|
179
|
+
inset-inline: 0;
|
|
180
|
+
z-index: 50;
|
|
181
|
+
display: grid;
|
|
182
|
+
grid-template-columns: repeat(
|
|
183
|
+
auto-fill,
|
|
184
|
+
minmax(var(--nav-mega-column-min-width), 1fr)
|
|
185
|
+
);
|
|
186
|
+
gap: var(--space-lg);
|
|
187
|
+
padding: var(--space-lg) var(--content-padding-x);
|
|
188
|
+
max-height: var(--nav-mega-panel-max-height);
|
|
189
|
+
overflow-y: auto;
|
|
190
|
+
background: var(--color-surface-raised);
|
|
191
|
+
border-top: var(--border-width) solid var(--color-border);
|
|
192
|
+
border-bottom: var(--border-width) solid var(--color-border);
|
|
193
|
+
box-shadow: var(--shadow-md);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.mega-menu-heading {
|
|
197
|
+
margin: 0 0 var(--space-2xs);
|
|
198
|
+
font-family: var(--type-label-font);
|
|
199
|
+
font-size: var(--type-body-sm-size);
|
|
200
|
+
font-weight: var(--raw-font-weight-semibold);
|
|
201
|
+
text-transform: uppercase;
|
|
202
|
+
letter-spacing: 0.04em;
|
|
203
|
+
color: var(--color-text-secondary);
|
|
204
|
+
}
|
|
205
|
+
.mega-menu-list {
|
|
206
|
+
margin: 0;
|
|
207
|
+
padding: 0;
|
|
208
|
+
list-style: none;
|
|
209
|
+
}
|
|
210
|
+
.mega-menu-item {
|
|
211
|
+
display: flex;
|
|
212
|
+
}
|
|
213
|
+
.mega-menu-link {
|
|
214
|
+
flex: 1;
|
|
215
|
+
padding: var(--space-2xs) var(--space-2xs);
|
|
216
|
+
border-radius: var(--radius-md);
|
|
217
|
+
color: var(--color-text);
|
|
218
|
+
text-decoration: none;
|
|
219
|
+
font-family: var(--type-label-font);
|
|
220
|
+
font-size: var(--type-body-sm-size);
|
|
221
|
+
}
|
|
222
|
+
.mega-menu-link:hover {
|
|
223
|
+
background: var(--color-surface-tertiary);
|
|
224
|
+
}
|
|
225
|
+
.mega-menu-link:focus-visible {
|
|
226
|
+
outline: var(--focus-ring-width) solid var(--focus-ring-color);
|
|
227
|
+
outline-offset: var(--focus-ring-offset);
|
|
228
|
+
}
|
|
229
|
+
.mega-menu-link[aria-current="page"] {
|
|
230
|
+
background: var(--color-surface-tertiary);
|
|
231
|
+
font-weight: var(--raw-font-weight-semibold);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/* Desktop-only: small screens use the consumer's native menu (SiteHeader's
|
|
235
|
+
unified disclosure), grouped by the same categories. */
|
|
236
|
+
@media (max-width: 47.99rem) {
|
|
237
|
+
.mega-menu {
|
|
238
|
+
display: none;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
@media (prefers-reduced-motion: reduce) {
|
|
243
|
+
.mega-menu-chevron {
|
|
244
|
+
transition: none;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
</style>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export default MegaMenu;
|
|
2
|
+
type MegaMenu = {
|
|
3
|
+
$on?(type: string, callback: (e: any) => void): () => void;
|
|
4
|
+
$set?(props: Partial<$$ComponentProps>): void;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* MegaMenu
|
|
8
|
+
*
|
|
9
|
+
* Category mega-menu for many-section portals (#507): a single "Services ▾"
|
|
10
|
+
* disclosure in the primary band that opens a full-width panel of category
|
|
11
|
+
* columns, so ~20 verticals collapse into one trigger instead of overflowing
|
|
12
|
+
* the bar. Modelled on the GOV.UK "Menu" / Designers Italia mega-nav pattern.
|
|
13
|
+
*
|
|
14
|
+
* Semantics: a disclosure (`button[aria-expanded][aria-controls]` + panel),
|
|
15
|
+
* NOT a `menu` role — the panel holds plain links in labelled groups, which
|
|
16
|
+
* screen readers navigate as ordinary lists. Escape closes and restores focus
|
|
17
|
+
* to the trigger; clicking outside or navigating closes. The trigger reads as
|
|
18
|
+
* current (underline bar) when any contained link is current.
|
|
19
|
+
*
|
|
20
|
+
* Desktop-only affordance: below `--breakpoint` it renders nothing — small
|
|
21
|
+
* screens keep their native menu (SiteHeader's unified disclosure), which the
|
|
22
|
+
* consumer feeds the same category groups.
|
|
23
|
+
*
|
|
24
|
+
* Layout note: the panel is absolutely positioned against the NEAREST
|
|
25
|
+
* POSITIONED ANCESTOR — inside `SiteHeader` (position: sticky) it spans the
|
|
26
|
+
* full header band, which is the intended composition:
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* <SiteHeader>
|
|
30
|
+
* {#snippet nav()}
|
|
31
|
+
* <ServiceNavigation label="Primary" items={primary} />
|
|
32
|
+
* <MegaMenu label="Serviços" categories={buckets} />
|
|
33
|
+
* {/snippet}
|
|
34
|
+
* </SiteHeader>
|
|
35
|
+
*/
|
|
36
|
+
declare const MegaMenu: import("svelte").Component<{
|
|
37
|
+
label?: string;
|
|
38
|
+
categories?: any[];
|
|
39
|
+
menuId?: string;
|
|
40
|
+
class?: string;
|
|
41
|
+
icon?: any;
|
|
42
|
+
} & Record<string, any>, {}, "">;
|
|
43
|
+
type $$ComponentProps = {
|
|
44
|
+
label?: string;
|
|
45
|
+
categories?: any[];
|
|
46
|
+
menuId?: string;
|
|
47
|
+
class?: string;
|
|
48
|
+
icon?: any;
|
|
49
|
+
} & Record<string, any>;
|
|
@@ -161,6 +161,11 @@
|
|
|
161
161
|
flex-direction: column;
|
|
162
162
|
align-items: stretch;
|
|
163
163
|
min-width: var(--nav-service-dropdown-min-width);
|
|
164
|
+
/* #507 overflow fix: many sections used to grow the dropdown past the
|
|
165
|
+
viewport and cover the page — cap it and scroll internally instead. */
|
|
166
|
+
max-width: calc(100vw - 2 * var(--content-padding-x));
|
|
167
|
+
max-height: min(70vh, 28rem);
|
|
168
|
+
overflow-y: auto;
|
|
164
169
|
padding: var(--space-2xs);
|
|
165
170
|
background: var(--color-surface);
|
|
166
171
|
border: var(--border-width) solid var(--color-border);
|
package/components/index.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ export { default as ListItem } from "./ListItem.svelte";
|
|
|
19
19
|
export { default as PageContainer } from "./PageContainer.svelte";
|
|
20
20
|
export { default as AppFrame } from "./AppFrame.svelte";
|
|
21
21
|
export { default as SiteHeader } from "./SiteHeader.svelte";
|
|
22
|
+
export { default as MegaMenu } from "./MegaMenu.svelte";
|
|
22
23
|
export { default as SiteFooter } from "./SiteFooter.svelte";
|
|
23
24
|
export { default as SkipLink } from "./SkipLink.svelte";
|
|
24
25
|
export { default as TextSizeAdjuster } from "./TextSizeAdjuster.svelte";
|
package/components/index.js
CHANGED
|
@@ -36,6 +36,7 @@ export { default as PageContainer } from "./PageContainer.svelte";
|
|
|
36
36
|
// Public site shell (locked a11y chrome — citizen portal, #7/#71)
|
|
37
37
|
export { default as AppFrame } from "./AppFrame.svelte";
|
|
38
38
|
export { default as SiteHeader } from "./SiteHeader.svelte";
|
|
39
|
+
export { default as MegaMenu } from "./MegaMenu.svelte";
|
|
39
40
|
export { default as SiteFooter } from "./SiteFooter.svelte";
|
|
40
41
|
export { default as SkipLink } from "./SkipLink.svelte";
|
|
41
42
|
export { default as TextSizeAdjuster } from "./TextSizeAdjuster.svelte";
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { WidgetProps } from "./types";
|
|
3
|
+
|
|
4
|
+
// `embed` widget (#517, atelier) — a Metabase analytics embed on the unified
|
|
5
|
+
// dashboard/portal `Block[]` grid. PRESENTATION-ONLY: the DS never mints or
|
|
6
|
+
// holds a Metabase URL; the HOST resolves the signed/proxy URL and passes it
|
|
7
|
+
// as `props.src` (an `/internal/metabase-embed/…` route or a signed URL), and
|
|
8
|
+
// this widget renders an `<iframe>`. Presentational kind → `resolveData`
|
|
9
|
+
// short-circuits it (no DS fetch); the widget owns its props.
|
|
10
|
+
//
|
|
11
|
+
// props.src — the iframe URL (host-resolved). Absent ⇒ placeholder.
|
|
12
|
+
// props.title — accessible iframe title / caption.
|
|
13
|
+
// props.height — CSS height (e.g. "400px"); defaults to 400px.
|
|
14
|
+
// props.width — "full" | "half" (grid-agnostic max-width hint).
|
|
15
|
+
// props.embed_type — "dashboard" | "question" (informational; caption).
|
|
16
|
+
let { props }: WidgetProps = $props();
|
|
17
|
+
|
|
18
|
+
const src = $derived(typeof props.src === "string" ? props.src : "");
|
|
19
|
+
const title = $derived(typeof props.title === "string" && props.title ? props.title : "Metabase embed");
|
|
20
|
+
const height = $derived(typeof props.height === "string" && props.height ? props.height : "400px");
|
|
21
|
+
const half = $derived(props.width === "half");
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<figure class="mb-embed" class:half data-widget="metabase-embed">
|
|
25
|
+
{#if src}
|
|
26
|
+
<iframe {src} {title} style:height loading="lazy" referrerpolicy="strict-origin-when-cross-origin"
|
|
27
|
+
></iframe>
|
|
28
|
+
{#if props.title}<figcaption>{props.title}</figcaption>{/if}
|
|
29
|
+
{:else}
|
|
30
|
+
<div class="mb-embed-empty" style:height>
|
|
31
|
+
<p>Metabase embed not configured — set the dashboard/question id.</p>
|
|
32
|
+
</div>
|
|
33
|
+
{/if}
|
|
34
|
+
</figure>
|
|
35
|
+
|
|
36
|
+
<style>
|
|
37
|
+
.mb-embed {
|
|
38
|
+
margin: 0;
|
|
39
|
+
display: flex;
|
|
40
|
+
flex-direction: column;
|
|
41
|
+
gap: var(--space-sm);
|
|
42
|
+
inline-size: 100%;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.mb-embed.half {
|
|
46
|
+
max-inline-size: 50%;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.mb-embed iframe {
|
|
50
|
+
inline-size: 100%;
|
|
51
|
+
border: var(--elevation-border);
|
|
52
|
+
border-radius: var(--radius-md);
|
|
53
|
+
background: var(--color-surface);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.mb-embed figcaption {
|
|
57
|
+
font-size: var(--type-caption-size);
|
|
58
|
+
color: var(--color-text-muted);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.mb-embed-empty {
|
|
62
|
+
display: grid;
|
|
63
|
+
place-items: center;
|
|
64
|
+
inline-size: 100%;
|
|
65
|
+
border: var(--border-width) dashed var(--color-border);
|
|
66
|
+
border-radius: var(--radius-md);
|
|
67
|
+
background: var(--color-surface-secondary);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.mb-embed-empty p {
|
|
71
|
+
margin: 0;
|
|
72
|
+
padding: var(--space-md);
|
|
73
|
+
font-size: var(--type-body-sm-size);
|
|
74
|
+
color: var(--color-text-muted);
|
|
75
|
+
}
|
|
76
|
+
</style>
|
|
@@ -14,6 +14,11 @@
|
|
|
14
14
|
label?: string;
|
|
15
15
|
value?: string;
|
|
16
16
|
variant?: string;
|
|
17
|
+
/** #517 — optional Phosphor icon NAME (kebab-case, e.g. "chart-bar"),
|
|
18
|
+
* rendered via the Phosphor web font. Lets a dashboard KPI block carry its
|
|
19
|
+
* per-card icon through the DS widget, so KPI renders via `resolveWidget`
|
|
20
|
+
* like every other block (no host-side StatCard special-case). */
|
|
21
|
+
icon?: string;
|
|
17
22
|
}
|
|
18
23
|
|
|
19
24
|
const view = $derived((data ?? {}) as Record<string, unknown>);
|
|
@@ -24,10 +29,21 @@
|
|
|
24
29
|
? props.stats
|
|
25
30
|
: []) as Stat[],
|
|
26
31
|
);
|
|
32
|
+
|
|
33
|
+
// Phosphor web-font class names are kebab-case (`ph ph-chart-bar`); keep only
|
|
34
|
+
// the safe charset so a bad name can't leak into `class` (defensive — attribute
|
|
35
|
+
// values are escaped, but this keeps the class well-formed).
|
|
36
|
+
const iconClass = (name: string): string => `ph ph-${name.replace(/[^a-z0-9-]/gi, "")}`;
|
|
27
37
|
</script>
|
|
28
38
|
|
|
29
39
|
<StatGrid>
|
|
30
40
|
{#each stats as stat (stat.label)}
|
|
31
|
-
<
|
|
41
|
+
{#snippet cardIcon()}<i class={iconClass(stat.icon ?? "")} aria-hidden="true"></i>{/snippet}
|
|
42
|
+
<StatCard
|
|
43
|
+
label={stat.label}
|
|
44
|
+
value={stat.value}
|
|
45
|
+
variant={stat.variant}
|
|
46
|
+
icon={stat.icon ? cardIcon : undefined}
|
|
47
|
+
/>
|
|
32
48
|
{/each}
|
|
33
49
|
</StatGrid>
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
import type { Block, OntologySchema, WidgetKind, WidgetProps } from "./types";
|
|
24
24
|
|
|
25
25
|
import EChartWidget from "./EChartWidget.svelte";
|
|
26
|
+
import MetabaseEmbedWidget from "./MetabaseEmbedWidget.svelte";
|
|
26
27
|
import ResultsChartWidget from "./ResultsChartWidget.svelte";
|
|
27
28
|
import StatGridWidget from "./StatGridWidget.svelte";
|
|
28
29
|
|
|
@@ -75,6 +76,9 @@ const _entries: RegistryEntry<WidgetComponent>[] = [
|
|
|
75
76
|
// the kind-specific registry keys + wrapper components are retired.
|
|
76
77
|
// `results-chart` stays as the SSR / no-JS a11y fallback key.
|
|
77
78
|
byTypeOnKind("chart", "aggregate", EChartWidget),
|
|
79
|
+
// #517 (atelier) — Metabase embed on the unified Block[] grid. Presentation-
|
|
80
|
+
// only (renders `props.src`); one kind-generic entry, no `type` ranking.
|
|
81
|
+
byKind("metabase-embed", "embed", MetabaseEmbedWidget),
|
|
78
82
|
];
|
|
79
83
|
|
|
80
84
|
/**
|
|
@@ -35,6 +35,9 @@ export const PRESENTATIONAL_KINDS: ReadonlySet<WidgetKind> =
|
|
|
35
35
|
new Set<WidgetKind>([
|
|
36
36
|
"hero",
|
|
37
37
|
"lookup",
|
|
38
|
+
// #517 — the Metabase embed renders from a host-resolved `props.src`; it has
|
|
39
|
+
// no DS-fetched data path, so short-circuit it like the other content kinds.
|
|
40
|
+
"embed",
|
|
38
41
|
"feature-grid",
|
|
39
42
|
"cta",
|
|
40
43
|
"media-text",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* here the descriptors are SEEDED (same staged pattern #70/#72 used).
|
|
14
14
|
*/
|
|
15
15
|
/** The bounded, governed widget kinds (spec §5 catalog). */
|
|
16
|
-
export type WidgetKind = "list" | "detail" | "form" | "map" | "calendar" | "kpi" | "content" | "vote" | "status" | "forms" | "hero" | "lookup" | "feed" | "filters" | "actions" | "aggregate" | "subscriptions" | "consent" | "deliveries" | "owned-list" | "feature-grid" | "cta" | "media-text" | "steps" | "testimonial" | "faq" | "logo-strip" | "media-gallery";
|
|
16
|
+
export type WidgetKind = "list" | "detail" | "form" | "map" | "calendar" | "kpi" | "content" | "embed" | "vote" | "status" | "forms" | "hero" | "lookup" | "feed" | "filters" | "actions" | "aggregate" | "subscriptions" | "consent" | "deliveries" | "owned-list" | "feature-grid" | "cta" | "media-text" | "steps" | "testimonial" | "faq" | "logo-strip" | "media-gallery";
|
|
17
17
|
/** The feed sub-views a `feed`/`filters` binding may toggle/scope (#308). */
|
|
18
18
|
export type FeedView = "list" | "map";
|
|
19
19
|
/** Declarative pointer — "I render <kind> of <entity|action>, scoped by filter". */
|
|
@@ -22,6 +22,12 @@ export type WidgetKind =
|
|
|
22
22
|
| "calendar"
|
|
23
23
|
| "kpi"
|
|
24
24
|
| "content"
|
|
25
|
+
// #517 (atelier) — a Metabase analytics embed. Presentational: the DS
|
|
26
|
+
// `MetabaseEmbedWidget` renders an `<iframe>` from a HOST-resolved `props.src`
|
|
27
|
+
// (the signed/proxy URL), so the DS stays presentation-only and carries no
|
|
28
|
+
// metabase auth. Lets a dashboard/portal `Block[]` hold embeds uniformly
|
|
29
|
+
// (resolved via `resolveWidget`, not a host special-case).
|
|
30
|
+
| "embed"
|
|
25
31
|
| "vote"
|
|
26
32
|
| "status"
|
|
27
33
|
| "forms"
|
package/package.json
CHANGED
package/tokens/components.css
CHANGED
|
@@ -229,6 +229,11 @@
|
|
|
229
229
|
rendered standalone, without SiteHeader's unified menu). */
|
|
230
230
|
--nav-service-dropdown-min-width: 12rem;
|
|
231
231
|
|
|
232
|
+
/* MegaMenu (#507) — category column sizing + the panel's internal-scroll cap
|
|
233
|
+
so an 8-category panel scrolls inside itself instead of covering the page. */
|
|
234
|
+
--nav-mega-column-min-width: 13rem;
|
|
235
|
+
--nav-mega-panel-max-height: min(70vh, 32rem);
|
|
236
|
+
|
|
232
237
|
/* ═══════════════════════════════════════════════
|
|
233
238
|
DATA DISPLAY
|
|
234
239
|
═══════════════════════════════════════════════ */
|