@routevn/creator-model 1.10.2 → 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 +217 -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.2",
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;
@@ -395,6 +396,13 @@ const LAYOUT_ELEMENT_BASE_TYPES = [
395
396
  "container-ref-confirm-dialog-ok",
396
397
  "container-ref-confirm-dialog-cancel",
397
398
  ];
399
+ const SAVE_LOAD_DATE_FORMATS = new Set([
400
+ "DD/MM/YYYY",
401
+ "MM/DD/YYYY",
402
+ "YYYY-MM-DD",
403
+ "DD MMM YYYY",
404
+ "YYYY年MM月DD日",
405
+ ]);
398
406
  export const SCHEMA_VERSION = 10;
399
407
  const LAYOUT_CONTAINER_ELEMENT_TYPES = [
400
408
  "folder",
@@ -2381,6 +2389,41 @@ const validateAnimationItems = ({ items, path, errorFactory }) => {
2381
2389
  }
2382
2390
  };
2383
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
+
2384
2427
  const validateFontItems = ({ items, path, errorFactory }) => {
2385
2428
  for (const [itemId, item] of Object.entries(items)) {
2386
2429
  const itemPath = `${path}.${itemId}`;
@@ -2406,6 +2449,7 @@ const validateFontItems = ({ items, path, errorFactory }) => {
2406
2449
  "tagIds",
2407
2450
  "fileId",
2408
2451
  "fontFamily",
2452
+ ...FONT_WEIGHT_KEYS,
2409
2453
  ],
2410
2454
  path: itemPath,
2411
2455
  errorFactory,
@@ -2469,6 +2513,17 @@ const validateFontItems = ({ items, path, errorFactory }) => {
2469
2513
  `${itemPath}.fontFamily must be a non-empty string`,
2470
2514
  );
2471
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
+ }
2472
2527
  }
2473
2528
  }
2474
2529
  };
@@ -3209,11 +3264,15 @@ const validateTextStyleItems = ({ items, path, errorFactory }) => {
3209
3264
  }
3210
3265
  }
3211
3266
 
3212
- if (!isNonEmptyString(item.fontId)) {
3213
- return invalidFromErrorFactory(
3267
+ {
3268
+ const result = validateRequiredStringOrUniqueIdArray({
3269
+ value: item.fontId,
3270
+ path: `${itemPath}.fontId`,
3214
3271
  errorFactory,
3215
- `${itemPath}.fontId must be a non-empty string`,
3216
- );
3272
+ });
3273
+ if (result?.valid === false) {
3274
+ return result;
3275
+ }
3217
3276
  }
3218
3277
 
3219
3278
  if (!isNonEmptyString(item.colorId)) {
@@ -3874,6 +3933,7 @@ const validateLayoutElementData = ({
3874
3933
  "border",
3875
3934
  "text",
3876
3935
  "content",
3936
+ "dateFormat",
3877
3937
  "textStyle",
3878
3938
  "displaySpeed",
3879
3939
  "revealEffect",
@@ -3966,6 +4026,27 @@ const validateLayoutElementData = ({
3966
4026
  }
3967
4027
  }
3968
4028
 
4029
+ if (
4030
+ data.dateFormat !== undefined &&
4031
+ !SAVE_LOAD_DATE_FORMATS.has(data.dateFormat)
4032
+ ) {
4033
+ return invalidFromErrorFactory(
4034
+ errorFactory,
4035
+ `${path}.dateFormat must be one of ${Array.from(SAVE_LOAD_DATE_FORMATS).join(", ")} when provided`,
4036
+ );
4037
+ }
4038
+
4039
+ if (
4040
+ data.dateFormat !== undefined &&
4041
+ data.type !== undefined &&
4042
+ data.type !== "text-ref-save-load-slot-date"
4043
+ ) {
4044
+ return invalidFromErrorFactory(
4045
+ errorFactory,
4046
+ `${path}.dateFormat can only be provided for save/load date elements`,
4047
+ );
4048
+ }
4049
+
3969
4050
  if (data.hidden !== undefined && typeof data.hidden !== "boolean") {
3970
4051
  return invalidFromErrorFactory(
3971
4052
  errorFactory,
@@ -4642,6 +4723,7 @@ const validateLayoutElementItems = ({ items, path, errorFactory }) => {
4642
4723
  "border",
4643
4724
  "text",
4644
4725
  "content",
4726
+ "dateFormat",
4645
4727
  "textStyle",
4646
4728
  "displaySpeed",
4647
4729
  "revealEffect",
@@ -7162,15 +7244,17 @@ export const assertInvariants = ({ state }) => {
7162
7244
  continue;
7163
7245
  }
7164
7246
 
7165
- const font = state.fonts.items[textStyle.fontId];
7166
- if (!isPlainObject(font) || font.type === "folder") {
7167
- return invalidInvariant(
7168
- "textStyle.fontId must reference an existing non-folder font",
7169
- {
7170
- textStyleId,
7171
- fontId: textStyle.fontId,
7172
- },
7173
- );
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
+ }
7174
7258
  }
7175
7259
 
7176
7260
  const color = state.colors.items[textStyle.colorId];
@@ -8659,6 +8743,27 @@ const validateRequiredUniqueIdArray = ({ value, path, errorFactory }) => {
8659
8743
  }
8660
8744
  };
8661
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
+
8662
8767
  const validateOptionalUniqueIdArray = ({
8663
8768
  value,
8664
8769
  path,
@@ -9948,7 +10053,15 @@ const validateFontCreateData = ({ data, errorFactory }) => {
9948
10053
  allowedKeys:
9949
10054
  data.type === "folder"
9950
10055
  ? ["type", "name", "description"]
9951
- : ["type", "name", "description", "tagIds", "fileId", "fontFamily"],
10056
+ : [
10057
+ "type",
10058
+ "name",
10059
+ "description",
10060
+ "tagIds",
10061
+ "fileId",
10062
+ "fontFamily",
10063
+ ...FONT_WEIGHT_KEYS,
10064
+ ],
9952
10065
  path: "payload.data",
9953
10066
  errorFactory,
9954
10067
  });
@@ -9996,6 +10109,17 @@ const validateFontCreateData = ({ data, errorFactory }) => {
9996
10109
  "payload.data.fontFamily must be a non-empty string",
9997
10110
  );
9998
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
+ }
9999
10123
  }
10000
10124
  };
10001
10125
 
@@ -10003,7 +10127,14 @@ const validateFontUpdateData = ({ data, errorFactory }) => {
10003
10127
  {
10004
10128
  const result = validateAllowedKeys({
10005
10129
  value: data,
10006
- allowedKeys: ["name", "description", "tagIds", "fileId", "fontFamily"],
10130
+ allowedKeys: [
10131
+ "name",
10132
+ "description",
10133
+ "tagIds",
10134
+ "fileId",
10135
+ "fontFamily",
10136
+ ...FONT_WEIGHT_KEYS,
10137
+ ],
10007
10138
  path: "payload.data",
10008
10139
  errorFactory,
10009
10140
  });
@@ -10057,6 +10188,17 @@ const validateFontUpdateData = ({ data, errorFactory }) => {
10057
10188
  "payload.data.fontFamily must be a non-empty string when provided",
10058
10189
  );
10059
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
+ }
10060
10202
  };
10061
10203
 
10062
10204
  const validateFileCreateData = ({ data, errorFactory }) => {
@@ -11253,7 +11395,18 @@ const validateTextStyleUpdateData = ({ data, errorFactory }) => {
11253
11395
  }
11254
11396
  }
11255
11397
 
11256
- 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"]) {
11257
11410
  if (data[key] !== undefined && !isNonEmptyString(data[key])) {
11258
11411
  return invalidFromErrorFactory(
11259
11412
  errorFactory,
@@ -12218,6 +12371,21 @@ const validateVisualElementReferenceTargets = ({
12218
12371
  state,
12219
12372
  errorFactory,
12220
12373
  }) => {
12374
+ if (
12375
+ data.dateFormat !== undefined &&
12376
+ data.type !== "text-ref-save-load-slot-date"
12377
+ ) {
12378
+ return invalidFromErrorFactory(
12379
+ errorFactory,
12380
+ `${ownerLabel} element dateFormat can only be provided for save/load date elements`,
12381
+ {
12382
+ [ownerIdField]: ownerId,
12383
+ elementId,
12384
+ field: "dateFormat",
12385
+ },
12386
+ );
12387
+ }
12388
+
12221
12389
  if (data.blur !== undefined && data.type !== "sprite") {
12222
12390
  return invalidFromErrorFactory(
12223
12391
  errorFactory,
@@ -17376,6 +17544,11 @@ const COMMAND_DEFINITIONS = [
17376
17544
  });
17377
17545
  nextFont.fileId = payload.data.fileId;
17378
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
+ }
17379
17552
  }
17380
17553
 
17381
17554
  state.fonts.items[payload.fontId] = nextFont;
@@ -17436,7 +17609,8 @@ const COMMAND_DEFINITIONS = [
17436
17609
  currentFont.type === "folder" &&
17437
17610
  (payload.data.tagIds !== undefined ||
17438
17611
  payload.data.fileId !== undefined ||
17439
- payload.data.fontFamily !== undefined)
17612
+ payload.data.fontFamily !== undefined ||
17613
+ FONT_WEIGHT_KEYS.some((key) => payload.data[key] !== undefined))
17440
17614
  ) {
17441
17615
  return invalidPrecondition(
17442
17616
  "folder font items cannot update font fields",
@@ -18486,16 +18660,24 @@ const COMMAND_DEFINITIONS = [
18486
18660
  }
18487
18661
  }
18488
18662
 
18489
- 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"]) {
18490
18673
  if (data[field] === undefined) {
18491
18674
  continue;
18492
18675
  }
18493
18676
 
18494
- const collectionKey = field === "fontId" ? "fonts" : "colors";
18495
- const item = state[collectionKey].items[data[field]];
18677
+ const item = state.colors.items[data[field]];
18496
18678
  if (!isPlainObject(item) || item.type === "folder") {
18497
18679
  return invalidPrecondition(
18498
- `payload.data.${field} must reference an existing non-folder ${collectionKey.slice(0, -1)}`,
18680
+ `payload.data.${field} must reference an existing non-folder color`,
18499
18681
  );
18500
18682
  }
18501
18683
  }
@@ -18538,16 +18720,26 @@ const COMMAND_DEFINITIONS = [
18538
18720
  }
18539
18721
  }
18540
18722
 
18541
- 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"]) {
18542
18735
  if (payload.data[field] === undefined) {
18543
18736
  continue;
18544
18737
  }
18545
18738
 
18546
- const collectionKey = field === "fontId" ? "fonts" : "colors";
18547
- const item = state[collectionKey].items[payload.data[field]];
18739
+ const item = state.colors.items[payload.data[field]];
18548
18740
  if (!isPlainObject(item) || item.type === "folder") {
18549
18741
  return invalidPrecondition(
18550
- `payload.data.${field} must reference an existing non-folder ${collectionKey.slice(0, -1)}`,
18742
+ `payload.data.${field} must reference an existing non-folder color`,
18551
18743
  );
18552
18744
  }
18553
18745
  }