@askrjs/themes 0.0.14 → 0.0.16
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/dist/components/_internal/pathname.js +13 -1
- package/dist/components/_internal/pathname.js.map +1 -1
- package/dist/components/_internal/style.js +30 -9
- package/dist/components/_internal/style.js.map +1 -1
- package/dist/components/nav/nav.js +2 -1
- package/dist/components/nav/nav.js.map +1 -1
- package/package.json +3 -3
- package/src/components/_internal/pathname.ts +11 -0
- package/src/components/_internal/style.ts +31 -12
- package/src/components/nav/nav.tsx +2 -1
|
@@ -5,6 +5,18 @@ function normalizePathname(pathname) {
|
|
|
5
5
|
function getWindowLocation() {
|
|
6
6
|
return typeof window === "undefined" ? null : window.location;
|
|
7
7
|
}
|
|
8
|
+
function assertSafeNavigationHref(href) {
|
|
9
|
+
const trimmed = href.trim();
|
|
10
|
+
if (!trimmed || trimmed !== href) throw new TypeError("Navigation href must be a non-empty canonical URL.");
|
|
11
|
+
const scheme = /^([a-z][a-z\d+.-]*):/iu.exec(trimmed)?.[1]?.toLowerCase();
|
|
12
|
+
if (scheme && ![
|
|
13
|
+
"http",
|
|
14
|
+
"https",
|
|
15
|
+
"mailto",
|
|
16
|
+
"tel"
|
|
17
|
+
].includes(scheme)) throw new TypeError(`Navigation URL scheme is not allowed: ${scheme}`);
|
|
18
|
+
if ([...trimmed].some((character) => character.charCodeAt(0) <= 31)) throw new TypeError("Navigation href must not contain control characters.");
|
|
19
|
+
}
|
|
8
20
|
function resolvePathname(href) {
|
|
9
21
|
if (href.startsWith("#")) return null;
|
|
10
22
|
const location = getWindowLocation();
|
|
@@ -34,4 +46,4 @@ function resolvePathname(href) {
|
|
|
34
46
|
}
|
|
35
47
|
}
|
|
36
48
|
//#endregion
|
|
37
|
-
export { resolvePathname };
|
|
49
|
+
export { assertSafeNavigationHref, resolvePathname };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pathname.js","names":[],"sources":["../../../src/components/_internal/pathname.ts"],"sourcesContent":["function normalizePathname(pathname: string): string {\n return pathname.endsWith(\"/\") && pathname !== \"/\" ? pathname.slice(0, -1) : pathname;\n}\n\nfunction getWindowLocation(): Location | null {\n return typeof window === \"undefined\" ? null : window.location;\n}\n\nexport function resolvePathname(href: string): string | null {\n if (href.startsWith(\"#\")) {\n return null;\n }\n\n const location = getWindowLocation();\n\n if (href.startsWith(\"/\") && !href.startsWith(\"//\")) {\n const queryIndex = href.indexOf(\"?\");\n const hashIndex = href.indexOf(\"#\");\n let pathEnd = href.length;\n\n if (queryIndex !== -1) {\n pathEnd = Math.min(pathEnd, queryIndex);\n }\n\n if (hashIndex !== -1) {\n pathEnd = Math.min(pathEnd, hashIndex);\n }\n\n const pathname = normalizePathname(href.slice(0, pathEnd));\n\n if (location && hashIndex !== -1) {\n const searchEnd = hashIndex === -1 ? href.length : hashIndex;\n const search = queryIndex === -1 ? \"\" : href.slice(queryIndex, searchEnd);\n const locationPathname = normalizePathname(location.pathname || \"/\");\n\n if (pathname === locationPathname && search === location.search) {\n return null;\n }\n }\n\n return pathname;\n }\n\n const baseHref = location?.href ?? \"http://localhost/\";\n const baseOrigin = location?.origin ?? \"http://localhost\";\n\n try {\n const target = new URL(href, baseHref);\n\n if (target.origin !== baseOrigin) {\n return null;\n }\n\n if (\n location &&\n target.hash &&\n normalizePathname(target.pathname) === normalizePathname(location.pathname || \"/\") &&\n target.search === location.search\n ) {\n return null;\n }\n\n return normalizePathname(target.pathname);\n } catch {\n return null;\n }\n}\n"],"mappings":";AAAA,SAAS,kBAAkB,UAA0B;CACnD,OAAO,SAAS,SAAS,GAAG,KAAK,aAAa,MAAM,SAAS,MAAM,GAAG,EAAE,IAAI;AAC9E;AAEA,SAAS,oBAAqC;CAC5C,OAAO,OAAO,WAAW,cAAc,OAAO,OAAO;AACvD;AAEA,SAAgB,gBAAgB,MAA6B;CAC3D,IAAI,KAAK,WAAW,GAAG,GACrB,OAAO;CAGT,MAAM,WAAW,kBAAkB;CAEnC,IAAI,KAAK,WAAW,GAAG,KAAK,CAAC,KAAK,WAAW,IAAI,GAAG;EAClD,MAAM,aAAa,KAAK,QAAQ,GAAG;EACnC,MAAM,YAAY,KAAK,QAAQ,GAAG;EAClC,IAAI,UAAU,KAAK;EAEnB,IAAI,eAAe,IACjB,UAAU,KAAK,IAAI,SAAS,UAAU;EAGxC,IAAI,cAAc,IAChB,UAAU,KAAK,IAAI,SAAS,SAAS;EAGvC,MAAM,WAAW,kBAAkB,KAAK,MAAM,GAAG,OAAO,CAAC;EAEzD,IAAI,YAAY,cAAc,IAAI;GAChC,MAAM,YAAY,cAAc,KAAK,KAAK,SAAS;GACnD,MAAM,SAAS,eAAe,KAAK,KAAK,KAAK,MAAM,YAAY,SAAS;GAGxE,IAAI,aAFqB,kBAAkB,SAAS,YAAY,GAEhC,KAAK,WAAW,SAAS,QACvD,OAAO;EAEX;EAEA,OAAO;CACT;CAEA,MAAM,WAAW,UAAU,QAAQ;CACnC,MAAM,aAAa,UAAU,UAAU;CAEvC,IAAI;EACF,MAAM,SAAS,IAAI,IAAI,MAAM,QAAQ;EAErC,IAAI,OAAO,WAAW,YACpB,OAAO;EAGT,IACE,YACA,OAAO,QACP,kBAAkB,OAAO,QAAQ,MAAM,kBAAkB,SAAS,YAAY,GAAG,KACjF,OAAO,WAAW,SAAS,QAE3B,OAAO;EAGT,OAAO,kBAAkB,OAAO,QAAQ;CAC1C,QAAQ;EACN,OAAO;CACT;AACF"}
|
|
1
|
+
{"version":3,"file":"pathname.js","names":[],"sources":["../../../src/components/_internal/pathname.ts"],"sourcesContent":["function normalizePathname(pathname: string): string {\n return pathname.endsWith(\"/\") && pathname !== \"/\" ? pathname.slice(0, -1) : pathname;\n}\n\nfunction getWindowLocation(): Location | null {\n return typeof window === \"undefined\" ? null : window.location;\n}\n\nexport function assertSafeNavigationHref(href: string): void {\n const trimmed = href.trim();\n if (!trimmed || trimmed !== href)\n throw new TypeError(\"Navigation href must be a non-empty canonical URL.\");\n const scheme = /^([a-z][a-z\\d+.-]*):/iu.exec(trimmed)?.[1]?.toLowerCase();\n if (scheme && ![\"http\", \"https\", \"mailto\", \"tel\"].includes(scheme))\n throw new TypeError(`Navigation URL scheme is not allowed: ${scheme}`);\n if ([...trimmed].some((character) => character.charCodeAt(0) <= 31))\n throw new TypeError(\"Navigation href must not contain control characters.\");\n}\n\nexport function resolvePathname(href: string): string | null {\n if (href.startsWith(\"#\")) {\n return null;\n }\n\n const location = getWindowLocation();\n\n if (href.startsWith(\"/\") && !href.startsWith(\"//\")) {\n const queryIndex = href.indexOf(\"?\");\n const hashIndex = href.indexOf(\"#\");\n let pathEnd = href.length;\n\n if (queryIndex !== -1) {\n pathEnd = Math.min(pathEnd, queryIndex);\n }\n\n if (hashIndex !== -1) {\n pathEnd = Math.min(pathEnd, hashIndex);\n }\n\n const pathname = normalizePathname(href.slice(0, pathEnd));\n\n if (location && hashIndex !== -1) {\n const searchEnd = hashIndex === -1 ? href.length : hashIndex;\n const search = queryIndex === -1 ? \"\" : href.slice(queryIndex, searchEnd);\n const locationPathname = normalizePathname(location.pathname || \"/\");\n\n if (pathname === locationPathname && search === location.search) {\n return null;\n }\n }\n\n return pathname;\n }\n\n const baseHref = location?.href ?? \"http://localhost/\";\n const baseOrigin = location?.origin ?? \"http://localhost\";\n\n try {\n const target = new URL(href, baseHref);\n\n if (target.origin !== baseOrigin) {\n return null;\n }\n\n if (\n location &&\n target.hash &&\n normalizePathname(target.pathname) === normalizePathname(location.pathname || \"/\") &&\n target.search === location.search\n ) {\n return null;\n }\n\n return normalizePathname(target.pathname);\n } catch {\n return null;\n }\n}\n"],"mappings":";AAAA,SAAS,kBAAkB,UAA0B;CACnD,OAAO,SAAS,SAAS,GAAG,KAAK,aAAa,MAAM,SAAS,MAAM,GAAG,EAAE,IAAI;AAC9E;AAEA,SAAS,oBAAqC;CAC5C,OAAO,OAAO,WAAW,cAAc,OAAO,OAAO;AACvD;AAEA,SAAgB,yBAAyB,MAAoB;CAC3D,MAAM,UAAU,KAAK,KAAK;CAC1B,IAAI,CAAC,WAAW,YAAY,MAC1B,MAAM,IAAI,UAAU,oDAAoD;CAC1E,MAAM,SAAS,yBAAyB,KAAK,OAAO,CAAC,GAAG,EAAE,EAAE,YAAY;CACxE,IAAI,UAAU,CAAC;EAAC;EAAQ;EAAS;EAAU;CAAK,CAAC,CAAC,SAAS,MAAM,GAC/D,MAAM,IAAI,UAAU,yCAAyC,QAAQ;CACvE,IAAI,CAAC,GAAG,OAAO,CAAC,CAAC,MAAM,cAAc,UAAU,WAAW,CAAC,KAAK,EAAE,GAChE,MAAM,IAAI,UAAU,sDAAsD;AAC9E;AAEA,SAAgB,gBAAgB,MAA6B;CAC3D,IAAI,KAAK,WAAW,GAAG,GACrB,OAAO;CAGT,MAAM,WAAW,kBAAkB;CAEnC,IAAI,KAAK,WAAW,GAAG,KAAK,CAAC,KAAK,WAAW,IAAI,GAAG;EAClD,MAAM,aAAa,KAAK,QAAQ,GAAG;EACnC,MAAM,YAAY,KAAK,QAAQ,GAAG;EAClC,IAAI,UAAU,KAAK;EAEnB,IAAI,eAAe,IACjB,UAAU,KAAK,IAAI,SAAS,UAAU;EAGxC,IAAI,cAAc,IAChB,UAAU,KAAK,IAAI,SAAS,SAAS;EAGvC,MAAM,WAAW,kBAAkB,KAAK,MAAM,GAAG,OAAO,CAAC;EAEzD,IAAI,YAAY,cAAc,IAAI;GAChC,MAAM,YAAY,cAAc,KAAK,KAAK,SAAS;GACnD,MAAM,SAAS,eAAe,KAAK,KAAK,KAAK,MAAM,YAAY,SAAS;GAGxE,IAAI,aAFqB,kBAAkB,SAAS,YAAY,GAEhC,KAAK,WAAW,SAAS,QACvD,OAAO;EAEX;EAEA,OAAO;CACT;CAEA,MAAM,WAAW,UAAU,QAAQ;CACnC,MAAM,aAAa,UAAU,UAAU;CAEvC,IAAI;EACF,MAAM,SAAS,IAAI,IAAI,MAAM,QAAQ;EAErC,IAAI,OAAO,WAAW,YACpB,OAAO;EAGT,IACE,YACA,OAAO,QACP,kBAAkB,OAAO,QAAQ,MAAM,kBAAkB,SAAS,YAAY,GAAG,KACjF,OAAO,WAAW,SAAS,QAE3B,OAAO;EAGT,OAAO,kBAAkB,OAAO,QAAQ;CAC1C,QAAQ;EACN,OAAO;CACT;AACF"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { cspNonce } from "@askrjs/askr";
|
|
1
2
|
//#region src/components/_internal/style.ts
|
|
2
3
|
const cssPropertyNameCache = /* @__PURE__ */ new Map();
|
|
3
4
|
function cssPropertyName(name) {
|
|
@@ -26,16 +27,29 @@ function serializeCssDeclarations(styles) {
|
|
|
26
27
|
const STYLE_REGISTRY_ATTR = "data-askr-style-registry";
|
|
27
28
|
const STYLE_CLASS_PREFIX = "ak-style-";
|
|
28
29
|
const styleClassCache = /* @__PURE__ */ new Map();
|
|
29
|
-
const
|
|
30
|
+
const registries = /* @__PURE__ */ new WeakMap();
|
|
31
|
+
const MAX_STYLE_RULES = 512;
|
|
30
32
|
let nextStyleClassId = 0;
|
|
31
|
-
function
|
|
33
|
+
function ensureStyleRegistry(nonce) {
|
|
32
34
|
if (typeof document === "undefined") return null;
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
+
const key = nonce ?? "";
|
|
36
|
+
let documentRegistries = registries.get(document);
|
|
37
|
+
if (!documentRegistries) {
|
|
38
|
+
documentRegistries = /* @__PURE__ */ new Map();
|
|
39
|
+
registries.set(document, documentRegistries);
|
|
40
|
+
}
|
|
41
|
+
const current = documentRegistries.get(key);
|
|
42
|
+
if (current?.element.isConnected) return current;
|
|
35
43
|
const styleElement = document.createElement("style");
|
|
36
44
|
styleElement.setAttribute(STYLE_REGISTRY_ATTR, "true");
|
|
45
|
+
if (nonce !== void 0) styleElement.nonce = nonce;
|
|
37
46
|
(document.head ?? document.documentElement).append(styleElement);
|
|
38
|
-
|
|
47
|
+
const registry = {
|
|
48
|
+
element: styleElement,
|
|
49
|
+
rules: /* @__PURE__ */ new Map()
|
|
50
|
+
};
|
|
51
|
+
documentRegistries.set(key, registry);
|
|
52
|
+
return registry;
|
|
39
53
|
}
|
|
40
54
|
function normalizeDeclarations(declarations) {
|
|
41
55
|
return declarations.trim().replace(/;+\s*$/, "");
|
|
@@ -44,17 +58,24 @@ function styleDeclarationsToClass(declarations) {
|
|
|
44
58
|
if (typeof declarations !== "string") return void 0;
|
|
45
59
|
const normalized = normalizeDeclarations(declarations);
|
|
46
60
|
if (!normalized) return void 0;
|
|
61
|
+
const registry = ensureStyleRegistry(cspNonce());
|
|
62
|
+
const registered = registry?.rules.get(normalized);
|
|
63
|
+
if (registered) return registered.className;
|
|
47
64
|
let className = styleClassCache.get(normalized);
|
|
48
65
|
if (className === void 0) {
|
|
49
66
|
className = `${STYLE_CLASS_PREFIX}${++nextStyleClassId}`;
|
|
50
67
|
styleClassCache.set(normalized, className);
|
|
51
68
|
}
|
|
52
|
-
if (
|
|
53
|
-
const styleElement =
|
|
69
|
+
if (registry) {
|
|
70
|
+
const styleElement = registry.element;
|
|
54
71
|
if (styleElement) {
|
|
72
|
+
if (registry.rules.size >= MAX_STYLE_RULES) throw new RangeError("Theme style registry capacity exceeded.");
|
|
55
73
|
const rule = `.${className}{${normalized}}`;
|
|
56
|
-
|
|
57
|
-
|
|
74
|
+
registry.rules.set(normalized, {
|
|
75
|
+
className,
|
|
76
|
+
rule
|
|
77
|
+
});
|
|
78
|
+
styleElement.append(rule, "\n");
|
|
58
79
|
}
|
|
59
80
|
}
|
|
60
81
|
return className;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"style.js","names":[],"sources":["../../../src/components/_internal/style.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"style.js","names":[],"sources":["../../../src/components/_internal/style.ts"],"sourcesContent":["import { cspNonce } from \"@askrjs/askr\";\n\nconst cssPropertyNameCache = new Map<string, string>();\n\nfunction cssPropertyName(name: string): string {\n let cached = cssPropertyNameCache.get(name);\n if (cached !== undefined) {\n return cached;\n }\n\n let result = \"\";\n\n for (let index = 0; index < name.length; index += 1) {\n const code = name.charCodeAt(index);\n\n if (code >= 65 && code <= 90) {\n result += `-${String.fromCharCode(code + 32)}`;\n } else {\n result += name[index];\n }\n }\n\n cssPropertyNameCache.set(name, result);\n return result;\n}\n\nexport function serializeCssDeclarations(styles: Record<string, unknown>): string {\n const keys = Object.keys(styles);\n let result = \"\";\n\n for (const key of keys) {\n const value = styles[key];\n if (value === undefined || value === null) {\n continue;\n }\n\n const declaration = `${cssPropertyName(key)}:${String(value)}`;\n result = result ? `${result};${declaration}` : declaration;\n }\n\n return result;\n}\n\nexport function mergeCssVar(style: unknown, name: string, value: string): string {\n const decl = `${name}:${value}`;\n\n if (typeof style === \"string\") {\n const trimmed = style.trim();\n return trimmed ? `${trimmed};${decl}` : decl;\n }\n\n if (style && typeof style === \"object\") {\n const entries = serializeCssDeclarations(style as Record<string, unknown>);\n return entries ? `${entries};${decl}` : decl;\n }\n\n return decl;\n}\n\nconst STYLE_REGISTRY_ATTR = \"data-askr-style-registry\";\nconst STYLE_CLASS_PREFIX = \"ak-style-\";\n\nconst styleClassCache = new Map<string, string>();\ntype StyleRegistry = {\n element: HTMLStyleElement;\n rules: Map<string, { className: string; rule: string }>;\n};\nconst registries = new WeakMap<Document, Map<string, StyleRegistry>>();\nconst MAX_STYLE_RULES = 512;\n\nlet nextStyleClassId = 0;\n\nfunction ensureStyleRegistry(nonce: string | undefined): StyleRegistry | null {\n if (typeof document === \"undefined\") return null;\n const key = nonce ?? \"\";\n let documentRegistries = registries.get(document);\n if (!documentRegistries) {\n documentRegistries = new Map();\n registries.set(document, documentRegistries);\n }\n const current = documentRegistries.get(key);\n if (current?.element.isConnected) return current;\n\n const styleElement = document.createElement(\"style\");\n styleElement.setAttribute(STYLE_REGISTRY_ATTR, \"true\");\n if (nonce !== undefined) styleElement.nonce = nonce;\n (document.head ?? document.documentElement).append(styleElement);\n const registry: StyleRegistry = { element: styleElement, rules: new Map() };\n documentRegistries.set(key, registry);\n return registry;\n}\n\nfunction normalizeDeclarations(declarations: string): string {\n return declarations.trim().replace(/;+\\s*$/, \"\");\n}\n\nexport function styleDeclarationsToClass(declarations: string | undefined): string | undefined {\n if (typeof declarations !== \"string\") return undefined;\n\n const normalized = normalizeDeclarations(declarations);\n if (!normalized) return undefined;\n\n const nonce = cspNonce();\n const registry = ensureStyleRegistry(nonce);\n const registered = registry?.rules.get(normalized);\n if (registered) return registered.className;\n\n let className = styleClassCache.get(normalized);\n if (className === undefined) {\n className = `${STYLE_CLASS_PREFIX}${++nextStyleClassId}`;\n styleClassCache.set(normalized, className);\n }\n\n if (registry) {\n const styleElement = registry.element;\n if (styleElement) {\n if (registry.rules.size >= MAX_STYLE_RULES)\n throw new RangeError(\"Theme style registry capacity exceeded.\");\n const rule = `.${className}{${normalized}}`;\n registry.rules.set(normalized, { className, rule });\n styleElement.append(rule, \"\\n\");\n }\n }\n\n return className;\n}\n"],"mappings":";;AAEA,MAAM,uCAAuB,IAAI,IAAoB;AAErD,SAAS,gBAAgB,MAAsB;CAC7C,IAAI,SAAS,qBAAqB,IAAI,IAAI;CAC1C,IAAI,WAAW,KAAA,GACb,OAAO;CAGT,IAAI,SAAS;CAEb,KAAK,IAAI,QAAQ,GAAG,QAAQ,KAAK,QAAQ,SAAS,GAAG;EACnD,MAAM,OAAO,KAAK,WAAW,KAAK;EAElC,IAAI,QAAQ,MAAM,QAAQ,IACxB,UAAU,IAAI,OAAO,aAAa,OAAO,EAAE;OAE3C,UAAU,KAAK;CAEnB;CAEA,qBAAqB,IAAI,MAAM,MAAM;CACrC,OAAO;AACT;AAEA,SAAgB,yBAAyB,QAAyC;CAChF,MAAM,OAAO,OAAO,KAAK,MAAM;CAC/B,IAAI,SAAS;CAEb,KAAK,MAAM,OAAO,MAAM;EACtB,MAAM,QAAQ,OAAO;EACrB,IAAI,UAAU,KAAA,KAAa,UAAU,MACnC;EAGF,MAAM,cAAc,GAAG,gBAAgB,GAAG,EAAE,GAAG,OAAO,KAAK;EAC3D,SAAS,SAAS,GAAG,OAAO,GAAG,gBAAgB;CACjD;CAEA,OAAO;AACT;AAkBA,MAAM,sBAAsB;AAC5B,MAAM,qBAAqB;AAE3B,MAAM,kCAAkB,IAAI,IAAoB;AAKhD,MAAM,6BAAa,IAAI,QAA8C;AACrE,MAAM,kBAAkB;AAExB,IAAI,mBAAmB;AAEvB,SAAS,oBAAoB,OAAiD;CAC5E,IAAI,OAAO,aAAa,aAAa,OAAO;CAC5C,MAAM,MAAM,SAAS;CACrB,IAAI,qBAAqB,WAAW,IAAI,QAAQ;CAChD,IAAI,CAAC,oBAAoB;EACvB,qCAAqB,IAAI,IAAI;EAC7B,WAAW,IAAI,UAAU,kBAAkB;CAC7C;CACA,MAAM,UAAU,mBAAmB,IAAI,GAAG;CAC1C,IAAI,SAAS,QAAQ,aAAa,OAAO;CAEzC,MAAM,eAAe,SAAS,cAAc,OAAO;CACnD,aAAa,aAAa,qBAAqB,MAAM;CACrD,IAAI,UAAU,KAAA,GAAW,aAAa,QAAQ;CAC9C,CAAC,SAAS,QAAQ,SAAS,gBAAA,CAAiB,OAAO,YAAY;CAC/D,MAAM,WAA0B;EAAE,SAAS;EAAc,uBAAO,IAAI,IAAI;CAAE;CAC1E,mBAAmB,IAAI,KAAK,QAAQ;CACpC,OAAO;AACT;AAEA,SAAS,sBAAsB,cAA8B;CAC3D,OAAO,aAAa,KAAK,CAAC,CAAC,QAAQ,UAAU,EAAE;AACjD;AAEA,SAAgB,yBAAyB,cAAsD;CAC7F,IAAI,OAAO,iBAAiB,UAAU,OAAO,KAAA;CAE7C,MAAM,aAAa,sBAAsB,YAAY;CACrD,IAAI,CAAC,YAAY,OAAO,KAAA;CAGxB,MAAM,WAAW,oBADH,SAC2B,CAAC;CAC1C,MAAM,aAAa,UAAU,MAAM,IAAI,UAAU;CACjD,IAAI,YAAY,OAAO,WAAW;CAElC,IAAI,YAAY,gBAAgB,IAAI,UAAU;CAC9C,IAAI,cAAc,KAAA,GAAW;EAC3B,YAAY,GAAG,qBAAqB,EAAE;EACtC,gBAAgB,IAAI,YAAY,SAAS;CAC3C;CAEA,IAAI,UAAU;EACZ,MAAM,eAAe,SAAS;EAC9B,IAAI,cAAc;GAChB,IAAI,SAAS,MAAM,QAAQ,iBACzB,MAAM,IAAI,WAAW,yCAAyC;GAChE,MAAM,OAAO,IAAI,UAAU,GAAG,WAAW;GACzC,SAAS,MAAM,IAAI,YAAY;IAAE;IAAW;GAAK,CAAC;GAClD,aAAa,OAAO,MAAM,IAAI;EAChC;CACF;CAEA,OAAO;AACT"}
|
|
@@ -2,7 +2,7 @@ import { classes } from "../_internal/classes.js";
|
|
|
2
2
|
import { mergeProps } from "../_internal/merge-props.js";
|
|
3
3
|
import { intrinsicElement } from "../_internal/jsx.js";
|
|
4
4
|
import { Block } from "../block/block.js";
|
|
5
|
-
import { resolvePathname } from "../_internal/pathname.js";
|
|
5
|
+
import { assertSafeNavigationHref, resolvePathname } from "../_internal/pathname.js";
|
|
6
6
|
import { jsx } from "@askrjs/askr/jsx-runtime";
|
|
7
7
|
import { Slot } from "@askrjs/askr/foundations";
|
|
8
8
|
import { Link, currentRoute, navigate } from "@askrjs/askr/router";
|
|
@@ -50,6 +50,7 @@ function renderRoutedLink(props, slot, options = {}) {
|
|
|
50
50
|
const { active, children, href: suppliedHref, to, onClick, ref, class: className, match = "prefix", target, ...rest } = props;
|
|
51
51
|
const href = to?.href ?? suppliedHref;
|
|
52
52
|
if (!href) throw new Error("Nav link requires href or to.");
|
|
53
|
+
assertSafeNavigationHref(href);
|
|
53
54
|
const resolvedSlot = (options.inheritSlot && typeof rest["data-slot"] === "string" ? String(rest["data-slot"]) : void 0) ?? slot;
|
|
54
55
|
const inheritedNavClass = slot === "nav-item" && resolvedSlot !== "nav-item" ? "nav-item" : void 0;
|
|
55
56
|
const { "data-slot": _dataSlot, ...childRest } = rest;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nav.js","names":[],"sources":["../../../src/components/nav/nav.tsx"],"sourcesContent":["import { Slot } from \"@askrjs/askr/foundations\";\nimport { currentRoute, Link, navigate } from \"@askrjs/askr/router\";\nimport { Block } from \"../block\";\nimport { classes } from \"../_internal/classes\";\nimport { mergeProps } from \"../_internal/merge-props\";\nimport { intrinsicElement } from \"../_internal/jsx\";\nimport { resolvePathname } from \"../_internal/pathname\";\nimport type {\n NavItemAsChildProps,\n NavItemProps,\n NavLinkProps,\n PillProps,\n PillsAsChildProps,\n PillsProps,\n TabProps,\n TabsAsChildProps,\n TabsProps,\n} from \"./nav.types\";\n\nconst LayoutBlock = Block as (props: Record<string, unknown>) => JSX.Element;\n\nfunction normalizePathname(pathname: string): string {\n return pathname.endsWith(\"/\") && pathname !== \"/\" ? pathname.slice(0, -1) : pathname;\n}\n\nfunction getCurrentPathname(): string | null {\n if (typeof window === \"undefined\") {\n return null;\n }\n\n return normalizePathname(window.location.pathname || \"/\");\n}\n\nfunction getReactiveCurrentPathname(): string | null {\n try {\n return normalizePathname(currentRoute().path || \"/\");\n } catch {\n return getCurrentPathname();\n }\n}\n\nfunction isActiveNavLink(\n currentPathname: string,\n targetPathname: string,\n match: NavLinkProps[\"match\"] = \"prefix\",\n): boolean {\n if (targetPathname === \"/\") {\n return currentPathname === \"/\";\n }\n\n if (match === \"exact\") {\n return currentPathname === targetPathname;\n }\n\n return currentPathname === targetPathname || currentPathname.startsWith(`${targetPathname}/`);\n}\n\nfunction shouldHandleClientNavigation(\n event: MouseEvent,\n target: string | undefined,\n targetPathname: string | null,\n): boolean {\n if (targetPathname === null || target) {\n return false;\n }\n\n return (\n !event.defaultPrevented &&\n (event.button ?? 0) === 0 &&\n !event.altKey &&\n !event.ctrlKey &&\n !event.metaKey &&\n !event.shiftKey\n );\n}\n\nfunction renderNavSet(\n props: TabsProps | TabsAsChildProps | PillsProps | PillsAsChildProps,\n slot: \"tabs\" | \"pills\",\n): JSX.Element {\n const className = \"class\" in props ? props.class : undefined;\n const { asChild, children, ref, ...rest } = props;\n const finalProps = mergeProps(rest, {\n ref,\n class: classes(slot, className),\n \"data-slot\": slot,\n });\n\n if (asChild) {\n return <Slot asChild {...finalProps} children={children as JSX.Element} />;\n }\n\n return intrinsicElement(\"nav\", finalProps, children);\n}\n\nfunction renderRoutedLink(\n props: NavLinkProps | TabProps | PillProps,\n slot: \"nav-item\" | \"tab\" | \"pill\",\n options: { activeBackground?: boolean; className?: string; inheritSlot?: boolean } = {},\n): JSX.Element {\n const {\n active,\n children,\n href: suppliedHref,\n to,\n onClick,\n ref,\n class: className,\n match = \"prefix\",\n target,\n ...rest\n } = props as NavLinkProps & { onClick?: (event: MouseEvent) => void };\n const href = to?.href ?? suppliedHref;\n if (!href) {\n throw new Error(\"Nav link requires href or to.\");\n }\n const inheritedSlot =\n options.inheritSlot && typeof (rest as Record<string, unknown>)[\"data-slot\"] === \"string\"\n ? String((rest as Record<string, unknown>)[\"data-slot\"])\n : undefined;\n const resolvedSlot = (inheritedSlot ?? slot) as \"nav-item\" | \"tab\" | \"pill\";\n const inheritedNavClass =\n slot === \"nav-item\" && resolvedSlot !== \"nav-item\" ? \"nav-item\" : undefined;\n const { \"data-slot\": _dataSlot, ...childRest } = rest as Record<string, unknown>;\n void _dataSlot;\n const currentPathname = getReactiveCurrentPathname();\n const targetPathname = resolvePathname(href);\n const routeActive =\n currentPathname !== null &&\n targetPathname !== null &&\n isActiveNavLink(currentPathname, targetPathname, match);\n const isActive = active ?? routeActive;\n const activeProps = isActive\n ? {\n \"aria-current\": \"page\" as const,\n \"data-active\": \"true\" as const,\n }\n : {\n \"data-active\": undefined,\n };\n const rendersRouterLink = targetPathname !== null && !target && typeof onClick !== \"function\";\n const childProps: NavLinkProps = to\n ? ({ ...childRest, to, target } as NavLinkProps)\n : ({ ...childRest, href, target } as NavLinkProps);\n const handleClick = (event: MouseEvent) => {\n onClick?.(event);\n\n if (!shouldHandleClientNavigation(event, target, targetPathname)) {\n return;\n }\n\n event.preventDefault();\n navigate(href);\n };\n\n return (\n <LayoutBlock\n asChild\n paddingX=\"sm\"\n paddingY=\"xs\"\n radius={resolvedSlot === \"pill\" ? \"round\" : \"md\"}\n background={isActive && options.activeBackground ? \"selected\" : undefined}\n ref={ref}\n className={classes(options.className, inheritedNavClass, className)}\n data-slot={resolvedSlot}\n {...activeProps}\n >\n {rendersRouterLink ? (\n <Link {...childProps}>{children}</Link>\n ) : (\n <a {...childRest} href={href} target={target} onClick={handleClick}>\n {children}\n </a>\n )}\n </LayoutBlock>\n );\n}\n\nexport function Tabs(props: TabsProps): JSX.Element;\nexport function Tabs(props: TabsAsChildProps): JSX.Element;\nexport function Tabs(props: TabsProps | TabsAsChildProps): JSX.Element {\n return renderNavSet(props, \"tabs\");\n}\n\nexport function Pills(props: PillsProps): JSX.Element;\nexport function Pills(props: PillsAsChildProps): JSX.Element;\nexport function Pills(props: PillsProps | PillsAsChildProps): JSX.Element {\n return renderNavSet(props, \"pills\");\n}\n\nexport function Tab(props: TabProps): JSX.Element {\n return renderRoutedLink(props, \"tab\", { className: \"tab\" });\n}\n\nexport function Pill(props: PillProps): JSX.Element {\n return renderRoutedLink(props, \"pill\", { activeBackground: true, className: \"pill\" });\n}\n\nexport function NavItem(props: NavItemProps): JSX.Element;\nexport function NavItem(props: NavItemAsChildProps): JSX.Element;\nexport function NavItem(props: NavItemProps | NavItemAsChildProps): JSX.Element {\n const {\n asChild,\n active = false,\n children,\n ref,\n class: className,\n match: _match,\n ...rest\n } = props as (NavItemProps | NavItemAsChildProps) & { match?: unknown };\n void _match;\n\n if (asChild) {\n return (\n <LayoutBlock\n asChild\n paddingX=\"sm\"\n paddingY=\"xs\"\n radius=\"md\"\n background={active ? \"selected\" : undefined}\n {...rest}\n ref={ref}\n className={className}\n data-active={active ? \"true\" : undefined}\n data-slot=\"nav-item\"\n >\n {children}\n </LayoutBlock>\n );\n }\n\n return (\n <LayoutBlock\n as=\"a\"\n paddingX=\"sm\"\n paddingY=\"xs\"\n radius=\"md\"\n background={active ? \"selected\" : undefined}\n {...rest}\n ref={ref}\n className={className}\n data-active={active ? \"true\" : undefined}\n data-slot=\"nav-item\"\n >\n {children}\n </LayoutBlock>\n );\n}\n\nexport function NavLink(props: NavLinkProps): JSX.Element {\n return renderRoutedLink(props, \"nav-item\", { activeBackground: true, inheritSlot: true });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"nav.js","names":[],"sources":["../../../src/components/nav/nav.tsx"],"sourcesContent":["import { Slot } from \"@askrjs/askr/foundations\";\nimport { currentRoute, Link, navigate } from \"@askrjs/askr/router\";\nimport { Block } from \"../block\";\nimport { classes } from \"../_internal/classes\";\nimport { mergeProps } from \"../_internal/merge-props\";\nimport { intrinsicElement } from \"../_internal/jsx\";\nimport { assertSafeNavigationHref, resolvePathname } from \"../_internal/pathname\";\nimport type {\n NavItemAsChildProps,\n NavItemProps,\n NavLinkProps,\n PillProps,\n PillsAsChildProps,\n PillsProps,\n TabProps,\n TabsAsChildProps,\n TabsProps,\n} from \"./nav.types\";\n\nconst LayoutBlock = Block as (props: Record<string, unknown>) => JSX.Element;\n\nfunction normalizePathname(pathname: string): string {\n return pathname.endsWith(\"/\") && pathname !== \"/\" ? pathname.slice(0, -1) : pathname;\n}\n\nfunction getCurrentPathname(): string | null {\n if (typeof window === \"undefined\") {\n return null;\n }\n\n return normalizePathname(window.location.pathname || \"/\");\n}\n\nfunction getReactiveCurrentPathname(): string | null {\n try {\n return normalizePathname(currentRoute().path || \"/\");\n } catch {\n return getCurrentPathname();\n }\n}\n\nfunction isActiveNavLink(\n currentPathname: string,\n targetPathname: string,\n match: NavLinkProps[\"match\"] = \"prefix\",\n): boolean {\n if (targetPathname === \"/\") {\n return currentPathname === \"/\";\n }\n\n if (match === \"exact\") {\n return currentPathname === targetPathname;\n }\n\n return currentPathname === targetPathname || currentPathname.startsWith(`${targetPathname}/`);\n}\n\nfunction shouldHandleClientNavigation(\n event: MouseEvent,\n target: string | undefined,\n targetPathname: string | null,\n): boolean {\n if (targetPathname === null || target) {\n return false;\n }\n\n return (\n !event.defaultPrevented &&\n (event.button ?? 0) === 0 &&\n !event.altKey &&\n !event.ctrlKey &&\n !event.metaKey &&\n !event.shiftKey\n );\n}\n\nfunction renderNavSet(\n props: TabsProps | TabsAsChildProps | PillsProps | PillsAsChildProps,\n slot: \"tabs\" | \"pills\",\n): JSX.Element {\n const className = \"class\" in props ? props.class : undefined;\n const { asChild, children, ref, ...rest } = props;\n const finalProps = mergeProps(rest, {\n ref,\n class: classes(slot, className),\n \"data-slot\": slot,\n });\n\n if (asChild) {\n return <Slot asChild {...finalProps} children={children as JSX.Element} />;\n }\n\n return intrinsicElement(\"nav\", finalProps, children);\n}\n\nfunction renderRoutedLink(\n props: NavLinkProps | TabProps | PillProps,\n slot: \"nav-item\" | \"tab\" | \"pill\",\n options: { activeBackground?: boolean; className?: string; inheritSlot?: boolean } = {},\n): JSX.Element {\n const {\n active,\n children,\n href: suppliedHref,\n to,\n onClick,\n ref,\n class: className,\n match = \"prefix\",\n target,\n ...rest\n } = props as NavLinkProps & { onClick?: (event: MouseEvent) => void };\n const href = to?.href ?? suppliedHref;\n if (!href) {\n throw new Error(\"Nav link requires href or to.\");\n }\n assertSafeNavigationHref(href);\n const inheritedSlot =\n options.inheritSlot && typeof (rest as Record<string, unknown>)[\"data-slot\"] === \"string\"\n ? String((rest as Record<string, unknown>)[\"data-slot\"])\n : undefined;\n const resolvedSlot = (inheritedSlot ?? slot) as \"nav-item\" | \"tab\" | \"pill\";\n const inheritedNavClass =\n slot === \"nav-item\" && resolvedSlot !== \"nav-item\" ? \"nav-item\" : undefined;\n const { \"data-slot\": _dataSlot, ...childRest } = rest as Record<string, unknown>;\n void _dataSlot;\n const currentPathname = getReactiveCurrentPathname();\n const targetPathname = resolvePathname(href);\n const routeActive =\n currentPathname !== null &&\n targetPathname !== null &&\n isActiveNavLink(currentPathname, targetPathname, match);\n const isActive = active ?? routeActive;\n const activeProps = isActive\n ? {\n \"aria-current\": \"page\" as const,\n \"data-active\": \"true\" as const,\n }\n : {\n \"data-active\": undefined,\n };\n const rendersRouterLink = targetPathname !== null && !target && typeof onClick !== \"function\";\n const childProps: NavLinkProps = to\n ? ({ ...childRest, to, target } as NavLinkProps)\n : ({ ...childRest, href, target } as NavLinkProps);\n const handleClick = (event: MouseEvent) => {\n onClick?.(event);\n\n if (!shouldHandleClientNavigation(event, target, targetPathname)) {\n return;\n }\n\n event.preventDefault();\n navigate(href);\n };\n\n return (\n <LayoutBlock\n asChild\n paddingX=\"sm\"\n paddingY=\"xs\"\n radius={resolvedSlot === \"pill\" ? \"round\" : \"md\"}\n background={isActive && options.activeBackground ? \"selected\" : undefined}\n ref={ref}\n className={classes(options.className, inheritedNavClass, className)}\n data-slot={resolvedSlot}\n {...activeProps}\n >\n {rendersRouterLink ? (\n <Link {...childProps}>{children}</Link>\n ) : (\n <a {...childRest} href={href} target={target} onClick={handleClick}>\n {children}\n </a>\n )}\n </LayoutBlock>\n );\n}\n\nexport function Tabs(props: TabsProps): JSX.Element;\nexport function Tabs(props: TabsAsChildProps): JSX.Element;\nexport function Tabs(props: TabsProps | TabsAsChildProps): JSX.Element {\n return renderNavSet(props, \"tabs\");\n}\n\nexport function Pills(props: PillsProps): JSX.Element;\nexport function Pills(props: PillsAsChildProps): JSX.Element;\nexport function Pills(props: PillsProps | PillsAsChildProps): JSX.Element {\n return renderNavSet(props, \"pills\");\n}\n\nexport function Tab(props: TabProps): JSX.Element {\n return renderRoutedLink(props, \"tab\", { className: \"tab\" });\n}\n\nexport function Pill(props: PillProps): JSX.Element {\n return renderRoutedLink(props, \"pill\", { activeBackground: true, className: \"pill\" });\n}\n\nexport function NavItem(props: NavItemProps): JSX.Element;\nexport function NavItem(props: NavItemAsChildProps): JSX.Element;\nexport function NavItem(props: NavItemProps | NavItemAsChildProps): JSX.Element {\n const {\n asChild,\n active = false,\n children,\n ref,\n class: className,\n match: _match,\n ...rest\n } = props as (NavItemProps | NavItemAsChildProps) & { match?: unknown };\n void _match;\n\n if (asChild) {\n return (\n <LayoutBlock\n asChild\n paddingX=\"sm\"\n paddingY=\"xs\"\n radius=\"md\"\n background={active ? \"selected\" : undefined}\n {...rest}\n ref={ref}\n className={className}\n data-active={active ? \"true\" : undefined}\n data-slot=\"nav-item\"\n >\n {children}\n </LayoutBlock>\n );\n }\n\n return (\n <LayoutBlock\n as=\"a\"\n paddingX=\"sm\"\n paddingY=\"xs\"\n radius=\"md\"\n background={active ? \"selected\" : undefined}\n {...rest}\n ref={ref}\n className={className}\n data-active={active ? \"true\" : undefined}\n data-slot=\"nav-item\"\n >\n {children}\n </LayoutBlock>\n );\n}\n\nexport function NavLink(props: NavLinkProps): JSX.Element {\n return renderRoutedLink(props, \"nav-item\", { activeBackground: true, inheritSlot: true });\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askrjs/themes",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.16",
|
|
4
4
|
"description": "Default theme tokens, styles, and component presets for Askr apps.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"askr",
|
|
@@ -328,7 +328,7 @@
|
|
|
328
328
|
"test:checks": "vp test run -c vitest.test.checks.config.ts"
|
|
329
329
|
},
|
|
330
330
|
"devDependencies": {
|
|
331
|
-
"@askrjs/askr": ">=0.0.
|
|
331
|
+
"@askrjs/askr": ">=0.0.64 <0.1.0",
|
|
332
332
|
"@askrjs/ui": ">=0.0.12 <0.1.0",
|
|
333
333
|
"@askrjs/vite": ">=0.0.5 <0.1.0",
|
|
334
334
|
"@tsdown/css": "0.22.8",
|
|
@@ -342,7 +342,7 @@
|
|
|
342
342
|
"vite-plus": "^0.2.4"
|
|
343
343
|
},
|
|
344
344
|
"peerDependencies": {
|
|
345
|
-
"@askrjs/askr": ">=0.0.
|
|
345
|
+
"@askrjs/askr": ">=0.0.64 <0.1.0",
|
|
346
346
|
"@askrjs/ui": ">=0.0.12 <0.1.0"
|
|
347
347
|
},
|
|
348
348
|
"engines": {
|
|
@@ -6,6 +6,17 @@ function getWindowLocation(): Location | null {
|
|
|
6
6
|
return typeof window === "undefined" ? null : window.location;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
export function assertSafeNavigationHref(href: string): void {
|
|
10
|
+
const trimmed = href.trim();
|
|
11
|
+
if (!trimmed || trimmed !== href)
|
|
12
|
+
throw new TypeError("Navigation href must be a non-empty canonical URL.");
|
|
13
|
+
const scheme = /^([a-z][a-z\d+.-]*):/iu.exec(trimmed)?.[1]?.toLowerCase();
|
|
14
|
+
if (scheme && !["http", "https", "mailto", "tel"].includes(scheme))
|
|
15
|
+
throw new TypeError(`Navigation URL scheme is not allowed: ${scheme}`);
|
|
16
|
+
if ([...trimmed].some((character) => character.charCodeAt(0) <= 31))
|
|
17
|
+
throw new TypeError("Navigation href must not contain control characters.");
|
|
18
|
+
}
|
|
19
|
+
|
|
9
20
|
export function resolvePathname(href: string): string | null {
|
|
10
21
|
if (href.startsWith("#")) {
|
|
11
22
|
return null;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { cspNonce } from "@askrjs/askr";
|
|
2
|
+
|
|
1
3
|
const cssPropertyNameCache = new Map<string, string>();
|
|
2
4
|
|
|
3
5
|
function cssPropertyName(name: string): string {
|
|
@@ -59,23 +61,33 @@ const STYLE_REGISTRY_ATTR = "data-askr-style-registry";
|
|
|
59
61
|
const STYLE_CLASS_PREFIX = "ak-style-";
|
|
60
62
|
|
|
61
63
|
const styleClassCache = new Map<string, string>();
|
|
62
|
-
|
|
64
|
+
type StyleRegistry = {
|
|
65
|
+
element: HTMLStyleElement;
|
|
66
|
+
rules: Map<string, { className: string; rule: string }>;
|
|
67
|
+
};
|
|
68
|
+
const registries = new WeakMap<Document, Map<string, StyleRegistry>>();
|
|
69
|
+
const MAX_STYLE_RULES = 512;
|
|
63
70
|
|
|
64
71
|
let nextStyleClassId = 0;
|
|
65
72
|
|
|
66
|
-
function
|
|
73
|
+
function ensureStyleRegistry(nonce: string | undefined): StyleRegistry | null {
|
|
67
74
|
if (typeof document === "undefined") return null;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (
|
|
71
|
-
|
|
75
|
+
const key = nonce ?? "";
|
|
76
|
+
let documentRegistries = registries.get(document);
|
|
77
|
+
if (!documentRegistries) {
|
|
78
|
+
documentRegistries = new Map();
|
|
79
|
+
registries.set(document, documentRegistries);
|
|
72
80
|
}
|
|
81
|
+
const current = documentRegistries.get(key);
|
|
82
|
+
if (current?.element.isConnected) return current;
|
|
73
83
|
|
|
74
84
|
const styleElement = document.createElement("style");
|
|
75
85
|
styleElement.setAttribute(STYLE_REGISTRY_ATTR, "true");
|
|
86
|
+
if (nonce !== undefined) styleElement.nonce = nonce;
|
|
76
87
|
(document.head ?? document.documentElement).append(styleElement);
|
|
77
|
-
|
|
78
|
-
|
|
88
|
+
const registry: StyleRegistry = { element: styleElement, rules: new Map() };
|
|
89
|
+
documentRegistries.set(key, registry);
|
|
90
|
+
return registry;
|
|
79
91
|
}
|
|
80
92
|
|
|
81
93
|
function normalizeDeclarations(declarations: string): string {
|
|
@@ -88,18 +100,25 @@ export function styleDeclarationsToClass(declarations: string | undefined): stri
|
|
|
88
100
|
const normalized = normalizeDeclarations(declarations);
|
|
89
101
|
if (!normalized) return undefined;
|
|
90
102
|
|
|
103
|
+
const nonce = cspNonce();
|
|
104
|
+
const registry = ensureStyleRegistry(nonce);
|
|
105
|
+
const registered = registry?.rules.get(normalized);
|
|
106
|
+
if (registered) return registered.className;
|
|
107
|
+
|
|
91
108
|
let className = styleClassCache.get(normalized);
|
|
92
109
|
if (className === undefined) {
|
|
93
110
|
className = `${STYLE_CLASS_PREFIX}${++nextStyleClassId}`;
|
|
94
111
|
styleClassCache.set(normalized, className);
|
|
95
112
|
}
|
|
96
113
|
|
|
97
|
-
if (
|
|
98
|
-
const styleElement =
|
|
114
|
+
if (registry) {
|
|
115
|
+
const styleElement = registry.element;
|
|
99
116
|
if (styleElement) {
|
|
117
|
+
if (registry.rules.size >= MAX_STYLE_RULES)
|
|
118
|
+
throw new RangeError("Theme style registry capacity exceeded.");
|
|
100
119
|
const rule = `.${className}{${normalized}}`;
|
|
101
|
-
|
|
102
|
-
|
|
120
|
+
registry.rules.set(normalized, { className, rule });
|
|
121
|
+
styleElement.append(rule, "\n");
|
|
103
122
|
}
|
|
104
123
|
}
|
|
105
124
|
|
|
@@ -4,7 +4,7 @@ import { Block } from "../block";
|
|
|
4
4
|
import { classes } from "../_internal/classes";
|
|
5
5
|
import { mergeProps } from "../_internal/merge-props";
|
|
6
6
|
import { intrinsicElement } from "../_internal/jsx";
|
|
7
|
-
import { resolvePathname } from "../_internal/pathname";
|
|
7
|
+
import { assertSafeNavigationHref, resolvePathname } from "../_internal/pathname";
|
|
8
8
|
import type {
|
|
9
9
|
NavItemAsChildProps,
|
|
10
10
|
NavItemProps,
|
|
@@ -114,6 +114,7 @@ function renderRoutedLink(
|
|
|
114
114
|
if (!href) {
|
|
115
115
|
throw new Error("Nav link requires href or to.");
|
|
116
116
|
}
|
|
117
|
+
assertSafeNavigationHref(href);
|
|
117
118
|
const inheritedSlot =
|
|
118
119
|
options.inheritSlot && typeof (rest as Record<string, unknown>)["data-slot"] === "string"
|
|
119
120
|
? String((rest as Record<string, unknown>)["data-slot"])
|