anentrypoint-design 0.0.478 → 0.0.481

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 (38) hide show
  1. package/chat.css +4 -4
  2. package/colors_and_type.css +33 -3
  3. package/community.css +22 -8
  4. package/dist/247420.css +195 -59
  5. package/dist/247420.js +24 -24
  6. package/package.json +1 -1
  7. package/src/community-app.js +4 -2
  8. package/src/components/community/navigation.js +2 -2
  9. package/src/components/community/presence.js +10 -4
  10. package/src/components/community/views.js +8 -2
  11. package/src/components/content/avatar.js +30 -0
  12. package/src/components/content.js +2 -2
  13. package/src/components/overlay-primitives/emoji-picker.js +15 -3
  14. package/src/components/shell/workspace-shell.js +1 -1
  15. package/src/components/theme-toggle.js +8 -3
  16. package/src/css/app-shell/base.css +18 -0
  17. package/src/css/app-shell/catalog-theme.css +14 -2
  18. package/src/css/app-shell/chat-polish.css +12 -0
  19. package/src/css/app-shell/collab.css +5 -0
  20. package/src/css/app-shell/data-density.css +4 -0
  21. package/src/css/app-shell/files.css +1 -1
  22. package/src/css/app-shell/hero-content.css +1 -1
  23. package/src/css/app-shell/loading-alerts.css +14 -12
  24. package/src/css/app-shell/otp-input.css +2 -2
  25. package/src/css/app-shell/plugins-config.css +3 -0
  26. package/src/css/app-shell/primitives.css +13 -4
  27. package/src/css/app-shell/responsive.css +13 -0
  28. package/src/css/app-shell/responsive2-workspace.css +4 -1
  29. package/src/css/app-shell/states-interactions.css +20 -4
  30. package/src/kits/os/app-panes.css +3 -2
  31. package/src/kits/os/icons.js +16 -0
  32. package/src/kits/os/shell-geometry.js +2 -2
  33. package/src/kits/os/shell.js +24 -3
  34. package/src/kits/os/theme.css +251 -78
  35. package/src/kits/os/wm.css +19 -6
  36. package/src/kits/spoint/host-join-lobby.css +3 -3
  37. package/src/web-components/freddie-chat.js +2 -2
  38. package/types/components.d.ts +4 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anentrypoint-design",
3
- "version": "0.0.478",
3
+ "version": "0.0.481",
4
4
  "description": "247420 design system SDK — webjsx + modified ripple-ui, single-file ESM bundle for reproducible use of the AnEntrypoint design.",
5
5
  "type": "module",
6
6
  "main": "./dist/247420.js",
@@ -15,7 +15,9 @@
15
15
  // audioQueueItems, audioQueueCurrentId, audioQueuePaused,
16
16
  // showAuthModal, settingsOpen, voiceSettingsOpen, replyTarget,
17
17
  // mobileMenuOpen, // drives the .ca-rail off-canvas drawer on narrow shells
18
- // canManage // gates the rail's "+ create channel" affordance
18
+ // canManage, // gates the rail's "+ create channel" affordance
19
+ // forumPosts, // ch.type==='forum': [{id,title,snippet,author,time,replyCount,tags?}]
20
+ // pageHtml, pageAuthor, pageUpdatedAt // ch.type==='page': PageView content + attribution
19
21
  // }
20
22
  // adapter.subscribe(cb) -> unsubscribe // cb fires when any snapshot field changes
21
23
  // adapter.actions = {
@@ -223,7 +225,7 @@ export function mountCommunityApp(root, adapter = {}) {
223
225
  const inVoiceChannel = ch.type === 'voice';
224
226
  const bodyMain = inVoiceChannel ? voiceView(s)
225
227
  : ch.type === 'forum' ? ForumView({ posts: s.forumPosts || [], onSelect: (id) => A.openThread && A.openThread(id), onNewPost: () => A.newForumPost && A.newForumPost() })
226
- : ch.type === 'page' ? PageView({ title: ch.name, html: s.pageHtml || '', isAdmin: !!s.canManage, onEdit: () => A.editPage && A.editPage() })
228
+ : ch.type === 'page' ? PageView({ title: ch.name, html: s.pageHtml || '', author: s.pageAuthor || '', updatedAt: s.pageUpdatedAt || 0, isAdmin: !!s.canManage, onEdit: () => A.editPage && A.editPage() })
227
229
  : chatView(s);
228
230
  const showVoiceBanner = s.voiceConnected && s.voiceChannelName && !(inVoiceChannel && s.voiceChannelName === ch.name);
229
231
  return h('div', { class: 'ca-app' },
@@ -4,7 +4,7 @@
4
4
 
5
5
  import * as webjsx from '../../../vendor/webjsx/index.js';
6
6
  import { Icon } from '../shell.js';
7
- import { Avatar, avatarInitial } from '../content.js';
7
+ import { Avatar, avatarInitial, avatarContrastFg } from '../content.js';
8
8
  import { UserPanel } from './presence.js';
9
9
  const h = webjsx.createElement;
10
10
 
@@ -97,7 +97,7 @@ export function ChannelItem({ id, name, type = 'text', active, voiceActive, voic
97
97
  ),
98
98
  voiceActive && participants.length ? h('div', { class: 'cm-ch-voice-users' },
99
99
  ...participants.map(p => h('div', { class: 'cm-ch-voice-user' + (p.speaking ? ' speaking' : '') },
100
- h('div', { class: 'cm-ch-voice-user-avatar', style: p.color ? `--avatar-bg:${p.color}` : null }, avatarInitial(p.identity)),
100
+ h('div', { class: 'cm-ch-voice-user-avatar', style: p.color ? (avatarContrastFg(p.color) ? `--avatar-bg:${p.color};--avatar-fg:${avatarContrastFg(p.color)}` : `--avatar-bg:${p.color}`) : null }, avatarInitial(p.identity)),
101
101
  h('span', { class: 'cm-ch-voice-user-name' }, p.identity)
102
102
  ))
103
103
  ) : null
@@ -4,13 +4,19 @@
4
4
 
5
5
  import * as webjsx from '../../../vendor/webjsx/index.js';
6
6
  import { Icon } from '../shell.js';
7
- import { avatarInitial } from '../content.js';
7
+ import { avatarInitial, avatarContrastFg } from '../content.js';
8
8
  const h = webjsx.createElement;
9
9
 
10
+ function avatarStyle(color) {
11
+ if (!color) return null;
12
+ const fg = avatarContrastFg(color);
13
+ return fg ? `--avatar-bg:${color};--avatar-fg:${fg}` : `--avatar-bg:${color}`;
14
+ }
15
+
10
16
  export function VoiceUser({ identity, speaking, color } = {}) {
11
17
  const initial = avatarInitial(identity);
12
18
  return h('div', { class: 'cm-voice-user' + (speaking ? ' speaking' : '') },
13
- h('div', { class: 'cm-voice-user-avatar', style: color ? `--avatar-bg:${color}` : null }, initial),
19
+ h('div', { class: 'cm-voice-user-avatar', style: avatarStyle(color) }, initial),
14
20
  h('span', { class: 'cm-voice-user-name' }, identity)
15
21
  );
16
22
  }
@@ -29,7 +35,7 @@ export function UserPanel({ name, tag, color, muted, deafened, onMute, onDeafen,
29
35
  }
30
36
  };
31
37
  return h('div', { class: 'cm-user-panel' },
32
- h('div', { class: 'cm-user-avatar', style: color ? `--avatar-bg:${color}` : null },
38
+ h('div', { class: 'cm-user-avatar', style: avatarStyle(color) },
33
39
  h('span', { class: 'cm-user-status-dot' }),
34
40
  initial
35
41
  ),
@@ -48,7 +54,7 @@ export function UserPanel({ name, tag, color, muted, deafened, onMute, onDeafen,
48
54
  export function MemberItem({ identity, name, color, status = 'online' } = {}) {
49
55
  const initial = avatarInitial(name || identity);
50
56
  return h('div', { class: 'cm-member-item' },
51
- h('div', { class: 'cm-member-avatar', style: color ? `--avatar-bg:${color}` : null },
57
+ h('div', { class: 'cm-member-avatar', style: avatarStyle(color) },
52
58
  h('span', { class: 'cm-member-status' + (status === 'online' ? ' online' : '') }),
53
59
  initial
54
60
  ),
@@ -111,10 +111,16 @@ export function ForumView({ posts = [], onSearch, onSort, onSelect, onNewPost, l
111
111
  );
112
112
  }
113
113
 
114
- export function PageView({ title = '', html = '', isAdmin = false, onEdit } = {}) {
114
+ export function PageView({ title = '', html = '', author = '', updatedAt = 0, isAdmin = false, onEdit } = {}) {
115
115
  return h('div', { class: 'cm-page', role: 'document' },
116
116
  h('div', { class: 'cm-page-head' },
117
- h('h1', { class: 'cm-page-title' }, title || ''),
117
+ h('div', { class: 'cm-page-head-title' },
118
+ h('h1', { class: 'cm-page-title' }, title || ''),
119
+ (author || updatedAt) ? h('div', { class: 'cm-page-meta' },
120
+ author ? h('span', { class: 'cm-page-author' }, author) : null,
121
+ updatedAt ? h('span', { class: 'cm-page-time' }, fmtRelTime(updatedAt)) : null
122
+ ) : null
123
+ ),
118
124
  isAdmin && onEdit ? h('button', { type: 'button', class: 'cm-page-edit', onclick: onEdit }, 'Edit') : null
119
125
  ),
120
126
  h('div', {
@@ -16,6 +16,36 @@ export function avatarInitial(name, count = 1) {
16
16
  return name ? String(name).trim().slice(0, count).toUpperCase() || '?' : '?';
17
17
  }
18
18
 
19
+ // avatarContrastFg — picks black or white text against an arbitrary
20
+ // per-user hashed --avatar-bg color, via WCAG relative luminance, so
21
+ // initials clear 4.5:1 regardless of which hue the hash lands on (a
22
+ // single fixed --fg-2 token can't: some hashed hues are light enough
23
+ // that dark-on-dark or light-on-light both existed in the wild).
24
+ // Returns null for an unparsable color so callers can omit the
25
+ // inline style entirely and fall back to the CSS default.
26
+ export function avatarContrastFg(color) {
27
+ if (!color) return null;
28
+ let r, g, b;
29
+ const hex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(String(color).trim());
30
+ if (hex) {
31
+ let h = hex[1];
32
+ if (h.length === 3) h = h.split('').map(c => c + c).join('');
33
+ r = parseInt(h.slice(0, 2), 16); g = parseInt(h.slice(2, 4), 16); b = parseInt(h.slice(4, 6), 16);
34
+ } else {
35
+ const rgb = /^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i.exec(String(color).trim());
36
+ if (!rgb) return null;
37
+ r = +rgb[1]; g = +rgb[2]; b = +rgb[3];
38
+ }
39
+ const lin = (c) => { const v = c / 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); };
40
+ const L = 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
41
+ // Contrast against pure black/white; pick whichever side clears more
42
+ // headroom (both landing >=4.5:1 is common, but some hues only clear
43
+ // one side, so max-headroom is the right tiebreak, not a 0.5 cutoff).
44
+ const contrastWhite = 1.05 / (L + 0.05);
45
+ const contrastBlack = (L + 0.05) / 0.05;
46
+ return contrastWhite >= contrastBlack ? '#fff' : '#000';
47
+ }
48
+
19
49
  // Avatar — the single letter-fallback/image avatar primitive. `initialsCount`
20
50
  // (default 1) controls how many leading characters of `name` become the
21
51
  // fallback letters when no `src`/`fallback` is given (community.js's
@@ -6,7 +6,7 @@
6
6
  // submodule under ./content/, and the public export surface here is unchanged
7
7
  // — no consumer import needs to move.
8
8
 
9
- import { avatarInitial, Avatar } from './content/avatar.js';
9
+ import { avatarInitial, avatarContrastFg, Avatar } from './content/avatar.js';
10
10
  import { Row, RowLink } from './content/row.js';
11
11
  import { Panel, Card, PanelFromItems, Section, Receipt, Changelog } from './content/panel.js';
12
12
  import { Hero, HeroFromPageData, Marquee, Manifesto, PageHeader } from './content/hero.js';
@@ -20,7 +20,7 @@ import { Spinner, Skeleton, Alert, FilterPills } from './content/feedback.js';
20
20
  import { HomeView, ProjectView } from './content/views.js';
21
21
 
22
22
  export {
23
- avatarInitial, Avatar,
23
+ avatarInitial, avatarContrastFg, Avatar,
24
24
  Row, RowLink,
25
25
  Panel, Card, PanelFromItems, Section, Receipt, Changelog,
26
26
  Hero, HeroFromPageData, Marquee, Manifesto, PageHeader,
@@ -44,12 +44,18 @@ const ALL_EMOJI = EMOJI_CATEGORIES.flatMap((c) => c.emoji);
44
44
  export function EmojiPicker({ open, anchorX = 0, anchorY = 0, onSelect, onClose, query = '' } = {}) {
45
45
  if (!open) return null;
46
46
  let cat = EMOJI_CATEGORIES[0].id;
47
- let rootEl = null, gridEl = null;
47
+ let rootEl = null, gridEl = null, searchEl = null;
48
+ // Internal search state, seeded from the `query` prop so a consumer that
49
+ // already knows the typed ':smile' trigger text (e.g. a composer keydown
50
+ // handler) can still pre-fill it — but typing in the picker's own input
51
+ // (added here since no consumer previously had anywhere to route text
52
+ // into `query`) is the primary path now.
53
+ let search = query || '';
48
54
  const close = () => onClose && onClose();
49
55
 
50
56
  const renderGrid = () => {
51
57
  if (!gridEl) return;
52
- const q = (query || '').trim().toLowerCase();
58
+ const q = (search || '').trim().toLowerCase();
53
59
  const cells = q
54
60
  ? ALL_EMOJI.filter(([, name]) => name.toLowerCase().includes(q))
55
61
  : (EMOJI_CATEGORIES.find(x => x.id === cat) || EMOJI_CATEGORIES[0]).emoji;
@@ -82,7 +88,13 @@ export function EmojiPicker({ open, anchorX = 0, anchorY = 0, onSelect, onClose,
82
88
  el._ovEmojiCleanup = _anchoredOverlayLifecycle(el, { anchorX, anchorY, fallbackW: 260, fallbackH: 240, close });
83
89
  },
84
90
  },
85
- (query || '').trim() ? null : h('div', { class: 'ov-emoji-tabs', role: 'tablist' },
91
+ h('input', {
92
+ type: 'search', class: 'ov-emoji-search', placeholder: 'Search emoji…',
93
+ 'aria-label': 'search emoji', value: search,
94
+ ref: (el) => { searchEl = el; },
95
+ oninput: (e) => { search = e.target.value; renderGrid(); },
96
+ }),
97
+ (search || '').trim() ? null : h('div', { class: 'ov-emoji-tabs', role: 'tablist' },
86
98
  ...EMOJI_CATEGORIES.map((c) => h('button', {
87
99
  type: 'button', class: 'ov-emoji-tab', role: 'tab',
88
100
  'aria-selected': c.id === cat ? 'true' : 'false',
@@ -173,7 +173,7 @@ export function WorkspaceRail({ brand = '247420', action, items = [], footer } =
173
173
  it.icon ? Icon(it.icon) : h('span', { class: 'ws-rail-item-glyph', 'aria-hidden': 'true' }),
174
174
  h('span', { class: 'ws-rail-item-label' }, it.label),
175
175
  (it.count != null && it.count !== 0 && it.count !== '0')
176
- ? h('span', { class: 'ws-rail-item-count', 'aria-hidden': 'true' }, String(it.count))
176
+ ? h('span', { class: 'ws-rail-item-count ds-badge ds-badge--sm', 'aria-hidden': 'true' }, String(it.count))
177
177
  : null,
178
178
  it.rail ? h('span', { class: 'ws-rail-item-flag tone-' + it.rail, 'aria-hidden': 'true' }) : null)))),
179
179
  footer ? h('div', { class: 'ws-rail-foot' }, footer) : null,
@@ -67,11 +67,16 @@ export function ThemeToggle({ compact = false, onChange } = {}) {
67
67
  el.setAttribute('title', titleFor(now));
68
68
  const lab = el.querySelector('.ds-theme-toggle-label');
69
69
  if (lab) lab.textContent = labelFor(now);
70
+ const disc = el.querySelector('.ds-theme-disc');
71
+ if (disc) disc.setAttribute('data-mode', now);
70
72
  })
71
73
  },
72
- // CSS-drawn half-disc so the control still reads as the theme switch
73
- // when the label is hidden (icon-only rail strip).
74
- h('span', { class: 'ds-theme-disc', 'aria-hidden': 'true' }),
74
+ // CSS-drawn disc so the control still reads as the theme switch when
75
+ // the label is hidden (icon-only rail strip). data-mode selects a
76
+ // per-mode fill (solid light / solid dark / half-and-half for auto)
77
+ // so the icon itself — not just the title tooltip — shows current
78
+ // state at a glance.
79
+ h('span', { class: 'ds-theme-disc', 'data-mode': current, 'aria-hidden': 'true' }),
75
80
  h('span', { class: 'ds-theme-toggle-label' }, labelFor(current)));
76
81
  }
77
82
 
@@ -75,6 +75,24 @@ body {
75
75
  scrollbar-width: thin;
76
76
  scrollbar-color: var(--bg-3) transparent;
77
77
  }
78
+ /* Firefox gets themed scrollbars via scrollbar-width/-color above; Chrome,
79
+ Safari, and Edge need the ::-webkit-scrollbar family too, or every
80
+ scrollable node in the app shell falls back to the raw OS scrollbar and
81
+ clashes with the dark UI. Same selector scope, same --bg-3 token. */
82
+ .app::-webkit-scrollbar, .ds-stage::-webkit-scrollbar,
83
+ .app *::-webkit-scrollbar, .ds-stage *::-webkit-scrollbar {
84
+ width: 8px;
85
+ height: 8px;
86
+ }
87
+ .app::-webkit-scrollbar-track, .ds-stage::-webkit-scrollbar-track,
88
+ .app *::-webkit-scrollbar-track, .ds-stage *::-webkit-scrollbar-track {
89
+ background: transparent;
90
+ }
91
+ .app::-webkit-scrollbar-thumb, .ds-stage::-webkit-scrollbar-thumb,
92
+ .app *::-webkit-scrollbar-thumb, .ds-stage *::-webkit-scrollbar-thumb {
93
+ background: var(--bg-3);
94
+ border-radius: var(--r-pill, 999px);
95
+ }
78
96
 
79
97
  h1, .t-h1 { font-family: var(--ff-display); font-size: var(--fs-h1); line-height: var(--lh-snug); letter-spacing: var(--tr-tight); font-weight: 600; margin: 0; }
80
98
  h2, .t-h2 { font-family: var(--ff-display); font-size: var(--fs-h2); line-height: var(--lh-snug); letter-spacing: var(--tr-tight); font-weight: 600; margin: 0; }
@@ -143,11 +143,23 @@
143
143
  font-family: var(--ff-mono); font-size: var(--fs-xs);
144
144
  display: inline-flex; align-items: center; gap: 6px;
145
145
  }
146
- /* CSS-drawn half-disc mark on the compact theme toggle: keeps the control
147
- legible when its text label is hidden (icon-only rail strip). */
146
+ /* CSS-drawn disc mark on the compact theme toggle: keeps the control
147
+ legible when its text label is hidden (icon-only rail strip). Per-mode
148
+ fill gives the icon itself a distinct look for auto/light/dark, so state
149
+ reads without relying on the title tooltip alone. */
148
150
  .ds-theme-disc {
149
151
  flex: 0 0 auto; width: 14px; height: 14px; border-radius: 50%;
150
152
  border: var(--bw-hair) solid var(--fg-3);
153
+ /* auto: half-and-half, matches default (no data-mode) for safety */
151
154
  background: linear-gradient(90deg, var(--fg-3) 0 50%, transparent 50% 100%);
152
155
  }
156
+ .ds-theme-disc[data-mode="auto"] {
157
+ background: linear-gradient(90deg, var(--fg-3) 0 50%, transparent 50% 100%);
158
+ }
159
+ .ds-theme-disc[data-mode="paper"] {
160
+ background: var(--bg); border-color: var(--fg-3);
161
+ }
162
+ .ds-theme-disc[data-mode="ink"] {
163
+ background: var(--fg-3); border-color: var(--fg-3);
164
+ }
153
165
 
@@ -69,6 +69,18 @@
69
69
  background: color-mix(in oklab, var(--mascot) 26%, var(--bg-2));
70
70
  color: var(--mascot-deep);
71
71
  }
72
+ /* Plain assistant turns ('them', no aicat persona) previously fell through to
73
+ the bare .chat-avatar gray disc with no role identity at all — 'you' and
74
+ 'aicat' both got a tinted background + ring, the default assistant got
75
+ nothing. Give it the same tinted-background treatment via --color-success
76
+ (the conventional assistant/bot association, distinct from the accent hue
77
+ 'you' owns and the mascot hue 'aicat' owns) plus a thin identity ring so
78
+ role is readable from the avatar alone, not just bubble position/color. */
79
+ .chat-msg.them:not(.aicat) .chat-avatar {
80
+ background: color-mix(in oklab, var(--color-success) 20%, var(--bg-3));
81
+ color: color-mix(in oklab, var(--color-success) 65%, var(--fg));
82
+ box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--color-success) 35%, transparent);
83
+ }
72
84
 
73
85
  .chat-stack {
74
86
  display: flex; flex-direction: column; gap: var(--space-1);
@@ -79,6 +79,11 @@
79
79
  border-radius: var(--r-pill);
80
80
  background: var(--bg-2);
81
81
  }
82
+ /* .ds-collab-chip-avatar (24px identity circle) and .ds-collab-chip-status
83
+ (8px status dot) below are both circular via border-radius: 50% but are
84
+ intentionally distinct: different sizes, different content (initial vs.
85
+ plain fill), different positioning role (layout box vs. absolute badge).
86
+ Not extracting a shared class -- there's nothing substantive to share. */
82
87
  .ds-collab-chip-avatar {
83
88
  position: relative;
84
89
  width: 24px;
@@ -14,6 +14,10 @@
14
14
 
15
15
  /* PhaseWalk */
16
16
  .ds-phasewalk { display: inline-flex; gap: var(--space-hair); vertical-align: middle; }
17
+ /* NOTE: a second toggle-switch-shaped implementation exists in
18
+ plugins-config.css:91 (.ds-plugins-toggle) at a different scale (40x22 vs
19
+ this 18x10) — consider consolidating into a shared .ds-switch primitive
20
+ with a size modifier. */
17
21
  .ds-phasewalk-seg {
18
22
  width: 18px; height: 10px; border-radius: var(--r-hair);
19
23
  background: var(--bg-3); position: relative;
@@ -735,7 +735,7 @@
735
735
  .ds-preview-code::-webkit-scrollbar,
736
736
  .ds-preview-text::-webkit-scrollbar,
737
737
  .ds-preview-body::-webkit-scrollbar,
738
- .ds-modal-body::-webkit-scrollbar { width: 10px; height: 10px; }
738
+ .ds-modal-body::-webkit-scrollbar { width: 8px; height: 8px; }
739
739
  .ds-preview-code::-webkit-scrollbar-thumb,
740
740
  .ds-preview-text::-webkit-scrollbar-thumb {
741
741
  background: color-mix(in oklab, var(--paper) 28%, transparent);
@@ -500,7 +500,7 @@ table tr.clickable:focus-visible td { background: var(--bg-2); }
500
500
  Form
501
501
  ============================================================ */
502
502
  .row-form {
503
- display: grid; gap: 12px;
503
+ display: grid; gap: var(--space-2-75);
504
504
  padding: var(--space-5);
505
505
  background: var(--bg-2);
506
506
  border-radius: var(--r-3);
@@ -2,15 +2,16 @@
2
2
  /* ============================================================
3
3
  Spinner — animated loading indicator
4
4
  ============================================================ */
5
+ /* Dots spinner — use for small inline/compact loading states. */
5
6
  .ds-spinner {
6
7
  display: inline-flex;
7
- gap: 4px;
8
+ gap: var(--space-1);
8
9
  align-items: center;
9
10
  height: 16px;
10
11
  }
11
12
  .ds-spinner span {
12
- width: 4px;
13
- height: 4px;
13
+ width: var(--space-1);
14
+ height: var(--space-1);
14
15
  border-radius: 50%;
15
16
  background: var(--accent);
16
17
  }
@@ -21,17 +22,17 @@
21
22
  .ds-spinner span:nth-child(3) { animation-delay: 0.36s; }
22
23
  }
23
24
 
24
- .ds-spinner-lg { height: 24px; gap: 6px; }
25
- .ds-spinner-lg span { width: 6px; height: 6px; }
25
+ .ds-spinner-lg { height: 24px; gap: var(--space-1-75); }
26
+ .ds-spinner-lg span { width: var(--space-1-75); height: var(--space-1-75); }
26
27
 
27
- .ds-spinner-sm { height: 12px; gap: 2px; }
28
- .ds-spinner-sm span { width: 2px; height: 2px; }
28
+ .ds-spinner-sm { height: 12px; gap: var(--space-hair); }
29
+ .ds-spinner-sm span { width: var(--space-hair); height: var(--space-hair); }
29
30
 
30
31
  .ds-spinner-xs { height: 8px; gap: 1px; }
31
32
  .ds-spinner-xs span { width: 1.5px; height: 1.5px; }
32
33
 
33
- .ds-spinner-xl { height: 32px; gap: 8px; }
34
- .ds-spinner-xl span { width: 8px; height: 8px; }
34
+ .ds-spinner-xl { height: 32px; gap: var(--space-2); }
35
+ .ds-spinner-xl span { width: var(--space-2); height: var(--space-2); }
35
36
 
36
37
  @keyframes ds-bounce {
37
38
  0%, 80%, 100% { transform: translateY(0); }
@@ -81,7 +82,7 @@
81
82
  flex-shrink: 0;
82
83
  color: var(--alert-tone);
83
84
  font-weight: 600;
84
- min-width: 20px;
85
+ min-width: var(--space-3-5);
85
86
  text-align: center;
86
87
  }
87
88
 
@@ -110,11 +111,12 @@
110
111
  cursor: pointer;
111
112
  color: var(--fg-3);
112
113
  padding: 0;
113
- font-size: 14px;
114
+ font-size: var(--fs-xs);
114
115
  line-height: 1;
115
- min-width: 20px;
116
+ min-width: var(--space-3-5);
116
117
  text-align: center;
117
118
  transition: color var(--dur-snap) var(--ease);
118
119
  }
119
120
  .ds-alert-dismiss:hover { color: var(--fg); }
121
+ .ds-alert-dismiss:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }
120
122
 
@@ -17,11 +17,11 @@
17
17
  .ds-otp-box:focus-visible {
18
18
  outline: none;
19
19
  border-color: var(--accent);
20
- box-shadow: 0 0 0 3px color-mix(in oklab, var(--accent) 40%, transparent);
20
+ box-shadow: var(--focus-ring-inset);
21
21
  }
22
22
  .ds-otp-box-filled { border-color: var(--rule-strong); background: var(--bg-3); }
23
23
  .ds-otp-box-error { border-color: var(--red); }
24
- .ds-otp-box-error:focus-visible { box-shadow: 0 0 0 3px color-mix(in oklab, var(--red) 40%, transparent); }
24
+ .ds-otp-box-error:focus-visible { box-shadow: var(--focus-ring-inset); }
25
25
  .ds-otp-box:disabled { opacity: 0.5; cursor: default; }
26
26
 
27
27
  @media (prefers-reduced-motion: reduce) {
@@ -87,6 +87,9 @@
87
87
  }
88
88
  .ds-plugins-version { font-size: var(--fs-micro); color: var(--fg-3); }
89
89
 
90
+ /* NOTE: a second toggle-switch implementation exists in data-density.css:18
91
+ (.ds-phasewalk-seg) at a different scale (18x10 vs this 40x22) — consider
92
+ consolidating into a shared .ds-switch primitive with a size modifier. */
90
93
  .ds-plugins-toggle {
91
94
  flex: 0 0 auto; width: 40px; height: 22px; border-radius: 11px; border: none; padding: 0;
92
95
  background: var(--rule); position: relative; cursor: pointer;
@@ -22,7 +22,7 @@
22
22
  }
23
23
  .btn-primary:hover { background: var(--fg); color: var(--green); transform: translateY(-1px); box-shadow: var(--shadow-2); }
24
24
  .btn-ghost { background: transparent; color: var(--fg); box-shadow: inset 0 0 0 2px var(--fg); }
25
- .btn-ghost:hover { background: var(--fg); color: var(--bg); transform: translateY(-1px); }
25
+ .btn-ghost:hover { background: color-mix(in oklab, var(--fg) 12%, transparent); transform: translateY(-1px); }
26
26
  .btn:active, .btn-primary:active, .btn-ghost:active { transform: translateY(1px); box-shadow: none; }
27
27
  .btn:focus-visible, .btn-primary:focus-visible, .btn-ghost:focus-visible, .ds-icon-btn:focus-visible {
28
28
  outline: var(--focus-w) solid var(--focus-color);
@@ -54,7 +54,9 @@
54
54
  border-radius: var(--r-1, 6px);
55
55
  background: transparent; color: var(--fg);
56
56
  width: 32px; height: 32px;
57
+ transition: background var(--dur-snap) var(--ease), color var(--dur-snap) var(--ease);
57
58
  }
59
+ .ds-icon-btn:hover { background: var(--bg-2); }
58
60
  .ds-icon-btn-xs { width: 22px; height: 22px; }
59
61
  .ds-icon-btn-sm { width: 26px; height: 26px; }
60
62
  .ds-icon-btn-base { width: 32px; height: 32px; }
@@ -64,7 +66,10 @@
64
66
  .ds-icon-btn-ghost:hover { background: var(--fg); color: var(--bg); }
65
67
  .ds-icon-btn-primary { background: var(--accent); color: var(--accent-fg); }
66
68
  .ds-icon-btn-primary:hover { background: var(--fg); color: var(--accent-ink); }
67
- .ds-icon-btn-danger { background: var(--warn); color: var(--on-color); }
69
+ /* --ink, not --on-color: --on-color is a fixed #fff, which measures 3.07:1 on
70
+ --warn (#FF5A52) -- below the 4.5:1 AA floor. --ink measures 6.27:1. Same
71
+ fix as .btn-primary.danger in app-shell/files.css. */
72
+ .ds-icon-btn-danger { background: var(--warn); color: var(--ink); }
68
73
  .ds-icon-btn-danger:hover { filter: brightness(0.92); }
69
74
  .ds-icon-btn:active { transform: translateY(1px); }
70
75
  .ds-icon-btn:disabled, .ds-icon-btn.is-disabled {
@@ -78,7 +83,7 @@
78
83
  border-radius: var(--r-pill);
79
84
  background: var(--bg-3); color: var(--fg-2);
80
85
  }
81
- .ds-badge.ds-badge--sm { min-width: 14px; height: 14px; padding: 0 var(--space-1); font-size: 10px; }
86
+ .ds-badge.ds-badge--sm { min-width: 14px; height: 14px; padding: 0 var(--space-1); font-size: var(--fs-pico); }
82
87
  .ds-badge.ds-badge--lg { min-width: 22px; height: 22px; padding: 0 var(--space-2); font-size: var(--fs-tiny); }
83
88
  .ds-badge.tone-green, .ds-badge.tone-live, .ds-badge.tone-success {
84
89
  background: var(--green-tint); color: var(--green-deep);
@@ -95,7 +100,7 @@
95
100
  background: var(--sun); color: var(--ink);
96
101
  }
97
102
  .ds-badge.tone-blue { background: var(--accent-tint); color: var(--accent-ink); }
98
- .ds-badge.tone-orange { background: color-mix(in oklab, var(--flame) 30%, var(--sun) 70%); color: var(--ink); }
103
+ .ds-badge.tone-orange { background: color-mix(in oklab, var(--flame) 22%, var(--bg)); color: color-mix(in oklab, var(--flame) 65%, black); }
99
104
 
100
105
  .chip {
101
106
  display: inline-flex; align-items: center; gap: var(--space-1-75);
@@ -163,6 +168,10 @@
163
168
  .ds-pill.tone-accent { background: var(--accent-tint); color: var(--accent-ink); }
164
169
  .ds-pill.tone-muted { background: transparent; color: var(--fg-3); box-shadow: inset 0 0 0 1px var(--rule); }
165
170
 
171
+ /* .glyph-* sizes (14/18/22px) are for small inline text-adjacent glyphs
172
+ (single characters/icons sitting in a text flow), a different scale and
173
+ purpose than the .ds-icon-btn-* tap-target scale (22/26/32/40/48px) below --
174
+ the -sm/-lg naming overlap is coincidental, not a shared system. */
166
175
  .glyph {
167
176
  display: inline-flex; align-items: center; justify-content: center;
168
177
  width: 18px; height: 18px;
@@ -3,6 +3,19 @@
3
3
  Responsive Design — Mobile (480px), Tablet (1024px), Desktop (1440px+)
4
4
  ============================================================ */
5
5
 
6
+ /* Breakpoint reference (CSS custom properties can't be used in @media queries,
7
+ so these are documented here for consistency — always use these exact values):
8
+ - Mobile/tablet boundary: 768px (max-width) — used for compact nav/sidebar
9
+ collapse (catalog-theme.css, sidebar-misc.css). Note: kits-appended.css
10
+ uses 760px for the same kind of collapse in a few places — a near-miss
11
+ of this value, worth aligning to 768px when touched.
12
+ - Tablet/desktop boundary: 900px (max-width) / 901px (min-width) — used for
13
+ main layout reflow (hero-content.css, responsive2-workspace.css)
14
+ - Legacy tablet band: 481px–1024px (min-width 481px / max-width 1025px) —
15
+ used in this file (responsive.css) for a specific tablet treatment,
16
+ consider migrating to the 768/900 pair
17
+ */
18
+
6
19
  /* --------------------------------------------------------------------
7
20
  Mobile Portrait Breakpoint (480px and below)
8
21
  ---------------------------------------------------------------------- */
@@ -281,7 +281,9 @@
281
281
  .ws-rail-item:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: -2px; }
282
282
  .ws-rail-item.active { background: var(--accent-tint); color: var(--fg); box-shadow: inset 2px 0 0 var(--accent); }
283
283
  .ws-rail-item-label { flex: 1; overflow: hidden; text-overflow: ellipsis; }
284
- .ws-rail-item-count { font-family: var(--ff-mono); font-size: var(--fs-tiny); color: var(--fg-3); }
284
+ /* Shape/color come from the shared .ds-badge component (see app-shell/primitives.css);
285
+ this class is now just the rail-item layout hook (spacing, collapsed-state targeting). */
286
+ .ws-rail-item-count { flex: none; margin-inline-start: var(--space-1); }
285
287
  .ws-rail-item-flag { width: 6px; height: 6px; border-radius: 50%; flex: none; }
286
288
  .ws-rail-item-flag.tone-flame { background: var(--flame); box-shadow: 0 0 0 2px color-mix(in oklab, var(--flame) 38%, transparent); }
287
289
  .ws-rail-foot { margin-top: auto; padding-top: var(--space-2); border-top: var(--bw-hair) solid var(--bg-3); }
@@ -324,6 +326,7 @@
324
326
  because the chat thread carries its own --measure gutter. */
325
327
  .ws-main { flex: 1; min-height: 0; overflow: auto; display: flex; flex-direction: column; padding: var(--space-4) var(--space-5); }
326
328
  .ws-main.ws-main--flush { padding: 0; }
329
+ /* Non-interactive scroll region (content column), not a focusable control — outline intentionally suppressed. */
327
330
  .ws-main:focus { outline: none; }
328
331
 
329
332
  /* Right context pane */
@@ -34,12 +34,26 @@ tr.clickable:focus-visible {
34
34
  .btn:disabled, .btn-primary:disabled, .btn-ghost:disabled,
35
35
  .btn.is-disabled, .btn-primary.is-disabled, .btn-ghost.is-disabled,
36
36
  button:disabled, [disabled] {
37
- opacity: 0.45;
37
+ opacity: 0.5;
38
38
  filter: grayscale(60%);
39
39
  cursor: not-allowed;
40
40
  pointer-events: none;
41
41
  }
42
42
 
43
+ /* Systemic hover fallback — button-like elements with no design-system class
44
+ (a bare <button>, [role="button"], or a generic .row/.chip usage) get a
45
+ `transition` declared above (line ~106) but nothing to transition to, so
46
+ they look inert until clicked. This is a low-specificity (0,0,1)/(0,1,1)
47
+ catch-all: any element that also carries `.btn`/`.btn-primary`/`.btn-ghost`/
48
+ `.ds-icon-btn`/etc. keeps its own component `:hover` (those selectors are
49
+ equal-or-higher specificity and, for the shared ones, declared later in
50
+ the cascade), so this never overrides a deliberate per-component hover —
51
+ it only fills the gap for elements that don't have one. */
52
+ button:not(:disabled):hover,
53
+ [role="button"]:not([aria-disabled="true"]):hover {
54
+ background-color: var(--panel-hover, var(--bg-3));
55
+ }
56
+
43
57
  /* Loading state — spinning indicator */
44
58
  @keyframes spinner-rotate {
45
59
  from { transform: rotate(0deg); }
@@ -47,10 +61,12 @@ button:disabled, [disabled] {
47
61
  }
48
62
  /* Reusable spin utility: rotates the element (e.g. a refresh Icon used as a
49
63
  busy indicator) via the shared spinner-rotate keyframe. Respects reduced
50
- motion. Use instead of a decorative rotation glyph. */
64
+ motion. Use instead of a decorative rotation glyph.
65
+ Ring spinner — use for larger blocking/full-region loading states. */
51
66
  .ds-spin { animation: spinner-rotate 0.8s linear infinite; transform-origin: center; }
52
67
  @media (prefers-reduced-motion: reduce) { .ds-spin { animation: none; } }
53
68
 
69
+ /* Ring spinner — use for larger blocking/full-region loading states. */
54
70
  .loading { position: relative; }
55
71
  .loading::after {
56
72
  content: '';
@@ -477,7 +493,7 @@ hr.rule-dotted { border-top: 2px dotted var(--rule-strong); }
477
493
  background: var(--accent-ink); border-color: var(--accent-ink);
478
494
  }
479
495
  .ds-toggle[aria-checked="true"] .ds-toggle-knob {
480
- background: var(--panel-0, #fff);
496
+ background: var(--panel-0, var(--bg));
481
497
  transform: translateX(18px);
482
498
  }
483
499
  .ds-toggle:disabled { opacity: 0.5; cursor: not-allowed; }
@@ -611,7 +627,7 @@ hr.rule-dotted { border-top: 2px dotted var(--rule-strong); }
611
627
  .ds-field input:not(.input):disabled,
612
628
  .ds-field textarea:not(.input):disabled,
613
629
  .ds-field .ds-select:disabled {
614
- opacity: .55;
630
+ opacity: 0.5;
615
631
  cursor: not-allowed;
616
632
  }
617
633
  .ds-field input:not(.input)[aria-invalid="true"],
@@ -17,6 +17,7 @@
17
17
  gap: var(--space-2);
18
18
  padding: var(--space-1) var(--space-2);
19
19
  background: var(--os-bg-2);
20
+ border-bottom: 1px solid color-mix(in oklab, var(--os-accent) 35%, transparent);
20
21
  font-family: var(--os-mono);
21
22
  font-size: 11px;
22
23
  color: var(--os-fg-2);
@@ -32,7 +33,7 @@
32
33
  }
33
34
  .terminal-app-status {
34
35
  font-family: var(--os-mono);
35
- color: var(--os-fg-3);
36
+ color: var(--os-accent);
36
37
  flex: 0 0 auto;
37
38
  overflow: hidden;
38
39
  text-overflow: ellipsis;
@@ -43,7 +44,7 @@
43
44
  min-width: 0;
44
45
  min-height: 0;
45
46
  overflow: hidden;
46
- background: var(--os-bg-0);
47
+ background: var(--os-bg-1);
47
48
  position: relative;
48
49
  }
49
50
  .terminal-app-slot > * { width: 100%; height: 100%; }