@pantoken/scaffold-base 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Danny Wahl
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,31 @@
1
+ # @pantoken/scaffold-base
2
+
3
+ The shared [`bingo-stratum`](https://www.create.bingo/engines/stratum) `Base` every pantoken scaffold
4
+ platform Preset is built from. Provides:
5
+
6
+ - **Common options schema**: `projectName` string with `"pantoken-app"` default.
7
+ - **Shared Blocks**: `blockCssdoc` (writes `cssdoc.json`), `blockWrapperContext` (provides markup derived
8
+ from [`@pantoken/plugin-layouts`](https://www.npmjs.com/package/@pantoken/plugin-layouts)'s `wrapper`
9
+ app-shell layout for platform-specific rendering).
10
+ - **Factory function**: `createPreset()` for type-safe Preset construction with `--isolatedDeclarations`
11
+ compatibility.
12
+
13
+ Used by platform packages (`@pantoken/components`, `@pantoken/react`, `@pantoken/vue`,
14
+ `@pantoken/web-components`) to define their own presets, which are then auto-discovered and
15
+ registered by [`@pantoken/scaffold`](https://www.npmjs.com/package/@pantoken/scaffold).
16
+
17
+ Not meant to be used standalone — see `@pantoken/scaffold` for the full scaffolding workflow.
18
+
19
+ ## API
20
+
21
+ - **`base`** — the shared `bingo-stratum` `Base`, exported for re-use in platform presets.
22
+ - **`createPreset(input)`** — factory function constructing a type-safe `Preset` with platform-specific
23
+ options. Workaround for `--isolatedDeclarations` mode, which requires explicit types on all exports.
24
+ - **`blockCssdoc`** — a `Base`-scoped Block that writes the shared `cssdoc.json` scaffold file.
25
+ - **`getWrapperContext()`** — returns `{ wrapperRootClass, wrapperContainerHtml(depth?), wrapperContainerJsx(depth?) }`,
26
+ derived from the `wrapper` app-shell layout, for use as Handlebars context in a platform's own
27
+ templates.
28
+
29
+ ## License
30
+
31
+ MIT
@@ -0,0 +1,50 @@
1
+ import { Base, BlockWithoutAddons, Preset, Preset as Preset$1 } from "bingo-stratum";
2
+ import { z } from "zod";
3
+ //#region src/base.d.ts
4
+ declare const options: {
5
+ name: z.ZodDefault<z.ZodString>;
6
+ };
7
+ /** Shared options every pantoken scaffold platform Preset takes in. */
8
+ declare const base: Base<typeof options>;
9
+ //#endregion
10
+ //#region src/preset.d.ts
11
+ interface PresetInput {
12
+ name: string;
13
+ base: Base<any>;
14
+ blocks?: any[];
15
+ }
16
+ /**
17
+ * Create a scaffold Preset with explicit return type for TypeScript `--isolatedDeclarations`.
18
+ *
19
+ * @param input - Preset configuration with name, base, and optional blocks
20
+ * @returns Properly typed Bingo Preset
21
+ */
22
+ declare function createPreset(input: PresetInput): Preset$1;
23
+ //#endregion
24
+ //#region src/blocks/cssdoc.d.ts
25
+ /** Writes the shared `cssdoc.json` schema file, unmodified by platform or options. */
26
+ declare const blockCssdoc: BlockWithoutAddons<{
27
+ name: string;
28
+ }>;
29
+ //#endregion
30
+ //#region src/wrapper-context.d.ts
31
+ /**
32
+ * Render the wrapper layout's `.container` part tree as markup, starting at the given indent depth.
33
+ * `format: "html"` covers plain HTML/Angular templates (`class="..."`); `"jsx"` covers React/Next
34
+ * (`className="..."`).
35
+ */
36
+ declare function renderWrapperContainer(format: "html" | "jsx", depth?: number): string;
37
+ /** The wrapper layout's root class name (applied to the real document `<body>` or component root). */
38
+ declare function wrapperRootClassName(): string;
39
+ /**
40
+ * The wrapper layout markup/tokens every platform Preset feeds into its own Handlebars context —
41
+ * `{{projectName}}` inside the rendered markup is left as-is, for the platform's own template to
42
+ * resolve.
43
+ */
44
+ declare function getWrapperContext(): {
45
+ wrapperRootClass: string;
46
+ wrapperContainerHtml: (depth?: number) => string;
47
+ wrapperContainerJsx: (depth?: number) => string;
48
+ };
49
+ //#endregion
50
+ export { type Preset, base, blockCssdoc, createPreset, getWrapperContext, renderWrapperContainer, wrapperRootClassName };
package/dist/index.mjs ADDED
@@ -0,0 +1,196 @@
1
+ import { createBase } from "bingo-stratum";
2
+ import { z } from "zod";
3
+ import { wrapperRules } from "@pantoken/plugin-layouts";
4
+ //#region src/base.ts
5
+ const options = { name: z.string().default("pantoken-app").describe("Name of the scaffolded project") };
6
+ /** Shared options every pantoken scaffold platform Preset takes in. */
7
+ const base = createBase({ options });
8
+ //#endregion
9
+ //#region src/preset.ts
10
+ /**
11
+ * Create a scaffold Preset with explicit return type for TypeScript `--isolatedDeclarations`.
12
+ *
13
+ * @param input - Preset configuration with name, base, and optional blocks
14
+ * @returns Properly typed Bingo Preset
15
+ */
16
+ function createPreset(input) {
17
+ return {
18
+ about: { name: input.name },
19
+ base: input.base,
20
+ blocks: input.blocks ?? []
21
+ };
22
+ }
23
+ //#endregion
24
+ //#region generated/cssdoc.ts
25
+ const CSSDOC_JSON = "{\n \"$schema\": \"https://cssdoc.dev/cssdoc.schema.json\",\n \"extends\": [\"./node_modules/@pantoken/pantoken/cssdoc-base.json\"],\n \"modifierConvention\": \"rscss\",\n \"inlineComments\": \"ignore\",\n // This provider resolves pantoken's documented components/utilities for lint/hover/completions. It\n // assumes the default \"instui-\" class prefix pantoken ships with — if you build your OWN pantoken\n // CSS with a different prefix spelling (e.g. componentsCss({ prefix: \"acme\" })), rewrite it here.\n // `to` is used verbatim — no \"-\" is assumed or added, so match whatever you actually built with:\n // \"prefix\": { \"from\": \"instui-\", \"to\": \"acme-\" }\n // Built unprefixed (componentsCss({ prefix: null })) or with no separator (e.g. \"ialert\" instead of\n // \"instui-alert\")? Set `to` to match exactly — omit it (or use \"\") to strip to a bare class:\n // \"prefix\": { \"from\": \"instui-\", \"to\": \"i\" }\n \"providers\": [{ \"path\": \"./node_modules/@pantoken/pantoken/model.json\" }]\n}\n";
26
+ //#endregion
27
+ //#region src/blocks/cssdoc.ts
28
+ /** Writes the shared `cssdoc.json` schema file, unmodified by platform or options. */
29
+ const blockCssdoc = base.createBlock({
30
+ about: { name: "cssdoc" },
31
+ produce: () => ({ files: { "cssdoc.json": CSSDOC_JSON } })
32
+ });
33
+ //#endregion
34
+ //#region src/wrapper-context.ts
35
+ /**
36
+ * Derives each scaffold platform's entry markup from `@pantoken/plugin-layouts`'s `wrapper` layout,
37
+ * so every preset's template stays structurally in sync with that layout automatically. Called at
38
+ * scaffold-produce-time (not build-time) — every platform preset feeds these values into its own
39
+ * Handlebars context instead of hand-authoring the wrapper markup.
40
+ *
41
+ * @module
42
+ * @alpha
43
+ */
44
+ function extractSelector(raw) {
45
+ const lastSemi = raw.lastIndexOf(";");
46
+ return (lastSemi === -1 ? raw : raw.slice(lastSemi + 1)).trim();
47
+ }
48
+ /** Split CSS text into top-level (brace-depth-aware) rules, ignoring plain declarations. */
49
+ function splitRules(css) {
50
+ const rules = [];
51
+ let depth = 0;
52
+ let selectorStart = 0;
53
+ let bodyStart = -1;
54
+ for (let i = 0; i < css.length; i++) {
55
+ const ch = css[i];
56
+ if (ch === "{") {
57
+ if (depth === 0) bodyStart = i + 1;
58
+ depth++;
59
+ continue;
60
+ }
61
+ if (ch !== "}") continue;
62
+ depth--;
63
+ if (depth !== 0 || bodyStart === -1) continue;
64
+ const selector = extractSelector(css.slice(selectorStart, bodyStart - 1));
65
+ if (selector) rules.push({
66
+ selector,
67
+ body: css.slice(bodyStart, i)
68
+ });
69
+ selectorStart = i + 1;
70
+ bodyStart = -1;
71
+ }
72
+ return rules;
73
+ }
74
+ /** Parse a single selector into a node shape (tag/class/attrs), or `null` for a `&`-scoped state rule. */
75
+ function parseSelector(selector) {
76
+ if (selector.startsWith("@")) return {
77
+ tag: "",
78
+ comment: selector.replace(/^@component\s+/, "")
79
+ };
80
+ if (selector.startsWith("&")) return null;
81
+ const optional = selector.includes(":optional");
82
+ const attrMatch = /^([a-z]*)\[([\w-]+)(?:~|)=(?:"([^"]*)"|'([^']*)')\]/i.exec(selector);
83
+ if (attrMatch) {
84
+ const [, tag, attrName, dq, sq] = attrMatch;
85
+ const value = dq ?? sq ?? "";
86
+ if (attrName === "class") return {
87
+ tag: tag || "div",
88
+ className: value,
89
+ optional
90
+ };
91
+ return {
92
+ tag: tag || "div",
93
+ attrs: { [attrName]: value },
94
+ optional
95
+ };
96
+ }
97
+ const tagMatch = /^[a-z][a-z0-9]*/i.exec(selector);
98
+ const classMatch = /\.([\w-]+)/.exec(selector);
99
+ return {
100
+ tag: tagMatch?.[0] ?? "div",
101
+ className: classMatch?.[1],
102
+ optional
103
+ };
104
+ }
105
+ function parseNodes(css) {
106
+ const nodes = [];
107
+ for (const { selector, body } of splitRules(css)) for (const single of selector.split(",")) {
108
+ const parsed = parseSelector(single.trim());
109
+ if (!parsed) continue;
110
+ nodes.push({
111
+ ...parsed,
112
+ children: parseNodes(body)
113
+ });
114
+ }
115
+ return nodes;
116
+ }
117
+ /** Strip the leading cssdoc block comment so it isn't parsed as a rule body. */
118
+ function stripLeadingDocComment(css) {
119
+ const end = css.indexOf("*/");
120
+ return end === -1 ? css : css.slice(end + 2);
121
+ }
122
+ /** The wrapper layout's root class name and its `.container` node (everything the root wraps). */
123
+ function parseWrapperLayout() {
124
+ const [root] = parseNodes(stripLeadingDocComment(wrapperRules()));
125
+ if (!root) throw new Error("wrapper layout: no root rule found");
126
+ const container = root.children.find((n) => n.className === "container");
127
+ if (!container) throw new Error("wrapper layout: no .container part found");
128
+ return {
129
+ rootClassName: root.className ?? "",
130
+ container
131
+ };
132
+ }
133
+ /** Placeholder text injected at specific parts, keyed by class name. */
134
+ const PART_TEXT = {
135
+ title: "{{projectName}}",
136
+ description: "Styled with <code>@pantoken/components</code> and laid out with the <code>wrapper</code> layout from <code>@pantoken/plugin-layouts</code>."
137
+ };
138
+ /** The `slot[name="content"]` part gets the platform's actual getting-started content, per format. */
139
+ const MAIN_CONTENT = {
140
+ html: "<p>Get started building with pantoken.</p>\n<button class=\"instui-button -color-primary\" type=\"button\">Get started</button>",
141
+ jsx: "<p>Get started building with pantoken.</p>\n<button className=\"instui-button -color-primary\" type=\"button\">Get started</button>"
142
+ };
143
+ function attrName(format) {
144
+ return format === "jsx" ? "className" : "class";
145
+ }
146
+ function comment(format, text) {
147
+ return format === "jsx" ? `{/* ${text} */}` : `<!-- ${text} -->`;
148
+ }
149
+ function renderNode(node, format, depth) {
150
+ const pad = " ".repeat(depth);
151
+ if (node.comment) return `${pad}${comment(format, node.comment)}`;
152
+ const attrs = [];
153
+ if (node.className) attrs.push(`${attrName(format)}="${node.className}"`);
154
+ for (const [name, value] of Object.entries(node.attrs ?? {})) attrs.push(`${name}="${value}"`);
155
+ const openTag = `<${node.tag}${attrs.length ? ` ${attrs.join(" ")}` : ""}>`;
156
+ const closeTag = `</${node.tag}>`;
157
+ const optionalNote = node.optional ? ` ${comment(format, "optional")}` : "";
158
+ const isMainSlot = node.tag === "slot" && node.attrs?.name === "content";
159
+ const text = PART_TEXT[node.className ?? ""] ?? (isMainSlot ? MAIN_CONTENT[format] : void 0);
160
+ const childLines = node.children.map((child) => renderNode(child, format, depth + 1));
161
+ const bodyPad = " ".repeat(depth + 1);
162
+ const bodyLines = text ? text.split("\n").map((line) => `${bodyPad}${line}`) : childLines;
163
+ if (bodyLines.length === 0) return `${pad}${openTag}${closeTag}${optionalNote}`;
164
+ return [
165
+ `${pad}${openTag}${optionalNote}`,
166
+ ...bodyLines,
167
+ `${pad}${closeTag}`
168
+ ].join("\n");
169
+ }
170
+ /**
171
+ * Render the wrapper layout's `.container` part tree as markup, starting at the given indent depth.
172
+ * `format: "html"` covers plain HTML/Angular templates (`class="..."`); `"jsx"` covers React/Next
173
+ * (`className="..."`).
174
+ */
175
+ function renderWrapperContainer(format, depth = 0) {
176
+ const { container } = parseWrapperLayout();
177
+ return renderNode(container, format, depth);
178
+ }
179
+ /** The wrapper layout's root class name (applied to the real document `<body>` or component root). */
180
+ function wrapperRootClassName() {
181
+ return parseWrapperLayout().rootClassName;
182
+ }
183
+ /**
184
+ * The wrapper layout markup/tokens every platform Preset feeds into its own Handlebars context —
185
+ * `{{projectName}}` inside the rendered markup is left as-is, for the platform's own template to
186
+ * resolve.
187
+ */
188
+ function getWrapperContext() {
189
+ return {
190
+ wrapperRootClass: wrapperRootClassName(),
191
+ wrapperContainerHtml: (depth = 0) => renderWrapperContainer("html", depth),
192
+ wrapperContainerJsx: (depth = 0) => renderWrapperContainer("jsx", depth)
193
+ };
194
+ }
195
+ //#endregion
196
+ export { base, blockCssdoc, createPreset, getWrapperContext, renderWrapperContainer, wrapperRootClassName };
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@pantoken/scaffold-base",
3
+ "version": "0.1.0",
4
+ "description": "Shared bingo-stratum Base for pantoken's scaffold presets: common options, the cssdoc.json block, and wrapper-layout markup context.",
5
+ "homepage": "https://pantoken.iywahl.com",
6
+ "bugs": "https://github.com/thedannywahl/pantoken/issues",
7
+ "license": "MIT",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/thedannywahl/pantoken.git",
11
+ "directory": "packages/scaffold-base"
12
+ },
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "type": "module",
17
+ "sideEffects": false,
18
+ "exports": {
19
+ ".": "./dist/index.mjs",
20
+ "./package.json": "./package.json"
21
+ },
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "dependencies": {
26
+ "@pantoken/plugin-layouts": "0.1.6",
27
+ "bingo-stratum": "^0.5.13",
28
+ "zod": "^3.24.2"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^26.3.0",
32
+ "typescript": "^7.0.2",
33
+ "vite": "npm:@voidzero-dev/vite-plus-core@0.3.0",
34
+ "vite-plus": "0.3.0"
35
+ },
36
+ "scripts": {
37
+ "generate": "node scripts/generate.ts",
38
+ "dev": "vp pack --watch",
39
+ "test": "vp run generate && vp test",
40
+ "check": "vp check"
41
+ }
42
+ }