anentrypoint-design 0.0.401 → 0.0.403
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.
- package/community.css +11 -6
- package/dist/247420.css +90 -11
- package/dist/247420.js +23 -23
- package/package.json +3 -2
- package/scripts/lint-css.mjs +7 -0
- package/src/community-app.js +13 -1
- package/src/components/community/chrome.js +8 -1
- package/src/components/community/shell.js +10 -1
- package/src/components/content/fields.js +10 -2
- package/src/components/content/hero.js +2 -2
- package/src/components/content/panel.js +2 -2
- package/src/components/shell/app-shell.js +21 -3
- package/src/components/theme-toggle.js +56 -18
- package/src/css/app-shell/panel-row.css +25 -3
- package/src/css/app-shell/primitives.css +22 -0
- package/src/css/app-shell/topbar.css +22 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "anentrypoint-design",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.403",
|
|
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",
|
|
@@ -116,7 +116,8 @@
|
|
|
116
116
|
"a11y": "node scripts/a11y-audit.mjs",
|
|
117
117
|
"a11y:baseline": "node scripts/a11y-audit.mjs --write-baseline",
|
|
118
118
|
"visual": "node scripts/visual-baseline.mjs check",
|
|
119
|
-
"visual:update": "node scripts/visual-baseline.mjs update"
|
|
119
|
+
"visual:update": "node scripts/visual-baseline.mjs update",
|
|
120
|
+
"lint:dead-controls": "node scripts/lint-dead-controls.mjs"
|
|
120
121
|
},
|
|
121
122
|
"repository": {
|
|
122
123
|
"type": "git",
|
package/scripts/lint-css.mjs
CHANGED
|
@@ -27,6 +27,7 @@ import { lintInlineStylesOrThrow } from './lint-inline-styles.mjs';
|
|
|
27
27
|
import { lintDuplicateSelectorsOrThrow } from './lint-duplicate-selectors.mjs';
|
|
28
28
|
import { lintSwallowCommentsOrThrow } from './lint-swallow-comments.mjs';
|
|
29
29
|
import { lintInlineCssOrThrow } from './lint-inline-css.mjs';
|
|
30
|
+
import { lintDeadControlsOrThrow } from './lint-dead-controls.mjs';
|
|
30
31
|
|
|
31
32
|
// Each entry: a human label for the report, and the check function to run.
|
|
32
33
|
// EVERY check here throws on violation and is counted in the pass/fail tally.
|
|
@@ -60,6 +61,12 @@ const CHECKS = [
|
|
|
60
61
|
['inline-styles', lintInlineStylesOrThrow],
|
|
61
62
|
['duplicate-selectors', lintDuplicateSelectorsOrThrow],
|
|
62
63
|
['swallow-comments', lintSwallowCommentsOrThrow],
|
|
64
|
+
// A rendered control that cannot act — an empty handler body, or an
|
|
65
|
+
// href="#" that goes nowhere. Both shipped in quantity (6 no-op handlers
|
|
66
|
+
// across three kits, 24 placeholder links across blog and docs) precisely
|
|
67
|
+
// because nothing checked: the affordance renders either way, so the defect
|
|
68
|
+
// is invisible until someone clicks.
|
|
69
|
+
['dead-controls', lintDeadControlsOrThrow],
|
|
63
70
|
];
|
|
64
71
|
|
|
65
72
|
// Runs every rule module's check, prints one aggregated report, and returns
|
package/src/community-app.js
CHANGED
|
@@ -202,6 +202,10 @@ export function mountCommunityApp(root, adapter = {}) {
|
|
|
202
202
|
: chatView(s);
|
|
203
203
|
const showVoiceBanner = s.voiceConnected && s.voiceChannelName && !(inVoiceChannel && s.voiceChannelName === ch.name);
|
|
204
204
|
return h('div', { class: 'ca-app' },
|
|
205
|
+
// Same skip-link contract AppShell() provides. This app builds its
|
|
206
|
+
// own chrome, so without this a keyboard user had to tab through
|
|
207
|
+
// the whole topbar nav and channel rail to reach the messages.
|
|
208
|
+
h('a', { href: '#app-main', class: 'skip-link' }, 'skip to main content'),
|
|
205
209
|
// top bar (sole app chrome above the chat-head)
|
|
206
210
|
h('header', { class: 'app-topbar' },
|
|
207
211
|
h('span', { class: 'brand' }, 'zellous', h('span', { class: 'slash' }, ' / '), h('span', {}, ch.name || 'general')),
|
|
@@ -217,7 +221,15 @@ export function mountCommunityApp(root, adapter = {}) {
|
|
|
217
221
|
Banner({ tone: 'success', visible: !!showVoiceBanner, message: showVoiceBanner ? ('In voice: ' + (s.voiceChannelName || '') + ' — click to return') : '', actionLabel: 'Leave', onAction: (e) => { if (e && e.stopPropagation) e.stopPropagation(); A.leaveVoice && A.leaveVoice(); }, onClick: () => A.returnToVoice && A.returnToVoice() }),
|
|
218
222
|
h('div', { class: 'app-body' + (s.mobileMenuOpen ? ' ca-rail-open' : '') },
|
|
219
223
|
h('aside', { class: 'app-side ca-rail' + (s.mobileMenuOpen ? ' open' : '') }, railView(s)),
|
|
220
|
-
|
|
224
|
+
// id + tabindex match AppShell()'s contract so the skip link
|
|
225
|
+
// above actually lands somewhere; this app builds its own shell
|
|
226
|
+
// and so inherited neither.
|
|
227
|
+
h('main', { class: 'app-main', id: 'app-main', tabindex: '0', onclick: () => { if (s.mobileMenuOpen && A.closeMobileMenu) A.closeMobileMenu(); } },
|
|
228
|
+
// The channel name is the page title, but it lived only in
|
|
229
|
+
// the topbar brand span, leaving the document with no
|
|
230
|
+
// heading at all. sr-only because the topbar and chat-head
|
|
231
|
+
// already show it — this adds the semantics, not a visual.
|
|
232
|
+
h('h1', { class: 'sr-only' }, ch.name || 'general'),
|
|
221
233
|
!inVoiceChannel && s.voiceConnected ? VoiceStrip({ channelName: s.voiceChannelName, status: s.voiceConnectionState || 'connected', muted: !!s.micMuted, deafened: !!s.voiceDeafened, onMute: () => A.toggleMic && A.toggleMic(), onDeafen: () => A.toggleDeafen && A.toggleDeafen(), onLeave: () => A.leaveVoice && A.leaveVoice(), open: true }) : null,
|
|
222
234
|
UserPanel({ name: (s.currentUser && (s.currentUser.displayName || s.currentUser.username || s.currentUser.name)) || 'You', tag: s.currentUser && s.currentUser.tag, color: avatarColor(s.userId), muted: !!s.micMuted, deafened: !!s.voiceDeafened, onMute: () => A.toggleMic && A.toggleMic(), onDeafen: () => A.toggleDeafen && A.toggleDeafen(), onSettings: () => A.openSettings && A.openSettings() }),
|
|
223
235
|
bodyMain,
|
|
@@ -10,7 +10,14 @@ const h = webjsx.createElement;
|
|
|
10
10
|
export function ChatHeader({ icon = '#', name, topic, toolbar = [] } = {}) {
|
|
11
11
|
return h('div', { class: 'cm-chat-header' },
|
|
12
12
|
h('span', { class: 'cm-chat-header-icon' }, icon),
|
|
13
|
-
|
|
13
|
+
// h1, not span: this is already the visible title of the content column
|
|
14
|
+
// — the channel you are reading. It carried no heading semantics, so
|
|
15
|
+
// both community kits rendered a document with zero headings. An h1
|
|
16
|
+
// here reuses the element that was always the page title rather than
|
|
17
|
+
// adding a second, hidden one. .cm-chat-header-name pins weight/size/
|
|
18
|
+
// colour and now also zeroes the UA h1 margin, so this is a semantic
|
|
19
|
+
// change with no visual change.
|
|
20
|
+
h('h1', { class: 'cm-chat-header-name' }, name),
|
|
14
21
|
topic ? h('span', { class: 'cm-chat-header-topic' }, topic) : null,
|
|
15
22
|
h('div', { class: 'cm-chat-header-toolbar' }, ...toolbar)
|
|
16
23
|
);
|
|
@@ -8,9 +8,18 @@ const h = webjsx.createElement;
|
|
|
8
8
|
|
|
9
9
|
export function CommunityShell({ serverRailProps, sidebarProps, children, memberListProps, voiceStripProps } = {}) {
|
|
10
10
|
return h('div', { class: 'cm-shell' },
|
|
11
|
+
// Same skip-link + <main id="app-main"> contract AppShell() provides.
|
|
12
|
+
// This shell is the AppShell alternative for the community layout, so
|
|
13
|
+
// the kits built on it were the only ones in the repo with no main
|
|
14
|
+
// landmark and no way for a keyboard user to get past the server rail
|
|
15
|
+
// and channel sidebar — two full columns of links before any content.
|
|
16
|
+
h('a', { href: '#app-main', class: 'skip-link' }, 'skip to main content'),
|
|
11
17
|
serverRailProps ? ServerRail(serverRailProps) : null,
|
|
12
18
|
sidebarProps ? ChannelSidebar(sidebarProps) : null,
|
|
13
|
-
|
|
19
|
+
// <main>, not <div>: this is the content column. tabindex=0 for the
|
|
20
|
+
// same reason AppShell uses it — the column scrolls, so it must be
|
|
21
|
+
// reachable by Tab for a keyboard-only user to scroll it at all.
|
|
22
|
+
h('main', { class: 'cm-main', id: 'app-main', tabindex: '0' }, ...(Array.isArray(children) ? children : [children])),
|
|
14
23
|
memberListProps ? MemberList(memberListProps) : null,
|
|
15
24
|
voiceStripProps ? VoiceStrip(voiceStripProps) : null
|
|
16
25
|
);
|
|
@@ -100,14 +100,22 @@ export function Select({ label, value = '', options = [], onChange, name, key, p
|
|
|
100
100
|
const lab = typeof o === 'string' ? o : (o.label != null ? o.label : (o.id || o.value));
|
|
101
101
|
opts.push(h('option', { key: 'o-' + id, value: id, selected: id === value }, lab));
|
|
102
102
|
}
|
|
103
|
+
// When this select is returned bare (the no-label/no-hint/md branch below),
|
|
104
|
+
// it is the node the caller keys, so it must carry the caller's key. The
|
|
105
|
+
// internal 'i' only has to be unique among THIS component's own children,
|
|
106
|
+
// which is why the wrapped branches can keep it. Previously 'i' was
|
|
107
|
+
// hardcoded here and the bare branch dropped `key` on the floor, so two
|
|
108
|
+
// sibling label-less Selects both keyed as 'i' and webjsx's keyed diff
|
|
109
|
+
// collapsed them into one on the next re-render.
|
|
110
|
+
const bare = label == null && hint == null && size === 'md';
|
|
103
111
|
const select = h('select', {
|
|
104
|
-
key: 'i', name, class: 'ds-select',
|
|
112
|
+
key: bare && key != null ? key : 'i', name, class: 'ds-select',
|
|
105
113
|
// Guarantee an accessible name even when rendered without a visible label.
|
|
106
114
|
'aria-label': ariaLabel || (label == null ? (title || placeholder || name) : null),
|
|
107
115
|
title,
|
|
108
116
|
onchange: onChange ? (e) => onChange(e.target.value, e) : null
|
|
109
117
|
}, ...opts);
|
|
110
|
-
if (
|
|
118
|
+
if (bare) return select;
|
|
111
119
|
if (label == null && hint == null) return h('label', { key, class: 'ds-field' + sizeCls }, select);
|
|
112
120
|
return h('label', { key, class: 'ds-field' + sizeCls },
|
|
113
121
|
label != null ? h('span', { key: 'l', class: 'ds-field-label' }, label) : null,
|
|
@@ -129,7 +129,7 @@ export function PageHeader({ title, lede, eyebrow, right, compact, dense, id })
|
|
|
129
129
|
// of a display H1 over a paragraph. App surfaces (files, dashboards,
|
|
130
130
|
// settings) should not spend 150px of fold on an intro.
|
|
131
131
|
if (dense) {
|
|
132
|
-
return h('section', { class: 'ds-section ds-section-compact ds-page-header-dense', id
|
|
132
|
+
return h('section', { class: 'ds-section ds-section-compact ds-page-header-dense', ...(id ? { id } : {}) },
|
|
133
133
|
h('div', { class: 'ds-page-header-dense-row' },
|
|
134
134
|
...[
|
|
135
135
|
title != null ? h('h1', { key: 'dh' }, title) : null,
|
|
@@ -137,7 +137,7 @@ export function PageHeader({ title, lede, eyebrow, right, compact, dense, id })
|
|
|
137
137
|
right != null ? h('div', { key: 'dr', class: 'ds-page-header-right' }, ...(Array.isArray(right) ? right : [right])) : null,
|
|
138
138
|
].filter(Boolean)));
|
|
139
139
|
}
|
|
140
|
-
return h('section', { class: 'ds-section' + (compact ? ' ds-section-compact' : ''), id
|
|
140
|
+
return h('section', { class: 'ds-section' + (compact ? ' ds-section-compact' : ''), ...(id ? { id } : {}) },
|
|
141
141
|
eyebrow ? h('span', { class: 'eyebrow' }, eyebrow) : null,
|
|
142
142
|
title != null ? h('h1', {}, title) : null,
|
|
143
143
|
lede != null ? h('p', { class: 'lede' }, lede) : null,
|
|
@@ -9,7 +9,7 @@ const h = webjsx.createElement;
|
|
|
9
9
|
|
|
10
10
|
export function Panel({ title, count, right, style = '', class: className = '', children, kind, id }) {
|
|
11
11
|
const cls = 'panel' + (kind ? ' panel-' + kind : '') + (className ? ' ' + className : '');
|
|
12
|
-
return h('div', { class: cls, style, id
|
|
12
|
+
return h('div', { class: cls, style, ...(id ? { id } : {}) },
|
|
13
13
|
title != null ? h('div', { class: 'panel-head' },
|
|
14
14
|
h('span', {}, title),
|
|
15
15
|
right != null ? right : (count != null ? h('span', {}, String(count)) : null)
|
|
@@ -48,7 +48,7 @@ export function PanelFromItems({ heading, items = [], keyPrefix = 'i', count, st
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
export function Section({ title, eyebrow, children, id }) {
|
|
51
|
-
return h('section', { class: 'ds-section', id
|
|
51
|
+
return h('section', { class: 'ds-section', ...(id ? { id } : {}) },
|
|
52
52
|
eyebrow ? h('span', { class: 'eyebrow' }, eyebrow) : null,
|
|
53
53
|
title ? h('h3', {}, title) : null,
|
|
54
54
|
...(Array.isArray(children) ? children : [children])
|
|
@@ -93,16 +93,34 @@ export function Side({ sections = [] } = {}) {
|
|
|
93
93
|
return h('div', { class: 'app-side-group', key: sec.group, role: 'group', 'aria-labelledby': groupId },
|
|
94
94
|
h('h2', { class: 'group', id: groupId }, sec.group),
|
|
95
95
|
...sec.items.map((item, i) => {
|
|
96
|
-
const { glyph, label, href
|
|
96
|
+
const { glyph, label, href, active, count, color, onClick } = item;
|
|
97
97
|
const countLabel = (count != null && count !== 0 && count !== '0') ? ` (${count})` : '';
|
|
98
|
+
// An item with neither href nor onClick is not a control, and
|
|
99
|
+
// must not look like one. href used to default to '#', so a
|
|
100
|
+
// forgotten destination silently produced a pointer-cursor,
|
|
101
|
+
// tab-stoppable anchor that navigated nowhere — which is why
|
|
102
|
+
// dead sidebar rows kept reappearing in new kits: the dead
|
|
103
|
+
// affordance was the DEFAULT. Such an item keeps the <a> (every
|
|
104
|
+
// .app-side row rule is anchor-scoped, so a different element
|
|
105
|
+
// would lose the whole grid layout) but drops href entirely: an
|
|
106
|
+
// anchor with no href is not a link, takes no tab stop, and is
|
|
107
|
+
// styled inert by .app-side a:not([href]) in topbar.css.
|
|
108
|
+
// An onClick with no href IS a real control and still gets
|
|
109
|
+
// href='#' so it stays keyboard-activatable.
|
|
110
|
+
const isControl = href != null || onClick != null;
|
|
111
|
+
// href is spread in only when this row is a control. Setting it
|
|
112
|
+
// to null instead would NOT omit the attribute — webjsx passes a
|
|
113
|
+
// null straight to updatePropOrAttr, which stringifies it, and
|
|
114
|
+
// the row renders href="null": still a link, still tab-stopped,
|
|
115
|
+
// still pointer-cursored. The key has to be absent.
|
|
98
116
|
return h('a', {
|
|
99
117
|
key: sec.group + i,
|
|
100
|
-
href,
|
|
118
|
+
...(isControl ? { href: href != null ? href : '#' } : {}),
|
|
101
119
|
class: active ? 'active' : '',
|
|
102
120
|
'aria-current': active ? 'page' : null,
|
|
103
121
|
'aria-label': label + countLabel,
|
|
104
122
|
onclick: onClick,
|
|
105
|
-
onkeydown: onSideLinkKeyDown
|
|
123
|
+
onkeydown: isControl ? onSideLinkKeyDown : null
|
|
106
124
|
},
|
|
107
125
|
glyph != null ? Glyph({ children: glyph, color }) : h('span', { class: 'glyph', 'aria-hidden': 'true' }),
|
|
108
126
|
h('span', {}, label),
|
|
@@ -19,52 +19,90 @@ const MODES = [
|
|
|
19
19
|
['ink', 'dark'],
|
|
20
20
|
];
|
|
21
21
|
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
22
|
+
// Both variants render from `getTheme()` at the moment their vnode is built,
|
|
23
|
+
// but nothing re-renders this control when the theme changes: a host kit's view
|
|
24
|
+
// is not subscribed to the theme, and applyTheme() only touches <html> and the
|
|
25
|
+
// theme listeners. So each instance repaints itself from the live theme through
|
|
26
|
+
// a ref — once on mount, then on every theme change. `el._dsThemeBound` makes
|
|
27
|
+
// that idempotent, since a ref callback runs again on each re-render of the
|
|
28
|
+
// host. (This replaces a `refresh` Set that nothing ever added to, so the
|
|
29
|
+
// onThemeChange subscription it fed always iterated an empty set.)
|
|
30
|
+
function bindThemePaint(el, paint) {
|
|
31
|
+
if (!el || el._dsThemeBound) return;
|
|
32
|
+
el._dsThemeBound = true;
|
|
33
|
+
paint();
|
|
34
|
+
onThemeChange(paint);
|
|
30
35
|
}
|
|
31
36
|
|
|
32
37
|
export function ThemeToggle({ compact = false, onChange } = {}) {
|
|
33
|
-
bindOnce();
|
|
34
38
|
const current = getTheme();
|
|
35
39
|
|
|
36
40
|
if (compact) {
|
|
37
41
|
// Plain words only - 'ink'/'paper' are internal theme codenames a user
|
|
38
42
|
// never chose; the resolved scheme rides in the title, not the label.
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
const
|
|
43
|
+
const wordFor = (t) => (t === 'auto' ? 'auto' : (t === 'ink' ? 'dark' : 'light'));
|
|
44
|
+
const labelFor = (t) => 'theme: ' + wordFor(t);
|
|
45
|
+
const titleFor = (t) => labelFor(t)
|
|
46
|
+
+ (t === 'auto' ? ' (currently ' + (resolvedTheme() === 'ink' ? 'dark' : 'light') + ')' : '')
|
|
47
|
+
+ ' — click to cycle';
|
|
42
48
|
return h('button', {
|
|
43
49
|
class: 'btn ds-theme-toggle',
|
|
44
50
|
type: 'button',
|
|
45
|
-
'aria-label':
|
|
46
|
-
title:
|
|
51
|
+
'aria-label': labelFor(current),
|
|
52
|
+
title: titleFor(current),
|
|
53
|
+
// getTheme() at click time, not the `current` captured when this
|
|
54
|
+
// vnode was built. Nothing re-renders this button on a theme
|
|
55
|
+
// change, so a captured value goes stale after the first click and
|
|
56
|
+
// the cycle sticks: auto -> paper, then every later click computes
|
|
57
|
+
// from a stale 'auto' and lands on paper again.
|
|
47
58
|
onclick: () => {
|
|
48
|
-
const
|
|
59
|
+
const now = getTheme();
|
|
60
|
+
const next = now === 'auto' ? 'paper' : (now === 'paper' ? 'ink' : 'auto');
|
|
49
61
|
applyTheme(next);
|
|
50
62
|
if (onChange) try { onChange(next); } catch { /* swallow: consumer onChange callback must not break the toggle */ }
|
|
51
|
-
}
|
|
63
|
+
},
|
|
64
|
+
ref: (el) => bindThemePaint(el, () => {
|
|
65
|
+
const now = getTheme();
|
|
66
|
+
el.setAttribute('aria-label', labelFor(now));
|
|
67
|
+
el.setAttribute('title', titleFor(now));
|
|
68
|
+
const lab = el.querySelector('.ds-theme-toggle-label');
|
|
69
|
+
if (lab) lab.textContent = labelFor(now);
|
|
70
|
+
})
|
|
52
71
|
},
|
|
53
72
|
// CSS-drawn half-disc so the control still reads as the theme switch
|
|
54
73
|
// when the label is hidden (icon-only rail strip).
|
|
55
74
|
h('span', { class: 'ds-theme-disc', 'aria-hidden': 'true' }),
|
|
56
|
-
h('span', { class: 'ds-theme-toggle-label' },
|
|
75
|
+
h('span', { class: 'ds-theme-toggle-label' }, labelFor(current)));
|
|
57
76
|
}
|
|
58
77
|
|
|
59
78
|
return h('div', {
|
|
60
79
|
class: 'ds-theme-toggle ds-segmented',
|
|
61
80
|
role: 'radiogroup',
|
|
62
|
-
'aria-label': 'theme'
|
|
81
|
+
'aria-label': 'theme',
|
|
82
|
+
// Paint the selected segment from the live theme rather than only from
|
|
83
|
+
// `current` (captured when this vnode was built). Nothing re-renders
|
|
84
|
+
// this control when it is clicked: the host kit's view is not
|
|
85
|
+
// subscribed to the theme, so applyTheme() correctly flipped
|
|
86
|
+
// <html data-theme> while the segment kept `is-on` on whatever was
|
|
87
|
+
// selected at mount — clicking "light" changed the theme but left
|
|
88
|
+
// "auto" looking selected, which reads as a control that did nothing.
|
|
89
|
+
// Doing it in a ref keeps this correct for BOTH triggers: a click here,
|
|
90
|
+
// and an external change (another toggle, or the OS scheme flipping
|
|
91
|
+
// while in auto) via the onThemeChange subscription.
|
|
92
|
+
ref: (el) => bindThemePaint(el, () => {
|
|
93
|
+
const now = getTheme();
|
|
94
|
+
for (const btn of el.querySelectorAll('.ds-seg-btn')) {
|
|
95
|
+
const on = btn.dataset.mode === now;
|
|
96
|
+
btn.classList.toggle('is-on', on);
|
|
97
|
+
btn.setAttribute('aria-checked', on ? 'true' : 'false');
|
|
98
|
+
}
|
|
99
|
+
})
|
|
63
100
|
}, ...MODES.map(([mode, label]) =>
|
|
64
101
|
h('button', {
|
|
65
102
|
key: mode,
|
|
66
103
|
type: 'button',
|
|
67
104
|
role: 'radio',
|
|
105
|
+
'data-mode': mode,
|
|
68
106
|
'aria-checked': current === mode ? 'true' : 'false',
|
|
69
107
|
class: 'ds-seg-btn' + (current === mode ? ' is-on' : ''),
|
|
70
108
|
onclick: () => {
|
|
@@ -155,10 +155,19 @@ a.row:hover .meta, .row[role="button"]:hover .meta { color: var(--accent-ink); t
|
|
|
155
155
|
takes the full width instead of wrapping in the narrow code gutter. */
|
|
156
156
|
.row.row-nocode { grid-template-columns: minmax(0, 1fr) auto; }
|
|
157
157
|
|
|
158
|
-
|
|
158
|
+
/* `.row-code` / `.row-title` / `.row-meta` are the BEM-ish spelling of the same
|
|
159
|
+
three cells as `.code` / `.title` / `.meta`. Both spellings are live in the
|
|
160
|
+
wild (preview/index-row.html uses the hyphenated one), and a cell that
|
|
161
|
+
matches no rule falls back to browser defaults — 16px body text in all three
|
|
162
|
+
tracks, which is precisely the undifferentiated render the specimen page was
|
|
163
|
+
shipping. Aliased rather than renamed so neither spelling can silently go
|
|
164
|
+
unstyled again. */
|
|
165
|
+
.row .code,
|
|
166
|
+
.row .row-code { font-family: var(--ff-mono); font-size: var(--fs-xs); color: var(--fg-3); font-variant-numeric: tabular-nums; letter-spacing: 0.01em; }
|
|
159
167
|
/* Title defaults to the quiet app voice (fs-sm/500) — the 18px/600 marketing
|
|
160
168
|
voice is now opt-in via .row--lede. */
|
|
161
|
-
.row .title
|
|
169
|
+
.row .title,
|
|
170
|
+
.row .row-title { font-family: var(--ff-body); font-weight: 500; font-size: var(--fs-sm); line-height: var(--lh-snug); display: flex; align-items: baseline; gap: var(--space-2-5); flex-wrap: wrap; min-width: 0; }
|
|
162
171
|
.row .title .sub { font-family: var(--ff-body); font-weight: 400; font-size: var(--fs-sm); color: var(--fg-3); }
|
|
163
172
|
/* Marketing/editorial index row — restores the roomier padding + larger title
|
|
164
173
|
the default carried before app-scale became the base. */
|
|
@@ -174,7 +183,20 @@ a.row:hover .meta, .row[role="button"]:hover .meta { color: var(--accent-ink); t
|
|
|
174
183
|
/* Context-pane cwd fact reads as a path (like .ds-dash-cwd/.ds-session-agent/
|
|
175
184
|
.ds-dash-model) -- scoped to that one row so no other Row .sub is affected. */
|
|
176
185
|
.ds-context-cwd-row .row .sub { font-family: var(--ff-mono); }
|
|
177
|
-
.row .meta
|
|
186
|
+
.row .meta,
|
|
187
|
+
.row .row-meta { font-family: var(--ff-mono); font-size: var(--fs-xs); color: var(--fg-3); text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; align-self: center; }
|
|
188
|
+
/* `.row-meta` carries free prose ("state machine · 3k · shipping"), unlike
|
|
189
|
+
`.meta`'s short tabular counts — so it wraps instead of forcing the row's
|
|
190
|
+
third track wide enough to overflow a phone viewport. */
|
|
191
|
+
.row .row-meta { white-space: normal; }
|
|
192
|
+
/* Hairline-divided index list. `.row` is deliberately hairline-FREE (the rail
|
|
193
|
+
is the affordance), but an editorial index reads as a printed table of
|
|
194
|
+
contents: the divider IS the structure, which is what
|
|
195
|
+
preview/index-row.html's caption claims. Opt-in on the container so no
|
|
196
|
+
existing rail-based list changes. */
|
|
197
|
+
.row-list > .row + .row { margin-top: 0; border-top: 1px solid var(--rule); border-radius: 0; }
|
|
198
|
+
.row-list > .row:first-child { border-radius: var(--r-1) var(--r-1) 0 0; }
|
|
199
|
+
.row-list > .row:last-child { border-radius: 0 0 var(--r-1) var(--r-1); }
|
|
178
200
|
/* WorksList meta pairs a label with a disclosure chevron, inline-aligned. */
|
|
179
201
|
.ds-works-meta { display: inline-flex; align-items: center; gap: .4em; }
|
|
180
202
|
|
|
@@ -165,6 +165,15 @@
|
|
|
165
165
|
.chip.tone-wip, .chip.tone-neutral {
|
|
166
166
|
background: var(--bg-3); color: var(--fg-2);
|
|
167
167
|
}
|
|
168
|
+
/* `.chip.accent` is the bare-adjective spelling of tone-accent (ui_kits/blog
|
|
169
|
+
uses it for a "draft" chip). It had no rule anywhere, so an accented chip
|
|
170
|
+
rendered byte-identical to a plain one. Folded into the alias grouping rather
|
|
171
|
+
than given its own block, so the two spellings cannot drift apart. --accent
|
|
172
|
+
is the FILL and --accent-ink the readable text tone (AGENTS.md) — the bare
|
|
173
|
+
lead is ~1.07:1 on paper and unreadable as text. */
|
|
174
|
+
.chip.accent, .chip.tone-accent {
|
|
175
|
+
background: var(--accent-tint); color: var(--accent-ink);
|
|
176
|
+
}
|
|
168
177
|
/* tone-dim was applied by five kits (aicat, project_page, signin, system_primer,
|
|
169
178
|
terminal) with no rule anywhere, so a "dim" chip rendered byte-identical to a
|
|
170
179
|
plain one — measured at rgb(234,230,218) on rgb(31,31,38), same opacity. It is
|
|
@@ -222,4 +231,17 @@
|
|
|
222
231
|
font-family: var(--ff-mono); font-size: var(--fs-tiny);
|
|
223
232
|
opacity: 0.8;
|
|
224
233
|
}
|
|
234
|
+
/* Glyph() (src/components/shell/atoms.js) emits `glyph glyph-{size}` and an
|
|
235
|
+
inline `font-size: var(--glyph-size-{size}, <literal>)`. The custom property
|
|
236
|
+
is the documented theming hook — it is what lets a theme retune glyph scale
|
|
237
|
+
without touching the component — but nothing declared it, so every glyph fell
|
|
238
|
+
through to the hard-coded inline literal and the hook was inert. Declaring
|
|
239
|
+
the three rungs on the scale makes the hook real; the sm/lg classes also give
|
|
240
|
+
the box a matching footprint instead of the one-size 18px square. */
|
|
241
|
+
/* Values mirror Glyph()'s own inline fallbacks exactly (11 / 13 / 16px), so
|
|
242
|
+
declaring the hook changes nothing visually — it only makes the property
|
|
243
|
+
themeable, which is what it always claimed to be. */
|
|
244
|
+
.glyph { --glyph-size-sm: var(--fs-nano); --glyph-size-base: var(--fs-tiny); --glyph-size-lg: var(--fs-body); }
|
|
245
|
+
.glyph-sm { width: 14px; height: 14px; }
|
|
246
|
+
.glyph-lg { width: 22px; height: 22px; }
|
|
225
247
|
|
|
@@ -164,6 +164,14 @@ body.canvas-host { background: transparent !important; }
|
|
|
164
164
|
padding: var(--space-5) var(--space-3);
|
|
165
165
|
font-size: var(--fs-sm);
|
|
166
166
|
}
|
|
167
|
+
/* Sidebar section wrapper emitted by Side() (src/components/shell/app-shell.js).
|
|
168
|
+
It is the `role="group"` that owns the heading + its links, so it must be the
|
|
169
|
+
thing that groups them visually too: without a rule it was a bare block, and
|
|
170
|
+
the only separation between sections came from .app-side's own flex gap —
|
|
171
|
+
which meant the heading floated equidistant between the group it labels and
|
|
172
|
+
the one above it. Stacking the group's own children tightly re-attaches the
|
|
173
|
+
heading to its links. */
|
|
174
|
+
.app-side-group { display: flex; flex-direction: column; gap: var(--space-hair); }
|
|
167
175
|
.app-side .group {
|
|
168
176
|
font-size: var(--fs-xs); font-weight: 600;
|
|
169
177
|
color: var(--fg-3); padding: 0 var(--space-3);
|
|
@@ -184,7 +192,20 @@ body.canvas-host { background: transparent !important; }
|
|
|
184
192
|
@media (pointer: coarse) {
|
|
185
193
|
.app-side a { min-height: 44px; padding: var(--space-2-75) var(--space-3); }
|
|
186
194
|
}
|
|
187
|
-
|
|
195
|
+
/* A sidebar row with no href and no handler is a static label, not a control:
|
|
196
|
+
Side() emits it as an <a> only so it keeps the grid layout above. It must not
|
|
197
|
+
claim to be pressable — no pointer, no hover highlight. (An anchor without
|
|
198
|
+
href is already skipped by the tab order and exposed as generic, not link.) */
|
|
199
|
+
.app-side a:not([href]) { cursor: default; }
|
|
200
|
+
/* An entry naming a section/tag that does not exist yet — in the sidebar tree
|
|
201
|
+
or the topbar nav. It stays visible so the intended shape of the docs is
|
|
202
|
+
legible, but it is not a link and must not read as one: dimmed, and inert
|
|
203
|
+
like any other href-less anchor. Both selectors are needed because the two
|
|
204
|
+
navs style their anchors separately. */
|
|
205
|
+
.app-side a.is-unwritten,
|
|
206
|
+
.app-topbar nav a.is-unwritten { color: var(--fg-3); cursor: default; }
|
|
207
|
+
.app-topbar nav a.is-unwritten:hover { background: none; color: var(--fg-3); }
|
|
208
|
+
.app-side a[href]:hover { background: var(--bg-2); color: var(--fg); }
|
|
188
209
|
.app-side a:focus-visible {
|
|
189
210
|
outline: var(--focus-w) solid var(--focus-color);
|
|
190
211
|
outline-offset: var(--focus-offset);
|