ultimate-jekyll-manager 1.7.2 → 1.8.1
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/CHANGELOG.md +69 -1
- package/CLAUDE.md +36 -15
- package/README.md +4 -2
- package/TODO-AUTH-TESTING.md +1 -1
- package/dist/assets/themes/newsflash/README.md +58 -0
- package/dist/assets/themes/newsflash/_config.scss +138 -0
- package/dist/assets/themes/newsflash/_theme.js +27 -0
- package/dist/assets/themes/newsflash/_theme.scss +37 -0
- package/dist/assets/themes/newsflash/css/base/_mixins.scss +50 -0
- package/dist/assets/themes/newsflash/css/base/_root.scss +134 -0
- package/dist/assets/themes/newsflash/css/base/_typography.scss +49 -0
- package/dist/assets/themes/newsflash/css/base/_utilities.scss +58 -0
- package/dist/assets/themes/newsflash/css/components/_badges.scss +65 -0
- package/dist/assets/themes/newsflash/css/components/_buttons.scss +139 -0
- package/dist/assets/themes/newsflash/css/components/_cards.scss +52 -0
- package/dist/assets/themes/newsflash/css/components/_editorial.scss +182 -0
- package/dist/assets/themes/newsflash/css/components/_forms.scss +75 -0
- package/dist/assets/themes/newsflash/css/components/_infinite-scroll.scss +102 -0
- package/dist/assets/themes/newsflash/css/components/_panels.scss +91 -0
- package/dist/assets/themes/newsflash/css/components/_ticker.scss +70 -0
- package/dist/assets/themes/newsflash/css/layout/_general.scss +264 -0
- package/dist/assets/themes/newsflash/css/layout/_navigation.scss +164 -0
- package/dist/assets/themes/newsflash/js/initialize-tooltips.js +20 -0
- package/dist/assets/themes/newsflash/js/masthead-scroll.js +29 -0
- package/dist/assets/themes/newsflash/pages/404/index.scss +27 -0
- package/dist/assets/themes/newsflash/pages/about/index.scss +70 -0
- package/dist/assets/themes/newsflash/pages/blog/index.scss +17 -0
- package/dist/assets/themes/newsflash/pages/blog/post.js +29 -0
- package/dist/assets/themes/newsflash/pages/blog/post.scss +164 -0
- package/dist/assets/themes/newsflash/pages/index.scss +159 -0
- package/dist/assets/themes/newsflash/pages/pricing/index.scss +194 -0
- package/dist/assets/themes/newsflash/pages/test/libraries/layers/index.js +9 -0
- package/dist/assets/themes/newsflash/pages/test/libraries/layers/index.scss +7 -0
- package/dist/commands/blogify.js +6 -3
- package/dist/commands/test.js +34 -5
- package/dist/defaults/CLAUDE.md +17 -4
- package/dist/defaults/dist/_includes/core/pricing/resolve-plan.html +59 -0
- package/dist/defaults/dist/_includes/themes/classy/frontend/sections/footer.html +20 -3
- package/dist/defaults/dist/_layouts/themes/classy/admin/core/minimal-viewport-locked.html +1 -1
- package/dist/defaults/dist/_layouts/themes/classy/admin/core/minimal.html +1 -1
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/pricing.html +5 -40
- package/dist/defaults/dist/_layouts/themes/neobrutalism/frontend/pages/pricing.html +33 -34
- package/dist/defaults/dist/_layouts/themes/newsflash/frontend/core/base.html +61 -0
- package/dist/defaults/dist/_layouts/themes/newsflash/frontend/pages/404.html +86 -0
- package/dist/defaults/dist/_layouts/themes/newsflash/frontend/pages/about.html +353 -0
- package/dist/defaults/dist/_layouts/themes/newsflash/frontend/pages/blog/categories/category.html +105 -0
- package/dist/defaults/dist/_layouts/themes/newsflash/frontend/pages/blog/categories/index.html +93 -0
- package/dist/defaults/dist/_layouts/themes/newsflash/frontend/pages/blog/index.html +373 -0
- package/dist/defaults/dist/_layouts/themes/newsflash/frontend/pages/blog/post.html +289 -0
- package/dist/defaults/dist/_layouts/themes/newsflash/frontend/pages/blog/tags/index.html +90 -0
- package/dist/defaults/dist/_layouts/themes/newsflash/frontend/pages/blog/tags/tag.html +107 -0
- package/dist/defaults/dist/_layouts/themes/newsflash/frontend/pages/contact.html +340 -0
- package/dist/defaults/dist/_layouts/themes/newsflash/frontend/pages/index.html +522 -0
- package/dist/defaults/dist/_layouts/themes/newsflash/frontend/pages/pricing.html +485 -0
- package/dist/defaults/dist/_layouts/themes/newsflash/frontend/pages/team/index.html +207 -0
- package/dist/defaults/dist/_layouts/themes/newsflash/frontend/pages/team/member.html +134 -0
- package/dist/defaults/test/README.md +4 -0
- package/dist/gulp/tasks/jekyll.js +4 -2
- package/dist/test/runner.js +50 -3
- package/dist/test/suites/build/attach-log-file.test.js +102 -0
- package/dist/test/suites/build/theme-contract.test.js +173 -0
- package/dist/test/utils/extended-mode-warning.js +13 -0
- package/dist/utils/attach-log-file.js +70 -43
- package/docs/appearance.md +1 -0
- package/docs/assets.md +9 -0
- package/docs/audit.md +78 -7
- package/docs/build-system.md +57 -0
- package/docs/common-mistakes.md +15 -0
- package/docs/{project-structure.md → directory-structure.md} +1 -1
- package/docs/environment-detection.md +1 -1
- package/docs/javascript-libraries.md +38 -1
- package/docs/layouts-and-pages.md +146 -0
- package/docs/local-development.md +1 -8
- package/docs/logging.md +30 -0
- package/docs/migration.md +131 -0
- package/docs/no-inline-scripts.md +304 -0
- package/docs/purgecss.md +164 -0
- package/docs/seo.md +131 -4
- package/docs/templating.md +23 -0
- package/docs/test-boot-layer.md +1 -1
- package/docs/test-framework.md +56 -8
- package/docs/themes.md +254 -13
- package/logs/test.log +111 -0
- package/package.json +9 -8
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// Theme contract — structural invariants every shipped theme must satisfy
|
|
2
|
+
// (docs/themes.md conventions turned into executable assertions). Globs the
|
|
3
|
+
// theme directories, so a new theme is covered the moment it lands:
|
|
4
|
+
// 1. Entry files exist ($avatar-sizes map, shared bootstrap overrides import)
|
|
5
|
+
// 2. Layouts are swappable (parent refs use [ site.theme.id ] brackets,
|
|
6
|
+
// no theme-prefixed classes in markup, no inline <script> bodies)
|
|
7
|
+
// 3. Cross-theme JS contracts hold (pricing data attributes, blog-post-content)
|
|
8
|
+
// 4. Page asset files use a shape the layouts' asset_path frontmatter declares
|
|
9
|
+
// (wrong shape = silent bundle skip)
|
|
10
|
+
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const jetpack = require('fs-jetpack');
|
|
13
|
+
const glob = require('glob').globSync;
|
|
14
|
+
|
|
15
|
+
const ROOT = path.join(__dirname, '../../../..');
|
|
16
|
+
const ASSETS = path.join(ROOT, 'src/assets/themes');
|
|
17
|
+
const LAYOUTS = path.join(ROOT, 'src/defaults/dist/_layouts/themes');
|
|
18
|
+
const INCLUDES = path.join(ROOT, 'src/defaults/dist/_includes/themes');
|
|
19
|
+
|
|
20
|
+
// _template is held to the asset contract too — it's what theme authors copy.
|
|
21
|
+
// bootstrap is the shared Bootstrap source, not a theme.
|
|
22
|
+
const themes = jetpack.list(ASSETS).filter((t) => t !== 'bootstrap');
|
|
23
|
+
|
|
24
|
+
// Class tokens that would couple markup to one theme (markup must stay
|
|
25
|
+
// swappable; theme prefixes live only in SCSS internals)
|
|
26
|
+
const THEME_PREFIXES = ['nf-', 'nb-', 'classy-', 'newsflash-', 'neobrutalism-', 'broadsheet-', 'template-'];
|
|
27
|
+
|
|
28
|
+
// Markers the framework pricing JS reads — identical across themes by contract
|
|
29
|
+
// (see docs/themes.md "Pricing page JS contract")
|
|
30
|
+
const PRICING_MARKERS = ['data-plan-id', 'billing-info', 'price-per-unit', 'pricing-promo-banner', 'card-title', 'data-monthly'];
|
|
31
|
+
|
|
32
|
+
// All markup files (layouts + includes) for a theme
|
|
33
|
+
function markupFiles(theme) {
|
|
34
|
+
return [
|
|
35
|
+
...glob(`${LAYOUTS}/${theme}/**/*.html`),
|
|
36
|
+
...glob(`${INCLUDES}/${theme}/**/*.html`),
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Inline <script> bodies are banned in theme markup (ld+json and src loaders OK)
|
|
41
|
+
function findInlineScript(html) {
|
|
42
|
+
const scripts = html.matchAll(/<script\b([^>]*)>([\s\S]*?)<\/script>/g);
|
|
43
|
+
|
|
44
|
+
for (const [, attrs, body] of scripts) {
|
|
45
|
+
if (attrs.includes('src=')) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (attrs.includes('application/ld+json')) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (body.trim() === '') {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return body.trim().slice(0, 80);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Flat page-asset names declared by ANY theme layout's asset_path frontmatter
|
|
62
|
+
// (the fallback means classy's declarations apply to every theme)
|
|
63
|
+
function declaredAssetPaths() {
|
|
64
|
+
const paths = new Set();
|
|
65
|
+
|
|
66
|
+
for (const file of glob(`${LAYOUTS}/*/**/*.html`)) {
|
|
67
|
+
const match = jetpack.read(file).match(/^asset_path:\s*(\S+)/m);
|
|
68
|
+
if (match) {
|
|
69
|
+
paths.add(match[1].replace(/['"]/g, ''));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return paths;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = {
|
|
77
|
+
layer: 'build',
|
|
78
|
+
description: 'theme contract (structure, swappability, cross-theme JS contracts)',
|
|
79
|
+
type: 'group',
|
|
80
|
+
tests: [
|
|
81
|
+
...themes.map((theme) => ({
|
|
82
|
+
name: `${theme}: entry files + config contract`,
|
|
83
|
+
run: (ctx) => {
|
|
84
|
+
const config = jetpack.read(`${ASSETS}/${theme}/_config.scss`);
|
|
85
|
+
const entry = jetpack.read(`${ASSETS}/${theme}/_theme.scss`);
|
|
86
|
+
|
|
87
|
+
ctx.expect(config).toBeTruthy();
|
|
88
|
+
ctx.expect(entry).toBeTruthy();
|
|
89
|
+
ctx.expect(jetpack.exists(`${ASSETS}/${theme}/_theme.js`)).toBeTruthy();
|
|
90
|
+
|
|
91
|
+
// Shared nav/account includes depend on the avatar size map
|
|
92
|
+
ctx.expect(config).toContain('$avatar-sizes');
|
|
93
|
+
|
|
94
|
+
// Universal Bootstrap overrides must close the cascade
|
|
95
|
+
ctx.expect(entry).toContain("@import '../bootstrap/overrides'");
|
|
96
|
+
},
|
|
97
|
+
})),
|
|
98
|
+
|
|
99
|
+
...themes.map((theme) => ({
|
|
100
|
+
name: `${theme}: layouts swappable, markup clean`,
|
|
101
|
+
run: (ctx) => {
|
|
102
|
+
for (const file of markupFiles(theme)) {
|
|
103
|
+
const html = jetpack.read(file);
|
|
104
|
+
const rel = path.relative(ROOT, file);
|
|
105
|
+
|
|
106
|
+
// Parent layout refs resolve via bracket templating, never a
|
|
107
|
+
// hardcoded theme id (swappability: change theme.id, done)
|
|
108
|
+
const parent = html.match(/^layout:\s*themes\/(.+)$/m);
|
|
109
|
+
if (parent) {
|
|
110
|
+
ctx.expect(`${rel}: ${parent[1]}`).toMatch(/\[\s*site\.theme\.id\s*\]/);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// No theme-prefixed classes in markup
|
|
114
|
+
for (const [, classes] of html.matchAll(/class="([^"]*)"/g)) {
|
|
115
|
+
for (const token of classes.split(/\s+/)) {
|
|
116
|
+
const prefixed = THEME_PREFIXES.some((p) => token.startsWith(p));
|
|
117
|
+
ctx.expect(prefixed ? `${rel}: class "${token}"` : '').toBeFalsy();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// No inline script bodies
|
|
122
|
+
const inline = findInlineScript(html);
|
|
123
|
+
ctx.expect(inline ? `${rel}: inline <script> "${inline}"` : null).toBeFalsy();
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
})),
|
|
127
|
+
|
|
128
|
+
...themes.map((theme) => ({
|
|
129
|
+
name: `${theme}: cross-theme JS contracts`,
|
|
130
|
+
run: (ctx) => {
|
|
131
|
+
// A theme that ships its own pricing/post layouts must keep the
|
|
132
|
+
// markers the framework JS targets; absent layouts inherit classy's,
|
|
133
|
+
// so the contract holds by definition.
|
|
134
|
+
const pricing = jetpack.read(`${LAYOUTS}/${theme}/frontend/pages/pricing.html`);
|
|
135
|
+
if (pricing) {
|
|
136
|
+
for (const marker of PRICING_MARKERS) {
|
|
137
|
+
ctx.expect(pricing).toContain(marker);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const post = jetpack.read(`${LAYOUTS}/${theme}/frontend/pages/blog/post.html`);
|
|
142
|
+
if (post) {
|
|
143
|
+
// Load-bearing for ad injection
|
|
144
|
+
ctx.expect(post).toContain('blog-post-content');
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
})),
|
|
148
|
+
|
|
149
|
+
{
|
|
150
|
+
name: 'page asset files match a declared asset_path shape',
|
|
151
|
+
run: (ctx) => {
|
|
152
|
+
const declared = declaredAssetPaths();
|
|
153
|
+
|
|
154
|
+
// Sanity: the derivation itself found the known flat shapes
|
|
155
|
+
ctx.expect(declared.has('blog/post')).toBeTruthy();
|
|
156
|
+
|
|
157
|
+
for (const theme of themes) {
|
|
158
|
+
for (const file of glob(`${ASSETS}/${theme}/pages/**/*.{scss,js}`)) {
|
|
159
|
+
const rel = path.relative(`${ASSETS}/${theme}/pages`, file);
|
|
160
|
+
const base = rel.replace(/\.(scss|js)$/, '');
|
|
161
|
+
|
|
162
|
+
// index.* shapes resolve by page path; flat shapes must be
|
|
163
|
+
// declared by some layout's asset_path or the bundle never loads
|
|
164
|
+
if (path.basename(base) === 'index') {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
ctx.expect(declared.has(base) ? '' : `${theme}/pages/${rel}: no layout declares asset_path "${base}"`).toBeFalsy();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// TEST_EXTENDED_MODE warning — SSOT for consistent messaging.
|
|
2
|
+
//
|
|
3
|
+
// Mirrors BEM/BXM/EM: `TEST_EXTENDED_MODE` is the shared, unprefixed env var that opts a
|
|
4
|
+
// test run into hitting REAL external services instead of skipping/stubbing them. Off by
|
|
5
|
+
// default so `npx mgr test` stays fast and offline-safe. Used by the test command (printed to
|
|
6
|
+
// console + teed to logs/test.log).
|
|
7
|
+
const EXTENDED_MODE_WARNING = [
|
|
8
|
+
'⚠️⚠️⚠️ WARNING: TEST_EXTENDED_MODE IS TRUE ⚠️⚠️⚠️',
|
|
9
|
+
'Tests that hit real external services (network fetches, Firebase via web-manager, live APIs) are ENABLED!',
|
|
10
|
+
'This will make real network calls against live backends.',
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
module.exports = { EXTENDED_MODE_WARNING };
|
|
@@ -12,75 +12,102 @@
|
|
|
12
12
|
// Truncates fresh on each call (O_TRUNC), so a new `npm start` doesn't accumulate stale
|
|
13
13
|
// lines from the previous run.
|
|
14
14
|
//
|
|
15
|
-
// Idempotent: calling twice with the same
|
|
15
|
+
// Idempotent: calling twice with the same name on one tee just returns the existing fd.
|
|
16
16
|
//
|
|
17
17
|
// Uses synchronous fs.writeSync(fd, ...) rather than createWriteStream(). Reason: gulp tasks
|
|
18
18
|
// crash via thrown errors that propagate to process.exit, and createWriteStream's internal
|
|
19
19
|
// buffer was being dropped before the kernel could flush it — so the very lines describing
|
|
20
20
|
// the crash (the most important ones) never made it to disk. Synchronous writes incur a
|
|
21
|
-
// per-line syscall but guarantee the tail of the log survives an immediate exit.
|
|
21
|
+
// per-line syscall but guarantee the tail of the log survives an immediate exit. (This is the
|
|
22
|
+
// key behavioral difference from EM, whose tee is async/stream-based with an awaited detach.)
|
|
23
|
+
//
|
|
24
|
+
// The default export is a process-wide SINGLETON (the common case: a CLI command tees its
|
|
25
|
+
// whole run to one file). `attachLogFile.createTee()` returns an INDEPENDENT tee with its own
|
|
26
|
+
// state. Tees STACK: a later attach() captures the CURRENT `process.stdout.write` (which may
|
|
27
|
+
// already be an outer tee) as its "original", so writes fan out through every layer and
|
|
28
|
+
// detach() restores the exact prior writer in LIFO order. That stacking is what lets the
|
|
29
|
+
// attach-log-file unit test exercise attach/detach on a throwaway instance WITHOUT killing
|
|
30
|
+
// the live singleton tee that's capturing the actual test run — the bug that previously
|
|
31
|
+
// truncated `logs/test.log` to ~9 lines (the test detached the live tee mid-run).
|
|
22
32
|
|
|
23
33
|
const fs = require('fs');
|
|
24
34
|
const path = require('path');
|
|
25
35
|
|
|
26
36
|
const ANSI_PATTERN = /\x1B\[[0-9;]*[a-zA-Z]/g;
|
|
27
37
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
let originalStderrWrite = null;
|
|
38
|
+
function stripAnsi(s) {
|
|
39
|
+
return String(s).replace(ANSI_PATTERN, '');
|
|
40
|
+
}
|
|
32
41
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
42
|
+
// Factory — each call returns an independent tee with its own closure state.
|
|
43
|
+
function createTee() {
|
|
44
|
+
let activeFd = null;
|
|
45
|
+
let activePath = null;
|
|
46
|
+
let originalStdoutWrite = null;
|
|
47
|
+
let originalStderrWrite = null;
|
|
37
48
|
|
|
38
|
-
|
|
49
|
+
function attach(name) {
|
|
50
|
+
// Skip on CI/cloud — controlled by UJ_IS_SERVER env var (set by workflows).
|
|
51
|
+
const Manager = require('../build.js');
|
|
52
|
+
if (Manager.isServer()) return null;
|
|
39
53
|
|
|
40
|
-
|
|
54
|
+
if (!name) return null;
|
|
41
55
|
|
|
42
|
-
|
|
43
|
-
if (activeFd !== null) detach();
|
|
56
|
+
const abs = path.resolve(process.cwd(), 'logs', `${name}.log`);
|
|
44
57
|
|
|
45
|
-
|
|
46
|
-
|
|
58
|
+
if (activeFd !== null && activePath === abs) return activeFd;
|
|
59
|
+
if (activeFd !== null) detach();
|
|
47
60
|
|
|
48
|
-
|
|
61
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
62
|
+
const fd = fs.openSync(abs, 'w');
|
|
49
63
|
|
|
50
|
-
|
|
51
|
-
originalStderrWrite = process.stderr.write.bind(process.stderr);
|
|
64
|
+
fs.writeSync(fd, `# ujm log — ${new Date().toISOString()} — pid=${process.pid}\n`);
|
|
52
65
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
process.stderr.write = function (chunk, ...rest) {
|
|
58
|
-
try { fs.writeSync(fd, stripAnsi(String(chunk))); } catch (e) { /* ignore */ }
|
|
59
|
-
return originalStderrWrite(chunk, ...rest);
|
|
60
|
-
};
|
|
66
|
+
// Capture whatever the CURRENT writer is — could be the raw stream OR an outer tee.
|
|
67
|
+
// Restoring this exact reference on detach() is what makes stacked tees safe.
|
|
68
|
+
originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
69
|
+
originalStderrWrite = process.stderr.write.bind(process.stderr);
|
|
61
70
|
|
|
62
|
-
|
|
63
|
-
|
|
71
|
+
process.stdout.write = function (chunk, ...rest) {
|
|
72
|
+
try { fs.writeSync(fd, stripAnsi(String(chunk))); } catch (e) { /* ignore */ }
|
|
73
|
+
return originalStdoutWrite(chunk, ...rest);
|
|
74
|
+
};
|
|
75
|
+
process.stderr.write = function (chunk, ...rest) {
|
|
76
|
+
try { fs.writeSync(fd, stripAnsi(String(chunk))); } catch (e) { /* ignore */ }
|
|
77
|
+
return originalStderrWrite(chunk, ...rest);
|
|
78
|
+
};
|
|
64
79
|
|
|
65
|
-
|
|
66
|
-
|
|
80
|
+
activeFd = fd;
|
|
81
|
+
activePath = abs;
|
|
67
82
|
|
|
68
|
-
|
|
69
|
-
if (originalStdoutWrite) process.stdout.write = originalStdoutWrite;
|
|
70
|
-
if (originalStderrWrite) process.stderr.write = originalStderrWrite;
|
|
71
|
-
if (activeFd !== null) {
|
|
72
|
-
try { fs.closeSync(activeFd); } catch (e) { /* ignore */ }
|
|
83
|
+
return fd;
|
|
73
84
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
85
|
+
|
|
86
|
+
// Restores stdout/stderr and closes the fd. Synchronous — UJM writes synchronously, so the
|
|
87
|
+
// tail is already on disk by the time detach() runs; this just cleans up the handle.
|
|
88
|
+
function detach() {
|
|
89
|
+
if (originalStdoutWrite) process.stdout.write = originalStdoutWrite;
|
|
90
|
+
if (originalStderrWrite) process.stderr.write = originalStderrWrite;
|
|
91
|
+
if (activeFd !== null) {
|
|
92
|
+
try { fs.closeSync(activeFd); } catch (e) { /* ignore */ }
|
|
93
|
+
}
|
|
94
|
+
activeFd = null;
|
|
95
|
+
activePath = null;
|
|
96
|
+
originalStdoutWrite = null;
|
|
97
|
+
originalStderrWrite = null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { attach, detach };
|
|
78
101
|
}
|
|
79
102
|
|
|
80
|
-
|
|
81
|
-
|
|
103
|
+
// Process-wide singleton — the production entry point.
|
|
104
|
+
const singleton = createTee();
|
|
105
|
+
|
|
106
|
+
function attachLogFile(name) {
|
|
107
|
+
return singleton.attach(name);
|
|
82
108
|
}
|
|
83
109
|
|
|
84
110
|
module.exports = attachLogFile;
|
|
85
|
-
module.exports.detach = detach;
|
|
111
|
+
module.exports.detach = singleton.detach;
|
|
86
112
|
module.exports.stripAnsi = stripAnsi;
|
|
113
|
+
module.exports.createTee = createTee;
|
package/docs/appearance.md
CHANGED
|
@@ -63,3 +63,4 @@ webManager.uj().appearance.clear(); // Clear saved preference
|
|
|
63
63
|
- **Module:** `src/assets/js/core/appearance.js` — API and UI handling
|
|
64
64
|
- **Storage:** Saved under `_manager.appearance.preference` in localStorage
|
|
65
65
|
- **Test page:** `/test/libraries/appearance`
|
|
66
|
+
- **Footer picker:** every theme footer ships the appearance dropdown next to the language dropdown (classy's footer provides it to fallback themes automatically; themes with custom footers include it themselves — see [docs/themes.md](themes.md#the-appearance-picker-is-required-in-every-footer)). The footer toggle button is **icon-only** — `data-appearance-icon` spans, no `data-appearance-current` text label (the mode words live in the menu items)
|
package/docs/assets.md
CHANGED
|
@@ -151,6 +151,15 @@ This is the single source of truth for account dropdown menu items. Consuming pr
|
|
|
151
151
|
|
|
152
152
|
This renders the full account dropdown: avatar button with profile photo, user info header (displayName + email), and the menu items from `account.json`.
|
|
153
153
|
|
|
154
|
+
Each dropdown item supports:
|
|
155
|
+
|
|
156
|
+
- `label` — Display text
|
|
157
|
+
- `href` — Link URL (omit for button behavior, e.g. Sign Out)
|
|
158
|
+
- `icon` — Font Awesome icon name
|
|
159
|
+
- `class` — Additional CSS classes
|
|
160
|
+
- `divider: true` — Renders a divider line
|
|
161
|
+
- `attributes` — Array of `[name, value]` pairs (e.g. `data-wm-bind` for visibility)
|
|
162
|
+
|
|
154
163
|
**Parameters:**
|
|
155
164
|
|
|
156
165
|
| Parameter | Default | Description |
|
package/docs/audit.md
CHANGED
|
@@ -1,11 +1,82 @@
|
|
|
1
1
|
# Audit Workflow
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Full-project audit for UJM — runs against a CONSUMER site or the FRAMEWORK repo itself (scope auto-detected). Invoked via the `omega:ujm` skill (`/omega:ujm audit`) or any "audit this site/project" request.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
2. Create a TODO list for each audit category
|
|
7
|
-
3. Read the ENTIRE audit file and plan fixes for each category
|
|
8
|
-
4. Tackle issues incrementally — DO NOT attempt to fix everything at once
|
|
9
|
-
5. Work through one category at a time
|
|
5
|
+
Every check has a stable ID, a severity, and a scope. Findings are reported as `ID @ file:line`, fixed one at a time, then re-verified. The tables below do NOT restate the rules — each check links to the doc that owns the rule and the fix.
|
|
10
6
|
|
|
11
|
-
|
|
7
|
+
## Protocol
|
|
8
|
+
|
|
9
|
+
1. **Detect scope** — read `package.json`: `name` is `ultimate-jekyll-manager` → **framework audit** (U + UJM + F checks); `ultimate-jekyll-manager` in (dev)dependencies → **consumer audit** (U + UJM checks).
|
|
10
|
+
2. **Run the catalog** — every check matching the scope. Search with Grep/Glob/Read over `src/` (+ `test/`, config files); ALWAYS exclude `dist/`, `node_modules/`, `_site/`, `_legacy/`, `_backup/`, `.temp/`, `.cache/`. Record each finding as `ID @ file:line` + a one-line description.
|
|
11
|
+
3. **Persist the report** — write the findings list to `.temp/audit/claude-audit.md` (the same scratch dir `npx mgr audit` uses) so a long fix loop survives session breaks. Summarize counts by severity in chat.
|
|
12
|
+
4. **Fix loop** — TodoWrite per finding, highest severity first, ONE at a time: mark in-progress → root cause → fix → verify → complete. Ask before structural or destructive fixes (file deletions, layout swaps, data changes).
|
|
13
|
+
5. **Re-verify** — re-run every check that produced findings until clean; finish with `npx mgr test` (must be green).
|
|
14
|
+
6. **Doc parity** — if fixes changed behavior, update README / CLAUDE.md / `docs/<topic>.md` / CHANGELOG in the same change set.
|
|
15
|
+
|
|
16
|
+
Severity: **CRIT** security or broken functionality · **HIGH** hard-rule violation · **MED** convention drift · **LOW** optional improvement.
|
|
17
|
+
Scope: **C** consumer · **F** framework repo · **B** both.
|
|
18
|
+
|
|
19
|
+
## Universal checks (U-xx)
|
|
20
|
+
|
|
21
|
+
Mirrored across all four OMEGA frameworks (UJM / BEM / BXM / EM) — same ID means the same check everywhere.
|
|
22
|
+
|
|
23
|
+
| ID | Sev | Scope | Check |
|
|
24
|
+
|----|-----|-------|-------|
|
|
25
|
+
| U-01 | HIGH | B | Every feature has tests at EVERY layer it surfaces (build / page / boot) — never mocked ([test-framework.md](test-framework.md)) |
|
|
26
|
+
| U-02 | HIGH | B | Test hygiene — real-external-API tests gated behind `TEST_EXTENDED_MODE` in-source (not mocked); no tests that assert nothing ([test-framework.md](test-framework.md)) |
|
|
27
|
+
| U-03 | CRIT | B | XSS — inline `webManager.utilities().escapeHTML(value)` at EVERY DOM sink, `sanitizeURL(url)` at executable URL sinks, zero local escape helpers ([xss-prevention.md](xss-prevention.md)) |
|
|
28
|
+
| U-04 | HIGH | B | web-manager owns Firebase — no direct `firebase` imports anywhere; `webManager.auth()` / `.firestore()` ([javascript-libraries.md](javascript-libraries.md)) |
|
|
29
|
+
| U-05 | HIGH | C | No UJM transitive deps installed in the consumer `package.json` (`firebase`, `web-manager`, …) — webpack `resolve.modules` resolves them ([common-mistakes.md](common-mistakes.md)) |
|
|
30
|
+
| U-06 | HIGH | B | Env behavior gated on the INTENTIONAL check — `isProduction()` or `isDevelopment() \|\| isTesting()`, never `!isDevelopment()`; no ad-hoc `process.env.UJ_*` reads where a helper exists ([environment-detection.md](environment-detection.md)) |
|
|
31
|
+
| U-07 | HIGH | B | Config canon — `src/_config.yml` + `config/ultimate-jekyll-manager.json` match the documented shapes; canonical cross-framework blocks (`brand`, flat 8-key `firebaseConfig`, …) not reinvented ([CLAUDE.md](../CLAUDE.md) §Config flow) |
|
|
32
|
+
| U-08 | CRIT | B | No private credentials committed — `.env`, tokens, secret keys; `.gitignore` covers them. (The Firebase WEB `apiKey` is public by design — do NOT flag it.) |
|
|
33
|
+
| U-09 | HIGH | B | Source discipline — nothing edited in `dist/` / `_site/`; no live code referencing `_legacy/` / `_backup/` ([common-mistakes.md](common-mistakes.md)) |
|
|
34
|
+
| U-10 | MED | B | Doc parity — README / CLAUDE.md / `docs/` / CHANGELOG match shipped behavior; CLAUDE.md < 250 lines; the docs index lists every `docs/*.md`; no stale names for renamed commands/patterns |
|
|
35
|
+
| U-11 | MED | B | SSOT/DRY — no duplicated constants/config/logic; one authoritative home per value, imported everywhere else |
|
|
36
|
+
| U-12 | MED | B | JS conventions — file structure, JSDoc, short-circuit returns, leading logical operators, `fs-jetpack`, one `module.exports` per file (global `js:patterns` skill + [CLAUDE.md](../CLAUDE.md) §File Conventions) |
|
|
37
|
+
| U-13 | MED | B | Dead code & stale patterns — no orphaned `src/` files nothing imports; no leftovers of migrated-away formats ([migration.md](migration.md)); inventory TODO/FIXME (report only) |
|
|
38
|
+
| U-14 | LOW | B | Dependency health — review `npm outdated` / `npm audit`; apply fixes via the `general:update-packages` workflow (includes supply-chain checks) |
|
|
39
|
+
|
|
40
|
+
## UJM-specific checks
|
|
41
|
+
|
|
42
|
+
| ID | Sev | Scope | Check |
|
|
43
|
+
|----|-----|-------|-------|
|
|
44
|
+
| UJM-01 | CRIT | B | ZERO inline `<script>` bodies in HTML under `src/` — JS belongs in page modules / `main.js`; migrate per the playbook ([no-inline-scripts.md](no-inline-scripts.md)) |
|
|
45
|
+
| UJM-02 | HIGH | B | Bootstrap-first markup; NO theme-prefixed (`<themeid>-*`) classes in pages/includes — theme SCSS restyles universal classes ([themes.md](themes.md), [css.md](css.md)) |
|
|
46
|
+
| UJM-03 | MED | C | Content writing rules — action-verb headings, sentence case, headline/accent structure; skip front matter, test pages, and blog posts ([seo.md](seo.md#content-writing-rules-applies-to-all-pages)) |
|
|
47
|
+
| UJM-04 | MED | C | Spelling and grammar in body text — skip code blocks, attributes, URLs |
|
|
48
|
+
| UJM-05 | HIGH | C | SEO meta — custom pages carry `meta.title` / `meta.description`; default pages customize via frontmatter per the per-page levels table ([layouts-and-pages.md](layouts-and-pages.md), [seo.md](seo.md)) |
|
|
49
|
+
| UJM-06 | HIGH | B | PurgeCSS — every dynamically-added JS class is safelisted ([purgecss.md](purgecss.md)) |
|
|
50
|
+
| UJM-07 | HIGH | C | Reads-vs-writes — Firestore SDK for dashboard READS only; all WRITES go through Cloud Functions ([javascript-libraries.md](javascript-libraries.md)) |
|
|
51
|
+
| UJM-08 | HIGH | B | Page JS pattern — modules at `src/assets/js/pages/<path>/index.js` with element-existence guards; forms via FormManager; no Liquid in JS (use `data-*` / `<template>` bridges) ([assets.md](assets.md), [page-loading.md](page-loading.md), [no-inline-scripts.md](no-inline-scripts.md)) |
|
|
52
|
+
| UJM-09 | MED | C | Images — imagemin source-size constraints respected, `data-lazy` lazy loading used, `@post/` shortcut in blog posts ([images.md](images.md), [lazy-loading.md](lazy-loading.md)) |
|
|
53
|
+
| UJM-10 | MED | B | Accessibility basics — meaningful `alt` text, labeled form fields, real `<button>`/`<a>` elements (no clickable `div`s) |
|
|
54
|
+
|
|
55
|
+
## Automated stage: `npx mgr audit`
|
|
56
|
+
|
|
57
|
+
After (or alongside) the catalog, run UJM's built-in audit tool — HTML validation + spellcheck + optional Lighthouse:
|
|
58
|
+
|
|
59
|
+
1. **Ask the user** whether to run it or whether they've already run it; run `npx mgr audit` if needed.
|
|
60
|
+
2. **Read EVERY file in `.temp/audit/` COMPLETELY** — audit outputs are large; don't plan from a skim.
|
|
61
|
+
3. **Fold the findings into the same TodoWrite fix loop** — one category at a time; do NOT attempt everything at once.
|
|
62
|
+
4. **Re-run `npx mgr audit` after each batch** — confirm fixed issues resolved and no new ones appeared.
|
|
63
|
+
|
|
64
|
+
Implementation: [`src/gulp/tasks/audit.js`](../src/gulp/tasks/audit.js); results land in `<projectRoot>/.temp/audit/`.
|
|
65
|
+
|
|
66
|
+
## Framework-repo checks (F-xx)
|
|
67
|
+
|
|
68
|
+
Only when auditing the UJM repo itself. Mirrored across the four frameworks.
|
|
69
|
+
|
|
70
|
+
| ID | Sev | Check |
|
|
71
|
+
|----|-----|-------|
|
|
72
|
+
| F-01 | MED | Sister parity — mirrored sections (config shapes, test contract, CLAUDE.md skeleton, shared env/test conventions) in sync with BEM / BXM / EM; deviations are deliberate and documented |
|
|
73
|
+
| F-02 | HIGH | Consumer-shipped defaults in sync — what `npx mgr setup` scaffolds matches current conventions and docs |
|
|
74
|
+
| F-03 | MED | Docs completeness — every `docs/*.md` indexed in CLAUDE.md; every subsystem has a doc; no "(planned)" links for things that have shipped |
|
|
75
|
+
| F-04 | HIGH | `npx mgr test mgr:` green before treating the audit as complete |
|
|
76
|
+
|
|
77
|
+
## See also
|
|
78
|
+
|
|
79
|
+
- [seo.md](seo.md) — the content conventions UJM-03 enforces
|
|
80
|
+
- [xss-prevention.md](xss-prevention.md) — the escaping rules behind U-03
|
|
81
|
+
- [no-inline-scripts.md](no-inline-scripts.md) — the UJM-01 migration playbook
|
|
82
|
+
- [test-framework.md](test-framework.md) — the layers behind U-01 / U-02
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Build System
|
|
2
|
+
|
|
3
|
+
UJM's build is a multi-stage gulp pipeline orchestrated by `src/gulp/main.js`, run from the consumer project (`npm start` for dev, `npm run build` for production).
|
|
4
|
+
|
|
5
|
+
## Pipeline overview
|
|
6
|
+
|
|
7
|
+
Build sequence:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
defaults → distribute → parallel(webpack, sass, imagemin) → jsonToHtml → jekyll → audit → translation → minifyHtml
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Dev sequence:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
serve → build → developmentRebuild
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Gulp tasks
|
|
20
|
+
|
|
21
|
+
15 tasks live in `src/gulp/tasks/`: `defaults` / `distribute` / `webpack` / `sass` / `imagemin` / `jekyll` / `jsonToHtml` / `preprocess` / `audit` / `translation` / `minifyHtml` / `serve` / `setup` / `developmentRebuild`.
|
|
22
|
+
|
|
23
|
+
Pure helpers are exposed under [src/gulp/tasks/utils/](../src/gulp/tasks/utils/) (`merge-jekyll-configs`, `_validate-yaml`, `template-transform`, `collectTextNodes`, `dictionary`, `github-cache`, `formatDocument`) — these are the highest-value test targets (zero I/O, callable directly in `build`-layer tests).
|
|
24
|
+
|
|
25
|
+
## Config flow
|
|
26
|
+
|
|
27
|
+
Three config files in the consumer project feed the build:
|
|
28
|
+
|
|
29
|
+
1. **`src/_config.yml`** — Jekyll config (brand, theme, meta, web_manager). Read by `Manager.getConfig('project')`.
|
|
30
|
+
2. **`config/ultimate-jekyll-manager.json`** — UJM-specific config (purgecss safelist, webpack target, imagemin opts, distribute glob patterns). JSON5.
|
|
31
|
+
3. **`package.json`** — read by `Manager.getPackage('project')`.
|
|
32
|
+
|
|
33
|
+
UJM ships defaults via `_config_default.yml` + `_config_development.yml` at `src/config/` — merged at Jekyll build time via the `--config` chain by [merge-jekyll-configs.js](../src/gulp/tasks/utils/merge-jekyll-configs.js).
|
|
34
|
+
|
|
35
|
+
## Build modes
|
|
36
|
+
|
|
37
|
+
| Mode | Trigger | Effect |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| Development | `npm start` (default) | Serve + watch + incremental rebuild via `developmentRebuild` |
|
|
40
|
+
| Production | `npm run build` (`UJ_BUILD_MODE=true`) | Full pipeline incl. minifyHtml; PurgeCSS runs automatically |
|
|
41
|
+
|
|
42
|
+
PurgeCSS can be enabled locally with `UJ_PURGECSS=true`; consumer safelist patterns live in `config/ultimate-jekyll-manager.json` under `sass.purgecss.safelist`. See [local-development.md](local-development.md).
|
|
43
|
+
|
|
44
|
+
## Serve / live reload
|
|
45
|
+
|
|
46
|
+
The dev server URL is stored in `.temp/_config_browsersync.yml` in the consuming project root — read it to determine the correct URL for browsing/testing. See [local-development.md](local-development.md) for emulator connection (`FIREBASE_EMULATOR_CONNECT=true`).
|
|
47
|
+
|
|
48
|
+
## Log files
|
|
49
|
+
|
|
50
|
+
The gulp pipeline tees all output to `logs/dev.log` (`npm start`) / `logs/build.log` (`npm run build`). Full reference: [logging.md](logging.md).
|
|
51
|
+
|
|
52
|
+
## See also
|
|
53
|
+
|
|
54
|
+
- [templating.md](templating.md) — node-powertools bracket conventions in the pipeline
|
|
55
|
+
- [css.md](css.md) — SCSS structure + PurgeCSS
|
|
56
|
+
- [local-development.md](local-development.md) — dev server, emulators, PurgeCSS toggles
|
|
57
|
+
- [test-framework.md](test-framework.md) — testing the pipeline's pure helpers
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Common Mistakes to Avoid
|
|
2
|
+
|
|
3
|
+
1. **🚫 Inline `<script>` tags in HTML files** — the #1 worst mistake. Move ALL JS to page modules (`src/assets/js/pages/<path>/index.js`) or `main.js`. Component/layout scripts go in `main.js` with element-existence guards. Liquid-templated scripts bridge via `data-*` attributes or `<template>` elements. **Only exceptions:** `type="application/ld+json"`, external `<script src="...">` loaders, and ≤10-line first-paint display helpers with an explaining comment.
|
|
4
|
+
2. **🚫 Reinventing Bootstrap — the #1 CSS mistake** — NEVER create custom classes for things Bootstrap already provides. No `.lm-btn` when `.btn .btn-primary` exists; no `.lm-wrap` when `.container` exists; no custom flex/gap/padding/margin/text-align classes when Bootstrap utilities do the same thing. Theme SCSS overrides how `.btn`/`.card`/`.navbar` LOOK — it doesn't create parallel replacements. Custom CSS is ONLY for genuinely novel components with no Bootstrap equivalent. Before writing ANY custom class, ask: "Does Bootstrap have this?" If yes, USE IT. See [themes.md](themes.md) and [css.md](css.md).
|
|
5
|
+
3. **Creating duplicate CSS** — check Bootstrap and the active theme first.
|
|
6
|
+
4. **Wrong imports** — FormManager needs curly braces: `import { FormManager } from ...`.
|
|
7
|
+
5. **Assuming `Manager`, `firebase`, `webManager` are on `window`** — they are NOT. Use `import webManager from 'web-manager'` in a module. `firebase.firestore()` → `webManager.firestore()`. **Consumer code NEVER imports Firebase directly** — Firebase is web-manager's internal dependency. Same rule in EM and BXM.
|
|
8
|
+
6. **Installing UJM's dependencies as direct consumer deps** — Consumer projects must NOT `npm install firebase`, `web-manager`, or any other UJM/web-manager transitive dep. UJM's webpack config includes `resolve.modules` pointing at the framework's own `node_modules/`. If a dependency isn't resolving, the fix is in UJM's webpack config — not the consumer's `package.json`. Mirrors EM and BXM.
|
|
9
|
+
7. **Not using FormManager** — use it for ALL forms.
|
|
10
|
+
8. **Calling `$form.requestSubmit()` directly** — use `formManager.submit()`.
|
|
11
|
+
9. **Wrong dark mode classes** — use `bg-body` variants, not `bg-light`/`bg-dark`.
|
|
12
|
+
10. **Not waiting for DOM** — always `await webManager.dom().ready()`.
|
|
13
|
+
11. **Using native fetch** — always use `wonderful-fetch` or `authorized-fetch`.
|
|
14
|
+
12. **XSS — unescaped dynamic data in innerHTML** — Use `webManager.utilities().escapeHTML()`. Dynamic URLs in `href`/`src`/`action`/`window.location`/`window.open` ALSO need `webManager.utilities().sanitizeURL()` — `escapeHTML` alone lets `javascript:` execute. See [xss-prevention.md](xss-prevention.md).
|
|
15
|
+
13. **Leaving Liquid `{{ }}` or `{% %}` inside moved JS modules** — Jekyll does NOT process `src/assets/js/**/*.js`. Use `data-*` attribute bridges or `<template>` cloning.
|
|
@@ -82,4 +82,4 @@ Write the function in [src/utils/mode-helpers.js](../src/utils/mode-helpers.js)
|
|
|
82
82
|
|
|
83
83
|
## See also
|
|
84
84
|
|
|
85
|
-
- [test-framework.md](test-framework.md) — `UJ_TEST_MODE` is set automatically by the test runners; `--
|
|
85
|
+
- [test-framework.md](test-framework.md) — `UJ_TEST_MODE` is set automatically by the test runners; `--extended` / `TEST_EXTENDED_MODE=true` gates real external APIs.
|
|
@@ -46,6 +46,43 @@ Raw subscription data (product.id, status, trial, cancellation) is on `account.s
|
|
|
46
46
|
|
|
47
47
|
The same function exists in BEM as `User.resolveSubscription(account)` with identical return shape.
|
|
48
48
|
|
|
49
|
+
### Reads vs Writes: When to Use What
|
|
50
|
+
|
|
51
|
+
| Operation | Method | Why |
|
|
52
|
+
|-----------|--------|-----|
|
|
53
|
+
| **Read** (list, fetch single) | `webManager.firestore()` | Faster, cheaper, no Cloud Function cold starts, no HTTP overhead |
|
|
54
|
+
| **Write** (create, update, delete) | `authorizedFetch` (Cloud Function) | Server-side validation, usage tracking, analytics |
|
|
55
|
+
| **Config/limits lookup** | `webManager.config.payment.products` | Already available client-side from `_config.yml`, no fetch needed |
|
|
56
|
+
|
|
57
|
+
**Dashboard and frontend reads MUST use the Firestore SDK directly** — never call a Cloud Function GET endpoint from the dashboard. Firestore reads are faster, cheaper, and don't incur cold starts. The backend GET routes (`routes/{resource}/get.js`) exist for **external API consumers only**, not for our own frontend. (Requires Firestore security rules that allow the read, e.g. `allow read: if isAuthenticated() && resource.data.owner == authUid()`.)
|
|
58
|
+
|
|
59
|
+
```javascript
|
|
60
|
+
// Collection query (list user's items) — wait for auth first
|
|
61
|
+
webManager.auth().listen({ once: true }, async ({ user }) => {
|
|
62
|
+
if (!user) return;
|
|
63
|
+
|
|
64
|
+
const result = await webManager.firestore()
|
|
65
|
+
.collection('codes')
|
|
66
|
+
.where('owner', '==', user.uid)
|
|
67
|
+
.orderBy('meta.created.timestampUNIX', 'desc')
|
|
68
|
+
.get();
|
|
69
|
+
|
|
70
|
+
if (result.empty) return; // show empty state
|
|
71
|
+
|
|
72
|
+
result.docs.forEach((doc) => {
|
|
73
|
+
const data = doc.data();
|
|
74
|
+
// Render item...
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Single document read
|
|
79
|
+
const doc = await webManager.firestore().doc(`codes/${id}`).get();
|
|
80
|
+
if (!doc.exists()) return; // not found
|
|
81
|
+
const data = doc.data();
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Full Firestore query API (chainable `.where()`/`.orderBy()`/`.limit()`/`.startAt()`, response shapes): see `web-manager`'s [docs/modules.md](../../web-manager/docs/modules.md) — in consumer projects, `node_modules/web-manager/docs/modules.md`.
|
|
85
|
+
|
|
49
86
|
## Ultimate Jekyll Libraries
|
|
50
87
|
|
|
51
88
|
Ultimate Jekyll provides helper libraries in `src/assets/js/libs/` that can be imported as needed.
|
|
@@ -314,7 +351,7 @@ initializing → ready ⇄ submitting → ready (or submitted)
|
|
|
314
351
|
allowResubmit: true, // Allow resubmission after success (false = 'submitted' state)
|
|
315
352
|
resetOnSuccess: false, // Clear form fields after successful submission
|
|
316
353
|
warnOnUnsavedChanges: true, // Warn user before leaving page with unsaved changes
|
|
317
|
-
submittingText: 'Processing...', // Text shown on submit button during submission
|
|
354
|
+
submittingText: 'Processing...', // Text shown on submit button during submission (use '' for icon-only buttons)
|
|
318
355
|
submittedText: 'Processed!', // Text shown on submit button after success (when allowResubmit: false)
|
|
319
356
|
inputGroup: null // Filter getData() by data-input-group attribute (null = all fields)
|
|
320
357
|
}
|