@routevn/creator-model 1.10.3 → 1.10.4

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.
Files changed (3) hide show
  1. package/README.md +29 -0
  2. package/package.json +1 -1
  3. package/src/model.js +172 -25
package/README.md CHANGED
@@ -106,6 +106,35 @@ Design rules:
106
106
  - random ids across RouteVN should use `nanoid` with the RouteVN base58 variant;
107
107
  deterministic derived tokens such as partition hashes are a separate case
108
108
 
109
+ ## Font Weight Metadata
110
+
111
+ Font resources may store extracted weight capabilities in three flat fields:
112
+
113
+ ```js
114
+ {
115
+ minWeight: 100,
116
+ defaultWeight: 400,
117
+ maxWeight: 900,
118
+ }
119
+ ```
120
+
121
+ The fields are optional for compatibility with existing font resources. When
122
+ one is present, all three are required and must satisfy
123
+ `1 <= minWeight <= defaultWeight <= maxWeight <= 1000`. Static fonts store the
124
+ same value in all three fields; variable fonts store their `fvar` `wght` axis
125
+ range and default.
126
+
127
+ ## Text Style Font References
128
+
129
+ `textStyle.fontId` accepts either one font ID or a non-empty array of unique
130
+ font IDs. Both forms are persisted as supplied, and every ID must reference an
131
+ existing non-folder font:
132
+
133
+ ```js
134
+ { fontId: "font-primary" }
135
+ { fontId: ["font-primary", "font-fallback"] }
136
+ ```
137
+
109
138
  ## File Structure
110
139
 
111
140
  ```text
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@routevn/creator-model",
3
- "version": "1.10.3",
3
+ "version": "1.10.4",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/model.js CHANGED
@@ -42,6 +42,7 @@ const LINE_UPDATE_ACTIONS_PRESERVE_PATHS_SET = new Set(
42
42
  LINE_UPDATE_ACTIONS_PRESERVE_PATHS,
43
43
  );
44
44
  const CURRENT_LAYOUT_SCHEMA_VERSION = 2;
45
+ const FONT_WEIGHT_KEYS = ["minWeight", "defaultWeight", "maxWeight"];
45
46
  const isPositiveFiniteNumber = (value) => isFiniteNumber(value) && value > 0;
46
47
  const normalizeLayoutSchemaVersion = (value) =>
47
48
  Number.isInteger(value) && value >= 1 ? value : 1;
@@ -2388,6 +2389,41 @@ const validateAnimationItems = ({ items, path, errorFactory }) => {
2388
2389
  }
2389
2390
  };
2390
2391
 
2392
+ const validateFontWeightFields = ({ value, path, errorFactory }) => {
2393
+ const presentKeys = FONT_WEIGHT_KEYS.filter((key) =>
2394
+ Object.hasOwn(value, key),
2395
+ );
2396
+ if (presentKeys.length === 0) {
2397
+ return;
2398
+ }
2399
+
2400
+ if (presentKeys.length !== FONT_WEIGHT_KEYS.length) {
2401
+ return invalidFromErrorFactory(
2402
+ errorFactory,
2403
+ `${path} must include minWeight, defaultWeight, and maxWeight together`,
2404
+ );
2405
+ }
2406
+
2407
+ for (const key of FONT_WEIGHT_KEYS) {
2408
+ if (!isFiniteNumber(value[key]) || value[key] < 1 || value[key] > 1000) {
2409
+ return invalidFromErrorFactory(
2410
+ errorFactory,
2411
+ `${path}.${key} must be a finite number between 1 and 1000`,
2412
+ );
2413
+ }
2414
+ }
2415
+
2416
+ if (
2417
+ value.minWeight > value.defaultWeight ||
2418
+ value.defaultWeight > value.maxWeight
2419
+ ) {
2420
+ return invalidFromErrorFactory(
2421
+ errorFactory,
2422
+ `${path} must satisfy minWeight <= defaultWeight <= maxWeight`,
2423
+ );
2424
+ }
2425
+ };
2426
+
2391
2427
  const validateFontItems = ({ items, path, errorFactory }) => {
2392
2428
  for (const [itemId, item] of Object.entries(items)) {
2393
2429
  const itemPath = `${path}.${itemId}`;
@@ -2413,6 +2449,7 @@ const validateFontItems = ({ items, path, errorFactory }) => {
2413
2449
  "tagIds",
2414
2450
  "fileId",
2415
2451
  "fontFamily",
2452
+ ...FONT_WEIGHT_KEYS,
2416
2453
  ],
2417
2454
  path: itemPath,
2418
2455
  errorFactory,
@@ -2476,6 +2513,17 @@ const validateFontItems = ({ items, path, errorFactory }) => {
2476
2513
  `${itemPath}.fontFamily must be a non-empty string`,
2477
2514
  );
2478
2515
  }
2516
+
2517
+ {
2518
+ const result = validateFontWeightFields({
2519
+ value: item,
2520
+ path: itemPath,
2521
+ errorFactory,
2522
+ });
2523
+ if (result?.valid === false) {
2524
+ return result;
2525
+ }
2526
+ }
2479
2527
  }
2480
2528
  }
2481
2529
  };
@@ -3216,11 +3264,15 @@ const validateTextStyleItems = ({ items, path, errorFactory }) => {
3216
3264
  }
3217
3265
  }
3218
3266
 
3219
- if (!isNonEmptyString(item.fontId)) {
3220
- return invalidFromErrorFactory(
3267
+ {
3268
+ const result = validateRequiredStringOrUniqueIdArray({
3269
+ value: item.fontId,
3270
+ path: `${itemPath}.fontId`,
3221
3271
  errorFactory,
3222
- `${itemPath}.fontId must be a non-empty string`,
3223
- );
3272
+ });
3273
+ if (result?.valid === false) {
3274
+ return result;
3275
+ }
3224
3276
  }
3225
3277
 
3226
3278
  if (!isNonEmptyString(item.colorId)) {
@@ -7192,15 +7244,17 @@ export const assertInvariants = ({ state }) => {
7192
7244
  continue;
7193
7245
  }
7194
7246
 
7195
- const font = state.fonts.items[textStyle.fontId];
7196
- if (!isPlainObject(font) || font.type === "folder") {
7197
- return invalidInvariant(
7198
- "textStyle.fontId must reference an existing non-folder font",
7199
- {
7200
- textStyleId,
7201
- fontId: textStyle.fontId,
7202
- },
7203
- );
7247
+ for (const fontId of toIdArray(textStyle.fontId)) {
7248
+ const font = state.fonts.items[fontId];
7249
+ if (!isPlainObject(font) || font.type === "folder") {
7250
+ return invalidInvariant(
7251
+ "textStyle.fontId must reference an existing non-folder font",
7252
+ {
7253
+ textStyleId,
7254
+ fontId,
7255
+ },
7256
+ );
7257
+ }
7204
7258
  }
7205
7259
 
7206
7260
  const color = state.colors.items[textStyle.colorId];
@@ -8689,6 +8743,27 @@ const validateRequiredUniqueIdArray = ({ value, path, errorFactory }) => {
8689
8743
  }
8690
8744
  };
8691
8745
 
8746
+ const validateRequiredStringOrUniqueIdArray = ({
8747
+ value,
8748
+ path,
8749
+ errorFactory,
8750
+ }) => {
8751
+ if (isNonEmptyString(value)) {
8752
+ return VALID_RESULT;
8753
+ }
8754
+
8755
+ if (!Array.isArray(value)) {
8756
+ return invalidFromErrorFactory(
8757
+ errorFactory,
8758
+ `${path} must be a non-empty string or a non-empty array of strings`,
8759
+ );
8760
+ }
8761
+
8762
+ return validateRequiredUniqueIdArray({ value, path, errorFactory });
8763
+ };
8764
+
8765
+ const toIdArray = (value) => (Array.isArray(value) ? value : [value]);
8766
+
8692
8767
  const validateOptionalUniqueIdArray = ({
8693
8768
  value,
8694
8769
  path,
@@ -9978,7 +10053,15 @@ const validateFontCreateData = ({ data, errorFactory }) => {
9978
10053
  allowedKeys:
9979
10054
  data.type === "folder"
9980
10055
  ? ["type", "name", "description"]
9981
- : ["type", "name", "description", "tagIds", "fileId", "fontFamily"],
10056
+ : [
10057
+ "type",
10058
+ "name",
10059
+ "description",
10060
+ "tagIds",
10061
+ "fileId",
10062
+ "fontFamily",
10063
+ ...FONT_WEIGHT_KEYS,
10064
+ ],
9982
10065
  path: "payload.data",
9983
10066
  errorFactory,
9984
10067
  });
@@ -10026,6 +10109,17 @@ const validateFontCreateData = ({ data, errorFactory }) => {
10026
10109
  "payload.data.fontFamily must be a non-empty string",
10027
10110
  );
10028
10111
  }
10112
+
10113
+ {
10114
+ const result = validateFontWeightFields({
10115
+ value: data,
10116
+ path: "payload.data",
10117
+ errorFactory,
10118
+ });
10119
+ if (result?.valid === false) {
10120
+ return result;
10121
+ }
10122
+ }
10029
10123
  }
10030
10124
  };
10031
10125
 
@@ -10033,7 +10127,14 @@ const validateFontUpdateData = ({ data, errorFactory }) => {
10033
10127
  {
10034
10128
  const result = validateAllowedKeys({
10035
10129
  value: data,
10036
- allowedKeys: ["name", "description", "tagIds", "fileId", "fontFamily"],
10130
+ allowedKeys: [
10131
+ "name",
10132
+ "description",
10133
+ "tagIds",
10134
+ "fileId",
10135
+ "fontFamily",
10136
+ ...FONT_WEIGHT_KEYS,
10137
+ ],
10037
10138
  path: "payload.data",
10038
10139
  errorFactory,
10039
10140
  });
@@ -10087,6 +10188,17 @@ const validateFontUpdateData = ({ data, errorFactory }) => {
10087
10188
  "payload.data.fontFamily must be a non-empty string when provided",
10088
10189
  );
10089
10190
  }
10191
+
10192
+ {
10193
+ const result = validateFontWeightFields({
10194
+ value: data,
10195
+ path: "payload.data",
10196
+ errorFactory,
10197
+ });
10198
+ if (result?.valid === false) {
10199
+ return result;
10200
+ }
10201
+ }
10090
10202
  };
10091
10203
 
10092
10204
  const validateFileCreateData = ({ data, errorFactory }) => {
@@ -11283,7 +11395,18 @@ const validateTextStyleUpdateData = ({ data, errorFactory }) => {
11283
11395
  }
11284
11396
  }
11285
11397
 
11286
- for (const key of ["fontId", "colorId", "strokeColorId"]) {
11398
+ if (data.fontId !== undefined) {
11399
+ const result = validateRequiredStringOrUniqueIdArray({
11400
+ value: data.fontId,
11401
+ path: "payload.data.fontId",
11402
+ errorFactory,
11403
+ });
11404
+ if (result?.valid === false) {
11405
+ return result;
11406
+ }
11407
+ }
11408
+
11409
+ for (const key of ["colorId", "strokeColorId"]) {
11287
11410
  if (data[key] !== undefined && !isNonEmptyString(data[key])) {
11288
11411
  return invalidFromErrorFactory(
11289
11412
  errorFactory,
@@ -17421,6 +17544,11 @@ const COMMAND_DEFINITIONS = [
17421
17544
  });
17422
17545
  nextFont.fileId = payload.data.fileId;
17423
17546
  nextFont.fontFamily = payload.data.fontFamily;
17547
+ for (const key of FONT_WEIGHT_KEYS) {
17548
+ if (payload.data[key] !== undefined) {
17549
+ nextFont[key] = payload.data[key];
17550
+ }
17551
+ }
17424
17552
  }
17425
17553
 
17426
17554
  state.fonts.items[payload.fontId] = nextFont;
@@ -17481,7 +17609,8 @@ const COMMAND_DEFINITIONS = [
17481
17609
  currentFont.type === "folder" &&
17482
17610
  (payload.data.tagIds !== undefined ||
17483
17611
  payload.data.fileId !== undefined ||
17484
- payload.data.fontFamily !== undefined)
17612
+ payload.data.fontFamily !== undefined ||
17613
+ FONT_WEIGHT_KEYS.some((key) => payload.data[key] !== undefined))
17485
17614
  ) {
17486
17615
  return invalidPrecondition(
17487
17616
  "folder font items cannot update font fields",
@@ -18531,16 +18660,24 @@ const COMMAND_DEFINITIONS = [
18531
18660
  }
18532
18661
  }
18533
18662
 
18534
- for (const field of ["fontId", "colorId", "strokeColorId"]) {
18663
+ for (const fontId of toIdArray(data.fontId)) {
18664
+ const item = state.fonts.items[fontId];
18665
+ if (!isPlainObject(item) || item.type === "folder") {
18666
+ return invalidPrecondition(
18667
+ "payload.data.fontId must reference an existing non-folder font",
18668
+ );
18669
+ }
18670
+ }
18671
+
18672
+ for (const field of ["colorId", "strokeColorId"]) {
18535
18673
  if (data[field] === undefined) {
18536
18674
  continue;
18537
18675
  }
18538
18676
 
18539
- const collectionKey = field === "fontId" ? "fonts" : "colors";
18540
- const item = state[collectionKey].items[data[field]];
18677
+ const item = state.colors.items[data[field]];
18541
18678
  if (!isPlainObject(item) || item.type === "folder") {
18542
18679
  return invalidPrecondition(
18543
- `payload.data.${field} must reference an existing non-folder ${collectionKey.slice(0, -1)}`,
18680
+ `payload.data.${field} must reference an existing non-folder color`,
18544
18681
  );
18545
18682
  }
18546
18683
  }
@@ -18583,16 +18720,26 @@ const COMMAND_DEFINITIONS = [
18583
18720
  }
18584
18721
  }
18585
18722
 
18586
- for (const field of ["fontId", "colorId", "strokeColorId"]) {
18723
+ if (payload.data.fontId !== undefined) {
18724
+ for (const fontId of toIdArray(payload.data.fontId)) {
18725
+ const item = state.fonts.items[fontId];
18726
+ if (!isPlainObject(item) || item.type === "folder") {
18727
+ return invalidPrecondition(
18728
+ "payload.data.fontId must reference an existing non-folder font",
18729
+ );
18730
+ }
18731
+ }
18732
+ }
18733
+
18734
+ for (const field of ["colorId", "strokeColorId"]) {
18587
18735
  if (payload.data[field] === undefined) {
18588
18736
  continue;
18589
18737
  }
18590
18738
 
18591
- const collectionKey = field === "fontId" ? "fonts" : "colors";
18592
- const item = state[collectionKey].items[payload.data[field]];
18739
+ const item = state.colors.items[payload.data[field]];
18593
18740
  if (!isPlainObject(item) || item.type === "folder") {
18594
18741
  return invalidPrecondition(
18595
- `payload.data.${field} must reference an existing non-folder ${collectionKey.slice(0, -1)}`,
18742
+ `payload.data.${field} must reference an existing non-folder color`,
18596
18743
  );
18597
18744
  }
18598
18745
  }