@aiaiai-pt/design-system 0.42.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.
@@ -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);
@@ -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";
@@ -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";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiaiai-pt/design-system",
3
- "version": "0.42.0",
3
+ "version": "0.43.0",
4
4
  "description": "Design system tokens and Svelte components for aiaiai products",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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
  ═══════════════════════════════════════════════ */