@tonycodes-ui/mortar 0.0.0-stage → 0.12.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/AGENTS.md +10 -0
- package/CHANGELOG.md +268 -0
- package/CONTRIBUTING.md +60 -0
- package/DESIGN.md +51 -0
- package/LICENSE +21 -0
- package/README.md +155 -2
- package/TESTING.md +83 -0
- package/baseline-policy.json +16 -0
- package/bin/mortar.mjs +258 -0
- package/catalog.json +9900 -0
- package/cli/audit.mjs +943 -0
- package/cli/data.mjs +176 -0
- package/cli/mcp.mjs +216 -0
- package/cli/migrations.mjs +128 -0
- package/cli/packet.mjs +267 -0
- package/cli/search.mjs +246 -0
- package/cli/tools.mjs +229 -0
- package/design-principles.json +30 -0
- package/dist/agent-data.json +1 -0
- package/dist/application.d.ts +340 -0
- package/dist/application.js +240 -0
- package/dist/auth.d.ts +121 -0
- package/dist/auth.js +97 -0
- package/dist/brand.d.ts +69 -0
- package/dist/brand.js +266 -0
- package/dist/calendar.d.ts +75 -0
- package/dist/calendar.js +310 -0
- package/dist/choice.d.ts +36 -0
- package/dist/choice.js +36 -0
- package/dist/composer.d.ts +28 -0
- package/dist/composer.js +47 -0
- package/dist/display.d.ts +136 -0
- package/dist/display.js +75 -0
- package/dist/docs.d.ts +145 -0
- package/dist/docs.js +141 -0
- package/dist/ecommerce.d.ts +261 -0
- package/dist/ecommerce.js +177 -0
- package/dist/feedback.d.ts +80 -0
- package/dist/feedback.js +98 -0
- package/dist/foundation.d.ts +42 -0
- package/dist/foundation.js +121 -0
- package/dist/heading.d.ts +5 -0
- package/dist/heading.js +9 -0
- package/dist/icons.d.ts +50 -0
- package/dist/icons.js +70 -0
- package/dist/illustration.d.ts +47 -0
- package/dist/illustration.js +47 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +18 -0
- package/dist/inspection.d.ts +24 -0
- package/dist/inspection.js +14 -0
- package/dist/internal.d.ts +514 -0
- package/dist/internal.js +332 -0
- package/dist/layout.d.ts +107 -0
- package/dist/layout.js +66 -0
- package/dist/marketing-sections.d.ts +161 -0
- package/dist/marketing-sections.js +58 -0
- package/dist/marketing.d.ts +277 -0
- package/dist/marketing.js +163 -0
- package/dist/navigation.d.ts +35 -0
- package/dist/navigation.js +44 -0
- package/dist/primitives.d.ts +258 -0
- package/dist/primitives.js +244 -0
- package/dist/showcase.d.ts +29 -0
- package/dist/showcase.js +10 -0
- package/dist/workspace.d.ts +80 -0
- package/dist/workspace.js +48 -0
- package/docs/agents.md +127 -0
- package/docs/auth-flows.md +71 -0
- package/docs/migrating-to-0.6.md +94 -0
- package/docs/theming.md +170 -0
- package/package.json +94 -4
- package/skills/mortar/SKILL.md +36 -0
- package/src/application.css +1487 -0
- package/src/application.tsx +1413 -0
- package/src/auth.tsx +452 -0
- package/src/brand.ts +335 -0
- package/src/calendar.css +468 -0
- package/src/calendar.tsx +868 -0
- package/src/choice.tsx +240 -0
- package/src/composer.tsx +158 -0
- package/src/data.css +690 -0
- package/src/display.tsx +452 -0
- package/src/docs.css +704 -0
- package/src/docs.tsx +737 -0
- package/src/ecommerce.css +810 -0
- package/src/ecommerce.tsx +1033 -0
- package/src/feedback.tsx +388 -0
- package/src/foundation.css +1867 -0
- package/src/foundation.tsx +227 -0
- package/src/heading.ts +13 -0
- package/src/icons.tsx +237 -0
- package/src/illustration.tsx +169 -0
- package/src/index.ts +233 -0
- package/src/inspection.tsx +91 -0
- package/src/internal.tsx +700 -0
- package/src/layout.tsx +342 -0
- package/src/marketing-sections.tsx +621 -0
- package/src/marketing.css +2579 -0
- package/src/marketing.tsx +1235 -0
- package/src/navigation.tsx +118 -0
- package/src/overlays.css +350 -0
- package/src/primitives.tsx +1135 -0
- package/src/showcase.tsx +120 -0
- package/src/tokens.css +449 -0
- package/src/workspace.tsx +333 -0
- package/styles.css +56 -0
package/src/brand.ts
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/* Programmatic brands. Mirrors the brand contract in tokens.css, so an app can
|
|
2
|
+
define a brand from a config object, a CMS value or a tenant record without
|
|
3
|
+
shipping brand CSS. Every value is optional; omitted values derive from
|
|
4
|
+
primary, or keep Mortar's defaults, exactly as the CSS layer does. */
|
|
5
|
+
|
|
6
|
+
export interface BrandAppearanceTokens {
|
|
7
|
+
primary?: string;
|
|
8
|
+
primaryHover?: string;
|
|
9
|
+
primaryActive?: string;
|
|
10
|
+
onPrimary?: string;
|
|
11
|
+
accent?: string;
|
|
12
|
+
onAccent?: string;
|
|
13
|
+
focus?: string;
|
|
14
|
+
link?: string;
|
|
15
|
+
selected?: string;
|
|
16
|
+
/** The page background. */
|
|
17
|
+
paper?: string;
|
|
18
|
+
/** Cards, panels and raised controls. */
|
|
19
|
+
surface?: string;
|
|
20
|
+
/** Recessed areas: wells, muted bands and hover fills. */
|
|
21
|
+
sunken?: string;
|
|
22
|
+
/**
|
|
23
|
+
* An optional accent fill: `linear-gradient()`, `radial-gradient()` or
|
|
24
|
+
* `conic-gradient()` (or a `repeating-` form) whose stops are colours. Only
|
|
25
|
+
* components that opt in paint it (Button `variant="accent"`, and
|
|
26
|
+
* `tone="accent"` on CallToAction, Hero and Banner), with `onAccent` text,
|
|
27
|
+
* so `onAccent` must reach 4.5:1 against every stop. The flat `accent`
|
|
28
|
+
* stays the fallback and the input for derived colours.
|
|
29
|
+
*/
|
|
30
|
+
accentGradient?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Font stacks, the same in light and dark. */
|
|
34
|
+
export interface BrandFonts {
|
|
35
|
+
/** Body text: a font-family list, e.g. `"Inter", sans-serif`. */
|
|
36
|
+
body?: string;
|
|
37
|
+
/** Headings and component titles. Defaults to the body font. */
|
|
38
|
+
heading?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** A brand's light and dark values. Inverse bands use the other appearance's. */
|
|
42
|
+
export interface BrandDefinition {
|
|
43
|
+
light?: BrandAppearanceTokens;
|
|
44
|
+
dark?: BrandAppearanceTokens;
|
|
45
|
+
fonts?: BrandFonts;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const KEYS: Record<keyof BrandAppearanceTokens, string> = {
|
|
49
|
+
primary: "primary",
|
|
50
|
+
primaryHover: "primary-hover",
|
|
51
|
+
primaryActive: "primary-active",
|
|
52
|
+
onPrimary: "on-primary",
|
|
53
|
+
accent: "accent",
|
|
54
|
+
onAccent: "on-accent",
|
|
55
|
+
focus: "focus",
|
|
56
|
+
link: "link",
|
|
57
|
+
selected: "selected",
|
|
58
|
+
paper: "paper",
|
|
59
|
+
surface: "surface",
|
|
60
|
+
sunken: "sunken",
|
|
61
|
+
accentGradient: "accent-gradient",
|
|
62
|
+
};
|
|
63
|
+
const FONT_KEYS: Record<keyof BrandFonts, string> = {
|
|
64
|
+
body: "font",
|
|
65
|
+
heading: "font-heading",
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const NAME = /^[a-z0-9][a-z0-9-]*$/i;
|
|
69
|
+
|
|
70
|
+
/* Value validation. A value from tenant data must be colour (or gradient, or
|
|
71
|
+
font-family) syntax and nothing else, so it can neither end the declaration
|
|
72
|
+
nor inject other CSS, and a value of the wrong kind throws here instead of
|
|
73
|
+
failing silently in the browser. Values are tokenised, and every function is
|
|
74
|
+
checked against an allowlist, recursively. */
|
|
75
|
+
const NAMED = new Set(
|
|
76
|
+
(
|
|
77
|
+
"transparent currentcolor aliceblue antiquewhite aqua aquamarine azure beige bisque black " +
|
|
78
|
+
"blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral " +
|
|
79
|
+
"cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen " +
|
|
80
|
+
"darkgrey darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon " +
|
|
81
|
+
"darkseagreen darkslateblue darkslategray darkslategrey darkturquoise darkviolet deeppink " +
|
|
82
|
+
"deepskyblue dimgray dimgrey dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro " +
|
|
83
|
+
"ghostwhite gold goldenrod gray green greenyellow grey honeydew hotpink indianred indigo " +
|
|
84
|
+
"ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan " +
|
|
85
|
+
"lightgoldenrodyellow lightgray lightgreen lightgrey lightpink lightsalmon lightseagreen " +
|
|
86
|
+
"lightskyblue lightslategray lightslategrey lightsteelblue lightyellow lime limegreen linen " +
|
|
87
|
+
"magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen " +
|
|
88
|
+
"mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream " +
|
|
89
|
+
"mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid " +
|
|
90
|
+
"palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum " +
|
|
91
|
+
"powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown " +
|
|
92
|
+
"seagreen seashell sienna silver skyblue slateblue slategray slategrey snow springgreen " +
|
|
93
|
+
"steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen"
|
|
94
|
+
).split(" "),
|
|
95
|
+
);
|
|
96
|
+
const COLOUR_FUNCTIONS = new Set([
|
|
97
|
+
"rgb",
|
|
98
|
+
"rgba",
|
|
99
|
+
"hsl",
|
|
100
|
+
"hsla",
|
|
101
|
+
"hwb",
|
|
102
|
+
"lab",
|
|
103
|
+
"lch",
|
|
104
|
+
"oklab",
|
|
105
|
+
"oklch",
|
|
106
|
+
"color",
|
|
107
|
+
"color-mix",
|
|
108
|
+
"light-dark",
|
|
109
|
+
"var",
|
|
110
|
+
]);
|
|
111
|
+
/** Allowed only inside a colour or gradient, e.g. for relative colour channels. */
|
|
112
|
+
const MATH_FUNCTIONS = new Set(["calc", "min", "max", "clamp"]);
|
|
113
|
+
const GRADIENTS = new Set(
|
|
114
|
+
["linear-gradient", "radial-gradient", "conic-gradient"].flatMap((name) => [
|
|
115
|
+
name,
|
|
116
|
+
`repeating-${name}`,
|
|
117
|
+
]),
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
type Fn = { kind: "function"; name: string; args: Node[] };
|
|
121
|
+
type Node =
|
|
122
|
+
| Fn
|
|
123
|
+
| {
|
|
124
|
+
kind: "hash" | "ident" | "number" | "comma" | "slash" | "op";
|
|
125
|
+
value: string;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// One token per match: whitespace, #hash, function name with its "(", ident
|
|
129
|
+
// (including --custom-property), number with optional unit, ")", ",", "/" or
|
|
130
|
+
// an arithmetic operator. Anything else (quotes, ; { } < > comments, bare
|
|
131
|
+
// parentheses, backslashes) fails to tokenise.
|
|
132
|
+
const TOKEN =
|
|
133
|
+
/(\s+)|(#[0-9a-z]+)|(-{0,2}[a-z_][a-z0-9_-]*)\(|(-{0,2}[a-z_][a-z0-9_-]*)|([+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?(?:%|[a-z]+)?)|(\))|(,)|(\/)|([*+-])/giy;
|
|
134
|
+
|
|
135
|
+
/** The value as a tree of functions and tokens, or undefined if it holds anything else. */
|
|
136
|
+
function parse(value: string): Node[] | undefined {
|
|
137
|
+
const root: Node[] = [];
|
|
138
|
+
const open: Fn[] = [];
|
|
139
|
+
TOKEN.lastIndex = 0;
|
|
140
|
+
while (TOKEN.lastIndex < value.length) {
|
|
141
|
+
const at = TOKEN.lastIndex;
|
|
142
|
+
const m = TOKEN.exec(value);
|
|
143
|
+
if (!m || m.index !== at) return undefined;
|
|
144
|
+
const [, space, hash, fn, ident, number, close, comma, slash, op] = m;
|
|
145
|
+
const top = open.length ? open[open.length - 1].args : root;
|
|
146
|
+
if (space) continue;
|
|
147
|
+
if (fn) {
|
|
148
|
+
const node: Fn = { kind: "function", name: fn.toLowerCase(), args: [] };
|
|
149
|
+
top.push(node);
|
|
150
|
+
open.push(node);
|
|
151
|
+
} else if (close) {
|
|
152
|
+
if (!open.pop()) return undefined;
|
|
153
|
+
} else if (hash) top.push({ kind: "hash", value: hash });
|
|
154
|
+
else if (ident) top.push({ kind: "ident", value: ident.toLowerCase() });
|
|
155
|
+
else if (number) top.push({ kind: "number", value: number });
|
|
156
|
+
else if (comma) top.push({ kind: "comma", value: comma });
|
|
157
|
+
else if (slash) top.push({ kind: "slash", value: slash });
|
|
158
|
+
else if (op) top.push({ kind: "op", value: op });
|
|
159
|
+
}
|
|
160
|
+
return open.length ? undefined : root;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Arguments of an allowed function: plain tokens and allowed functions only. */
|
|
164
|
+
function validArgs(fn: Fn): boolean {
|
|
165
|
+
if (fn.name === "var") {
|
|
166
|
+
const [name, next] = fn.args;
|
|
167
|
+
if (name?.kind !== "ident" || !name.value.startsWith("--")) return false;
|
|
168
|
+
if (next && next.kind !== "comma") return false;
|
|
169
|
+
}
|
|
170
|
+
return fn.args.every(
|
|
171
|
+
(node) =>
|
|
172
|
+
node.kind !== "function" ||
|
|
173
|
+
((COLOUR_FUNCTIONS.has(node.name) || MATH_FUNCTIONS.has(node.name)) &&
|
|
174
|
+
validArgs(node)),
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function isColourNode(node: Node): boolean {
|
|
179
|
+
if (node.kind === "hash")
|
|
180
|
+
return /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(node.value);
|
|
181
|
+
if (node.kind === "ident") return NAMED.has(node.value);
|
|
182
|
+
return (
|
|
183
|
+
node.kind === "function" &&
|
|
184
|
+
COLOUR_FUNCTIONS.has(node.name) &&
|
|
185
|
+
validArgs(node)
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* True for colour syntax only: hex, a colour name (including `transparent`
|
|
191
|
+
* and `currentcolor`), or rgb(), rgba(), hsl(), hsla(), hwb(), lab(), lch(),
|
|
192
|
+
* oklab(), oklch(), color(), color-mix(), light-dark() or var(), whose
|
|
193
|
+
* arguments may nest those functions and calc(), min(), max() or clamp().
|
|
194
|
+
*/
|
|
195
|
+
export function isBrandColour(value: string): boolean {
|
|
196
|
+
const nodes = parse(value.trim());
|
|
197
|
+
return nodes?.length === 1 && isColourNode(nodes[0]);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* True for a CSS gradient whose stops are colour syntax: linear-, radial- or
|
|
202
|
+
* conic-gradient() (or a repeating- form) with at least two colour stops.
|
|
203
|
+
* Angles, positions, percentages and keywords such as `to right` are allowed;
|
|
204
|
+
* other functions (url(), image-set(), attr() …) are not.
|
|
205
|
+
*/
|
|
206
|
+
export function isBrandGradient(value: string): boolean {
|
|
207
|
+
const nodes = parse(value.trim());
|
|
208
|
+
if (nodes?.length !== 1) return false;
|
|
209
|
+
const [gradient] = nodes;
|
|
210
|
+
if (gradient.kind !== "function" || !GRADIENTS.has(gradient.name))
|
|
211
|
+
return false;
|
|
212
|
+
if (!validArgs(gradient)) return false;
|
|
213
|
+
const segments: Node[][] = [[]];
|
|
214
|
+
for (const node of gradient.args)
|
|
215
|
+
if (node.kind === "comma") segments.push([]);
|
|
216
|
+
else segments[segments.length - 1].push(node);
|
|
217
|
+
return segments.filter((s) => s.some(isColourNode)).length >= 2;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const FAMILY =
|
|
221
|
+
/\s*("[^"]*"|'[^']*'|-?[a-z_][a-z0-9_-]*(?:\s+-?[a-z_][a-z0-9_-]*)*)\s*(,|$)/iy;
|
|
222
|
+
/**
|
|
223
|
+
* True for a font-family list: family names, quoted names, generic families
|
|
224
|
+
* and bare `var(--name)` references (as framework font loaders hand out),
|
|
225
|
+
* separated by commas. No other functions, url() or CSS punctuation.
|
|
226
|
+
*/
|
|
227
|
+
export function isBrandFont(value: string): boolean {
|
|
228
|
+
// A bare var(--name) stands in for one whole entry, like a quoted name, so
|
|
229
|
+
// two in a row without a comma fail. Anything else with parentheses (a
|
|
230
|
+
// fallback, url(), local()) is still rejected.
|
|
231
|
+
const text = value.trim().replace(/var\(\s*--[a-z0-9_-]+\s*\)/gi, '""');
|
|
232
|
+
if (!text || /[;{}<>\\()]|\/\*/.test(text)) return false;
|
|
233
|
+
FAMILY.lastIndex = 0;
|
|
234
|
+
let last = "";
|
|
235
|
+
while (FAMILY.lastIndex < text.length) {
|
|
236
|
+
const at = FAMILY.lastIndex;
|
|
237
|
+
const m = FAMILY.exec(text);
|
|
238
|
+
if (!m || m.index !== at) return false;
|
|
239
|
+
last = m[2];
|
|
240
|
+
}
|
|
241
|
+
return last !== ",";
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function assertName(name: string) {
|
|
245
|
+
if (!NAME.test(name))
|
|
246
|
+
throw new Error(
|
|
247
|
+
`Mortar brand names use letters, digits and hyphens: "${name}"`,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const COLOUR_SYNTAX =
|
|
252
|
+
"a hex value, a colour name, or rgb(), hsl(), hwb(), lab(), lch(), oklab(), oklch(), color(), color-mix(), light-dark() or var()";
|
|
253
|
+
|
|
254
|
+
/** The brand's custom properties, validated, as `--m-brand-*` entries. */
|
|
255
|
+
export function brandToStyle(
|
|
256
|
+
brand: BrandDefinition,
|
|
257
|
+
): Record<`--m-brand-${string}`, string> {
|
|
258
|
+
const out: Record<`--m-brand-${string}`, string> = {};
|
|
259
|
+
for (const [prefix, mode, tokens] of [
|
|
260
|
+
["l", "light", brand.light],
|
|
261
|
+
["d", "dark", brand.dark],
|
|
262
|
+
] as const) {
|
|
263
|
+
if (!tokens) continue;
|
|
264
|
+
for (const key of Object.keys(KEYS) as (keyof BrandAppearanceTokens)[]) {
|
|
265
|
+
const value = tokens[key]?.trim();
|
|
266
|
+
if (!value) continue;
|
|
267
|
+
if (key === "accentGradient") {
|
|
268
|
+
if (!isBrandGradient(value))
|
|
269
|
+
throw new Error(
|
|
270
|
+
`Invalid Mortar brand gradient for ${mode}.${key}: "${value}". Use linear-gradient(), radial-gradient() or conic-gradient(), or a repeating- form, with at least two colour stops, each ${COLOUR_SYNTAX}.`,
|
|
271
|
+
);
|
|
272
|
+
} else if (!isBrandColour(value))
|
|
273
|
+
throw new Error(
|
|
274
|
+
`Invalid Mortar brand colour for ${mode}.${key}: "${value}". Use ${COLOUR_SYNTAX}.`,
|
|
275
|
+
);
|
|
276
|
+
out[`--m-brand-${prefix}-${KEYS[key]}`] = value;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
for (const key of Object.keys(FONT_KEYS) as (keyof BrandFonts)[]) {
|
|
280
|
+
const value = brand.fonts?.[key]?.trim();
|
|
281
|
+
if (!value) continue;
|
|
282
|
+
if (!isBrandFont(value))
|
|
283
|
+
throw new Error(
|
|
284
|
+
`Invalid Mortar brand font for fonts.${key}: "${value}". Use family names, quoted names and generic families separated by commas, e.g. "Inter", sans-serif.`,
|
|
285
|
+
);
|
|
286
|
+
out[`--m-brand-${FONT_KEYS[key]}`] = value;
|
|
287
|
+
}
|
|
288
|
+
return out;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** The brand as a CSS rule, for a stylesheet or a `<style>` element. */
|
|
292
|
+
export function brandToCss(name: string, brand: BrandDefinition): string {
|
|
293
|
+
assertName(name);
|
|
294
|
+
const body = Object.entries(brandToStyle(brand))
|
|
295
|
+
.map(([property, value]) => ` ${property}: ${value};`)
|
|
296
|
+
.join("\n");
|
|
297
|
+
return `[data-mortar-brand="${name}"] {\n${body}\n}`;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const registered = new Map<
|
|
301
|
+
string,
|
|
302
|
+
{ element: HTMLStyleElement; count: number }
|
|
303
|
+
>();
|
|
304
|
+
/**
|
|
305
|
+
* Adds the brand's stylesheet to the document, for apps that don't use
|
|
306
|
+
* ThemeProvider's `brandTokens`. Each call returns a disposer; the stylesheet
|
|
307
|
+
* stays until every caller for that name has disposed.
|
|
308
|
+
*/
|
|
309
|
+
export function registerBrand(
|
|
310
|
+
name: string,
|
|
311
|
+
brand: BrandDefinition,
|
|
312
|
+
): () => void {
|
|
313
|
+
const css = brandToCss(name, brand);
|
|
314
|
+
if (typeof document === "undefined") return () => {};
|
|
315
|
+
let entry = registered.get(name);
|
|
316
|
+
if (!entry) {
|
|
317
|
+
const element = document.createElement("style");
|
|
318
|
+
element.dataset.mortarBrandStyle = name;
|
|
319
|
+
document.head.append(element);
|
|
320
|
+
entry = { element, count: 0 };
|
|
321
|
+
registered.set(name, entry);
|
|
322
|
+
}
|
|
323
|
+
entry.element.textContent = css;
|
|
324
|
+
entry.count += 1;
|
|
325
|
+
let disposed = false;
|
|
326
|
+
return () => {
|
|
327
|
+
if (disposed || !entry) return;
|
|
328
|
+
disposed = true;
|
|
329
|
+
entry.count -= 1;
|
|
330
|
+
if (entry.count === 0) {
|
|
331
|
+
entry.element.remove();
|
|
332
|
+
registered.delete(name);
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
}
|