Scaffold CommiTea: yarn workspaces, Electron shell, core label schema, design system

- apps/desktop: electron-vite + React + Tailwind mapped onto design tokens
  (preflight off; tokens/base.css owns the reset); boots to a Reginald
  placeholder proving fonts/tokens/core wiring
- packages/core: pure TS; gitea label schema (est/*, p/*, deadline/hard)
  with pessimistic conflict resolution + 15 unit tests
- docs/design: full design handoff (tokens, 16 component contracts,
  interactive 14-screen prototype, Reginald voice rules)
- docs/PLAN.md: product plan (purity rule, pm-state repo, deterministic
  scheduler + Monte Carlo, directive log)
- Deliberate deviation from novelpad stack: no ElectricSQL/PGlite — local
  store is a rebuildable cache over gitea REST/webhooks (better-sqlite3
  in main process, arriving in P1)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Christian LeDoux
2026-07-07 20:42:46 -04:00
commit 7a5cacc54c
268 changed files with 15766 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
/**
* Checkbox with label; 16px box, spruce when checked.
*/
export interface CheckboxProps {
label?: React.ReactNode;
checked?: boolean;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
disabled?: boolean;
style?: React.CSSProperties;
}
export declare function Checkbox(props: CheckboxProps): JSX.Element;

View File

@@ -0,0 +1,43 @@
import React from 'react';
import { Icon } from '../core/Icon';
const CSS = `
.ct-check { display: inline-flex; align-items: center; gap: 9px; cursor: pointer; font: var(--text-body); color: var(--ink-1); }
.ct-check--disabled { opacity: 0.5; cursor: not-allowed; }
.ct-check__input { position: absolute; opacity: 0; width: 0; height: 0; }
.ct-check__box {
width: 16px; height: 16px; flex-shrink: 0;
display: inline-flex; align-items: center; justify-content: center;
background: var(--surface-card);
border: 1px solid var(--line-2);
border-radius: var(--radius-1);
color: transparent;
transition: background var(--duration-fast) var(--ease-out), border-color var(--duration-fast) var(--ease-out);
}
.ct-check:hover:not(.ct-check--disabled) .ct-check__box { border-color: var(--border-strong); }
.ct-check__input:checked + .ct-check__box { background: var(--accent); border-color: var(--accent); color: var(--ink-inverse); }
.ct-check__input:focus-visible + .ct-check__box { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
`;
(function inject() {
if (typeof document !== 'undefined' && !document.getElementById('ct-check-css')) {
const s = document.createElement('style'); s.id = 'ct-check-css'; s.textContent = CSS;
document.head.appendChild(s);
}
})();
export function Checkbox({ label, checked, onChange, disabled = false, style, ...rest }) {
return (
<label className={`ct-check${disabled ? ' ct-check--disabled' : ''}`} style={style}>
<input
type="checkbox"
className="ct-check__input"
checked={checked}
onChange={onChange}
disabled={disabled}
{...rest}
/>
<span className="ct-check__box"><Icon name="check" size={12} strokeWidth={2.5} /></span>
{label ? <span>{label}</span> : null}
</label>
);
}

View File

@@ -0,0 +1,5 @@
Checkbox for multi-selects and settings toggles that read as options (use `Switch` for live on/off state).
```jsx
<Checkbox label="Include closed issues" checked={inc} onChange={(e) => setInc(e.target.checked)} />
```

View File

@@ -0,0 +1,21 @@
/**
* Single-line text input with optional label, hint/error and leading icon.
*/
export interface InputProps {
label?: string;
/** Small gray helper under the field */
hint?: string;
/** Replaces hint; turns the border madder red */
error?: string;
/** Lucide icon name, leading */
icon?: string;
/** Mono text for machine values (urls, tokens) @default false */
mono?: boolean;
placeholder?: string;
value?: string;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
disabled?: boolean;
type?: string;
style?: React.CSSProperties;
}
export declare function Input(props: InputProps): JSX.Element;

View File

@@ -0,0 +1,54 @@
import React from 'react';
import { Icon } from '../core/Icon';
const CSS = `
.ct-field { display: flex; flex-direction: column; gap: 6px; }
.ct-field__label { font: 600 13px/1.2 var(--font-sans); color: var(--ink-1); }
.ct-field__wrap { position: relative; display: flex; align-items: center; }
.ct-field__icon { position: absolute; left: 10px; color: var(--ink-3); pointer-events: none; display: flex; }
.ct-input {
width: 100%; height: 34px;
font: var(--text-body);
color: var(--ink-1);
background: var(--surface-card);
border: 1px solid var(--line-2);
border-radius: var(--radius-2);
padding: 0 12px;
transition: border-color var(--duration-fast) var(--ease-out);
}
.ct-input::placeholder { color: var(--ink-3); }
.ct-input:hover:not(:disabled):not(:focus) { border-color: var(--border-strong); background: var(--paper-1); }
.ct-input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.ct-input:disabled { opacity: 0.5; background: var(--paper-2); cursor: not-allowed; }
.ct-input--icon { padding-left: 32px; }
.ct-input--error { border-color: var(--danger); }
.ct-input--error:focus { border-color: var(--danger); box-shadow: 0 0 0 1px var(--danger); }
.ct-input--mono { font: var(--text-data); }
.ct-field__hint { font: var(--text-caption); color: var(--ink-3); margin: 0; }
.ct-field__error { font: var(--text-caption); color: var(--danger); margin: 0; }
`;
(function inject() {
if (typeof document !== 'undefined' && !document.getElementById('ct-input-css')) {
const s = document.createElement('style'); s.id = 'ct-input-css'; s.textContent = CSS;
document.head.appendChild(s);
}
})();
export function Input({ label, hint, error, icon, mono = false, style, ...rest }) {
const cls = [
'ct-input',
icon ? 'ct-input--icon' : '',
error ? 'ct-input--error' : '',
mono ? 'ct-input--mono' : '',
].filter(Boolean).join(' ');
return (
<div className="ct-field" style={style}>
{label ? <label className="ct-field__label">{label}</label> : null}
<div className="ct-field__wrap">
{icon ? <span className="ct-field__icon"><Icon name={icon} size={15} /></span> : null}
<input className={cls} {...rest} />
</div>
{error ? <p className="ct-field__error">{error}</p> : hint ? <p className="ct-field__hint">{hint}</p> : null}
</div>
);
}

View File

@@ -0,0 +1,8 @@
Text input; focus is a spruce border, errors are stated plainly.
```jsx
<Input label="Gitea base URL" icon="link" mono placeholder="https://gitea.example.io" />
<Input label="Milestone name" error="Every milestone needs a name. Even a bad one." />
```
`mono` for machine values (URLs, tokens, label strings). Error copy may carry the wit; the field itself stays sober.

View File

@@ -0,0 +1,13 @@
/**
* Radio button with label; group by `name`.
*/
export interface RadioProps {
label?: React.ReactNode;
checked?: boolean;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
name?: string;
value?: string;
disabled?: boolean;
style?: React.CSSProperties;
}
export declare function Radio(props: RadioProps): JSX.Element;

View File

@@ -0,0 +1,50 @@
import React from 'react';
const CSS = `
.ct-radio { display: inline-flex; align-items: center; gap: 9px; cursor: pointer; font: var(--text-body); color: var(--ink-1); }
.ct-radio--disabled { opacity: 0.5; cursor: not-allowed; }
.ct-radio__input { position: absolute; opacity: 0; width: 0; height: 0; }
.ct-radio__dot {
width: 16px; height: 16px; flex-shrink: 0;
border: 1px solid var(--line-2);
border-radius: 50%;
background: var(--surface-card);
display: inline-flex; align-items: center; justify-content: center;
transition: border-color var(--duration-fast) var(--ease-out);
}
.ct-radio__dot::after {
content: '';
width: 8px; height: 8px; border-radius: 50%;
background: transparent;
transition: background var(--duration-fast) var(--ease-out);
}
.ct-radio:hover:not(.ct-radio--disabled) .ct-radio__dot { border-color: var(--border-strong); }
.ct-radio__input:checked + .ct-radio__dot { border-color: var(--accent); }
.ct-radio__input:checked + .ct-radio__dot::after { background: var(--accent); }
.ct-radio__input:focus-visible + .ct-radio__dot { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
`;
(function inject() {
if (typeof document !== 'undefined' && !document.getElementById('ct-radio-css')) {
const s = document.createElement('style'); s.id = 'ct-radio-css'; s.textContent = CSS;
document.head.appendChild(s);
}
})();
export function Radio({ label, checked, onChange, name, value, disabled = false, style, ...rest }) {
return (
<label className={`ct-radio${disabled ? ' ct-radio--disabled' : ''}`} style={style}>
<input
type="radio"
className="ct-radio__input"
checked={checked}
onChange={onChange}
name={name}
value={value}
disabled={disabled}
{...rest}
/>
<span className="ct-radio__dot"></span>
{label ? <span>{label}</span> : null}
</label>
);
}

View File

@@ -0,0 +1,6 @@
Radio for one-of choices ("deadline: hard or soft?" — Reginald asks at milestone creation).
```jsx
<Radio name="deadline" value="hard" label="Hard — the date matters more than the scope" checked={d === 'hard'} onChange={set} />
<Radio name="deadline" value="soft" label="Soft — the scope matters more than the date" checked={d === 'soft'} onChange={set} />
```

View File

@@ -0,0 +1,16 @@
/**
* Styled native select with Lucide chevron.
*/
export interface SelectOption {
value: string;
label: string;
}
export interface SelectProps {
label?: string;
options: SelectOption[];
value?: string;
onChange?: (e: React.ChangeEvent<HTMLSelectElement>) => void;
disabled?: boolean;
style?: React.CSSProperties;
}
export declare function Select(props: SelectProps): JSX.Element;

View File

@@ -0,0 +1,46 @@
import React from 'react';
import { Icon } from '../core/Icon';
const CSS = `
.ct-select-field { display: flex; flex-direction: column; gap: 6px; }
.ct-select-field__label { font: 600 13px/1.2 var(--font-sans); color: var(--ink-1); }
.ct-select__wrap { position: relative; display: flex; align-items: center; }
.ct-select {
width: 100%; height: 34px;
font: var(--text-body);
color: var(--ink-1);
background: var(--surface-card);
border: 1px solid var(--line-2);
border-radius: var(--radius-2);
padding: 0 30px 0 12px;
appearance: none;
cursor: pointer;
transition: border-color var(--duration-fast) var(--ease-out);
}
.ct-select:hover:not(:disabled):not(:focus) { border-color: var(--border-strong); }
.ct-select:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.ct-select:disabled { opacity: 0.5; cursor: not-allowed; }
.ct-select__chevron { position: absolute; right: 10px; color: var(--ink-3); pointer-events: none; display: flex; }
`;
(function inject() {
if (typeof document !== 'undefined' && !document.getElementById('ct-select-css')) {
const s = document.createElement('style'); s.id = 'ct-select-css'; s.textContent = CSS;
document.head.appendChild(s);
}
})();
export function Select({ label, options, style, ...rest }) {
return (
<div className="ct-select-field" style={style}>
{label ? <label className="ct-select-field__label">{label}</label> : null}
<div className="ct-select__wrap">
<select className="ct-select" {...rest}>
{options.map((o) => (
<option key={o.value} value={o.value}>{o.label}</option>
))}
</select>
<span className="ct-select__chevron"><Icon name="chevron-down" size={15} /></span>
</div>
</div>
);
}

View File

@@ -0,0 +1,14 @@
Native select, restyled; use for small closed sets (estimate labels, priority, model role).
```jsx
<Select
label="Estimate"
options={[
{ value: 'est/1d', label: 'est/1d' },
{ value: 'est/2d', label: 'est/2d' },
{ value: 'est/3d', label: 'est/3d' },
]}
value={est}
onChange={(e) => setEst(e.target.value)}
/>
```

View File

@@ -0,0 +1,11 @@
/**
* On/off switch; spruce when on. For live state, not form options.
*/
export interface SwitchProps {
label?: React.ReactNode;
checked?: boolean;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
disabled?: boolean;
style?: React.CSSProperties;
}
export declare function Switch(props: SwitchProps): JSX.Element;

View File

@@ -0,0 +1,50 @@
import React from 'react';
const CSS = `
.ct-switch { display: inline-flex; align-items: center; gap: 9px; cursor: pointer; font: var(--text-body); color: var(--ink-1); }
.ct-switch--disabled { opacity: 0.5; cursor: not-allowed; }
.ct-switch__input { position: absolute; opacity: 0; width: 0; height: 0; }
.ct-switch__track {
width: 34px; height: 20px; flex-shrink: 0;
border-radius: var(--radius-round);
background: var(--paper-3);
border: 1px solid var(--line-2);
position: relative;
transition: background var(--duration-base) var(--ease-out), border-color var(--duration-base) var(--ease-out);
}
.ct-switch__track::after {
content: '';
position: absolute; top: 2px; left: 2px;
width: 14px; height: 14px; border-radius: 50%;
background: var(--surface-card);
box-shadow: var(--shadow-1);
transition: transform var(--duration-base) var(--ease-out);
}
.ct-switch__input:checked + .ct-switch__track { background: var(--accent); border-color: var(--accent); }
.ct-switch__input:checked + .ct-switch__track::after { transform: translateX(14px); }
.ct-switch__input:focus-visible + .ct-switch__track { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
`;
(function inject() {
if (typeof document !== 'undefined' && !document.getElementById('ct-switch-css')) {
const s = document.createElement('style'); s.id = 'ct-switch-css'; s.textContent = CSS;
document.head.appendChild(s);
}
})();
export function Switch({ label, checked, onChange, disabled = false, style, ...rest }) {
return (
<label className={`ct-switch${disabled ? ' ct-switch--disabled' : ''}`} style={style}>
<input
type="checkbox"
role="switch"
className="ct-switch__input"
checked={checked}
onChange={onChange}
disabled={disabled}
{...rest}
/>
<span className="ct-switch__track"></span>
{label ? <span>{label}</span> : null}
</label>
);
}

View File

@@ -0,0 +1,5 @@
Switch for live on/off state (webhooks, morning service, theme). Motion is settled — 200ms ease-out slide.
```jsx
<Switch label="Morning service" checked={on} onChange={(e) => setOn(e.target.checked)} />
```

View File

@@ -0,0 +1,48 @@
<!-- handoff copy (card tag removed) -->
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="../../styles.css">
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
<script src="../../_ds_bundle.js"></script>
<style>
body { padding: 18px 22px; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { Input, Select, Checkbox, Radio, Switch } = window.CommiTeaDesignSystem_20e63b;
function Demo() {
const [est, setEst] = React.useState('est/3d');
const [inc, setInc] = React.useState(true);
const [dl, setDl] = React.useState('hard');
const [svc, setSvc] = React.useState(true);
return (
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px 20px' }}>
<Input label="Gitea base URL" icon="link" mono defaultValue="https://gitea.stephenmann.io" />
<Input label="Milestone name" placeholder="Beta" error="Every milestone needs a name. Even a bad one." />
<Select label="Estimate" options={['est/1d','est/2d','est/3d','est/5d','est/8d'].map(v => ({ value: v, label: v }))}
value={est} onChange={(e) => setEst(e.target.value)} />
<Input label="Search" icon="search" placeholder="Search the pot…" />
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
<Checkbox label="Include closed issues" checked={inc} onChange={(e) => setInc(e.target.checked)} />
<Checkbox label="Disabled option" disabled />
<Switch label="Morning service" checked={svc} onChange={(e) => setSvc(e.target.checked)} />
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
<Radio name="dl" value="hard" label="Hard deadline" checked={dl === 'hard'} onChange={() => setDl('hard')} />
<Radio name="dl" value="soft" label="Soft deadline" checked={dl === 'soft'} onChange={() => setDl('soft')} />
<Switch label="Off state" />
</div>
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<Demo />);
</script>
</body>
</html>