@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,135 @@
|
|
|
1
|
+
/* Shared styles — error states, loading, icons, utilities, transitions.
|
|
2
|
+
Loaded globally in index.html. All light DOM components inherit these. */
|
|
3
|
+
|
|
4
|
+
/* --- Error & Validation States --- */
|
|
5
|
+
|
|
6
|
+
.error {
|
|
7
|
+
color: var(--color-danger);
|
|
8
|
+
|
|
9
|
+
& input,
|
|
10
|
+
& select,
|
|
11
|
+
& textarea {
|
|
12
|
+
border-color: var(--color-danger);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.error-message {
|
|
17
|
+
font-size: var(--text-sm);
|
|
18
|
+
color: var(--color-danger);
|
|
19
|
+
margin-top: var(--space-xs);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.success-message {
|
|
23
|
+
font-size: var(--text-sm);
|
|
24
|
+
color: var(--color-success);
|
|
25
|
+
margin-top: var(--space-xs);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* --- Loading States --- */
|
|
29
|
+
|
|
30
|
+
.loading {
|
|
31
|
+
opacity: 0.6;
|
|
32
|
+
pointer-events: none;
|
|
33
|
+
position: relative;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.spinner {
|
|
37
|
+
display: inline-block;
|
|
38
|
+
width: 1em;
|
|
39
|
+
height: 1em;
|
|
40
|
+
border: 2px solid var(--color-border);
|
|
41
|
+
border-top-color: var(--color-primary);
|
|
42
|
+
border-radius: 50%;
|
|
43
|
+
animation: spin 0.6s linear infinite;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@keyframes spin {
|
|
47
|
+
to {
|
|
48
|
+
transform: rotate(360deg);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* --- Icon Base --- */
|
|
53
|
+
|
|
54
|
+
.icon {
|
|
55
|
+
display: inline-flex;
|
|
56
|
+
align-items: center;
|
|
57
|
+
justify-content: center;
|
|
58
|
+
width: 1.25em;
|
|
59
|
+
height: 1.25em;
|
|
60
|
+
vertical-align: middle;
|
|
61
|
+
fill: currentColor;
|
|
62
|
+
|
|
63
|
+
& svg {
|
|
64
|
+
width: 100%;
|
|
65
|
+
height: 100%;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.icon-sm {
|
|
70
|
+
width: 1em;
|
|
71
|
+
height: 1em;
|
|
72
|
+
}
|
|
73
|
+
.icon-lg {
|
|
74
|
+
width: 1.5em;
|
|
75
|
+
height: 1.5em;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* --- Screen Reader Only --- */
|
|
79
|
+
|
|
80
|
+
.sr-only {
|
|
81
|
+
position: absolute;
|
|
82
|
+
width: 1px;
|
|
83
|
+
height: 1px;
|
|
84
|
+
padding: 0;
|
|
85
|
+
margin: -1px;
|
|
86
|
+
overflow: hidden;
|
|
87
|
+
clip: rect(0, 0, 0, 0);
|
|
88
|
+
white-space: nowrap;
|
|
89
|
+
border: 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* --- Common Transitions --- */
|
|
93
|
+
|
|
94
|
+
.fade-in {
|
|
95
|
+
animation: fadeIn 0.2s ease-in;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@keyframes fadeIn {
|
|
99
|
+
from {
|
|
100
|
+
opacity: 0;
|
|
101
|
+
}
|
|
102
|
+
to {
|
|
103
|
+
opacity: 1;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/* --- Badge / Status Indicators --- */
|
|
108
|
+
|
|
109
|
+
.badge {
|
|
110
|
+
display: inline-block;
|
|
111
|
+
padding: var(--space-xs) var(--space-sm);
|
|
112
|
+
font-size: var(--text-sm);
|
|
113
|
+
font-weight: 500;
|
|
114
|
+
border-radius: var(--radius-sm);
|
|
115
|
+
line-height: 1;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.badge-success {
|
|
119
|
+
background: color-mix(in srgb, var(--color-success) 15%, transparent);
|
|
120
|
+
color: var(--color-success);
|
|
121
|
+
}
|
|
122
|
+
.badge-warning {
|
|
123
|
+
background: color-mix(in srgb, var(--color-warning) 15%, transparent);
|
|
124
|
+
color: var(--color-warning);
|
|
125
|
+
}
|
|
126
|
+
.badge-danger {
|
|
127
|
+
background: color-mix(in srgb, var(--color-danger) 15%, transparent);
|
|
128
|
+
color: var(--color-danger);
|
|
129
|
+
}
|
|
130
|
+
.badge-info {
|
|
131
|
+
background: color-mix(in srgb, var(--color-info) 15%, transparent);
|
|
132
|
+
color: var(--color-info);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/* --- Form Shared --- see forms.css --- */
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
/* Colors — Light */
|
|
3
|
+
--color-primary: #2563eb;
|
|
4
|
+
--color-primary-hover: #1d4ed8;
|
|
5
|
+
--color-success: #16a34a;
|
|
6
|
+
--color-success-hover: #15803d;
|
|
7
|
+
--color-warning: #d97706;
|
|
8
|
+
--color-warning-hover: #b45309;
|
|
9
|
+
--color-danger: #dc2626;
|
|
10
|
+
--color-danger-hover: #b91c1c;
|
|
11
|
+
--color-info: #0284c7;
|
|
12
|
+
--color-info-hover: #0369a1;
|
|
13
|
+
--color-text: #1e293b;
|
|
14
|
+
--color-text-muted: #64748b;
|
|
15
|
+
--color-text-inverse: #ffffff;
|
|
16
|
+
--color-bg: #f8fafc;
|
|
17
|
+
--color-surface: #ffffff;
|
|
18
|
+
--color-border: #e2e8f0;
|
|
19
|
+
|
|
20
|
+
/* Spacing */
|
|
21
|
+
--space-xs: 0.25rem;
|
|
22
|
+
--space-sm: 0.5rem;
|
|
23
|
+
--space-md: 1rem;
|
|
24
|
+
--space-lg: 1.5rem;
|
|
25
|
+
--space-xl: 2rem;
|
|
26
|
+
|
|
27
|
+
/* Typography */
|
|
28
|
+
--font-sans: system-ui, -apple-system, sans-serif;
|
|
29
|
+
--font-mono: ui-monospace, monospace;
|
|
30
|
+
--text-sm: 0.875rem;
|
|
31
|
+
--text-base: 1rem;
|
|
32
|
+
--text-lg: 1.25rem;
|
|
33
|
+
--text-xl: 1.5rem;
|
|
34
|
+
|
|
35
|
+
/* Borders */
|
|
36
|
+
--radius-sm: 0.25rem;
|
|
37
|
+
--radius-md: 0.375rem;
|
|
38
|
+
--radius-lg: 0.5rem;
|
|
39
|
+
|
|
40
|
+
/* Shadows */
|
|
41
|
+
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
|
|
42
|
+
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.07);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* Dark mode */
|
|
46
|
+
[data-theme='dark'] {
|
|
47
|
+
--color-primary: #3b82f6;
|
|
48
|
+
--color-primary-hover: #60a5fa;
|
|
49
|
+
--color-success: #22c55e;
|
|
50
|
+
--color-success-hover: #4ade80;
|
|
51
|
+
--color-warning: #f59e0b;
|
|
52
|
+
--color-warning-hover: #fbbf24;
|
|
53
|
+
--color-danger: #ef4444;
|
|
54
|
+
--color-danger-hover: #f87171;
|
|
55
|
+
--color-info: #38bdf8;
|
|
56
|
+
--color-info-hover: #7dd3fc;
|
|
57
|
+
--color-text: #e2e8f0;
|
|
58
|
+
--color-text-muted: #94a3b8;
|
|
59
|
+
--color-text-inverse: #0f172a;
|
|
60
|
+
--color-bg: #0f172a;
|
|
61
|
+
--color-surface: #1e293b;
|
|
62
|
+
--color-border: #334155;
|
|
63
|
+
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
|
|
64
|
+
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.4);
|
|
65
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Date formatting helpers.
|
|
3
|
+
* @module utils/formatDate
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Format an ISO 8601 string to a readable date.
|
|
8
|
+
* @param {string|null|undefined} iso - ISO date string
|
|
9
|
+
* @returns {string} Formatted date or empty string
|
|
10
|
+
*/
|
|
11
|
+
export function formatDate(iso) {
|
|
12
|
+
if (!iso) return '';
|
|
13
|
+
const d = new Date(iso);
|
|
14
|
+
if (isNaN(d.getTime())) return '';
|
|
15
|
+
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Format an ISO 8601 string to relative time (e.g. "3 days ago").
|
|
20
|
+
* @param {string|null|undefined} iso - ISO date string
|
|
21
|
+
* @returns {string} Relative time or empty string
|
|
22
|
+
*/
|
|
23
|
+
export function timeAgo(iso) {
|
|
24
|
+
if (!iso) return '';
|
|
25
|
+
const seconds = Math.floor((Date.now() - new Date(iso).getTime()) / 1000);
|
|
26
|
+
if (isNaN(seconds)) return '';
|
|
27
|
+
|
|
28
|
+
const intervals = [
|
|
29
|
+
{ label: 'year', seconds: 31536000 },
|
|
30
|
+
{ label: 'month', seconds: 2592000 },
|
|
31
|
+
{ label: 'day', seconds: 86400 },
|
|
32
|
+
{ label: 'hour', seconds: 3600 },
|
|
33
|
+
{ label: 'minute', seconds: 60 },
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
for (const { label, seconds: s } of intervals) {
|
|
37
|
+
const count = Math.floor(seconds / s);
|
|
38
|
+
if (count >= 1) return `${count} ${label}${count > 1 ? 's' : ''} ago`;
|
|
39
|
+
}
|
|
40
|
+
return 'just now';
|
|
41
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps entity status/priority values to badge colors and display titles.
|
|
3
|
+
* @module utils/statusColors
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** @type {Record<string, 'info'|'success'|'warning'|'danger'>} */
|
|
7
|
+
const STATUS_COLORS = {
|
|
8
|
+
active: 'success',
|
|
9
|
+
archived: 'info',
|
|
10
|
+
todo: 'info',
|
|
11
|
+
doing: 'warning',
|
|
12
|
+
done: 'success',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** @type {Record<string, string>} */
|
|
16
|
+
const STATUS_TITLES = {
|
|
17
|
+
active: 'Active',
|
|
18
|
+
archived: 'Archived',
|
|
19
|
+
todo: 'To Do',
|
|
20
|
+
doing: 'In Progress',
|
|
21
|
+
done: 'Done',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** @type {Record<string, 'info'|'success'|'warning'|'danger'>} */
|
|
25
|
+
const PRIORITY_COLORS = {
|
|
26
|
+
low: 'info',
|
|
27
|
+
med: 'warning',
|
|
28
|
+
high: 'danger',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** @type {Record<string, string>} */
|
|
32
|
+
const PRIORITY_TITLES = {
|
|
33
|
+
low: 'Low',
|
|
34
|
+
med: 'Medium',
|
|
35
|
+
high: 'High',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {string} status
|
|
40
|
+
* @returns {'info'|'success'|'warning'|'danger'}
|
|
41
|
+
*/
|
|
42
|
+
export const statusColor = (status) => STATUS_COLORS[status] || 'info';
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {string} status
|
|
46
|
+
* @returns {string}
|
|
47
|
+
*/
|
|
48
|
+
export const statusTitle = (status) => STATUS_TITLES[status] || status;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {string} priority
|
|
52
|
+
* @returns {'info'|'success'|'warning'|'danger'}
|
|
53
|
+
*/
|
|
54
|
+
export const priorityColor = (priority) => PRIORITY_COLORS[priority] || 'info';
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {string} priority
|
|
58
|
+
* @returns {string}
|
|
59
|
+
*/
|
|
60
|
+
export const priorityTitle = (priority) => PRIORITY_TITLES[priority] || priority;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Home page — demonstrates localStorage-backed state.
|
|
3
|
+
* @module pages/home
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { html, define, store } from 'hybrids';
|
|
7
|
+
import AppState from '../../store/AppState.js';
|
|
8
|
+
import '../../components/atoms/app-button/app-button.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {Object} HomeViewHost
|
|
12
|
+
* @property {import('../../store/AppState.js').AppState} state
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** @param {HomeViewHost & HTMLElement} host */
|
|
16
|
+
function increment(host) {
|
|
17
|
+
if (!store.ready(host.state)) return;
|
|
18
|
+
store.set(host.state, { count: host.state.count + 1 });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default define({
|
|
22
|
+
tag: 'home-view',
|
|
23
|
+
state: store(AppState),
|
|
24
|
+
render: {
|
|
25
|
+
value: ({ state }) => html`
|
|
26
|
+
<div class="home-view">
|
|
27
|
+
<h1>{{name}}</h1>
|
|
28
|
+
<p>Your Clearstack project is ready. Start building!</p>
|
|
29
|
+
${store.ready(state) && html`
|
|
30
|
+
<p>Count: ${state.count}</p>
|
|
31
|
+
<button class="btn btn-primary" onclick="${increment}">Increment</button>
|
|
32
|
+
<p class="hint">State persists in localStorage — refresh to verify.</p>
|
|
33
|
+
`}
|
|
34
|
+
</div>
|
|
35
|
+
`,
|
|
36
|
+
shadow: false,
|
|
37
|
+
},
|
|
38
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App router shell — manages view stack.
|
|
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,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App state — singleton, localStorage-backed. No server needed.
|
|
3
|
+
* @module store/AppState
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { store } from 'hybrids';
|
|
7
|
+
|
|
8
|
+
/** @typedef {{ theme: 'light'|'dark', count: number }} AppState */
|
|
9
|
+
|
|
10
|
+
/** @type {import('hybrids').Model<AppState>} */
|
|
11
|
+
const AppState = {
|
|
12
|
+
theme: 'light',
|
|
13
|
+
count: 0,
|
|
14
|
+
[store.connect]: {
|
|
15
|
+
get: () => {
|
|
16
|
+
const raw = localStorage.getItem('appState');
|
|
17
|
+
return raw ? JSON.parse(raw) : {};
|
|
18
|
+
},
|
|
19
|
+
set: (id, values) => {
|
|
20
|
+
localStorage.setItem('appState', JSON.stringify(values));
|
|
21
|
+
return values;
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export default AppState;
|