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:
11
docs/design/components/forms/Checkbox.d-ts.txt
Normal file
11
docs/design/components/forms/Checkbox.d-ts.txt
Normal 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;
|
||||
43
docs/design/components/forms/Checkbox.js.txt
Normal file
43
docs/design/components/forms/Checkbox.js.txt
Normal 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>
|
||||
);
|
||||
}
|
||||
5
docs/design/components/forms/Checkbox.prompt.md
Normal file
5
docs/design/components/forms/Checkbox.prompt.md
Normal 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)} />
|
||||
```
|
||||
21
docs/design/components/forms/Input.d-ts.txt
Normal file
21
docs/design/components/forms/Input.d-ts.txt
Normal 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;
|
||||
54
docs/design/components/forms/Input.js.txt
Normal file
54
docs/design/components/forms/Input.js.txt
Normal 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>
|
||||
);
|
||||
}
|
||||
8
docs/design/components/forms/Input.prompt.md
Normal file
8
docs/design/components/forms/Input.prompt.md
Normal 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.
|
||||
13
docs/design/components/forms/Radio.d-ts.txt
Normal file
13
docs/design/components/forms/Radio.d-ts.txt
Normal 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;
|
||||
50
docs/design/components/forms/Radio.js.txt
Normal file
50
docs/design/components/forms/Radio.js.txt
Normal 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>
|
||||
);
|
||||
}
|
||||
6
docs/design/components/forms/Radio.prompt.md
Normal file
6
docs/design/components/forms/Radio.prompt.md
Normal 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} />
|
||||
```
|
||||
16
docs/design/components/forms/Select.d-ts.txt
Normal file
16
docs/design/components/forms/Select.d-ts.txt
Normal 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;
|
||||
46
docs/design/components/forms/Select.js.txt
Normal file
46
docs/design/components/forms/Select.js.txt
Normal 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>
|
||||
);
|
||||
}
|
||||
14
docs/design/components/forms/Select.prompt.md
Normal file
14
docs/design/components/forms/Select.prompt.md
Normal 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)}
|
||||
/>
|
||||
```
|
||||
11
docs/design/components/forms/Switch.d-ts.txt
Normal file
11
docs/design/components/forms/Switch.d-ts.txt
Normal 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;
|
||||
50
docs/design/components/forms/Switch.js.txt
Normal file
50
docs/design/components/forms/Switch.js.txt
Normal 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>
|
||||
);
|
||||
}
|
||||
5
docs/design/components/forms/Switch.prompt.md
Normal file
5
docs/design/components/forms/Switch.prompt.md
Normal 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)} />
|
||||
```
|
||||
48
docs/design/components/forms/forms.card.html
Normal file
48
docs/design/components/forms/forms.card.html
Normal 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>
|
||||
Reference in New Issue
Block a user