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:
14
docs/design/components/feedback/Dialog.d-ts.txt
Normal file
14
docs/design/components/feedback/Dialog.d-ts.txt
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Modal dialog — Caslon title over a double stationery rule; used for propose-approve
|
||||
* moments and destructive confirms.
|
||||
*/
|
||||
export interface DialogProps {
|
||||
open: boolean;
|
||||
onClose?: () => void;
|
||||
title: React.ReactNode;
|
||||
/** Right-aligned action row (Buttons) */
|
||||
footer?: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
export declare function Dialog(props: DialogProps): JSX.Element | null;
|
||||
68
docs/design/components/feedback/Dialog.js.txt
Normal file
68
docs/design/components/feedback/Dialog.js.txt
Normal file
@@ -0,0 +1,68 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '../core/IconButton';
|
||||
|
||||
const CSS = `
|
||||
.ct-dialog-scrim {
|
||||
position: fixed; inset: 0;
|
||||
background: rgba(32, 38, 29, 0.4);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
z-index: 100;
|
||||
animation: ct-dialog-fade var(--duration-base) var(--ease-out);
|
||||
}
|
||||
.ct-dialog {
|
||||
background: var(--surface-card);
|
||||
border: 1px solid var(--border-hairline);
|
||||
border-radius: var(--radius-3);
|
||||
box-shadow: var(--shadow-3);
|
||||
width: min(480px, calc(100vw - 48px));
|
||||
max-height: calc(100vh - 96px);
|
||||
display: flex; flex-direction: column;
|
||||
animation: ct-dialog-rise var(--duration-base) var(--ease-out);
|
||||
}
|
||||
.ct-dialog__header {
|
||||
display: flex; align-items: flex-start; justify-content: space-between; gap: 12px;
|
||||
padding: 20px 20px 12px;
|
||||
border-bottom: 3px double var(--border-strong);
|
||||
margin: 0 20px; padding-left: 0; padding-right: 0;
|
||||
}
|
||||
.ct-dialog__title { font: var(--text-title); color: var(--ink-1); margin: 0; }
|
||||
.ct-dialog__body { padding: 16px 20px; overflow-y: auto; font: var(--text-body); color: var(--ink-1); }
|
||||
.ct-dialog__footer {
|
||||
display: flex; justify-content: flex-end; gap: 8px;
|
||||
padding: 12px 20px 20px;
|
||||
}
|
||||
@keyframes ct-dialog-fade { from { opacity: 0; } }
|
||||
@keyframes ct-dialog-rise { from { opacity: 0; transform: translateY(8px); } }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.ct-dialog-scrim, .ct-dialog { animation: none; }
|
||||
}
|
||||
`;
|
||||
(function inject() {
|
||||
if (typeof document !== 'undefined' && !document.getElementById('ct-dialog-css')) {
|
||||
const s = document.createElement('style'); s.id = 'ct-dialog-css'; s.textContent = CSS;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
})();
|
||||
|
||||
export function Dialog({ open, onClose, title, footer, children, style }) {
|
||||
React.useEffect(() => {
|
||||
if (!open) return;
|
||||
const onKey = (e) => { if (e.key === 'Escape' && onClose) onClose(); };
|
||||
document.addEventListener('keydown', onKey);
|
||||
return () => document.removeEventListener('keydown', onKey);
|
||||
}, [open, onClose]);
|
||||
|
||||
if (!open) return null;
|
||||
return (
|
||||
<div className="ct-dialog-scrim" onClick={(e) => { if (e.target === e.currentTarget && onClose) onClose(); }}>
|
||||
<div className="ct-dialog" role="dialog" aria-modal="true" style={style}>
|
||||
<header className="ct-dialog__header">
|
||||
<h2 className="ct-dialog__title">{title}</h2>
|
||||
{onClose ? <IconButton icon="x" label="Close" size="sm" onClick={onClose} /> : null}
|
||||
</header>
|
||||
<div className="ct-dialog__body">{children}</div>
|
||||
{footer ? <footer className="ct-dialog__footer">{footer}</footer> : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
17
docs/design/components/feedback/Dialog.prompt.md
Normal file
17
docs/design/components/feedback/Dialog.prompt.md
Normal file
@@ -0,0 +1,17 @@
|
||||
Modal for propose-approve and destructive confirms; the title sits over a double stationery rule.
|
||||
|
||||
```jsx
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={close}
|
||||
title="Delete milestone"
|
||||
footer={<>
|
||||
<Button variant="ghost" onClick={close}>Keep it</Button>
|
||||
<Button variant="danger" onClick={confirm}>Yes, delete</Button>
|
||||
</>}
|
||||
>
|
||||
This deletes the milestone and unhouses 12 issues. I'd like to hear you say yes.
|
||||
</Dialog>
|
||||
```
|
||||
|
||||
Body copy may be Reginald's (serif if quoted). Escape and scrim-click close it.
|
||||
12
docs/design/components/feedback/Toast.d-ts.txt
Normal file
12
docs/design/components/feedback/Toast.d-ts.txt
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Transient notification card. Presentational — the consumer owns positioning/stacking.
|
||||
*/
|
||||
export interface ToastProps {
|
||||
/** @default 'info' */
|
||||
tone?: 'ok' | 'warn' | 'danger' | 'info';
|
||||
title?: React.ReactNode;
|
||||
onDismiss?: () => void;
|
||||
children?: React.ReactNode;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
export declare function Toast(props: ToastProps): JSX.Element;
|
||||
53
docs/design/components/feedback/Toast.js.txt
Normal file
53
docs/design/components/feedback/Toast.js.txt
Normal file
@@ -0,0 +1,53 @@
|
||||
import React from 'react';
|
||||
import { Icon } from '../core/Icon';
|
||||
import { IconButton } from '../core/IconButton';
|
||||
|
||||
const CSS = `
|
||||
.ct-toast {
|
||||
display: flex; align-items: flex-start; gap: 10px;
|
||||
background: var(--surface-card);
|
||||
border: 1px solid var(--border-hairline);
|
||||
border-radius: var(--radius-2);
|
||||
box-shadow: var(--shadow-2);
|
||||
padding: 12px 14px;
|
||||
max-width: 420px;
|
||||
font: var(--text-body);
|
||||
color: var(--ink-1);
|
||||
animation: ct-toast-in var(--duration-slow) var(--ease-out);
|
||||
}
|
||||
.ct-toast__icon { display: flex; margin-top: 1px; }
|
||||
.ct-toast--ok .ct-toast__icon { color: var(--ok); }
|
||||
.ct-toast--warn .ct-toast__icon { color: var(--warn); }
|
||||
.ct-toast--danger .ct-toast__icon { color: var(--danger); }
|
||||
.ct-toast--info .ct-toast__icon { color: var(--info); }
|
||||
.ct-toast__content { flex: 1; min-width: 0; }
|
||||
.ct-toast__title { font: var(--text-body-strong); margin: 0 0 2px; }
|
||||
@keyframes ct-toast-in { from { opacity: 0; transform: translateY(6px); } }
|
||||
@media (prefers-reduced-motion: reduce) { .ct-toast { animation: none; } }
|
||||
`;
|
||||
(function inject() {
|
||||
if (typeof document !== 'undefined' && !document.getElementById('ct-toast-css')) {
|
||||
const s = document.createElement('style'); s.id = 'ct-toast-css'; s.textContent = CSS;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
})();
|
||||
|
||||
const TOAST_ICONS = {
|
||||
ok: 'circle-check',
|
||||
warn: 'triangle-alert',
|
||||
danger: 'circle-alert',
|
||||
info: 'info',
|
||||
};
|
||||
|
||||
export function Toast({ tone = 'info', title, onDismiss, children, style, ...rest }) {
|
||||
return (
|
||||
<div className={`ct-toast ct-toast--${tone}`} role="status" style={style} {...rest}>
|
||||
<span className="ct-toast__icon"><Icon name={TOAST_ICONS[tone]} size={16} /></span>
|
||||
<div className="ct-toast__content">
|
||||
{title ? <p className="ct-toast__title">{title}</p> : null}
|
||||
{children}
|
||||
</div>
|
||||
{onDismiss ? <IconButton icon="x" label="Dismiss" size="sm" onClick={onDismiss} /> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
10
docs/design/components/feedback/Toast.prompt.md
Normal file
10
docs/design/components/feedback/Toast.prompt.md
Normal file
@@ -0,0 +1,10 @@
|
||||
Notification card for sync events and quiet agent asides; position/stacking is the consumer's job (fixed bottom-right, 12px gap).
|
||||
|
||||
```jsx
|
||||
<Toast tone="ok" title="Reconcile finished" onDismiss={hide}>
|
||||
500 issues in 3.2s. Nothing drifted.
|
||||
</Toast>
|
||||
<Toast tone="warn" title="Gitea unreachable">
|
||||
I'll keep trying and say nothing more about it.
|
||||
</Toast>
|
||||
```
|
||||
11
docs/design/components/feedback/Tooltip.d-ts.txt
Normal file
11
docs/design/components/feedback/Tooltip.d-ts.txt
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Hover/focus tooltip — ink capsule, plain facts only.
|
||||
*/
|
||||
export interface TooltipProps {
|
||||
content: React.ReactNode;
|
||||
/** @default 'top' */
|
||||
side?: 'top' | 'bottom';
|
||||
children?: React.ReactNode;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
export declare function Tooltip(props: TooltipProps): JSX.Element;
|
||||
43
docs/design/components/feedback/Tooltip.js.txt
Normal file
43
docs/design/components/feedback/Tooltip.js.txt
Normal file
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
|
||||
const CSS = `
|
||||
.ct-tooltip-wrap { position: relative; display: inline-flex; }
|
||||
.ct-tooltip {
|
||||
position: absolute; bottom: calc(100% + 7px); left: 50%;
|
||||
transform: translateX(-50%) translateY(2px);
|
||||
background: var(--ink-1);
|
||||
color: var(--ink-inverse);
|
||||
font: 500 12px/1.4 var(--font-sans);
|
||||
padding: 5px 9px;
|
||||
border-radius: var(--radius-1);
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity var(--duration-fast) var(--ease-out), transform var(--duration-fast) var(--ease-out);
|
||||
z-index: 50;
|
||||
}
|
||||
.ct-tooltip--bottom { bottom: auto; top: calc(100% + 7px); transform: translateX(-50%) translateY(-2px); }
|
||||
.ct-tooltip-wrap:hover .ct-tooltip,
|
||||
.ct-tooltip-wrap:focus-within .ct-tooltip {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
.ct-tooltip code { font: 500 11px var(--font-mono); }
|
||||
`;
|
||||
(function inject() {
|
||||
if (typeof document !== 'undefined' && !document.getElementById('ct-tooltip-css')) {
|
||||
const s = document.createElement('style'); s.id = 'ct-tooltip-css'; s.textContent = CSS;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
})();
|
||||
|
||||
export function Tooltip({ content, side = 'top', children, style }) {
|
||||
return (
|
||||
<span className="ct-tooltip-wrap" style={style}>
|
||||
{children}
|
||||
<span className={`ct-tooltip${side === 'bottom' ? ' ct-tooltip--bottom' : ''}`} role="tooltip">
|
||||
{content}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
8
docs/design/components/feedback/Tooltip.prompt.md
Normal file
8
docs/design/components/feedback/Tooltip.prompt.md
Normal file
@@ -0,0 +1,8 @@
|
||||
Tooltip for icon buttons and truncated data; content is plain facts, never wit.
|
||||
|
||||
```jsx
|
||||
<Tooltip content="Critical path: 4 issues">
|
||||
<Icon name="network" size={16} />
|
||||
</Tooltip>
|
||||
<Tooltip content={<>opened <code>2026-07-01</code></>} side="bottom">…</Tooltip>
|
||||
```
|
||||
52
docs/design/components/feedback/feedback.card.html
Normal file
52
docs/design/components/feedback/feedback.card.html
Normal file
@@ -0,0 +1,52 @@
|
||||
<!-- 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; }
|
||||
/* keep the dialog demo inside the card */
|
||||
.dialog-stage { position: relative; height: 218px; overflow: hidden; border-radius: var(--radius-3); }
|
||||
.dialog-stage .ct-dialog-scrim { position: absolute; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/babel">
|
||||
const { Dialog, Toast, Tooltip, Button, Icon } = window.CommiTeaDesignSystem_20e63b;
|
||||
|
||||
function Demo() {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
|
||||
<div style={{ display: 'flex', gap: 10, alignItems: 'flex-start', flexWrap: 'wrap' }}>
|
||||
<Toast tone="ok" title="Reconcile finished" onDismiss={() => {}}>500 issues in 3.2s. Nothing drifted.</Toast>
|
||||
<Toast tone="warn" title="Gitea unreachable">I'll keep trying and say nothing more about it.</Toast>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 18, alignItems: 'center' }}>
|
||||
<Tooltip content="Critical path: 4 issues">
|
||||
<span style={{ display: 'inline-flex', color: 'var(--ink-2)' }}><Icon name="network" size={18} /></span>
|
||||
</Tooltip>
|
||||
<span style={{ font: 'var(--text-caption)', color: 'var(--ink-3)' }}>← hover for Tooltip</span>
|
||||
</div>
|
||||
<div className="dialog-stage">
|
||||
<Dialog open title="Delete milestone"
|
||||
onClose={() => {}}
|
||||
footer={<>
|
||||
<Button variant="ghost">Keep it</Button>
|
||||
<Button variant="danger">Yes, delete</Button>
|
||||
</>}>
|
||||
This deletes the milestone and unhouses 12 issues. I'd like to hear you say yes.
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(<Demo />);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user