mirror of
https://github.com/nexu-io/open-design.git
synced 2026-05-31 19:04:39 +07:00
Merge b0c0559198 into 53fb175855
This commit is contained in:
commit
0b551c8afe
48 changed files with 802 additions and 463 deletions
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
|
|
@ -61,7 +61,7 @@ jobs:
|
|||
if [[ "$file" == "apps/daemon/"* || "$file" == "packages/contracts/"* || "$file" == "packages/platform/"* || "$file" == "packages/sidecar/"* || "$file" == "packages/sidecar-proto/"* ]]; then
|
||||
daemon_tests_required=true
|
||||
fi
|
||||
if [[ "$file" == "apps/web/"* || "$file" == "packages/contracts/"* || "$file" == "packages/host/"* || "$file" == "packages/platform/"* || "$file" == "packages/sidecar/"* || "$file" == "packages/sidecar-proto/"* ]]; then
|
||||
if [[ "$file" == "apps/web/"* || "$file" == "packages/components/"* || "$file" == "packages/contracts/"* || "$file" == "packages/host/"* || "$file" == "packages/platform/"* || "$file" == "packages/sidecar/"* || "$file" == "packages/sidecar-proto/"* ]]; then
|
||||
web_tests_required=true
|
||||
fi
|
||||
if [[ "$file" == "scripts/"* || "$file" == "assets/"* || "$file" == "skills/"* || "$file" == "prompt-templates/"* || "$file" == "design-systems/"* || "$file" == "design-templates/"* || "$file" == "craft/"* ]]; then
|
||||
|
|
@ -81,7 +81,7 @@ jobs:
|
|||
tools_pack_tests_required=true
|
||||
fi
|
||||
# Keep this filter in sync with flake.nix daemonWorkspacePaths / webWorkspacePaths.
|
||||
if [[ "$file" == "package.json" || "$file" == "pnpm-lock.yaml" || "$file" == "pnpm-workspace.yaml" || "$file" == "flake.nix" || "$file" == "flake.lock" || "$file" == "nix/"* || "$file" == ".github/workflows/ci.yml" || "$file" == ".github/workflows/nix-check.yml" || "$file" == ".github/workflows/nix-hash-autofix.yml" || "$file" == "apps/daemon/"* || "$file" == "apps/web/"* || "$file" == "packages/contracts/"* || "$file" == "packages/registry-protocol/"* || "$file" == "packages/agui-adapter/"* || "$file" == "packages/plugin-runtime/"* || "$file" == "packages/sidecar-proto/"* || "$file" == "packages/sidecar/"* || "$file" == "packages/platform/"* || "$file" == "packages/diagnostics/"* || "$file" == "packages/host/"* || "$file" == "assets/"* || "$file" == "plugins/"* || "$file" == "skills/"* || "$file" == "design-systems/"* || "$file" == "design-templates/"* || "$file" == "craft/"* || "$file" == "prompt-templates/"* || "$file" == "scripts/update-nix-pnpm-deps-hash.ts" ]]; then
|
||||
if [[ "$file" == "package.json" || "$file" == "pnpm-lock.yaml" || "$file" == "pnpm-workspace.yaml" || "$file" == "flake.nix" || "$file" == "flake.lock" || "$file" == "nix/"* || "$file" == ".github/workflows/ci.yml" || "$file" == ".github/workflows/nix-check.yml" || "$file" == ".github/workflows/nix-hash-autofix.yml" || "$file" == "apps/daemon/"* || "$file" == "apps/web/"* || "$file" == "packages/components/"* || "$file" == "packages/contracts/"* || "$file" == "packages/registry-protocol/"* || "$file" == "packages/agui-adapter/"* || "$file" == "packages/plugin-runtime/"* || "$file" == "packages/sidecar-proto/"* || "$file" == "packages/sidecar/"* || "$file" == "packages/platform/"* || "$file" == "packages/diagnostics/"* || "$file" == "packages/host/"* || "$file" == "assets/"* || "$file" == "plugins/"* || "$file" == "skills/"* || "$file" == "design-systems/"* || "$file" == "design-templates/"* || "$file" == "craft/"* || "$file" == "prompt-templates/"* || "$file" == "scripts/update-nix-pnpm-deps-hash.ts" ]]; then
|
||||
nix_validation_required=true
|
||||
fi
|
||||
case "$file" in
|
||||
|
|
|
|||
|
|
@ -152,6 +152,14 @@ root `pnpm tools-pr` script without a new explicit maintainer decision.
|
|||
- Keep global class names only for deliberate shared contracts: reusable primitives, theme hooks, third-party/content styling, cross-component layout, or selectors that rely on global cascade/specificity. Document any new global selector group with its owning feature.
|
||||
- CSS refactors must preserve cascade semantics. For mechanical splits, verify expanded import content/order matches the previous stylesheet; for CSS Module migrations, validate the affected UI path with `pnpm --filter @open-design/web typecheck` and a focused build/test or visual check when practical.
|
||||
|
||||
## Web component reuse
|
||||
|
||||
- New `apps/web` UI should reuse shared primitives from `@open-design/components` when one exists instead of styling plain HTML elements directly. For example, use `Button` for app buttons and `VisuallyHidden` for screen-reader-only text/status content.
|
||||
- Do not add new raw primitive classes such as `primary`, `primary-ghost`, `ghost`, `subtle`, `icon-btn`, or `sr-only` for new UI. Those classes are legacy compatibility surface for existing markup until it is migrated.
|
||||
- If a needed primitive is missing, prefer adding a small focused primitive to `packages/components` with colocated CSS Modules, then consume it from the app. Keep product-specific layout and workflow styling in the app, not in `packages/components`.
|
||||
- Keep semantic plain HTML when it is content markup or a specialized control that the shared package does not model yet; do not force a migration that would hide native behavior or make a custom widget harder to reason about.
|
||||
- `apps/web` transpiles `@open-design/components` from source during dev, so component and CSS Module edits should work through the normal web dev loop without rebuilding the package.
|
||||
|
||||
## i18n keys
|
||||
|
||||
- `apps/web/src/i18n/types.ts` is the typed `Dict`; every key must be defined in all 18 locale files under `apps/web/src/i18n/locales/*.ts` (`ar`, `de`, `en`, `es-ES`, `fa`, `fr`, `hu`, `id`, `ja`, `ko`, `pl`, `pt-BR`, `ru`, `th`, `tr`, `uk`, `zh-CN`, `zh-TW`). Add the key to `types.ts` first; missing translations produce a typecheck error.
|
||||
|
|
|
|||
|
|
@ -164,6 +164,7 @@ const nextConfig: NextConfig = {
|
|||
// to inject chunk IDs, upload to PostHog, and ALWAYS delete the .map files
|
||||
// before packaging so source never ships inside an installer.
|
||||
productionBrowserSourceMaps: true,
|
||||
transpilePackages: ['@open-design/components'],
|
||||
turbopack: {
|
||||
root: WORKSPACE_ROOT,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "0.32.1",
|
||||
"@open-design/components": "workspace:*",
|
||||
"@open-design/contracts": "workspace:*",
|
||||
"@open-design/host": "workspace:*",
|
||||
"@open-design/platform": "workspace:*",
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { Button, Select } from '@open-design/components';
|
||||
import { useT } from '../i18n';
|
||||
import type { AgentInfo, ExecMode } from '../types';
|
||||
|
||||
|
|
@ -27,7 +28,7 @@ export function AgentPicker({
|
|||
return (
|
||||
<div className="picker agent-picker">
|
||||
<span className="picker-label">{t('agentPicker.label')}</span>
|
||||
<select
|
||||
<Select
|
||||
value={mode}
|
||||
onChange={(e) => onModeChange(e.target.value as ExecMode)}
|
||||
title={t('agentPicker.modeChoose')}
|
||||
|
|
@ -36,10 +37,10 @@ export function AgentPicker({
|
|||
{t('agentPicker.localCli')} {daemonLive ? '' : `· ${t('agentPicker.daemonOff')}`}
|
||||
</option>
|
||||
<option value="api">{t('agentPicker.byok')}</option>
|
||||
</select>
|
||||
</Select>
|
||||
{mode === 'daemon' ? (
|
||||
<>
|
||||
<select
|
||||
<Select
|
||||
value={agentId ?? ''}
|
||||
onChange={(e) => onAgentChange(e.target.value)}
|
||||
disabled={available.length === 0}
|
||||
|
|
@ -58,14 +59,14 @@ export function AgentPicker({
|
|||
{a.available ? '' : ` · ${t('agentPicker.notInstalled')}`}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
</Select>
|
||||
<Button
|
||||
size="icon"
|
||||
onClick={onRefresh}
|
||||
title={t('agentPicker.rescan')}
|
||||
className="icon-btn"
|
||||
>
|
||||
↻
|
||||
</button>
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { CSSProperties } from 'react';
|
||||
import { Button, Textarea } from '@open-design/components';
|
||||
import { useRef } from 'react';
|
||||
|
||||
import type { PreviewCommentSnapshot } from '../comments';
|
||||
|
|
@ -325,14 +326,14 @@ export function BoardComposerPopover({
|
|||
{notes.map((note, index) => (
|
||||
<div key={`${target.elementId}-${index}`} className="board-note-item">
|
||||
<span>{note}</span>
|
||||
<button type="button" className="ghost" onClick={() => onRemoveQueuedNote(index)}>
|
||||
<Button variant="ghost" onClick={() => onRemoveQueuedNote(index)}>
|
||||
{t('chat.comments.remove')}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
<textarea
|
||||
<Textarea
|
||||
data-testid="comment-popover-input"
|
||||
value={draft}
|
||||
autoFocus
|
||||
|
|
@ -385,35 +386,32 @@ export function BoardComposerPopover({
|
|||
</div>
|
||||
<div className="comment-popover-actions-end">
|
||||
{target.selectionKind === 'pod' ? (
|
||||
<button
|
||||
type="button"
|
||||
className="ghost"
|
||||
<Button
|
||||
variant="ghost"
|
||||
data-testid="comment-popover-add-note"
|
||||
disabled={!draft.trim()}
|
||||
onClick={onAddDraft}
|
||||
>
|
||||
{t('chat.comments.addNote')}
|
||||
</button>
|
||||
</Button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="ghost"
|
||||
<Button
|
||||
variant="ghost"
|
||||
data-testid="comment-popover-save"
|
||||
disabled={!draft.trim() || !hasCommentChange}
|
||||
onClick={() => void onSaveComment()}
|
||||
>
|
||||
{t('chat.comments.comment')}
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="primary"
|
||||
<Button
|
||||
variant="primary"
|
||||
data-testid="comment-add-send"
|
||||
disabled={sendDisabled}
|
||||
onClick={() => void onSendBatch()}
|
||||
>
|
||||
{sending ? t('chat.comments.sending') : t('chat.comments.sendToChat')}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
type ReactNode,
|
||||
} from "react";
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Button } from '@open-design/components';
|
||||
import { useI18n, useT } from '../i18n';
|
||||
import type { Dict } from '../i18n/types';
|
||||
import {
|
||||
|
|
@ -2206,8 +2207,8 @@ export const ChatComposer = forwardRef<ChatComposerHandle, Props>(
|
|||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
<button
|
||||
className="icon-btn"
|
||||
<Button
|
||||
size="icon"
|
||||
data-testid="chat-attach"
|
||||
onClick={() => {
|
||||
trackChatPanelClick(analytics.track, {
|
||||
|
|
@ -2226,7 +2227,7 @@ export const ChatComposer = forwardRef<ChatComposerHandle, Props>(
|
|||
) : (
|
||||
<Icon name="attach" size={15} />
|
||||
)}
|
||||
</button>
|
||||
</Button>
|
||||
{footerAccessory}
|
||||
<span className="composer-spacer" />
|
||||
{showStopButton ? (
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
type KeyboardEvent as ReactKeyboardEvent,
|
||||
type SyntheticEvent,
|
||||
} from 'react';
|
||||
import { VisuallyHidden } from '@open-design/components';
|
||||
import type { ConnectorConnectResponse, ConnectorDetail, ConnectorStatusResponse } from '@open-design/contracts';
|
||||
import { useT } from '../i18n';
|
||||
import type { Dict } from '../i18n/types';
|
||||
|
|
@ -900,7 +901,7 @@ export function ConnectorsBrowser({
|
|||
>
|
||||
<p className="connector-panel-alert-copy" role="status">
|
||||
<strong title={alert.connectorName}>{alert.connectorName}</strong>
|
||||
<span className="sr-only">: </span>
|
||||
<VisuallyHidden>: </VisuallyHidden>
|
||||
<span title={alert.message}>{alert.message}</span>
|
||||
</p>
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Button } from '@open-design/components';
|
||||
import { useAnalytics } from '../analytics/provider';
|
||||
import { trackFileManagerClick } from '../analytics/events';
|
||||
import { useT } from '../i18n';
|
||||
|
|
@ -1657,10 +1658,10 @@ function DfPreview({
|
|||
</div>
|
||||
<div className="df-preview-meta" data-testid="design-file-preview">
|
||||
<div className="df-preview-actions">
|
||||
<button type="button" className="ghost" onClick={onOpen}>
|
||||
<Button variant="ghost" onClick={onOpen}>
|
||||
<Icon name="eye" size={13} />
|
||||
<span>{t('designFiles.previewOpen')}</span>
|
||||
</button>
|
||||
</Button>
|
||||
<a
|
||||
className="ghost-link"
|
||||
href={url}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useCallback, useEffect, useMemo, useRef, useState, type ChangeEvent, type ReactNode } from 'react';
|
||||
import { Button, Textarea } from '@open-design/components';
|
||||
import type { ConnectorConnectResponse, ConnectorDetail, ConnectorStatusResponse } from '@open-design/contracts';
|
||||
import { streamViaDaemon } from '../providers/daemon';
|
||||
import {
|
||||
|
|
@ -651,19 +652,18 @@ export function DesignSystemCreationFlow({
|
|||
<h1>It will take about 5 minutes to generate your design system.</h1>
|
||||
<p>You can step away. Keep the tab open in the background.</p>
|
||||
<div className="ds-setup-actions">
|
||||
<button type="button" className="ghost" onClick={() => setStep('setup')}>
|
||||
<Button variant="ghost" onClick={() => setStep('setup')}>
|
||||
<Icon name="arrow-left" />
|
||||
Back
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="primary"
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={generationStarting}
|
||||
onClick={() => void generate()}
|
||||
>
|
||||
<Icon name="sparkles" />
|
||||
{generationStarting ? 'Opening project...' : 'Generate'}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -687,16 +687,15 @@ export function DesignSystemCreationFlow({
|
|||
) : null}
|
||||
{embedded ? null : (
|
||||
<header className="ds-setup-topbar">
|
||||
<button type="button" className="ghost" onClick={onBack}>
|
||||
<Button variant="ghost" onClick={onBack}>
|
||||
<Icon name="arrow-left" />
|
||||
Back
|
||||
</button>
|
||||
</Button>
|
||||
<span className="ds-setup-mark">
|
||||
<Icon name="blocks" />
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="primary"
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={!state.company.trim()}
|
||||
onClick={() => {
|
||||
if (!state.company.trim()) {
|
||||
|
|
@ -708,7 +707,7 @@ export function DesignSystemCreationFlow({
|
|||
>
|
||||
Continue to generation
|
||||
<Icon name="chevron-right" />
|
||||
</button>
|
||||
</Button>
|
||||
</header>
|
||||
)}
|
||||
|
||||
|
|
@ -842,7 +841,7 @@ export function DesignSystemCreationFlow({
|
|||
{embedded ? null : (
|
||||
<label className="ds-setup-field">
|
||||
<span>Notes</span>
|
||||
<textarea
|
||||
<Textarea
|
||||
rows={4}
|
||||
value={state.notes}
|
||||
onChange={(event) => setState((curr) => ({ ...curr, notes: event.target.value }))}
|
||||
|
|
@ -853,13 +852,12 @@ export function DesignSystemCreationFlow({
|
|||
{error ? <div className="ds-editor-error">{error}</div> : null}
|
||||
{embedded ? (
|
||||
<div className="ds-setup-actions ds-setup-actions--embedded">
|
||||
<button type="button" className="ghost" onClick={onBack}>
|
||||
<Button variant="ghost" onClick={onBack}>
|
||||
<Icon name="arrow-left" />
|
||||
Back
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="primary"
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={!state.company.trim()}
|
||||
onClick={() => {
|
||||
if (!state.company.trim()) {
|
||||
|
|
@ -871,7 +869,7 @@ export function DesignSystemCreationFlow({
|
|||
>
|
||||
Generate
|
||||
<Icon name="chevron-right" />
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</main>
|
||||
|
|
@ -1786,10 +1784,10 @@ export function DesignSystemDetailView({
|
|||
|
||||
<main className="ds-review-main">
|
||||
<header className="ds-review-tabs">
|
||||
<button type="button" className="ghost" onClick={onBack}>
|
||||
<Button variant="ghost" onClick={onBack}>
|
||||
<Icon name="arrow-left" />
|
||||
Back
|
||||
</button>
|
||||
</Button>
|
||||
<div className="segmented">
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -1806,9 +1804,9 @@ export function DesignSystemDetailView({
|
|||
Design Files
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" className="ghost">
|
||||
<Button variant="ghost">
|
||||
Share
|
||||
</button>
|
||||
</Button>
|
||||
</header>
|
||||
|
||||
{tab === 'system' ? (
|
||||
|
|
@ -1834,9 +1832,9 @@ export function DesignSystemDetailView({
|
|||
Published
|
||||
</label>
|
||||
{selectedId !== system.id ? (
|
||||
<button
|
||||
type="button"
|
||||
className="ghost compact"
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="compact"
|
||||
onClick={() => {
|
||||
const statusBefore = mapDsStatusToTracking(system.status);
|
||||
onSetDefault(system.id);
|
||||
|
|
@ -1856,7 +1854,7 @@ export function DesignSystemDetailView({
|
|||
}}
|
||||
>
|
||||
Make default
|
||||
</button>
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
<DesignSystemPackageCard system={system} />
|
||||
|
|
@ -1866,10 +1864,10 @@ export function DesignSystemDetailView({
|
|||
<strong>Missing brand fonts</strong>
|
||||
Open Design is rendering typography with substitute web fonts.
|
||||
</span>
|
||||
<button type="button" className="ghost compact">
|
||||
<Button variant="ghost" className="compact">
|
||||
<Icon name="upload" />
|
||||
Upload fonts
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
{statusLine ? <div className="ds-status-line">{statusLine}</div> : null}
|
||||
<WorkspaceActivityCard message={workspaceActivityMessage} active={chatStreaming} />
|
||||
|
|
@ -1939,16 +1937,16 @@ export function DesignSystemDetailView({
|
|||
</div>
|
||||
<label className="ds-body-editor">
|
||||
DESIGN.md
|
||||
<textarea
|
||||
<Textarea
|
||||
value={body}
|
||||
onChange={(event) => setBody(event.target.value)}
|
||||
rows={16}
|
||||
disabled={!editable}
|
||||
/>
|
||||
</label>
|
||||
<button type="button" className="primary" disabled={!editable || saving} onClick={() => void saveBody()}>
|
||||
<Button variant="primary" disabled={!editable || saving} onClick={() => void saveBody()}>
|
||||
Save DESIGN.md
|
||||
</button>
|
||||
</Button>
|
||||
{recentRevisions.length > 0 ? <RevisionHistoryList revisions={recentRevisions} /> : null}
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -2734,9 +2732,9 @@ function DropZone({
|
|||
<span>{names.length > 0 && !onRemoveName ? names.join(', ') : prompt}</span>
|
||||
</label>
|
||||
{onBrowseFolder ? (
|
||||
<button type="button" className="ghost" onClick={onBrowseFolder}>
|
||||
<Button variant="ghost" onClick={onBrowseFolder}>
|
||||
Browse folder
|
||||
</button>
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
{names.length > 0 && onRemoveName ? (
|
||||
|
|
@ -2923,24 +2921,24 @@ function GitHubRepositoryAccessPanel({
|
|||
}
|
||||
|
||||
const composioAction = !composioConfigured ? (
|
||||
<button type="button" className="ghost" onClick={onOpenConnectorsTab}>
|
||||
<Button variant="ghost" onClick={onOpenConnectorsTab}>
|
||||
Configure Composio
|
||||
</button>
|
||||
</Button>
|
||||
) : connected || authorizationPending ? (
|
||||
<>
|
||||
{authorizationPending && authorizationUrl ? (
|
||||
<button type="button" className="ghost" disabled={busy} onClick={onOpenAuthorization}>
|
||||
<Button variant="ghost" disabled={busy} onClick={onOpenAuthorization}>
|
||||
Open authorization
|
||||
</button>
|
||||
</Button>
|
||||
) : null}
|
||||
<button type="button" className="ghost" disabled={busy} onClick={onDisconnect}>
|
||||
<Button variant="ghost" disabled={busy} onClick={onDisconnect}>
|
||||
{action === 'disconnect' ? 'Disconnecting...' : 'Disconnect'}
|
||||
</button>
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<button type="button" className="ghost" disabled={busy} onClick={onConnect}>
|
||||
<Button variant="ghost" disabled={busy} onClick={onConnect}>
|
||||
{action === 'connect' ? 'Connecting...' : 'Connect via Composio'}
|
||||
</button>
|
||||
</Button>
|
||||
);
|
||||
|
||||
const methods: GitHubAccessMethod[] = [
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Button } from '@open-design/components';
|
||||
import { useAnalytics } from '../analytics/provider';
|
||||
import {
|
||||
trackDesignSystemsTemplateCardClick,
|
||||
|
|
@ -543,14 +544,13 @@ export function DesignSystemsTab({
|
|||
<i aria-hidden />
|
||||
</button>
|
||||
{onOpenSystem ? (
|
||||
<button
|
||||
type="button"
|
||||
className="icon-btn"
|
||||
<Button
|
||||
size="icon"
|
||||
aria-label={`Open ${system.title}`}
|
||||
onClick={() => onOpenSystem(system.id)}
|
||||
>
|
||||
<Icon name="external-link" />
|
||||
</button>
|
||||
</Button>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useCallback, useEffect, useId, useMemo, useRef, useState, type CSSProperties, type MouseEvent as ReactMouseEvent, type ReactNode } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Button, Input, Select } from '@open-design/components';
|
||||
import { APP_CHROME_FILE_ACTIONS_ID, APP_CHROME_FILE_ACTIONS_SELECTOR } from './AppChromeHeader';
|
||||
import {
|
||||
anonymizeArtifactId,
|
||||
|
|
@ -2309,18 +2310,17 @@ export function CommentSidePanel({
|
|||
{selectedCount > 0 ? (
|
||||
<div className="comment-side-selectbar" data-testid="comment-side-selectbar">
|
||||
<span className="comment-side-selectcount">{t('chat.comments.nSelected', { n: selectedCount })}</span>
|
||||
<button type="button" className="ghost" onClick={onClearSelection}>
|
||||
<Button variant="ghost" onClick={onClearSelection}>
|
||||
{t('chat.comments.clear')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="primary"
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
data-testid="comment-side-send-claude"
|
||||
disabled={sending}
|
||||
onClick={() => void onSendSelected()}
|
||||
>
|
||||
{sending ? t('chat.comments.sending') : t('chat.comments.sendToChat')}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
{composer ? <div className="comment-side-composer">{composer}</div> : null}
|
||||
|
|
@ -2563,9 +2563,9 @@ function InspectPanel({
|
|||
<strong title={target.label || target.elementId}>{target.label || target.elementId}</strong>
|
||||
<code title={target.selector}>{target.elementId}</code>
|
||||
</div>
|
||||
<button type="button" className="ghost" onClick={onClose} aria-label="Close inspect">
|
||||
<Button variant="ghost" onClick={onClose} aria-label="Close inspect">
|
||||
×
|
||||
</button>
|
||||
</Button>
|
||||
</header>
|
||||
|
||||
{target.clickedDescendant ? (
|
||||
|
|
@ -2588,14 +2588,14 @@ function InspectPanel({
|
|||
<div className="inspect-section-label">Colors</div>
|
||||
<div className="inspect-row">
|
||||
<label htmlFor="ip-color">Text</label>
|
||||
<input
|
||||
<Input
|
||||
id="ip-color"
|
||||
data-testid="inspect-color"
|
||||
type="color"
|
||||
value={colorHex}
|
||||
onChange={(e) => setVal('color', e.target.value)}
|
||||
/>
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
value={colorHex}
|
||||
onChange={(e) => setVal('color', e.target.value)}
|
||||
|
|
@ -2604,14 +2604,14 @@ function InspectPanel({
|
|||
</div>
|
||||
<div className="inspect-row">
|
||||
<label htmlFor="ip-bg">Background</label>
|
||||
<input
|
||||
<Input
|
||||
id="ip-bg"
|
||||
data-testid="inspect-bg"
|
||||
type="color"
|
||||
value={bgHex}
|
||||
onChange={(e) => setVal('background-color', e.target.value)}
|
||||
/>
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
value={bgHex}
|
||||
onChange={(e) => setVal('background-color', e.target.value)}
|
||||
|
|
@ -2638,7 +2638,7 @@ function InspectPanel({
|
|||
</div>
|
||||
<div className="inspect-row">
|
||||
<label htmlFor="ip-fw">Weight</label>
|
||||
<select
|
||||
<Select
|
||||
id="ip-fw"
|
||||
value={fontWeight}
|
||||
onChange={(e) => setVal('font-weight', e.target.value)}
|
||||
|
|
@ -2646,11 +2646,11 @@ function InspectPanel({
|
|||
{['100', '300', '400', '500', '600', '700', '800', '900'].map((w) => (
|
||||
<option key={w} value={w}>{w}</option>
|
||||
))}
|
||||
</select>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="inspect-row">
|
||||
<label htmlFor="ip-ta">Align</label>
|
||||
<select
|
||||
<Select
|
||||
id="ip-ta"
|
||||
value={textAlign}
|
||||
onChange={(e) => setVal('text-align', e.target.value)}
|
||||
|
|
@ -2658,7 +2658,7 @@ function InspectPanel({
|
|||
{['left', 'center', 'right', 'justify'].map((a) => (
|
||||
<option key={a} value={a}>{a}</option>
|
||||
))}
|
||||
</select>
|
||||
</Select>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
|
@ -2695,25 +2695,23 @@ function InspectPanel({
|
|||
</section>
|
||||
|
||||
<footer className="inspect-panel-footer">
|
||||
<button
|
||||
type="button"
|
||||
className="ghost"
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setDraft({});
|
||||
onResetElement(target.elementId);
|
||||
}}
|
||||
>
|
||||
Reset element
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="primary"
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
data-testid="inspect-save"
|
||||
disabled={saving}
|
||||
onClick={onSaveToSource}
|
||||
>
|
||||
{saving ? 'Saving…' : justSaved ? 'Saved ✓' : 'Save to source'}
|
||||
</button>
|
||||
</Button>
|
||||
</footer>
|
||||
{error ? <div className="inspect-panel-error">{error}</div> : null}
|
||||
</aside>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
useState,
|
||||
type DragEvent as ReactDragEvent,
|
||||
} from 'react';
|
||||
import { Button } from '@open-design/components';
|
||||
import type { TrackingProjectKind } from '@open-design/contracts/analytics';
|
||||
import { useAnalytics } from '../analytics/provider';
|
||||
import {
|
||||
|
|
@ -1598,14 +1599,14 @@ function DesignSystemProjectPanel({
|
|||
{published ? (
|
||||
<div className="ds-project-use-row">
|
||||
<span>Use this system</span>
|
||||
<button
|
||||
type="button"
|
||||
className="ghost compact"
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="compact"
|
||||
onClick={() => onUseDesignSystem?.(system.id, system.title)}
|
||||
>
|
||||
<Icon name="external-link" size={13} />
|
||||
New design
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
|
@ -1618,20 +1619,20 @@ function DesignSystemProjectPanel({
|
|||
<small>{repoConnectCopy(githubConnected).bannerBody}</small>
|
||||
</span>
|
||||
{onConnectRepo ? (
|
||||
<button
|
||||
type="button"
|
||||
className="ghost compact"
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="compact"
|
||||
disabled={githubConnected === undefined}
|
||||
onClick={onConnectRepo}
|
||||
>
|
||||
<Icon name="github" size={13} />
|
||||
{repoConnectCopy(githubConnected).buttonLabel}
|
||||
</button>
|
||||
</Button>
|
||||
) : githubEvidence.hasSourceManifest ? (
|
||||
<button type="button" className="ghost compact" onClick={() => onOpenFile('context/source-context.md')}>
|
||||
<Button variant="ghost" className="compact" onClick={() => onOpenFile('context/source-context.md')}>
|
||||
<Icon name="file" size={13} />
|
||||
Open source context
|
||||
</button>
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Button } from '@open-design/components';
|
||||
import { useAnalytics } from '../analytics/provider';
|
||||
import { trackIntegrationsMcpTabClick } from '../analytics/events';
|
||||
import {
|
||||
|
|
@ -780,33 +781,32 @@ function McpRow({ row, idx, total, template, onChange, onRemove, onMoveUp, onMov
|
|||
</span>
|
||||
<div className="mcp-row-actions">
|
||||
{onMoveUp ? (
|
||||
<button type="button" className="icon-btn" onClick={onMoveUp} title="Move up">
|
||||
<Button size="icon" onClick={onMoveUp} title="Move up">
|
||||
↑
|
||||
</button>
|
||||
</Button>
|
||||
) : null}
|
||||
{onMoveDown ? (
|
||||
<button type="button" className="icon-btn" onClick={onMoveDown} title="Move down">
|
||||
<Button size="icon" onClick={onMoveDown} title="Move down">
|
||||
↓
|
||||
</button>
|
||||
</Button>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
className="icon-btn"
|
||||
<Button
|
||||
size="icon"
|
||||
onClick={onRemove}
|
||||
title="Remove this MCP server"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="icon-btn mcp-row-toggle-btn"
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
className="mcp-row-toggle-btn"
|
||||
onClick={() => setExpanded((v) => !v)}
|
||||
aria-expanded={expanded}
|
||||
aria-label={expanded ? 'Collapse this MCP server' : 'Expand this MCP server'}
|
||||
title={expanded ? 'Collapse' : 'Expand'}
|
||||
>
|
||||
<Icon name="chevron-down" size={13} />
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
useState,
|
||||
type CSSProperties,
|
||||
} from 'react';
|
||||
import { Button } from '@open-design/components';
|
||||
import { Icon, type IconName } from './Icon';
|
||||
import { ConnectorLogo, useResolvedTheme } from './ConnectorLogo';
|
||||
import { useT } from '../i18n';
|
||||
|
|
@ -1837,17 +1838,16 @@ export function MemorySection({
|
|||
{t('settings.memorySaveHint')}
|
||||
</span>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<button type="button" className="ghost" onClick={cancelEdit}>
|
||||
<Button variant="ghost" onClick={cancelEdit}>
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="primary"
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={onSave}
|
||||
disabled={busy || !editing.name.trim()}
|
||||
>
|
||||
{editing.id ? t('common.save') : t('common.create')}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { Button } from '@open-design/components';
|
||||
|
||||
import { Icon } from './Icon';
|
||||
|
||||
|
|
@ -69,14 +70,14 @@ export function MissingBrandFontsBanner({
|
|||
</span>
|
||||
<div className="ds-warning-card-actions">
|
||||
{onUploadAssets ? (
|
||||
<button type="button" className="ghost compact" onClick={onUploadAssets}>
|
||||
<Button variant="ghost" className="compact" onClick={onUploadAssets}>
|
||||
<Icon name="upload" size={13} />
|
||||
Upload fonts
|
||||
</button>
|
||||
</Button>
|
||||
) : null}
|
||||
<button type="button" className="ghost compact" onClick={useSystemFonts}>
|
||||
<Button variant="ghost" className="compact" onClick={useSystemFonts}>
|
||||
Use system fonts
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useState } from 'react';
|
||||
import { Button, Input, Textarea } from '@open-design/components';
|
||||
import { useT } from '../i18n';
|
||||
|
||||
interface Props {
|
||||
|
|
@ -25,7 +26,7 @@ export function PasteTextDialog({ onSave, onClose }: Props) {
|
|||
<p className="hint">{t('pasteDialog.hint')}</p>
|
||||
<label>
|
||||
{t('pasteDialog.fileNameLabel')}
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
value={name}
|
||||
placeholder={t('pasteDialog.namePlaceholder')}
|
||||
|
|
@ -35,7 +36,7 @@ export function PasteTextDialog({ onSave, onClose }: Props) {
|
|||
</label>
|
||||
<label>
|
||||
{t('pasteDialog.contentLabel')}
|
||||
<textarea
|
||||
<Textarea
|
||||
rows={10}
|
||||
value={content}
|
||||
placeholder={t('pasteDialog.contentPlaceholder')}
|
||||
|
|
@ -43,10 +44,10 @@ export function PasteTextDialog({ onSave, onClose }: Props) {
|
|||
/>
|
||||
</label>
|
||||
<div className="row">
|
||||
<button onClick={onClose}>{t('pasteDialog.cancel')}</button>
|
||||
<button className="primary" onClick={commit} disabled={!content.trim()}>
|
||||
<Button onClick={onClose}>{t('pasteDialog.cancel')}</Button>
|
||||
<Button variant="primary" onClick={commit} disabled={!content.trim()}>
|
||||
{t('pasteDialog.save')}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import type { CSSProperties, Dispatch, SetStateAction } from 'react';
|
||||
import { Button, VisuallyHidden } from '@open-design/components';
|
||||
import { validateBaseUrl } from '@open-design/contracts/api/connectionTest';
|
||||
import {
|
||||
agentIdToTracking,
|
||||
|
|
@ -5280,9 +5281,9 @@ function MediaProvidersSection({
|
|||
// Off-screen announcement so assistive tech still hears the
|
||||
// success state even though the visible feedback collapses
|
||||
// into a transient button label change.
|
||||
<span className="sr-only" role="status">
|
||||
<VisuallyHidden role="status">
|
||||
{reloadNotice.message}
|
||||
</span>
|
||||
</VisuallyHidden>
|
||||
) : null}
|
||||
{onReloadMediaProviders ? (
|
||||
<div className="media-provider-reload-row">
|
||||
|
|
@ -6580,7 +6581,7 @@ function NotificationsSection({
|
|||
) : null}
|
||||
{notif.desktopEnabled && permission === 'granted' ? (
|
||||
<>
|
||||
<button type="button" className="ghost" onClick={() => {
|
||||
<Button variant="ghost" onClick={() => {
|
||||
trackSettingsNotificationsClick(analytics.track, {
|
||||
page_name: 'settings',
|
||||
area: 'notifications',
|
||||
|
|
@ -6589,7 +6590,7 @@ function NotificationsSection({
|
|||
void sendTestNotification();
|
||||
}}>
|
||||
{t('settings.notifyTest')}
|
||||
</button>
|
||||
</Button>
|
||||
{testStatus ? <p className="hint" role="status">{t(testStatus)}</p> : null}
|
||||
</>
|
||||
) : null}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Button, Input } from '@open-design/components';
|
||||
import { useT } from '../i18n';
|
||||
import { Icon } from './Icon';
|
||||
import { readDefaultSketchToolColor } from './sketch-colors';
|
||||
|
|
@ -245,30 +246,30 @@ export function SketchEditor({
|
|||
className="sketch-size"
|
||||
/>
|
||||
<span className="sketch-divider" />
|
||||
<button className="ghost" onClick={handleUndo} disabled={items.length === 0}>
|
||||
<Button variant="ghost" onClick={handleUndo} disabled={items.length === 0}>
|
||||
{t('sketch.undo')}
|
||||
</button>
|
||||
<button className="ghost" onClick={handleClear} disabled={!canClear}>
|
||||
</Button>
|
||||
<Button variant="ghost" onClick={handleClear} disabled={!canClear}>
|
||||
{t('sketch.clear')}
|
||||
</button>
|
||||
</Button>
|
||||
<span className="sketch-spacer" />
|
||||
<span className="sketch-name" title={fileName}>
|
||||
{fileName}
|
||||
{dirty ? ' •' : ''}
|
||||
</span>
|
||||
{onCancel ? (
|
||||
<button className="ghost" onClick={onCancel}>
|
||||
<Button variant="ghost" onClick={onCancel}>
|
||||
{t('sketch.close')}
|
||||
</button>
|
||||
</Button>
|
||||
) : null}
|
||||
<button
|
||||
className="primary"
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleSave}
|
||||
disabled={saving || !canSave}
|
||||
aria-label={saving ? t('sketch.saving') : showSaved ? t('sketch.saved') : t('common.save')}
|
||||
>
|
||||
{saving ? t('sketch.saving') : showSaved ? <Icon name="check" size={14} /> : t('common.save')}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="sketch-canvas-wrap" ref={wrapRef}>
|
||||
<canvas
|
||||
|
|
@ -288,7 +289,7 @@ export function SketchEditor({
|
|||
</div>
|
||||
<label>
|
||||
<span>{t('sketch.textPrompt')}</span>
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
value={textModalValue}
|
||||
autoFocus
|
||||
|
|
@ -305,17 +306,16 @@ export function SketchEditor({
|
|||
/>
|
||||
</label>
|
||||
<div className="modal-foot">
|
||||
<button type="button" className="ghost" onClick={cancelTextModal}>
|
||||
<Button variant="ghost" onClick={cancelTextModal}>
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="primary"
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={!textModalValue.trim()}
|
||||
onClick={submitTextModal}
|
||||
>
|
||||
{t('common.save')}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
import { Button } from '@open-design/components';
|
||||
import { useI18n, useT } from '../i18n';
|
||||
import {
|
||||
localizeSkillDescription,
|
||||
|
|
@ -647,25 +648,23 @@ function SkillRow({
|
|||
</span>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="icon-btn"
|
||||
<Button
|
||||
size="icon"
|
||||
onClick={onStartEdit}
|
||||
title={t('settings.skillsEdit')}
|
||||
data-testid="skills-edit"
|
||||
>
|
||||
<Icon name="edit" size={13} />
|
||||
</button>
|
||||
</Button>
|
||||
{canDelete ? (
|
||||
<button
|
||||
type="button"
|
||||
className="icon-btn"
|
||||
<Button
|
||||
size="icon"
|
||||
onClick={onArmDelete}
|
||||
title={t('settings.skillsDelete')}
|
||||
data-testid="skills-delete"
|
||||
>
|
||||
<Icon name="close" size={13} />
|
||||
</button>
|
||||
</Button>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
// needs to commit.
|
||||
|
||||
import { useMemo, useState } from 'react';
|
||||
import { VisuallyHidden } from '@open-design/components';
|
||||
import type { InstalledPluginRecord } from '@open-design/contracts';
|
||||
import { useI18n } from '../../i18n';
|
||||
import type { PluginShareAction } from '../../state/projects';
|
||||
|
|
@ -261,7 +262,7 @@ export function PluginCard({
|
|||
data-testid={`plugins-home-save-${record.id}`}
|
||||
>
|
||||
<Icon name={isSaved ? 'check' : 'star'} size={12} />
|
||||
<span className="sr-only">{isSaved ? 'Saved' : 'Save'}</span>
|
||||
<VisuallyHidden>{isSaved ? 'Saved' : 'Save'}</VisuallyHidden>
|
||||
</button>
|
||||
<span className="plugins-home__card-title" title={title}>
|
||||
<span className="plugins-home__card-title-text">{title}</span>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
@import './styles/design-system-flow.css';
|
||||
@import './styles/tokens.css';
|
||||
@import './styles/base.css';
|
||||
@import '@open-design/components/styles.css';
|
||||
@import './styles/primitives.css';
|
||||
@import './styles/shell.css';
|
||||
@import './styles/chat.css';
|
||||
|
|
|
|||
|
|
@ -1,279 +1,3 @@
|
|||
/* -------- Buttons --------------------------------------------------- */
|
||||
button {
|
||||
font: inherit;
|
||||
color: var(--text);
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 6px 12px;
|
||||
cursor: pointer;
|
||||
transition: background 120ms ease, border-color 120ms ease, color 120ms ease, box-shadow 120ms ease;
|
||||
}
|
||||
button:hover:not(:disabled) { background: var(--bg-subtle); border-color: var(--border-strong); }
|
||||
button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||
|
||||
button.primary {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 1px 0 rgba(180, 90, 59, 0.18) inset, var(--shadow-xs);
|
||||
}
|
||||
button.primary:hover:not(:disabled) {
|
||||
background: var(--accent-hover);
|
||||
border-color: var(--accent-hover);
|
||||
}
|
||||
button.primary-ghost {
|
||||
background: var(--bg-panel);
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
font-weight: 500;
|
||||
}
|
||||
button.primary-ghost:hover:not(:disabled) { background: var(--accent-tint); }
|
||||
|
||||
button.ghost {
|
||||
background: transparent;
|
||||
border-color: var(--border);
|
||||
color: var(--text);
|
||||
}
|
||||
button.ghost:hover:not(:disabled) { background: var(--bg-subtle); border-color: var(--border-strong); }
|
||||
/*
|
||||
* Transient success state for ghost buttons. Used by the Media
|
||||
* Providers "Reload from daemon" button after a successful reload —
|
||||
* the button briefly turns green with a check icon for ~2s then snaps
|
||||
* back to its idle treatment, replacing what used to be a permanent
|
||||
* success paragraph under the section header. Reusable on any
|
||||
* ghost button that wants the same "did it" pulse.
|
||||
*/
|
||||
button.ghost.is-success-flash {
|
||||
border-color: var(--green-border, color-mix(in srgb, #1f9d55 32%, var(--border)));
|
||||
background: var(--green-bg, color-mix(in srgb, #1f9d55 8%, transparent));
|
||||
color: var(--green, #137a3d);
|
||||
}
|
||||
button.ghost.is-success-flash:hover:not(:disabled) {
|
||||
background: var(--green-bg, color-mix(in srgb, #1f9d55 14%, transparent));
|
||||
}
|
||||
button.ghost.is-success-flash svg {
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
/*
|
||||
* Visually-hidden but assistive-tech accessible. We use it to keep
|
||||
* a `role="status"` live-region for actions whose visible feedback
|
||||
* is too transient (e.g. a 2s button-label flash) for a screen
|
||||
* reader to catch reliably. Borrowed from Tailwind's `sr-only` so
|
||||
* it reads as the same primitive everyone already knows.
|
||||
*/
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
button.subtle {
|
||||
background: var(--bg-subtle);
|
||||
border-color: transparent;
|
||||
color: var(--text);
|
||||
}
|
||||
button.subtle:hover:not(:disabled) { background: var(--bg-muted); }
|
||||
|
||||
button.icon-btn { padding: 6px 10px; font-size: 13px; }
|
||||
button:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
|
||||
/* -------- Inputs ---------------------------------------------------- */
|
||||
input, textarea, select {
|
||||
font: inherit;
|
||||
color: var(--text);
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 7px 10px;
|
||||
width: 100%;
|
||||
transition: border-color 120ms ease, box-shadow 120ms ease;
|
||||
}
|
||||
input::placeholder, textarea::placeholder { color: var(--text-faint); }
|
||||
input:focus, textarea:focus, select:focus {
|
||||
outline: none;
|
||||
border-color: var(--selected);
|
||||
box-shadow: 0 0 0 3px var(--selected-soft);
|
||||
}
|
||||
/* Entry sidebar form fields keep a quieter, neutral focus so the orange
|
||||
"Create" CTA stays the loudest thing in the panel. A low-opacity black
|
||||
halo reads as "focused" without re-introducing the global blue/accent
|
||||
ring. Using rgba directly because the Next CSS pipeline collapses
|
||||
`color-mix(in srgb, var(--text) 8%, transparent)` to a solid var(--text)
|
||||
ring (would render a 3px black band — too loud). */
|
||||
.entry-side input:focus,
|
||||
.entry-side textarea:focus,
|
||||
.entry-side select:focus {
|
||||
border-color: var(--text);
|
||||
box-shadow: 0 0 0 3px rgba(28, 27, 26, 0.08);
|
||||
}
|
||||
/* Native <select> on macOS uses its own intrinsic min-height that ends up
|
||||
shorter than <input> at the same `padding: 7px 10px` rule above, so any
|
||||
form that flex-aligns an input next to a select (e.g. the memory editor's
|
||||
Title + Type row) renders with mismatched heights. Stripping the native
|
||||
chrome lets the shared padding and inherited line-height compute the
|
||||
same box dimensions as the input, then we paint our own chevron via a
|
||||
background SVG. The chevron color is a per-theme override so it stays
|
||||
readable against the panel in both light and dark. */
|
||||
select {
|
||||
padding-right: 32px;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='none' stroke='%2374716b' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='3 5 6 8 9 5'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 10px center;
|
||||
background-size: 12px 12px;
|
||||
}
|
||||
select::-ms-expand { display: none; }
|
||||
[data-theme='dark'] select {
|
||||
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='none' stroke='%239a9690' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='3 5 6 8 9 5'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 10px center;
|
||||
background-size: 12px 12px;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html:not([data-theme]) select {
|
||||
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='none' stroke='%239a9690' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='3 5 6 8 9 5'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 10px center;
|
||||
background-size: 12px 12px;
|
||||
}
|
||||
}
|
||||
textarea { resize: vertical; font-family: inherit; }
|
||||
|
||||
.od-select {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
.od-select-trigger {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 36px;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 7px 10px;
|
||||
font: inherit;
|
||||
color: var(--text);
|
||||
text-align: left;
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
transition: border-color 120ms ease, box-shadow 120ms ease, background 120ms ease;
|
||||
}
|
||||
.od-select-trigger:hover:not(:disabled),
|
||||
.od-select-trigger[aria-expanded='true'] {
|
||||
border-color: var(--border-strong);
|
||||
background: var(--bg-subtle);
|
||||
}
|
||||
.od-select-trigger:focus-visible,
|
||||
.od-select-trigger[aria-expanded='true'] {
|
||||
outline: none;
|
||||
border-color: var(--selected);
|
||||
box-shadow: 0 0 0 3px var(--selected-soft);
|
||||
}
|
||||
.od-select-trigger:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.od-select-trigger svg {
|
||||
color: var(--text-muted);
|
||||
transition: transform 120ms ease;
|
||||
}
|
||||
.od-select-trigger[aria-expanded='true'] svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.od-select-value,
|
||||
.od-select-option-label {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.od-select-menu {
|
||||
z-index: 9000;
|
||||
padding: 4px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
overscroll-behavior: contain;
|
||||
max-width: calc(100vw - 24px);
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
.od-select-menu.portal {
|
||||
position: fixed;
|
||||
}
|
||||
.od-select-menu.inline {
|
||||
position: absolute;
|
||||
top: calc(100% + 4px);
|
||||
left: 0;
|
||||
right: 0;
|
||||
max-height: min(280px, 48vh);
|
||||
}
|
||||
.od-select-option {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 30px;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 16px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 8px;
|
||||
color: var(--text);
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
font: inherit;
|
||||
font-size: 12.5px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
.od-select-option:hover:not(:disabled),
|
||||
.od-select-option.active:not(:disabled) {
|
||||
background: var(--bg-subtle);
|
||||
}
|
||||
.od-select-option.selected {
|
||||
color: var(--text);
|
||||
font-weight: 600;
|
||||
}
|
||||
.od-select-option:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.od-select-option-check {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
color: var(--selected);
|
||||
opacity: 0;
|
||||
}
|
||||
.od-select-option.selected .od-select-option-check {
|
||||
opacity: 1;
|
||||
}
|
||||
.od-select-group + .od-select-group {
|
||||
margin-top: 4px;
|
||||
padding-top: 4px;
|
||||
border-top: 1px solid var(--border-soft);
|
||||
}
|
||||
.od-select-group-label {
|
||||
padding: 6px 8px 4px;
|
||||
color: var(--text-faint);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: var(--mono);
|
||||
background: var(--bg-subtle);
|
||||
|
|
|
|||
|
|
@ -357,8 +357,8 @@
|
|||
}
|
||||
.picker select {
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 4px 6px;
|
||||
background-color: transparent;
|
||||
padding: 4px 26px 4px 6px;
|
||||
width: auto;
|
||||
min-width: 120px;
|
||||
box-shadow: none;
|
||||
|
|
|
|||
|
|
@ -1912,13 +1912,16 @@ button.ghost.mcp-copy-btn:hover:not(:disabled) {
|
|||
.inspect-row input[type='text'],
|
||||
.inspect-row select {
|
||||
min-width: 0;
|
||||
padding: 3px 6px;
|
||||
padding: 3px 26px 3px 6px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
background: var(--bg);
|
||||
background-color: var(--bg);
|
||||
font-size: 11px;
|
||||
font-family: inherit;
|
||||
}
|
||||
.inspect-row input[type='text'] {
|
||||
padding-right: 6px;
|
||||
}
|
||||
.inspect-row input[type='range'] {
|
||||
width: 100%;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
|
||||
const packageCssImports = new Map([
|
||||
['@open-design/components/styles.css', join(process.cwd(), '../../packages/components/src/styles.css')],
|
||||
]);
|
||||
|
||||
function expandCssFile(filePath: string, seen = new Set<string>()): string {
|
||||
const key = filePath;
|
||||
if (seen.has(key)) {
|
||||
|
|
@ -12,7 +16,8 @@ function expandCssFile(filePath: string, seen = new Set<string>()): string {
|
|||
return css.replace(/@import\s+(?:url\(([^)]+)\)|(['"])([^'"]+)\2);/g, (_match, urlImport, _quote, quotedImport) => {
|
||||
const specifier = (quotedImport ?? urlImport ?? '').trim().replace(/^['"]|['"]$/g, '');
|
||||
if (!specifier.startsWith('./') && !specifier.startsWith('../')) {
|
||||
return '';
|
||||
const packageCssPath = packageCssImports.get(specifier);
|
||||
return packageCssPath == null ? '' : expandCssFile(packageCssPath, seen);
|
||||
}
|
||||
return expandCssFile(join(dirname(filePath), specifier), seen);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -142,6 +142,7 @@ async function runToolsDevSuite(
|
|||
break;
|
||||
} catch (error) {
|
||||
if (attempt === 3 || !toolsDev.isToolsDevPortConflict(error)) throw error;
|
||||
await toolsDev.stopToolsDevWeb(suite).catch(() => undefined);
|
||||
runtime = await toolsDev.allocateToolsDevRuntime();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@
|
|||
# Keep in sync with .github/workflows/ci.yml change_scopes
|
||||
# nix_validation_required filter.
|
||||
webWorkspacePaths = [
|
||||
"packages/components"
|
||||
"packages/contracts"
|
||||
"packages/host"
|
||||
"packages/platform"
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ Follow the root `AGENTS.md` first. This file only records module-level boundarie
|
|||
## Package responsibilities
|
||||
|
||||
- `packages/contracts`: web/daemon app contract layer. Keep it pure TypeScript; it must not depend on Next.js, Express, Node filesystem/process APIs, browser APIs, SQLite, daemon internals, or the sidecar control-plane protocol.
|
||||
- `packages/components`: shared React UI primitives and primitive CSS. It may depend on React types/runtime only; keep product workflows and app-specific layout/styling in the apps.
|
||||
- `packages/host`: web/desktop host bridge contract. It models renderer-facing host capabilities and helpers while keeping `window.__od__` access out of app UI code.
|
||||
- `packages/sidecar-proto`: Open Design sidecar business protocol. Owns app/mode/source constants, namespace validation, stamp descriptor/fields/flags, IPC message schema, status shapes, error semantics, and default product path constants.
|
||||
- `packages/sidecar`: generic sidecar runtime primitives. Includes bootstrap, IPC transport, path/runtime resolution, launch env, and JSON runtime file helpers; it must not hard-code Open Design app keys or IPC business messages.
|
||||
|
|
|
|||
17
packages/components/esbuild.config.ts
Normal file
17
packages/components/esbuild.config.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { rmSync } from 'node:fs';
|
||||
|
||||
import { build } from 'esbuild';
|
||||
|
||||
rmSync('./dist', { force: true, recursive: true });
|
||||
|
||||
await build({
|
||||
bundle: true,
|
||||
entryPoints: ['./src/index.ts'],
|
||||
format: 'esm',
|
||||
outbase: './src',
|
||||
outdir: './dist',
|
||||
outExtension: { '.js': '.mjs' },
|
||||
packages: 'external',
|
||||
platform: 'browser',
|
||||
target: 'es2022',
|
||||
});
|
||||
37
packages/components/package.json
Normal file
37
packages/components/package.json
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"name": "@open-design/components",
|
||||
"version": "0.8.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Shared Open Design React UI primitives.",
|
||||
"main": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"src/styles.css"
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"development": "./src/index.ts",
|
||||
"default": "./dist/index.mjs"
|
||||
},
|
||||
"./styles.css": "./src/styles.css"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsx ./esbuild.config.ts && tsc -p tsconfig.json --emitDeclarationOnly",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "18.3.28",
|
||||
"esbuild": "0.28.0",
|
||||
"tsx": "4.22.3",
|
||||
"typescript": "5.9.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "~24"
|
||||
}
|
||||
}
|
||||
75
packages/components/src/button.module.css
Normal file
75
packages/components/src/button.module.css
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
.button {
|
||||
font: inherit;
|
||||
color: var(--text);
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 6px 12px;
|
||||
cursor: pointer;
|
||||
transition: background 120ms ease, border-color 120ms ease, color 120ms ease, box-shadow 120ms ease;
|
||||
}
|
||||
|
||||
.button:hover:not(:disabled) {
|
||||
background: var(--bg-subtle);
|
||||
border-color: var(--border-strong);
|
||||
}
|
||||
|
||||
.button:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.primary {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 1px 0 rgba(180, 90, 59, 0.18) inset, var(--shadow-xs);
|
||||
}
|
||||
|
||||
.primary:hover:not(:disabled) {
|
||||
background: var(--accent-hover);
|
||||
border-color: var(--accent-hover);
|
||||
}
|
||||
|
||||
.primaryGhost {
|
||||
background: var(--bg-panel);
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.primaryGhost:hover:not(:disabled) {
|
||||
background: var(--accent-tint);
|
||||
}
|
||||
|
||||
.ghost {
|
||||
background: transparent;
|
||||
border-color: var(--border);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.ghost:hover:not(:disabled) {
|
||||
background: var(--bg-subtle);
|
||||
border-color: var(--border-strong);
|
||||
}
|
||||
|
||||
.subtle {
|
||||
background: var(--bg-subtle);
|
||||
border-color: transparent;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.subtle:hover:not(:disabled) {
|
||||
background: var(--bg-muted);
|
||||
}
|
||||
|
||||
.icon {
|
||||
padding: 6px 10px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
45
packages/components/src/button.tsx
Normal file
45
packages/components/src/button.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { forwardRef } from 'react';
|
||||
import type { ButtonHTMLAttributes } from 'react';
|
||||
|
||||
import { joinClassNames } from './class-names';
|
||||
import styles from './button.module.css';
|
||||
|
||||
export type ButtonVariant = 'default' | 'primary' | 'primary-ghost' | 'ghost' | 'subtle';
|
||||
export type ButtonSize = 'default' | 'icon';
|
||||
|
||||
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: ButtonVariant;
|
||||
size?: ButtonSize;
|
||||
}
|
||||
|
||||
const variantClassNames: Record<ButtonVariant, string | undefined> = {
|
||||
default: undefined,
|
||||
primary: joinClassNames(styles.primary, 'primary'),
|
||||
'primary-ghost': joinClassNames(styles.primaryGhost, 'primary-ghost'),
|
||||
ghost: joinClassNames(styles.ghost, 'ghost'),
|
||||
subtle: joinClassNames(styles.subtle, 'subtle'),
|
||||
};
|
||||
|
||||
const sizeClassNames: Record<ButtonSize, string | undefined> = {
|
||||
default: undefined,
|
||||
icon: joinClassNames(styles.icon, 'icon-btn'),
|
||||
};
|
||||
|
||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
|
||||
{ className, type = 'button', variant = 'default', size = 'default', ...props },
|
||||
ref,
|
||||
) {
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
type={type}
|
||||
className={joinClassNames(
|
||||
styles.button,
|
||||
variantClassNames[variant],
|
||||
sizeClassNames[size],
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
3
packages/components/src/class-names.ts
Normal file
3
packages/components/src/class-names.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export function joinClassNames(...classNames: Array<string | false | null | undefined>) {
|
||||
return classNames.filter(Boolean).join(' ') || undefined;
|
||||
}
|
||||
4
packages/components/src/css-modules.d.ts
vendored
Normal file
4
packages/components/src/css-modules.d.ts
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
declare module '*.module.css' {
|
||||
const classes: Record<string, string>;
|
||||
export default classes;
|
||||
}
|
||||
49
packages/components/src/form-controls.module.css
Normal file
49
packages/components/src/form-controls.module.css
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
.control {
|
||||
font: inherit;
|
||||
color: var(--text);
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 7px 10px;
|
||||
width: 100%;
|
||||
transition: border-color 120ms ease, box-shadow 120ms ease;
|
||||
}
|
||||
|
||||
.control::placeholder {
|
||||
color: var(--text-faint);
|
||||
}
|
||||
|
||||
.control:focus {
|
||||
outline: none;
|
||||
border-color: var(--selected);
|
||||
box-shadow: 0 0 0 3px var(--selected-soft);
|
||||
}
|
||||
|
||||
.textarea {
|
||||
resize: vertical;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.select {
|
||||
padding-right: 32px;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='none' stroke='%2374716b' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='3 5 6 8 9 5'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 10px center;
|
||||
background-size: 12px 12px;
|
||||
}
|
||||
|
||||
.select::-ms-expand {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .select {
|
||||
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='none' stroke='%239a9690' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='3 5 6 8 9 5'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html:not([data-theme]) .select {
|
||||
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='none' stroke='%239a9690' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='3 5 6 8 9 5'/%3E%3C/svg%3E");
|
||||
}
|
||||
}
|
||||
32
packages/components/src/form-controls.tsx
Normal file
32
packages/components/src/form-controls.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { forwardRef } from 'react';
|
||||
import type { InputHTMLAttributes, SelectHTMLAttributes, TextareaHTMLAttributes } from 'react';
|
||||
|
||||
import { joinClassNames } from './class-names';
|
||||
import styles from './form-controls.module.css';
|
||||
|
||||
export type InputProps = InputHTMLAttributes<HTMLInputElement>;
|
||||
|
||||
export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
|
||||
{ className, ...props },
|
||||
ref,
|
||||
) {
|
||||
return <input ref={ref} className={joinClassNames(styles.control, className)} {...props} />;
|
||||
});
|
||||
|
||||
export type TextareaProps = TextareaHTMLAttributes<HTMLTextAreaElement>;
|
||||
|
||||
export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(function Textarea(
|
||||
{ className, ...props },
|
||||
ref,
|
||||
) {
|
||||
return <textarea ref={ref} className={joinClassNames(styles.control, styles.textarea, className)} {...props} />;
|
||||
});
|
||||
|
||||
export type SelectProps = SelectHTMLAttributes<HTMLSelectElement>;
|
||||
|
||||
export const Select = forwardRef<HTMLSelectElement, SelectProps>(function Select(
|
||||
{ className, ...props },
|
||||
ref,
|
||||
) {
|
||||
return <select ref={ref} className={joinClassNames(styles.control, styles.select, className)} {...props} />;
|
||||
});
|
||||
6
packages/components/src/index.ts
Normal file
6
packages/components/src/index.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export { Button } from './button';
|
||||
export type { ButtonProps, ButtonSize, ButtonVariant } from './button';
|
||||
export { Input, Select, Textarea } from './form-controls';
|
||||
export type { InputProps, SelectProps, TextareaProps } from './form-controls';
|
||||
export { VisuallyHidden } from './visually-hidden';
|
||||
export type { VisuallyHiddenProps } from './visually-hidden';
|
||||
6
packages/components/src/primitives.tsx
Normal file
6
packages/components/src/primitives.tsx
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export { Button } from './button';
|
||||
export type { ButtonProps, ButtonSize, ButtonVariant } from './button';
|
||||
export { Input, Select, Textarea } from './form-controls';
|
||||
export type { InputProps, SelectProps, TextareaProps } from './form-controls';
|
||||
export { VisuallyHidden } from './visually-hidden';
|
||||
export type { VisuallyHiddenProps } from './visually-hidden';
|
||||
275
packages/components/src/styles.css
Normal file
275
packages/components/src/styles.css
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
/* -------- Buttons --------------------------------------------------- */
|
||||
button {
|
||||
font: inherit;
|
||||
color: var(--text);
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 6px 12px;
|
||||
cursor: pointer;
|
||||
transition: background 120ms ease, border-color 120ms ease, color 120ms ease, box-shadow 120ms ease;
|
||||
}
|
||||
button:hover:not(:disabled) { background: var(--bg-subtle); border-color: var(--border-strong); }
|
||||
button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||
|
||||
button.primary {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 1px 0 rgba(180, 90, 59, 0.18) inset, var(--shadow-xs);
|
||||
}
|
||||
button.primary:hover:not(:disabled) {
|
||||
background: var(--accent-hover);
|
||||
border-color: var(--accent-hover);
|
||||
}
|
||||
button.primary-ghost {
|
||||
background: var(--bg-panel);
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
font-weight: 500;
|
||||
}
|
||||
button.primary-ghost:hover:not(:disabled) { background: var(--accent-tint); }
|
||||
|
||||
button.ghost {
|
||||
background: transparent;
|
||||
border-color: var(--border);
|
||||
color: var(--text);
|
||||
}
|
||||
button.ghost:hover:not(:disabled) { background: var(--bg-subtle); border-color: var(--border-strong); }
|
||||
/*
|
||||
* Transient success state for ghost buttons. Used by the Media
|
||||
* Providers "Reload from daemon" button after a successful reload —
|
||||
* the button briefly turns green with a check icon for ~2s then snaps
|
||||
* back to its idle treatment, replacing what used to be a permanent
|
||||
* success paragraph under the section header. Reusable on any
|
||||
* ghost button that wants the same "did it" pulse.
|
||||
*/
|
||||
button.ghost.is-success-flash {
|
||||
border-color: var(--green-border, color-mix(in srgb, #1f9d55 32%, var(--border)));
|
||||
background: var(--green-bg, color-mix(in srgb, #1f9d55 8%, transparent));
|
||||
color: var(--green, #137a3d);
|
||||
}
|
||||
button.ghost.is-success-flash:hover:not(:disabled) {
|
||||
background: var(--green-bg, color-mix(in srgb, #1f9d55 14%, transparent));
|
||||
}
|
||||
button.ghost.is-success-flash svg {
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
/*
|
||||
* Visually-hidden but assistive-tech accessible. We use it to keep
|
||||
* a `role="status"` live-region for actions whose visible feedback
|
||||
* is too transient (e.g. a 2s button-label flash) for a screen
|
||||
* reader to catch reliably. Borrowed from Tailwind's `sr-only` so
|
||||
* it reads as the same primitive everyone already knows.
|
||||
*/
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
button.subtle {
|
||||
background: var(--bg-subtle);
|
||||
border-color: transparent;
|
||||
color: var(--text);
|
||||
}
|
||||
button.subtle:hover:not(:disabled) { background: var(--bg-muted); }
|
||||
|
||||
button.icon-btn { padding: 6px 10px; font-size: 13px; }
|
||||
button:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
|
||||
/* -------- Inputs ---------------------------------------------------- */
|
||||
input, textarea, select {
|
||||
font: inherit;
|
||||
color: var(--text);
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 7px 10px;
|
||||
width: 100%;
|
||||
transition: border-color 120ms ease, box-shadow 120ms ease;
|
||||
}
|
||||
input::placeholder, textarea::placeholder { color: var(--text-faint); }
|
||||
input:focus, textarea:focus, select:focus {
|
||||
outline: none;
|
||||
border-color: var(--selected);
|
||||
box-shadow: 0 0 0 3px var(--selected-soft);
|
||||
}
|
||||
/* Entry sidebar form fields keep a quieter, neutral focus so the orange
|
||||
"Create" CTA stays the loudest thing in the panel. A low-opacity black
|
||||
halo reads as "focused" without re-introducing the global blue/accent
|
||||
ring. Using rgba directly because the Next CSS pipeline collapses
|
||||
`color-mix(in srgb, var(--text) 8%, transparent)` to a solid var(--text)
|
||||
ring (would render a 3px black band — too loud). */
|
||||
.entry-side input:focus,
|
||||
.entry-side textarea:focus,
|
||||
.entry-side select:focus {
|
||||
border-color: var(--text);
|
||||
box-shadow: 0 0 0 3px rgba(28, 27, 26, 0.08);
|
||||
}
|
||||
/* Native <select> on macOS uses its own intrinsic min-height that ends up
|
||||
shorter than <input> at the same `padding: 7px 10px` rule above, so any
|
||||
form that flex-aligns an input next to a select (e.g. the memory editor's
|
||||
Title + Type row) renders with mismatched heights. Stripping the native
|
||||
chrome lets the shared padding and inherited line-height compute the
|
||||
same box dimensions as the input, then we paint our own chevron via a
|
||||
background SVG. The chevron color is a per-theme override so it stays
|
||||
readable against the panel in both light and dark. */
|
||||
select {
|
||||
padding-right: 32px;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='none' stroke='%2374716b' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='3 5 6 8 9 5'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 10px center;
|
||||
background-size: 12px 12px;
|
||||
}
|
||||
select::-ms-expand { display: none; }
|
||||
[data-theme='dark'] select {
|
||||
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='none' stroke='%239a9690' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='3 5 6 8 9 5'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 10px center;
|
||||
background-size: 12px 12px;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html:not([data-theme]) select {
|
||||
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='none' stroke='%239a9690' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='3 5 6 8 9 5'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 10px center;
|
||||
background-size: 12px 12px;
|
||||
}
|
||||
}
|
||||
textarea { resize: vertical; font-family: inherit; }
|
||||
|
||||
.od-select {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
.od-select-trigger {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 36px;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 7px 10px;
|
||||
font: inherit;
|
||||
color: var(--text);
|
||||
text-align: left;
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
transition: border-color 120ms ease, box-shadow 120ms ease, background 120ms ease;
|
||||
}
|
||||
.od-select-trigger:hover:not(:disabled),
|
||||
.od-select-trigger[aria-expanded='true'] {
|
||||
border-color: var(--border-strong);
|
||||
background: var(--bg-subtle);
|
||||
}
|
||||
.od-select-trigger:focus-visible,
|
||||
.od-select-trigger[aria-expanded='true'] {
|
||||
outline: none;
|
||||
border-color: var(--selected);
|
||||
box-shadow: 0 0 0 3px var(--selected-soft);
|
||||
}
|
||||
.od-select-trigger:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.od-select-trigger svg {
|
||||
color: var(--text-muted);
|
||||
transition: transform 120ms ease;
|
||||
}
|
||||
.od-select-trigger[aria-expanded='true'] svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.od-select-value,
|
||||
.od-select-option-label {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.od-select-menu {
|
||||
z-index: 9000;
|
||||
padding: 4px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
overscroll-behavior: contain;
|
||||
max-width: calc(100vw - 24px);
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
.od-select-menu.portal {
|
||||
position: fixed;
|
||||
}
|
||||
.od-select-menu.inline {
|
||||
position: absolute;
|
||||
top: calc(100% + 4px);
|
||||
left: 0;
|
||||
right: 0;
|
||||
max-height: min(280px, 48vh);
|
||||
}
|
||||
.od-select-option {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 30px;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 16px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 8px;
|
||||
color: var(--text);
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
font: inherit;
|
||||
font-size: 12.5px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
.od-select-option:hover:not(:disabled),
|
||||
.od-select-option.active:not(:disabled) {
|
||||
background: var(--bg-subtle);
|
||||
}
|
||||
.od-select-option.selected {
|
||||
color: var(--text);
|
||||
font-weight: 600;
|
||||
}
|
||||
.od-select-option:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.od-select-option-check {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
color: var(--selected);
|
||||
opacity: 0;
|
||||
}
|
||||
.od-select-option.selected .od-select-option-check {
|
||||
opacity: 1;
|
||||
}
|
||||
.od-select-group + .od-select-group {
|
||||
margin-top: 4px;
|
||||
padding-top: 4px;
|
||||
border-top: 1px solid var(--border-soft);
|
||||
}
|
||||
.od-select-group-label {
|
||||
padding: 6px 8px 4px;
|
||||
color: var(--text-faint);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
11
packages/components/src/visually-hidden.tsx
Normal file
11
packages/components/src/visually-hidden.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import type { HTMLAttributes, ReactNode } from 'react';
|
||||
|
||||
import { joinClassNames } from './class-names';
|
||||
|
||||
export interface VisuallyHiddenProps extends HTMLAttributes<HTMLSpanElement> {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function VisuallyHidden({ children, className, ...props }: VisuallyHiddenProps) {
|
||||
return <span className={joinClassNames('sr-only', className)} {...props}>{children}</span>;
|
||||
}
|
||||
19
packages/components/tsconfig.json
Normal file
19
packages/components/tsconfig.json
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"]
|
||||
}
|
||||
|
|
@ -263,6 +263,9 @@ importers:
|
|||
'@anthropic-ai/sdk':
|
||||
specifier: 0.32.1
|
||||
version: 0.32.1
|
||||
'@open-design/components':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/components
|
||||
'@open-design/contracts':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/contracts
|
||||
|
|
@ -383,6 +386,25 @@ importers:
|
|||
specifier: 4.1.6
|
||||
version: 4.1.6(@opentelemetry/api@1.9.1)(@types/node@20.19.39)(jsdom@29.1.1)(vite@7.3.3(@types/node@20.19.39)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.22.3)(yaml@2.9.0))
|
||||
|
||||
packages/components:
|
||||
dependencies:
|
||||
react:
|
||||
specifier: 18.3.1
|
||||
version: 18.3.1
|
||||
devDependencies:
|
||||
'@types/react':
|
||||
specifier: 18.3.28
|
||||
version: 18.3.28
|
||||
esbuild:
|
||||
specifier: 0.28.0
|
||||
version: 0.28.0
|
||||
tsx:
|
||||
specifier: 4.22.3
|
||||
version: 4.22.3
|
||||
typescript:
|
||||
specifier: 5.9.3
|
||||
version: 5.9.3
|
||||
|
||||
packages/contracts:
|
||||
dependencies:
|
||||
zod:
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ const repoRoot = resolve(scriptDir, "..");
|
|||
|
||||
const buildTargets = [
|
||||
"packages/contracts",
|
||||
"packages/components",
|
||||
"packages/platform",
|
||||
"packages/download",
|
||||
"packages/host",
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ const CONTAINER_NODE_VERSION = "24.14.1";
|
|||
const CONTAINER_TOOLS_PACK_CLI_PATH = "tools/pack/bin/tools-pack.mjs";
|
||||
|
||||
export const INTERNAL_PACKAGES = [
|
||||
{ directory: "packages/components", name: "@open-design/components" },
|
||||
{ directory: "packages/contracts", name: "@open-design/contracts" },
|
||||
{ directory: "packages/registry-protocol", name: "@open-design/registry-protocol" },
|
||||
{ directory: "packages/sidecar-proto", name: "@open-design/sidecar-proto" },
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
export const PRODUCT_NAME = "Open Design";
|
||||
|
||||
export const INTERNAL_PACKAGES = [
|
||||
{ directory: "packages/components", name: "@open-design/components" },
|
||||
{ directory: "packages/contracts", name: "@open-design/contracts" },
|
||||
{ directory: "packages/registry-protocol", name: "@open-design/registry-protocol" },
|
||||
{ directory: "packages/sidecar-proto", name: "@open-design/sidecar-proto" },
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ export const NSIS_INSTALLER_LANGUAGE_BY_WEB_LOCALE = {
|
|||
"zh-TW": "zh_TW",
|
||||
} as const;
|
||||
export const INTERNAL_PACKAGES = [
|
||||
{ directory: "packages/components", name: "@open-design/components" },
|
||||
{ directory: "packages/contracts", name: "@open-design/contracts" },
|
||||
{ directory: "packages/registry-protocol", name: "@open-design/registry-protocol" },
|
||||
{ directory: "packages/sidecar-proto", name: "@open-design/sidecar-proto" },
|
||||
|
|
|
|||
|
|
@ -5,24 +5,10 @@ import { join } from "node:path";
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { ToolPackConfig } from "../src/config.js";
|
||||
import { INTERNAL_PACKAGES } from "../src/win/constants.js";
|
||||
import { createWorkspaceTarballsCacheKey } from "../src/win/app.js";
|
||||
|
||||
const PACKAGE_DIRS = [
|
||||
"packages/contracts",
|
||||
"packages/registry-protocol",
|
||||
"packages/sidecar-proto",
|
||||
"packages/sidecar",
|
||||
"packages/platform",
|
||||
"packages/download",
|
||||
"packages/host",
|
||||
"packages/agui-adapter",
|
||||
"packages/plugin-runtime",
|
||||
"packages/diagnostics",
|
||||
"apps/daemon",
|
||||
"apps/web",
|
||||
"apps/desktop",
|
||||
"apps/packaged",
|
||||
] as const;
|
||||
const PACKAGE_DIRS = INTERNAL_PACKAGES.map((packageInfo) => packageInfo.directory);
|
||||
|
||||
async function writeWorkspace(root: string): Promise<void> {
|
||||
await writeFile(join(root, "package.json"), `${JSON.stringify({ packageManager: "pnpm@10.33.2" }, null, 2)}\n`, "utf8");
|
||||
|
|
|
|||
Loading…
Reference in a new issue