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:
12
docs/design/components/core/Badge.d-ts.txt
Normal file
12
docs/design/components/core/Badge.d-ts.txt
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Status pill — muted tint background, colored text. For state words like "ahead", "behind", "steeping".
|
||||
*/
|
||||
export interface BadgeProps {
|
||||
/** @default 'neutral' */
|
||||
tone?: 'ok' | 'warn' | 'danger' | 'info' | 'neutral' | 'jade';
|
||||
/** Leading status dot @default false */
|
||||
dot?: boolean;
|
||||
children?: React.ReactNode;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
export declare function Badge(props: BadgeProps): JSX.Element;
|
||||
32
docs/design/components/core/Badge.js.txt
Normal file
32
docs/design/components/core/Badge.js.txt
Normal file
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
|
||||
const CSS = `
|
||||
.ct-badge {
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
font: 500 12px/1 var(--font-sans);
|
||||
padding: 4px 9px;
|
||||
border-radius: var(--radius-round);
|
||||
}
|
||||
.ct-badge__dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
||||
.ct-badge--ok { background: var(--ok-tint); color: var(--ok); }
|
||||
.ct-badge--warn { background: var(--warn-tint); color: var(--warn); }
|
||||
.ct-badge--danger { background: var(--danger-tint); color: var(--danger); }
|
||||
.ct-badge--info { background: var(--info-tint); color: var(--info); }
|
||||
.ct-badge--neutral { background: var(--paper-2); color: var(--ink-2); }
|
||||
.ct-badge--jade { background: var(--jade-tint); color: var(--jade-7); }
|
||||
`;
|
||||
(function inject() {
|
||||
if (typeof document !== 'undefined' && !document.getElementById('ct-badge-css')) {
|
||||
const s = document.createElement('style'); s.id = 'ct-badge-css'; s.textContent = CSS;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
})();
|
||||
|
||||
export function Badge({ tone = 'neutral', dot = false, children, style, ...rest }) {
|
||||
return (
|
||||
<span className={`ct-badge ct-badge--${tone}`} style={style} {...rest}>
|
||||
{dot ? <span className="ct-badge__dot"></span> : null}
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
9
docs/design/components/core/Badge.prompt.md
Normal file
9
docs/design/components/core/Badge.prompt.md
Normal file
@@ -0,0 +1,9 @@
|
||||
Status pill for state words (ahead / behind / steeping / blocked); sans text on a muted tint.
|
||||
|
||||
```jsx
|
||||
<Badge tone="ok" dot>ahead</Badge>
|
||||
<Badge tone="warn" dot>drifting</Badge>
|
||||
<Badge tone="danger">blocked</Badge>
|
||||
```
|
||||
|
||||
Tones: ok, warn, danger, info, neutral, jade (jade = agent/forecast flavor). Use `Tag` instead for verbatim gitea labels like `est/3d`.
|
||||
18
docs/design/components/core/Button.d-ts.txt
Normal file
18
docs/design/components/core/Button.d-ts.txt
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* CommiTea button. Plain verbs only ("Approve", "Brew plan", "Defer") — never witty.
|
||||
*/
|
||||
export interface ButtonProps {
|
||||
/** @default 'primary' */
|
||||
variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
|
||||
/** @default 'md' */
|
||||
size?: 'sm' | 'md';
|
||||
/** Lucide icon name rendered before the label */
|
||||
icon?: string;
|
||||
/** Lucide icon name rendered after the label */
|
||||
iconRight?: string;
|
||||
disabled?: boolean;
|
||||
onClick?: (e: React.MouseEvent) => void;
|
||||
children?: React.ReactNode;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
export declare function Button(props: ButtonProps): JSX.Element;
|
||||
63
docs/design/components/core/Button.js.txt
Normal file
63
docs/design/components/core/Button.js.txt
Normal file
@@ -0,0 +1,63 @@
|
||||
import React from 'react';
|
||||
|
||||
const CSS = `
|
||||
.ct-btn {
|
||||
display: inline-flex; align-items: center; justify-content: center; gap: 8px;
|
||||
font: 600 14px/1 var(--font-sans);
|
||||
border-radius: var(--radius-2);
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: background var(--duration-fast) var(--ease-out),
|
||||
border-color var(--duration-fast) var(--ease-out),
|
||||
color var(--duration-fast) var(--ease-out);
|
||||
}
|
||||
.ct-btn:disabled { opacity: 0.45; cursor: not-allowed; }
|
||||
.ct-btn--md { height: 34px; padding: 0 14px; }
|
||||
.ct-btn--sm { height: 28px; padding: 0 10px; font-size: 13px; }
|
||||
.ct-btn--primary { background: var(--accent); color: var(--text-on-accent); }
|
||||
.ct-btn--primary:hover:not(:disabled) { background: var(--accent-hover); }
|
||||
.ct-btn--primary:active:not(:disabled) { background: var(--accent-pressed); }
|
||||
.ct-btn--secondary { background: var(--surface-card); border-color: var(--line-2); color: var(--ink-1); }
|
||||
.ct-btn--secondary:hover:not(:disabled) { background: var(--paper-2); }
|
||||
.ct-btn--secondary:active:not(:disabled) { background: var(--paper-3); }
|
||||
.ct-btn--ghost { background: transparent; color: var(--ink-2); }
|
||||
.ct-btn--ghost:hover:not(:disabled) { background: var(--paper-2); color: var(--ink-1); }
|
||||
.ct-btn--ghost:active:not(:disabled) { background: var(--paper-3); }
|
||||
.ct-btn--danger { background: var(--danger); color: var(--ink-inverse); }
|
||||
.ct-btn--danger:hover:not(:disabled) { filter: brightness(0.92); }
|
||||
`;
|
||||
(function inject() {
|
||||
if (typeof document !== 'undefined' && !document.getElementById('ct-button-css')) {
|
||||
const s = document.createElement('style'); s.id = 'ct-button-css'; s.textContent = CSS;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
})();
|
||||
|
||||
import { Icon } from './Icon';
|
||||
|
||||
export function Button({
|
||||
variant = 'primary',
|
||||
size = 'md',
|
||||
icon,
|
||||
iconRight,
|
||||
disabled = false,
|
||||
children,
|
||||
style,
|
||||
...rest
|
||||
}) {
|
||||
const iconSize = size === 'sm' ? 15 : 17;
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={`ct-btn ct-btn--${size} ct-btn--${variant}`}
|
||||
disabled={disabled}
|
||||
style={style}
|
||||
{...rest}
|
||||
>
|
||||
{icon ? <Icon name={icon} size={iconSize} /> : null}
|
||||
{children}
|
||||
{iconRight ? <Icon name={iconRight} size={iconSize} /> : null}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
10
docs/design/components/core/Button.prompt.md
Normal file
10
docs/design/components/core/Button.prompt.md
Normal file
@@ -0,0 +1,10 @@
|
||||
Button for all actions; labels are plain sentence-case verbs — the wit lives elsewhere.
|
||||
|
||||
```jsx
|
||||
<Button onClick={approve}>Approve</Button>
|
||||
<Button variant="secondary" icon="sparkles">Brew plan</Button>
|
||||
<Button variant="ghost" size="sm">Defer</Button>
|
||||
<Button variant="danger">Delete milestone</Button>
|
||||
```
|
||||
|
||||
Variants: `primary` (spruce fill — one per view), `secondary` (hairline outline), `ghost` (bare), `danger` (madder red, destructive confirms only). Sizes `md` 34px / `sm` 28px. `icon`/`iconRight` take Lucide names.
|
||||
21
docs/design/components/core/Card.d-ts.txt
Normal file
21
docs/design/components/core/Card.d-ts.txt
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Paper card — hairline border, 10px radius, whispered shadow. `jade` adds the jade top rule
|
||||
* reserved for the view's key card (e.g. the Focus card).
|
||||
*/
|
||||
export interface CardProps {
|
||||
/** Caslon title */
|
||||
title?: React.ReactNode;
|
||||
/** Small uppercase label above the title */
|
||||
overline?: string;
|
||||
/** Right-aligned header actions (IconButtons) */
|
||||
actions?: React.ReactNode;
|
||||
/** Footer row below a hairline */
|
||||
footer?: React.ReactNode;
|
||||
/** Jade top rule — one per view @default false */
|
||||
jade?: boolean;
|
||||
/** Remove body padding (tables, charts) @default false */
|
||||
flush?: boolean;
|
||||
children?: React.ReactNode;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
export declare function Card(props: CardProps): JSX.Element;
|
||||
58
docs/design/components/core/Card.js.txt
Normal file
58
docs/design/components/core/Card.js.txt
Normal file
@@ -0,0 +1,58 @@
|
||||
import React from 'react';
|
||||
|
||||
const CSS = `
|
||||
.ct-card {
|
||||
background: var(--surface-card);
|
||||
border: 1px solid var(--border-hairline);
|
||||
border-radius: var(--radius-3);
|
||||
box-shadow: var(--shadow-1);
|
||||
}
|
||||
.ct-card--jade { box-shadow: var(--shadow-jade-line), var(--shadow-1); }
|
||||
.ct-card__header {
|
||||
display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
|
||||
padding: 16px 20px 0;
|
||||
}
|
||||
.ct-card__title {
|
||||
font: var(--text-title);
|
||||
color: var(--ink-1);
|
||||
margin: 0;
|
||||
}
|
||||
.ct-card__overline {
|
||||
font: var(--text-overline);
|
||||
letter-spacing: var(--letter-spacing-wide);
|
||||
text-transform: uppercase;
|
||||
color: var(--ink-3);
|
||||
margin: 0 0 6px;
|
||||
}
|
||||
.ct-card__body { padding: 14px 20px 18px; }
|
||||
.ct-card__body--flush { padding: 0; }
|
||||
.ct-card__footer {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
padding: 12px 20px;
|
||||
border-top: 1px solid var(--border-hairline);
|
||||
}
|
||||
`;
|
||||
(function inject() {
|
||||
if (typeof document !== 'undefined' && !document.getElementById('ct-card-css')) {
|
||||
const s = document.createElement('style'); s.id = 'ct-card-css'; s.textContent = CSS;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
})();
|
||||
|
||||
export function Card({ title, overline, actions, footer, jade = false, flush = false, children, style, ...rest }) {
|
||||
return (
|
||||
<section className={`ct-card${jade ? ' ct-card--jade' : ''}`} style={style} {...rest}>
|
||||
{(title || overline || actions) ? (
|
||||
<header className="ct-card__header">
|
||||
<div>
|
||||
{overline ? <p className="ct-card__overline">{overline}</p> : null}
|
||||
{title ? <h2 className="ct-card__title">{title}</h2> : null}
|
||||
</div>
|
||||
{actions ? <div style={{ display: 'flex', gap: '4px', flexShrink: 0 }}>{actions}</div> : null}
|
||||
</header>
|
||||
) : null}
|
||||
<div className={`ct-card__body${flush ? ' ct-card__body--flush' : ''}`}>{children}</div>
|
||||
{footer ? <footer className="ct-card__footer">{footer}</footer> : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
11
docs/design/components/core/Card.prompt.md
Normal file
11
docs/design/components/core/Card.prompt.md
Normal file
@@ -0,0 +1,11 @@
|
||||
Paper card container; title is set in Caslon, elevation is whispered.
|
||||
|
||||
```jsx
|
||||
<Card overline="Now" title="Fix lifecycle inference" jade
|
||||
actions={<IconButton icon="ellipsis" label="More" size="sm" />}
|
||||
footer={<Button size="sm">Start</Button>}>
|
||||
Body content.
|
||||
</Card>
|
||||
```
|
||||
|
||||
`jade` (jade top rule) marks the single most important card in a view. `flush` removes body padding for tables/charts.
|
||||
15
docs/design/components/core/Icon.d-ts.txt
Normal file
15
docs/design/components/core/Icon.d-ts.txt
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Inline Lucide icon (1.5px stroke), tinted by currentColor.
|
||||
*/
|
||||
export interface IconProps {
|
||||
/** Lucide icon name, e.g. "circle-dot", "sparkles", "git-branch" */
|
||||
name: string;
|
||||
/** Square size in px. 16 inline, 18–20 in buttons/nav. @default 16 */
|
||||
size?: number;
|
||||
/** @default 1.5 */
|
||||
strokeWidth?: number;
|
||||
/** Accessible label; omit for decorative icons */
|
||||
title?: string;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
export declare function Icon(props: IconProps): JSX.Element;
|
||||
32
docs/design/components/core/Icon.js.txt
Normal file
32
docs/design/components/core/Icon.js.txt
Normal file
File diff suppressed because one or more lines are too long
8
docs/design/components/core/Icon.prompt.md
Normal file
8
docs/design/components/core/Icon.prompt.md
Normal file
@@ -0,0 +1,8 @@
|
||||
Inline SVG icon from the local Lucide set (69 icons, 1.5px stroke); color inherits currentColor.
|
||||
|
||||
```jsx
|
||||
<Icon name="circle-dot" />
|
||||
<Icon name="sparkles" size={18} style={{ color: 'var(--jade)' }} />
|
||||
```
|
||||
|
||||
Domain mapping: issues circle-dot · milestones milestone · board square-kanban · forecast chart-line · deps network · agent sparkles · standup coffee · capacity gauge · directives flag. ICON_NAMES export lists all available names. Icons are decorative by default (aria-hidden); pass title for meaningful ones.
|
||||
17
docs/design/components/core/IconButton.d-ts.txt
Normal file
17
docs/design/components/core/IconButton.d-ts.txt
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Square icon-only button; label is mandatory (becomes aria-label + title).
|
||||
*/
|
||||
export interface IconButtonProps {
|
||||
/** Lucide icon name */
|
||||
icon: string;
|
||||
/** Required accessible label (also the tooltip) */
|
||||
label: string;
|
||||
/** @default 'ghost' */
|
||||
variant?: 'ghost' | 'outline';
|
||||
/** @default 'md' */
|
||||
size?: 'sm' | 'md';
|
||||
disabled?: boolean;
|
||||
onClick?: (e: React.MouseEvent) => void;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
export declare function IconButton(props: IconButtonProps): JSX.Element;
|
||||
51
docs/design/components/core/IconButton.js.txt
Normal file
51
docs/design/components/core/IconButton.js.txt
Normal file
@@ -0,0 +1,51 @@
|
||||
import React from 'react';
|
||||
import { Icon } from './Icon';
|
||||
|
||||
const CSS = `
|
||||
.ct-iconbtn {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
border-radius: var(--radius-2);
|
||||
border: 1px solid transparent;
|
||||
background: transparent;
|
||||
color: var(--ink-2);
|
||||
cursor: pointer;
|
||||
transition: background var(--duration-fast) var(--ease-out), color var(--duration-fast) var(--ease-out);
|
||||
}
|
||||
.ct-iconbtn:hover:not(:disabled) { background: var(--paper-2); color: var(--ink-1); }
|
||||
.ct-iconbtn:active:not(:disabled) { background: var(--paper-3); }
|
||||
.ct-iconbtn:disabled { opacity: 0.45; cursor: not-allowed; }
|
||||
.ct-iconbtn--md { width: 34px; height: 34px; }
|
||||
.ct-iconbtn--sm { width: 28px; height: 28px; }
|
||||
.ct-iconbtn--outline { border-color: var(--line-2); background: var(--surface-card); }
|
||||
.ct-iconbtn--outline:hover:not(:disabled) { background: var(--paper-2); }
|
||||
`;
|
||||
(function inject() {
|
||||
if (typeof document !== 'undefined' && !document.getElementById('ct-iconbtn-css')) {
|
||||
const s = document.createElement('style'); s.id = 'ct-iconbtn-css'; s.textContent = CSS;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
})();
|
||||
|
||||
export function IconButton({
|
||||
icon,
|
||||
label,
|
||||
variant = 'ghost',
|
||||
size = 'md',
|
||||
disabled = false,
|
||||
style,
|
||||
...rest
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={`ct-iconbtn ct-iconbtn--${size}${variant === 'outline' ? ' ct-iconbtn--outline' : ''}`}
|
||||
aria-label={label}
|
||||
title={label}
|
||||
disabled={disabled}
|
||||
style={style}
|
||||
{...rest}
|
||||
>
|
||||
<Icon name={icon} size={size === 'sm' ? 15 : 17} />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
8
docs/design/components/core/IconButton.prompt.md
Normal file
8
docs/design/components/core/IconButton.prompt.md
Normal file
@@ -0,0 +1,8 @@
|
||||
Icon-only square button for toolbars and row actions; `label` is required and doubles as the tooltip.
|
||||
|
||||
```jsx
|
||||
<IconButton icon="ellipsis" label="More actions" />
|
||||
<IconButton icon="refresh-cw" label="Reconcile now" variant="outline" size="sm" />
|
||||
```
|
||||
|
||||
Variants: `ghost` (default), `outline`. Sizes `md` 34px / `sm` 28px.
|
||||
19
docs/design/components/core/Tabs.d-ts.txt
Normal file
19
docs/design/components/core/Tabs.d-ts.txt
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Underline tabs for view switching (Board / Gantt / Dependencies).
|
||||
*/
|
||||
export interface TabItem {
|
||||
id: string;
|
||||
label: string;
|
||||
/** Lucide icon name */
|
||||
icon?: string;
|
||||
/** Mono count rendered after the label */
|
||||
count?: number;
|
||||
}
|
||||
export interface TabsProps {
|
||||
items: TabItem[];
|
||||
/** id of the active tab */
|
||||
active: string;
|
||||
onChange?: (id: string) => void;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
export declare function Tabs(props: TabsProps): JSX.Element;
|
||||
54
docs/design/components/core/Tabs.js.txt
Normal file
54
docs/design/components/core/Tabs.js.txt
Normal file
@@ -0,0 +1,54 @@
|
||||
import React from 'react';
|
||||
import { Icon } from './Icon';
|
||||
|
||||
const CSS = `
|
||||
.ct-tabs {
|
||||
display: flex; gap: 2px;
|
||||
border-bottom: 1px solid var(--border-hairline);
|
||||
}
|
||||
.ct-tab {
|
||||
display: inline-flex; align-items: center; gap: 7px;
|
||||
font: 500 13.5px/1 var(--font-sans);
|
||||
color: var(--ink-2);
|
||||
background: none; border: none;
|
||||
padding: 10px 12px;
|
||||
margin-bottom: -1px;
|
||||
border-bottom: 2px solid transparent;
|
||||
cursor: pointer;
|
||||
transition: color var(--duration-fast) var(--ease-out);
|
||||
}
|
||||
.ct-tab:hover { color: var(--ink-1); }
|
||||
.ct-tab--active {
|
||||
color: var(--ink-1);
|
||||
font-weight: 600;
|
||||
border-bottom-color: var(--accent);
|
||||
}
|
||||
.ct-tab__count { font: 400 11.5px/1 var(--font-mono); color: var(--ink-3); }
|
||||
`;
|
||||
(function inject() {
|
||||
if (typeof document !== 'undefined' && !document.getElementById('ct-tabs-css')) {
|
||||
const s = document.createElement('style'); s.id = 'ct-tabs-css'; s.textContent = CSS;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
})();
|
||||
|
||||
export function Tabs({ items, active, onChange, style, ...rest }) {
|
||||
return (
|
||||
<div className="ct-tabs" role="tablist" style={style} {...rest}>
|
||||
{items.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={item.id === active}
|
||||
className={`ct-tab${item.id === active ? ' ct-tab--active' : ''}`}
|
||||
onClick={() => onChange && onChange(item.id)}
|
||||
>
|
||||
{item.icon ? <Icon name={item.icon} size={15} /> : null}
|
||||
{item.label}
|
||||
{item.count != null ? <span className="ct-tab__count">{item.count}</span> : null}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
13
docs/design/components/core/Tabs.prompt.md
Normal file
13
docs/design/components/core/Tabs.prompt.md
Normal file
@@ -0,0 +1,13 @@
|
||||
Underline tab strip for switching sibling views; active tab gets a 2px spruce underline.
|
||||
|
||||
```jsx
|
||||
<Tabs
|
||||
items={[
|
||||
{ id: 'board', label: 'Board', icon: 'square-kanban', count: 24 },
|
||||
{ id: 'gantt', label: 'Gantt', icon: 'chart-no-axes-gantt' },
|
||||
{ id: 'deps', label: 'Dependencies', icon: 'network' },
|
||||
]}
|
||||
active={view}
|
||||
onChange={setView}
|
||||
/>
|
||||
```
|
||||
12
docs/design/components/core/Tag.d-ts.txt
Normal file
12
docs/design/components/core/Tag.d-ts.txt
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Gitea label chip — renders label text verbatim in mono; tone derives from the label itself
|
||||
* (est/[1d 2d 3d 5d 8d], p/1..4, deadline/hard).
|
||||
*/
|
||||
export interface TagProps {
|
||||
/** Verbatim gitea label, e.g. "est/3d", "p/1", "deadline/hard" */
|
||||
label: string;
|
||||
/** Renders a small remove button when provided */
|
||||
onRemove?: (e: React.MouseEvent) => void;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
export declare function Tag(props: TagProps): JSX.Element;
|
||||
54
docs/design/components/core/Tag.js.txt
Normal file
54
docs/design/components/core/Tag.js.txt
Normal file
@@ -0,0 +1,54 @@
|
||||
import React from 'react';
|
||||
import { Icon } from './Icon';
|
||||
|
||||
const CSS = `
|
||||
.ct-tag {
|
||||
display: inline-flex; align-items: center; gap: 5px;
|
||||
font: 500 11.5px/1 var(--font-mono);
|
||||
letter-spacing: var(--letter-spacing-label);
|
||||
padding: 4px 9px;
|
||||
border-radius: var(--radius-round);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.ct-tag--est { background: var(--label-est-bg); color: var(--label-est-text); }
|
||||
.ct-tag--p1 { background: var(--danger-tint); color: var(--label-p1); }
|
||||
.ct-tag--p2 { background: var(--warn-tint); color: var(--label-p2); }
|
||||
.ct-tag--p3 { background: var(--info-tint); color: var(--label-p3); }
|
||||
.ct-tag--p4 { background: var(--paper-2); color: var(--label-p4); }
|
||||
.ct-tag--hard { background: var(--label-hard); color: var(--ink-inverse); }
|
||||
.ct-tag--plain { background: var(--paper-2); color: var(--ink-2); }
|
||||
.ct-tag__x {
|
||||
display: inline-flex; padding: 0; margin: 0 -3px 0 0;
|
||||
background: none; border: none; color: inherit; cursor: pointer; opacity: 0.6;
|
||||
}
|
||||
.ct-tag__x:hover { opacity: 1; }
|
||||
`;
|
||||
(function inject() {
|
||||
if (typeof document !== 'undefined' && !document.getElementById('ct-tag-css')) {
|
||||
const s = document.createElement('style'); s.id = 'ct-tag-css'; s.textContent = CSS;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
})();
|
||||
|
||||
function toneFor(label) {
|
||||
if (label.startsWith('est/')) return 'est';
|
||||
if (label === 'p/1') return 'p1';
|
||||
if (label === 'p/2') return 'p2';
|
||||
if (label === 'p/3') return 'p3';
|
||||
if (label === 'p/4') return 'p4';
|
||||
if (label === 'deadline/hard') return 'hard';
|
||||
return 'plain';
|
||||
}
|
||||
|
||||
export function Tag({ label, onRemove, style, ...rest }) {
|
||||
return (
|
||||
<span className={`ct-tag ct-tag--${toneFor(label)}`} style={style} {...rest}>
|
||||
{label}
|
||||
{onRemove ? (
|
||||
<button type="button" className="ct-tag__x" aria-label={`Remove ${label}`} onClick={onRemove}>
|
||||
<Icon name="x" size={11} strokeWidth={2} />
|
||||
</button>
|
||||
) : null}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
10
docs/design/components/core/Tag.prompt.md
Normal file
10
docs/design/components/core/Tag.prompt.md
Normal file
@@ -0,0 +1,10 @@
|
||||
Gitea label chip; pass the label verbatim and the tone is derived (est/* neutral, p/1 red → p/4 gray, deadline/hard filled red).
|
||||
|
||||
```jsx
|
||||
<Tag label="est/3d" />
|
||||
<Tag label="p/1" />
|
||||
<Tag label="deadline/hard" />
|
||||
<Tag label="est/5d" onRemove={remove} />
|
||||
```
|
||||
|
||||
Machine data stays mono and verbatim — never paraphrase a label. Use `Badge` for human state words.
|
||||
75
docs/design/components/core/core.card.html
Normal file
75
docs/design/components/core/core.card.html
Normal file
@@ -0,0 +1,75 @@
|
||||
<!-- 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; }
|
||||
.demo-grid { display: flex; flex-direction: column; gap: 14px; }
|
||||
.demo-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/babel">
|
||||
const { Button, IconButton, Badge, Tag, Card, Tabs, Icon } = window.CommiTeaDesignSystem_20e63b;
|
||||
|
||||
function Demo() {
|
||||
const [tab, setTab] = React.useState('board');
|
||||
return (
|
||||
<div className="demo-grid">
|
||||
<div className="demo-row">
|
||||
<Button>Approve</Button>
|
||||
<Button variant="secondary" icon="sparkles">Brew plan</Button>
|
||||
<Button variant="ghost">Defer</Button>
|
||||
<Button variant="danger" size="sm">Delete</Button>
|
||||
<Button disabled>Approve</Button>
|
||||
<Button size="sm" iconRight="arrow-right">Next</Button>
|
||||
<IconButton icon="ellipsis" label="More" />
|
||||
<IconButton icon="refresh-cw" label="Reconcile" variant="outline" size="sm" />
|
||||
</div>
|
||||
<div className="demo-row">
|
||||
<Badge tone="ok" dot>ahead</Badge>
|
||||
<Badge tone="warn" dot>drifting</Badge>
|
||||
<Badge tone="danger">blocked</Badge>
|
||||
<Badge tone="info">syncing</Badge>
|
||||
<Badge tone="jade">forecast</Badge>
|
||||
<Badge>neutral</Badge>
|
||||
<Tag label="est/3d" />
|
||||
<Tag label="p/1" />
|
||||
<Tag label="p/3" />
|
||||
<Tag label="deadline/hard" />
|
||||
<Tag label="est/5d" onRemove={() => {}} />
|
||||
</div>
|
||||
<Tabs
|
||||
items={[
|
||||
{ id: 'board', label: 'Board', icon: 'square-kanban', count: 24 },
|
||||
{ id: 'gantt', label: 'Gantt', icon: 'chart-no-axes-gantt' },
|
||||
{ id: 'deps', label: 'Dependencies', icon: 'network', count: 7 },
|
||||
]}
|
||||
active={tab}
|
||||
onChange={setTab}
|
||||
/>
|
||||
<Card overline="Now" title="Fix lifecycle inference" jade
|
||||
actions={<IconButton icon="ellipsis" label="More" size="sm" />}
|
||||
footer={<><Button size="sm">Start</Button><Button size="sm" variant="ghost">Defer</Button></>}>
|
||||
<div className="demo-row" style={{ marginBottom: 8 }}>
|
||||
<Tag label="est/2d" /><Tag label="p/1" />
|
||||
<span style={{ font: 'var(--text-data)', color: 'var(--ink-3)' }}>#87 · steeping 4d</span>
|
||||
</div>
|
||||
<span style={{ font: 'var(--text-agent)', color: 'var(--ink-2)' }}>
|
||||
It blocks #91 and #92. I'd take it first — the critical path agrees with me.
|
||||
</span>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(<Demo />);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
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>
|
||||
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