@techninja/clearstack 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +81 -0
- package/bin/cli.js +62 -0
- package/docs/BACKEND_API_SPEC.md +281 -0
- package/docs/BUILD_LOG.md +193 -0
- package/docs/COMPONENT_PATTERNS.md +481 -0
- package/docs/CONVENTIONS.md +226 -0
- package/docs/FRONTEND_IMPLEMENTATION_RULES.md +239 -0
- package/docs/JSDOC_TYPING.md +86 -0
- package/docs/QUICKSTART.md +190 -0
- package/docs/SERVER_AND_DEPS.md +163 -0
- package/docs/STATE_AND_ROUTING.md +363 -0
- package/docs/TESTING.md +268 -0
- package/docs/app-spec/ENTITIES.md +37 -0
- package/docs/app-spec/README.md +19 -0
- package/lib/check.js +115 -0
- package/lib/copy.js +43 -0
- package/lib/init.js +73 -0
- package/lib/package-gen.js +83 -0
- package/lib/update.js +73 -0
- package/package.json +69 -0
- package/templates/fullstack/data/seed.json +1 -0
- package/templates/fullstack/src/api/db.js +75 -0
- package/templates/fullstack/src/api/entities.js +114 -0
- package/templates/fullstack/src/api/events.js +35 -0
- package/templates/fullstack/src/api/schemas.js +104 -0
- package/templates/fullstack/src/api/validate.js +52 -0
- package/templates/fullstack/src/pages/home/home-view.js +19 -0
- package/templates/fullstack/src/router/index.js +16 -0
- package/templates/fullstack/src/server.js +46 -0
- package/templates/fullstack/src/store/AppState.js +33 -0
- package/templates/fullstack/src/store/UserPrefs.js +31 -0
- package/templates/fullstack/src/store/realtimeSync.js +54 -0
- package/templates/shared/.configs/.prettierrc +8 -0
- package/templates/shared/.configs/eslint.config.js +64 -0
- package/templates/shared/.configs/jsconfig.json +24 -0
- package/templates/shared/.configs/web-test-runner.config.js +8 -0
- package/templates/shared/.env +9 -0
- package/templates/shared/.github/ISSUE_TEMPLATE/bug_report.md +42 -0
- package/templates/shared/.github/ISSUE_TEMPLATE/feature_request.md +30 -0
- package/templates/shared/.github/ISSUE_TEMPLATE/spec_correction.md +26 -0
- package/templates/shared/.github/pull_request_template.md +51 -0
- package/templates/shared/.github/workflows/spec.yml +46 -0
- package/templates/shared/README.md +22 -0
- package/templates/shared/docs/app-spec/README.md +40 -0
- package/templates/shared/docs/clearstack/BACKEND_API_SPEC.md +281 -0
- package/templates/shared/docs/clearstack/BUILD_LOG.md +193 -0
- package/templates/shared/docs/clearstack/COMPONENT_PATTERNS.md +481 -0
- package/templates/shared/docs/clearstack/CONVENTIONS.md +226 -0
- package/templates/shared/docs/clearstack/FRONTEND_IMPLEMENTATION_RULES.md +239 -0
- package/templates/shared/docs/clearstack/JSDOC_TYPING.md +86 -0
- package/templates/shared/docs/clearstack/QUICKSTART.md +190 -0
- package/templates/shared/docs/clearstack/SERVER_AND_DEPS.md +163 -0
- package/templates/shared/docs/clearstack/STATE_AND_ROUTING.md +363 -0
- package/templates/shared/docs/clearstack/TESTING.md +268 -0
- package/templates/shared/public/index.html +26 -0
- package/templates/shared/scripts/build-icons.js +86 -0
- package/templates/shared/scripts/vendor-deps.js +25 -0
- package/templates/shared/src/components/atoms/app-badge/app-badge.css +4 -0
- package/templates/shared/src/components/atoms/app-badge/app-badge.js +23 -0
- package/templates/shared/src/components/atoms/app-badge/app-badge.test.js +26 -0
- package/templates/shared/src/components/atoms/app-badge/index.js +1 -0
- package/templates/shared/src/components/atoms/app-button/app-button.css +3 -0
- package/templates/shared/src/components/atoms/app-button/app-button.js +41 -0
- package/templates/shared/src/components/atoms/app-button/app-button.test.js +43 -0
- package/templates/shared/src/components/atoms/app-button/index.js +1 -0
- package/templates/shared/src/components/atoms/app-icon/app-icon.css +4 -0
- package/templates/shared/src/components/atoms/app-icon/app-icon.js +57 -0
- package/templates/shared/src/components/atoms/app-icon/app-icon.test.js +30 -0
- package/templates/shared/src/components/atoms/app-icon/index.js +1 -0
- package/templates/shared/src/components/atoms/theme-toggle/index.js +1 -0
- package/templates/shared/src/components/atoms/theme-toggle/theme-toggle.css +10 -0
- package/templates/shared/src/components/atoms/theme-toggle/theme-toggle.js +42 -0
- package/templates/shared/src/styles/buttons.css +79 -0
- package/templates/shared/src/styles/components.css +31 -0
- package/templates/shared/src/styles/forms.css +20 -0
- package/templates/shared/src/styles/reset.css +32 -0
- package/templates/shared/src/styles/shared.css +135 -0
- package/templates/shared/src/styles/tokens.css +65 -0
- package/templates/shared/src/utils/formatDate.js +41 -0
- package/templates/shared/src/utils/statusColors.js +60 -0
- package/templates/static/src/pages/home/home-view.js +38 -0
- package/templates/static/src/router/index.js +16 -0
- package/templates/static/src/store/AppState.js +26 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON Schema + form layout registry for all entities.
|
|
3
|
+
* Each entry has { schema, layout } where layout defines form structure.
|
|
4
|
+
* @module api/schemas
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** @type {Map<string, { schema: object, layout: object }>} */
|
|
8
|
+
export const schemas = new Map();
|
|
9
|
+
|
|
10
|
+
schemas.set('projects', {
|
|
11
|
+
schema: {
|
|
12
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
13
|
+
title: 'Project',
|
|
14
|
+
type: 'object',
|
|
15
|
+
required: ['name'],
|
|
16
|
+
properties: {
|
|
17
|
+
id: { type: 'string', format: 'uuid', readOnly: true },
|
|
18
|
+
name: {
|
|
19
|
+
type: 'string',
|
|
20
|
+
minLength: 1,
|
|
21
|
+
maxLength: 200,
|
|
22
|
+
title: 'Name',
|
|
23
|
+
description: 'Project name',
|
|
24
|
+
placeholder: 'My Project',
|
|
25
|
+
},
|
|
26
|
+
description: {
|
|
27
|
+
type: 'string',
|
|
28
|
+
maxLength: 1000,
|
|
29
|
+
default: '',
|
|
30
|
+
title: 'Description',
|
|
31
|
+
description: 'Brief summary of the project',
|
|
32
|
+
},
|
|
33
|
+
status: {
|
|
34
|
+
type: 'string',
|
|
35
|
+
enum: ['active', 'archived'],
|
|
36
|
+
enumTitles: ['Active', 'Archived'],
|
|
37
|
+
default: 'active',
|
|
38
|
+
title: 'Status',
|
|
39
|
+
description: 'Current project status',
|
|
40
|
+
},
|
|
41
|
+
createdAt: { type: 'string', format: 'date-time', readOnly: true },
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
layout: {
|
|
45
|
+
groups: [
|
|
46
|
+
{ fields: ['name'], columns: 1 },
|
|
47
|
+
{ fields: ['description'], columns: 1 },
|
|
48
|
+
{ fields: ['status'], columns: 1 },
|
|
49
|
+
],
|
|
50
|
+
actions: { align: 'right' },
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
schemas.set('tasks', {
|
|
55
|
+
schema: {
|
|
56
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
57
|
+
title: 'Task',
|
|
58
|
+
type: 'object',
|
|
59
|
+
required: ['title', 'projectId'],
|
|
60
|
+
properties: {
|
|
61
|
+
id: { type: 'string', format: 'uuid', readOnly: true },
|
|
62
|
+
projectId: {
|
|
63
|
+
type: 'string',
|
|
64
|
+
format: 'uuid',
|
|
65
|
+
title: 'Project',
|
|
66
|
+
description: 'Parent project ID',
|
|
67
|
+
writeOnly: true,
|
|
68
|
+
},
|
|
69
|
+
title: {
|
|
70
|
+
type: 'string',
|
|
71
|
+
minLength: 1,
|
|
72
|
+
maxLength: 200,
|
|
73
|
+
title: 'Title',
|
|
74
|
+
description: 'Task title',
|
|
75
|
+
placeholder: 'What needs to be done?',
|
|
76
|
+
},
|
|
77
|
+
status: {
|
|
78
|
+
type: 'string',
|
|
79
|
+
enum: ['todo', 'doing', 'done'],
|
|
80
|
+
enumTitles: ['To Do', 'In Progress', 'Done'],
|
|
81
|
+
default: 'todo',
|
|
82
|
+
title: 'Status',
|
|
83
|
+
description: 'Current progress',
|
|
84
|
+
},
|
|
85
|
+
priority: {
|
|
86
|
+
type: 'string',
|
|
87
|
+
enum: ['low', 'med', 'high'],
|
|
88
|
+
enumTitles: ['Low', 'Medium', 'High'],
|
|
89
|
+
default: 'med',
|
|
90
|
+
title: 'Priority',
|
|
91
|
+
description: 'Urgency level',
|
|
92
|
+
},
|
|
93
|
+
sortOrder: { type: 'number', default: 0, writeOnly: true },
|
|
94
|
+
createdAt: { type: 'string', format: 'date-time', readOnly: true },
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
layout: {
|
|
98
|
+
groups: [
|
|
99
|
+
{ fields: ['title'], columns: 1 },
|
|
100
|
+
{ fields: ['status', 'priority'], columns: 2 },
|
|
101
|
+
],
|
|
102
|
+
actions: { align: 'right' },
|
|
103
|
+
},
|
|
104
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side validation against JSON Schema.
|
|
3
|
+
* Returns field-level errors for 422 responses.
|
|
4
|
+
* @module api/validate
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { schemas } from './db.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Validate a request body against the entity's JSON Schema.
|
|
11
|
+
* @param {string} entity - Entity name, e.g. 'projects'
|
|
12
|
+
* @param {object} body - Request body to validate
|
|
13
|
+
* @param {boolean} [partial=false] - If true, skip required checks for missing keys
|
|
14
|
+
* @returns {{ valid: boolean, fields: Record<string, string> }}
|
|
15
|
+
*/
|
|
16
|
+
export function validate(entity, body, partial = false) {
|
|
17
|
+
const entry = schemas.get(entity);
|
|
18
|
+
if (!entry) return { valid: true, fields: /** @type {Record<string, string>} */ ({}) };
|
|
19
|
+
|
|
20
|
+
/** @type {Record<string, string>} */
|
|
21
|
+
const fields = {};
|
|
22
|
+
const props = entry.schema.properties || {};
|
|
23
|
+
const required = entry.schema.required || [];
|
|
24
|
+
|
|
25
|
+
if (!partial) {
|
|
26
|
+
for (const name of required) {
|
|
27
|
+
const val = body[name];
|
|
28
|
+
if (val === undefined || val === null || val === '') {
|
|
29
|
+
fields[name] = `${name} is required`;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
for (const [name, val] of Object.entries(body)) {
|
|
35
|
+
const prop = props[name];
|
|
36
|
+
if (!prop || prop.readOnly) continue;
|
|
37
|
+
|
|
38
|
+
if (typeof val === 'string') {
|
|
39
|
+
if (prop.minLength && val.length < prop.minLength) {
|
|
40
|
+
fields[name] = `Minimum ${prop.minLength} characters`;
|
|
41
|
+
}
|
|
42
|
+
if (prop.maxLength && val.length > prop.maxLength) {
|
|
43
|
+
fields[name] = `Maximum ${prop.maxLength} characters`;
|
|
44
|
+
}
|
|
45
|
+
if (prop.enum && !prop.enum.includes(val)) {
|
|
46
|
+
fields[name] = `Must be one of: ${prop.enum.join(', ')}`;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return { valid: Object.keys(fields).length === 0, fields };
|
|
52
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Home page — starting point for your app.
|
|
3
|
+
* @module pages/home
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { html, define } from 'hybrids';
|
|
7
|
+
|
|
8
|
+
export default define({
|
|
9
|
+
tag: 'home-view',
|
|
10
|
+
render: {
|
|
11
|
+
value: () => html`
|
|
12
|
+
<div class="home-view">
|
|
13
|
+
<h1>{{name}}</h1>
|
|
14
|
+
<p>Your Clearstack project is ready. Start building!</p>
|
|
15
|
+
</div>
|
|
16
|
+
`,
|
|
17
|
+
shadow: false,
|
|
18
|
+
},
|
|
19
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App router shell — manages view stack and realtime sync.
|
|
3
|
+
* @module router
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { html, define, router } from 'hybrids';
|
|
7
|
+
import HomeView from '../pages/home/home-view.js';
|
|
8
|
+
|
|
9
|
+
export default define({
|
|
10
|
+
tag: 'app-router',
|
|
11
|
+
stack: router(HomeView, { url: '/' }),
|
|
12
|
+
render: {
|
|
13
|
+
value: ({ stack }) => html`<div class="app-router">${stack}</div>`,
|
|
14
|
+
shadow: false,
|
|
15
|
+
},
|
|
16
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Express server entry point.
|
|
3
|
+
* @module server
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import express from 'express';
|
|
7
|
+
import { entityRouter } from './api/entities.js';
|
|
8
|
+
import { eventsRouter } from './api/events.js';
|
|
9
|
+
|
|
10
|
+
const app = express();
|
|
11
|
+
|
|
12
|
+
app.use(express.json());
|
|
13
|
+
app.use(express.static('public'));
|
|
14
|
+
app.use('/src', express.static('src'));
|
|
15
|
+
|
|
16
|
+
app.use('/api', eventsRouter);
|
|
17
|
+
app.use('/api', entityRouter);
|
|
18
|
+
|
|
19
|
+
// SPA fallback
|
|
20
|
+
app.use((req, res, next) => {
|
|
21
|
+
if (req.method === 'GET' && !req.path.includes('.')) {
|
|
22
|
+
return res.sendFile('index.html', { root: 'public' });
|
|
23
|
+
}
|
|
24
|
+
next();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Start the server.
|
|
29
|
+
* @param {number} [port=3000]
|
|
30
|
+
* @returns {import('node:http').Server}
|
|
31
|
+
*/
|
|
32
|
+
export function start(port = {{port}}) {
|
|
33
|
+
const server = app.listen(port, () => console.log(`http://localhost:${port}`));
|
|
34
|
+
server.on('error', (/** @type {NodeJS.ErrnoException} */ err) => {
|
|
35
|
+
if (err.code === 'EADDRINUSE') console.error(`Port ${port} in use. Try: PORT=4354 npm start`);
|
|
36
|
+
else console.error(err);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
});
|
|
39
|
+
return server;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
43
|
+
start(parseInt(process.env.PORT) || {{port}});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default app;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global UI state — singleton, persisted to localStorage.
|
|
3
|
+
* Theme, sidebar, active filters. No API calls.
|
|
4
|
+
* @module store/AppState
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { store } from 'hybrids';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Object} AppState
|
|
11
|
+
* @property {string} theme - 'light' or 'dark'
|
|
12
|
+
* @property {boolean} sidebarOpen - Sidebar visibility
|
|
13
|
+
* @property {string} activeFilter - Current task filter value
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** @type {import('hybrids').Model<AppState>} */
|
|
17
|
+
const AppState = {
|
|
18
|
+
theme: 'light',
|
|
19
|
+
sidebarOpen: true,
|
|
20
|
+
activeFilter: 'all',
|
|
21
|
+
[store.connect]: {
|
|
22
|
+
get: () => {
|
|
23
|
+
const raw = localStorage.getItem('appState');
|
|
24
|
+
return raw ? JSON.parse(raw) : {};
|
|
25
|
+
},
|
|
26
|
+
set: (id, values) => {
|
|
27
|
+
localStorage.setItem('appState', JSON.stringify(values));
|
|
28
|
+
return values;
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default AppState;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User preferences — singleton, persisted to localStorage.
|
|
3
|
+
* Display preferences that don't need an API.
|
|
4
|
+
* @module store/UserPrefs
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { store } from 'hybrids';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Object} UserPrefs
|
|
11
|
+
* @property {'board'|'list'} defaultView - Preferred task view mode
|
|
12
|
+
* @property {boolean} compactMode - Dense UI toggle
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** @type {import('hybrids').Model<UserPrefs>} */
|
|
16
|
+
const UserPrefs = {
|
|
17
|
+
defaultView: 'list',
|
|
18
|
+
compactMode: false,
|
|
19
|
+
[store.connect]: {
|
|
20
|
+
get: () => {
|
|
21
|
+
const raw = localStorage.getItem('userPrefs');
|
|
22
|
+
return raw ? JSON.parse(raw) : {};
|
|
23
|
+
},
|
|
24
|
+
set: (id, values) => {
|
|
25
|
+
localStorage.setItem('userPrefs', JSON.stringify(values));
|
|
26
|
+
return values;
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export default UserPrefs;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Realtime sync via Server-Sent Events.
|
|
3
|
+
* Debounces rapid updates to avoid clearing stores mid-render.
|
|
4
|
+
* @module utils/realtimeSync
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { store } from 'hybrids';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Connect to the SSE endpoint and clear store caches on entity updates.
|
|
11
|
+
* Debounces clears — multiple events within 300ms trigger one clear.
|
|
12
|
+
* @param {string} url - SSE endpoint, e.g. '/api/events'
|
|
13
|
+
* @param {Record<string, import('hybrids').Model<any>>} modelMap
|
|
14
|
+
* @returns {() => void} Disconnect function
|
|
15
|
+
*/
|
|
16
|
+
export function connectRealtime(url, modelMap) {
|
|
17
|
+
const source = new EventSource(url);
|
|
18
|
+
/** @type {Record<string, ReturnType<typeof setTimeout>>} */
|
|
19
|
+
const timers = {};
|
|
20
|
+
|
|
21
|
+
source.addEventListener('open', () => {
|
|
22
|
+
console.log('[SSE] Connected to', url);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
source.addEventListener('update', (event) => {
|
|
26
|
+
const { type, action } = JSON.parse(event.data);
|
|
27
|
+
const Model = modelMap[type];
|
|
28
|
+
if (!Model) return;
|
|
29
|
+
|
|
30
|
+
// Debounce: batch rapid events (e.g. reorder sends N PUTs)
|
|
31
|
+
clearTimeout(timers[type]);
|
|
32
|
+
timers[type] = setTimeout(() => {
|
|
33
|
+
console.log(`[SSE] ${type} ${action} — clearing store cache`);
|
|
34
|
+
try {
|
|
35
|
+
store.clear([Model]);
|
|
36
|
+
} catch {
|
|
37
|
+
/* list may not exist */
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
store.clear(Model);
|
|
41
|
+
} catch {
|
|
42
|
+
/* singular may not exist */
|
|
43
|
+
}
|
|
44
|
+
}, 300);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
source.addEventListener('error', () => {
|
|
48
|
+
console.log('[SSE] Connection lost, reconnecting in 5s...');
|
|
49
|
+
source.close();
|
|
50
|
+
setTimeout(() => connectRealtime(url, modelMap), 5000);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return () => source.close();
|
|
54
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import prettier from 'eslint-config-prettier';
|
|
2
|
+
import jsdoc from 'eslint-plugin-jsdoc';
|
|
3
|
+
|
|
4
|
+
export default [
|
|
5
|
+
{
|
|
6
|
+
files: ['**/*.js'],
|
|
7
|
+
plugins: { jsdoc },
|
|
8
|
+
languageOptions: {
|
|
9
|
+
ecmaVersion: 2024,
|
|
10
|
+
sourceType: 'module',
|
|
11
|
+
globals: {
|
|
12
|
+
console: 'readonly',
|
|
13
|
+
document: 'readonly',
|
|
14
|
+
window: 'readonly',
|
|
15
|
+
HTMLElement: 'readonly',
|
|
16
|
+
CustomEvent: 'readonly',
|
|
17
|
+
EventSource: 'readonly',
|
|
18
|
+
localStorage: 'readonly',
|
|
19
|
+
fetch: 'readonly',
|
|
20
|
+
setTimeout: 'readonly',
|
|
21
|
+
setInterval: 'readonly',
|
|
22
|
+
clearInterval: 'readonly',
|
|
23
|
+
requestAnimationFrame: 'readonly',
|
|
24
|
+
process: 'readonly',
|
|
25
|
+
URL: 'readonly',
|
|
26
|
+
URLSearchParams: 'readonly',
|
|
27
|
+
Event: 'readonly',
|
|
28
|
+
globalThis: 'readonly',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
rules: {
|
|
32
|
+
semi: ['error', 'always'],
|
|
33
|
+
indent: ['error', 2, { SwitchCase: 1 }],
|
|
34
|
+
'no-var': 'error',
|
|
35
|
+
'prefer-const': 'error',
|
|
36
|
+
eqeqeq: ['error', 'always'],
|
|
37
|
+
'no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
|
|
38
|
+
'no-console': 'off',
|
|
39
|
+
|
|
40
|
+
// JSDoc enforcement
|
|
41
|
+
'jsdoc/require-jsdoc': [
|
|
42
|
+
'warn',
|
|
43
|
+
{
|
|
44
|
+
require: { FunctionDeclaration: true },
|
|
45
|
+
checkConstructors: false,
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
'jsdoc/require-param-type': 'warn',
|
|
49
|
+
'jsdoc/require-returns-type': 'warn',
|
|
50
|
+
'jsdoc/valid-types': 'warn',
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
files: ['**/*.test.js'],
|
|
55
|
+
rules: {
|
|
56
|
+
'jsdoc/require-jsdoc': 'off',
|
|
57
|
+
'no-unused-vars': 'off',
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
ignores: ['node_modules/', 'public/vendor/'],
|
|
62
|
+
},
|
|
63
|
+
prettier,
|
|
64
|
+
];
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ES2022",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"checkJs": true,
|
|
7
|
+
"allowJs": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"strict": false,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"paths": {
|
|
12
|
+
"hybrids": ["../node_modules/hybrids/types/index.d.ts"]
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"include": [
|
|
16
|
+
"../src/**/*.js",
|
|
17
|
+
"../scripts/**/*.js"
|
|
18
|
+
],
|
|
19
|
+
"exclude": [
|
|
20
|
+
"../node_modules",
|
|
21
|
+
"../public/vendor",
|
|
22
|
+
"../**/*.test.js"
|
|
23
|
+
]
|
|
24
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Bug Report
|
|
3
|
+
about: Something isn't working as expected
|
|
4
|
+
title: "[Bug] "
|
|
5
|
+
labels: bug
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Describe the bug
|
|
9
|
+
|
|
10
|
+
<!-- Clear description of what's wrong -->
|
|
11
|
+
|
|
12
|
+
## Steps to reproduce
|
|
13
|
+
|
|
14
|
+
1.
|
|
15
|
+
2.
|
|
16
|
+
3.
|
|
17
|
+
|
|
18
|
+
## Expected behavior
|
|
19
|
+
|
|
20
|
+
<!-- What should happen -->
|
|
21
|
+
|
|
22
|
+
## Actual behavior
|
|
23
|
+
|
|
24
|
+
<!-- What actually happens -->
|
|
25
|
+
|
|
26
|
+
## Environment
|
|
27
|
+
|
|
28
|
+
- Browser:
|
|
29
|
+
- OS:
|
|
30
|
+
- Node version:
|
|
31
|
+
|
|
32
|
+
## Console errors
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
<!-- Paste any console errors here -->
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Spec check output
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
<!-- Paste output of `npm run spec all` if relevant -->
|
|
42
|
+
```
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Feature Request
|
|
3
|
+
about: Suggest a new feature or improvement
|
|
4
|
+
title: "[Feature] "
|
|
5
|
+
labels: enhancement
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## What
|
|
9
|
+
|
|
10
|
+
<!-- What feature or improvement do you want? -->
|
|
11
|
+
|
|
12
|
+
## Why
|
|
13
|
+
|
|
14
|
+
<!-- What problem does it solve? What use case does it enable? -->
|
|
15
|
+
|
|
16
|
+
## Proposed approach
|
|
17
|
+
|
|
18
|
+
<!-- How would you implement it? Which files/components would change? -->
|
|
19
|
+
|
|
20
|
+
## Spec impact
|
|
21
|
+
|
|
22
|
+
<!-- Would this require changes to any spec docs in docs/? -->
|
|
23
|
+
|
|
24
|
+
- [ ] No spec changes needed
|
|
25
|
+
- [ ] New patterns to document
|
|
26
|
+
- [ ] Existing spec needs correction
|
|
27
|
+
|
|
28
|
+
## Alternatives considered
|
|
29
|
+
|
|
30
|
+
<!-- What other approaches did you think about? -->
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Spec Correction
|
|
3
|
+
about: The spec says one thing but implementation reveals another
|
|
4
|
+
title: "[Spec] "
|
|
5
|
+
labels: spec
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Which spec doc?
|
|
9
|
+
|
|
10
|
+
<!-- e.g. COMPONENT_PATTERNS.md, STATE_AND_ROUTING.md -->
|
|
11
|
+
|
|
12
|
+
## What the spec says
|
|
13
|
+
|
|
14
|
+
<!-- Quote the relevant section -->
|
|
15
|
+
|
|
16
|
+
## What actually happens
|
|
17
|
+
|
|
18
|
+
<!-- What did you discover during implementation? -->
|
|
19
|
+
|
|
20
|
+
## Proposed correction
|
|
21
|
+
|
|
22
|
+
<!-- How should the spec be updated? -->
|
|
23
|
+
|
|
24
|
+
## Code evidence
|
|
25
|
+
|
|
26
|
+
<!-- Link to the file/line that demonstrates the correct behavior -->
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
## What
|
|
2
|
+
|
|
3
|
+
<!-- Brief description of what this PR does -->
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
<!-- What problem does it solve or what feature does it add? -->
|
|
8
|
+
|
|
9
|
+
## Changes
|
|
10
|
+
|
|
11
|
+
<!-- List the key changes. Group by category if helpful. -->
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
-
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
-
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
-
|
|
21
|
+
|
|
22
|
+
## Spec Compliance
|
|
23
|
+
|
|
24
|
+
<!-- Paste the output of `npm run spec all` -->
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
npm run spec all
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Tests
|
|
31
|
+
|
|
32
|
+
<!-- What tests were added or updated? -->
|
|
33
|
+
|
|
34
|
+
- [ ] New tests cover the changes
|
|
35
|
+
- [ ] All existing tests still pass
|
|
36
|
+
- [ ] `npm run spec all` passes (7/7)
|
|
37
|
+
|
|
38
|
+
## Spec Updates
|
|
39
|
+
|
|
40
|
+
<!-- Did any spec docs need updating? If so, which ones and why? -->
|
|
41
|
+
|
|
42
|
+
- [ ] No spec changes needed
|
|
43
|
+
- [ ] Updated: <!-- list docs -->
|
|
44
|
+
|
|
45
|
+
## Session Retrospective
|
|
46
|
+
|
|
47
|
+
<!-- Answer briefly — helps future contributors understand the journey -->
|
|
48
|
+
|
|
49
|
+
1. **Patterns discovered:**
|
|
50
|
+
2. **Unexpected breakage:**
|
|
51
|
+
3. **What would you test differently?**
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
name: Spec Check
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
spec:
|
|
11
|
+
name: Full Spec Compliance
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- uses: actions/setup-node@v4
|
|
18
|
+
with:
|
|
19
|
+
node-version: 20
|
|
20
|
+
cache: npm
|
|
21
|
+
|
|
22
|
+
- run: npm ci
|
|
23
|
+
|
|
24
|
+
- name: Install Playwright Chromium
|
|
25
|
+
run: npx playwright install chromium --with-deps
|
|
26
|
+
|
|
27
|
+
- name: Code line counts (≤150)
|
|
28
|
+
run: node scripts/spec.js code
|
|
29
|
+
|
|
30
|
+
- name: Doc line counts (≤500)
|
|
31
|
+
run: node scripts/spec.js docs
|
|
32
|
+
|
|
33
|
+
- name: ESLint
|
|
34
|
+
run: npx eslint --config .configs/eslint.config.js .
|
|
35
|
+
|
|
36
|
+
- name: Prettier
|
|
37
|
+
run: npx prettier --config .configs/.prettierrc --check src scripts server.js tests
|
|
38
|
+
|
|
39
|
+
- name: JSDoc types (tsc --checkJs)
|
|
40
|
+
run: npx tsc --project .configs/jsconfig.json
|
|
41
|
+
|
|
42
|
+
- name: Node tests
|
|
43
|
+
run: node --test tests/*.test.js src/utils/*.test.js src/store/*.test.js
|
|
44
|
+
|
|
45
|
+
- name: Browser tests
|
|
46
|
+
run: npx web-test-runner --config .configs/web-test-runner.config.js
|