@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.
- package/capability.json +33 -0
- package/dist/helpers.d.ts +5 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/helpers.js +83 -0
- package/dist/helpers.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/request-logger.d.ts +8 -0
- package/dist/request-logger.d.ts.map +1 -0
- package/dist/request-logger.js +22 -0
- package/dist/request-logger.js.map +1 -0
- package/dist/static.d.ts +12 -0
- package/dist/static.d.ts.map +1 -0
- package/dist/static.js +16 -0
- package/dist/static.js.map +1 -0
- package/dist/view-engine.d.ts +18 -0
- package/dist/view-engine.d.ts.map +1 -0
- package/dist/view-engine.js +32 -0
- package/dist/view-engine.js.map +1 -0
- package/package.json +41 -0
- package/scaffold/public/css/.gitkeep +1 -0
- package/scaffold/public/favicon.svg +6 -0
- package/scaffold/reference/web-ui.ref.md +116 -0
- package/scaffold/src/controllers/pages.ts +17 -0
- package/scaffold/src/routes/pages.ts +10 -0
- package/scaffold/styles/styles.css +6 -0
- package/scaffold/tailwind.config.ts +10 -0
- package/scaffold/views/404.hbs +8 -0
- package/scaffold/views/about.hbs +8 -0
- package/scaffold/views/home.hbs +9 -0
- package/scaffold/views/layouts/main.hbs +15 -0
- package/skills/design.web-ui-design.skill.md +189 -0
package/capability.json
ADDED
|
@@ -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 @@
|
|
|
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"}
|
package/dist/helpers.js
ADDED
|
@@ -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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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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"}
|
package/dist/static.d.ts
ADDED
|
@@ -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,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'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'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)
|