@aiaiai-pt/design-system 0.17.1 → 0.19.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/ActionFormRenderer.svelte +41 -3
- package/components/ContrastToggle.svelte +36 -0
- package/components/ContrastToggle.svelte.d.ts +30 -0
- package/components/FeedView.svelte +190 -0
- package/components/FeedView.svelte.d.ts +54 -0
- package/components/FileUpload.svelte +14 -10
- package/components/FilterBar.svelte +147 -58
- package/components/FilterBar.svelte.d.ts +56 -14
- package/components/LinkHighlightToggle.svelte +37 -0
- package/components/LinkHighlightToggle.svelte.d.ts +31 -0
- package/components/MapCluster.svelte +84 -7
- package/components/MapCluster.svelte.d.ts +2 -0
- package/components/TextSizeAdjuster.svelte +150 -0
- package/components/TextSizeAdjuster.svelte.d.ts +42 -0
- package/components/index.d.ts +4 -0
- package/components/index.js +4 -0
- package/components/map-utils.d.ts +107 -0
- package/components/text-size.d.ts +50 -0
- package/components/text-size.js +72 -0
- package/package.json +1 -1
- package/tokens/components.css +19 -0
- package/tokens/semantic.css +75 -0
- package/tokens/themes/valongo.css +28 -1
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component TextSizeAdjuster
|
|
3
|
+
|
|
4
|
+
Citizen text-size control (#244 C7 accessibility pack). Three buttons —
|
|
5
|
+
decrease (A−), reset (A), increase (A+) — step the root font scale across four
|
|
6
|
+
rungs (100–160%), so a citizen can enlarge the rem-based type system without
|
|
7
|
+
breaking the layout (WCAG 1.4.4 Resize Text). The classic pt-gov
|
|
8
|
+
"diminuir / normal / aumentar" pattern.
|
|
9
|
+
|
|
10
|
+
Presentational: it reports the chosen size via `onchange`; the consumer
|
|
11
|
+
persists it (cookie) and applies the `:root[data-text-size="N"]` layer
|
|
12
|
+
(tokens/semantic.css) server-side, so SSR paints it with no flash. Controls
|
|
13
|
+
disable at the bounds; a polite live region announces the current size for
|
|
14
|
+
screen-reader users. Labels are props for i18n (default English).
|
|
15
|
+
|
|
16
|
+
@example
|
|
17
|
+
<TextSizeAdjuster size={prefs.textSize} onchange={(n) => setTextSize(n)} />
|
|
18
|
+
-->
|
|
19
|
+
<script>
|
|
20
|
+
import {
|
|
21
|
+
DEFAULT_TEXT_SIZE,
|
|
22
|
+
normalizeTextSize,
|
|
23
|
+
increaseTextSize,
|
|
24
|
+
decreaseTextSize,
|
|
25
|
+
isMinTextSize,
|
|
26
|
+
isMaxTextSize,
|
|
27
|
+
} from "./text-size.js";
|
|
28
|
+
|
|
29
|
+
let {
|
|
30
|
+
/** @type {number} Current size step (percent). Off-ladder values normalize. */
|
|
31
|
+
size = DEFAULT_TEXT_SIZE,
|
|
32
|
+
/** @type {((size: number) => void) | undefined} */
|
|
33
|
+
onchange = undefined,
|
|
34
|
+
/** @type {string} Group label (i18n). */
|
|
35
|
+
label = "Text size",
|
|
36
|
+
/** @type {string} Decrease-button accessible label (i18n). */
|
|
37
|
+
decreaseLabel = "Decrease text size",
|
|
38
|
+
/** @type {string} Reset-button accessible label (i18n). */
|
|
39
|
+
resetLabel = "Reset text size",
|
|
40
|
+
/** @type {string} Increase-button accessible label (i18n). */
|
|
41
|
+
increaseLabel = "Increase text size",
|
|
42
|
+
/** @type {string} */
|
|
43
|
+
class: className = "",
|
|
44
|
+
...rest
|
|
45
|
+
} = $props();
|
|
46
|
+
|
|
47
|
+
const current = $derived(normalizeTextSize(size));
|
|
48
|
+
const atMin = $derived(isMinTextSize(current));
|
|
49
|
+
const atMax = $derived(isMaxTextSize(current));
|
|
50
|
+
const atDefault = $derived(current === DEFAULT_TEXT_SIZE);
|
|
51
|
+
|
|
52
|
+
function emit(next) {
|
|
53
|
+
if (next !== current) onchange?.(next);
|
|
54
|
+
}
|
|
55
|
+
</script>
|
|
56
|
+
|
|
57
|
+
<div
|
|
58
|
+
class="text-size-adjuster {className}"
|
|
59
|
+
role="group"
|
|
60
|
+
aria-label={label}
|
|
61
|
+
data-testid="text-size-adjuster"
|
|
62
|
+
{...rest}
|
|
63
|
+
>
|
|
64
|
+
<button
|
|
65
|
+
type="button"
|
|
66
|
+
class="ts-btn ts-decrease"
|
|
67
|
+
aria-label={decreaseLabel}
|
|
68
|
+
disabled={atMin}
|
|
69
|
+
onclick={() => emit(decreaseTextSize(current))}
|
|
70
|
+
>A<span aria-hidden="true">−</span></button>
|
|
71
|
+
<button
|
|
72
|
+
type="button"
|
|
73
|
+
class="ts-btn ts-reset"
|
|
74
|
+
aria-label={resetLabel}
|
|
75
|
+
disabled={atDefault}
|
|
76
|
+
onclick={() => emit(DEFAULT_TEXT_SIZE)}
|
|
77
|
+
>A</button>
|
|
78
|
+
<button
|
|
79
|
+
type="button"
|
|
80
|
+
class="ts-btn ts-increase"
|
|
81
|
+
aria-label={increaseLabel}
|
|
82
|
+
disabled={atMax}
|
|
83
|
+
onclick={() => emit(increaseTextSize(current))}
|
|
84
|
+
>A<span aria-hidden="true">+</span></button>
|
|
85
|
+
<span class="sr-only" aria-live="polite" data-testid="text-size-readout"
|
|
86
|
+
>{current}%</span
|
|
87
|
+
>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<style>
|
|
91
|
+
.text-size-adjuster {
|
|
92
|
+
display: inline-flex;
|
|
93
|
+
align-items: center;
|
|
94
|
+
gap: var(--space-2xs);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.ts-btn {
|
|
98
|
+
display: inline-flex;
|
|
99
|
+
align-items: baseline;
|
|
100
|
+
justify-content: center;
|
|
101
|
+
min-width: var(--space-lg);
|
|
102
|
+
font: inherit;
|
|
103
|
+
color: var(--color-text-secondary);
|
|
104
|
+
background: none;
|
|
105
|
+
border: var(--border-width-thin, 1px) solid var(--color-border);
|
|
106
|
+
border-radius: var(--radius-md);
|
|
107
|
+
padding: var(--space-2xs) var(--space-xs);
|
|
108
|
+
cursor: pointer;
|
|
109
|
+
line-height: 1;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* The glyph "A" sizes telegraph the control: smaller on decrease, larger on
|
|
113
|
+
increase, so the affordance reads without relying on the +/− alone. */
|
|
114
|
+
.ts-decrease {
|
|
115
|
+
font-size: var(--type-body-sm-size);
|
|
116
|
+
}
|
|
117
|
+
.ts-reset {
|
|
118
|
+
font-size: var(--type-body-size);
|
|
119
|
+
}
|
|
120
|
+
.ts-increase {
|
|
121
|
+
font-size: var(--type-heading-sm-size);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.ts-btn:hover:not(:disabled) {
|
|
125
|
+
color: var(--color-text);
|
|
126
|
+
background: var(--color-surface-secondary);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.ts-btn:focus-visible {
|
|
130
|
+
outline: var(--focus-ring-width) solid var(--focus-ring-color);
|
|
131
|
+
outline-offset: var(--focus-ring-offset);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.ts-btn:disabled {
|
|
135
|
+
opacity: 0.4;
|
|
136
|
+
cursor: not-allowed;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.sr-only {
|
|
140
|
+
position: absolute;
|
|
141
|
+
width: 1px;
|
|
142
|
+
height: 1px;
|
|
143
|
+
padding: 0;
|
|
144
|
+
margin: -1px;
|
|
145
|
+
overflow: hidden;
|
|
146
|
+
clip: rect(0, 0, 0, 0);
|
|
147
|
+
white-space: nowrap;
|
|
148
|
+
border: 0;
|
|
149
|
+
}
|
|
150
|
+
</style>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export default TextSizeAdjuster;
|
|
2
|
+
type TextSizeAdjuster = {
|
|
3
|
+
$on?(type: string, callback: (e: any) => void): () => void;
|
|
4
|
+
$set?(props: Partial<$$ComponentProps>): void;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* TextSizeAdjuster
|
|
8
|
+
*
|
|
9
|
+
* Citizen text-size control (#244 C7 accessibility pack). Three buttons —
|
|
10
|
+
* decrease (A−), reset (A), increase (A+) — step the root font scale across four
|
|
11
|
+
* rungs (100–160%), so a citizen can enlarge the rem-based type system without
|
|
12
|
+
* breaking the layout (WCAG 1.4.4 Resize Text). The classic pt-gov
|
|
13
|
+
* "diminuir / normal / aumentar" pattern.
|
|
14
|
+
*
|
|
15
|
+
* Presentational: it reports the chosen size via `onchange`; the consumer
|
|
16
|
+
* persists it (cookie) and applies the `:root[data-text-size="N"]` layer
|
|
17
|
+
* (tokens/semantic.css) server-side, so SSR paints it with no flash. Controls
|
|
18
|
+
* disable at the bounds; a polite live region announces the current size for
|
|
19
|
+
* screen-reader users. Labels are props for i18n (default English).
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* <TextSizeAdjuster size={prefs.textSize} onchange={(n) => setTextSize(n)} />
|
|
23
|
+
*/
|
|
24
|
+
declare const TextSizeAdjuster: import("svelte").Component<{
|
|
25
|
+
size?: typeof DEFAULT_TEXT_SIZE;
|
|
26
|
+
onchange?: any;
|
|
27
|
+
label?: string;
|
|
28
|
+
decreaseLabel?: string;
|
|
29
|
+
resetLabel?: string;
|
|
30
|
+
increaseLabel?: string;
|
|
31
|
+
class?: string;
|
|
32
|
+
} & Record<string, any>, {}, "">;
|
|
33
|
+
type $$ComponentProps = {
|
|
34
|
+
size?: typeof DEFAULT_TEXT_SIZE;
|
|
35
|
+
onchange?: any;
|
|
36
|
+
label?: string;
|
|
37
|
+
decreaseLabel?: string;
|
|
38
|
+
resetLabel?: string;
|
|
39
|
+
increaseLabel?: string;
|
|
40
|
+
class?: string;
|
|
41
|
+
} & Record<string, any>;
|
|
42
|
+
import { DEFAULT_TEXT_SIZE } from "./text-size.js";
|
package/components/index.d.ts
CHANGED
|
@@ -21,6 +21,9 @@ export { default as AppFrame } from "./AppFrame.svelte";
|
|
|
21
21
|
export { default as SiteHeader } from "./SiteHeader.svelte";
|
|
22
22
|
export { default as SiteFooter } from "./SiteFooter.svelte";
|
|
23
23
|
export { default as SkipLink } from "./SkipLink.svelte";
|
|
24
|
+
export { default as TextSizeAdjuster } from "./TextSizeAdjuster.svelte";
|
|
25
|
+
export { default as ContrastToggle } from "./ContrastToggle.svelte";
|
|
26
|
+
export { default as LinkHighlightToggle } from "./LinkHighlightToggle.svelte";
|
|
24
27
|
export { default as ServiceNavigation } from "./ServiceNavigation.svelte";
|
|
25
28
|
export { default as Link } from "./Link.svelte";
|
|
26
29
|
export { default as Hero } from "./Hero.svelte";
|
|
@@ -47,6 +50,7 @@ export { default as Tabs } from "./Tabs.svelte";
|
|
|
47
50
|
export { default as TabList } from "./TabList.svelte";
|
|
48
51
|
export { default as Tab } from "./Tab.svelte";
|
|
49
52
|
export { default as TabPanel } from "./TabPanel.svelte";
|
|
53
|
+
export { default as FeedView } from "./FeedView.svelte";
|
|
50
54
|
export { default as Alert } from "./Alert.svelte";
|
|
51
55
|
export { default as Toast } from "./Toast.svelte";
|
|
52
56
|
export { default as ToastManager } from "./ToastManager.svelte";
|
package/components/index.js
CHANGED
|
@@ -38,6 +38,9 @@ export { default as AppFrame } from "./AppFrame.svelte";
|
|
|
38
38
|
export { default as SiteHeader } from "./SiteHeader.svelte";
|
|
39
39
|
export { default as SiteFooter } from "./SiteFooter.svelte";
|
|
40
40
|
export { default as SkipLink } from "./SkipLink.svelte";
|
|
41
|
+
export { default as TextSizeAdjuster } from "./TextSizeAdjuster.svelte";
|
|
42
|
+
export { default as ContrastToggle } from "./ContrastToggle.svelte";
|
|
43
|
+
export { default as LinkHighlightToggle } from "./LinkHighlightToggle.svelte";
|
|
41
44
|
export { default as ServiceNavigation } from "./ServiceNavigation.svelte";
|
|
42
45
|
export { default as Link } from "./Link.svelte";
|
|
43
46
|
export { default as Hero } from "./Hero.svelte";
|
|
@@ -72,6 +75,7 @@ export { default as Tabs } from "./Tabs.svelte";
|
|
|
72
75
|
export { default as TabList } from "./TabList.svelte";
|
|
73
76
|
export { default as Tab } from "./Tab.svelte";
|
|
74
77
|
export { default as TabPanel } from "./TabPanel.svelte";
|
|
78
|
+
export { default as FeedView } from "./FeedView.svelte";
|
|
75
79
|
|
|
76
80
|
// Feedback
|
|
77
81
|
export { default as Alert } from "./Alert.svelte";
|
|
@@ -73,6 +73,77 @@ export function cssPx(el: Element, prop: string, fallback: number): number;
|
|
|
73
73
|
* @returns {string[]}
|
|
74
74
|
*/
|
|
75
75
|
export function getHeatmapGradient(el: Element): string[];
|
|
76
|
+
/**
|
|
77
|
+
* @typedef {object} OverlayLayerStyle
|
|
78
|
+
* Flat style subset applied to a whole overlay layer. Colors are CSS color
|
|
79
|
+
* strings (they come from layer DATA — geolayers GeoStyler styles — not
|
|
80
|
+
* from design tokens; absent values fall back to the DS-tokened defaults).
|
|
81
|
+
* @property {string} [pointColor]
|
|
82
|
+
* @property {number} [pointRadius]
|
|
83
|
+
* @property {string} [strokeColor]
|
|
84
|
+
* @property {number} [strokeWidth]
|
|
85
|
+
* @property {string} [fillColor]
|
|
86
|
+
*
|
|
87
|
+
* @typedef {object} OverlayLayerDef
|
|
88
|
+
* One ordered overlay rendered between the tile layer and the component's
|
|
89
|
+
* own interactive vector layer. v1 is GeoJSON-only — the platform's citizen
|
|
90
|
+
* data plane serves GeoJSON (`/{app}/public/layers/{id}/features`); WMS/tile
|
|
91
|
+
* overlays arrive with the per-tenant raster principal (spec open question).
|
|
92
|
+
* @property {string} [id]
|
|
93
|
+
* @property {'geojson'} [type]
|
|
94
|
+
* @property {object} [data] — inline GeoJSON (Feature/FeatureCollection)
|
|
95
|
+
* @property {string} [url] — fetched when `data` is absent
|
|
96
|
+
* @property {OverlayLayerStyle | object} [style] — flat style or GeoStyler
|
|
97
|
+
* JSON (best-effort subset via `geoStylerToFlat`)
|
|
98
|
+
* @property {boolean} [visible]
|
|
99
|
+
*/
|
|
100
|
+
/**
|
|
101
|
+
* Best-effort GeoStyler → flat style. Reads the first symbolizer of the
|
|
102
|
+
* first rule — enough for the platform's single-rule layer styles. Unknown
|
|
103
|
+
* shapes return {} so the DS-tokened defaults apply.
|
|
104
|
+
*
|
|
105
|
+
* @param {any} style
|
|
106
|
+
* @returns {OverlayLayerStyle}
|
|
107
|
+
*/
|
|
108
|
+
export function geoStylerToFlat(style: any): OverlayLayerStyle;
|
|
109
|
+
/**
|
|
110
|
+
* Builds OL vector layers for `layers` overlay defs, ordered as given.
|
|
111
|
+
* URL-sourced layers load asynchronously into their source; a fetch failure
|
|
112
|
+
* leaves that overlay empty and logs a warning (the map still renders).
|
|
113
|
+
*
|
|
114
|
+
* @param {OverlayLayerDef[]} defs
|
|
115
|
+
* @param {import('./map-utils.js').MapStyles} styles — DS default styles
|
|
116
|
+
* @returns {Promise<import('ol/layer/Vector.js').default[]>}
|
|
117
|
+
*/
|
|
118
|
+
export function createOverlayLayers(defs: OverlayLayerDef[], styles: import("./map-utils.js").MapStyles): Promise<import("ol/layer/Vector.js").default[]>;
|
|
119
|
+
/**
|
|
120
|
+
* Normalises a boundary prop into polygon rings (lon/lat WGS84).
|
|
121
|
+
* Accepts: raw ring `number[][]`, GeoJSON Polygon / MultiPolygon,
|
|
122
|
+
* Feature, or FeatureCollection (first polygonal feature per entry).
|
|
123
|
+
*
|
|
124
|
+
* @param {any} boundary
|
|
125
|
+
* @returns {number[][][]} — array of outer rings (one per polygon)
|
|
126
|
+
*/
|
|
127
|
+
export function boundaryToRings(boundary: any): number[][][];
|
|
128
|
+
/**
|
|
129
|
+
* Ray-casting point-in-polygon over WGS84 rings (outer rings only — holes
|
|
130
|
+
* are out of scope for the boundary gate; the BFF hard-gate is authoritative).
|
|
131
|
+
*
|
|
132
|
+
* @param {[number, number]} lonLat
|
|
133
|
+
* @param {number[][][]} rings — from `boundaryToRings`
|
|
134
|
+
* @returns {boolean} true when the point is inside ANY ring
|
|
135
|
+
*/
|
|
136
|
+
export function pointInRings(lonLat: [number, number], rings: number[][][]): boolean;
|
|
137
|
+
/**
|
|
138
|
+
* Builds the boundary overlay layer (dashed outline, faint fill — distinct
|
|
139
|
+
* from the selection polygon so "the edge of the allowed area" reads as
|
|
140
|
+
* context, not content). Token-driven: --map-boundary-*.
|
|
141
|
+
*
|
|
142
|
+
* @param {number[][][]} rings
|
|
143
|
+
* @param {Element} el — token read context
|
|
144
|
+
* @returns {Promise<import('ol/layer/Vector.js').default | null>}
|
|
145
|
+
*/
|
|
146
|
+
export function createBoundaryLayer(rings: number[][][], el: Element): Promise<import("ol/layer/Vector.js").default | null>;
|
|
76
147
|
export type OsmSource = {
|
|
77
148
|
type: "osm";
|
|
78
149
|
};
|
|
@@ -98,3 +169,39 @@ export type MapStyles = {
|
|
|
98
169
|
*/
|
|
99
170
|
refresh: () => void;
|
|
100
171
|
};
|
|
172
|
+
/**
|
|
173
|
+
* Flat style subset applied to a whole overlay layer. Colors are CSS color
|
|
174
|
+
* strings (they come from layer DATA — geolayers GeoStyler styles — not
|
|
175
|
+
* from design tokens; absent values fall back to the DS-tokened defaults).
|
|
176
|
+
*/
|
|
177
|
+
export type OverlayLayerStyle = {
|
|
178
|
+
pointColor?: string;
|
|
179
|
+
pointRadius?: number;
|
|
180
|
+
strokeColor?: string;
|
|
181
|
+
strokeWidth?: number;
|
|
182
|
+
fillColor?: string;
|
|
183
|
+
};
|
|
184
|
+
/**
|
|
185
|
+
* One ordered overlay rendered between the tile layer and the component's
|
|
186
|
+
* own interactive vector layer. v1 is GeoJSON-only — the platform's citizen
|
|
187
|
+
* data plane serves GeoJSON (`/{app}/public/layers/{id}/features`); WMS/tile
|
|
188
|
+
* overlays arrive with the per-tenant raster principal (spec open question).
|
|
189
|
+
*/
|
|
190
|
+
export type OverlayLayerDef = {
|
|
191
|
+
id?: string;
|
|
192
|
+
type?: "geojson";
|
|
193
|
+
/**
|
|
194
|
+
* — inline GeoJSON (Feature/FeatureCollection)
|
|
195
|
+
*/
|
|
196
|
+
data?: object;
|
|
197
|
+
/**
|
|
198
|
+
* — fetched when `data` is absent
|
|
199
|
+
*/
|
|
200
|
+
url?: string;
|
|
201
|
+
/**
|
|
202
|
+
* — flat style or GeoStyler
|
|
203
|
+
* JSON (best-effort subset via `geoStylerToFlat`)
|
|
204
|
+
*/
|
|
205
|
+
style?: OverlayLayerStyle | object;
|
|
206
|
+
visible?: boolean;
|
|
207
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clamp an arbitrary value to a valid ladder step. The cookie can carry junk or
|
|
3
|
+
* a numeric string (`"120"`), so coerce + validate; anything off-ladder falls
|
|
4
|
+
* back to the default. Idempotent.
|
|
5
|
+
* @param {unknown} value
|
|
6
|
+
* @returns {number}
|
|
7
|
+
*/
|
|
8
|
+
export function normalizeTextSize(value: unknown): number;
|
|
9
|
+
/**
|
|
10
|
+
* The next step up, clamped at the top rung.
|
|
11
|
+
* @param {unknown} value
|
|
12
|
+
* @returns {number}
|
|
13
|
+
*/
|
|
14
|
+
export function increaseTextSize(value: unknown): number;
|
|
15
|
+
/**
|
|
16
|
+
* The next step down, clamped at the bottom rung.
|
|
17
|
+
* @param {unknown} value
|
|
18
|
+
* @returns {number}
|
|
19
|
+
*/
|
|
20
|
+
export function decreaseTextSize(value: unknown): number;
|
|
21
|
+
/**
|
|
22
|
+
* Whether the value is at the bottom rung (decrease control should be disabled).
|
|
23
|
+
* @param {unknown} value
|
|
24
|
+
* @returns {boolean}
|
|
25
|
+
*/
|
|
26
|
+
export function isMinTextSize(value: unknown): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Whether the value is at the top rung (increase control should be disabled).
|
|
29
|
+
* @param {unknown} value
|
|
30
|
+
* @returns {boolean}
|
|
31
|
+
*/
|
|
32
|
+
export function isMaxTextSize(value: unknown): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Text-size preference ladder (#244 C7 — DS accessibility pack).
|
|
35
|
+
*
|
|
36
|
+
* Four steps spanning 100–160% let a citizen scale the rem-based type system
|
|
37
|
+
* (WCAG 1.4.4 Resize Text) without breaking the layout. Pure logic: the
|
|
38
|
+
* `TextSizeAdjuster` component renders the controls, and the consumer persists
|
|
39
|
+
* the choice (cookie) + applies it as a root font-scale via the
|
|
40
|
+
* `:root[data-text-size="N"]` layer in tokens/semantic.css. Kept dependency-free
|
|
41
|
+
* + framework-agnostic so it is unit-testable with `node:test`.
|
|
42
|
+
*/
|
|
43
|
+
/** The valid text-size steps, ascending (percent of the base root font-size). */
|
|
44
|
+
export const TEXT_SIZE_STEPS: number[];
|
|
45
|
+
/**
|
|
46
|
+
* The default (no preference) — the bottom rung. Derived from the ladder (not a
|
|
47
|
+
* literal `100`) so its inferred type stays `number`: consumers bind it to a
|
|
48
|
+
* `number`-typed prop, and a literal type would reject any other rung.
|
|
49
|
+
*/
|
|
50
|
+
export const DEFAULT_TEXT_SIZE: number;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text-size preference ladder (#244 C7 — DS accessibility pack).
|
|
3
|
+
*
|
|
4
|
+
* Four steps spanning 100–160% let a citizen scale the rem-based type system
|
|
5
|
+
* (WCAG 1.4.4 Resize Text) without breaking the layout. Pure logic: the
|
|
6
|
+
* `TextSizeAdjuster` component renders the controls, and the consumer persists
|
|
7
|
+
* the choice (cookie) + applies it as a root font-scale via the
|
|
8
|
+
* `:root[data-text-size="N"]` layer in tokens/semantic.css. Kept dependency-free
|
|
9
|
+
* + framework-agnostic so it is unit-testable with `node:test`.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** The valid text-size steps, ascending (percent of the base root font-size). */
|
|
13
|
+
export const TEXT_SIZE_STEPS = [100, 120, 140, 160];
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The default (no preference) — the bottom rung. Derived from the ladder (not a
|
|
17
|
+
* literal `100`) so its inferred type stays `number`: consumers bind it to a
|
|
18
|
+
* `number`-typed prop, and a literal type would reject any other rung.
|
|
19
|
+
*/
|
|
20
|
+
export const DEFAULT_TEXT_SIZE = TEXT_SIZE_STEPS[0];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Clamp an arbitrary value to a valid ladder step. The cookie can carry junk or
|
|
24
|
+
* a numeric string (`"120"`), so coerce + validate; anything off-ladder falls
|
|
25
|
+
* back to the default. Idempotent.
|
|
26
|
+
* @param {unknown} value
|
|
27
|
+
* @returns {number}
|
|
28
|
+
*/
|
|
29
|
+
export function normalizeTextSize(value) {
|
|
30
|
+
const n = Number(value);
|
|
31
|
+
return TEXT_SIZE_STEPS.includes(n) ? n : DEFAULT_TEXT_SIZE;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The next step up, clamped at the top rung.
|
|
36
|
+
* @param {unknown} value
|
|
37
|
+
* @returns {number}
|
|
38
|
+
*/
|
|
39
|
+
export function increaseTextSize(value) {
|
|
40
|
+
const i = TEXT_SIZE_STEPS.indexOf(normalizeTextSize(value));
|
|
41
|
+
return TEXT_SIZE_STEPS[Math.min(i + 1, TEXT_SIZE_STEPS.length - 1)];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The next step down, clamped at the bottom rung.
|
|
46
|
+
* @param {unknown} value
|
|
47
|
+
* @returns {number}
|
|
48
|
+
*/
|
|
49
|
+
export function decreaseTextSize(value) {
|
|
50
|
+
const i = TEXT_SIZE_STEPS.indexOf(normalizeTextSize(value));
|
|
51
|
+
return TEXT_SIZE_STEPS[Math.max(i - 1, 0)];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Whether the value is at the bottom rung (decrease control should be disabled).
|
|
56
|
+
* @param {unknown} value
|
|
57
|
+
* @returns {boolean}
|
|
58
|
+
*/
|
|
59
|
+
export function isMinTextSize(value) {
|
|
60
|
+
return normalizeTextSize(value) === TEXT_SIZE_STEPS[0];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Whether the value is at the top rung (increase control should be disabled).
|
|
65
|
+
* @param {unknown} value
|
|
66
|
+
* @returns {boolean}
|
|
67
|
+
*/
|
|
68
|
+
export function isMaxTextSize(value) {
|
|
69
|
+
return (
|
|
70
|
+
normalizeTextSize(value) === TEXT_SIZE_STEPS[TEXT_SIZE_STEPS.length - 1]
|
|
71
|
+
);
|
|
72
|
+
}
|
package/package.json
CHANGED
package/tokens/components.css
CHANGED
|
@@ -403,6 +403,25 @@
|
|
|
403
403
|
--tabs-trigger-transition: var(--duration-instant) var(--easing-default);
|
|
404
404
|
--tabs-content-padding-top: var(--space-md);
|
|
405
405
|
|
|
406
|
+
/* ═══════════════════════════════════════════════
|
|
407
|
+
FEEDVIEW (segmented view-switch — list ⇄ map)
|
|
408
|
+
═══════════════════════════════════════════════ */
|
|
409
|
+
|
|
410
|
+
--feedview-bg: var(--color-surface-secondary);
|
|
411
|
+
--feedview-radius: var(--radius-md);
|
|
412
|
+
--feedview-padding: var(--space-2xs);
|
|
413
|
+
--feedview-tab-font: var(--type-label-font);
|
|
414
|
+
--feedview-tab-size: var(--type-label-size);
|
|
415
|
+
--feedview-tab-tracking: var(--type-label-tracking);
|
|
416
|
+
--feedview-tab-height: 32px;
|
|
417
|
+
--feedview-tab-padding-x: var(--space-md);
|
|
418
|
+
--feedview-tab-radius: var(--radius-sm);
|
|
419
|
+
--feedview-tab-color: var(--color-text-secondary);
|
|
420
|
+
--feedview-tab-color-active: var(--color-text);
|
|
421
|
+
--feedview-tab-bg-active: var(--color-surface);
|
|
422
|
+
--feedview-tab-shadow-active: var(--elevation-raised);
|
|
423
|
+
--feedview-tab-transition: var(--duration-instant) var(--easing-default);
|
|
424
|
+
|
|
406
425
|
/* ═══════════════════════════════════════════════
|
|
407
426
|
MODAL (centered overlay)
|
|
408
427
|
═══════════════════════════════════════════════ */
|
package/tokens/semantic.css
CHANGED
|
@@ -255,6 +255,15 @@
|
|
|
255
255
|
--color-text-secondary: var(--raw-color-neutral-400);
|
|
256
256
|
--color-text-muted: var(--raw-color-neutral-500);
|
|
257
257
|
|
|
258
|
+
/* Status colours lifted one ramp step (600 → 400) for dark surfaces. The
|
|
259
|
+
light -600 values are too dark on the dark (or dark-tinted -subtle) surface
|
|
260
|
+
a badge/alert sits on — e.g. info blue-600 #2e63a3 was 2.57:1, an AA fail
|
|
261
|
+
(axe #244 S5). The -400 ramp reads as the status colour AND clears 4.5:1. */
|
|
262
|
+
--color-destructive: var(--raw-color-red-400);
|
|
263
|
+
--color-success: var(--raw-color-green-400);
|
|
264
|
+
--color-warning: var(--raw-color-amber-400);
|
|
265
|
+
--color-info: var(--raw-color-blue-400);
|
|
266
|
+
|
|
258
267
|
/* Subtle washes — derived from the LIVE hue (theme-set or default),
|
|
259
268
|
mixed into the dark surface instead of the light-only raw tints. */
|
|
260
269
|
--color-accent-subtle: color-mix(
|
|
@@ -284,6 +293,72 @@
|
|
|
284
293
|
);
|
|
285
294
|
}
|
|
286
295
|
|
|
296
|
+
/* ═══════════════════════════════════════════════
|
|
297
|
+
ACCESSIBILITY PREFERENCE LAYERS — #244 C7
|
|
298
|
+
═══════════════════════════════════════════════
|
|
299
|
+
|
|
300
|
+
Citizen-selectable a11y affordances, each driven by a `data-*` attribute the
|
|
301
|
+
consumer stamps on <html> server-side (from a cookie, no flash) and toggles
|
|
302
|
+
live. Independent of scheme/theme — they LAYER on whatever brand + light/dark
|
|
303
|
+
is active. Placed after the dark block so a tie on specificity resolves here.
|
|
304
|
+
|
|
305
|
+
- text-size → `:root[data-text-size="N"]` scales the root font-size; the
|
|
306
|
+
whole rem-based type system grows with it (WCAG 1.4.4).
|
|
307
|
+
- contrast → `:root[data-contrast="high"]` re-maps the neutral roles to a
|
|
308
|
+
black-on-white (or white-on-black in dark) maximum, kills the
|
|
309
|
+
muted greys that fail AA, and strengthens borders (the
|
|
310
|
+
"alto contraste" pattern). A second selector handles the dark
|
|
311
|
+
pairing; (0,2,0) beats the generic dark layer.
|
|
312
|
+
- link-mark → `:root[data-link-highlight="on"] a` underlines every in-content
|
|
313
|
+
link so it is distinguishable by more than colour (1.4.1). */
|
|
314
|
+
|
|
315
|
+
:root[data-text-size="100"] {
|
|
316
|
+
font-size: 100%;
|
|
317
|
+
}
|
|
318
|
+
:root[data-text-size="120"] {
|
|
319
|
+
font-size: 120%;
|
|
320
|
+
}
|
|
321
|
+
:root[data-text-size="140"] {
|
|
322
|
+
font-size: 140%;
|
|
323
|
+
}
|
|
324
|
+
:root[data-text-size="160"] {
|
|
325
|
+
font-size: 160%;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
:root[data-contrast="high"] {
|
|
329
|
+
--color-text: #000000;
|
|
330
|
+
--color-text-secondary: #000000;
|
|
331
|
+
--color-text-muted: #1a1a1a;
|
|
332
|
+
--color-surface: #ffffff;
|
|
333
|
+
--color-surface-secondary: #ffffff;
|
|
334
|
+
--color-surface-tertiary: #ffffff;
|
|
335
|
+
--color-surface-raised: #ffffff;
|
|
336
|
+
--color-border: #000000;
|
|
337
|
+
--color-border-strong: #000000;
|
|
338
|
+
--focus-ring-color: #000000;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
:root[data-contrast="high"][data-scheme="dark"] {
|
|
342
|
+
--color-text: #ffffff;
|
|
343
|
+
--color-text-secondary: #ffffff;
|
|
344
|
+
--color-text-muted: #e6e6e6;
|
|
345
|
+
--color-surface: #000000;
|
|
346
|
+
--color-surface-secondary: #000000;
|
|
347
|
+
--color-surface-tertiary: #000000;
|
|
348
|
+
--color-surface-raised: #000000;
|
|
349
|
+
--color-border: #ffffff;
|
|
350
|
+
--color-border-strong: #ffffff;
|
|
351
|
+
--focus-ring-color: #ffffff;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/* High contrast implies link distinction — underline links even if the citizen
|
|
355
|
+
hasn't separately enabled the link-highlight pref. */
|
|
356
|
+
:root[data-contrast="high"] a,
|
|
357
|
+
:root[data-link-highlight="on"] a {
|
|
358
|
+
text-decoration: underline;
|
|
359
|
+
text-underline-offset: 0.15em;
|
|
360
|
+
}
|
|
361
|
+
|
|
287
362
|
/* ─── Tablet (768px+) ─── */
|
|
288
363
|
@media (min-width: 768px) {
|
|
289
364
|
:root {
|
|
@@ -34,7 +34,9 @@
|
|
|
34
34
|
/* ─── Text ─── */
|
|
35
35
|
--color-text: #1c241b;
|
|
36
36
|
--color-text-secondary: #44513f;
|
|
37
|
-
|
|
37
|
+
/* Muted hits 4.5:1 even on the secondary/tertiary surfaces it sits on
|
|
38
|
+
(#f4f6f3 → 5.2:1) — the old #71806b was 3.86:1, an AA fail (axe #244 S5). */
|
|
39
|
+
--color-text-muted: #5e6b57;
|
|
38
40
|
|
|
39
41
|
/* ─── Overlay + focus follow the brand ─── */
|
|
40
42
|
--color-overlay: rgba(28, 36, 27, 0.5);
|
|
@@ -42,3 +44,28 @@
|
|
|
42
44
|
|
|
43
45
|
/* Total overrides: 14 tokens. Still an aiaiai product, in Valongo's colours. */
|
|
44
46
|
}
|
|
47
|
+
|
|
48
|
+
/*
|
|
49
|
+
* Dark-scheme brand override (#244 C7 + S5 contrast remediation).
|
|
50
|
+
*
|
|
51
|
+
* The generic dark layer (`:root[data-scheme="dark"]` in semantic.css) re-derives
|
|
52
|
+
* surfaces/text/borders from the neutral ramp but leaves the BRAND accent
|
|
53
|
+
* untouched, and the light civic green (#2e7d32) is too dark on dark surfaces.
|
|
54
|
+
*
|
|
55
|
+
* The accent is DUAL-USE — link text (on a dark surface) AND fill (a button,
|
|
56
|
+
* with `--color-text-on-accent` text). Those pull opposite ways: link text wants
|
|
57
|
+
* a LIGHT accent (≥4.5:1 vs the surface), white-on-fill wants a DARK one. So lift
|
|
58
|
+
* the accent for link contrast (#57bb5d clears 4.5:1 even on the lightest dark
|
|
59
|
+
* surface, neutral-800) AND flip `--color-text-on-accent` to dark, so fills read
|
|
60
|
+
* dark-on-light-green (the standard dark-mode resolution). Validated by axe.
|
|
61
|
+
*
|
|
62
|
+
* Selector (0,2,0) ties the theme block + beats the generic dark layer (both
|
|
63
|
+
* (0,1,1)). `auto` is resolved before paint, so this only matches a real
|
|
64
|
+
* `data-scheme="dark"`. The portal injects the equivalent at runtime; this seeds.
|
|
65
|
+
*/
|
|
66
|
+
[data-theme="valongo"][data-scheme="dark"] {
|
|
67
|
+
--color-accent: #57bb5d; /* link text ≥4.5:1 on every dark surface (4.78 on n-800) */
|
|
68
|
+
--color-accent-hover: #81c784;
|
|
69
|
+
--color-accent-subtle: #15311a;
|
|
70
|
+
--color-text-on-accent: #14210f; /* dark text on the lighter accent fill */
|
|
71
|
+
}
|