staffa 0.3.0 → 0.4.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/README.md +35 -2
- package/dist/components/box.d.ts +2 -2
- package/dist/components/box.js +3 -4
- package/dist/components/button.d.ts +9 -11
- package/dist/components/button.js +16 -18
- package/dist/components/buttonChooser.d.ts +5 -5
- package/dist/components/buttonChooser.js +7 -5
- package/dist/components/buttonGroup.js +2 -2
- package/dist/components/dialog.d.ts +2 -2
- package/dist/components/dialog.js +10 -10
- package/dist/components/form.d.ts +4 -4
- package/dist/components/form.js +6 -6
- package/dist/components/main.d.ts +5 -5
- package/dist/components/main.js +28 -40
- package/dist/components/menu.d.ts +3 -3
- package/dist/components/menu.js +8 -6
- package/dist/components/tabs.d.ts +3 -3
- package/dist/components/tabs.js +1 -1
- package/dist/components/toast.d.ts +1 -3
- package/dist/components/toast.js +2 -2
- package/dist/components/tooltip.js +2 -2
- package/dist/core.d.ts +10 -4
- package/dist/core.js +13 -0
- package/dist/index.d.ts +19 -65
- package/dist/index.js +18 -48
- package/dist/staffa.esm.js +1 -1
- package/dist/theme.d.ts +0 -7
- package/dist/theme.js +37 -14
- package/package.json +9 -2
- package/src/components/box.ts +5 -5
- package/src/components/button.ts +21 -22
- package/src/components/buttonChooser.ts +10 -8
- package/src/components/buttonGroup.ts +2 -1
- package/src/components/dialog.ts +10 -10
- package/src/components/form.ts +8 -8
- package/src/components/main.ts +31 -40
- package/src/components/menu.ts +9 -7
- package/src/components/tabs.ts +4 -4
- package/src/components/toast.ts +3 -5
- package/src/components/tooltip.ts +2 -2
- package/src/core.ts +16 -5
- package/src/index.ts +21 -76
- package/src/theme.ts +36 -22
package/src/components/tabs.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import A from "aberdeen";
|
|
2
|
-
import { type Bindable, type
|
|
2
|
+
import { type Bindable, type Slot, type Attributes, drawSlot, uniqueId } from "../core.js";
|
|
3
3
|
|
|
4
4
|
/** A single tab definition. */
|
|
5
5
|
export interface Tab {
|
|
@@ -9,8 +9,8 @@ export interface Tab {
|
|
|
9
9
|
label: Slot;
|
|
10
10
|
/** Optional leading icon. */
|
|
11
11
|
icon?: Slot;
|
|
12
|
-
/** Content rendered in the panel when this tab is active. */
|
|
13
|
-
content?:
|
|
12
|
+
/** Content rendered in the panel when this tab is active. A string is rendered as rich text. */
|
|
13
|
+
content?: Slot;
|
|
14
14
|
/** Disables selecting this tab. */
|
|
15
15
|
disabled?: boolean;
|
|
16
16
|
}
|
|
@@ -101,7 +101,7 @@ export function tabs(opts: TabsOptions): void {
|
|
|
101
101
|
const tab = opts.tabs[index] ?? opts.tabs[0];
|
|
102
102
|
if (!tab) return;
|
|
103
103
|
A(`id=${groupId}-panel-${keyOf(tab, index)} aria-labelledby=${groupId}-tab-${keyOf(tab, index)}`);
|
|
104
|
-
tab.content
|
|
104
|
+
drawSlot(tab.content);
|
|
105
105
|
});
|
|
106
106
|
});
|
|
107
107
|
});
|
package/src/components/toast.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import A from "aberdeen";
|
|
2
2
|
import { grow, shrink } from "aberdeen/transitions";
|
|
3
|
-
import { type Slot, type Attributes, drawSlot } from "../core.js";
|
|
4
|
-
import type { SurfaceRole } from "../theme.js";
|
|
3
|
+
import { type Slot, type Attributes, drawSlot, mountPortal } from "../core.js";
|
|
5
4
|
|
|
6
5
|
/** Options for {@link toast}. */
|
|
7
6
|
export interface ToastOptions {
|
|
@@ -11,9 +10,8 @@ export interface ToastOptions {
|
|
|
11
10
|
title?: Slot;
|
|
12
11
|
/**
|
|
13
12
|
* Colour role. Defaults to `"neutral"`.
|
|
14
|
-
* Use `"success"` / `"danger"` / `"warning"` for semantic feedback.
|
|
15
13
|
*/
|
|
16
|
-
type?:
|
|
14
|
+
type?: "success" | "danger" | "warning" | "neutral"
|
|
17
15
|
/**
|
|
18
16
|
* Auto-dismiss delay in milliseconds. Defaults to `4000`.
|
|
19
17
|
* Pass `0` to make the toast persistent until dismissed manually.
|
|
@@ -55,7 +53,7 @@ let toastCount = 0;
|
|
|
55
53
|
// Keyed by stable ID so A.onEach scopes are per-toast — removing one never re-renders others.
|
|
56
54
|
const toasts = A.proxy({} as Record<number, ToastEntry>);
|
|
57
55
|
|
|
58
|
-
|
|
56
|
+
mountPortal(() => {
|
|
59
57
|
A("div.s-toasts aria-live=polite aria-atomic=false", () => {
|
|
60
58
|
A.onEach(toasts, (entry) => {
|
|
61
59
|
const { opts } = entry;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import A from "aberdeen";
|
|
2
|
-
import { type Slot, type Attributes, drawSlot } from "../core.js";
|
|
2
|
+
import { type Slot, type Attributes, drawSlot, mountPortal } from "../core.js";
|
|
3
3
|
|
|
4
4
|
/** Options for {@link addTooltip}. */
|
|
5
5
|
export interface TooltipOptions {
|
|
@@ -79,7 +79,7 @@ function scheduleHide(): void {
|
|
|
79
79
|
|
|
80
80
|
// ─── Portal ──────────────────────────────────────────────────────────────────
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
mountPortal(() => {
|
|
83
83
|
const active = $ttActive.value;
|
|
84
84
|
if (!active) return;
|
|
85
85
|
const { opts, anchor } = active;
|
package/src/core.ts
CHANGED
|
@@ -29,9 +29,6 @@ export type Attributes = string;
|
|
|
29
29
|
/** A reactive "value box", such as the result of `A.proxy(x)` or `A.ref(obj, key)`. */
|
|
30
30
|
export type Bindable<T> = { value: T };
|
|
31
31
|
|
|
32
|
-
/** A content function. It runs inside the relevant element's reactive scope. */
|
|
33
|
-
export type Content = () => void;
|
|
34
|
-
|
|
35
32
|
/**
|
|
36
33
|
* Something that renders a small piece of content: either a plain string or a
|
|
37
34
|
* draw function (for icons, badges, custom markup, ...).
|
|
@@ -52,8 +49,8 @@ export type Slot<Args extends unknown[] = []> = string | ((...args: Args) => voi
|
|
|
52
49
|
export interface ContentOptions {
|
|
53
50
|
/** Aberdeen attr/style string applied to the widget's outermost element. */
|
|
54
51
|
attrs?: Attributes;
|
|
55
|
-
/** Draws the children of this component. */
|
|
56
|
-
content?:
|
|
52
|
+
/** Draws the children of this component. A string is rendered as rich text. */
|
|
53
|
+
content?: Slot;
|
|
57
54
|
}
|
|
58
55
|
|
|
59
56
|
let idCounter = 0;
|
|
@@ -72,3 +69,17 @@ export function drawSlot<Args extends unknown[] = []>(slot: Slot<Args> | undefin
|
|
|
72
69
|
if (typeof slot === "function") slot(...args);
|
|
73
70
|
else A("rich=", slot);
|
|
74
71
|
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Mount a portal (tooltip, toast, menu, dialog, …) in its own dedicated
|
|
75
|
+
* container under `<body>`. Each portal gets a private parent element because
|
|
76
|
+
* two `A.mount`s sharing one parent can't tell their nodes apart: a redraw of
|
|
77
|
+
* one (e.g. a tooltip hiding) may sweep up nodes another portal inserted
|
|
78
|
+
* earlier in the same parent (e.g. an open menu).
|
|
79
|
+
*/
|
|
80
|
+
export function mountPortal(draw: () => void): void {
|
|
81
|
+
const container = document.createElement("div");
|
|
82
|
+
container.style.display = "contents";
|
|
83
|
+
document.body.appendChild(container);
|
|
84
|
+
A.mount(container, draw);
|
|
85
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Import the default `S` object and call its component functions:
|
|
6
6
|
*
|
|
7
7
|
* ```ts
|
|
8
|
-
* import S from "staffa";
|
|
8
|
+
* import * as S from "staffa";
|
|
9
9
|
*
|
|
10
10
|
* S.main({
|
|
11
11
|
* title: "Hello",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* S.textline({ label: "Email", type: "email", bind: A.ref($u, "email") });
|
|
18
18
|
* S.checkbox({ label: "Remember me", bind: A.ref($u, "remember") });
|
|
19
19
|
* },
|
|
20
|
-
* actions: () => S.button({
|
|
20
|
+
* actions: () => S.button({ content: "Sign in", type: "submit" }),
|
|
21
21
|
* });
|
|
22
22
|
* }});
|
|
23
23
|
* },
|
|
@@ -32,80 +32,25 @@
|
|
|
32
32
|
// Importing the theme module installs spacing vars, the reactive theme and the
|
|
33
33
|
// base stylesheet. Customise it from your app with A.insertGlobalCss (see
|
|
34
34
|
// theme.ts); toggle modes with setDarkMode / getDarkMode.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
/** The Staffa component namespace. */
|
|
55
|
-
export const S = {
|
|
56
|
-
main,
|
|
57
|
-
box,
|
|
58
|
-
dialog,
|
|
59
|
-
alert,
|
|
60
|
-
confirm,
|
|
61
|
-
prompt,
|
|
62
|
-
form,
|
|
63
|
-
menuButton,
|
|
64
|
-
showFloatingMenu,
|
|
65
|
-
textline,
|
|
66
|
-
textarea,
|
|
67
|
-
checkbox,
|
|
68
|
-
tabs,
|
|
69
|
-
button,
|
|
70
|
-
buttonChooser,
|
|
71
|
-
buttonGroup,
|
|
72
|
-
autocomplete,
|
|
73
|
-
select,
|
|
74
|
-
toast,
|
|
75
|
-
addTooltip,
|
|
76
|
-
setDarkMode,
|
|
77
|
-
getDarkMode,
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
export default S;
|
|
35
|
+
export { setDarkMode, getDarkMode } from "./theme.js";
|
|
36
|
+
export { autocomplete, type AutocompleteOptions, type AutocompleteOptionInput } from "./components/autocomplete.js";
|
|
37
|
+
export { box, type BoxOptions } from "./components/box.js";
|
|
38
|
+
export { button, type ButtonOptions } from "./components/button.js";
|
|
39
|
+
export { buttonChooser, type ButtonChooserOptions } from "./components/buttonChooser.js";
|
|
40
|
+
export { buttonGroup, type ButtonGroupOptions } from "./components/buttonGroup.js";
|
|
41
|
+
export { checkbox, type CheckboxOptions } from "./components/checkbox.js";
|
|
42
|
+
export { form, type FormOptions } from "./components/form.js";
|
|
43
|
+
export { main, type MainOptions } from "./components/main.js";
|
|
44
|
+
export { menuButton, showFloatingMenu, type MenuOptions, type MenuEntry, type MenuItem, type MenuSeparator, type FloatingMenuOptions } from "./components/menu.js";
|
|
45
|
+
export { dialog, alert, confirm, prompt, type DialogOptions } from "./components/dialog.js";
|
|
46
|
+
export { select, type SelectOptions, type SelectOptionInput } from "./components/select.js";
|
|
47
|
+
export { tabs, type Tab, type TabsOptions } from "./components/tabs.js";
|
|
48
|
+
export { textarea, type TextareaOptions } from "./components/textarea.js";
|
|
49
|
+
export { textline, type TextlineOptions, type TextlineType } from "./components/textline.js";
|
|
50
|
+
export { toast, type ToastOptions } from "./components/toast.js";
|
|
51
|
+
export { addTooltip, type TooltipOptions } from "./components/tooltip.js";
|
|
52
|
+
export type { FieldOptions } from "./components/field.js";
|
|
81
53
|
|
|
82
54
|
// Re-export theming and shared types for advanced use.
|
|
83
|
-
export {
|
|
84
|
-
export type { SurfaceRole, Variant } from "./theme.js";
|
|
85
|
-
export type {
|
|
86
|
-
ContentOptions,
|
|
87
|
-
Bindable,
|
|
88
|
-
Content,
|
|
89
|
-
Slot,
|
|
90
|
-
Attributes as Styling,
|
|
91
|
-
} from "./core.js";
|
|
92
|
-
export { drawSlot, uniqueId } from "./core.js";
|
|
55
|
+
export type {ContentOptions, Bindable, Slot, Attributes} from "./core.js";
|
|
93
56
|
|
|
94
|
-
export type { FieldOptions } from "./components/field.js";
|
|
95
|
-
export type { MainOptions } from "./components/main.js";
|
|
96
|
-
export type { MenuOptions, MenuEntry, MenuItem, MenuSeparator, FloatingMenuOptions } from "./components/menu.js";
|
|
97
|
-
export { drawMenu } from "./components/menu.js";
|
|
98
|
-
export type { ToastOptions } from "./components/toast.js";
|
|
99
|
-
export type { TooltipOptions } from "./components/tooltip.js";
|
|
100
|
-
export type { BoxOptions } from "./components/box.js";
|
|
101
|
-
export type { FormOptions } from "./components/form.js";
|
|
102
|
-
export type { TextlineOptions, TextlineType } from "./components/textline.js";
|
|
103
|
-
export type { TextareaOptions } from "./components/textarea.js";
|
|
104
|
-
export type { CheckboxOptions } from "./components/checkbox.js";
|
|
105
|
-
export type { Tab, TabsOptions } from "./components/tabs.js";
|
|
106
|
-
export type { ButtonOptions } from "./components/button.js";
|
|
107
|
-
export type { ButtonChooserOptions } from "./components/buttonChooser.js";
|
|
108
|
-
export type { ButtonGroupOptions } from "./components/buttonGroup.js";
|
|
109
|
-
export type { AutocompleteOptions, AutocompleteOptionInput } from "./components/autocomplete.js";
|
|
110
|
-
export type { SelectOptions, SelectOptionInput } from "./components/select.js";
|
|
111
|
-
export type { DialogOptions } from "./components/dialog.js";
|
package/src/theme.ts
CHANGED
|
@@ -1,14 +1,5 @@
|
|
|
1
1
|
import A from "aberdeen";
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* The named accent roles for interactive elements (buttons, tabs, badges, …).
|
|
5
|
-
* Each maps to a `.s-s.<role>` class that sets `--s-a` (ink) and `--s-b` (fill).
|
|
6
|
-
*/
|
|
7
|
-
export type SurfaceRole = "primary" | "secondary" | "gradient" | "neutral" | "danger" | "success" | "warning";
|
|
8
|
-
|
|
9
|
-
/** How a surface's two colours are rendered. `filled` is the default. */
|
|
10
|
-
export type Variant = "filled" | "tonal" | "outlined";
|
|
11
|
-
|
|
12
3
|
/**
|
|
13
4
|
* Theming and global base styles for Staffa.
|
|
14
5
|
*
|
|
@@ -48,7 +39,9 @@ export type Variant = "filled" | "tonal" | "outlined";
|
|
|
48
39
|
* | `--s-fg-faint` | placeholders, disabled |
|
|
49
40
|
* | `--s-border` / `--s-border-strong` | borders |
|
|
50
41
|
* | `--s-accent` | brand "pop" colour (active indicators, etc.) |
|
|
51
|
-
* | `--s-gradient` | primary→secondary brand sweep (mark,
|
|
42
|
+
* | `--s-gradient` | full primary→secondary brand sweep (mark, active nav) |
|
|
43
|
+
* | `--s-gradient-surface` | compressed sweep for filled gradient surfaces (buttons) |
|
|
44
|
+
* | `--s-tint` | brand mid colour; the hue the neutral greys lean toward |
|
|
52
45
|
* | `--s-glow` | soft coloured shadow for lit brand elements |
|
|
53
46
|
* | `--s-link` / `--s-focus` | link & focus-ring colours |
|
|
54
47
|
* | `--s-radius` / `--s-radius-lg` / `--s-shadow` | shape tokens |
|
|
@@ -145,13 +138,23 @@ export function getDarkMode(allowAuto = false): boolean | undefined {
|
|
|
145
138
|
// names (--s-primary/-danger/-success/-warning) double as semantic *ink*
|
|
146
139
|
// colours, legible as text/borders on neutral surfaces.
|
|
147
140
|
// ---------------------------------------------------------------------------
|
|
141
|
+
// The neutral fills/inks aren't hard-coded greys: each one mixes a small dose
|
|
142
|
+
// of `--s-tint` (the brand's mid colour, defined in the static block below)
|
|
143
|
+
// into a true-grey base. Re-skin the brand and every "grey" — page, panels,
|
|
144
|
+
// ink, the neutral fill — drifts subtly toward the new brand hue, light and
|
|
145
|
+
// dark alike. The percentages are deliberately small: a tint you'd only spot
|
|
146
|
+
// in a side-by-side, never a colour cast.
|
|
148
147
|
A(() => {
|
|
149
148
|
if (getDarkMode()) {
|
|
150
149
|
A.insertGlobalCss({
|
|
151
150
|
":root":
|
|
152
151
|
"--s-primary:#8b7bff --s-secondary:#ef7fd0 --s-danger:#ff6b6b --s-success:#46d39a --s-warning:#fbbf24 " +
|
|
153
|
-
"--s-neutral
|
|
154
|
-
"--s-
|
|
152
|
+
"--s-neutral: color-mix(in oklab, #3d4047, $s-tint 14%); " +
|
|
153
|
+
"--s-page: color-mix(in oklab, #0e0f12, $s-tint 5%); " +
|
|
154
|
+
"--s-panel: color-mix(in oklab, #17181c, $s-tint 6%); " +
|
|
155
|
+
"--s-raised: color-mix(in oklab, #212327, $s-tint 8%); " +
|
|
156
|
+
"--s-ink: color-mix(in oklab, #e9eaec, $s-tint 8%); " +
|
|
157
|
+
"--s-on-accent:#0c0a14 --s-focus: color-mix(in srgb, $s-primary 45%, transparent); " +
|
|
155
158
|
"--s-radius:12px --s-radius-lg:18px --s-shadow: 0 10px 34px rgba(0,0,0,0.5);",
|
|
156
159
|
// Contextual link, restored across the neutral group (see static block).
|
|
157
160
|
":root, .s-s.base, .s-s.panel, .s-s.raised, .s-s.neutral": "--s-link:#6db3ff",
|
|
@@ -160,8 +163,12 @@ A(() => {
|
|
|
160
163
|
A.insertGlobalCss({
|
|
161
164
|
":root":
|
|
162
165
|
"--s-primary:#6c5ce7 --s-secondary:#d6459e --s-danger:#e23b3b --s-success:#1f9d6b --s-warning:#d97706 " +
|
|
163
|
-
"--s-neutral
|
|
164
|
-
"--s-
|
|
166
|
+
"--s-neutral: color-mix(in oklab, #c9cbd0, $s-tint 14%); " +
|
|
167
|
+
"--s-page: color-mix(in oklab, #f3f4f6, $s-tint 5%); " +
|
|
168
|
+
"--s-panel: color-mix(in oklab, #ffffff, $s-tint 2%); " +
|
|
169
|
+
"--s-raised: color-mix(in oklab, #edeef0, $s-tint 7%); " +
|
|
170
|
+
"--s-ink: color-mix(in oklab, #1d1f24, $s-tint 7%); " +
|
|
171
|
+
"--s-on-accent:#0c0a14 --s-focus: color-mix(in srgb, $s-primary 35%, transparent); " +
|
|
165
172
|
"--s-radius:12px --s-radius-lg:18px --s-shadow: 0 10px 30px rgba(20,24,40,0.13);",
|
|
166
173
|
":root, .s-s.base, .s-s.panel, .s-s.raised, .s-s.neutral": "--s-link:#2563eb",
|
|
167
174
|
});
|
|
@@ -189,13 +196,19 @@ A.setSpacingCssVars();
|
|
|
189
196
|
A.insertGlobalCss({
|
|
190
197
|
// Derived brand tokens. These only reference the per-mode palette colours, so
|
|
191
198
|
// they're defined once here and track the active mode (and any re-skin):
|
|
192
|
-
// `--s-
|
|
193
|
-
//
|
|
194
|
-
//
|
|
199
|
+
// `--s-tint` is the brand's mid colour, the hue the neutral greys above lean
|
|
200
|
+
// toward; `--s-gradient` is the full primary→secondary sweep used where the
|
|
201
|
+
// brand should *show* (the headline mark, the active nav pill, tab edges);
|
|
202
|
+
// `--s-gradient-surface` is a compressed, near-vertical cut of that same sweep
|
|
203
|
+
// for *filled* gradient surfaces (buttons): it reads as one rich colour with
|
|
204
|
+
// depth rather than a two-colour banner; `--s-glow` is a soft coloured shadow
|
|
205
|
+
// that makes lit brand elements feel raised; `--s-page-bg` is a faint
|
|
195
206
|
// twin-corner aurora wash painted on the page surface.
|
|
196
207
|
":root":
|
|
208
|
+
"--s-tint: color-mix(in oklab, $s-primary, $s-secondary); " +
|
|
197
209
|
"--s-gradient: linear-gradient(135deg, $s-primary, $s-secondary); " +
|
|
198
|
-
"--s-
|
|
210
|
+
"--s-gradient-surface: linear-gradient(170deg, color-mix(in oklab, $s-primary 85%, $s-secondary), color-mix(in oklab, $s-primary 30%, $s-secondary)); " +
|
|
211
|
+
"--s-glow: 0 5px 16px color-mix(in srgb, $s-primary 26%, transparent); " +
|
|
199
212
|
"--s-page-bg: radial-gradient(120% 80% at 100% 0%, color-mix(in oklab, $s-secondary, transparent 86%), transparent 56%), radial-gradient(120% 80% at 0% 0%, color-mix(in oklab, $s-primary, transparent 87%), transparent 56%), $s-page;",
|
|
200
213
|
|
|
201
214
|
// Level/role modifier → anchors. Levels (neutral elevations) use the shared
|
|
@@ -250,10 +263,11 @@ A.insertGlobalCss({
|
|
|
250
263
|
// overrides a component's default `.tonal`/`.outlined` (resetting both the
|
|
251
264
|
// anchors and the painted background).
|
|
252
265
|
".s-s.filled": "--s-fg:$s-a --s-bg:$s-b background:$s-bg;",
|
|
253
|
-
// Paint the brand sweep for a filled `.gradient` surface
|
|
254
|
-
//
|
|
255
|
-
// variants keep their
|
|
256
|
-
|
|
266
|
+
// Paint the brand sweep for a filled `.gradient` surface — the compressed
|
|
267
|
+
// surface cut, not the full banner sweep. Sits after the variant rules and is
|
|
268
|
+
// keyed on `:not(.tonal):not(.outlined)`, so those variants keep their
|
|
269
|
+
// solid-primary tint/edge.
|
|
270
|
+
".s-s.gradient:not(.tonal):not(.outlined)": "background: $s-gradient-surface;",
|
|
257
271
|
|
|
258
272
|
// Contextual accent: the brand pop colour on neutral surfaces. Declared on the
|
|
259
273
|
// whole neutral group so re-entering a neutral surface under a coloured one
|