@boopkit/cap-web 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.
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "web",
3
+ "description": "Web UI with Handlebars + Tailwind",
4
+ "type": "application",
5
+ "package": "@boopkit/cap-web",
6
+ "dependencies": ["express-handlebars", "tailwindcss"],
7
+ "devDependencies": ["@tailwindcss/cli"],
8
+ "files": [
9
+ "views/",
10
+ "public/",
11
+ "styles/",
12
+ "src/controllers/pages.ts",
13
+ "src/routes/pages.ts",
14
+ "tailwind.config.ts",
15
+ "reference/web-ui.ref.md"
16
+ ],
17
+ "skills": ["design.web-ui-design.skill.md"],
18
+ "scripts": {
19
+ "tailwind:build": { "cmd": "npx @tailwindcss/cli -i styles/styles.css -o public/css/style.css --minify", "description": "Compile Tailwind CSS for production" },
20
+ "tailwind:watch": { "cmd": "npx @tailwindcss/cli -i styles/styles.css -o public/css/style.css --watch", "description": "Watch for changes and rebuild Tailwind CSS" }
21
+ },
22
+ "postInstall": {
23
+ "steps": ["Build CSS for the first time: npm run tailwind:build"]
24
+ },
25
+ "hooks": [
26
+ { "phase": "imports", "code": "import { configureViewEngine, configureStatic, requestLogger } from '@boopkit/cap-web';", "description": "view engine, static file serving, and request logging" },
27
+ { "phase": "imports", "code": "import pageRoutes from './routes/pages';", "description": "page route definitions" },
28
+ { "phase": "preRoutes", "order": 12, "code": "app.use(requestLogger());", "description": "log every request with method, URL, status, and timing" },
29
+ { "phase": "preRoutes", "order": 40, "code": "configureViewEngine(app);", "description": "configure Handlebars as view engine" },
30
+ { "phase": "preRoutes", "order": 45, "code": "configureStatic(app);", "description": "serve static content such as CSS" },
31
+ { "phase": "preRoutes", "order": 50, "code": "app.use(pageRoutes);", "description": "mount page routes" }
32
+ ]
33
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Common Handlebars helpers for use in templates.
3
+ */
4
+ export declare const helpers: Record<string, (...args: any[]) => any>;
5
+ //# sourceMappingURL=helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../src/helpers.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,eAAO,MAAM,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,CAmE3D,CAAC"}
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.helpers = void 0;
7
+ const handlebars_1 = __importDefault(require("handlebars"));
8
+ const marked_1 = require("marked");
9
+ /**
10
+ * Common Handlebars helpers for use in templates.
11
+ */
12
+ exports.helpers = {
13
+ /** Equality check: {{#if (eq a b)}} */
14
+ eq: (a, b) => a === b,
15
+ /** JSON stringify: {{json obj}} */
16
+ json: (value) => JSON.stringify(value, null, 2),
17
+ /** Truncate string: {{truncate text 100}} */
18
+ truncate: (str, len) => {
19
+ if (typeof str !== 'string')
20
+ return '';
21
+ if (str.length <= len)
22
+ return str;
23
+ return str.slice(0, len) + '…';
24
+ },
25
+ /** Uppercase: {{upper text}} */
26
+ upper: (str) => (typeof str === 'string' ? str.toUpperCase() : ''),
27
+ /** Lowercase: {{lower text}} */
28
+ lower: (str) => (typeof str === 'string' ? str.toLowerCase() : ''),
29
+ /** Format as currency: {{fmt_dollars 1234}} → "$1,234", {{fmt_dollars 12345 sigfigs=4}} → "$12,350" */
30
+ fmt_dollars: (value, options) => {
31
+ if (value == null)
32
+ return '-';
33
+ const opts = {
34
+ style: 'currency',
35
+ currency: 'USD',
36
+ };
37
+ const sigfigs = options?.hash?.sigfigs;
38
+ if (sigfigs != null) {
39
+ opts.maximumSignificantDigits = Number(sigfigs);
40
+ }
41
+ else {
42
+ opts.maximumFractionDigits = 0;
43
+ }
44
+ return value.toLocaleString('en-US', opts);
45
+ },
46
+ /** Format as percentage: {{fmt_percent 45}} → "45%" */
47
+ fmt_percent: (value) => {
48
+ if (value == null)
49
+ return 'n/a';
50
+ const ratio = value / 100;
51
+ let digits = 2;
52
+ if (ratio === 0)
53
+ digits = 0;
54
+ if (Math.abs(ratio) >= 0.01)
55
+ digits = 1;
56
+ if (Math.abs(ratio) >= 0.1)
57
+ digits = 0;
58
+ return ratio.toLocaleString('en-US', {
59
+ style: 'percent',
60
+ minimumFractionDigits: digits,
61
+ });
62
+ },
63
+ /** Format a date: {{fmt_date createdAt}} → "March 28, 2026" */
64
+ fmt_date: (date) => {
65
+ if (!date)
66
+ return '';
67
+ if (typeof date === 'string')
68
+ date = new Date(date);
69
+ return date.toLocaleDateString('en-US', {
70
+ month: 'long',
71
+ day: 'numeric',
72
+ year: 'numeric',
73
+ });
74
+ },
75
+ /** Render markdown to HTML: {{markdown text}} or {{#markdown}}...{{/markdown}} */
76
+ markdown: function (options) {
77
+ if (!options)
78
+ return '';
79
+ const content = options.fn ? options.fn(this) : options;
80
+ return new handlebars_1.default.SafeString(marked_1.marked.parse(content || ''));
81
+ },
82
+ };
83
+ //# sourceMappingURL=helpers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helpers.js","sourceRoot":"","sources":["../src/helpers.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ export { configureViewEngine } from './view-engine';
2
+ export type { ViewEngineOptions } from './view-engine';
3
+ export { configureStatic } from './static';
4
+ export type { StaticOptions } from './static';
5
+ export { requestLogger } from './request-logger';
6
+ export { helpers } from './helpers';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACpD,YAAY,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AACvD,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAC3C,YAAY,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.helpers = exports.requestLogger = exports.configureStatic = exports.configureViewEngine = void 0;
4
+ var view_engine_1 = require("./view-engine");
5
+ Object.defineProperty(exports, "configureViewEngine", { enumerable: true, get: function () { return view_engine_1.configureViewEngine; } });
6
+ var static_1 = require("./static");
7
+ Object.defineProperty(exports, "configureStatic", { enumerable: true, get: function () { return static_1.configureStatic; } });
8
+ var request_logger_1 = require("./request-logger");
9
+ Object.defineProperty(exports, "requestLogger", { enumerable: true, get: function () { return request_logger_1.requestLogger; } });
10
+ var helpers_1 = require("./helpers");
11
+ Object.defineProperty(exports, "helpers", { enumerable: true, get: function () { return helpers_1.helpers; } });
12
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,6CAAoD;AAA3C,kHAAA,mBAAmB,OAAA;AAE5B,mCAA2C;AAAlC,yGAAA,eAAe,OAAA;AAExB,mDAAiD;AAAxC,+GAAA,aAAa,OAAA;AACtB,qCAAoC;AAA3B,kGAAA,OAAO,OAAA"}
@@ -0,0 +1,8 @@
1
+ import type { Request, Response, NextFunction } from 'express';
2
+ /**
3
+ * Lightweight request logger middleware.
4
+ * Logs method, URL, status code, and response time to stdout.
5
+ * Mount early (after requestId if present) so it captures all requests.
6
+ */
7
+ export declare function requestLogger(): (req: Request, res: Response, next: NextFunction) => void;
8
+ //# sourceMappingURL=request-logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-logger.d.ts","sourceRoot":"","sources":["../src/request-logger.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE/D;;;;GAIG;AACH,wBAAgB,aAAa,KACnB,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,KAAG,IAAI,CAa/D"}
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.requestLogger = requestLogger;
4
+ /**
5
+ * Lightweight request logger middleware.
6
+ * Logs method, URL, status code, and response time to stdout.
7
+ * Mount early (after requestId if present) so it captures all requests.
8
+ */
9
+ function requestLogger() {
10
+ return (req, res, next) => {
11
+ const start = Date.now();
12
+ const id = req.id || '-';
13
+ res.on('finish', () => {
14
+ const ms = Date.now() - start;
15
+ const status = res.statusCode;
16
+ const level = status >= 500 ? 'error' : status >= 400 ? 'warn' : 'info';
17
+ console.log(`${new Date().toISOString()} ${level} [http] ${req.method} ${req.originalUrl} ${status} ${ms}ms req=${id}`);
18
+ });
19
+ next();
20
+ };
21
+ }
22
+ //# sourceMappingURL=request-logger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-logger.js","sourceRoot":"","sources":["../src/request-logger.ts"],"names":[],"mappings":";;AAOA,sCAcC;AAnBD;;;;GAIG;AACH,SAAgB,aAAa;IAC3B,OAAO,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB,EAAQ,EAAE;QAC/D,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACzB,MAAM,EAAE,GAAI,GAAW,CAAC,EAAE,IAAI,GAAG,CAAC;QAElC,GAAG,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE;YACpB,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC;YAC9B,MAAM,MAAM,GAAG,GAAG,CAAC,UAAU,CAAC;YAC9B,MAAM,KAAK,GAAG,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC;YACxE,OAAO,CAAC,GAAG,CAAC,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,IAAI,KAAK,WAAW,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,WAAW,IAAI,MAAM,IAAI,EAAE,UAAU,EAAE,EAAE,CAAC,CAAC;QAC1H,CAAC,CAAC,CAAC;QAEH,IAAI,EAAE,CAAC;IACT,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,12 @@
1
+ import type { Express } from 'express';
2
+ export interface StaticOptions {
3
+ /** Absolute path to the static assets directory. Defaults to `<cwd>/public`. */
4
+ dir?: string;
5
+ /** Cache-Control max-age value. Defaults to `'1d'` in production, `0` in development. */
6
+ maxAge?: string | number;
7
+ }
8
+ /**
9
+ * Serve static assets with configurable cache headers.
10
+ */
11
+ export declare function configureStatic(app: Express, options?: StaticOptions): void;
12
+ //# sourceMappingURL=static.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"static.d.ts","sourceRoot":"","sources":["../src/static.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEvC,MAAM,WAAW,aAAa;IAC5B,gFAAgF;IAChF,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,yFAAyF;IACzF,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC1B;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,GAAE,aAAkB,GAAG,IAAI,CAO/E"}
package/dist/static.js ADDED
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.configureStatic = configureStatic;
7
+ const path_1 = __importDefault(require("path"));
8
+ const express_1 = __importDefault(require("express"));
9
+ /**
10
+ * Serve static assets with configurable cache headers.
11
+ */
12
+ function configureStatic(app, options = {}) {
13
+ const { dir = path_1.default.join(process.cwd(), 'public'), maxAge = process.env.NODE_ENV === 'production' ? '1d' : 0, } = options;
14
+ app.use(express_1.default.static(dir, { maxAge }));
15
+ }
16
+ //# sourceMappingURL=static.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"static.js","sourceRoot":"","sources":["../src/static.ts"],"names":[],"mappings":";;;;;AAcA,0CAOC;AArBD,gDAAwB;AACxB,sDAA8B;AAU9B;;GAEG;AACH,SAAgB,eAAe,CAAC,GAAY,EAAE,UAAyB,EAAE;IACvE,MAAM,EACJ,GAAG,GAAG,cAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,QAAQ,CAAC,EACxC,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAC1D,GAAG,OAAO,CAAC;IAEZ,GAAG,CAAC,GAAG,CAAC,iBAAO,CAAC,MAAM,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC;AAC3C,CAAC"}
@@ -0,0 +1,18 @@
1
+ import type { Express } from 'express';
2
+ export interface ViewEngineOptions {
3
+ /** Absolute path to the views directory. Defaults to `<cwd>/views`. */
4
+ viewsDir?: string;
5
+ /** Absolute path to the layouts directory. Defaults to `<cwd>/views/layouts`. */
6
+ layoutsDir?: string;
7
+ /** Name of the default layout file (without extension). Defaults to `'main'`. */
8
+ defaultLayout?: string;
9
+ }
10
+ /**
11
+ * Configure Handlebars as the view engine for an Express app.
12
+ * Sets up layouts, partials directory, and common helpers.
13
+ *
14
+ * Includes ~130 standard helpers from `handlebars-helpers` (comparison, logical,
15
+ * string, math, array, etc.) plus custom formatting helpers.
16
+ */
17
+ export declare function configureViewEngine(app: Express, options?: ViewEngineOptions): void;
18
+ //# sourceMappingURL=view-engine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"view-engine.d.ts","sourceRoot":"","sources":["../src/view-engine.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAOvC,MAAM,WAAW,iBAAiB;IAChC,uEAAuE;IACvE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iFAAiF;IACjF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iFAAiF;IACjF,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,GAAE,iBAAsB,GAAG,IAAI,CAkBvF"}
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.configureViewEngine = configureViewEngine;
7
+ const path_1 = __importDefault(require("path"));
8
+ const handlebars_1 = __importDefault(require("handlebars"));
9
+ const express_handlebars_1 = require("express-handlebars");
10
+ const handlebars_helpers_1 = __importDefault(require("handlebars-helpers"));
11
+ const helpers_1 = require("./helpers");
12
+ // Register all standard helpers (comparison, logical, string, math, array, etc.)
13
+ (0, handlebars_helpers_1.default)({ handlebars: handlebars_1.default });
14
+ /**
15
+ * Configure Handlebars as the view engine for an Express app.
16
+ * Sets up layouts, partials directory, and common helpers.
17
+ *
18
+ * Includes ~130 standard helpers from `handlebars-helpers` (comparison, logical,
19
+ * string, math, array, etc.) plus custom formatting helpers.
20
+ */
21
+ function configureViewEngine(app, options = {}) {
22
+ const { viewsDir = path_1.default.join(process.cwd(), 'views'), layoutsDir = path_1.default.join(process.cwd(), 'views', 'layouts'), defaultLayout = 'main', } = options;
23
+ app.engine('hbs', (0, express_handlebars_1.engine)({
24
+ extname: '.hbs',
25
+ helpers: helpers_1.helpers,
26
+ layoutsDir,
27
+ defaultLayout,
28
+ }));
29
+ app.set('view engine', 'hbs');
30
+ app.set('views', viewsDir);
31
+ }
32
+ //# sourceMappingURL=view-engine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"view-engine.js","sourceRoot":"","sources":["../src/view-engine.ts"],"names":[],"mappings":";;;;;AA0BA,kDAkBC;AA5CD,gDAAwB;AACxB,4DAAoC;AACpC,2DAA4C;AAE5C,4EAAmD;AACnD,uCAAoC;AAEpC,iFAAiF;AACjF,IAAA,4BAAiB,EAAC,EAAE,UAAU,EAAE,oBAAU,EAAE,CAAC,CAAC;AAW9C;;;;;;GAMG;AACH,SAAgB,mBAAmB,CAAC,GAAY,EAAE,UAA6B,EAAE;IAC/E,MAAM,EACJ,QAAQ,GAAG,cAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,CAAC,EAC5C,UAAU,GAAG,cAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,SAAS,CAAC,EACzD,aAAa,GAAG,MAAM,GACvB,GAAG,OAAO,CAAC;IAEZ,GAAG,CAAC,MAAM,CACR,KAAK,EACL,IAAA,2BAAM,EAAC;QACL,OAAO,EAAE,MAAM;QACf,OAAO,EAAP,iBAAO;QACP,UAAU;QACV,aAAa;KACd,CAAC,CACH,CAAC;IACF,GAAG,CAAC,GAAG,CAAC,aAAa,EAAE,KAAK,CAAC,CAAC;IAC9B,GAAG,CAAC,GAAG,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;AAC7B,CAAC"}
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@boopkit/cap-web",
3
+ "version": "0.1.0",
4
+ "description": "Express view engine configuration, Handlebars helpers, and static asset middleware",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist/",
9
+ "scaffold/",
10
+ "skills/",
11
+ "capability.json"
12
+ ],
13
+ "boopkit": {
14
+ "capability": true,
15
+ "scaffoldDir": "scaffold",
16
+ "skillsDir": "skills",
17
+ "manifest": "capability.json"
18
+ },
19
+ "scripts": {
20
+ "build": "tsc -p tsconfig.json"
21
+ },
22
+ "dependencies": {
23
+ "handlebars-helpers": "^0.10.0",
24
+ "marked": "^15.0.0"
25
+ },
26
+ "peerDependencies": {
27
+ "express": ">=4.0.0",
28
+ "express-handlebars": ">=7.0.0",
29
+ "handlebars": ">=4.0.0"
30
+ },
31
+ "devDependencies": {
32
+ "@types/express": "^5.0.0",
33
+ "express-handlebars": "^8.0.1",
34
+ "handlebars": "^4.7.8",
35
+ "typescript": "^5.9.3"
36
+ },
37
+ "license": "ISC",
38
+ "publishConfig": {
39
+ "access": "public"
40
+ }
41
+ }
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,6 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
2
+ <circle cx="16" cy="16" r="14" fill="#6C5CE7"/>
3
+ <circle cx="11" cy="13" r="2.5" fill="#FFF"/>
4
+ <circle cx="21" cy="13" r="2.5" fill="#FFF"/>
5
+ <path d="M10 20q6 5 12 0" stroke="#FFF" stroke-width="2" fill="none" stroke-linecap="round"/>
6
+ </svg>
@@ -0,0 +1,116 @@
1
+ # Web UI Infrastructure
2
+
3
+ How this project renders pages: Handlebars templates, Tailwind CSS, static assets, and the request pipeline.
4
+
5
+ **Related skills**: [design.web-ui-design.skill.md](../skills/design.web-ui-design.skill.md)
6
+
7
+ ---
8
+
9
+ ## Request Pipeline
10
+
11
+ Web requests flow through middleware configured at preRoutes phase:
12
+
13
+ | Order | Middleware | Purpose |
14
+ |-------|-----------|---------|
15
+ | 12 | `requestLogger()` | Log method, URL, status, and response time |
16
+ | 40 | `configureViewEngine(app)` | Register Handlebars as view engine |
17
+ | 45 | `configureStatic(app)` | Serve `public/` with cache headers |
18
+ | 50 | `app.use(pageRoutes)` | Mount page routes |
19
+
20
+ ---
21
+
22
+ ## View Engine
23
+
24
+ Handlebars via `express-handlebars`. Templates in `views/`, layouts in `views/layouts/`.
25
+
26
+ **Default layout**: `views/layouts/main.hbs` — provides DOCTYPE, meta tags, CSS link, and `{{{body}}}` placeholder.
27
+
28
+ **Rendering**: Controllers call `res.render('viewName', data)`. The view name maps to `views/<viewName>.hbs`.
29
+
30
+ ### Helpers
31
+
32
+ ~130 helpers registered from `handlebars-helpers` (comparison, string, math, array). Custom helpers from `@boopkit/cap-web`:
33
+
34
+ | Helper | Purpose |
35
+ |--------|---------|
36
+ | `json` | JSON stringify with formatting |
37
+ | `truncate` | Truncate string with ellipsis |
38
+ | `fmt_dollars` | Currency formatting |
39
+ | `fmt_percent` | Percentage formatting |
40
+ | `fmt_date` | Human-readable date (e.g., "March 28, 2026") |
41
+ | `markdown` | Render markdown to HTML |
42
+
43
+ ---
44
+
45
+ ## Tailwind CSS
46
+
47
+ **Input**: `styles/styles.css` — imports Tailwind, theme tokens, and base styles from `@boopkit/cap-web`.
48
+
49
+ **Output**: `public/css/style.css` — compiled CSS served as a static asset.
50
+
51
+ **Build**:
52
+ ```bash
53
+ npm run tailwind:build # Production (minified)
54
+ npm run tailwind:watch # Development (watch mode)
55
+ ```
56
+
57
+ **Config**: `tailwind.config.ts` scans `views/**/*.hbs`, `styles/**/*.css`, and `node_modules/@boopkit/*/styles/**/*.css`.
58
+
59
+ ### CSS approach
60
+
61
+ Use semantic class names in templates, define them in `styles/styles.css` with `@apply` for Tailwind utilities. Avoid inline Tailwind classes in templates except for one-off spacing.
62
+
63
+ ---
64
+
65
+ ## Static Assets
66
+
67
+ `public/` is served via Express static middleware. Cache defaults:
68
+ - Production: 24-hour `Cache-Control`
69
+ - Development: no cache
70
+
71
+ ---
72
+
73
+ ## Page Pattern
74
+
75
+ Controllers are thin dispatch — parse request, call service (if needed), render view:
76
+
77
+ ```typescript
78
+ export function home(_req: Request, res: Response): void {
79
+ res.render('home');
80
+ }
81
+ ```
82
+
83
+ Routes use Express Router:
84
+
85
+ ```typescript
86
+ const router = Router();
87
+ router.get('/', pagesController.home);
88
+ router.get('/about', pagesController.about);
89
+ export default router;
90
+ ```
91
+
92
+ ---
93
+
94
+ ## Package Exports
95
+
96
+ `@boopkit/cap-web` exports:
97
+
98
+ | Export | Purpose |
99
+ |--------|---------|
100
+ | `configureViewEngine(app, opts?)` | Register Handlebars with helpers |
101
+ | `configureStatic(app, opts?)` | Mount static file serving |
102
+ | `requestLogger()` | Request logging middleware |
103
+
104
+ ---
105
+
106
+ ## Key Files
107
+
108
+ | File | Purpose |
109
+ |------|---------|
110
+ | `views/layouts/main.hbs` | Default page layout |
111
+ | `views/*.hbs` | Page templates |
112
+ | `styles/styles.css` | CSS entry point (Tailwind + theme) |
113
+ | `public/` | Static assets (compiled CSS, images) |
114
+ | `tailwind.config.ts` | Tailwind content paths and theme |
115
+ | `src/routes/pages.ts` | Page route definitions |
116
+ | `src/controllers/pages.ts` | Page controller dispatch |
@@ -0,0 +1,17 @@
1
+ import type { Request, Response } from 'express';
2
+
3
+ /**
4
+ * @boopkit/cap-web example — Replace with your own page controllers.
5
+ * Controllers are thin dispatch: parse input, call a service, render a view.
6
+ * See design.controllers.skill.md for core principles.
7
+ *
8
+ * When the `services` capability is installed, import services:
9
+ * import services from '@services';
10
+ */
11
+ export function home(_req: Request, res: Response): void {
12
+ res.render('home');
13
+ }
14
+
15
+ export function about(_req: Request, res: Response): void {
16
+ res.render('about');
17
+ }
@@ -0,0 +1,10 @@
1
+ import { Router } from 'express';
2
+ import * as pages from '../controllers/pages';
3
+
4
+ const router = Router();
5
+
6
+ /** @boopkit/cap-web example — replace with your own page routes */
7
+ router.get('/', pages.home);
8
+ router.get('/about', pages.about);
9
+
10
+ export default router;
@@ -0,0 +1,6 @@
1
+ /* @boopkit/cap-web template — Add project overrides below the imports. */
2
+ @import "tailwindcss";
3
+ @import "@boopkit/cap-web/styles/theme.css";
4
+ @import "@boopkit/cap-web/styles/base.css";
5
+
6
+ /* Project overrides — later rules win via cascade */
@@ -0,0 +1,10 @@
1
+ /** @boopkit/cap-web template — Customize Tailwind settings for your project. */
2
+ import type { Config } from 'tailwindcss';
3
+
4
+ export default {
5
+ content: [
6
+ './views/**/*.{hbs,handlebars}',
7
+ './styles/**/*.css',
8
+ './node_modules/@boopkit/*/styles/**/*.css',
9
+ ],
10
+ } satisfies Config;
@@ -0,0 +1,8 @@
1
+ {{!-- @boopkit/cap-web — 404 page. Customize to match your app's design. --}}
2
+ <main class="welcome-page">
3
+ <h1 class="page-title">404</h1>
4
+ <p class="page-subtitle">There&apos;s nothing at <code>{{path}}</code></p>
5
+ <nav class="welcome-actions">
6
+ <a href="/" class="btn-primary">Go home</a>
7
+ </nav>
8
+ </main>
@@ -0,0 +1,8 @@
1
+ {{!-- @boopkit/cap-web — About page. Customize to describe your project. --}}
2
+ <main class="welcome-page">
3
+ <h1 class="page-title">{{projectName}}</h1>
4
+ <p class="page-subtitle">Scaffolded with boop.</p>
5
+ <nav class="welcome-actions">
6
+ <a href="/" class="btn-secondary">Home</a>
7
+ </nav>
8
+ </main>
@@ -0,0 +1,9 @@
1
+ {{!-- @boopkit/cap-web example — Replace with your own page content. --}}
2
+ <main class="welcome-page">
3
+ <h1 class="page-title">{{projectName}}</h1>
4
+ <p class="page-subtitle"><strong>boop</strong>! and it&apos;s done. Your app <strong>{{projectName}}</strong> is running.</p>
5
+ <nav class="welcome-actions">
6
+ <a href="https://github.com/your-org/boop" class="btn-primary">boop docs</a>
7
+ <a href="/about" class="btn-secondary">about {{projectName}}</a>
8
+ </nav>
9
+ </main>
@@ -0,0 +1,15 @@
1
+ {{!-- @boopkit/cap-web template — Customize this layout for your app. --}}
2
+ <!DOCTYPE html>
3
+ <html lang="en">
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>{{projectName}}</title>
8
+ <meta name="description" content="{{#if meta_description}}{{meta_description}}{{else}}Built with boopkit{{/if}}">
9
+ <link rel="icon" href="/favicon.svg" type="image/svg+xml">
10
+ <link rel="stylesheet" href="/css/style.css">
11
+ </head>
12
+ <body class="app-shell">
13
+ {{{body}}}
14
+ </body>
15
+ </html>
@@ -0,0 +1,189 @@
1
+ # Web UI Design Principles
2
+ This skill guides the design and implementation of user interfaces following our established patterns and philosophy.
3
+
4
+ For controller design (the code that handles requests and renders views), apply the core principles in `design.controllers.skill.md` from the `services` capability: thin dispatch, no business logic in controllers, services handle the work. This skill adds web-specific guidance on top: view rendering, template structure, CSS patterns.
5
+
6
+ ## Core Philosophy
7
+ When building UI components, align with the **Spartan Code Philosophy** (see [development.skill.md](development.skill.md)):
8
+ - Only introduce markup, styles, or components required for current functionality
9
+ - Avoid building UI elements in anticipation of future needs
10
+ - Resist adding extra styling, animations, or features beyond what's requested
11
+ - If additional UI enhancements seem beneficial, discuss with the team first
12
+
13
+ ## Semantic CSS Approach
14
+ We use **semantic CSS classes** that describe the purpose of elements rather than their appearance:
15
+
16
+ **Good (semantic):**
17
+ ```html
18
+ <h1 class="page-title">Dashboard</h1>
19
+ <p class="page-subtitle">Welcome back</p>
20
+ <button class="btn-primary">Save Changes</button>
21
+ <div class="policy-item">...</div>
22
+ ```
23
+
24
+ **Avoid (presentational):**
25
+ ```html
26
+ <h1 class="text-2xl font-bold text-gray-900">Dashboard</h1>
27
+ <p class="text-sm text-gray-600 mt-2">Welcome back</p>
28
+ <button class="bg-blue-500 text-white px-4 py-2 rounded">Save Changes</button>
29
+ ```
30
+
31
+ ### Implementation
32
+ - Define semantic class names in `styles.css` using nested structure
33
+ - Apply Tailwind utility classes with `@apply` directive inside semantic classes
34
+ - Use semantic classes in HTML/Handlebars templates
35
+ - Group related styles together with CSS nesting
36
+
37
+ **Example:**
38
+ ```css
39
+ .settings-page {
40
+ @apply px-6 py-4;
41
+
42
+ .settings-block {
43
+ @apply mb-8 bg-white border border-gray-200 rounded-lg p-6;
44
+ }
45
+
46
+ .block-heading {
47
+ @apply text-lg font-semibold text-gray-900 mb-4;
48
+ }
49
+ }
50
+ ```
51
+
52
+ ## Modular Templates and Partials
53
+ Structure templates for reusability while maintaining simplicity:
54
+
55
+ ### When to Create Partials
56
+ Create reusable partials for:
57
+ - Repeated UI patterns used across multiple pages (e.g., navigation bars, footers)
58
+ - Complex components that would clutter the main template
59
+ - Self-contained blocks with clear boundaries and purpose
60
+
61
+ ### When NOT to Create Partials
62
+ Avoid creating partials for:
63
+ - One-off page-specific content
64
+ - Simple markup that's only used once
65
+ - Breaking down templates just for the sake of breaking them down
66
+
67
+ ### The Simplicity Rule
68
+ > **Two simple things are better than one complex thing**
69
+
70
+ When considering whether to reuse existing components:
71
+ - **DO reuse** components that are designed for reuse and fit your needs naturally
72
+ - **DON'T force-fit** a complex reusable component when simple custom markup would be clearer
73
+ - **DON'T make** components overly configurable to handle every edge case
74
+ - **DO create** a second simpler component if the existing one requires too many parameters or conditions
75
+
76
+ ### Handlebars Helpers
77
+ `@boopkit/cap-web` includes the [`handlebars-helpers`](https://github.com/helpers/handlebars-helpers) library (~130 helpers) plus custom formatting helpers.
78
+
79
+ **Standard helpers (from handlebars-helpers) — most useful:**
80
+ - Comparison: `gt`, `lt`, `gte`, `lte`, `eq`, `is`, `not`, `compare`
81
+ - Logical: `and`, `or`, `not`
82
+ - String: `capitalize`, `trim`, `replace`, `startsWith`, `contains`
83
+ - Array: `first`, `last`, `sort`, `forEach`, `join`
84
+ - Math: `add`, `subtract`, `multiply`, `divide`, `ceil`, `floor`, `round`
85
+
86
+ **Custom helpers (from @boopkit/cap-web):**
87
+ - `fmt_dollars` — format number as currency: `{{fmt_dollars 1234}}` → `$1,234` (optional `sigfigs` for data viz: `{{fmt_dollars 12345 sigfigs=4}}` → `$12,350`)
88
+ - `fmt_percent` — format as percentage: `{{fmt_percent 45}}` → `45%`
89
+ - `fmt_date` — human-readable date: `{{fmt_date createdAt}}` → `March 28, 2026`
90
+ - `markdown` — render markdown to HTML: `{{markdown text}}` or `{{#markdown}}...{{/markdown}}`
91
+ - `json` — JSON stringify: `{{json obj}}`
92
+ - `truncate` — truncate string: `{{truncate text 100}}`
93
+ - `upper` / `lower` — case conversion
94
+ - `eq` — strict equality check (also available from handlebars-helpers)
95
+
96
+ Keep template logic minimal—complex logic belongs in the controller/service layer.
97
+
98
+ ## UI Structure Patterns
99
+
100
+ ### Page Structure
101
+ Follow consistent page patterns:
102
+ ```html
103
+ <div class="[page-type]-page">
104
+ <div class="page-header mb-6">
105
+ <h1 class="page-title">Page Title</h1>
106
+ <p class="page-subtitle">Brief description of the page</p>
107
+ </div>
108
+
109
+ <section class="[content-section-class]">
110
+ <!-- Page content -->
111
+ </section>
112
+ </div>
113
+ ```
114
+
115
+ ### Form Structure
116
+ Use consistent form patterns:
117
+ ```html
118
+ <form method="POST" action="/path" class="form">
119
+ <div class="form-group horizontal">
120
+ <label for="field-id" class="form-label">Field Name</label>
121
+ <input id="field-id" name="fieldName" type="text" class="form-input">
122
+ </div>
123
+
124
+ <div class="form-actions">
125
+ <button type="submit" class="btn-primary">Save</button>
126
+ <button type="button" class="btn-cancel">Cancel</button>
127
+ </div>
128
+ </form>
129
+ ```
130
+
131
+ ### Component Structure
132
+ Group related elements with semantic containers:
133
+ ```html
134
+ <div class="policy-list">
135
+ <div class="policy-item">
136
+ <div class="policy-header">
137
+ <h2 class="policy-name">Policy Name</h2>
138
+ <!-- controls -->
139
+ </div>
140
+ <p class="policy-description">Description text</p>
141
+ </div>
142
+ </div>
143
+ ```
144
+
145
+ ## Best Practices
146
+
147
+ ### Naming Conventions
148
+ - Use lowercase with hyphens: `page-title`, `btn-primary`, `policy-item`
149
+ - Be descriptive of purpose, not appearance: `block-heading` not `large-bold-text`
150
+ - Group related classes with common prefixes: `policy-item`, `policy-header`, `policy-name`
151
+
152
+ ### CSS Organization
153
+ - Group page-specific styles under page class selector
154
+ - Use nesting to show hierarchy and relationships
155
+ - Add comments to mark major sections: `/** === Settings Page === **/`
156
+ - Keep specificity low—avoid deep nesting unless necessary
157
+
158
+ ### Reuse Strategy
159
+ 1. **First**, check if an existing semantic class fits your needs
160
+ 2. **If close**, use the existing class—don't create near-duplicates
161
+ 3. **If not quite right**, evaluate:
162
+ - Is it worth making the existing class more flexible?
163
+ - Or is a new simple class clearer?
164
+ 4. **Remember**: Two simple classes are better than one complex parameterized system
165
+
166
+ ## Anti-Patterns to Avoid
167
+ - ❌ Using Tailwind utility classes directly in templates (except for one-off spacing tweaks)
168
+ - ❌ Creating overly abstract/configurable components for simple use cases
169
+ - ❌ Deep partial nesting that makes template flow hard to follow
170
+ - ❌ Adding UI elements or styles "just in case" they're needed later
171
+ - ❌ Creating partials for markup that only appears once
172
+ - ❌ Duplicating semantic classes with slightly different names
173
+
174
+ ## "Paper-Style" Reports (Analytics & Insights)
175
+ When designing report/analytics UIs, prefer a **paper-like** presentation:
176
+ - Keep it Edward Tufte-inspired: clean, dense, information-rich
177
+ - Use clear headings and subheadings to organize content
178
+ - Present data in a logical flow, minimizing distractions
179
+ - Start with a brief narrative intro (what this section means / how to use it)
180
+ - Use light structure (lists and subtle indentation); avoid heavy tables and gridlines
181
+ - Keep spacing tight and consistent; avoid boxed sub-sections unless necessary
182
+ - Prefer aligned numbers/labels over icons/emoji for density and scanability
183
+
184
+ ---
185
+
186
+ See also:
187
+ - [development.skill.md](development.skill.md) for Spartan Code Philosophy
188
+ - [../reference/web-development.ref.md](../reference/web-development.ref.md) for current implementation examples
189
+ - [../reference/boop-guide.ref.md](../reference/boop-guide.ref.md) for CSS extension model (`@theme` tokens, package styles, cascade overrides)