diff --git a/toolkits/blueprint-rails-vue/template/.gitignore b/toolkits/blueprint-rails-vue/template/.gitignore index 626016c..ee5465a 100644 --- a/toolkits/blueprint-rails-vue/template/.gitignore +++ b/toolkits/blueprint-rails-vue/template/.gitignore @@ -19,8 +19,9 @@ data/ node_modules/ # The built View (the Vite build output of ui/) — rebuilt by the pipeline, -# never committed, never hand-edited. ui/ is the source. -public/ +# never committed, never hand-edited. ui/ is the source. Anchored to this +# directory: ui/public/ (tokens.css, ui.css) is source, not build output. +/public/ # Rails runtime artifacts (the minted secret lives in tmp/). log/ diff --git a/toolkits/blueprint-rails-vue/template/ui/public/tokens.css b/toolkits/blueprint-rails-vue/template/ui/public/tokens.css new file mode 100644 index 0000000..60258ba --- /dev/null +++ b/toolkits/blueprint-rails-vue/template/ui/public/tokens.css @@ -0,0 +1,232 @@ +/* ============================================================================ + Agent App design tokens — shared visual foundation (AGENT-OWNED to re-point, + ships in lockstep with @craftos/agent-app-kit's DESIGN_TOKENS_CSS — keep this body identical). + + The kit's warm-neutral --agent-app-* system. Components and app CSS read ONLY + semantic tokens, never hardcoded colors, so light/dark AND every [data-style] + pack keep working with no per-component edits. Three layers: + + 1. BASE globals — app font smoothing, selection color, themed scrollbars. + 2. --agent-app-* FOUNDATION — brand identity: light + dark primitives, + color-mix-derived surfaces, and the named style packs. + 3. SEMANTIC + STRUCTURAL BRIDGE — surfaces, state tones, and the + spacing/type/radius/elevation/motion scales, all re-pointed onto the + foundation so a theme is a re-point (or a [data-style] switch), never a + component edit. + + Theme by re-pointing the FOUNDATION or selecting a [data-style]; never + hardcode a color in a component. + ============================================================================ */ + +/* 1. BASE globals ---------------------------------------------------------- */ +::selection { background-color: color-mix(in srgb, var(--agent-app-accent) 26%, transparent); } +* { scrollbar-width: thin; scrollbar-color: var(--agent-app-border) transparent; } +::-webkit-scrollbar { width: 10px; height: 10px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { + background-color: var(--agent-app-border); border-radius: 9999px; + border: 2px solid transparent; background-clip: padding-box; +} +::-webkit-scrollbar-thumb:hover { background-color: var(--agent-app-muted); } + +/* 2. --agent-app-* FOUNDATION --------------------------------------------- */ +:root, +:root[data-theme='light'] { + color-scheme: light; + + /* Primitives (light) */ + --agent-app-bg: #f6f5f2; + --agent-app-surface: #ffffff; + --agent-app-text: #1f1e1b; + --agent-app-muted: #6f6d67; + --agent-app-border: #e7e5e0; + --agent-app-accent: #ff4f18; + --agent-app-accent-contrast: #ffffff; + + /* Shape + type */ + --agent-app-radius: 0.5rem; + --agent-app-font: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; + + /* Derived: declared once, resolved per element so they track theme + pack. */ + --agent-app-fg: var(--agent-app-text); + --agent-app-surface-2: color-mix(in srgb, var(--agent-app-surface), var(--agent-app-text) 4%); + --agent-app-hover: color-mix(in srgb, var(--agent-app-surface), var(--agent-app-text) 7%); + --agent-app-selected: color-mix(in srgb, var(--agent-app-surface), var(--agent-app-accent) 12%); + --agent-app-ring: var(--agent-app-accent); +} + +:root[data-theme='dark'] { + color-scheme: dark; + --agent-app-bg: #191919; + --agent-app-surface: #202020; + --agent-app-text: #e6e6e4; + --agent-app-muted: #9b9a97; + --agent-app-border: #2e2d2b; + --agent-app-accent: #ff4f18; + --agent-app-accent-contrast: #ffffff; +} + +/* Standalone dark: with no host bridge to set data-theme, follow the OS scheme + unless the app has explicitly opted into light. */ +@media (prefers-color-scheme: dark) { + :root:not([data-theme='light']) { + color-scheme: dark; + --agent-app-bg: #191919; + --agent-app-surface: #202020; + --agent-app-text: #e6e6e4; + --agent-app-muted: #9b9a97; + --agent-app-border: #2e2d2b; + } +} + +/* ---- Style packs ([data-style]; brand default 'craftbot' == base) -------- */ +:root[data-style='normal'] { --agent-app-accent: #2563eb; } +:root[data-style='normal'][data-theme='dark'] { --agent-app-accent: #3b82f6; } + +:root[data-style='ocean'] { --agent-app-accent: #0284c7; --agent-app-bg: #f0f7fb; --agent-app-border: #d3e5ef; } +:root[data-style='ocean'][data-theme='dark'] { --agent-app-accent: #38bdf8; --agent-app-bg: #0b1b26; --agent-app-surface: #102635; --agent-app-border: #1e3a4d; } + +:root[data-style='forest'] { --agent-app-accent: #16a34a; --agent-app-bg: #f2f8f2; --agent-app-border: #d6e7d6; } +:root[data-style='forest'][data-theme='dark'] { --agent-app-accent: #4ade80; --agent-app-bg: #0e1a12; --agent-app-surface: #14261a; --agent-app-border: #22402c; } + +:root[data-style='pastel'] { --agent-app-accent: #a855f7; --agent-app-bg: #faf7fd; --agent-app-surface: #fffdfa; --agent-app-border: #eadff5; } +:root[data-style='pastel'][data-theme='dark'] { --agent-app-accent: #c084fc; --agent-app-bg: #1a1420; --agent-app-surface: #251c2e; --agent-app-border: #3a2d47; } + +:root[data-style='glass'] { --agent-app-bg: #eef1f8; --agent-app-surface: rgba(255, 255, 255, 0.72); --agent-app-border: rgba(120, 130, 160, 0.25); --agent-app-accent: #6366f1; } +:root[data-style='glass'][data-theme='dark'] { --agent-app-bg: #10131c; --agent-app-surface: rgba(30, 36, 54, 0.72); --agent-app-border: rgba(140, 150, 190, 0.22); --agent-app-accent: #818cf8; } + +:root[data-style='classic'] { --agent-app-bg: #f5f2ea; --agent-app-surface: #fffdf7; --agent-app-border: #ddd6c5; --agent-app-accent: #b8860b; --agent-app-radius: 0.25rem; } +:root[data-style='classic'][data-theme='dark'] { --agent-app-bg: #1c1a14; --agent-app-surface: #26231b; --agent-app-border: #3d3828; --agent-app-accent: #d4a017; } + +:root[data-style='velvet'] { --agent-app-bg: #f8f2f6; --agent-app-surface: #fffbfe; --agent-app-border: #e8d8e4; --agent-app-accent: #9d174d; } +:root[data-style='velvet'][data-theme='dark'] { --agent-app-bg: #1c1018; --agent-app-surface: #281826; --agent-app-border: #43263c; --agent-app-accent: #ec4899; } + +:root[data-style='ink'] { --agent-app-bg: #ffffff; --agent-app-surface: #ffffff; --agent-app-border: #111111; --agent-app-accent: #111111; --agent-app-accent-contrast: #ffffff; --agent-app-radius: 0; } +:root[data-style='ink'][data-theme='dark'] { --agent-app-bg: #0a0a0a; --agent-app-surface: #0a0a0a; --agent-app-border: #f5f5f5; --agent-app-accent: #f5f5f5; --agent-app-accent-contrast: #0a0a0a; } + +:root[data-style='acid'] { --agent-app-bg: #fafff2; --agent-app-surface: #ffffff; --agent-app-border: #d9f99d; --agent-app-accent: #65a30d; } +:root[data-style='acid'][data-theme='dark'] { --agent-app-bg: #131a0c; --agent-app-surface: #1b2513; --agent-app-border: #365314; --agent-app-accent: #a3e635; --agent-app-accent-contrast: #1a2e05; } + +:root[data-style='blueprint'] { --agent-app-bg: #eef4fb; --agent-app-surface: #ffffff; --agent-app-border: #93c5fd; --agent-app-accent: #1d4ed8; --agent-app-radius: 0.125rem; } +:root[data-style='blueprint'][data-theme='dark'] { --agent-app-bg: #0b1526; --agent-app-surface: #102039; --agent-app-border: #1e40af; --agent-app-accent: #60a5fa; } + +:root[data-style='modern'] { --agent-app-bg: #f4f5fa; --agent-app-surface: #ffffff; --agent-app-border: #e2e4f0; --agent-app-accent: #6366f1; --agent-app-radius: 0.75rem; } +:root[data-style='modern'][data-theme='dark'] { --agent-app-bg: #12141d; --agent-app-surface: #1a1d2a; --agent-app-border: #2a2e42; --agent-app-accent: #7c8aff; } + +:root[data-style='brutalist'] { --agent-app-bg: #ffffff; --agent-app-surface: #ffffff; --agent-app-text: #0a0a0a; --agent-app-border: #0a0a0a; --agent-app-accent: #7c3aed; --agent-app-radius: 0; } +:root[data-style='brutalist'][data-theme='dark'] { --agent-app-bg: #0a0a0a; --agent-app-surface: #0a0a0a; --agent-app-text: #fafafa; --agent-app-border: #fafafa; --agent-app-accent: #a78bfa; --agent-app-accent-contrast: #0a0a0a; } + +:root[data-style='drafting'] { --agent-app-bg: #e9ede4; --agent-app-surface: #e9ede4; --agent-app-text: #2e3528; --agent-app-muted: #5b6552; --agent-app-border: #2e3528; --agent-app-accent: #3a4232; --agent-app-radius: 0.25rem; } +:root[data-style='drafting'][data-theme='dark'] { --agent-app-bg: #232920; --agent-app-surface: #232920; --agent-app-text: #dde3d6; --agent-app-muted: #9aa590; --agent-app-border: #c8d0c0; --agent-app-accent: #aab5a0; --agent-app-accent-contrast: #232920; } + +:root[data-style='clay'] { --agent-app-bg: #e4e6ec; --agent-app-surface: #e4e6ec; --agent-app-text: #3a3f4c; --agent-app-border: #c9cdd8; --agent-app-accent: #5b7cfa; --agent-app-radius: 0.875rem; } +:root[data-style='clay'][data-theme='dark'] { --agent-app-bg: #23262e; --agent-app-surface: #23262e; --agent-app-text: #d5d8e0; --agent-app-border: #343947; --agent-app-accent: #7c96ff; } + +:root[data-style='atelier'] { --agent-app-bg: #edeff2; --agent-app-surface: #f8f9fb; --agent-app-text: #1c1e22; --agent-app-border: #d8dbe1; --agent-app-accent: #17181b; --agent-app-accent-contrast: #f8f9fb; --agent-app-radius: 0.375rem; } +:root[data-style='atelier'][data-theme='dark'] { --agent-app-bg: #17181b; --agent-app-surface: #202227; --agent-app-text: #e8e9ec; --agent-app-border: #33363d; --agent-app-accent: #f2f2f4; --agent-app-accent-contrast: #17181b; } + +/* 3. SEMANTIC + STRUCTURAL BRIDGE ----------------------------------------- */ +:root { + /* type */ + --font-sans: var(--agent-app-font); + --font-mono: ui-monospace, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace; + --fs-xs: 12px; --fs-sm: 13px; --fs-base: 14px; --fs-md: 15px; --fs-lg: 16px; --fs-xl: 20px; --fs-2xl: 24px; + --lh-tight: 1.25; --lh-normal: 1.5; + --fw-regular: 400; --fw-medium: 500; --fw-semibold: 600; --fw-bold: 700; + + /* spacing (4px grid) */ + --sp-1: 4px; --sp-2: 8px; --sp-3: 12px; --sp-4: 16px; --sp-5: 20px; --sp-6: 24px; + --sp-8: 32px; --sp-10: 40px; --sp-12: 48px; --sp-16: 64px; + + /* radius — scaled off the foundation so style packs (ink/brutalist=0, + clay/modern=larger) reshape the whole app */ + --r-sm: calc(var(--agent-app-radius) * 0.5); + --r-md: var(--agent-app-radius); + --r-lg: calc(var(--agent-app-radius) * 1.25); + --r-xl: calc(var(--agent-app-radius) * 1.75); + --r-full: 9999px; + + /* elevation */ + --sh-1: 0 1px 2px rgb(16 24 40 / .06), 0 1px 3px rgb(16 24 40 / .10); + --sh-2: 0 2px 4px rgb(16 24 40 / .06), 0 4px 8px rgb(16 24 40 / .08); + --sh-3: 0 4px 6px rgb(16 24 40 / .05), 0 10px 20px rgb(16 24 40 / .12); + --sh-4: 0 8px 16px rgb(16 24 40 / .10), 0 16px 32px rgb(16 24 40 / .16); + + /* motion */ + --dur-fast: 120ms; --dur-mid: 200ms; --dur-slow: 300ms; + --ease-out: cubic-bezier(0.16, 1, 0.3, 1); --ease-std: cubic-bezier(0.4, 0, 0.2, 1); + + /* controls */ + --control-h: 36px; --row-h: 44px; + + /* surfaces + text re-pointed onto the foundation */ + --bg-canvas: var(--agent-app-bg); + --bg-surface: var(--agent-app-surface); + --bg-raised: var(--agent-app-surface); + --bg-sunken: var(--agent-app-surface-2); + --bg-hover: var(--agent-app-hover); + --bg-active: var(--agent-app-selected); + --bg-selected: var(--agent-app-selected); + --text-primary: var(--agent-app-fg); + --text-secondary: var(--agent-app-muted); + --text-tertiary: var(--agent-app-muted); + --text-disabled: color-mix(in srgb, var(--agent-app-muted), var(--agent-app-surface) 45%); + --text-on-accent: var(--agent-app-accent-contrast); + --border-subtle: color-mix(in srgb, var(--agent-app-border), var(--agent-app-surface) 45%); + --border-default: var(--agent-app-border); + --border-strong: color-mix(in srgb, var(--agent-app-border), var(--agent-app-text) 25%); + --accent-solid: var(--agent-app-accent); + --accent-hover: color-mix(in srgb, var(--agent-app-accent), var(--agent-app-text) 14%); + --accent-text: var(--agent-app-accent); + --accent-bg: color-mix(in srgb, var(--agent-app-accent), var(--agent-app-surface) 88%); + --accent-border: color-mix(in srgb, var(--agent-app-accent), var(--agent-app-surface) 68%); + --focus-ring: var(--agent-app-ring); + + /* state tones — solid + text carry the hue; bg + border derive off the solid + against the current surface, so they follow theme AND every style pack */ + --success-solid: #16a34a; --success-text: #15803d; + --warning-solid: #d97706; --warning-text: #b45309; + --danger-solid: #dc2626; --danger-text: #b91c1c; + --info-solid: #2563eb; --info-text: #1d4ed8; + --success-bg: color-mix(in srgb, var(--success-solid) 12%, var(--agent-app-surface)); + --success-border: color-mix(in srgb, var(--success-solid) 28%, var(--agent-app-surface)); + --warning-bg: color-mix(in srgb, var(--warning-solid) 12%, var(--agent-app-surface)); + --warning-border: color-mix(in srgb, var(--warning-solid) 28%, var(--agent-app-surface)); + --danger-bg: color-mix(in srgb, var(--danger-solid) 12%, var(--agent-app-surface)); + --danger-border: color-mix(in srgb, var(--danger-solid) 28%, var(--agent-app-surface)); + --info-bg: color-mix(in srgb, var(--info-solid) 12%, var(--agent-app-surface)); + --info-border: color-mix(in srgb, var(--info-solid) 28%, var(--agent-app-surface)); + --neutral-text: var(--agent-app-muted); + --neutral-bg: color-mix(in srgb, var(--agent-app-text) 6%, var(--agent-app-surface)); + --neutral-border: var(--agent-app-border); +} + +/* Dark: brighten state solids/text + deepen elevation. bg/border derive off the + solids above, so they update automatically. Applied for explicit dark and for + OS-dark when the app has not opted into light. */ +:root[data-theme='dark'] { + --success-solid: #22c55e; --success-text: #4ade80; + --warning-solid: #f59e0b; --warning-text: #fbbf24; + --danger-solid: #ef4444; --danger-text: #f87171; + --info-solid: #3b82f6; --info-text: #60a5fa; + --sh-1: 0 1px 2px rgb(0 0 0 / .4); + --sh-2: 0 2px 8px rgb(0 0 0 / .45); + --sh-3: 0 8px 20px rgb(0 0 0 / .5); + --sh-4: 0 16px 32px rgb(0 0 0 / .55); +} +@media (prefers-color-scheme: dark) { + :root:not([data-theme='light']) { + --success-solid: #22c55e; --success-text: #4ade80; + --warning-solid: #f59e0b; --warning-text: #fbbf24; + --danger-solid: #ef4444; --danger-text: #f87171; + --info-solid: #3b82f6; --info-text: #60a5fa; + --sh-1: 0 1px 2px rgb(0 0 0 / .4); + --sh-2: 0 2px 8px rgb(0 0 0 / .45); + --sh-3: 0 8px 20px rgb(0 0 0 / .5); + --sh-4: 0 16px 32px rgb(0 0 0 / .55); + } +} + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; } +} diff --git a/toolkits/blueprint-rails-vue/template/ui/public/ui.css b/toolkits/blueprint-rails-vue/template/ui/public/ui.css new file mode 100644 index 0000000..d795803 --- /dev/null +++ b/toolkits/blueprint-rails-vue/template/ui/public/ui.css @@ -0,0 +1,208 @@ +/* ============================================================================ + UI components (AGENT-OWNED). Consumes ONLY semantic tokens from tokens.css. + Every interactive element carries cursor + hover + focus-visible + active + + disabled states; every screen region has loading / empty / error styling. + ============================================================================ */ + +/* ---------------------------------------------------------------- base */ +*, *::before, *::after { box-sizing: border-box; } +html { height: 100%; } +body { + margin: 0; min-height: 100%; + font-family: var(--font-sans); font-size: var(--fs-md); line-height: var(--lh-normal); + color: var(--text-primary); background: var(--bg-canvas); + -webkit-font-smoothing: antialiased; +} +:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; border-radius: var(--r-sm); } +::selection { background: var(--accent-bg); } + +.sr-only { + position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; + overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; +} + +/* ---------------------------------------------------------------- layout */ +.app { max-width: 640px; margin: 0 auto; padding: var(--sp-8) var(--sp-4) var(--sp-12); } +.app-header { margin-bottom: var(--sp-6); } +.app-header h1 { margin: 0; font-size: var(--fs-2xl); font-weight: var(--fw-bold); line-height: var(--lh-tight); } +.app-header .subtitle { margin: var(--sp-1) 0 0; color: var(--text-secondary); font-size: var(--fs-base); } +.app-footer { margin-top: var(--sp-8); color: var(--text-tertiary); font-size: var(--fs-sm); display: flex; justify-content: space-between; gap: var(--sp-3); flex-wrap: wrap; } +.card { + background: var(--bg-surface); border: 1px solid var(--border-subtle); + border-radius: var(--r-xl); box-shadow: var(--sh-1); +} + +/* ---------------------------------------------------------------- buttons */ +.btn { + display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-2); + height: var(--control-h); padding: 0 var(--sp-4); + font: inherit; font-size: var(--fs-base); font-weight: var(--fw-medium); + border-radius: var(--r-md); border: 1px solid transparent; + cursor: pointer; user-select: none; white-space: nowrap; + transition: background var(--dur-fast) var(--ease-std), border-color var(--dur-fast) var(--ease-std), + color var(--dur-fast) var(--ease-std), box-shadow var(--dur-fast) var(--ease-std); +} +.btn:disabled { cursor: not-allowed; opacity: .55; } +.btn:active:not(:disabled) { transform: translateY(0.5px); } + +.btn-primary { background: var(--accent-solid); color: var(--text-on-accent); } +.btn-primary:hover:not(:disabled) { background: var(--accent-hover); } + +.btn-ghost { background: transparent; color: var(--text-secondary); border-color: var(--border-default); } +.btn-ghost:hover:not(:disabled) { background: var(--bg-hover); color: var(--text-primary); } + +.btn-danger { background: transparent; color: var(--danger-text); border-color: var(--danger-border); } +.btn-danger:hover:not(:disabled) { background: var(--danger-bg); } +.btn-danger-solid { background: var(--danger-solid); color: var(--text-on-accent); } +.btn-danger-solid:hover:not(:disabled) { filter: brightness(1.08); } + +/* icon-only buttons stay >= 24px pointer targets (comfortably above, at 32px) */ +.btn-icon { + width: 32px; height: 32px; min-width: 32px; padding: 0; + border-radius: var(--r-md); background: transparent; border: none; + color: var(--text-tertiary); cursor: pointer; display: inline-flex; + align-items: center; justify-content: center; + transition: background var(--dur-fast) var(--ease-std), color var(--dur-fast) var(--ease-std); +} +.btn-icon:hover:not(:disabled) { background: var(--bg-hover); color: var(--text-primary); } +.btn-icon.danger:hover:not(:disabled) { background: var(--danger-bg); color: var(--danger-text); } +.btn-icon:disabled { cursor: not-allowed; opacity: .5; } + +/* pending: the control acknowledges immediately and blocks double submission */ +.btn.pending { position: relative; color: transparent !important; pointer-events: none; } +.btn.pending::after { + content: ""; position: absolute; width: 16px; height: 16px; + border: 2px solid currentColor; border-right-color: transparent; border-radius: var(--r-full); + color: var(--text-on-accent); animation: spin 600ms linear infinite; +} +.btn-ghost.pending::after, .btn-danger.pending::after { color: var(--text-secondary); } +@keyframes spin { to { transform: rotate(360deg); } } + +/* ---------------------------------------------------------------- forms */ +.field { display: flex; flex-direction: column; gap: var(--sp-1); } +.field label { font-size: var(--fs-sm); font-weight: var(--fw-medium); color: var(--text-secondary); } +.input, .select { + height: var(--control-h); padding: 0 var(--sp-3); + font: inherit; font-size: var(--fs-base); color: var(--text-primary); + background: var(--bg-surface); border: 1px solid var(--border-default); border-radius: var(--r-md); + transition: border-color var(--dur-fast) var(--ease-std), box-shadow var(--dur-fast) var(--ease-std); +} +.input::placeholder { color: var(--text-tertiary); } +.input:hover, .select:hover { border-color: var(--border-strong); } +.input:focus, .select:focus { outline: none; border-color: var(--focus-ring); box-shadow: 0 0 0 3px var(--accent-bg); } +.input[aria-invalid="true"] { border-color: var(--danger-solid); } +.input[aria-invalid="true"]:focus { box-shadow: 0 0 0 3px var(--danger-bg); } +.select { cursor: pointer; } +.field-error { color: var(--danger-text); font-size: var(--fs-sm); } + +.add-form { display: flex; gap: var(--sp-2); padding: var(--sp-4); align-items: flex-start; } +.add-form .field { flex: 1; } + +/* ---------------------------------------------------------------- filters */ +.toolbar { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); margin: var(--sp-5) 0 var(--sp-3); flex-wrap: wrap; } +.seg { display: inline-flex; background: var(--bg-sunken); border: 1px solid var(--border-subtle); border-radius: var(--r-lg); padding: 2px; gap: 2px; } +.seg button { + font: inherit; font-size: var(--fs-sm); font-weight: var(--fw-medium); + color: var(--text-secondary); background: transparent; border: none; cursor: pointer; + padding: var(--sp-1) var(--sp-3); border-radius: var(--r-md); min-height: 28px; + transition: background var(--dur-fast) var(--ease-std), color var(--dur-fast) var(--ease-std); +} +.seg button:hover { color: var(--text-primary); } +.seg button[aria-pressed="true"] { background: var(--bg-surface); color: var(--text-primary); box-shadow: var(--sh-1); } +.seg .count { color: var(--text-tertiary); font-weight: var(--fw-regular); margin-left: var(--sp-1); } + +/* ---------------------------------------------------------------- task list */ +.task-list { list-style: none; margin: 0; padding: var(--sp-1) 0; } +.task { + display: flex; align-items: center; gap: var(--sp-3); + min-height: var(--row-h); padding: var(--sp-2) var(--sp-4); + border-bottom: 1px solid var(--border-subtle); + transition: background var(--dur-fast) var(--ease-std), opacity var(--dur-mid) var(--ease-std); +} +.task:last-child { border-bottom: none; } +.task:hover { background: var(--bg-hover); } +.task.busy { opacity: .55; pointer-events: none; } +.task .title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.task.done .title { color: var(--text-tertiary); text-decoration: line-through; } +.task .due { font-size: var(--fs-xs); color: var(--text-tertiary); font-variant-numeric: tabular-nums; white-space: nowrap; } +.task .due.overdue { color: var(--danger-text); font-weight: var(--fw-medium); } + +/* status: a labelled 3-state control; colour is never the only channel (text names the state) */ +.status-btn { + font: inherit; font-size: var(--fs-xs); font-weight: var(--fw-medium); + border-radius: var(--r-full); border: 1px solid; cursor: pointer; + padding: 2px var(--sp-3); min-height: 24px; min-width: 56px; white-space: nowrap; + transition: filter var(--dur-fast) var(--ease-std), background var(--dur-fast) var(--ease-std); +} +.status-btn:hover:not(:disabled) { filter: brightness(.96); } +.status-btn:disabled { cursor: not-allowed; opacity: .6; } +.status-todo { background: var(--neutral-bg); color: var(--neutral-text); border-color: var(--neutral-border); } +.status-doing { background: var(--info-bg); color: var(--info-text); border-color: var(--info-border); } +.status-done { background: var(--success-bg); color: var(--success-text); border-color: var(--success-border); } + +.load-more { display: flex; justify-content: center; padding: var(--sp-3); border-top: 1px solid var(--border-subtle); } + +/* ---------------------------------------------------------------- states */ +.skel-row { display: flex; align-items: center; gap: var(--sp-3); min-height: var(--row-h); padding: var(--sp-2) var(--sp-4); border-bottom: 1px solid var(--border-subtle); } +.skel-row:last-child { border-bottom: none; } +.skel { border-radius: var(--r-sm); background: var(--bg-active); animation: pulse 1.2s var(--ease-std) infinite; } +@keyframes pulse { 50% { opacity: .5; } } + +.empty { display: flex; flex-direction: column; align-items: center; gap: var(--sp-3); padding: var(--sp-12) var(--sp-6); text-align: center; } +.empty .empty-icon { color: var(--text-disabled); } +.empty h2 { margin: 0; font-size: var(--fs-lg); font-weight: var(--fw-semibold); } +.empty p { margin: 0; color: var(--text-secondary); font-size: var(--fs-base); max-width: 36ch; } + +.banner { + display: flex; align-items: center; gap: var(--sp-3); + padding: var(--sp-3) var(--sp-4); border-radius: var(--r-lg); + background: var(--danger-bg); border: 1px solid var(--danger-border); color: var(--danger-text); + margin-bottom: var(--sp-4); +} +.banner .msg { flex: 1; } + +/* ---------------------------------------------------------------- toasts */ +.toast-region { position: fixed; bottom: var(--sp-6); left: 50%; transform: translateX(-50%); display: flex; flex-direction: column; gap: var(--sp-2); z-index: 60; width: max-content; max-width: min(92vw, 420px); } +.toast { + display: flex; align-items: center; gap: var(--sp-2); + padding: var(--sp-3) var(--sp-4); border-radius: var(--r-lg); box-shadow: var(--sh-3); + background: var(--bg-raised); border: 1px solid var(--border-subtle); + color: var(--text-primary); font-size: var(--fs-base); + animation: toast-in var(--dur-mid) var(--ease-out); +} +.toast.success { border-color: var(--success-border); } +.toast.success .toast-icon { color: var(--success-solid); } +.toast.error { border-color: var(--danger-border); } +.toast.error .toast-icon { color: var(--danger-solid); } +.toast.leaving { opacity: 0; transition: opacity var(--dur-mid) var(--ease-std); } +@keyframes toast-in { from { opacity: 0; transform: translateY(var(--sp-2)); } } + +/* ---------------------------------------------------------------- dialog */ +.dialog-backdrop { + position: fixed; inset: 0; z-index: 50; + background: rgb(2 6 23 / .45); + display: flex; align-items: center; justify-content: center; padding: var(--sp-4); + animation: fade-in var(--dur-fast) var(--ease-std); +} +.dialog { + width: 100%; max-width: 400px; + background: var(--bg-raised); border: 1px solid var(--border-subtle); + border-radius: var(--r-xl); box-shadow: var(--sh-4); padding: var(--sp-6); + animation: dialog-in var(--dur-mid) var(--ease-out); +} +.dialog h2 { margin: 0 0 var(--sp-2); font-size: var(--fs-lg); font-weight: var(--fw-semibold); } +.dialog p { margin: 0 0 var(--sp-5); color: var(--text-secondary); overflow-wrap: break-word; } +.dialog .dialog-actions { display: flex; justify-content: flex-end; gap: var(--sp-2); } +@keyframes fade-in { from { opacity: 0; } } +@keyframes dialog-in { from { opacity: 0; transform: translateY(var(--sp-2)) scale(.98); } } + +/* ---------------------------------------------------------------- narrow screens */ +@media (max-width: 480px) { + .app { padding: var(--sp-5) var(--sp-3) var(--sp-10); } + .add-form { flex-wrap: wrap; } + .add-form .field { flex-basis: 100%; } + .task .due { display: none; } + /* touch-first: primary controls grow toward the platform norm */ + .btn, .input, .select { min-height: 44px; } + .btn-icon { width: 40px; height: 40px; } +}