@polderlabs/bizar 7.0.0 → 7.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +125 -47
  2. package/bizar-dash/dist/assets/EnvVarsSection-BfhAz3ax.js +114 -0
  3. package/bizar-dash/dist/assets/EnvVarsSection-BfhAz3ax.js.map +1 -0
  4. package/bizar-dash/dist/assets/MobileChat-DLWdv6tQ.js +1 -0
  5. package/bizar-dash/dist/assets/{MobileChat-D7zRhU9K.js.map → MobileChat-DLWdv6tQ.js.map} +1 -1
  6. package/bizar-dash/dist/assets/MobileSettings-Bxv62uPY.js +1 -0
  7. package/bizar-dash/dist/assets/MobileSettings-Bxv62uPY.js.map +1 -0
  8. package/bizar-dash/dist/assets/Toast-ATSAmbdi.js +26 -0
  9. package/bizar-dash/dist/assets/Toast-ATSAmbdi.js.map +1 -0
  10. package/bizar-dash/dist/assets/main-306CZfn6.js +430 -0
  11. package/bizar-dash/dist/assets/main-306CZfn6.js.map +1 -0
  12. package/bizar-dash/dist/assets/main-G3bNDpY3.css +1 -0
  13. package/bizar-dash/dist/assets/mobile-C_naUN91.js +1 -0
  14. package/bizar-dash/dist/assets/{mobile-DXYCA0hC.js.map → mobile-C_naUN91.js.map} +1 -1
  15. package/bizar-dash/dist/assets/mobile-layout-BWLtwtfQ.css +1 -0
  16. package/bizar-dash/dist/assets/mobile-layout-GZ3bE8L6.js +211 -0
  17. package/bizar-dash/dist/assets/mobile-layout-GZ3bE8L6.js.map +1 -0
  18. package/bizar-dash/dist/assets/useSlashCommands-BOwD7lNb.js +65 -0
  19. package/bizar-dash/dist/assets/useSlashCommands-BOwD7lNb.js.map +1 -0
  20. package/bizar-dash/dist/index.html +7 -13
  21. package/bizar-dash/dist/mobile.html +3 -8
  22. package/bizar-dash/src/server/memory-consolidator.mjs +8 -1
  23. package/bizar-dash/src/web/mobile/MobileApp.tsx +33 -2
  24. package/bizar-dash/src/web/mobile/MobileBottomNav.tsx +2 -2
  25. package/bizar-dash/src/web/mobile/MobileEval.tsx +4 -4
  26. package/bizar-dash/src/web/mobile/MobileMemory.tsx +2 -2
  27. package/bizar-dash/src/web/mobile/MobileSettings.tsx +3 -3
  28. package/bizar-dash/src/web/mobile/MobileTasks.tsx +2 -2
  29. package/bizar-dash/src/web/mobile/components/MobileListItem.tsx +3 -3
  30. package/bizar-dash/src/web/styles/mobile.css +41 -0
  31. package/bizar-dash/src/web/styles/tasks-redesign.css +34 -12
  32. package/bizar-dash/src/web/ui/controls/Textarea.tsx +56 -0
  33. package/bizar-dash/src/web/ui/controls/controls.css +22 -0
  34. package/bizar-dash/src/web/ui/controls/index.ts +2 -0
  35. package/bizar-dash/src/web/ui/layout/ViewHeader.tsx +48 -0
  36. package/bizar-dash/src/web/ui/layout/index.ts +1 -0
  37. package/bizar-dash/src/web/ui/layout/layout.css +51 -0
  38. package/bizar-dash/src/web/ui/styles/tokens.css +13 -0
  39. package/bizar-dash/src/web/views/Agents.tsx +10 -2
  40. package/bizar-dash/src/web/views/Overview.tsx +2 -13
  41. package/bizar-dash/src/web/views/Tasks.tsx +1 -7
  42. package/bizar-dash/tests/ui/controls/Textarea.test.tsx +56 -0
  43. package/bizar-dash/tests/ui/layout/ViewHeader.test.tsx +47 -0
  44. package/cli/provision-claude.mjs +94 -0
  45. package/package.json +1 -1
  46. package/bizar-dash/dist/assets/EnvVarsSection-CW6fWKds.js +0 -4
  47. package/bizar-dash/dist/assets/EnvVarsSection-CW6fWKds.js.map +0 -1
  48. package/bizar-dash/dist/assets/MobileChat-D7zRhU9K.js +0 -1
  49. package/bizar-dash/dist/assets/MobileSettings-K-sNsn_r.js +0 -1
  50. package/bizar-dash/dist/assets/MobileSettings-K-sNsn_r.js.map +0 -1
  51. package/bizar-dash/dist/assets/Toast-WTxc3m2g.js +0 -1
  52. package/bizar-dash/dist/assets/Toast-WTxc3m2g.js.map +0 -1
  53. package/bizar-dash/dist/assets/flow-BpU0-gK2.js +0 -1
  54. package/bizar-dash/dist/assets/flow-BpU0-gK2.js.map +0 -1
  55. package/bizar-dash/dist/assets/fuzzy-l0sNRNKZ.js +0 -1
  56. package/bizar-dash/dist/assets/fuzzy-l0sNRNKZ.js.map +0 -1
  57. package/bizar-dash/dist/assets/icons-B0AKnYI3.js +0 -731
  58. package/bizar-dash/dist/assets/icons-B0AKnYI3.js.map +0 -1
  59. package/bizar-dash/dist/assets/main-C-Md3DaY.css +0 -1
  60. package/bizar-dash/dist/assets/main-CO8fIxQK.js +0 -18
  61. package/bizar-dash/dist/assets/main-CO8fIxQK.js.map +0 -1
  62. package/bizar-dash/dist/assets/markdown-w2bDDkAH.js +0 -1
  63. package/bizar-dash/dist/assets/markdown-w2bDDkAH.js.map +0 -1
  64. package/bizar-dash/dist/assets/mobile-DXYCA0hC.js +0 -1
  65. package/bizar-dash/dist/assets/mobile-layout-CJnZNLy3.css +0 -1
  66. package/bizar-dash/dist/assets/mobile-layout-DSmtNPja.js +0 -2
  67. package/bizar-dash/dist/assets/mobile-layout-DSmtNPja.js.map +0 -1
  68. package/bizar-dash/dist/assets/react-vendor-BnuhLJ6X.css +0 -1
  69. package/bizar-dash/dist/assets/react-vendor-D3pra6w-.js +0 -46
  70. package/bizar-dash/dist/assets/react-vendor-D3pra6w-.js.map +0 -1
  71. package/bizar-dash/dist/assets/useSlashCommands-3DFLOfpk.js +0 -2
  72. package/bizar-dash/dist/assets/useSlashCommands-3DFLOfpk.js.map +0 -1
  73. package/bizar-dash/dist/assets/vendor-Di7Cp-rw.js +0 -45
  74. package/bizar-dash/dist/assets/vendor-Di7Cp-rw.js.map +0 -1
  75. package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
@@ -29,19 +29,13 @@
29
29
  }
30
30
  })();
31
31
  </script>
32
- <script type="module" crossorigin src="/assets/main-CO8fIxQK.js"></script>
33
- <link rel="modulepreload" crossorigin href="/assets/vendor-Di7Cp-rw.js">
34
- <link rel="modulepreload" crossorigin href="/assets/flow-BpU0-gK2.js">
35
- <link rel="modulepreload" crossorigin href="/assets/react-vendor-D3pra6w-.js">
36
- <link rel="modulepreload" crossorigin href="/assets/icons-B0AKnYI3.js">
37
- <link rel="modulepreload" crossorigin href="/assets/mobile-layout-DSmtNPja.js">
38
- <link rel="modulepreload" crossorigin href="/assets/Toast-WTxc3m2g.js">
39
- <link rel="modulepreload" crossorigin href="/assets/EnvVarsSection-CW6fWKds.js">
40
- <link rel="modulepreload" crossorigin href="/assets/markdown-w2bDDkAH.js">
41
- <link rel="modulepreload" crossorigin href="/assets/useSlashCommands-3DFLOfpk.js">
42
- <link rel="stylesheet" crossorigin href="/assets/mobile-layout-CJnZNLy3.css">
43
- <link rel="stylesheet" crossorigin href="/assets/react-vendor-BnuhLJ6X.css">
44
- <link rel="stylesheet" crossorigin href="/assets/main-C-Md3DaY.css">
32
+ <script type="module" crossorigin src="/assets/main-306CZfn6.js"></script>
33
+ <link rel="modulepreload" crossorigin href="/assets/mobile-layout-GZ3bE8L6.js">
34
+ <link rel="modulepreload" crossorigin href="/assets/Toast-ATSAmbdi.js">
35
+ <link rel="modulepreload" crossorigin href="/assets/EnvVarsSection-BfhAz3ax.js">
36
+ <link rel="modulepreload" crossorigin href="/assets/useSlashCommands-BOwD7lNb.js">
37
+ <link rel="stylesheet" crossorigin href="/assets/mobile-layout-BWLtwtfQ.css">
38
+ <link rel="stylesheet" crossorigin href="/assets/main-G3bNDpY3.css">
45
39
  </head>
46
40
  <body>
47
41
  <div id="root"></div>
@@ -12,14 +12,9 @@
12
12
  <link rel="preconnect" href="https://fonts.googleapis.com" />
13
13
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
14
14
  <link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
15
- <script type="module" crossorigin src="/assets/mobile-DXYCA0hC.js"></script>
16
- <link rel="modulepreload" crossorigin href="/assets/vendor-Di7Cp-rw.js">
17
- <link rel="modulepreload" crossorigin href="/assets/flow-BpU0-gK2.js">
18
- <link rel="modulepreload" crossorigin href="/assets/react-vendor-D3pra6w-.js">
19
- <link rel="modulepreload" crossorigin href="/assets/icons-B0AKnYI3.js">
20
- <link rel="modulepreload" crossorigin href="/assets/mobile-layout-DSmtNPja.js">
21
- <link rel="stylesheet" crossorigin href="/assets/react-vendor-BnuhLJ6X.css">
22
- <link rel="stylesheet" crossorigin href="/assets/mobile-layout-CJnZNLy3.css">
15
+ <script type="module" crossorigin src="/assets/mobile-C_naUN91.js"></script>
16
+ <link rel="modulepreload" crossorigin href="/assets/mobile-layout-GZ3bE8L6.js">
17
+ <link rel="stylesheet" crossorigin href="/assets/mobile-layout-BWLtwtfQ.css">
23
18
  </head>
24
19
  <body>
25
20
  <div id="root"></div>
@@ -22,7 +22,14 @@ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
22
22
  import { dirname, join } from "node:path";
23
23
 
24
24
  import { listNotes, writeNote } from "./memory-store.mjs";
25
- import { resolveVaultRoot } from "../../../packages/sdk/src/memory/index.js";
25
+ // F-041 follow-up: import the BUILT SDK memory module from `dist/`,
26
+ // not the TS source at `src/`. The src/ directory only ships the
27
+ // .ts files; the dashboard runs against the compiled .js output in
28
+ // dist/. v7.0.0 shipped with the src/ path, which makes the dashboard
29
+ // crash with `Cannot find module .../src/memory/index.js` on startup.
30
+ // Pointing at `dist/` matches what the SDK's package.json `exports`
31
+ // map already advertises.
32
+ import { resolveVaultRoot } from "../../../packages/sdk/dist/memory/index.js";
26
33
  import { runDistillation } from "./memory-distillation.mjs";
27
34
 
28
35
  /**
@@ -1,5 +1,5 @@
1
1
  // src/mobile/MobileApp.tsx — mobile root with state-based routing + stack navigation.
2
- import { useCallback, useEffect, useState } from 'react';
2
+ import { Component, useCallback, useEffect, useState, type ReactNode } from 'react';
3
3
  import { Activity, MessageSquare, CheckSquare, Settings, Grid } from 'lucide-react';
4
4
  import { api } from '../lib/api';
5
5
  import type { Snapshot, Settings as SettingsType } from '../lib/types';
@@ -20,6 +20,37 @@ import { MobileSchedules } from './views/MobileSchedules';
20
20
  import { MobileHistory } from './views/MobileHistory';
21
21
  import { MobileConfig } from './views/MobileConfig';
22
22
 
23
+ /** Tiny ErrorBoundary for mobile views — keeps a render error from
24
+ * blanking the whole shell so the user sees a "Reload" prompt with
25
+ * the chrome (topbar + back button) still intact. */
26
+ class MobileViewBoundary extends Component<{ children: ReactNode }, { error: Error | null }> {
27
+ constructor(props: { children: ReactNode }) {
28
+ super(props);
29
+ this.state = { error: null };
30
+ }
31
+ componentDidCatch(error: Error) {
32
+ this.setState({ error });
33
+ }
34
+ render() {
35
+ if (this.state.error) {
36
+ return (
37
+ <div className="mobile-view-error">
38
+ <h2>Something went wrong</h2>
39
+ <p>{this.state.error.message}</p>
40
+ <button
41
+ type="button"
42
+ className="mobile-btn mobile-btn-secondary"
43
+ onClick={() => window.location.reload()}
44
+ >
45
+ Reload
46
+ </button>
47
+ </div>
48
+ );
49
+ }
50
+ return this.props.children;
51
+ }
52
+ }
53
+
23
54
  export type MobileView =
24
55
  | { id: 'activity' }
25
56
  | { id: 'chat' }
@@ -212,7 +243,7 @@ export function MobileApp() {
212
243
  />
213
244
 
214
245
  <main className="mobile-content">
215
- {renderView()}
246
+ <MobileViewBoundary>{renderView()}</MobileViewBoundary>
216
247
  </main>
217
248
 
218
249
  {/* Bottom nav only on main tabs */}
@@ -1,6 +1,6 @@
1
1
  // src/mobile/MobileBottomNav.tsx — bottom tab bar for mobile.
2
2
  import type { LucideIcon } from 'lucide-react';
3
- import { cn } from '../lib/utils';
3
+ import { cx } from '../ui/utils/cx';
4
4
 
5
5
  export type MobileTab = {
6
6
  id: string;
@@ -23,7 +23,7 @@ export function MobileBottomNav({ tabs, activeTab, onChange }: Props) {
23
23
  <button
24
24
  key={t.id}
25
25
  type="button"
26
- className={cn('mobile-nav-btn', activeTab === t.id && 'mobile-nav-btn-active')}
26
+ className={cx('mobile-nav-btn', { 'mobile-nav-btn-active': activeTab === t.id })}
27
27
  onClick={() => onChange(t.id)}
28
28
  aria-label={t.label}
29
29
  aria-current={activeTab === t.id ? 'page' : undefined}
@@ -1,7 +1,7 @@
1
1
  // src/web/mobile/MobileEval.tsx — v5.4 mobile eval view with runs and schedules tabs.
2
2
  import { useEffect, useState } from 'react';
3
3
  import { ClipboardCheck, Clock } from 'lucide-react';
4
- import { cn } from '../lib/utils';
4
+ import { cx } from '../ui/utils/cx';
5
5
  import { api } from '../lib/api';
6
6
  import type { EvalRunSummary } from '../components/EvalRunCard';
7
7
 
@@ -54,7 +54,7 @@ export function MobileEval() {
54
54
  <div className="mobile-eval-tabs">
55
55
  <button
56
56
  type="button"
57
- className={cn('mobile-eval-tab', tab === 'runs' && 'is-active')}
57
+ className={cx('mobile-eval-tab', tab === 'runs' && 'is-active')}
58
58
  onClick={() => setTab('runs')}
59
59
  >
60
60
  <ClipboardCheck size={12} aria-hidden />
@@ -62,7 +62,7 @@ export function MobileEval() {
62
62
  </button>
63
63
  <button
64
64
  type="button"
65
- className={cn('mobile-eval-tab', tab === 'schedules' && 'is-active')}
65
+ className={cx('mobile-eval-tab', tab === 'schedules' && 'is-active')}
66
66
  onClick={() => setTab('schedules')}
67
67
  >
68
68
  <Clock size={12} aria-hidden />
@@ -91,7 +91,7 @@ export function MobileEval() {
91
91
  tabIndex={0}
92
92
  onKeyDown={(e) => e.key === 'Enter' && setSelectedRun(r)}
93
93
  >
94
- <div className={cn('mobile-eval-status', `is-${status}`)}>
94
+ <div className={cx('mobile-eval-status', `is-${status}`)}>
95
95
  {r.passed}/{r.total}
96
96
  </div>
97
97
  <div className="mobile-eval-run-info">
@@ -13,7 +13,7 @@ import {
13
13
  Clipboard,
14
14
  Scan,
15
15
  } from 'lucide-react';
16
- import { cn } from '../lib/utils';
16
+ import { cx } from '../ui/utils/cx';
17
17
  import { MemoryOverview } from '../views/memory/MemoryOverview';
18
18
  import { LightragPanel } from '../views/memory/LightragPanel';
19
19
  import { ObsidianPanel } from '../views/memory/ObsidianPanel';
@@ -89,7 +89,7 @@ export function MobileMemory({ vaultPath }: Props) {
89
89
  <button
90
90
  key={s.id}
91
91
  type="button"
92
- className={cn('mobile-memory-tab', activeSource === s.id && 'is-active')}
92
+ className={cx('mobile-memory-tab', activeSource === s.id && 'is-active')}
93
93
  onClick={() => setActiveSource(s.id)}
94
94
  aria-pressed={activeSource === s.id}
95
95
  >
@@ -4,7 +4,7 @@ import { useState } from 'react';
4
4
  import {
5
5
  Settings2, Brain, KeyRound, Cpu, RefreshCw, Globe, Info, ChevronRight,
6
6
  } from 'lucide-react';
7
- import { cn } from '../lib/utils';
7
+ import { cx } from '../ui/utils/cx';
8
8
  import type { Settings, Snapshot } from '../lib/types';
9
9
 
10
10
  // Sub-section components — each renders its section content inline.
@@ -206,7 +206,7 @@ export function MobileSettings({ settings, onSettingsChange, autoSave, snapshot
206
206
  const Icon = s.icon;
207
207
  const isExpanded = expandedSection === s.id;
208
208
  return (
209
- <div key={s.id} className={cn('mobile-settings-item', isExpanded && 'is-expanded')}>
209
+ <div key={s.id} className={cx('mobile-settings-item', isExpanded && 'is-expanded')}>
210
210
  <button
211
211
  className="mobile-settings-item-head"
212
212
  onClick={() => setExpandedSection(isExpanded ? null : s.id)}
@@ -222,7 +222,7 @@ export function MobileSettings({ settings, onSettingsChange, autoSave, snapshot
222
222
  </div>
223
223
  <ChevronRight
224
224
  size={16}
225
- className={cn('mobile-settings-item-chevron', isExpanded && 'is-rotated')}
225
+ className={cx('mobile-settings-item-chevron', isExpanded && 'is-rotated')}
226
226
  />
227
227
  </button>
228
228
  {isExpanded && (
@@ -1,7 +1,7 @@
1
1
  // src/mobile/MobileTasks.tsx — filter-tab task list with search and FAB create sheet.
2
2
  import { useState } from 'react';
3
3
  import { Search, Plus } from 'lucide-react';
4
- import { cn } from '../lib/utils';
4
+ import { cx } from '../ui/utils/cx';
5
5
  import { TaskCard } from '../components/TaskCard';
6
6
  import { TaskCreateSheet } from '../components/TaskCreateSheet';
7
7
  import type { Snapshot } from '../lib/types';
@@ -54,7 +54,7 @@ export function MobileTasks({ snapshot, refreshSnapshot, userId, workspaceId }:
54
54
  <button
55
55
  key={f}
56
56
  type="button"
57
- className={cn('mobile-tasks-tab', filter === f && 'is-active')}
57
+ className={cx('mobile-tasks-tab', filter === f && 'is-active')}
58
58
  onClick={() => setFilter(f)}
59
59
  >
60
60
  {f.charAt(0).toUpperCase() + f.slice(1)}
@@ -1,6 +1,6 @@
1
1
  // src/mobile/components/MobileListItem.tsx — reusable list row for mobile.
2
2
  import { type ComponentType } from 'react';
3
- import { cn } from '../../lib/utils';
3
+ import { cx } from '../../ui/utils/cx';
4
4
 
5
5
  type Props = {
6
6
  icon?: ComponentType<{ size?: number }>;
@@ -30,7 +30,7 @@ export function MobileListItem({
30
30
  return (
31
31
  <Tag
32
32
  {...(onClick ? { type: 'button' as const } : {})}
33
- className={cn('mobile-list-item', onClick && 'mobile-list-item-interactive', className)}
33
+ className={cx('mobile-list-item', onClick && 'mobile-list-item-interactive', className)}
34
34
  onClick={onClick}
35
35
  >
36
36
  {Icon && (
@@ -46,7 +46,7 @@ export function MobileListItem({
46
46
  {meta && <span className="mobile-list-meta">{meta}</span>}
47
47
  </div>
48
48
  {badge && (
49
- <span className={cn('mobile-list-badge', badgeVariant !== 'default' && `badge-${badgeVariant}`)}>
49
+ <span className={cx('mobile-list-badge', badgeVariant !== 'default' && `badge-${badgeVariant}`)}>
50
50
  {badge}
51
51
  </span>
52
52
  )}
@@ -243,6 +243,17 @@ code,
243
243
  color: var(--accent);
244
244
  }
245
245
 
246
+ /* Active-state pulse per DESIGN.md §9.1: a soft --accent-soft wash
247
+ behind the icon so the active tab reads at a glance without a heavy
248
+ fill. The pulse sits behind the icon, not the whole button, so the
249
+ tap target keeps its full nav-btn footprint for thumb reach. */
250
+ .mobile-nav-btn-active > svg {
251
+ background: var(--accent-soft);
252
+ border-radius: 999px;
253
+ padding: 4px;
254
+ box-sizing: content-box;
255
+ }
256
+
246
257
  .mobile-nav-label {
247
258
  font-size: 10px;
248
259
  font-weight: 500;
@@ -2129,6 +2140,36 @@ code,
2129
2140
  cursor: pointer;
2130
2141
  }
2131
2142
 
2143
+ /* ─── View error boundary ───────────────────────────────────────────── */
2144
+ /* Shown when a child view throws during render (see MobileViewBoundary
2145
+ in MobileApp.tsx). Keeps the chrome intact so the user can still tap
2146
+ the back button or reload instead of staring at a blank white screen. */
2147
+ .mobile-view-error {
2148
+ display: flex;
2149
+ flex-direction: column;
2150
+ align-items: center;
2151
+ justify-content: center;
2152
+ gap: 12px;
2153
+ padding: 32px 24px;
2154
+ text-align: center;
2155
+ min-height: 50vh;
2156
+ }
2157
+
2158
+ .mobile-view-error h2 {
2159
+ font-size: 18px;
2160
+ font-weight: 600;
2161
+ color: var(--text);
2162
+ margin: 0;
2163
+ }
2164
+
2165
+ .mobile-view-error p {
2166
+ font-size: 13px;
2167
+ color: var(--text-muted);
2168
+ margin: 0;
2169
+ max-width: 32ch;
2170
+ word-break: break-word;
2171
+ }
2172
+
2132
2173
  /* ─── Reduce motion ──────────────────────────────────────────────────── */
2133
2174
  @media (prefers-reduced-motion: reduce) {
2134
2175
  *,
@@ -135,7 +135,6 @@
135
135
  padding: var(--space-3);
136
136
  background: var(--surface-0);
137
137
  border: 1px solid var(--border-subtle);
138
- border-left-width: 3px;
139
138
  border-radius: var(--radius-md);
140
139
  cursor: pointer;
141
140
  transition: border-color var(--duration-fast) var(--ease-out),
@@ -153,17 +152,11 @@
153
152
  outline-offset: 1px;
154
153
  }
155
154
 
156
- .tasks-wave3__row--priority-high {
157
- border-left-color: var(--danger);
158
- }
159
-
160
- .tasks-wave3__row--priority-normal {
161
- border-left-color: var(--info);
162
- }
163
-
164
- .tasks-wave3__row--priority-low {
165
- border-left-color: var(--text-tertiary);
166
- }
155
+ /* Priority hint inside the row is encoded by the <Badge> + <StatusDot>
156
+ rendered in JSX (see PRIORITY_BADGE in Tasks.tsx). We deliberately
157
+ drop the previous left-edge priority stripe — DESIGN.md §14 bans
158
+ vertical accent edges on containers; the badge + dot handle the
159
+ state encoding in one canonical place. */
167
160
 
168
161
  .tasks-wave3__row-title-row {
169
162
  display: flex;
@@ -229,3 +222,32 @@
229
222
  .tasks-wave3__dot--success { background: var(--success); }
230
223
  .tasks-wave3__dot--warning { background: var(--warning); }
231
224
  .tasks-wave3__dot--danger { background: var(--danger); }
225
+
226
+ /* ── Responsive (F-041) ─────────────────────────────────────────── */
227
+
228
+ /* Below 1099px the 5-column kanban becomes a single vertical stack
229
+ with section dividers — DESIGN.md §13 lists 1099px as the "info
230
+ panel becomes sheet" breakpoint, so the kanban degrades the same
231
+ way. Each Panel becomes a section, not a column. */
232
+ @media (max-width: 1099px) {
233
+ .tasks-wave3__kanban {
234
+ grid-template-columns: 1fr !important;
235
+ gap: var(--space-4) !important;
236
+ }
237
+ .tasks-wave3__column {
238
+ /* Drop the panel chrome on the collapsed view — section header
239
+ carries the title. */
240
+ border-color: transparent;
241
+ background: transparent;
242
+ padding: 0 !important;
243
+ }
244
+ }
245
+
246
+ /* On the mobile shell (< 768px) the mobile view takes over so we
247
+ just make sure no horizontal scroll leaks through if the desktop
248
+ view is briefly shown at that width. */
249
+ @media (max-width: 767px) {
250
+ .tasks-wave3 {
251
+ overflow-x: hidden;
252
+ }
253
+ }
@@ -0,0 +1,56 @@
1
+ /*
2
+ * Textarea.tsx — multi-line text field (F-041).
3
+ *
4
+ * Sibling of TextInput. Wraps a native <textarea> so vertical resize,
5
+ * mobile keyboard avoidance, and selection behave natively. Same focus
6
+ * ring, error state, and disabled treatment as TextInput via the shared
7
+ * .field wrapper in controls.css. Use for any free-form multi-line input
8
+ * (prompts, descriptions, system prompts).
9
+ */
10
+
11
+ import {
12
+ forwardRef,
13
+ type TextareaHTMLAttributes,
14
+ type ReactNode,
15
+ } from 'react';
16
+ import { cx } from '../utils/cx';
17
+
18
+ export type TextareaSize = 'sm' | 'md' | 'lg';
19
+
20
+ export type TextareaProps = Omit<
21
+ TextareaHTMLAttributes<HTMLTextAreaElement>,
22
+ 'children'
23
+ > & {
24
+ inputSize?: TextareaSize;
25
+ error?: string | null;
26
+ hint?: ReactNode;
27
+ };
28
+
29
+ export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
30
+ function Textarea(
31
+ {
32
+ inputSize = 'md',
33
+ error,
34
+ hint,
35
+ disabled,
36
+ className,
37
+ rows = 4,
38
+ ...rest
39
+ },
40
+ ref,
41
+ ) {
42
+ return (
43
+ <span className={cx('field', 'field-textarea', `field-size-${inputSize}`, className)}>
44
+ <textarea
45
+ ref={ref}
46
+ rows={rows}
47
+ disabled={disabled}
48
+ aria-invalid={error ? true : undefined}
49
+ className={cx('field-input', 'field-input-textarea', error && 'field-error')}
50
+ {...rest}
51
+ />
52
+ {hint && <span className="field-hint">{hint}</span>}
53
+ </span>
54
+ );
55
+ },
56
+ );
@@ -243,6 +243,28 @@
243
243
  }
244
244
  .field-clear:hover { color: var(--text-primary); background-color: var(--surface-3); }
245
245
 
246
+ /* ===== Textarea (F-041) ===== */
247
+ .field-textarea {
248
+ display: flex;
249
+ flex-direction: column;
250
+ gap: var(--space-1);
251
+ }
252
+ .field-input-textarea {
253
+ /* Resets the single-line .field-input height so the textarea can grow. */
254
+ height: auto;
255
+ min-height: 80px;
256
+ resize: vertical;
257
+ font-family: var(--font-sans);
258
+ line-height: var(--leading-normal);
259
+ }
260
+ .field-size-sm .field-input-textarea { font-size: var(--text-sm); min-height: 60px; }
261
+ .field-size-md .field-input-textarea { font-size: var(--text-base); min-height: 80px; }
262
+ .field-size-lg .field-input-textarea { font-size: var(--text-md); min-height: 120px; }
263
+ .field-hint {
264
+ font-size: var(--text-xs);
265
+ color: var(--text-tertiary);
266
+ }
267
+
246
268
  /* ===== Select ===== */
247
269
  .select {
248
270
  display: inline-block;
@@ -17,6 +17,8 @@ export { Toggle } from './Toggle';
17
17
  export type { ToggleProps, ToggleSize } from './Toggle';
18
18
  export { TextInput } from './TextInput';
19
19
  export type { TextInputProps, TextInputSize } from './TextInput';
20
+ export { Textarea } from './Textarea';
21
+ export type { TextareaProps, TextareaSize } from './Textarea';
20
22
  export { NumberInput } from './NumberInput';
21
23
  export type { NumberInputProps, NumberInputSize } from './NumberInput';
22
24
  export { Select } from './Select';
@@ -0,0 +1,48 @@
1
+ /*
2
+ * ViewHeader.tsx — top-of-view title row (F-041).
3
+ *
4
+ * Distinct from PanelHeader (which lives inside a Panel). Sits above the
5
+ * view's content as the page-level header: a single row with the title on
6
+ * the left, an optional subtitle, and a right-aligned actions slot. Used
7
+ * at the top of every top-level view (Overview, Tasks, Agents, Chat,
8
+ * Settings, MobileOverview, MobileTasks, etc.) so the dashboard reads as
9
+ * one system instead of five bespoke title bars.
10
+ *
11
+ * Visual contract (see DESIGN.md §9.22):
12
+ * - Title: --fs-h3 (16px) weight 600, --text-primary
13
+ * - Subtitle: --text-sm (12px), --text-secondary
14
+ * - Actions: right-aligned via Inline; typically IconButton + Button
15
+ * - Padding: --space-3 from top, --space-4 horizontal
16
+ * - No bottom border (the first child panel provides the visual divider)
17
+ */
18
+
19
+ import type { ReactNode } from 'react';
20
+ import { cx } from '../utils/cx';
21
+
22
+ export type ViewHeaderProps = {
23
+ title?: ReactNode;
24
+ subtitle?: ReactNode;
25
+ actions?: ReactNode;
26
+ /** Optional eyebrow row above the title (e.g. breadcrumbs, view label). */
27
+ eyebrow?: ReactNode;
28
+ className?: string;
29
+ };
30
+
31
+ export function ViewHeader({
32
+ title,
33
+ subtitle,
34
+ actions,
35
+ eyebrow,
36
+ className,
37
+ }: ViewHeaderProps): React.JSX.Element {
38
+ return (
39
+ <header className={cx('bizar-view-header', className)}>
40
+ <div className="bizar-view-header__text">
41
+ {eyebrow && <div className="bizar-view-header__eyebrow">{eyebrow}</div>}
42
+ {title && <h1 className="bizar-view-header__title">{title}</h1>}
43
+ {subtitle && <p className="bizar-view-header__subtitle">{subtitle}</p>}
44
+ </div>
45
+ {actions && <div className="bizar-view-header__actions">{actions}</div>}
46
+ </header>
47
+ );
48
+ }
@@ -25,6 +25,7 @@ export {
25
25
  type PanelVariant,
26
26
  } from './Panel';
27
27
  export { PanelHeader, type PanelHeaderProps } from './PanelHeader';
28
+ export { ViewHeader, type ViewHeaderProps } from './ViewHeader';
28
29
  export { Tabs, type TabsProps, type TabDef } from './Tabs';
29
30
  export {
30
31
  Breadcrumbs,
@@ -263,6 +263,57 @@
263
263
  .bizar-panel__padding--4 { padding: var(--space-4); }
264
264
  .bizar-panel__padding--5 { padding: var(--space-5); }
265
265
 
266
+ /* ─────────────────────────────────────────────────────────────────────
267
+ * ViewHeader — top-of-view title row (F-041)
268
+ *
269
+ * Sibling of PanelHeader, distinct because it lives at the page level
270
+ * (above the first Panel) rather than inside one. One header per top-
271
+ * level view so the dashboard reads as one system.
272
+ * ──────────────────────────────────────────────────────────────────── */
273
+
274
+ .bizar-view-header {
275
+ display: flex;
276
+ align-items: flex-start;
277
+ justify-content: space-between;
278
+ gap: var(--space-4);
279
+ padding: var(--space-3) 0 var(--space-4);
280
+ flex-wrap: wrap;
281
+ }
282
+ .bizar-view-header__text {
283
+ display: flex;
284
+ flex-direction: column;
285
+ gap: var(--space-1);
286
+ min-width: 0;
287
+ flex: 1 1 auto;
288
+ }
289
+ .bizar-view-header__eyebrow {
290
+ font-size: var(--text-xs);
291
+ font-weight: var(--weight-medium);
292
+ color: var(--text-tertiary);
293
+ text-transform: uppercase;
294
+ letter-spacing: 0.04em;
295
+ }
296
+ .bizar-view-header__title {
297
+ margin: 0;
298
+ font-size: var(--text-xl);
299
+ font-weight: var(--weight-semibold);
300
+ color: var(--text-primary);
301
+ line-height: var(--leading-tight);
302
+ }
303
+ .bizar-view-header__subtitle {
304
+ margin: 0;
305
+ font-size: var(--text-sm);
306
+ color: var(--text-secondary);
307
+ line-height: var(--leading-snug);
308
+ }
309
+ .bizar-view-header__actions {
310
+ display: flex;
311
+ align-items: center;
312
+ gap: var(--space-2);
313
+ flex-wrap: wrap;
314
+ flex: 0 0 auto;
315
+ }
316
+
266
317
  /* ─────────────────────────────────────────────────────────────────────
267
318
  * Tabs — horizontal tab strip
268
319
  * ──────────────────────────────────────────────────────────────────── */
@@ -174,6 +174,19 @@
174
174
  0 8px 24px rgba(0, 0, 0, 0.40);
175
175
  --shadow-tooltip: 0 0 0 1px var(--border-default),
176
176
  0 4px 12px rgba(0, 0, 0, 0.50);
177
+
178
+ /* Agent swatch (F-041) — used as the placeholder when an agent has
179
+ no configured color; falls back to the brand accent so empty swatches
180
+ still read as "this is the brand". */
181
+ --default-agent-swatch: var(--accent);
182
+
183
+ /* Safe-area insets (F-041) — wire to env(safe-area-inset-*) so the
184
+ mobile shell clears the iOS notch + home indicator on phones and
185
+ the macOS notch on laptops without per-component media queries. */
186
+ --safe-top: env(safe-area-inset-top, 0px);
187
+ --safe-bottom: env(safe-area-inset-bottom, 0px);
188
+ --safe-left: env(safe-area-inset-left, 0px);
189
+ --safe-right: env(safe-area-inset-right, 0px);
177
190
  }
178
191
 
179
192
  :root[data-theme='light'] {
@@ -62,6 +62,14 @@ const MODELS = [
62
62
  'openrouter/minimax/minimax-m2.7',
63
63
  ];
64
64
 
65
+ // Default agent swatch — the only place in the dashboard where a
66
+ // literal hex appears. HTML <input type="color"> requires CSS Color
67
+ // Module Level 3 hex format (no `oklch()`, no `var(--token)`). Keep
68
+ // this in sync with --default-agent-swatch in ui/styles/tokens.css;
69
+ // the token is what every CSS-side swatch reads, this is just the
70
+ // native color-input seed value.
71
+ const DEFAULT_AGENT_SWATCH_HEX = '#8b5cf6';
72
+
65
73
  const CATEGORIES = [
66
74
  { id: 'reasoning', label: 'Reasoning', variant: 'accent' as const },
67
75
  { id: 'code', label: 'Code', variant: 'info' as const },
@@ -229,7 +237,7 @@ export function Agents({ snapshot, refreshSnapshot }: Props) {
229
237
  </div>
230
238
  <div className="task-form-field" style={{ flex: '0 0 80px' }}>
231
239
  <label className="field-label" htmlFor="agent-new-color">Color</label>
232
- <input id="agent-new-color" ref={(el) => (colorEl = el)} className="input" type="color" defaultValue="#8b5cf6" />
240
+ <input id="agent-new-color" ref={(el) => (colorEl = el)} className="input" type="color" defaultValue={DEFAULT_AGENT_SWATCH_HEX} />
233
241
  </div>
234
242
  </div>
235
243
  <div className="task-form-row">
@@ -362,7 +370,7 @@ export function Agents({ snapshot, refreshSnapshot }: Props) {
362
370
  </div>
363
371
  <div className="task-form-field" style={{ flex: '0 0 80px' }}>
364
372
  <label className="field-label" htmlFor="agent-edit-color">Color</label>
365
- <input id="agent-edit-color" ref={(el) => (colorEl = el)} className="input" type="color" defaultValue={full.color || '#8b5cf6'} />
373
+ <input id="agent-edit-color" ref={(el) => (colorEl = el)} className="input" type="color" defaultValue={full.color || DEFAULT_AGENT_SWATCH_HEX} />
366
374
  </div>
367
375
  </div>
368
376
  <div className="task-form-row">