@srcroot/ui 0.0.64 → 0.0.65
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.js +117 -90
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -5,18 +5,43 @@ import { Command } from "commander";
|
|
|
5
5
|
import chalk3 from "chalk";
|
|
6
6
|
|
|
7
7
|
// src/cli/services/project-initializer.ts
|
|
8
|
-
import
|
|
9
|
-
import
|
|
8
|
+
import fs5 from "fs-extra";
|
|
9
|
+
import path5 from "path";
|
|
10
10
|
import ora from "ora";
|
|
11
11
|
import prompts from "prompts";
|
|
12
|
-
import { fileURLToPath as
|
|
12
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
13
13
|
import { execa } from "execa";
|
|
14
14
|
|
|
15
15
|
// src/cli/services/theme-service.ts
|
|
16
|
-
import
|
|
16
|
+
import fs2 from "fs-extra";
|
|
17
|
+
import path2 from "path";
|
|
18
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
19
|
+
|
|
20
|
+
// src/cli/utils/get-registry-path.ts
|
|
17
21
|
import path from "path";
|
|
22
|
+
import fs from "fs-extra";
|
|
18
23
|
import { fileURLToPath } from "url";
|
|
19
|
-
|
|
24
|
+
function getRegistryPath() {
|
|
25
|
+
const __filename2 = fileURLToPath(import.meta.url);
|
|
26
|
+
const __dirname5 = path.dirname(__filename2);
|
|
27
|
+
const pathsToCheck = [
|
|
28
|
+
path.resolve(__dirname5, "..", "src", "registry"),
|
|
29
|
+
// Production case
|
|
30
|
+
path.resolve(__dirname5, "..", "..", "registry"),
|
|
31
|
+
// Development case
|
|
32
|
+
path.resolve(process.cwd(), "src", "registry")
|
|
33
|
+
// Fallback/CWD case
|
|
34
|
+
];
|
|
35
|
+
for (const p of pathsToCheck) {
|
|
36
|
+
if (fs.existsSync(p)) {
|
|
37
|
+
return p;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return path.resolve(__dirname5, "..", "src", "registry");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// src/cli/services/theme-service.ts
|
|
44
|
+
var __dirname2 = path2.dirname(fileURLToPath2(import.meta.url));
|
|
20
45
|
var THEME_METADATA = {
|
|
21
46
|
slate: { name: "Slate", description: "Cool gray with strong blue undertones (default)" },
|
|
22
47
|
neutral: { name: "Neutral", description: "Pure gray, no undertones" },
|
|
@@ -33,18 +58,18 @@ var THEME_METADATA = {
|
|
|
33
58
|
var ThemeService = class {
|
|
34
59
|
registryThemesPath;
|
|
35
60
|
constructor() {
|
|
36
|
-
this.registryThemesPath =
|
|
61
|
+
this.registryThemesPath = path2.join(getRegistryPath(), "themes");
|
|
37
62
|
}
|
|
38
63
|
/**
|
|
39
64
|
* Get list of available themes from registry/themes/v3/*.css (v3 as primary source)
|
|
40
65
|
*/
|
|
41
66
|
getAvailableThemes() {
|
|
42
67
|
const themes = [];
|
|
43
|
-
const v3Path =
|
|
44
|
-
if (!
|
|
68
|
+
const v3Path = path2.join(this.registryThemesPath, "v3");
|
|
69
|
+
if (!fs2.existsSync(v3Path)) {
|
|
45
70
|
return themes;
|
|
46
71
|
}
|
|
47
|
-
const files =
|
|
72
|
+
const files = fs2.readdirSync(v3Path);
|
|
48
73
|
for (const file of files) {
|
|
49
74
|
if (file.endsWith(".css")) {
|
|
50
75
|
const themeName = file.replace(".css", "");
|
|
@@ -66,11 +91,11 @@ var ThemeService = class {
|
|
|
66
91
|
*/
|
|
67
92
|
async getThemeCss(themeName, isTailwind4) {
|
|
68
93
|
const versionFolder = isTailwind4 ? "v4" : "v3";
|
|
69
|
-
const themeFilePath =
|
|
70
|
-
if (!
|
|
94
|
+
const themeFilePath = path2.join(this.registryThemesPath, versionFolder, `${themeName}.css`);
|
|
95
|
+
if (!fs2.existsSync(themeFilePath)) {
|
|
71
96
|
throw new Error(`Theme file not found: ${themeFilePath}`);
|
|
72
97
|
}
|
|
73
|
-
const content = await
|
|
98
|
+
const content = await fs2.readFile(themeFilePath, "utf-8");
|
|
74
99
|
return content;
|
|
75
100
|
}
|
|
76
101
|
};
|
|
@@ -150,14 +175,14 @@ export default config
|
|
|
150
175
|
`;
|
|
151
176
|
|
|
152
177
|
// src/cli/utils/get-package-manager.ts
|
|
153
|
-
import
|
|
154
|
-
import
|
|
178
|
+
import fs3 from "fs";
|
|
179
|
+
import path3 from "path";
|
|
155
180
|
function getPackageManager(cwd) {
|
|
156
181
|
const dir = cwd || process.cwd();
|
|
157
|
-
if (
|
|
158
|
-
if (
|
|
159
|
-
if (
|
|
160
|
-
if (
|
|
182
|
+
if (fs3.existsSync(path3.join(dir, "bun.lockb"))) return "bun";
|
|
183
|
+
if (fs3.existsSync(path3.join(dir, "pnpm-lock.yaml"))) return "pnpm";
|
|
184
|
+
if (fs3.existsSync(path3.join(dir, "yarn.lock"))) return "yarn";
|
|
185
|
+
if (fs3.existsSync(path3.join(dir, "package-lock.json"))) return "npm";
|
|
161
186
|
const userAgent = process.env.npm_config_user_agent;
|
|
162
187
|
if (userAgent) {
|
|
163
188
|
if (userAgent.startsWith("yarn")) return "yarn";
|
|
@@ -168,19 +193,19 @@ function getPackageManager(cwd) {
|
|
|
168
193
|
}
|
|
169
194
|
|
|
170
195
|
// src/cli/utils/get-package-info.ts
|
|
171
|
-
import
|
|
172
|
-
import
|
|
173
|
-
import { fileURLToPath as
|
|
196
|
+
import path4 from "path";
|
|
197
|
+
import fs4 from "fs-extra";
|
|
198
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
174
199
|
function getPackageInfo() {
|
|
175
|
-
const __filename2 =
|
|
176
|
-
const __dirname5 =
|
|
200
|
+
const __filename2 = fileURLToPath3(import.meta.url);
|
|
201
|
+
const __dirname5 = path4.dirname(__filename2);
|
|
177
202
|
const pathsToCheck = [
|
|
178
|
-
|
|
179
|
-
|
|
203
|
+
path4.resolve(__dirname5, "..", "package.json"),
|
|
204
|
+
path4.resolve(__dirname5, "..", "..", "..", "package.json")
|
|
180
205
|
];
|
|
181
206
|
for (const pkgPath of pathsToCheck) {
|
|
182
|
-
if (
|
|
183
|
-
return
|
|
207
|
+
if (fs4.existsSync(pkgPath)) {
|
|
208
|
+
return fs4.readJSONSync(pkgPath);
|
|
184
209
|
}
|
|
185
210
|
}
|
|
186
211
|
return { version: "0.0.0" };
|
|
@@ -204,7 +229,7 @@ var logger = {
|
|
|
204
229
|
};
|
|
205
230
|
|
|
206
231
|
// src/cli/services/project-initializer.ts
|
|
207
|
-
var __dirname3 =
|
|
232
|
+
var __dirname3 = path5.dirname(fileURLToPath4(import.meta.url));
|
|
208
233
|
var ProjectInitializer = class {
|
|
209
234
|
options;
|
|
210
235
|
config = {};
|
|
@@ -223,13 +248,13 @@ var ProjectInitializer = class {
|
|
|
223
248
|
this.printSuccess();
|
|
224
249
|
}
|
|
225
250
|
async validateEnvironment() {
|
|
226
|
-
const cwd =
|
|
227
|
-
const packageJsonPath =
|
|
228
|
-
if (!
|
|
251
|
+
const cwd = path5.resolve(this.options.cwd);
|
|
252
|
+
const packageJsonPath = path5.join(cwd, "package.json");
|
|
253
|
+
if (!fs5.existsSync(packageJsonPath)) {
|
|
229
254
|
logger.error("Error: No package.json found. Please run this in a project directory.");
|
|
230
255
|
process.exit(1);
|
|
231
256
|
}
|
|
232
|
-
const pkg = await
|
|
257
|
+
const pkg = await fs5.readJson(packageJsonPath);
|
|
233
258
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
234
259
|
if (!allDeps["react"]) {
|
|
235
260
|
logger.error("Error: React not found in dependencies. Please initialize this in a React project.");
|
|
@@ -237,33 +262,33 @@ var ProjectInitializer = class {
|
|
|
237
262
|
}
|
|
238
263
|
}
|
|
239
264
|
async detectConfiguration() {
|
|
240
|
-
const cwd =
|
|
265
|
+
const cwd = path5.resolve(this.options.cwd);
|
|
241
266
|
const packageManager = getPackageManager(cwd);
|
|
242
267
|
const installCmd = packageManager === "npm" ? "install" : "add";
|
|
243
|
-
const pkg = await
|
|
268
|
+
const pkg = await fs5.readJson(path5.join(cwd, "package.json"));
|
|
244
269
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
245
270
|
const tailwindVersion = allDeps["tailwindcss"] || "";
|
|
246
271
|
const isTailwind4 = tailwindVersion.includes("^4") || tailwindVersion.startsWith("4") || allDeps["@tailwindcss/postcss"];
|
|
247
|
-
const hasSrc =
|
|
248
|
-
const srcPath = hasSrc ?
|
|
249
|
-
const appPath =
|
|
250
|
-
const pagesPath =
|
|
251
|
-
const hasAppDir =
|
|
252
|
-
const hasPagesDir =
|
|
253
|
-
const libDir =
|
|
254
|
-
const componentsDir =
|
|
272
|
+
const hasSrc = fs5.existsSync(path5.join(cwd, "src"));
|
|
273
|
+
const srcPath = hasSrc ? path5.join(cwd, "src") : cwd;
|
|
274
|
+
const appPath = path5.join(srcPath, "app");
|
|
275
|
+
const pagesPath = path5.join(srcPath, "pages");
|
|
276
|
+
const hasAppDir = fs5.existsSync(appPath);
|
|
277
|
+
const hasPagesDir = fs5.existsSync(pagesPath);
|
|
278
|
+
const libDir = path5.join(srcPath, "lib");
|
|
279
|
+
const componentsDir = path5.join(srcPath, "components", "ui");
|
|
255
280
|
let globalsPath = "";
|
|
256
281
|
if (hasAppDir) {
|
|
257
|
-
if (
|
|
258
|
-
else if (
|
|
259
|
-
else globalsPath =
|
|
282
|
+
if (fs5.existsSync(path5.join(appPath, "globals.css"))) globalsPath = path5.join(appPath, "globals.css");
|
|
283
|
+
else if (fs5.existsSync(path5.join(appPath, "global.css"))) globalsPath = path5.join(appPath, "global.css");
|
|
284
|
+
else globalsPath = path5.join(appPath, "globals.css");
|
|
260
285
|
} else if (hasPagesDir) {
|
|
261
|
-
const stylesPath =
|
|
262
|
-
if (
|
|
263
|
-
else if (
|
|
264
|
-
else globalsPath =
|
|
286
|
+
const stylesPath = path5.join(srcPath, "styles");
|
|
287
|
+
if (fs5.existsSync(path5.join(stylesPath, "globals.css"))) globalsPath = path5.join(stylesPath, "globals.css");
|
|
288
|
+
else if (fs5.existsSync(path5.join(stylesPath, "global.css"))) globalsPath = path5.join(stylesPath, "global.css");
|
|
289
|
+
else globalsPath = path5.join(stylesPath, "globals.css");
|
|
265
290
|
} else {
|
|
266
|
-
globalsPath =
|
|
291
|
+
globalsPath = path5.join(srcPath, "globals.css");
|
|
267
292
|
}
|
|
268
293
|
this.config = {
|
|
269
294
|
cwd,
|
|
@@ -311,13 +336,13 @@ var ProjectInitializer = class {
|
|
|
311
336
|
const spinner = ora("Creating project structure...").start();
|
|
312
337
|
const cfg = this.config;
|
|
313
338
|
try {
|
|
314
|
-
await
|
|
315
|
-
await
|
|
316
|
-
const utilsPath =
|
|
317
|
-
const registryUtilsPath =
|
|
339
|
+
await fs5.ensureDir(cfg.libDir);
|
|
340
|
+
await fs5.ensureDir(cfg.componentsDir);
|
|
341
|
+
const utilsPath = path5.join(cfg.libDir, "utils.ts");
|
|
342
|
+
const registryUtilsPath = path5.join(getRegistryPath(), "lib", "utils.ts");
|
|
318
343
|
let utilsContent = "";
|
|
319
|
-
if (
|
|
320
|
-
utilsContent = await
|
|
344
|
+
if (fs5.existsSync(registryUtilsPath)) {
|
|
345
|
+
utilsContent = await fs5.readFile(registryUtilsPath, "utf-8");
|
|
321
346
|
} else {
|
|
322
347
|
utilsContent = `import { type ClassValue, clsx } from "clsx"
|
|
323
348
|
import { twMerge } from "tailwind-merge"
|
|
@@ -328,15 +353,15 @@ export function cn(...inputs: ClassValue[]) {
|
|
|
328
353
|
`;
|
|
329
354
|
spinner.warn(`Could not find registry/utils.ts, using fallback content.`);
|
|
330
355
|
}
|
|
331
|
-
await
|
|
332
|
-
spinner.succeed(`Created ${
|
|
356
|
+
await fs5.writeFile(utilsPath, utilsContent);
|
|
357
|
+
spinner.succeed(`Created ${path5.relative(cfg.cwd, utilsPath)}`);
|
|
333
358
|
spinner.start(`Setting up ${cfg.selectedTheme} theme...`);
|
|
334
|
-
const stylesDir =
|
|
335
|
-
await
|
|
359
|
+
const stylesDir = path5.dirname(cfg.globalsPath);
|
|
360
|
+
await fs5.ensureDir(stylesDir);
|
|
336
361
|
try {
|
|
337
362
|
const cssContent = await this.themeService.getThemeCss(cfg.selectedTheme, cfg.isTailwind4);
|
|
338
|
-
await
|
|
339
|
-
spinner.succeed(`Updated ${
|
|
363
|
+
await fs5.writeFile(cfg.globalsPath, cssContent);
|
|
364
|
+
spinner.succeed(`Updated ${path5.relative(cfg.cwd, cfg.globalsPath)} with ${cfg.selectedTheme} theme (${cfg.isTailwind4 ? "Tailwind 4" : "Tailwind 3"})`);
|
|
340
365
|
} catch (error) {
|
|
341
366
|
spinner.fail(`Failed to load theme: ${cfg.selectedTheme}`);
|
|
342
367
|
console.error(error);
|
|
@@ -344,8 +369,8 @@ export function cn(...inputs: ClassValue[]) {
|
|
|
344
369
|
}
|
|
345
370
|
if (!cfg.isTailwind4) {
|
|
346
371
|
spinner.start("Setting up Tailwind config...");
|
|
347
|
-
const tailwindConfigPath =
|
|
348
|
-
await
|
|
372
|
+
const tailwindConfigPath = path5.join(cfg.cwd, "tailwind.config.ts");
|
|
373
|
+
await fs5.writeFile(tailwindConfigPath, TAILWIND_CONFIG);
|
|
349
374
|
spinner.succeed(`Created tailwind.config.ts`);
|
|
350
375
|
}
|
|
351
376
|
const packageInfo = getPackageInfo();
|
|
@@ -353,11 +378,11 @@ export function cn(...inputs: ClassValue[]) {
|
|
|
353
378
|
version: packageInfo.version || "0.0.0",
|
|
354
379
|
theme: cfg.selectedTheme,
|
|
355
380
|
paths: {
|
|
356
|
-
components:
|
|
357
|
-
utils:
|
|
381
|
+
components: path5.relative(cfg.cwd, cfg.componentsDir),
|
|
382
|
+
utils: path5.relative(cfg.cwd, utilsPath)
|
|
358
383
|
}
|
|
359
384
|
};
|
|
360
|
-
await
|
|
385
|
+
await fs5.writeJSON(path5.join(cfg.cwd, "srcroot.config.json"), configObj, { spaces: 2 });
|
|
361
386
|
spinner.succeed("Created srcroot.config.json");
|
|
362
387
|
} catch (error) {
|
|
363
388
|
spinner.fail("Failed to initialize project");
|
|
@@ -378,8 +403,8 @@ export function cn(...inputs: ClassValue[]) {
|
|
|
378
403
|
deps.push("tailwindcss-animate");
|
|
379
404
|
}
|
|
380
405
|
try {
|
|
381
|
-
const packageJsonPath =
|
|
382
|
-
const pkg = await
|
|
406
|
+
const packageJsonPath = path5.join(cfg.cwd, "package.json");
|
|
407
|
+
const pkg = await fs5.readJson(packageJsonPath);
|
|
383
408
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
384
409
|
const missingDeps = deps.filter((dep) => !allDeps[dep]);
|
|
385
410
|
if (missingDeps.length === 0) {
|
|
@@ -414,15 +439,15 @@ async function init(options) {
|
|
|
414
439
|
}
|
|
415
440
|
|
|
416
441
|
// src/cli/commands/add.ts
|
|
417
|
-
import
|
|
442
|
+
import path7 from "path";
|
|
418
443
|
|
|
419
444
|
// src/cli/services/component-adder.ts
|
|
420
|
-
import
|
|
421
|
-
import
|
|
445
|
+
import fs6 from "fs-extra";
|
|
446
|
+
import path6 from "path";
|
|
422
447
|
import ora2 from "ora";
|
|
423
448
|
import prompts2 from "prompts";
|
|
424
449
|
import { execa as execa2 } from "execa";
|
|
425
|
-
import { fileURLToPath as
|
|
450
|
+
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
426
451
|
|
|
427
452
|
// src/cli/registry.ts
|
|
428
453
|
var REGISTRY = {
|
|
@@ -898,7 +923,7 @@ var REGISTRY = {
|
|
|
898
923
|
};
|
|
899
924
|
|
|
900
925
|
// src/cli/services/component-adder.ts
|
|
901
|
-
var __dirname4 =
|
|
926
|
+
var __dirname4 = path6.dirname(fileURLToPath5(import.meta.url));
|
|
902
927
|
var ComponentAdder = class {
|
|
903
928
|
cwd;
|
|
904
929
|
options;
|
|
@@ -1013,20 +1038,20 @@ Please manually install: ${packages.join(" ")}`);
|
|
|
1013
1038
|
}
|
|
1014
1039
|
async copyComponents(components) {
|
|
1015
1040
|
const spinner = ora2("Adding components...").start();
|
|
1016
|
-
const hasSrc =
|
|
1017
|
-
const srcPath = hasSrc ?
|
|
1018
|
-
const componentsDir =
|
|
1041
|
+
const hasSrc = fs6.existsSync(path6.join(this.cwd, "src"));
|
|
1042
|
+
const srcPath = hasSrc ? path6.join(this.cwd, "src") : this.cwd;
|
|
1043
|
+
const componentsDir = path6.join(srcPath, "components", "ui");
|
|
1019
1044
|
try {
|
|
1020
|
-
await
|
|
1045
|
+
await fs6.ensureDir(componentsDir);
|
|
1021
1046
|
let overwriteAll = false;
|
|
1022
1047
|
let skipAll = false;
|
|
1023
1048
|
if (!this.options.overwrite) {
|
|
1024
1049
|
const conflicts = [];
|
|
1025
1050
|
for (const name of components) {
|
|
1026
1051
|
const comp = REGISTRY[name];
|
|
1027
|
-
const fileName =
|
|
1028
|
-
const targetPath =
|
|
1029
|
-
if (
|
|
1052
|
+
const fileName = path6.basename(comp.file);
|
|
1053
|
+
const targetPath = path6.join(componentsDir, fileName);
|
|
1054
|
+
if (fs6.existsSync(targetPath)) {
|
|
1030
1055
|
conflicts.push(fileName);
|
|
1031
1056
|
}
|
|
1032
1057
|
}
|
|
@@ -1055,11 +1080,12 @@ Please manually install: ${packages.join(" ")}`);
|
|
|
1055
1080
|
}
|
|
1056
1081
|
spinner.start("Adding components...");
|
|
1057
1082
|
}
|
|
1083
|
+
let addedCount = 0;
|
|
1058
1084
|
for (const name of components) {
|
|
1059
1085
|
const comp = REGISTRY[name];
|
|
1060
|
-
const fileName =
|
|
1061
|
-
const targetPath =
|
|
1062
|
-
if (
|
|
1086
|
+
const fileName = path6.basename(comp.file);
|
|
1087
|
+
const targetPath = path6.join(componentsDir, fileName);
|
|
1088
|
+
if (fs6.existsSync(targetPath) && !this.options.overwrite && !overwriteAll) {
|
|
1063
1089
|
if (skipAll) {
|
|
1064
1090
|
spinner.info(`Skipped ${fileName}`);
|
|
1065
1091
|
continue;
|
|
@@ -1078,13 +1104,14 @@ Please manually install: ${packages.join(" ")}`);
|
|
|
1078
1104
|
}
|
|
1079
1105
|
spinner.start("Adding components...");
|
|
1080
1106
|
}
|
|
1081
|
-
const registryPath =
|
|
1082
|
-
if (!
|
|
1107
|
+
const registryPath = path6.resolve(getRegistryPath(), comp.file);
|
|
1108
|
+
if (!fs6.existsSync(registryPath)) {
|
|
1083
1109
|
spinner.warn(`Registry file not found for ${name}: ${registryPath}`);
|
|
1084
1110
|
continue;
|
|
1085
1111
|
}
|
|
1086
|
-
const content = await
|
|
1087
|
-
await
|
|
1112
|
+
const content = await fs6.readFile(registryPath, "utf-8");
|
|
1113
|
+
await fs6.writeFile(targetPath, content);
|
|
1114
|
+
addedCount++;
|
|
1088
1115
|
if (components.length > 10) {
|
|
1089
1116
|
spinner.text = `Adding ${fileName}...`;
|
|
1090
1117
|
} else {
|
|
@@ -1092,7 +1119,7 @@ Please manually install: ${packages.join(" ")}`);
|
|
|
1092
1119
|
}
|
|
1093
1120
|
}
|
|
1094
1121
|
if (components.length > 10) {
|
|
1095
|
-
spinner.succeed(`Added ${
|
|
1122
|
+
spinner.succeed(`Added ${addedCount} components`);
|
|
1096
1123
|
}
|
|
1097
1124
|
} catch (error) {
|
|
1098
1125
|
spinner.fail("Failed to add components");
|
|
@@ -1104,7 +1131,7 @@ Please manually install: ${packages.join(" ")}`);
|
|
|
1104
1131
|
|
|
1105
1132
|
// src/cli/commands/add.ts
|
|
1106
1133
|
async function add(components, options) {
|
|
1107
|
-
const cwd =
|
|
1134
|
+
const cwd = path7.resolve(options.cwd);
|
|
1108
1135
|
const adder = new ComponentAdder(cwd, options);
|
|
1109
1136
|
await adder.add(components);
|
|
1110
1137
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@srcroot/ui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.65",
|
|
4
4
|
"description": "A UI library with polymorphic, accessible React components",
|
|
5
5
|
"author": "Shifaul Islam",
|
|
6
6
|
"license": "MIT",
|
|
@@ -71,4 +71,4 @@
|
|
|
71
71
|
"optional": true
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
|
-
}
|
|
74
|
+
}
|