@feeef.dev/cli 0.2.0

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 (61) hide show
  1. package/LICENSE +6 -0
  2. package/README.md +94 -0
  3. package/dist/bin.js +2752 -0
  4. package/dist/bin.js.map +1 -0
  5. package/dist/index.d.ts +6 -0
  6. package/dist/index.js +2730 -0
  7. package/dist/index.js.map +1 -0
  8. package/package.json +67 -0
  9. package/scaffolds/blank/.cursor/rules/feeef-theme.mdc +34 -0
  10. package/scaffolds/blank/.cursor/rules/filterator.mdc +13 -0
  11. package/scaffolds/blank/.cursor/rules/order-form.mdc +17 -0
  12. package/scaffolds/blank/.cursor/rules/theme-source.mdc +32 -0
  13. package/scaffolds/blank/.cursor/skills/feeef-filterator/SKILL.md +15 -0
  14. package/scaffolds/blank/.cursor/skills/feeef-theme/SKILL.md +56 -0
  15. package/scaffolds/blank/.cursor/skills/feeef-theme/reference.md +73 -0
  16. package/scaffolds/blank/.vscode/extensions.json +3 -0
  17. package/scaffolds/blank/.vscode/settings.json +13 -0
  18. package/scaffolds/blank/AGENTS.md +104 -0
  19. package/scaffolds/blank/README.md +21 -0
  20. package/scaffolds/blank/docs/00-INDEX.md +33 -0
  21. package/scaffolds/blank/docs/03-CUSTOM-COMPONENTS.md +116 -0
  22. package/scaffolds/blank/docs/04-WORKFLOW.md +129 -0
  23. package/scaffolds/blank/docs/05-ANTI-PATTERNS.md +80 -0
  24. package/scaffolds/blank/docs/07-SHARED-COMPONENTS.md +144 -0
  25. package/scaffolds/blank/docs/08-ORDER-FORM.md +376 -0
  26. package/scaffolds/blank/docs/09-THEME-PORTING.md +211 -0
  27. package/scaffolds/blank/docs/10-SCHEMA-LIBRARY.md +127 -0
  28. package/scaffolds/blank/docs/11-PAGES-CHECKOUT-THANKS-PRODUCT.md +81 -0
  29. package/scaffolds/blank/docs/12-DARK-LIGHT-THEME.md +164 -0
  30. package/scaffolds/blank/docs/13-TEMPLATE-I18N.md +320 -0
  31. package/scaffolds/blank/docs/14-FILTERATOR.md +433 -0
  32. package/scaffolds/blank/docs/16-AUTHORING-TS-IMPORTS.md +73 -0
  33. package/scaffolds/blank/feeef.template.json +7 -0
  34. package/scaffolds/blank/locales/README.md +4 -0
  35. package/scaffolds/blank/locales/ar.json +10 -0
  36. package/scaffolds/blank/locales/en.json +10 -0
  37. package/scaffolds/blank/package-lock.json +1975 -0
  38. package/scaffolds/blank/package.json +23 -0
  39. package/scaffolds/blank/pages/home/components/hero.tsx +34 -0
  40. package/scaffolds/blank/pages/home/page.json +3 -0
  41. package/scaffolds/blank/props.json +6 -0
  42. package/scaffolds/blank/schema.ts +49 -0
  43. package/scaffolds/blank/tsconfig.json +24 -0
  44. package/scaffolds/blank/types/feeef-live-scope.d.ts +162 -0
  45. package/scaffolds/blank/types/feeef-template-schema.d.ts +84 -0
  46. package/vendor/template-kit/README.md +64 -0
  47. package/vendor/template-kit/bin/dev.mjs +61 -0
  48. package/vendor/template-kit/bin/feeef-template.mjs +212 -0
  49. package/vendor/template-kit/data/lithium-schema.json +4733 -0
  50. package/vendor/template-kit/lib/build.mjs +377 -0
  51. package/vendor/template-kit/lib/compile-component.mjs +188 -0
  52. package/vendor/template-kit/lib/component-meta.mjs +481 -0
  53. package/vendor/template-kit/lib/library.mjs +168 -0
  54. package/vendor/template-kit/lib/locales.mjs +68 -0
  55. package/vendor/template-kit/lib/paths.mjs +84 -0
  56. package/vendor/template-kit/lib/shared.mjs +268 -0
  57. package/vendor/template-kit/lib/theme-schema.mjs +300 -0
  58. package/vendor/template-kit/lib/unpack.mjs +324 -0
  59. package/vendor/template-kit/lib/validate.mjs +207 -0
  60. package/vendor/template-kit/schemas/component.schema.json +55 -0
  61. package/vendor/template-kit/schemas/template.schema.json +18 -0
@@ -0,0 +1,324 @@
1
+ /**
2
+ * Unpack a Lithium TemplateData JSON blob into a folder-based source tree.
3
+ *
4
+ * Layout:
5
+ * <out>/
6
+ * feeef.template.json
7
+ * props.json
8
+ * pages/<pageId>/page.json
9
+ * pages/<pageId>/components/ (main-only pages — preferred)
10
+ * <name>.tsx | <name>.json | <name>/…
11
+ * pages/<pageId>/sections/<sectionId>/components/ (legacy multi-section)
12
+ * …
13
+ */
14
+
15
+ import fs from "node:fs";
16
+ import path from "node:path";
17
+ import { fileURLToPath } from "node:url";
18
+ import {
19
+ COMPONENT_META_KEYS,
20
+ folderNameForComponent,
21
+ rel,
22
+ } from "./paths.mjs";
23
+ import { formatMetaExport } from "./component-meta.mjs";
24
+
25
+ const SCHEMA_ABS = path.resolve(
26
+ path.dirname(fileURLToPath(import.meta.url)),
27
+ "../schemas/component.schema.json",
28
+ );
29
+
30
+ /**
31
+ * @param {string} filePath
32
+ * @param {unknown} data
33
+ */
34
+ function writeJson(filePath, data) {
35
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
36
+ fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, "utf8");
37
+ }
38
+
39
+ /**
40
+ * @param {string} filePath
41
+ * @param {string} content
42
+ */
43
+ function writeText(filePath, content) {
44
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
45
+ // Normalize trailing newline for clean git diffs.
46
+ const body = content.endsWith("\n") ? content : `${content}\n`;
47
+ fs.writeFileSync(filePath, body, "utf8");
48
+ }
49
+
50
+ /**
51
+ * Light cleanup so VS Code / tsc treat common marketplace helpers kindly.
52
+ * (e.g. local `function t(key, params)` is called with one arg everywhere.)
53
+ * @param {string} code
54
+ */
55
+ function sanitizeComponentSourceForEditor(code) {
56
+ return code.replace(
57
+ /\bfunction\s+t\s*\(\s*([A-Za-z_$][\w$]*)\s*,\s*([A-Za-z_$][\w$]*)\s*\)/g,
58
+ "function t($1, $2?)",
59
+ );
60
+ }
61
+
62
+ /**
63
+ * Write one component (and nested slots/children).
64
+ * Leaf customs → `<name>.tsx`; leaf built-ins → `<name>.json`;
65
+ * nested trees → `<name>/` folder (legacy layout).
66
+ *
67
+ * @param {Record<string, any>} component
68
+ * @param {string} destPath Intended path without extension (`…/components/hero`)
69
+ * @param {number} order
70
+ * @param {{ files: number }} stats
71
+ */
72
+ function writeComponent(component, destPath, order, stats) {
73
+ const children = component.children;
74
+ const hasChildFolders = Array.isArray(children) && children.length > 0;
75
+
76
+ const slots = component.slots;
77
+ const slotEntries =
78
+ slots && typeof slots === "object" && !Array.isArray(slots)
79
+ ? Object.entries(slots)
80
+ : [];
81
+ const hasSlotFolders = slotEntries.some(
82
+ ([, list]) => Array.isArray(list) && list.length > 0,
83
+ );
84
+ const needsFolder = hasChildFolders || hasSlotFolders;
85
+
86
+ const schemaBase = needsFolder ? destPath : path.dirname(destPath);
87
+ /** @type {Record<string, any>} */
88
+ const meta = {
89
+ $schema: path.relative(schemaBase, SCHEMA_ABS).split(path.sep).join("/"),
90
+ order,
91
+ };
92
+ for (const key of COMPONENT_META_KEYS) {
93
+ // Preserve explicit nulls (Dawn/editor blobs often include them).
94
+ if (Object.prototype.hasOwnProperty.call(component, key)) {
95
+ meta[key] = component[key];
96
+ }
97
+ }
98
+ // Preserve unknown top-level keys (forward-compat) except tree/code.
99
+ for (const [key, value] of Object.entries(component)) {
100
+ if (
101
+ key === "code" ||
102
+ key === "children" ||
103
+ key === "slots" ||
104
+ key === "order" ||
105
+ COMPONENT_META_KEYS.includes(key)
106
+ ) {
107
+ continue;
108
+ }
109
+ if (value !== undefined) meta[key] = value;
110
+ }
111
+
112
+ // Nested trees: non-empty → folders; empty/null → keep on meta for round-trip.
113
+ if (Object.prototype.hasOwnProperty.call(component, "children")) {
114
+ if (hasChildFolders) {
115
+ delete meta.children;
116
+ } else {
117
+ meta.children = children ?? null;
118
+ }
119
+ }
120
+
121
+ if (Object.prototype.hasOwnProperty.call(component, "slots")) {
122
+ if (hasSlotFolders) {
123
+ delete meta.slots;
124
+ } else {
125
+ meta.slots = slots ?? null;
126
+ }
127
+ }
128
+
129
+ const hasCode =
130
+ typeof component.code === "string" && component.code.trim().length > 0;
131
+
132
+ if (!needsFolder) {
133
+ // Flat leaf: hero.tsx or hero.json
134
+ if (hasCode) {
135
+ const metaForExport = { ...meta };
136
+ delete metaForExport.$schema;
137
+ const tsx = `${formatMetaExport(metaForExport)}${sanitizeComponentSourceForEditor(component.code)}`;
138
+ writeText(`${destPath}.tsx`, tsx);
139
+ stats.files += 1;
140
+ } else {
141
+ if (Object.prototype.hasOwnProperty.call(component, "code")) {
142
+ meta.code = component.code ?? null;
143
+ }
144
+ writeJson(`${destPath}.json`, meta);
145
+ stats.files += 1;
146
+ }
147
+ return;
148
+ }
149
+
150
+ // Folder layout for nested children/slots
151
+ const dir = destPath;
152
+ fs.mkdirSync(dir, { recursive: true });
153
+
154
+ if (hasCode) {
155
+ const metaForExport = { ...meta };
156
+ delete metaForExport.$schema;
157
+ const tsx = `${formatMetaExport(metaForExport)}${sanitizeComponentSourceForEditor(component.code)}`;
158
+ writeText(path.join(dir, "component.tsx"), tsx);
159
+ stats.files += 1;
160
+ } else {
161
+ if (Object.prototype.hasOwnProperty.call(component, "code")) {
162
+ meta.code = component.code ?? null;
163
+ }
164
+ writeJson(path.join(dir, "component.json"), meta);
165
+ stats.files += 1;
166
+ }
167
+
168
+ if (hasChildFolders) {
169
+ const used = new Set();
170
+ children.forEach((child, i) => {
171
+ if (!child || typeof child !== "object") return;
172
+ const name = folderNameForComponent(child, i, used);
173
+ writeComponent(child, path.join(dir, "children", name), i, stats);
174
+ });
175
+ }
176
+
177
+ if (hasSlotFolders) {
178
+ for (const [slotId, list] of slotEntries) {
179
+ if (!Array.isArray(list) || list.length === 0) {
180
+ continue;
181
+ }
182
+ const used = new Set();
183
+ list.forEach((child, i) => {
184
+ if (!child || typeof child !== "object") return;
185
+ const name = folderNameForComponent(child, i, used);
186
+ writeComponent(
187
+ child,
188
+ path.join(dir, "slots", slotId, name),
189
+ i,
190
+ stats,
191
+ );
192
+ });
193
+ }
194
+ const emptySlots = {};
195
+ let anyEmpty = false;
196
+ for (const [slotId, list] of slotEntries) {
197
+ if (!Array.isArray(list) || list.length === 0) {
198
+ emptySlots[slotId] = Array.isArray(list) ? list : [];
199
+ anyEmpty = true;
200
+ }
201
+ }
202
+ if (anyEmpty) {
203
+ writeJson(path.join(dir, "slots.empty.json"), emptySlots);
204
+ stats.files += 1;
205
+ }
206
+ }
207
+ }
208
+
209
+ /**
210
+ * @param {object} opts
211
+ * @param {string} opts.inputPath Absolute path to data.json
212
+ * @param {string} opts.outDir Absolute output directory
213
+ * @param {boolean} [opts.force] Wipe outDir if it exists
214
+ */
215
+ export function unpackTemplate({ inputPath, outDir, force = false }) {
216
+ if (!fs.existsSync(inputPath)) {
217
+ throw new Error(`Input not found: ${inputPath}`);
218
+ }
219
+
220
+ const raw = fs.readFileSync(inputPath, "utf8");
221
+ /** @type {Record<string, any>} */
222
+ const data = JSON.parse(raw);
223
+
224
+ if (!data.pages || typeof data.pages !== "object") {
225
+ throw new Error("Invalid TemplateData: missing pages");
226
+ }
227
+
228
+ if (fs.existsSync(outDir)) {
229
+ if (!force) {
230
+ throw new Error(
231
+ `Output exists: ${rel(outDir)} (pass --force to overwrite)`,
232
+ );
233
+ }
234
+ fs.rmSync(outDir, { recursive: true, force: true });
235
+ }
236
+
237
+ const pageIds = Object.keys(data.pages);
238
+ const stats = { files: 0, pages: pageIds.length, components: 0 };
239
+
240
+ writeJson(path.join(outDir, "feeef.template.json"), {
241
+ $schema: "../../template-kit/schemas/template.schema.json",
242
+ name: path.basename(outDir),
243
+ version: "0.1.0",
244
+ source: path.relative(outDir, inputPath),
245
+ pages: pageIds,
246
+ });
247
+ stats.files += 1;
248
+
249
+ fs.writeFileSync(
250
+ path.join(outDir, ".gitignore"),
251
+ "# Compiled output — regenerate with `npm run template:build`\ndist/\n",
252
+ "utf8",
253
+ );
254
+
255
+ if (data.props && typeof data.props === "object") {
256
+ writeJson(path.join(outDir, "props.json"), data.props);
257
+ stats.files += 1;
258
+ }
259
+
260
+ for (const pageId of pageIds) {
261
+ const page = data.pages[pageId] || {};
262
+ const pageDir = path.join(outDir, "pages", pageId);
263
+ writeJson(path.join(pageDir, "page.json"), {
264
+ props: page.props && typeof page.props === "object" ? page.props : {},
265
+ });
266
+ stats.files += 1;
267
+
268
+ const sections =
269
+ page.sections && typeof page.sections === "object" ? page.sections : {};
270
+
271
+ // Main-only pages → flat `pages/<id>/components/` (sections optional on disk).
272
+ // Multi-section pages → legacy `sections/<id>/components/`.
273
+ const sectionKeys = Object.keys(sections);
274
+ const nonEmptyKeys = sectionKeys.filter((id) => {
275
+ const list = sections[id]?.components;
276
+ return Array.isArray(list) && list.length > 0;
277
+ });
278
+ const useFlatPageComponents =
279
+ nonEmptyKeys.length === 0
280
+ ? sectionKeys.length === 0 ||
281
+ (sectionKeys.length === 1 && sectionKeys[0] === "main")
282
+ : nonEmptyKeys.length === 1 && nonEmptyKeys[0] === "main";
283
+
284
+ if (useFlatPageComponents) {
285
+ const components = Array.isArray(sections.main?.components)
286
+ ? sections.main.components
287
+ : [];
288
+ const componentsDir = path.join(pageDir, "components");
289
+ fs.mkdirSync(componentsDir, { recursive: true });
290
+ const used = new Set();
291
+ components.forEach((comp, i) => {
292
+ if (!comp || typeof comp !== "object") return;
293
+ const name = folderNameForComponent(comp, i, used);
294
+ writeComponent(comp, path.join(componentsDir, name), i, stats);
295
+ stats.components += 1;
296
+ });
297
+ continue;
298
+ }
299
+
300
+ for (const [sectionId, section] of Object.entries(sections)) {
301
+ const components = Array.isArray(section?.components)
302
+ ? section.components
303
+ : [];
304
+ const sectionDir = path.join(pageDir, "sections", sectionId);
305
+ // Always create the section folder so empty sections round-trip.
306
+ fs.mkdirSync(path.join(sectionDir, "components"), { recursive: true });
307
+
308
+ const used = new Set();
309
+ components.forEach((comp, i) => {
310
+ if (!comp || typeof comp !== "object") return;
311
+ const name = folderNameForComponent(comp, i, used);
312
+ writeComponent(
313
+ comp,
314
+ path.join(sectionDir, "components", name),
315
+ i,
316
+ stats,
317
+ );
318
+ stats.components += 1;
319
+ });
320
+ }
321
+ }
322
+
323
+ return stats;
324
+ }
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Lightweight structural validation for compiled TemplateData.
3
+ * Returns issues (errors + warnings) instead of throwing.
4
+ */
5
+
6
+ /**
7
+ * @typedef {{ level: 'error' | 'warning'; path: string; message: string }} Issue
8
+ */
9
+
10
+ /**
11
+ * @param {any} data
12
+ * @param {{ srcDir?: string }} [_ctx]
13
+ * @returns {Issue[]}
14
+ */
15
+ export function validateTemplateData(data, _ctx = {}) {
16
+ /** @type {Issue[]} */
17
+ const issues = [];
18
+
19
+ if (!data || typeof data !== "object") {
20
+ issues.push({
21
+ level: "error",
22
+ path: "/",
23
+ message: "Root must be an object",
24
+ });
25
+ return issues;
26
+ }
27
+
28
+ if (!data.pages || typeof data.pages !== "object") {
29
+ issues.push({
30
+ level: "error",
31
+ path: "/pages",
32
+ message: "Missing pages object",
33
+ });
34
+ return issues;
35
+ }
36
+
37
+ for (const [pageId, page] of Object.entries(data.pages)) {
38
+ const pagePath = `/pages/${pageId}`;
39
+ if (!page || typeof page !== "object") {
40
+ issues.push({
41
+ level: "error",
42
+ path: pagePath,
43
+ message: "Page must be an object",
44
+ });
45
+ continue;
46
+ }
47
+ if (!page.sections || typeof page.sections !== "object") {
48
+ issues.push({
49
+ level: "error",
50
+ path: `${pagePath}/sections`,
51
+ message: "Page must have sections",
52
+ });
53
+ continue;
54
+ }
55
+ for (const [sectionId, section] of Object.entries(page.sections)) {
56
+ const sectionPath = `${pagePath}/sections/${sectionId}`;
57
+ if (!Array.isArray(section?.components)) {
58
+ issues.push({
59
+ level: "error",
60
+ path: `${sectionPath}/components`,
61
+ message: "Section.components must be an array",
62
+ });
63
+ continue;
64
+ }
65
+ section.components.forEach((c, i) =>
66
+ validateComponent(c, `${sectionPath}/components[${i}]`, issues),
67
+ );
68
+ }
69
+ }
70
+
71
+ return issues;
72
+ }
73
+
74
+ /**
75
+ * @param {any} component
76
+ * @param {string} path
77
+ * @param {Issue[]} issues
78
+ */
79
+ function validateComponent(component, path, issues) {
80
+ if (!component || typeof component !== "object") {
81
+ issues.push({
82
+ level: "error",
83
+ path,
84
+ message: "Component must be an object",
85
+ });
86
+ return;
87
+ }
88
+
89
+ if (typeof component.type !== "string" || !component.type.trim()) {
90
+ issues.push({
91
+ level: "error",
92
+ path: `${path}/type`,
93
+ message: "type is required",
94
+ });
95
+ }
96
+
97
+ if (component.props === undefined || component.props === null) {
98
+ issues.push({
99
+ level: "warning",
100
+ path: `${path}/props`,
101
+ message: "props missing — Lithium usually expects an object",
102
+ });
103
+ } else if (typeof component.props !== "object" || Array.isArray(component.props)) {
104
+ issues.push({
105
+ level: "error",
106
+ path: `${path}/props`,
107
+ message: "props must be an object",
108
+ });
109
+ }
110
+
111
+ if (component.type === "custom") {
112
+ if (
113
+ typeof component.code !== "string" ||
114
+ component.code.trim().length === 0
115
+ ) {
116
+ issues.push({
117
+ level: "error",
118
+ path: `${path}/code`,
119
+ message:
120
+ 'type "custom" requires <name>.tsx or component.tsx with function App() (and export const meta)',
121
+ });
122
+ } else if (!/\bfunction\s+App\s*\(/.test(component.code)) {
123
+ issues.push({
124
+ level: "warning",
125
+ path: `${path}/code`,
126
+ message:
127
+ "react-live expects `function App()` — code may not render",
128
+ });
129
+ }
130
+ }
131
+
132
+ const slotSchemaKeys =
133
+ component.slotsSchema && typeof component.slotsSchema === "object"
134
+ ? Object.keys(component.slotsSchema)
135
+ : [];
136
+
137
+ if (component.slots && typeof component.slots === "object") {
138
+ for (const [slotId, list] of Object.entries(component.slots)) {
139
+ if (slotSchemaKeys.length > 0 && !slotSchemaKeys.includes(slotId)) {
140
+ issues.push({
141
+ level: "warning",
142
+ path: `${path}/slots/${slotId}`,
143
+ message: `slot "${slotId}" not declared in slotsSchema`,
144
+ });
145
+ }
146
+ if (!Array.isArray(list)) {
147
+ issues.push({
148
+ level: "error",
149
+ path: `${path}/slots/${slotId}`,
150
+ message: "slot contents must be an array",
151
+ });
152
+ continue;
153
+ }
154
+ list.forEach((c, i) =>
155
+ validateComponent(c, `${path}/slots/${slotId}[${i}]`, issues),
156
+ );
157
+ }
158
+ }
159
+
160
+ if (component.slotsLayout && typeof component.slotsLayout === "object") {
161
+ const leaves = collectLayoutLeaves(component.slotsLayout);
162
+ for (const leaf of leaves) {
163
+ if (slotSchemaKeys.length > 0 && !slotSchemaKeys.includes(leaf)) {
164
+ issues.push({
165
+ level: "warning",
166
+ path: `${path}/slotsLayout`,
167
+ message: `slotsLayout leaf "${leaf}" is not a slotsSchema key`,
168
+ });
169
+ }
170
+ }
171
+ }
172
+
173
+ if (Array.isArray(component.children)) {
174
+ component.children.forEach((c, i) =>
175
+ validateComponent(c, `${path}/children[${i}]`, issues),
176
+ );
177
+ }
178
+ }
179
+
180
+ /**
181
+ * @param {any} node
182
+ * @returns {string[]}
183
+ */
184
+ function collectLayoutLeaves(node) {
185
+ /** @type {string[]} */
186
+ const out = [];
187
+ /** @param {any} n */
188
+ function walk(n) {
189
+ if (n == null) return;
190
+ if (typeof n === "string") {
191
+ out.push(n);
192
+ return;
193
+ }
194
+ if (Array.isArray(n)) {
195
+ n.forEach(walk);
196
+ return;
197
+ }
198
+ if (typeof n === "object") {
199
+ if (Array.isArray(n.children)) n.children.forEach(walk);
200
+ else {
201
+ for (const v of Object.values(n)) walk(v);
202
+ }
203
+ }
204
+ }
205
+ walk(node);
206
+ return out;
207
+ }
@@ -0,0 +1,55 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://feeef.org/schemas/template-component.json",
4
+ "title": "Feeef Template Component",
5
+ "description": "Metadata for one component instance. Prefer flat <name>.tsx (export const meta + App); legacy folder uses component.tsx / component.json.",
6
+ "type": "object",
7
+ "required": ["type"],
8
+ "properties": {
9
+ "$schema": { "type": "string" },
10
+ "order": {
11
+ "type": "integer",
12
+ "minimum": 0,
13
+ "description": "Sibling sort order within the parent components/slots/children folder."
14
+ },
15
+ "type": {
16
+ "type": "string",
17
+ "description": "Registry type, or custom / reference / grid / flex / container."
18
+ },
19
+ "instanceId": { "type": ["string", "null"] },
20
+ "title": { "type": ["string", "null"] },
21
+ "props": {
22
+ "type": "object",
23
+ "additionalProperties": true
24
+ },
25
+ "propsSchema": {
26
+ "type": "object",
27
+ "additionalProperties": true
28
+ },
29
+ "slotsSchema": {
30
+ "type": "object",
31
+ "additionalProperties": {
32
+ "type": "object",
33
+ "properties": {
34
+ "name": { "type": ["string", "null"] },
35
+ "maxChildren": { "type": ["integer", "null"] }
36
+ }
37
+ }
38
+ },
39
+ "slotsLayout": {
40
+ "type": "object",
41
+ "additionalProperties": true
42
+ },
43
+ "refId": { "type": "string" },
44
+ "refVersion": { "type": "number" },
45
+ "$ref": {
46
+ "type": "string",
47
+ "description": "Compile-time shared placement, e.g. \"shared.footer\". Expanded by template-kit build."
48
+ },
49
+ "shared": {
50
+ "type": "string",
51
+ "description": "Shorthand for $ref shared.<id> (e.g. \"footer\")."
52
+ }
53
+ },
54
+ "additionalProperties": true
55
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://feeef.org/schemas/feeef-template.json",
4
+ "title": "Feeef Template Manifest",
5
+ "type": "object",
6
+ "properties": {
7
+ "$schema": { "type": "string" },
8
+ "name": { "type": "string" },
9
+ "version": { "type": "string" },
10
+ "source": { "type": "string" },
11
+ "pages": {
12
+ "type": "array",
13
+ "items": { "type": "string" },
14
+ "description": "Page id order for the compiled data.json"
15
+ }
16
+ },
17
+ "additionalProperties": true
18
+ }