@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.
Files changed (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +81 -0
  3. package/bin/cli.js +62 -0
  4. package/docs/BACKEND_API_SPEC.md +281 -0
  5. package/docs/BUILD_LOG.md +193 -0
  6. package/docs/COMPONENT_PATTERNS.md +481 -0
  7. package/docs/CONVENTIONS.md +226 -0
  8. package/docs/FRONTEND_IMPLEMENTATION_RULES.md +239 -0
  9. package/docs/JSDOC_TYPING.md +86 -0
  10. package/docs/QUICKSTART.md +190 -0
  11. package/docs/SERVER_AND_DEPS.md +163 -0
  12. package/docs/STATE_AND_ROUTING.md +363 -0
  13. package/docs/TESTING.md +268 -0
  14. package/docs/app-spec/ENTITIES.md +37 -0
  15. package/docs/app-spec/README.md +19 -0
  16. package/lib/check.js +115 -0
  17. package/lib/copy.js +43 -0
  18. package/lib/init.js +73 -0
  19. package/lib/package-gen.js +83 -0
  20. package/lib/update.js +73 -0
  21. package/package.json +69 -0
  22. package/templates/fullstack/data/seed.json +1 -0
  23. package/templates/fullstack/src/api/db.js +75 -0
  24. package/templates/fullstack/src/api/entities.js +114 -0
  25. package/templates/fullstack/src/api/events.js +35 -0
  26. package/templates/fullstack/src/api/schemas.js +104 -0
  27. package/templates/fullstack/src/api/validate.js +52 -0
  28. package/templates/fullstack/src/pages/home/home-view.js +19 -0
  29. package/templates/fullstack/src/router/index.js +16 -0
  30. package/templates/fullstack/src/server.js +46 -0
  31. package/templates/fullstack/src/store/AppState.js +33 -0
  32. package/templates/fullstack/src/store/UserPrefs.js +31 -0
  33. package/templates/fullstack/src/store/realtimeSync.js +54 -0
  34. package/templates/shared/.configs/.prettierrc +8 -0
  35. package/templates/shared/.configs/eslint.config.js +64 -0
  36. package/templates/shared/.configs/jsconfig.json +24 -0
  37. package/templates/shared/.configs/web-test-runner.config.js +8 -0
  38. package/templates/shared/.env +9 -0
  39. package/templates/shared/.github/ISSUE_TEMPLATE/bug_report.md +42 -0
  40. package/templates/shared/.github/ISSUE_TEMPLATE/feature_request.md +30 -0
  41. package/templates/shared/.github/ISSUE_TEMPLATE/spec_correction.md +26 -0
  42. package/templates/shared/.github/pull_request_template.md +51 -0
  43. package/templates/shared/.github/workflows/spec.yml +46 -0
  44. package/templates/shared/README.md +22 -0
  45. package/templates/shared/docs/app-spec/README.md +40 -0
  46. package/templates/shared/docs/clearstack/BACKEND_API_SPEC.md +281 -0
  47. package/templates/shared/docs/clearstack/BUILD_LOG.md +193 -0
  48. package/templates/shared/docs/clearstack/COMPONENT_PATTERNS.md +481 -0
  49. package/templates/shared/docs/clearstack/CONVENTIONS.md +226 -0
  50. package/templates/shared/docs/clearstack/FRONTEND_IMPLEMENTATION_RULES.md +239 -0
  51. package/templates/shared/docs/clearstack/JSDOC_TYPING.md +86 -0
  52. package/templates/shared/docs/clearstack/QUICKSTART.md +190 -0
  53. package/templates/shared/docs/clearstack/SERVER_AND_DEPS.md +163 -0
  54. package/templates/shared/docs/clearstack/STATE_AND_ROUTING.md +363 -0
  55. package/templates/shared/docs/clearstack/TESTING.md +268 -0
  56. package/templates/shared/public/index.html +26 -0
  57. package/templates/shared/scripts/build-icons.js +86 -0
  58. package/templates/shared/scripts/vendor-deps.js +25 -0
  59. package/templates/shared/src/components/atoms/app-badge/app-badge.css +4 -0
  60. package/templates/shared/src/components/atoms/app-badge/app-badge.js +23 -0
  61. package/templates/shared/src/components/atoms/app-badge/app-badge.test.js +26 -0
  62. package/templates/shared/src/components/atoms/app-badge/index.js +1 -0
  63. package/templates/shared/src/components/atoms/app-button/app-button.css +3 -0
  64. package/templates/shared/src/components/atoms/app-button/app-button.js +41 -0
  65. package/templates/shared/src/components/atoms/app-button/app-button.test.js +43 -0
  66. package/templates/shared/src/components/atoms/app-button/index.js +1 -0
  67. package/templates/shared/src/components/atoms/app-icon/app-icon.css +4 -0
  68. package/templates/shared/src/components/atoms/app-icon/app-icon.js +57 -0
  69. package/templates/shared/src/components/atoms/app-icon/app-icon.test.js +30 -0
  70. package/templates/shared/src/components/atoms/app-icon/index.js +1 -0
  71. package/templates/shared/src/components/atoms/theme-toggle/index.js +1 -0
  72. package/templates/shared/src/components/atoms/theme-toggle/theme-toggle.css +10 -0
  73. package/templates/shared/src/components/atoms/theme-toggle/theme-toggle.js +42 -0
  74. package/templates/shared/src/styles/buttons.css +79 -0
  75. package/templates/shared/src/styles/components.css +31 -0
  76. package/templates/shared/src/styles/forms.css +20 -0
  77. package/templates/shared/src/styles/reset.css +32 -0
  78. package/templates/shared/src/styles/shared.css +135 -0
  79. package/templates/shared/src/styles/tokens.css +65 -0
  80. package/templates/shared/src/utils/formatDate.js +41 -0
  81. package/templates/shared/src/utils/statusColors.js +60 -0
  82. package/templates/static/src/pages/home/home-view.js +38 -0
  83. package/templates/static/src/router/index.js +16 -0
  84. package/templates/static/src/store/AppState.js +26 -0
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Extracts SVG path data from lucide-static for icons used in the app.
5
+ * Generates public/icons.json — loaded by app-icon at runtime.
6
+ * Runs on `npm postinstall`.
7
+ */
8
+
9
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
10
+ import { resolve, dirname } from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
12
+
13
+ const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
14
+ const ICONS_DIR = resolve(ROOT, 'node_modules/lucide-static/icons');
15
+ const OUT = resolve(ROOT, 'public/icons.json');
16
+
17
+ /** Icons used in the app — lucide name → app name */
18
+ const ICON_MAP = {
19
+ plus: 'plus',
20
+ check: 'check',
21
+ 'trash-2': 'trash',
22
+ x: 'x',
23
+ pencil: 'edit',
24
+ 'chevron-right': 'chevron',
25
+ folder: 'folder',
26
+ 'grip-vertical': 'grip',
27
+ list: 'list',
28
+ 'layout-grid': 'grid',
29
+ filter: 'filter',
30
+ settings: 'settings',
31
+ 'mouse-pointer': 'pointer',
32
+ 'pen-tool': 'pen',
33
+ square: 'rect',
34
+ circle: 'circle',
35
+ minus: 'line',
36
+ shapes: 'shapes',
37
+ cloud: 'cloud',
38
+ database: 'database',
39
+ diamond: 'diamond',
40
+ hexagon: 'hexagon',
41
+ 'file-text': 'document',
42
+ 'arrow-right': 'arrow-right',
43
+ cylinder: 'cylinder',
44
+ server: 'server',
45
+ monitor: 'monitor',
46
+ globe: 'globe',
47
+ lock: 'lock',
48
+ shield: 'shield',
49
+ user: 'user',
50
+ cpu: 'cpu',
51
+ wifi: 'wifi',
52
+ zap: 'zap',
53
+ box: 'box',
54
+ star: 'star',
55
+ type: 'text',
56
+ };
57
+
58
+ /**
59
+ * Extract all <path>, <line>, <circle>, <rect>, <polyline> inner content from an SVG file.
60
+ * @param {string} file
61
+ * @returns {string} Combined SVG inner elements
62
+ */
63
+ function extractInner(file) {
64
+ const svg = readFileSync(file, 'utf-8');
65
+ const inner = svg.match(/<(path|line|circle|rect|polyline|ellipse)\s[^>]*\/>/g);
66
+ return inner ? inner.join('') : '';
67
+ }
68
+
69
+ mkdirSync(dirname(OUT), { recursive: true });
70
+
71
+ /** @type {Record<string, string>} */
72
+ const icons = {};
73
+ let count = 0;
74
+
75
+ for (const [lucideName, appName] of Object.entries(ICON_MAP)) {
76
+ const file = resolve(ICONS_DIR, `${lucideName}.svg`);
77
+ if (!existsSync(file)) {
78
+ console.warn(`⚠ Icon not found: ${lucideName}`);
79
+ continue;
80
+ }
81
+ icons[appName] = extractInner(file);
82
+ count++;
83
+ }
84
+
85
+ writeFileSync(OUT, JSON.stringify(icons, null, 2));
86
+ console.log(`✓ Built ${count} icons → public/icons.json`);
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Copies third-party ES module sources into public/vendor/ so the browser
5
+ * can load them directly via import map. Runs on `npm postinstall`.
6
+ */
7
+
8
+ import { cpSync, mkdirSync } from 'node:fs';
9
+ import { resolve, dirname } from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+
12
+ const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
13
+ const VENDOR_DIR = resolve(ROOT, 'public/vendor');
14
+
15
+ /** @type {{ name: string, src: string }[]} */
16
+ const DEPS = [{ name: 'hybrids', src: 'node_modules/hybrids/src' }];
17
+
18
+ mkdirSync(VENDOR_DIR, { recursive: true });
19
+
20
+ for (const dep of DEPS) {
21
+ const src = resolve(ROOT, dep.src);
22
+ const dest = resolve(VENDOR_DIR, dep.name);
23
+ cpSync(src, dest, { recursive: true });
24
+ console.log(`✓ Vendored: ${dep.name} → public/vendor/${dep.name}/`);
25
+ }
@@ -0,0 +1,4 @@
1
+ app-badge {
2
+ display: inline-block;
3
+ line-height: 1;
4
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Badge atom — inline status/priority indicator.
3
+ * @module components/atoms/app-badge
4
+ */
5
+
6
+ import { html, define } from 'hybrids';
7
+
8
+ /**
9
+ * @typedef {Object} AppBadgeHost
10
+ * @property {string} label - Badge text
11
+ * @property {'info'|'success'|'warning'|'danger'} color - Color variant
12
+ */
13
+
14
+ /** @type {import('hybrids').Component<AppBadgeHost>} */
15
+ export default define({
16
+ tag: 'app-badge',
17
+ label: '',
18
+ color: 'info',
19
+ render: {
20
+ value: ({ label, color }) => html` <span class="badge badge-${color}">${label}</span> `,
21
+ shadow: false,
22
+ },
23
+ });
@@ -0,0 +1,26 @@
1
+ import { fixture, expect } from '@open-wc/testing';
2
+ import '../app-badge/app-badge.js';
3
+
4
+ describe('app-badge', () => {
5
+ it('renders a span with label text', async () => {
6
+ const el = await fixture(`<app-badge label="Active"></app-badge>`);
7
+ await new Promise((r) => requestAnimationFrame(r));
8
+ const span = el.querySelector('.badge');
9
+ expect(span).to.exist;
10
+ expect(span.textContent).to.contain('Active');
11
+ });
12
+
13
+ it('applies color variant class', async () => {
14
+ const el = await fixture(`<app-badge label="High" color="danger"></app-badge>`);
15
+ await new Promise((r) => requestAnimationFrame(r));
16
+ const span = el.querySelector('.badge');
17
+ expect(span.classList.contains('badge-danger')).to.be.true;
18
+ });
19
+
20
+ it('defaults to info color', async () => {
21
+ const el = await fixture(`<app-badge label="Note"></app-badge>`);
22
+ await new Promise((r) => requestAnimationFrame(r));
23
+ const span = el.querySelector('.badge');
24
+ expect(span.classList.contains('badge-info')).to.be.true;
25
+ });
26
+ });
@@ -0,0 +1 @@
1
+ export { default } from './app-badge.js';
@@ -0,0 +1,3 @@
1
+ app-button {
2
+ display: inline-block;
3
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Button atom — single-purpose action element.
3
+ * @module components/atoms/app-button
4
+ */
5
+
6
+ import { html, define, dispatch } from 'hybrids';
7
+
8
+ /**
9
+ * @typedef {Object} AppButtonHost
10
+ * @property {string} label - Button display text
11
+ * @property {'primary'|'secondary'|'ghost'|'danger'} variant - Visual style
12
+ * @property {boolean} disabled - Disabled state
13
+ */
14
+
15
+ /**
16
+ * @param {AppButtonHost & HTMLElement} host
17
+ * @param {MouseEvent} event
18
+ */
19
+ function handleClick(host, event) {
20
+ if (host.disabled) {
21
+ event.preventDefault();
22
+ return;
23
+ }
24
+ dispatch(host, 'press', { bubbles: true });
25
+ }
26
+
27
+ /** @type {import('hybrids').Component<AppButtonHost>} */
28
+ export default define({
29
+ tag: 'app-button',
30
+ label: '',
31
+ variant: 'primary',
32
+ disabled: false,
33
+ render: {
34
+ value: ({ label, variant, disabled }) => html`
35
+ <button class="btn btn-${variant}" disabled="${disabled}" onclick="${handleClick}">
36
+ ${label}
37
+ </button>
38
+ `,
39
+ shadow: false,
40
+ },
41
+ });
@@ -0,0 +1,43 @@
1
+ import { fixture, expect, oneEvent } from '@open-wc/testing';
2
+ import { html } from 'hybrids';
3
+ import '../app-button/app-button.js';
4
+
5
+ describe('app-button', () => {
6
+ it('renders a button with label', async () => {
7
+ const el = await fixture(`<app-button label="Click me"></app-button>`);
8
+ await new Promise((r) => requestAnimationFrame(r));
9
+ const btn = el.querySelector('button');
10
+ expect(btn).to.exist;
11
+ expect(btn.textContent).to.contain('Click me');
12
+ });
13
+
14
+ it('applies variant class', async () => {
15
+ const el = await fixture(`<app-button label="Go" variant="danger"></app-button>`);
16
+ await new Promise((r) => requestAnimationFrame(r));
17
+ const btn = el.querySelector('button');
18
+ expect(btn.classList.contains('btn-danger')).to.be.true;
19
+ });
20
+
21
+ it('defaults to primary variant', async () => {
22
+ const el = await fixture(`<app-button label="Go"></app-button>`);
23
+ await new Promise((r) => requestAnimationFrame(r));
24
+ const btn = el.querySelector('button');
25
+ expect(btn.classList.contains('btn-primary')).to.be.true;
26
+ });
27
+
28
+ it('reflects disabled attribute to button', async () => {
29
+ const el = await fixture(`<app-button label="No" disabled></app-button>`);
30
+ await new Promise((r) => requestAnimationFrame(r));
31
+ const btn = el.querySelector('button');
32
+ expect(btn.disabled).to.be.true;
33
+ });
34
+
35
+ it('dispatches press event on click', async () => {
36
+ const el = await fixture(`<app-button label="Go"></app-button>`);
37
+ await new Promise((r) => requestAnimationFrame(r));
38
+ const btn = el.querySelector('button');
39
+ setTimeout(() => btn.click());
40
+ const event = await oneEvent(el, 'press');
41
+ expect(event).to.exist;
42
+ });
43
+ });
@@ -0,0 +1 @@
1
+ export { default } from './app-button.js';
@@ -0,0 +1,4 @@
1
+ app-icon {
2
+ display: inline-flex;
3
+ vertical-align: middle;
4
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Icon atom — renders Lucide SVG icons by name.
3
+ * Icons loaded from /icons.json (generated at install from lucide-static).
4
+ * @module components/atoms/app-icon
5
+ */
6
+
7
+ import { html, define } from 'hybrids';
8
+
9
+ /** @type {Record<string, string>|null} */
10
+ let iconCache = null;
11
+
12
+ /** @type {Promise<Record<string, string>>} */
13
+ const loading = fetch('/icons.json')
14
+ .then((r) => (r.ok ? r : fetch('/public/icons.json')))
15
+ .then((r) => r.json())
16
+ .then((data) => {
17
+ iconCache = data;
18
+ return data;
19
+ })
20
+ .catch(() => {
21
+ iconCache = {};
22
+ return {};
23
+ });
24
+
25
+ /**
26
+ * @typedef {Object} AppIconHost
27
+ * @property {string} name
28
+ * @property {'sm'|'md'|'lg'} size
29
+ * @property {string} svgContent - Resolved SVG inner markup
30
+ */
31
+
32
+ /** @type {import('hybrids').Component<AppIconHost>} */
33
+ export default define({
34
+ tag: 'app-icon',
35
+ name: '',
36
+ size: 'md',
37
+ svgContent: {
38
+ value: '',
39
+ connect(host, _key, invalidate) {
40
+ loading.then(() => {
41
+ host.svgContent = iconCache?.[host.name] || '';
42
+ invalidate();
43
+ });
44
+ },
45
+ observe(host, val) {
46
+ const span = host.querySelector('.icon');
47
+ if (!span) return;
48
+ span.innerHTML = val
49
+ ? `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${val}</svg>`
50
+ : '';
51
+ },
52
+ },
53
+ render: {
54
+ value: ({ size }) => html`<span class="icon icon-${size}"></span>`,
55
+ shadow: false,
56
+ },
57
+ });
@@ -0,0 +1,30 @@
1
+ import { fixture, expect } from '@open-wc/testing';
2
+ import '../app-icon/app-icon.js';
3
+
4
+ const frame = () => new Promise((r) => requestAnimationFrame(r));
5
+
6
+ describe('app-icon', () => {
7
+ it('renders a span with icon class', async () => {
8
+ const el = await fixture(`<app-icon name="plus"></app-icon>`);
9
+ await frame();
10
+ const span = el.querySelector('.icon');
11
+ expect(span).to.exist;
12
+ });
13
+
14
+ it('applies size class', async () => {
15
+ const el = await fixture(`<app-icon name="check" size="lg"></app-icon>`);
16
+ await frame();
17
+ expect(el.querySelector('.icon-lg')).to.exist;
18
+ });
19
+
20
+ it('applies sm size class', async () => {
21
+ const el = await fixture(`<app-icon name="check" size="sm"></app-icon>`);
22
+ await frame();
23
+ expect(el.querySelector('.icon-sm')).to.exist;
24
+ });
25
+
26
+ it('reflects name property', async () => {
27
+ const el = await fixture(`<app-icon name="folder"></app-icon>`);
28
+ expect(el.name).to.equal('folder');
29
+ });
30
+ });
@@ -0,0 +1 @@
1
+ export { default } from './app-icon.js';
@@ -0,0 +1 @@
1
+ export { default } from './theme-toggle.js';
@@ -0,0 +1,10 @@
1
+ theme-toggle {
2
+ display: inline-block;
3
+ margin-left: auto;
4
+
5
+ & .theme-toggle-btn {
6
+ font-size: var(--text-lg);
7
+ padding: var(--space-xs) var(--space-sm);
8
+ line-height: 1;
9
+ }
10
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Theme toggle atom — switches between light and dark mode.
3
+ * Persists via AppState store (localStorage-backed).
4
+ * @module components/atoms/theme-toggle
5
+ */
6
+
7
+ import { html, define, store } from 'hybrids';
8
+ import AppState from '../../../store/AppState.js';
9
+
10
+ /**
11
+ * @typedef {Object} ThemeToggleHost
12
+ * @property {import('../../../store/AppState.js').AppState} state
13
+ */
14
+
15
+ /**
16
+ * @param {ThemeToggleHost & HTMLElement} host
17
+ */
18
+ function toggle(host) {
19
+ if (!store.ready(host.state)) return;
20
+ const next = host.state.theme === 'light' ? 'dark' : 'light';
21
+ store.set(host.state, { theme: next });
22
+ document.documentElement.setAttribute('data-theme', next);
23
+ }
24
+
25
+ /** @type {import('hybrids').Component<ThemeToggleHost>} */
26
+ export default define({
27
+ tag: 'theme-toggle',
28
+ state: store(AppState),
29
+ render: {
30
+ value: ({ state }) => {
31
+ if (store.ready(state)) {
32
+ document.documentElement.setAttribute('data-theme', state.theme);
33
+ }
34
+ return html`
35
+ <button class="btn btn-ghost theme-toggle-btn" onclick="${toggle}">
36
+ ${store.ready(state) && state.theme === 'light' ? '🌙' : '☀️'}
37
+ </button>
38
+ `;
39
+ },
40
+ shadow: false,
41
+ },
42
+ });
@@ -0,0 +1,79 @@
1
+ /* Global button styles — usable on any <button class="btn"> element.
2
+ Loaded in index.html via components.css or directly. */
3
+
4
+ .btn {
5
+ display: inline-flex;
6
+ align-items: center;
7
+ gap: var(--space-sm);
8
+ padding: var(--space-sm) var(--space-md);
9
+ border: none;
10
+ border-radius: var(--radius-md);
11
+ font: inherit;
12
+ font-size: var(--text-sm);
13
+ font-weight: 500;
14
+ cursor: pointer;
15
+ transition:
16
+ background 0.15s ease,
17
+ opacity 0.15s ease;
18
+ &:disabled {
19
+ opacity: 0.5;
20
+ cursor: not-allowed;
21
+ }
22
+ }
23
+
24
+ .btn-primary {
25
+ background: var(--color-primary);
26
+ color: var(--color-text-inverse);
27
+ &:hover:not(:disabled) {
28
+ background: var(--color-primary-hover);
29
+ }
30
+ }
31
+
32
+ .btn-secondary {
33
+ background: var(--color-surface);
34
+ color: var(--color-text);
35
+ border: 1px solid var(--color-border);
36
+ &:hover:not(:disabled) {
37
+ background: var(--color-bg);
38
+ }
39
+ }
40
+
41
+ .btn-ghost {
42
+ background: transparent;
43
+ color: var(--color-primary);
44
+ &:hover:not(:disabled) {
45
+ background: var(--color-bg);
46
+ }
47
+ }
48
+
49
+ .btn-success {
50
+ background: var(--color-success);
51
+ color: var(--color-text-inverse);
52
+ &:hover:not(:disabled) {
53
+ background: var(--color-success-hover);
54
+ }
55
+ }
56
+
57
+ .btn-warning {
58
+ background: var(--color-warning);
59
+ color: var(--color-text-inverse);
60
+ &:hover:not(:disabled) {
61
+ background: var(--color-warning-hover);
62
+ }
63
+ }
64
+
65
+ .btn-danger {
66
+ background: var(--color-danger);
67
+ color: var(--color-text-inverse);
68
+ &:hover:not(:disabled) {
69
+ background: var(--color-danger-hover);
70
+ }
71
+ }
72
+
73
+ .btn-info {
74
+ background: var(--color-info);
75
+ color: var(--color-text-inverse);
76
+ &:hover:not(:disabled) {
77
+ background: var(--color-info-hover);
78
+ }
79
+ }
@@ -0,0 +1,31 @@
1
+ /* Component styles aggregator.
2
+ Add one @import per component as they are created.
3
+ Loaded once in index.html — all light DOM components inherit these. */
4
+
5
+ /* Atoms */
6
+ @import '../components/atoms/app-button/app-button.css';
7
+ @import '../components/atoms/app-badge/app-badge.css';
8
+ @import '../components/atoms/app-icon/app-icon.css';
9
+ @import '../components/atoms/theme-toggle/theme-toggle.css';
10
+ @import '../components/atoms/canvas-toolbar/canvas-toolbar.css';
11
+
12
+ /* Molecules */
13
+ @import '../components/molecules/task-card/task-card.css';
14
+ @import '../components/molecules/project-card/project-card.css';
15
+ @import '../components/molecules/form-field/form-field.css';
16
+
17
+ /* Organisms */
18
+ @import '../components/organisms/task-list/task-list.css';
19
+ @import '../components/organisms/project-header/project-header.css';
20
+ @import '../components/organisms/schema-form/schema-form.css';
21
+ @import '../components/organisms/project-canvas/project-canvas.css';
22
+
23
+ /* Templates */
24
+ @import '../components/templates/page-layout/page-layout.css';
25
+
26
+ /* Pages */
27
+ @import '../pages/home/home-view.css';
28
+ @import '../pages/project/project-view.css';
29
+
30
+ /* Router */
31
+ @import '../router/router.css';
@@ -0,0 +1,20 @@
1
+ /* Shared form input styles.
2
+ Loaded globally in index.html. */
3
+
4
+ input,
5
+ select,
6
+ textarea {
7
+ font: inherit;
8
+ padding: var(--space-sm);
9
+ border: 1px solid var(--color-border);
10
+ border-radius: var(--radius-md);
11
+ background: var(--color-surface);
12
+ color: var(--color-text);
13
+ transition: border-color 0.15s ease;
14
+
15
+ &:focus {
16
+ outline: none;
17
+ border-color: var(--color-primary);
18
+ box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.15);
19
+ }
20
+ }
@@ -0,0 +1,32 @@
1
+ *,
2
+ *::before,
3
+ *::after {
4
+ box-sizing: border-box;
5
+ margin: 0;
6
+ padding: 0;
7
+ }
8
+ html {
9
+ font-family: var(--font-sans);
10
+ line-height: 1.5;
11
+ color: var(--color-text);
12
+ background: var(--color-bg);
13
+ }
14
+ body {
15
+ min-height: 100vh;
16
+ }
17
+ a {
18
+ color: var(--color-primary);
19
+ text-decoration: none;
20
+ }
21
+ a:hover {
22
+ text-decoration: underline;
23
+ }
24
+ img,
25
+ svg {
26
+ display: block;
27
+ max-width: 100%;
28
+ }
29
+ button {
30
+ font: inherit;
31
+ cursor: pointer;
32
+ }