@shapio/schema 0.2.1 → 0.3.1
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/index.d.ts +132 -2
- package/dist/index.js +266 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -941,10 +941,37 @@ export declare const sha256: (text: string) => Promise<string>;
|
|
|
941
941
|
/** Hash of a normalized definition's canonical serialization; recorded per model in the lock file. */
|
|
942
942
|
export declare const hashDefinition: (definition: SchemaDefinition) => Promise<string>;
|
|
943
943
|
//#endregion
|
|
944
|
+
//#region src/fileFormat/layout.d.ts
|
|
945
|
+
/**
|
|
946
|
+
* Where schema files live in a repository (plan site-schema): shared definitions in `models/` and
|
|
947
|
+
* `components/`, a site's own in `sites/<siteKey>/models/` and `sites/<siteKey>/components/`. Paths are
|
|
948
|
+
* relative to the schema directory and always use `/` (git paths); callers join them for their file system.
|
|
949
|
+
*/
|
|
950
|
+
export declare const SCHEMA_KIND_DIRECTORIES: readonly ["models", "components"];
|
|
951
|
+
export type SchemaKindDirectory = (typeof SCHEMA_KIND_DIRECTORIES)[number];
|
|
952
|
+
/** The folder that holds one directory per site key. */
|
|
953
|
+
export declare const SITES_DIRECTORY = "sites";
|
|
954
|
+
/** The directory of a scope's files: `''` (shared) or `sites/<siteKey>`. */
|
|
955
|
+
export declare const scopeDirectory: (siteKey: string | null) => string;
|
|
956
|
+
/** A definition's file path: `models/post.json`, or `sites/blog/models/post.json` for site `blog`. */
|
|
957
|
+
export declare const schemaFilePath: (definition: Pick<SchemaDefinition, "kind" | "apiKey">, siteKey: string | null) => string;
|
|
958
|
+
/**
|
|
959
|
+
* The scope a schema file path is in: `{ site: null }` for `models/x.json` or `components/x.json`,
|
|
960
|
+
* `{ site: key }` under `sites/<key>/`, undefined for anything else (not a schema file).
|
|
961
|
+
*/
|
|
962
|
+
export declare const scopeOfSchemaFilePath: (path: string) => {
|
|
963
|
+
site: string | null;
|
|
964
|
+
} | undefined;
|
|
965
|
+
//#endregion
|
|
944
966
|
//#region src/fileFormat/lockFile.d.ts
|
|
945
967
|
/** Where `shapio schema pull` keeps its record of what it pulled, relative to the project root. */
|
|
946
968
|
export declare const LOCK_FILE_PATH = ".shapio/schema-lock.json";
|
|
947
|
-
|
|
969
|
+
/**
|
|
970
|
+
* Format 2 (per-site schemas): each entry records its scope (`site`: a site key, or null when shared) and
|
|
971
|
+
* the lock lists the sites the tree covers. Format 1 (every definition shared) still reads: its entries
|
|
972
|
+
* have no `site`, which means shared, and it covers no site. `upgradeLockFile` spells both the same way.
|
|
973
|
+
*/
|
|
974
|
+
export declare const LOCK_FILE_FORMAT_VERSION = 2;
|
|
948
975
|
export declare const LockEntrySchema: Type.TObject<{
|
|
949
976
|
kind: Type.TEnum<["collection", "singleton", "component"]>;
|
|
950
977
|
apiKey: Type.TString;
|
|
@@ -952,11 +979,18 @@ export declare const LockEntrySchema: Type.TObject<{
|
|
|
952
979
|
version: Type.TInteger;
|
|
953
980
|
/** `hashDefinition` of the pulled definition; a local file with another hash has been edited. */
|
|
954
981
|
hash: Type.TString;
|
|
982
|
+
/**
|
|
983
|
+
* The key of the site the definition belongs to; null (or absent, format 1) when shared. Null comes first:
|
|
984
|
+
* Fastify's Ajv coerces types, and a string branch tried first would turn null into "" (no site at all).
|
|
985
|
+
*/
|
|
986
|
+
site: Type.TOptional<Type.TUnion<[Type.TNull, Type.TString]>>;
|
|
955
987
|
}>;
|
|
956
988
|
export declare const LockFileSchema: Type.TObject<{
|
|
957
|
-
formatVersion: Type.TLiteral<1>;
|
|
989
|
+
formatVersion: Type.TUnion<[Type.TLiteral<1>, Type.TLiteral<2>]>;
|
|
958
990
|
/** The instance's global schema version at pull time (informational; the guard is per model). */
|
|
959
991
|
schemaVersion: Type.TInteger;
|
|
992
|
+
/** Keys of the sites whose definitions this tree holds (format 2; absent: none, shared only). */
|
|
993
|
+
sites: Type.TOptional<Type.TArray<Type.TString>>;
|
|
960
994
|
/** Keyed by definition ID. */
|
|
961
995
|
definitions: Type.TRecord<"^.*$", Type.TObject<{
|
|
962
996
|
kind: Type.TEnum<["collection", "singleton", "component"]>;
|
|
@@ -965,10 +999,49 @@ export declare const LockFileSchema: Type.TObject<{
|
|
|
965
999
|
version: Type.TInteger;
|
|
966
1000
|
/** `hashDefinition` of the pulled definition; a local file with another hash has been edited. */
|
|
967
1001
|
hash: Type.TString;
|
|
1002
|
+
/**
|
|
1003
|
+
* The key of the site the definition belongs to; null (or absent, format 1) when shared. Null comes first:
|
|
1004
|
+
* Fastify's Ajv coerces types, and a string branch tried first would turn null into "" (no site at all).
|
|
1005
|
+
*/
|
|
1006
|
+
site: Type.TOptional<Type.TUnion<[Type.TNull, Type.TString]>>;
|
|
968
1007
|
}>>;
|
|
969
1008
|
}>;
|
|
970
1009
|
export type LockEntry = Static<typeof LockEntrySchema>;
|
|
971
1010
|
export type LockFile = Static<typeof LockFileSchema>;
|
|
1011
|
+
/** A lock in format 2 with every scope spelled out. */
|
|
1012
|
+
export type ScopedLockEntry = LockEntry & {
|
|
1013
|
+
site: string | null;
|
|
1014
|
+
};
|
|
1015
|
+
export type ScopedLockFile = Omit<LockFile, 'formatVersion' | 'sites' | 'definitions'> & {
|
|
1016
|
+
formatVersion: typeof LOCK_FILE_FORMAT_VERSION;
|
|
1017
|
+
sites: string[];
|
|
1018
|
+
definitions: Record<string, ScopedLockEntry>;
|
|
1019
|
+
};
|
|
1020
|
+
/** Format 1 or 2 → format 2: absent scopes are shared, absent sites none. */
|
|
1021
|
+
export declare const upgradeLockFile: (lock: LockFile) => ScopedLockFile;
|
|
1022
|
+
/** The lock entries one site's tree is about: the shared definitions and that site's (`siteKey`). */
|
|
1023
|
+
export declare const lockEntriesForSite: (lock: LockFile, siteKey: string | null) => LockFile["definitions"];
|
|
1024
|
+
/** A tree applies to a site when it covers no site yet (shared only, or format 1) or lists that site. */
|
|
1025
|
+
export declare const lockCoversSite: (lock: LockFile, siteKey: string) => boolean;
|
|
1026
|
+
/** `sites` plus `siteKey`, sorted and without duplicates (stable diffs in git). */
|
|
1027
|
+
export declare const withSiteCovered: (sites: readonly string[], siteKey: string) => string[];
|
|
1028
|
+
/**
|
|
1029
|
+
* The lock after pulling one site's view (`shapio schema pull --site <siteKey>`): the shared entries and that
|
|
1030
|
+
* site's are replaced by what was pulled, another site's entries are kept as they are (one tree can hold
|
|
1031
|
+
* several sites), and the tree now covers `siteKey`. A format 1 lock upgrades first (every entry shared).
|
|
1032
|
+
*/
|
|
1033
|
+
export declare const mergePulledLock: (lock: LockFile | undefined, pulled: {
|
|
1034
|
+
schemaVersion: number;
|
|
1035
|
+
siteKey: string;
|
|
1036
|
+
definitions: readonly ExportedDefinition[];
|
|
1037
|
+
}) => ScopedLockFile;
|
|
1038
|
+
/** One definition as `schema export` returns it, with its scope (a site key, or null when shared). */
|
|
1039
|
+
export type ExportedDefinition = {
|
|
1040
|
+
definition: SchemaDefinition;
|
|
1041
|
+
version: number;
|
|
1042
|
+
hash: string;
|
|
1043
|
+
site: string | null;
|
|
1044
|
+
};
|
|
972
1045
|
export declare class LockFileError extends Error {
|
|
973
1046
|
constructor(message: string);
|
|
974
1047
|
}
|
|
@@ -1078,5 +1151,62 @@ export declare const richTextToPlainText: (document: RichTextDocument) => string
|
|
|
1078
1151
|
/** Plain text → a document of paragraphs (blank lines split paragraphs; single newlines are hard breaks). */
|
|
1079
1152
|
export declare const plainTextToRichText: (text: string) => RichTextDocument;
|
|
1080
1153
|
//#endregion
|
|
1154
|
+
//#region src/themes/tokens.d.ts
|
|
1155
|
+
/**
|
|
1156
|
+
* Admin themes (docs/plans/themes.md): a theme is a named, complete set of these tokens for a light variant, a
|
|
1157
|
+
* dark variant or both. One list, shared by the admin's stylesheets (and their tests) and the API, which
|
|
1158
|
+
* validates themes declared by extensions (`shapio.config` `themes`).
|
|
1159
|
+
*/
|
|
1160
|
+
/** The UI tokens: every theme variant sets all of them. */
|
|
1161
|
+
export declare const THEME_SEMANTIC_TOKENS: readonly ["background", "foreground", "card", "card-foreground", "popover", "popover-foreground", "primary", "primary-foreground", "primary-hover", "secondary", "secondary-foreground", "muted", "muted-foreground", "accent", "accent-foreground", "destructive", "destructive-foreground", "destructive-muted", "success", "success-muted", "warning", "warning-muted", "info", "info-muted", "border", "input", "ring", "link", "overlay", "sidebar", "sidebar-foreground", "sidebar-primary", "sidebar-primary-foreground", "sidebar-accent", "sidebar-accent-foreground", "sidebar-border", "sidebar-ring"];
|
|
1162
|
+
/**
|
|
1163
|
+
* The logo (tile, S, letters) and the signed-out screens' brand panel. Built-in themes set them all; an
|
|
1164
|
+
* extension theme may leave them out and gets Shapio's for that variant.
|
|
1165
|
+
*/
|
|
1166
|
+
export declare const THEME_BRAND_TOKENS: readonly ["brand-logo", "brand-logo-foreground", "brand-letters", "brand-panel-from", "brand-panel-to", "brand-panel-foreground", "brand-panel-mark"];
|
|
1167
|
+
export declare const THEME_TOKENS: readonly ["background", "foreground", "card", "card-foreground", "popover", "popover-foreground", "primary", "primary-foreground", "primary-hover", "secondary", "secondary-foreground", "muted", "muted-foreground", "accent", "accent-foreground", "destructive", "destructive-foreground", "destructive-muted", "success", "success-muted", "warning", "warning-muted", "info", "info-muted", "border", "input", "ring", "link", "overlay", "sidebar", "sidebar-foreground", "sidebar-primary", "sidebar-primary-foreground", "sidebar-accent", "sidebar-accent-foreground", "sidebar-border", "sidebar-ring", "brand-logo", "brand-logo-foreground", "brand-letters", "brand-panel-from", "brand-panel-to", "brand-panel-foreground", "brand-panel-mark"];
|
|
1168
|
+
export type ThemeToken = (typeof THEME_TOKENS)[number];
|
|
1169
|
+
export declare const THEME_VARIANTS: readonly ["light", "dark"];
|
|
1170
|
+
export type ThemeVariant = (typeof THEME_VARIANTS)[number];
|
|
1171
|
+
/** A theme key: used in `data-theme` and in CSS selectors, so deliberately narrow. */
|
|
1172
|
+
export declare const THEME_KEY_PATTERN: RegExp;
|
|
1173
|
+
/** The themes the admin ships; an extension theme may not reuse these keys. */
|
|
1174
|
+
export declare const BUILT_IN_THEME_KEYS: readonly ["shapio", "classic", "murdered-out", "snowed"];
|
|
1175
|
+
export type BuiltInThemeKey = (typeof BUILT_IN_THEME_KEYS)[number];
|
|
1176
|
+
/**
|
|
1177
|
+
* A token value an extension may use: a hex colour or a colour function whose arguments are numbers, units
|
|
1178
|
+
* and separators only (no `;`, quotes, braces or nested functions such as `url()` or `var()`), so a value
|
|
1179
|
+
* can never escape its declaration.
|
|
1180
|
+
*/
|
|
1181
|
+
export declare const THEME_COLOUR_PATTERN: RegExp;
|
|
1182
|
+
//#endregion
|
|
1183
|
+
//#region src/themes/contrast.d.ts
|
|
1184
|
+
/**
|
|
1185
|
+
* WCAG 2.1 AA contrast of a theme's tokens: text pairs need 4.5:1 (1.4.3); what identifies a control or the
|
|
1186
|
+
* focus (input borders, unchecked switches, focus rings, primary buttons against the page) needs 3:1 (1.4.11).
|
|
1187
|
+
* The admin's built-in themes are gated on this by a unit test; extension themes get warnings from
|
|
1188
|
+
* `shapio extensions check`. axe checks rendered text only, not these pairs.
|
|
1189
|
+
*/
|
|
1190
|
+
export type ContrastPair = readonly [foreground: string, background: string];
|
|
1191
|
+
export declare const THEME_TEXT_PAIRS: readonly ContrastPair[];
|
|
1192
|
+
export declare const THEME_UI_PAIRS: readonly ContrastPair[];
|
|
1193
|
+
/**
|
|
1194
|
+
* Dark only: control borders on the selection surface. In light, selected table rows use accent/40, which
|
|
1195
|
+
* passes, and other selected surfaces hold no controls.
|
|
1196
|
+
*/
|
|
1197
|
+
export declare const THEME_DARK_UI_PAIRS: readonly ContrastPair[];
|
|
1198
|
+
/** The WCAG contrast ratio of two `#rrggbb` colours. */
|
|
1199
|
+
export declare const contrastRatio: (a: string, b: string) => number;
|
|
1200
|
+
export type ContrastCheck = {
|
|
1201
|
+
foreground: string;
|
|
1202
|
+
background: string;
|
|
1203
|
+
/** 4.5 for text, 3 for controls. */
|
|
1204
|
+
required: number;
|
|
1205
|
+
/** Undefined when a value is not a `#rrggbb` colour (functions such as `oklch()` are not checked). */
|
|
1206
|
+
ratio: number | undefined;
|
|
1207
|
+
};
|
|
1208
|
+
/** Every pair the variant must meet, with its ratio. Missing tokens report an undefined ratio. */
|
|
1209
|
+
export declare const checkThemeContrast: (tokens: Readonly<Record<string, string>>, variant: ThemeVariant) => ContrastCheck[];
|
|
1210
|
+
//#endregion
|
|
1081
1211
|
export { RichTextDocument, RichTextNode, RichTextProblem, RichTextResult, isEmptyRichText, richTextMediaIds, validateRichText };
|
|
1082
1212
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -2058,25 +2058,106 @@ const sha256 = async (text) => `sha256:${toHex(await globalThis.crypto.subtle.di
|
|
|
2058
2058
|
/** Hash of a normalized definition's canonical serialization; recorded per model in the lock file. */
|
|
2059
2059
|
const hashDefinition = (definition) => sha256(serializeDefinition(definition));
|
|
2060
2060
|
//#endregion
|
|
2061
|
+
//#region src/fileFormat/layout.ts
|
|
2062
|
+
/**
|
|
2063
|
+
* Where schema files live in a repository (plan site-schema): shared definitions in `models/` and
|
|
2064
|
+
* `components/`, a site's own in `sites/<siteKey>/models/` and `sites/<siteKey>/components/`. Paths are
|
|
2065
|
+
* relative to the schema directory and always use `/` (git paths); callers join them for their file system.
|
|
2066
|
+
*/
|
|
2067
|
+
const SCHEMA_KIND_DIRECTORIES = ["models", "components"];
|
|
2068
|
+
/** The folder that holds one directory per site key. */
|
|
2069
|
+
const SITES_DIRECTORY = "sites";
|
|
2070
|
+
const kindDirectoryOf = (definition) => definition.kind === "component" ? "components" : "models";
|
|
2071
|
+
/** The directory of a scope's files: `''` (shared) or `sites/<siteKey>`. */
|
|
2072
|
+
const scopeDirectory = (siteKey) => siteKey === null ? "" : `${SITES_DIRECTORY}/${siteKey}`;
|
|
2073
|
+
/** A definition's file path: `models/post.json`, or `sites/blog/models/post.json` for site `blog`. */
|
|
2074
|
+
const schemaFilePath = (definition, siteKey) => {
|
|
2075
|
+
const file = `${kindDirectoryOf(definition)}/${definition.apiKey}.json`;
|
|
2076
|
+
return siteKey === null ? file : `${scopeDirectory(siteKey)}/${file}`;
|
|
2077
|
+
};
|
|
2078
|
+
/**
|
|
2079
|
+
* The scope a schema file path is in: `{ site: null }` for `models/x.json` or `components/x.json`,
|
|
2080
|
+
* `{ site: key }` under `sites/<key>/`, undefined for anything else (not a schema file).
|
|
2081
|
+
*/
|
|
2082
|
+
const scopeOfSchemaFilePath = (path) => {
|
|
2083
|
+
const parts = path.split("/").filter((part) => part.length > 0);
|
|
2084
|
+
const isFile = (kind, name) => SCHEMA_KIND_DIRECTORIES.includes(kind ?? "") && (name ?? "").endsWith(".json");
|
|
2085
|
+
if (parts.length === 2 && isFile(parts[0], parts[1])) return { site: null };
|
|
2086
|
+
if (parts.length === 4 && parts[0] === "sites" && parts[1] && isFile(parts[2], parts[3])) return { site: parts[1] };
|
|
2087
|
+
};
|
|
2088
|
+
//#endregion
|
|
2061
2089
|
//#region src/fileFormat/lockFile.ts
|
|
2062
2090
|
/** Where `shapio schema pull` keeps its record of what it pulled, relative to the project root. */
|
|
2063
2091
|
const LOCK_FILE_PATH = ".shapio/schema-lock.json";
|
|
2064
|
-
|
|
2092
|
+
/**
|
|
2093
|
+
* Format 2 (per-site schemas): each entry records its scope (`site`: a site key, or null when shared) and
|
|
2094
|
+
* the lock lists the sites the tree covers. Format 1 (every definition shared) still reads: its entries
|
|
2095
|
+
* have no `site`, which means shared, and it covers no site. `upgradeLockFile` spells both the same way.
|
|
2096
|
+
*/
|
|
2097
|
+
const LOCK_FILE_FORMAT_VERSION = 2;
|
|
2065
2098
|
const LockEntrySchema = Type.Object({
|
|
2066
2099
|
kind: Type.Enum(DEFINITION_KINDS),
|
|
2067
2100
|
apiKey: Type.String(),
|
|
2068
2101
|
/** The definition's per-model version on the instance when it was pulled. */
|
|
2069
2102
|
version: Type.Integer({ minimum: 1 }),
|
|
2070
2103
|
/** `hashDefinition` of the pulled definition; a local file with another hash has been edited. */
|
|
2071
|
-
hash: Type.String()
|
|
2104
|
+
hash: Type.String(),
|
|
2105
|
+
/**
|
|
2106
|
+
* The key of the site the definition belongs to; null (or absent, format 1) when shared. Null comes first:
|
|
2107
|
+
* Fastify's Ajv coerces types, and a string branch tried first would turn null into "" (no site at all).
|
|
2108
|
+
*/
|
|
2109
|
+
site: Type.Optional(Type.Union([Type.Null(), Type.String()]))
|
|
2072
2110
|
}, { additionalProperties: false });
|
|
2073
2111
|
const LockFileSchema = Type.Object({
|
|
2074
|
-
formatVersion: Type.Literal(1),
|
|
2112
|
+
formatVersion: Type.Union([Type.Literal(1), Type.Literal(2)]),
|
|
2075
2113
|
/** The instance's global schema version at pull time (informational; the guard is per model). */
|
|
2076
2114
|
schemaVersion: Type.Integer({ minimum: 0 }),
|
|
2115
|
+
/** Keys of the sites whose definitions this tree holds (format 2; absent: none, shared only). */
|
|
2116
|
+
sites: Type.Optional(Type.Array(Type.String())),
|
|
2077
2117
|
/** Keyed by definition ID. */
|
|
2078
2118
|
definitions: Type.Record(Type.String(), LockEntrySchema)
|
|
2079
2119
|
}, { additionalProperties: false });
|
|
2120
|
+
/** Format 1 or 2 → format 2: absent scopes are shared, absent sites none. */
|
|
2121
|
+
const upgradeLockFile = (lock) => ({
|
|
2122
|
+
formatVersion: 2,
|
|
2123
|
+
schemaVersion: lock.schemaVersion,
|
|
2124
|
+
sites: [...lock.sites ?? []],
|
|
2125
|
+
definitions: Object.fromEntries(Object.entries(lock.definitions).map(([id, entry]) => [id, {
|
|
2126
|
+
...entry,
|
|
2127
|
+
site: entry.site ?? null
|
|
2128
|
+
}]))
|
|
2129
|
+
});
|
|
2130
|
+
/** The lock entries one site's tree is about: the shared definitions and that site's (`siteKey`). */
|
|
2131
|
+
const lockEntriesForSite = (lock, siteKey) => Object.fromEntries(Object.entries(lock.definitions).filter(([, entry]) => (entry.site ?? null) === null || entry.site === siteKey));
|
|
2132
|
+
/** A tree applies to a site when it covers no site yet (shared only, or format 1) or lists that site. */
|
|
2133
|
+
const lockCoversSite = (lock, siteKey) => {
|
|
2134
|
+
const sites = lock.sites ?? [];
|
|
2135
|
+
return sites.length === 0 || sites.includes(siteKey);
|
|
2136
|
+
};
|
|
2137
|
+
/** `sites` plus `siteKey`, sorted and without duplicates (stable diffs in git). */
|
|
2138
|
+
const withSiteCovered = (sites, siteKey) => [.../* @__PURE__ */ new Set([...sites, siteKey])].sort();
|
|
2139
|
+
/**
|
|
2140
|
+
* The lock after pulling one site's view (`shapio schema pull --site <siteKey>`): the shared entries and that
|
|
2141
|
+
* site's are replaced by what was pulled, another site's entries are kept as they are (one tree can hold
|
|
2142
|
+
* several sites), and the tree now covers `siteKey`. A format 1 lock upgrades first (every entry shared).
|
|
2143
|
+
*/
|
|
2144
|
+
const mergePulledLock = (lock, pulled) => {
|
|
2145
|
+
const current = lock ? upgradeLockFile(lock) : void 0;
|
|
2146
|
+
const pulledIds = new Set(pulled.definitions.map(({ definition }) => definition.id));
|
|
2147
|
+
const kept = Object.entries(current?.definitions ?? {}).filter(([id, entry]) => entry.site !== null && entry.site !== pulled.siteKey && !pulledIds.has(id));
|
|
2148
|
+
return {
|
|
2149
|
+
formatVersion: 2,
|
|
2150
|
+
schemaVersion: pulled.schemaVersion,
|
|
2151
|
+
sites: withSiteCovered(current?.sites ?? [], pulled.siteKey),
|
|
2152
|
+
definitions: Object.fromEntries([...kept, ...pulled.definitions.map(({ definition, version, hash, site }) => [definition.id, {
|
|
2153
|
+
kind: definition.kind,
|
|
2154
|
+
apiKey: definition.apiKey,
|
|
2155
|
+
version,
|
|
2156
|
+
hash,
|
|
2157
|
+
site
|
|
2158
|
+
}])])
|
|
2159
|
+
};
|
|
2160
|
+
};
|
|
2080
2161
|
var LockFileError = class extends Error {
|
|
2081
2162
|
constructor(message) {
|
|
2082
2163
|
super(message);
|
|
@@ -2263,6 +2344,187 @@ const plainTextToRichText = (text) => {
|
|
|
2263
2344
|
};
|
|
2264
2345
|
};
|
|
2265
2346
|
//#endregion
|
|
2266
|
-
|
|
2347
|
+
//#region src/themes/tokens.ts
|
|
2348
|
+
/**
|
|
2349
|
+
* Admin themes (docs/plans/themes.md): a theme is a named, complete set of these tokens for a light variant, a
|
|
2350
|
+
* dark variant or both. One list, shared by the admin's stylesheets (and their tests) and the API, which
|
|
2351
|
+
* validates themes declared by extensions (`shapio.config` `themes`).
|
|
2352
|
+
*/
|
|
2353
|
+
/** The UI tokens: every theme variant sets all of them. */
|
|
2354
|
+
const THEME_SEMANTIC_TOKENS = [
|
|
2355
|
+
"background",
|
|
2356
|
+
"foreground",
|
|
2357
|
+
"card",
|
|
2358
|
+
"card-foreground",
|
|
2359
|
+
"popover",
|
|
2360
|
+
"popover-foreground",
|
|
2361
|
+
"primary",
|
|
2362
|
+
"primary-foreground",
|
|
2363
|
+
"primary-hover",
|
|
2364
|
+
"secondary",
|
|
2365
|
+
"secondary-foreground",
|
|
2366
|
+
"muted",
|
|
2367
|
+
"muted-foreground",
|
|
2368
|
+
"accent",
|
|
2369
|
+
"accent-foreground",
|
|
2370
|
+
"destructive",
|
|
2371
|
+
"destructive-foreground",
|
|
2372
|
+
"destructive-muted",
|
|
2373
|
+
"success",
|
|
2374
|
+
"success-muted",
|
|
2375
|
+
"warning",
|
|
2376
|
+
"warning-muted",
|
|
2377
|
+
"info",
|
|
2378
|
+
"info-muted",
|
|
2379
|
+
"border",
|
|
2380
|
+
"input",
|
|
2381
|
+
"ring",
|
|
2382
|
+
"link",
|
|
2383
|
+
"overlay",
|
|
2384
|
+
"sidebar",
|
|
2385
|
+
"sidebar-foreground",
|
|
2386
|
+
"sidebar-primary",
|
|
2387
|
+
"sidebar-primary-foreground",
|
|
2388
|
+
"sidebar-accent",
|
|
2389
|
+
"sidebar-accent-foreground",
|
|
2390
|
+
"sidebar-border",
|
|
2391
|
+
"sidebar-ring"
|
|
2392
|
+
];
|
|
2393
|
+
/**
|
|
2394
|
+
* The logo (tile, S, letters) and the signed-out screens' brand panel. Built-in themes set them all; an
|
|
2395
|
+
* extension theme may leave them out and gets Shapio's for that variant.
|
|
2396
|
+
*/
|
|
2397
|
+
const THEME_BRAND_TOKENS = [
|
|
2398
|
+
"brand-logo",
|
|
2399
|
+
"brand-logo-foreground",
|
|
2400
|
+
"brand-letters",
|
|
2401
|
+
"brand-panel-from",
|
|
2402
|
+
"brand-panel-to",
|
|
2403
|
+
"brand-panel-foreground",
|
|
2404
|
+
"brand-panel-mark"
|
|
2405
|
+
];
|
|
2406
|
+
const THEME_TOKENS = [...THEME_SEMANTIC_TOKENS, ...THEME_BRAND_TOKENS];
|
|
2407
|
+
const THEME_VARIANTS = ["light", "dark"];
|
|
2408
|
+
/** A theme key: used in `data-theme` and in CSS selectors, so deliberately narrow. */
|
|
2409
|
+
const THEME_KEY_PATTERN = /^[a-z][a-z0-9-]{0,40}$/;
|
|
2410
|
+
/** The themes the admin ships; an extension theme may not reuse these keys. */
|
|
2411
|
+
const BUILT_IN_THEME_KEYS = [
|
|
2412
|
+
"shapio",
|
|
2413
|
+
"classic",
|
|
2414
|
+
"murdered-out",
|
|
2415
|
+
"snowed"
|
|
2416
|
+
];
|
|
2417
|
+
/**
|
|
2418
|
+
* A token value an extension may use: a hex colour or a colour function whose arguments are numbers, units
|
|
2419
|
+
* and separators only (no `;`, quotes, braces or nested functions such as `url()` or `var()`), so a value
|
|
2420
|
+
* can never escape its declaration.
|
|
2421
|
+
*/
|
|
2422
|
+
const THEME_COLOUR_PATTERN = /^(?:#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|(?:rgba?|hsla?|oklch|oklab)\([0-9a-z.,%\s/+-]{1,80}\))$/;
|
|
2423
|
+
//#endregion
|
|
2424
|
+
//#region src/themes/contrast.ts
|
|
2425
|
+
const SURFACES = [
|
|
2426
|
+
"background",
|
|
2427
|
+
"card",
|
|
2428
|
+
"popover"
|
|
2429
|
+
];
|
|
2430
|
+
const THEME_TEXT_PAIRS = [
|
|
2431
|
+
...SURFACES.map((surface) => ["foreground", surface]),
|
|
2432
|
+
...[
|
|
2433
|
+
...SURFACES,
|
|
2434
|
+
"muted",
|
|
2435
|
+
"secondary",
|
|
2436
|
+
"accent",
|
|
2437
|
+
"sidebar"
|
|
2438
|
+
].map((surface) => ["muted-foreground", surface]),
|
|
2439
|
+
["primary-foreground", "primary"],
|
|
2440
|
+
["primary-foreground", "primary-hover"],
|
|
2441
|
+
["secondary-foreground", "secondary"],
|
|
2442
|
+
["accent-foreground", "accent"],
|
|
2443
|
+
["destructive-foreground", "destructive"],
|
|
2444
|
+
["destructive", "background"],
|
|
2445
|
+
["destructive", "card"],
|
|
2446
|
+
["success", "background"],
|
|
2447
|
+
["success", "card"],
|
|
2448
|
+
["success", "success-muted"],
|
|
2449
|
+
["warning", "warning-muted"],
|
|
2450
|
+
["warning", "card"],
|
|
2451
|
+
["info", "info-muted"],
|
|
2452
|
+
["info", "card"],
|
|
2453
|
+
["destructive", "destructive-muted"],
|
|
2454
|
+
["foreground", "muted"],
|
|
2455
|
+
["link", "background"],
|
|
2456
|
+
["link", "card"],
|
|
2457
|
+
["sidebar-foreground", "sidebar"],
|
|
2458
|
+
["sidebar-primary-foreground", "sidebar-primary"],
|
|
2459
|
+
["sidebar-accent-foreground", "sidebar-accent"],
|
|
2460
|
+
["sidebar-foreground", "sidebar-accent"],
|
|
2461
|
+
["muted-foreground", "sidebar-accent"],
|
|
2462
|
+
...[
|
|
2463
|
+
"destructive",
|
|
2464
|
+
"success",
|
|
2465
|
+
"warning",
|
|
2466
|
+
"link"
|
|
2467
|
+
].flatMap((text) => [[text, "popover"], [text, "muted"]]),
|
|
2468
|
+
["brand-panel-foreground", "brand-panel-from"],
|
|
2469
|
+
["brand-panel-foreground", "brand-panel-to"]
|
|
2470
|
+
];
|
|
2471
|
+
const THEME_UI_PAIRS = [
|
|
2472
|
+
...[
|
|
2473
|
+
...SURFACES,
|
|
2474
|
+
"muted",
|
|
2475
|
+
"secondary"
|
|
2476
|
+
].map((surface) => ["input", surface]),
|
|
2477
|
+
...SURFACES.map((surface) => ["ring", surface]),
|
|
2478
|
+
["ring", "muted"],
|
|
2479
|
+
["ring", "accent"],
|
|
2480
|
+
["primary", "background"],
|
|
2481
|
+
["primary", "card"],
|
|
2482
|
+
["primary", "popover"],
|
|
2483
|
+
["primary", "muted"],
|
|
2484
|
+
["info", "muted"],
|
|
2485
|
+
["sidebar-ring", "sidebar"],
|
|
2486
|
+
["sidebar-primary", "sidebar"],
|
|
2487
|
+
["sidebar-primary", "sidebar-accent"]
|
|
2488
|
+
];
|
|
2489
|
+
/**
|
|
2490
|
+
* Dark only: control borders on the selection surface. In light, selected table rows use accent/40, which
|
|
2491
|
+
* passes, and other selected surfaces hold no controls.
|
|
2492
|
+
*/
|
|
2493
|
+
const THEME_DARK_UI_PAIRS = [["input", "accent"]];
|
|
2494
|
+
const HEX = /^#[0-9a-fA-F]{6}$/;
|
|
2495
|
+
const channel = (value) => {
|
|
2496
|
+
const c = value / 255;
|
|
2497
|
+
return c <= .03928 ? c / 12.92 : ((c + .055) / 1.055) ** 2.4;
|
|
2498
|
+
};
|
|
2499
|
+
const luminance = (hex) => {
|
|
2500
|
+
const [r, g, b] = [
|
|
2501
|
+
1,
|
|
2502
|
+
3,
|
|
2503
|
+
5
|
|
2504
|
+
].map((index) => channel(parseInt(hex.slice(index, index + 2), 16)));
|
|
2505
|
+
return .2126 * (r ?? 0) + .7152 * (g ?? 0) + .0722 * (b ?? 0);
|
|
2506
|
+
};
|
|
2507
|
+
/** The WCAG contrast ratio of two `#rrggbb` colours. */
|
|
2508
|
+
const contrastRatio = (a, b) => {
|
|
2509
|
+
const [high = 0, low = 0] = [luminance(a), luminance(b)].sort((x, y) => y - x);
|
|
2510
|
+
return (high + .05) / (low + .05);
|
|
2511
|
+
};
|
|
2512
|
+
/** Every pair the variant must meet, with its ratio. Missing tokens report an undefined ratio. */
|
|
2513
|
+
const checkThemeContrast = (tokens, variant) => {
|
|
2514
|
+
const uiPairs = variant === "dark" ? [...THEME_UI_PAIRS, ...THEME_DARK_UI_PAIRS] : THEME_UI_PAIRS;
|
|
2515
|
+
const measure = ([foreground, background], required) => {
|
|
2516
|
+
const a = tokens[foreground];
|
|
2517
|
+
const b = tokens[background];
|
|
2518
|
+
return {
|
|
2519
|
+
foreground,
|
|
2520
|
+
background,
|
|
2521
|
+
required,
|
|
2522
|
+
ratio: a && b && HEX.test(a) && HEX.test(b) ? contrastRatio(a, b) : void 0
|
|
2523
|
+
};
|
|
2524
|
+
};
|
|
2525
|
+
return [...THEME_TEXT_PAIRS.map((pair) => measure(pair, 4.5)), ...uiPairs.map((pair) => measure(pair, 3))];
|
|
2526
|
+
};
|
|
2527
|
+
//#endregion
|
|
2528
|
+
export { API_KEY_PATTERN, BIGINTEGER_PATTERN, BUILT_IN_THEME_KEYS, CANVAS_ELIGIBLE, CHANGE_KINDS, COMPONENT_TYPE_SUFFIXES, CUSTOM_EDITOR_ID_PATTERN, ComponentDefinitionInputSchema, ComponentDisplayInputSchema, DATA_TYPES, DATETIME_PATTERN, DATE_PATTERN, DECIMAL_PATTERN, DEFAULT_EDITORS, DEFINITION_KINDS, DefinitionInputSchema, EDITOR_CATALOGUE, EditorChoiceInputSchema, EnumValueSchema, FieldInputSchema, INLINE_TITLE_TYPES, LOCALE_CODE_PATTERN, LOCK_FILE_FORMAT_VERSION, LOCK_FILE_PATH, LocaleInputSchema, LockEntrySchema, LockFileError, LockFileSchema, MARK_RULES, MAX_API_KEY_LENGTH, MAX_COMPONENT_DEPTH, MAX_DESCRIPTION_LENGTH, MAX_FALLBACK_CHAIN, MAX_FIELDS_PER_DEFINITION, MAX_LABEL_LENGTH, MAX_RICHTEXT_DEPTH, MEDIA_KINDS, MODEL_KINDS, MODEL_TYPE_SUFFIXES, MUTATION_PREFIXES, ModelDefinitionInputSchema, ModelDisplayInputSchema, NODE_RULES, PLURAL_FALLBACK_SUFFIX, REFERENCE_DATA_TYPES, RELATION_CARDINALITIES, RESERVED_DEFINITION_API_KEYS, RESERVED_FIELD_API_KEYS, RESERVED_QUERY_NAMES, RESERVED_TYPE_NAMES, RICHTEXT_FORMAT, RICHTEXT_FORMAT_VERSION, SAFE_LINK_PATTERN, SCALAR_DATA_TYPES, SCHEMA_KIND_DIRECTORIES, SETTINGS_SCHEMAS, SITES_DIRECTORY, SLUG_PATTERN, SORT_DIRECTIONS, STABLE_ID_PATTERN, STORAGE_FAMILY, STRIP_DEFAULT_COUNT, SchemaValidationError, TEXT_TITLE_FIELD_TYPES, THEME_BRAND_TOKENS, THEME_COLOUR_PATTERN, THEME_DARK_UI_PAIRS, THEME_KEY_PATTERN, THEME_SEMANTIC_TOKENS, THEME_TEXT_PAIRS, THEME_TOKENS, THEME_UI_PAIRS, THEME_VARIANTS, TIME_PATTERN, TITLE_FIELD_TYPES, UID_PATTERN, UNIQUE_CAPABLE_DATA_TYPES, assignIds, canonicalJson, checkApiKeySyntax, checkDefaultValue, checkThemeContrast, classifyChange, classifyChanges, classifyLocaleChange, collectionQueryName, contrastRatio, createStableId, decideSync, diffDefinitions, effectiveLayout, effectiveTitleField, embeddedComponentIds, escapeHtml, findDependentModels, findReferencingDefinitions, foldApiKey, generatedNames, hashDefinition, isApiKeySyntaxValid, isCanvasEligible, isComponentDefinition, isCoverEligible, isCustomEditorId, isDataType, isEmptyRichText, isModelDefinition, isStableId, issue, listCompatibleEditors, lockCoversSite, lockEntriesForSite, mergePulledLock, normalizeDefinition, parseDefinition, parseLockFile, plainTextToRichText, renderRichTextHtml, renormalizeDefinition, richTextMediaIds, richTextToPlainText, routeKeyOf, schemaFilePath, scopeDirectory, scopeOfSchemaFilePath, serializeDefinition, serializeLockFile, sha256, stripFieldsOf, suggestPlural, summarizeChanges, toTypeName, upgradeLockFile, validateDefinition, validateRichText, validateSchema, withDerivedPlural, withSiteCovered };
|
|
2267
2529
|
|
|
2268
2530
|
//# sourceMappingURL=index.js.map
|