@aiaiai-pt/design-system 0.32.0 → 0.33.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,185 @@
1
+ <!--
2
+ @component SectionNavigation
3
+
4
+ Contextual SECONDARY navigation for the public portal — the band that sits
5
+ directly under the `SiteHeader` primary row and surfaces the pages of the
6
+ section the visitor is currently inside (e.g. inside "Ocorrências": Reportar /
7
+ Consultar / Acompanhar). It is the second tier of a two-level information
8
+ architecture; the first tier (the sections themselves) lives in the header's
9
+ `ServiceNavigation`.
10
+
11
+ Why a separate band instead of a second inline nav: stacking two navs in one
12
+ header row makes the row reflow sideways every time the visitor crosses into
13
+ or out of a section. This component owns its OWN full-width band and animates
14
+ its disclosure (grid-rows 0fr→1fr) BELOW the fixed header, so the primary tier
15
+ never moves and there is no layout shift on the content above it. Render it in
16
+ `SiteHeader`'s `subnav` snippet.
17
+
18
+ Information architecture (GOV.UK / National Archives secondary-navigation
19
+ pattern): the band lists ONLY the section's pages — it does NOT repeat the
20
+ section name as a visible label. The section identity is carried by the active
21
+ PRIMARY item directly above it, and exposed to assistive tech via the nav
22
+ landmark's `aria-label` (the section name). The pages read one step quieter
23
+ than the primary tier (smaller text), and the current page gets a single
24
+ accent bar flush to the band's bottom border — the same device the primary
25
+ tier uses, one level down, so the two tiers read as one calm system.
26
+
27
+ Accessibility:
28
+ - `<nav aria-label={label}>` landmark — pass the localized SECTION name; it
29
+ is announced to screen readers but never rendered as a visible chip.
30
+ - The current page carries `aria-current="page"`.
31
+ - Empty `items` → the band collapses to zero height and is `aria-hidden`,
32
+ so it is never an empty landmark and never a dead row.
33
+ - Hidden below `--breakpoint`: on mobile the section's pages live in the
34
+ header's single unified disclosure (`SiteHeader` `menu` snippet), so there
35
+ is no second mobile toggle.
36
+
37
+ @example
38
+ <SectionNavigation
39
+ label="Ocorrências"
40
+ items={[
41
+ { href: "/ocorrencias/submit", label: "Reportar ocorrência", current: true },
42
+ { href: "/ocorrencias/track", label: "Acompanhar" },
43
+ ]}
44
+ />
45
+ -->
46
+ <script>
47
+ let {
48
+ /** @type {Array<{ href: string, label: string, current?: boolean }>} */
49
+ items = [],
50
+ /** @type {string} The section's (localized) name — the nav landmark's
51
+ * aria-label (announced, never rendered as a visible label). */
52
+ label = "",
53
+ /** @type {string} */
54
+ class: className = "",
55
+ ...rest
56
+ } = $props();
57
+
58
+ const open = $derived(items.length > 0);
59
+ </script>
60
+
61
+ <div class="section-nav-band {className}" class:open aria-hidden={!open} {...rest}>
62
+ <div class="section-nav-clip">
63
+ <nav class="section-nav-inner" aria-label={label || "Section"}>
64
+ <ul class="section-nav-list">
65
+ {#each items as item (item.href)}
66
+ <li class="section-nav-item">
67
+ <a
68
+ href={item.href}
69
+ class="section-nav-link"
70
+ aria-current={item.current ? "page" : undefined}
71
+ >
72
+ {item.label}
73
+ </a>
74
+ </li>
75
+ {/each}
76
+ </ul>
77
+ </nav>
78
+ </div>
79
+ </div>
80
+
81
+ <style>
82
+ /* Full-width band on the page surface, set apart by a hairline only (no
83
+ competing fill). Animates its disclosure via grid-template-rows so opening /
84
+ closing is a vertical reveal, never a sideways shove of the header tier. */
85
+ .section-nav-band {
86
+ display: grid;
87
+ grid-template-rows: 0fr;
88
+ background: var(--color-surface);
89
+ border-bottom: var(--border-width) solid transparent;
90
+ transition:
91
+ grid-template-rows var(--duration-normal) var(--easing-enter),
92
+ border-color var(--duration-normal) var(--easing-default);
93
+ }
94
+ .section-nav-band.open {
95
+ grid-template-rows: 1fr;
96
+ border-bottom-color: var(--color-border);
97
+ }
98
+ .section-nav-clip {
99
+ overflow: hidden;
100
+ min-height: 0;
101
+ }
102
+ /* The inner band is the nav landmark itself — fixed gridded height so the
103
+ links can fill it and the active bar sits flush on the bottom border. */
104
+ .section-nav-inner {
105
+ display: flex;
106
+ align-items: stretch;
107
+ height: var(--nav-section-height);
108
+ width: 100%;
109
+ max-width: var(--content-width-wide);
110
+ margin-inline: auto;
111
+ padding-inline: var(--content-padding-x);
112
+ /* Content fades in just behind the height reveal. */
113
+ opacity: 0;
114
+ transform: translateY(calc(-1 * var(--space-xs)));
115
+ transition:
116
+ opacity var(--duration-normal) var(--easing-default),
117
+ transform var(--duration-normal) var(--easing-default);
118
+ }
119
+ .section-nav-band.open .section-nav-inner {
120
+ opacity: 1;
121
+ transform: none;
122
+ }
123
+
124
+ .section-nav-list {
125
+ display: flex;
126
+ align-items: stretch;
127
+ height: 100%;
128
+ margin: 0;
129
+ padding: 0;
130
+ list-style: none;
131
+ /* Long sections scroll horizontally inside the band rather than wrapping
132
+ into a second row (which would reintroduce the height jump). */
133
+ overflow-x: auto;
134
+ scrollbar-width: none;
135
+ }
136
+ .section-nav-list::-webkit-scrollbar {
137
+ display: none;
138
+ }
139
+ .section-nav-item {
140
+ display: flex;
141
+ }
142
+
143
+ /* One step quieter than the primary tier: smaller text, full height so the
144
+ active bar is flush on the band's bottom border. Horizontal padding only;
145
+ the first item aligns to the content edge (under the primary tier). */
146
+ .section-nav-link {
147
+ display: inline-flex;
148
+ align-items: center;
149
+ height: 100%;
150
+ white-space: nowrap;
151
+ padding-inline: var(--space-md);
152
+ color: var(--color-text-secondary);
153
+ text-decoration: none;
154
+ font-family: var(--type-label-font);
155
+ font-size: var(--nav-section-link-size);
156
+ font-weight: var(--raw-font-weight-medium);
157
+ transition: color var(--duration-fast) var(--easing-default);
158
+ }
159
+ .section-nav-item:first-child .section-nav-link {
160
+ padding-inline-start: 0;
161
+ }
162
+ .section-nav-link:hover {
163
+ color: var(--color-text);
164
+ }
165
+ .section-nav-link:focus-visible {
166
+ outline: var(--focus-ring-width) solid var(--focus-ring-color);
167
+ outline-offset: var(--focus-ring-offset);
168
+ }
169
+ /* Current page: emphasized text + accent underline bar (shared
170
+ --nav-service-underline token), flush on the band's bottom border. */
171
+ .section-nav-link[aria-current="page"] {
172
+ color: var(--color-text);
173
+ font-weight: var(--raw-font-weight-semibold);
174
+ box-shadow: inset 0 calc(-1 * var(--nav-service-underline)) 0 0
175
+ var(--color-accent);
176
+ }
177
+
178
+ /* Mobile: the band is suppressed — the section's pages are reachable through
179
+ SiteHeader's single unified disclosure, so there is no second toggle. */
180
+ @media (max-width: 47.99rem) {
181
+ .section-nav-band {
182
+ display: none;
183
+ }
184
+ }
185
+ </style>
@@ -0,0 +1,60 @@
1
+ export default SectionNavigation;
2
+ type SectionNavigation = {
3
+ $on?(type: string, callback: (e: any) => void): () => void;
4
+ $set?(props: Partial<$$ComponentProps>): void;
5
+ };
6
+ /**
7
+ * SectionNavigation
8
+ *
9
+ * Contextual SECONDARY navigation for the public portal — the band that sits
10
+ * directly under the `SiteHeader` primary row and surfaces the pages of the
11
+ * section the visitor is currently inside (e.g. inside "Ocorrências": Reportar /
12
+ * Consultar / Acompanhar). It is the second tier of a two-level information
13
+ * architecture; the first tier (the sections themselves) lives in the header's
14
+ * `ServiceNavigation`.
15
+ *
16
+ * Why a separate band instead of a second inline nav: stacking two navs in one
17
+ * header row makes the row reflow sideways every time the visitor crosses into
18
+ * or out of a section. This component owns its OWN full-width band and animates
19
+ * its disclosure (grid-rows 0fr→1fr) BELOW the fixed header, so the primary tier
20
+ * never moves and there is no layout shift on the content above it. Render it in
21
+ * `SiteHeader`'s `subnav` snippet.
22
+ *
23
+ * Information architecture (GOV.UK / National Archives secondary-navigation
24
+ * pattern): the band lists ONLY the section's pages — it does NOT repeat the
25
+ * section name as a visible label. The section identity is carried by the active
26
+ * PRIMARY item directly above it, and exposed to assistive tech via the nav
27
+ * landmark's `aria-label` (the section name). The pages read one step quieter
28
+ * than the primary tier (smaller text), and the current page gets a single
29
+ * accent bar flush to the band's bottom border — the same device the primary
30
+ * tier uses, one level down, so the two tiers read as one calm system.
31
+ *
32
+ * Accessibility:
33
+ * - `<nav aria-label={label}>` landmark — pass the localized SECTION name; it
34
+ * is announced to screen readers but never rendered as a visible chip.
35
+ * - The current page carries `aria-current="page"`.
36
+ * - Empty `items` → the band collapses to zero height and is `aria-hidden`,
37
+ * so it is never an empty landmark and never a dead row.
38
+ * - Hidden below `--breakpoint`: on mobile the section's pages live in the
39
+ * header's single unified disclosure (`SiteHeader` `menu` snippet), so there
40
+ * is no second mobile toggle.
41
+ *
42
+ * @example
43
+ * <SectionNavigation
44
+ * label="Ocorrências"
45
+ * items={[
46
+ * { href: "/ocorrencias/submit", label: "Reportar ocorrência", current: true },
47
+ * { href: "/ocorrencias/track", label: "Acompanhar" },
48
+ * ]}
49
+ * />
50
+ */
51
+ declare const SectionNavigation: import("svelte").Component<{
52
+ items?: any[];
53
+ label?: string;
54
+ class?: string;
55
+ } & Record<string, any>, {}, "">;
56
+ type $$ComponentProps = {
57
+ items?: any[];
58
+ label?: string;
59
+ class?: string;
60
+ } & Record<string, any>;
@@ -78,7 +78,8 @@
78
78
  <style>
79
79
  .service-nav {
80
80
  display: flex;
81
- align-items: center;
81
+ align-items: stretch;
82
+ height: 100%;
82
83
  }
83
84
 
84
85
  /* Toggle hidden on wide viewports; the list is a horizontal row. */
@@ -87,7 +88,7 @@
87
88
  align-items: center;
88
89
  gap: var(--space-2xs);
89
90
  padding: var(--space-2xs) var(--space-sm);
90
- border: 1px solid var(--color-border);
91
+ border: var(--border-width) solid var(--color-border);
91
92
  border-radius: var(--radius-md);
92
93
  background: var(--color-surface);
93
94
  color: var(--color-text);
@@ -103,37 +104,50 @@
103
104
 
104
105
  .service-nav-list {
105
106
  display: flex;
106
- align-items: center;
107
- gap: var(--space-2xs);
107
+ align-items: stretch;
108
+ height: 100%;
108
109
  margin: 0;
109
110
  padding: 0;
110
111
  list-style: none;
111
112
  }
113
+ .service-nav-item {
114
+ display: flex;
115
+ }
112
116
 
117
+ /* Full-height links → the active underline bar sits flush on the band's
118
+ bottom border. Horizontal padding only; no radius (a bar, not a pill). */
113
119
  .service-nav-link {
114
- display: block;
115
- padding: var(--space-2xs) var(--space-sm);
116
- border-radius: var(--radius-md);
120
+ display: inline-flex;
121
+ align-items: center;
122
+ height: 100%;
123
+ padding-inline: var(--space-md);
117
124
  color: var(--color-text-secondary);
118
125
  text-decoration: none;
119
126
  font-family: var(--type-label-font);
120
- font-size: var(--type-label-size);
127
+ font-size: var(--nav-service-link-size);
128
+ font-weight: var(--raw-font-weight-medium);
129
+ transition: color var(--duration-fast) var(--easing-default);
121
130
  }
122
131
  .service-nav-link:hover {
123
132
  color: var(--color-text);
124
- background: var(--color-surface-tertiary);
125
133
  }
126
- /* Current page: emphasized text + an accent underline bar (GOV.UK pattern). */
134
+ /* Current page: emphasized text + an accent underline bar (GOV.UK pattern).
135
+ Thickness via the shared --nav-service-underline token (also used by
136
+ SectionNavigation) so the two nav tiers stay visually identical. */
127
137
  .service-nav-link[aria-current="page"] {
128
138
  color: var(--color-text);
129
139
  font-weight: var(--raw-font-weight-semibold);
130
- box-shadow: inset 0 -3px 0 0 var(--color-accent);
140
+ box-shadow: inset 0 calc(-1 * var(--nav-service-underline)) 0 0
141
+ var(--color-accent);
131
142
  }
132
143
 
133
- /* Mobile: button shows, list collapses behind it. */
144
+ /* Mobile: button shows, list collapses into a vertical dropdown. The links
145
+ revert to a padded list style (no full-height flush bar — that treatment is
146
+ for the horizontal row only); the current item reads via a subtle fill. */
134
147
  @media (max-width: 47.99rem) {
135
148
  .service-nav {
136
149
  position: relative;
150
+ height: auto;
137
151
  }
138
152
  .service-nav-toggle {
139
153
  display: inline-flex;
@@ -143,12 +157,13 @@
143
157
  position: absolute;
144
158
  top: calc(100% + var(--space-2xs));
145
159
  inset-inline-end: 0;
160
+ height: auto;
146
161
  flex-direction: column;
147
162
  align-items: stretch;
148
- min-width: 12rem;
163
+ min-width: var(--nav-service-dropdown-min-width);
149
164
  padding: var(--space-2xs);
150
165
  background: var(--color-surface);
151
- border: 1px solid var(--color-border);
166
+ border: var(--border-width) solid var(--color-border);
152
167
  border-radius: var(--radius-md);
153
168
  box-shadow: var(--shadow-md);
154
169
  z-index: 50;
@@ -156,5 +171,14 @@
156
171
  .service-nav-list.open {
157
172
  display: flex;
158
173
  }
174
+ .service-nav-link {
175
+ height: auto;
176
+ padding: var(--space-2xs) var(--space-sm);
177
+ border-radius: var(--radius-md);
178
+ }
179
+ .service-nav-link[aria-current="page"] {
180
+ background: var(--color-surface-tertiary);
181
+ box-shadow: none;
182
+ }
159
183
  }
160
184
  </style>
@@ -2,17 +2,36 @@
2
2
  @component SiteHeader
3
3
 
4
4
  Public site banner landmark (`<header role="banner">`). Locked accessibility
5
- chrome for the citizen portal: a brand area, a primary navigation slot, and
6
- an actions slot (locale switch, sign-in). Structure and landmarks are fixed
7
- (conformance-checkable); only brand tokens vary per tenant via `data-theme`.
5
+ chrome for the citizen portal: a brand area, a primary navigation slot, an
6
+ actions slot (locale switch, sign-in), and for two-level information
7
+ architectures an optional contextual `subnav` band and a single unified
8
+ mobile disclosure. Structure and landmarks are fixed (conformance-checkable);
9
+ only brand tokens vary per tenant via `data-theme`.
8
10
 
9
- Icon-agnostic, per DS convention — pass icons/logo as snippets.
11
+ Two tiers, one banner:
12
+ - `nav` — the PRIMARY row (sections), a `ServiceNavigation`.
13
+ - `subnav` — an OPTIONAL second band directly beneath the primary row,
14
+ a `SectionNavigation` showing the active section's pages. It
15
+ lives inside this same sticky banner so it scrolls/sticks as one
16
+ unit and never reflows the primary row sideways.
17
+
18
+ Unified mobile menu (fixes the "two hamburgers" problem): when a `menu` snippet
19
+ is provided, this header renders ONE toggle below `--breakpoint` and hides the
20
+ inline `nav` + `subnav`; both tiers collapse into a single hierarchical panel
21
+ (the `menu` snippet) instead of each nav growing its own toggle. Consumers
22
+ without a `menu` snippet keep the previous behaviour unchanged (backward
23
+ compatible) — the inline `nav`'s own responsive collapse still applies.
24
+
25
+ Icon-agnostic, per DS convention — pass the toggle/close icons as snippets;
26
+ minimal glyphs are used as defaults.
10
27
 
11
28
  @example
12
29
  <SiteHeader>
13
30
  {#snippet brand()}<a href="/"><Logo /> Valongo</a>{/snippet}
14
- {#snippet nav()}<ServiceNavigation items={navItems} />{/snippet}
31
+ {#snippet nav()}<ServiceNavigation items={sections} />{/snippet}
32
+ {#snippet subnav()}<SectionNavigation label={sectionName} items={pages} />{/snippet}
15
33
  {#snippet actions()}<LocaleSwitch /> <Button>Sign in</Button>{/snippet}
34
+ {#snippet menu()}…hierarchical sections → pages tree…{/snippet}
16
35
  </SiteHeader>
17
36
  -->
18
37
  <script>
@@ -25,13 +44,45 @@
25
44
  brand = undefined,
26
45
  /** @type {import('svelte').Snippet | undefined} Primary navigation links. */
27
46
  nav = undefined,
47
+ /** @type {import('svelte').Snippet | undefined} Contextual second band (compose `SectionNavigation`). */
48
+ subnav = undefined,
28
49
  /** @type {import('svelte').Snippet | undefined} Trailing actions (locale, auth). */
29
50
  actions = undefined,
51
+ /** @type {import('svelte').Snippet | undefined} Hierarchical menu content for the unified mobile disclosure. */
52
+ menu = undefined,
53
+ /** @type {string} Visible/aria label for the mobile toggle (localize it). */
54
+ menuLabel = "Menu",
55
+ /** @type {string} aria-label for the mobile menu panel (localize it). */
56
+ menuPanelLabel = "Menu",
57
+ /** @type {string} aria-label for the close button (localize it). */
58
+ closeLabel = "Close",
59
+ /** @type {string} id linking the toggle to the panel (unique per page). */
60
+ menuId = "site-header-menu",
61
+ /** @type {import('svelte').Snippet | undefined} Icon for the mobile toggle. */
62
+ menuIcon = undefined,
63
+ /** @type {import('svelte').Snippet | undefined} Icon for the panel close button. */
64
+ closeIcon = undefined,
30
65
  ...rest
31
66
  } = $props();
67
+
68
+ let open = $state(false);
69
+ const hasMenu = $derived(!!menu);
70
+
71
+ // Lock body scroll while the mobile panel is open (no-op during SSR).
72
+ $effect(() => {
73
+ if (typeof document === "undefined") return;
74
+ document.body.style.overflow = open ? "hidden" : "";
75
+ return () => {
76
+ document.body.style.overflow = "";
77
+ };
78
+ });
32
79
  </script>
33
80
 
34
- <header class="site-header {className}" {...rest}>
81
+ <header
82
+ class="site-header {className}"
83
+ class:has-mobile-menu={hasMenu}
84
+ {...rest}
85
+ >
35
86
  <div class="site-header-inner">
36
87
  {#if brand}
37
88
  <div class="site-header-brand">{@render brand()}</div>
@@ -42,26 +93,106 @@
42
93
  {#if actions}
43
94
  <div class="site-header-actions">{@render actions()}</div>
44
95
  {/if}
96
+ {#if hasMenu}
97
+ <button
98
+ type="button"
99
+ class="site-header-toggle"
100
+ aria-expanded={open}
101
+ aria-controls={menuId}
102
+ aria-haspopup="true"
103
+ onclick={() => (open = true)}
104
+ >
105
+ {#if menuIcon}{@render menuIcon()}{:else}
106
+ <svg
107
+ class="site-header-toggle-glyph"
108
+ viewBox="0 0 16 16"
109
+ aria-hidden="true"
110
+ >
111
+ <path
112
+ d="M2 4h12M2 8h12M2 12h12"
113
+ stroke="currentColor"
114
+ stroke-width="1.5"
115
+ stroke-linecap="round"
116
+ />
117
+ </svg>
118
+ {/if}
119
+ <span class="site-header-toggle-label">{menuLabel}</span>
120
+ </button>
121
+ {/if}
45
122
  </div>
123
+
124
+ <!-- Contextual second band: inside the banner, beneath the primary row. -->
125
+ {#if subnav}{@render subnav()}{/if}
46
126
  </header>
47
127
 
128
+ {#if hasMenu}
129
+ <!-- Single unified mobile disclosure: one panel hosting both nav tiers as a
130
+ hierarchy. Rendered outside the banner so it overlays the page (the
131
+ header stays sticky underneath). -->
132
+ <div
133
+ class="site-header-scrim"
134
+ class:open
135
+ onclick={() => (open = false)}
136
+ aria-hidden="true"
137
+ ></div>
138
+ <div
139
+ id={menuId}
140
+ class="site-header-panel"
141
+ class:open
142
+ role="dialog"
143
+ aria-modal="true"
144
+ aria-label={menuPanelLabel}
145
+ >
146
+ <div class="site-header-panel-head">
147
+ <span class="site-header-panel-title">{menuPanelLabel}</span>
148
+ <button
149
+ type="button"
150
+ class="site-header-close"
151
+ aria-label={closeLabel}
152
+ onclick={() => (open = false)}
153
+ >
154
+ {#if closeIcon}{@render closeIcon()}{:else}
155
+ <svg viewBox="0 0 16 16" aria-hidden="true">
156
+ <path
157
+ d="M4 4l8 8M12 4l-8 8"
158
+ stroke="currentColor"
159
+ stroke-width="1.5"
160
+ stroke-linecap="round"
161
+ />
162
+ </svg>
163
+ {/if}
164
+ </button>
165
+ </div>
166
+ <div class="site-header-panel-body">{@render menu()}</div>
167
+ </div>
168
+ {/if}
169
+
170
+ <svelte:window
171
+ onkeydown={(e) => {
172
+ if (e.key === "Escape") open = false;
173
+ }}
174
+ />
175
+
48
176
  <style>
49
177
  .site-header {
50
178
  position: sticky;
51
179
  top: 0;
52
180
  z-index: 40;
53
181
  background: var(--color-surface);
54
- border-bottom: 1px solid var(--color-border);
182
+ border-bottom: var(--border-width) solid var(--color-border);
55
183
  }
56
184
 
185
+ /* Fixed gridded height + stretch so the primary nav links fill the band and
186
+ their active underline bar sits flush on the header's bottom border. */
57
187
  .site-header-inner {
58
188
  display: flex;
59
- align-items: center;
189
+ align-items: stretch;
60
190
  gap: var(--space-lg);
61
191
  width: 100%;
62
192
  max-width: var(--content-width-wide);
63
193
  margin-inline: auto;
64
- padding: var(--space-sm) var(--content-padding-x);
194
+ height: var(--nav-service-height);
195
+ padding-inline: var(--content-padding-x);
65
196
  }
66
197
 
67
198
  .site-header-brand {
@@ -74,10 +205,11 @@
74
205
  }
75
206
 
76
207
  /* The nav slot hosts a `ServiceNavigation` (data-driven, self-styled).
77
- The header doesn't style anchors itself no descendant-anchor magic. */
208
+ Stretch so its full-height links can fill the band; the header doesn't
209
+ style anchors itself — no descendant-anchor magic. */
78
210
  .site-header-nav {
79
211
  display: flex;
80
- align-items: center;
212
+ align-items: stretch;
81
213
  }
82
214
 
83
215
  /* Push trailing actions to the far end regardless of nav presence. */
@@ -87,4 +219,121 @@
87
219
  gap: var(--space-sm);
88
220
  margin-inline-start: auto;
89
221
  }
222
+
223
+ /* ─── Mobile toggle (only when a `menu` snippet is provided) ─── */
224
+ .site-header-toggle {
225
+ display: none;
226
+ align-self: center;
227
+ align-items: center;
228
+ gap: var(--space-2xs);
229
+ padding: var(--space-2xs) var(--space-sm);
230
+ border: var(--border-width) solid var(--color-border);
231
+ border-radius: var(--radius-md);
232
+ background: var(--color-surface);
233
+ color: var(--color-text);
234
+ font-family: var(--type-label-font);
235
+ font-size: var(--type-label-size);
236
+ font-weight: var(--type-label-weight);
237
+ cursor: pointer;
238
+ }
239
+ .site-header-toggle:focus-visible,
240
+ .site-header-close:focus-visible {
241
+ outline: var(--focus-ring-width) solid var(--focus-ring-color);
242
+ outline-offset: var(--focus-ring-offset);
243
+ }
244
+ .site-header-toggle-glyph {
245
+ width: var(--icon-size-sm);
246
+ height: var(--icon-size-sm);
247
+ }
248
+
249
+ /* ─── Mobile panel + scrim ─── */
250
+ .site-header-scrim {
251
+ position: fixed;
252
+ inset: 0;
253
+ z-index: 60;
254
+ background: var(--color-overlay);
255
+ opacity: 0;
256
+ visibility: hidden;
257
+ transition:
258
+ opacity var(--duration-fast) var(--easing-default),
259
+ visibility var(--duration-fast) var(--easing-default);
260
+ }
261
+ .site-header-scrim.open {
262
+ opacity: 1;
263
+ visibility: visible;
264
+ }
265
+ .site-header-panel {
266
+ position: fixed;
267
+ inset-block: 0;
268
+ inset-inline-end: 0;
269
+ z-index: 61;
270
+ width: var(--nav-menu-panel-width);
271
+ max-width: 100vw;
272
+ display: flex;
273
+ flex-direction: column;
274
+ overflow-y: auto;
275
+ background: var(--color-surface-raised);
276
+ border-inline-start: var(--border-width) solid var(--color-border);
277
+ box-shadow: var(--elevation-overlay);
278
+ transform: translateX(100%);
279
+ transition: transform var(--duration-normal) var(--easing-enter);
280
+ }
281
+ .site-header-panel.open {
282
+ transform: none;
283
+ }
284
+ .site-header-panel-head {
285
+ display: flex;
286
+ align-items: center;
287
+ justify-content: space-between;
288
+ padding: var(--space-md);
289
+ border-bottom: var(--border-width) solid var(--color-border);
290
+ }
291
+ .site-header-panel-title {
292
+ font-family: var(--type-heading-sm-font);
293
+ font-size: var(--type-heading-sm-size);
294
+ font-weight: var(--type-heading-sm-weight);
295
+ color: var(--color-text);
296
+ }
297
+ .site-header-close {
298
+ display: inline-flex;
299
+ align-items: center;
300
+ justify-content: center;
301
+ padding: var(--space-xs);
302
+ border: var(--border-width) solid transparent;
303
+ border-radius: var(--radius-md);
304
+ background: none;
305
+ color: var(--color-text-secondary);
306
+ cursor: pointer;
307
+ }
308
+ .site-header-close svg {
309
+ width: var(--icon-size-md);
310
+ height: var(--icon-size-md);
311
+ }
312
+ .site-header-close:hover {
313
+ color: var(--color-text);
314
+ }
315
+ .site-header-panel-body {
316
+ flex: 1 0 auto;
317
+ }
318
+
319
+ /* Below the breakpoint, when a unified menu exists: hide both inline nav tiers
320
+ and reveal the single toggle. The actions cluster (locale/auth) stays in the
321
+ bar. Consumers WITHOUT a `menu` keep the inline nav + its own collapse.
322
+ (SectionNavigation already self-hides below the breakpoint.) */
323
+ @media (max-width: 47.99rem) {
324
+ .has-mobile-menu .site-header-nav {
325
+ display: none;
326
+ }
327
+ .has-mobile-menu .site-header-toggle {
328
+ display: inline-flex;
329
+ }
330
+ }
331
+
332
+ /* Reduced-motion: drop the slide/fade, keep the state change instant. */
333
+ @media (prefers-reduced-motion: reduce) {
334
+ .site-header-scrim,
335
+ .site-header-panel {
336
+ transition: none;
337
+ }
338
+ }
90
339
  </style>
@@ -42,6 +42,7 @@ export { default as TextSizeAdjuster } from "./TextSizeAdjuster.svelte";
42
42
  export { default as ContrastToggle } from "./ContrastToggle.svelte";
43
43
  export { default as LinkHighlightToggle } from "./LinkHighlightToggle.svelte";
44
44
  export { default as ServiceNavigation } from "./ServiceNavigation.svelte";
45
+ export { default as SectionNavigation } from "./SectionNavigation.svelte";
45
46
  export { default as Link } from "./Link.svelte";
46
47
  export { default as Hero } from "./Hero.svelte";
47
48
  export { default as ContentBlock } from "./ContentBlock.svelte";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiaiai-pt/design-system",
3
- "version": "0.32.0",
3
+ "version": "0.33.0",
4
4
  "description": "Design system tokens and Svelte components for aiaiai products",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -200,6 +200,35 @@
200
200
  --nav-bottom-item-font: var(--type-caption-font);
201
201
  --nav-bottom-item-size: var(--type-caption-size);
202
202
 
203
+ /* ═══════════════════════════════════════════════
204
+ NAVIGATION — SERVICE & SECTION (PUBLIC PORTAL)
205
+ ═══════════════════════════════════════════════ */
206
+
207
+ /* Active-item underline bar shared by ServiceNavigation (primary tier) and
208
+ SectionNavigation (contextual second band) so both tiers read as one
209
+ system — the single source of truth for the bar thickness. The links fill
210
+ their band's full height so the bar sits flush on the band's bottom border
211
+ (the GOV.UK / National Archives treatment), never floating mid-row. */
212
+ --nav-service-underline: 3px;
213
+
214
+ /* Gridded band heights (8px base) — the primary header band and the
215
+ contextual section band below it. */
216
+ --nav-service-height: 64px;
217
+ --nav-section-height: 48px;
218
+
219
+ /* Type step-down: the primary tier reads larger than the contextual tier so
220
+ the two-level hierarchy is legible at a glance. */
221
+ --nav-service-link-size: var(--type-body-size);
222
+ --nav-section-link-size: var(--type-body-sm-size);
223
+
224
+ /* Width of SiteHeader's unified mobile disclosure panel (the single drawer
225
+ that replaces the per-nav hamburgers). Mirrors the narrow content panel. */
226
+ --nav-menu-panel-width: var(--panel-width-narrow);
227
+
228
+ /* Min width of ServiceNavigation's own mobile dropdown (used when the nav is
229
+ rendered standalone, without SiteHeader's unified menu). */
230
+ --nav-service-dropdown-min-width: 12rem;
231
+
203
232
  /* ═══════════════════════════════════════════════
204
233
  DATA DISPLAY
205
234
  ═══════════════════════════════════════════════ */