anentrypoint-design 0.0.389 → 0.0.391
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/chat.css +14 -3
- package/colors_and_type.css +40 -0
- package/dist/247420.css +64 -4
- package/dist/247420.js +28 -28
- package/package.json +6 -2
- package/scripts/lint-css.mjs +2 -0
- package/src/components/chat/message.js +10 -2
- package/src/components/chat/threads.js +6 -1
- package/src/components/community/navigation.js +14 -3
- package/src/components/shell/app-shell.js +16 -4
- package/src/components/shell/icons.js +1 -0
- package/src/components/shell/workspace-columns.js +25 -4
- package/src/css/app-shell/chat-polish.css +10 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "anentrypoint-design",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.391",
|
|
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",
|
|
@@ -98,7 +98,11 @@
|
|
|
98
98
|
"docs:components": "node scripts/generate-component-docs.mjs",
|
|
99
99
|
"lint:component-docs": "node scripts/generate-component-docs.mjs --check",
|
|
100
100
|
"generate:ui-kits": "node scripts/generate-ui-kit-scaffolds.mjs",
|
|
101
|
-
"generate:preview-index": "node scripts/generate-preview-index.mjs"
|
|
101
|
+
"generate:preview-index": "node scripts/generate-preview-index.mjs",
|
|
102
|
+
"a11y": "node scripts/a11y-audit.mjs",
|
|
103
|
+
"a11y:baseline": "node scripts/a11y-audit.mjs --write-baseline",
|
|
104
|
+
"visual": "node scripts/visual-baseline.mjs check",
|
|
105
|
+
"visual:update": "node scripts/visual-baseline.mjs update"
|
|
102
106
|
},
|
|
103
107
|
"repository": {
|
|
104
108
|
"type": "git",
|
package/scripts/lint-css.mjs
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
lintFontSizeOrThrow,
|
|
18
18
|
lintZIndexOrThrow,
|
|
19
19
|
lintTransitionAllOrThrow,
|
|
20
|
+
lintDarkParityOrThrow,
|
|
20
21
|
lintImportantOrThrow,
|
|
21
22
|
} from './lint-tokens.mjs';
|
|
22
23
|
import { lintGlyphsOrThrow } from './lint-glyphs.mjs';
|
|
@@ -42,6 +43,7 @@ const CHECKS = [
|
|
|
42
43
|
['radius', lintRadiusOrThrow],
|
|
43
44
|
['zindex', lintZIndexOrThrow],
|
|
44
45
|
['transition-all', lintTransitionAllOrThrow],
|
|
46
|
+
['dark-parity', lintDarkParityOrThrow],
|
|
45
47
|
['spacing', lintSpacingOrThrow],
|
|
46
48
|
['fontsize', lintFontSizeOrThrow],
|
|
47
49
|
['important', lintImportantOrThrow],
|
|
@@ -71,8 +71,16 @@ export function ChatMessage({ role, who = 'them', avatar, text, parts, time, typ
|
|
|
71
71
|
}, 'retry') : null)];
|
|
72
72
|
const reactionRow = reactions && reactions.length
|
|
73
73
|
? h('div', { class: 'chat-reactions' },
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
// A bare <span> has no role, so it can carry no accessible name —
|
|
75
|
+
// an aria-label here was silently DISCARDED and the whole reaction
|
|
76
|
+
// announced as nothing, while aria-hidden suppressed the only real
|
|
77
|
+
// text. So the visible label/count stay in the accessibility tree
|
|
78
|
+
// as content, and an .sr-only span supplies just the wording the
|
|
79
|
+
// visuals imply but do not spell out.
|
|
80
|
+
...reactions.map((r, i) => h('span', { class: 'rxn' + (r.you ? ' you' : ''), key: 'r' + i },
|
|
81
|
+
h('span', { class: 'e' }, r.emoji),
|
|
82
|
+
h('span', { class: 'n' }, String(r.count)),
|
|
83
|
+
h('span', { class: 'sr-only' }, ` ${String(r.count) === '1' ? 'reaction' : 'reactions'}${r.you ? ', you reacted' : ''}`))))
|
|
76
84
|
: null;
|
|
77
85
|
const tickNode = resolvedWho === 'you' && receipt
|
|
78
86
|
? h('span', { class: 'tick' + (receipt === 'read' ? ' read' : ''), role: 'img', 'aria-label': receipt === 'read' ? 'message read' : 'message sent' }, Icon(receipt === 'read' ? 'check-check' : 'check', { size: 14 }))
|
|
@@ -71,7 +71,12 @@ export const AICAT_FACE = ` /\\_/\\\n( o.o )\n > ^ <`;
|
|
|
71
71
|
|
|
72
72
|
export function AICatPortrait({ name = 'aicat', status = 'idle', face } = {}) {
|
|
73
73
|
return h('div', { class: 'aicat-portrait' },
|
|
74
|
-
|
|
74
|
+
// role="img" collapses the ASCII art into a single named image for a
|
|
75
|
+
// screen reader (otherwise the slashes and parens are read out
|
|
76
|
+
// character by character as text). It is also what makes the
|
|
77
|
+
// aria-label legal here: a bare <pre> has no role and so can carry no
|
|
78
|
+
// accessible name, exactly like the chat reaction spans.
|
|
79
|
+
h('pre', { class: 'aicat-face', role: 'img', 'aria-label': `${name} portrait` }, face || AICAT_FACE),
|
|
75
80
|
h('div', { class: 'aicat-meta' },
|
|
76
81
|
h('span', { class: 'name' }, name),
|
|
77
82
|
h('span', { class: 'status', 'aria-label': `status: ${status}` }, h('span', { class: 'dot ds-dot ds-dot-on', 'aria-hidden': 'true' }), ' ', status)
|
|
@@ -46,7 +46,7 @@ export function ChannelItem({ id, name, type = 'text', active, voiceActive, voic
|
|
|
46
46
|
const ICON_FOR = CHANNEL_ICON_FOR;
|
|
47
47
|
const icon = Icon(ICON_FOR[type] || 'hash', { size: 15 });
|
|
48
48
|
const handleActionClick = (a, e) => { e.stopPropagation(); a.onClick && a.onClick(id, e); };
|
|
49
|
-
return h('div', { class: 'cm-channel-item-wrap', 'data-channel-wrap': id },
|
|
49
|
+
return h('div', { class: 'cm-channel-item-wrap', 'data-channel-wrap': id, role: 'listitem' },
|
|
50
50
|
h('div', {
|
|
51
51
|
class: 'cm-channel-item' + (active ? ' active' : '') + (voiceActive ? ' voice-active' : '') + (voiceConnecting ? ' voice-connecting' : ''),
|
|
52
52
|
'data-id': id,
|
|
@@ -72,7 +72,15 @@ export function ChannelItem({ id, name, type = 'text', active, voiceActive, voic
|
|
|
72
72
|
}
|
|
73
73
|
},
|
|
74
74
|
tabindex: '0',
|
|
75
|
-
role:
|
|
75
|
+
// NOT role="option": this is channel NAVIGATION, not a select-one
|
|
76
|
+
// listbox — activating a row changes the view, and each row also
|
|
77
|
+
// contains its own action buttons, which `option` forbids (its
|
|
78
|
+
// children must be presentational). A link inside a list is the
|
|
79
|
+
// structure a screen reader should announce, and the current
|
|
80
|
+
// channel is conveyed by aria-current="page", the navigation
|
|
81
|
+
// idiom, rather than aria-selected.
|
|
82
|
+
role: 'link',
|
|
83
|
+
'aria-current': active ? 'page' : null
|
|
76
84
|
},
|
|
77
85
|
h('span', { class: 'cm-ch-icon' + (voiceActive ? ' voice-active-badge' : ''), 'data-voice-active': voiceActive ? 'true' : null }, icon),
|
|
78
86
|
voiceConnecting ? h('span', { class: 'cm-ch-spinner', title: 'Connecting…', 'aria-label': 'Connecting to voice channel…' }) : voiceActive ? h('span', { class: 'cm-ch-voice-badge', title: 'Voice active', 'aria-label': 'Voice channel active' }) : null,
|
|
@@ -108,7 +116,10 @@ export function ChannelCategory({ id, name, channels = [], collapsed, activeId,
|
|
|
108
116
|
extraButton ? h('button', { class: 'cm-cat-extra', onclick: (e) => { e.stopPropagation(); extraButton.onClick && extraButton.onClick(id, e); }, 'aria-label': extraButton.title || 'Category action' }, extraButton.icon || extraButton.label || '+') : null,
|
|
109
117
|
onAddChannel ? h('button', { class: 'cm-cat-add', onclick: (e) => { e.stopPropagation(); onAddChannel(id); }, 'aria-label': 'Add channel to ' + name }, '+') : null
|
|
110
118
|
),
|
|
111
|
-
|
|
119
|
+
// role=list + role=listitem on each wrap: the channel rows are a real
|
|
120
|
+
// list of navigation targets, so a screen reader announces position
|
|
121
|
+
// and count ("3 of 7") instead of a flat run of links.
|
|
122
|
+
collapsed ? null : h('div', { class: 'cm-cat-channels', role: 'list', 'aria-label': name + ' channels' },
|
|
112
123
|
...channels.map(c => ChannelItem({
|
|
113
124
|
...c,
|
|
114
125
|
draggable: channelDraggable,
|
|
@@ -13,7 +13,13 @@ export function Topbar({ brand = '247420', leaf = '', items = [], active = '', o
|
|
|
13
13
|
return h('header', { class: 'app-topbar', role: 'banner' },
|
|
14
14
|
Brand({ name: brand, leaf }),
|
|
15
15
|
search ? h('label', { class: 'app-search' },
|
|
16
|
-
|
|
16
|
+
// Line-icon, not the literal word "search" as a pseudo-glyph: the
|
|
17
|
+
// text stand-in inherited .app-search .icon's 0.6 opacity, which
|
|
18
|
+
// dropped --fg-3 to 3.74:1 on --bg-2 and failed AA as real text.
|
|
19
|
+
// An SVG is decorative (aria-hidden) rather than text, so the
|
|
20
|
+
// contrast rule no longer applies to it and the affordance stops
|
|
21
|
+
// depending on a colour value at all.
|
|
22
|
+
h('span', { class: 'icon', 'aria-hidden': 'true' }, Icon('search', { size: 15 })),
|
|
17
23
|
// `search` is either a plain placeholder string (renders the
|
|
18
24
|
// default uncontrolled input) or a caller-built VElement (has
|
|
19
25
|
// .type/.props — e.g. a controlled <input> wired to app state)
|
|
@@ -186,9 +192,15 @@ export function AppShell({ topbar, crumb, side, main, status, narrow } = {}) {
|
|
|
186
192
|
h('div', { class: 'app-body' + (hasSide ? '' : ' no-side') },
|
|
187
193
|
h('div', { class: 'app-side-scrim', 'aria-hidden': 'true', onclick: (e) => toggleSide(false, e.currentTarget) }),
|
|
188
194
|
h('div', { class: 'app-side-shell', id: 'app-side-shell', onclick: (e) => { if (e.target.closest('a')) toggleSide(false, e.currentTarget); } }, sideNode),
|
|
189
|
-
// tabindex
|
|
190
|
-
//
|
|
191
|
-
|
|
195
|
+
// tabindex=0 (not -1): .app-main is a scroll container
|
|
196
|
+
// (overflow:auto), so it must be reachable by Tab for a
|
|
197
|
+
// keyboard-only user to scroll it with the arrow keys at all —
|
|
198
|
+
// tabindex=-1 made it focusable only programmatically, which
|
|
199
|
+
// satisfied the skip-link but left the region unscrollable
|
|
200
|
+
// without a pointer. 0 keeps the skip-link target working AND
|
|
201
|
+
// puts the region in the tab order. <main> is a landmark, so it
|
|
202
|
+
// is already named for assistive tech without an aria-label.
|
|
203
|
+
h('main', { class: 'app-main' + (narrow ? ' narrow' : ''), id: 'app-main', tabindex: '0' }, ...(Array.isArray(main) ? main : [main]))
|
|
192
204
|
),
|
|
193
205
|
status || null
|
|
194
206
|
);
|
|
@@ -11,6 +11,7 @@ const h = webjsx.createElement;
|
|
|
11
11
|
// coherent line-icon set instead of multicolor OS emoji. 16px box, 1.6 stroke.
|
|
12
12
|
export const ICON_PATHS = {
|
|
13
13
|
lock: '<rect x="4" y="10" width="16" height="11" rx="2"/><path d="M8 10V7a4 4 0 0 1 8 0v3"/>',
|
|
14
|
+
search: '<circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/>',
|
|
14
15
|
mic: '<path d="M12 3a3 3 0 0 0-3 3v5a3 3 0 0 0 6 0V6a3 3 0 0 0-3-3z"/><path d="M5 11a7 7 0 0 0 14 0M12 18v3"/>',
|
|
15
16
|
'mic-off': '<path d="M9 9v2a3 3 0 0 0 4.5 2.6M15 11V6a3 3 0 0 0-5.9-.8"/><path d="M5 11a7 7 0 0 0 11.5 5.4M12 18v3"/><path d="m4 4 16 16"/>',
|
|
16
17
|
speaker: '<path d="M11 5 6 9H3v6h3l5 4z"/><path d="M15.5 8.5a5 5 0 0 1 0 7M18.5 5.5a9 9 0 0 1 0 13"/>',
|
|
@@ -101,16 +101,37 @@ export function WsResizer(col) {
|
|
|
101
101
|
document.body.style.cursor = 'col-resize';
|
|
102
102
|
};
|
|
103
103
|
const [lo, hi] = WS_RESIZE_CLAMP[col] || [120, 600];
|
|
104
|
-
// Seed aria-valuenow from the rendered track width so AT announces real
|
|
104
|
+
// Seed aria-valuenow from the rendered track width so AT announces real
|
|
105
|
+
// widths. Deferred a frame: the resizers are rendered as the LAST children
|
|
106
|
+
// of .ws-shell, so at ref time the .ws-<col> track is not in the DOM yet
|
|
107
|
+
// and this measured nothing — aria-valuenow was simply never set, which is
|
|
108
|
+
// what `aria-required-attr` (role=separator requires it) was reporting.
|
|
109
|
+
// The markup below now also ships a valid value up-front, so the attribute
|
|
110
|
+
// is present even if this correction never runs.
|
|
105
111
|
const seedNow = (el) => {
|
|
106
112
|
if (!el) return;
|
|
107
|
-
const
|
|
108
|
-
|
|
113
|
+
const measure = () => {
|
|
114
|
+
const shell = el.closest('.ws-shell');
|
|
115
|
+
const track = shell && shell.querySelector('.ws-' + col);
|
|
116
|
+
if (!track) return;
|
|
117
|
+
const w = Math.round(track.getBoundingClientRect().width);
|
|
118
|
+
if (!w) return;
|
|
119
|
+
el.setAttribute('aria-valuenow', String(w));
|
|
120
|
+
el.setAttribute('aria-valuetext', w + ' pixels');
|
|
121
|
+
};
|
|
122
|
+
if (typeof requestAnimationFrame === 'function') requestAnimationFrame(measure);
|
|
123
|
+
else measure();
|
|
109
124
|
};
|
|
110
125
|
return h('div', {
|
|
111
126
|
class: 'ws-resizer ws-resizer-' + col, role: 'separator', tabindex: '0',
|
|
112
127
|
'aria-orientation': 'vertical', 'aria-label': 'resize ' + col + ' column (arrow keys)',
|
|
113
|
-
|
|
128
|
+
// valuenow/valuetext seeded at the clamp MINIMUM (and corrected to the
|
|
129
|
+
// measured width by seedNow on the next frame). The previous markup
|
|
130
|
+
// omitted aria-valuenow entirely — required by role=separator when it
|
|
131
|
+
// is focusable — and hardcoded valuetext to the clamp MAXIMUM, so the
|
|
132
|
+
// announced width contradicted the actual one.
|
|
133
|
+
'aria-valuemin': String(lo), 'aria-valuemax': String(hi),
|
|
134
|
+
'aria-valuenow': String(lo), 'aria-valuetext': String(lo) + ' pixels',
|
|
114
135
|
onpointerdown: onDown, onkeydown: onKey, ref: seedNow,
|
|
115
136
|
});
|
|
116
137
|
}
|
|
@@ -501,8 +501,17 @@
|
|
|
501
501
|
display: inline-flex; align-items: center; justify-content: center;
|
|
502
502
|
background: color-mix(in oklab, var(--mascot) 22%, var(--bg-2));
|
|
503
503
|
border-radius: 50%; flex-shrink: 0;
|
|
504
|
+
/* Cancels the global `pre { overflow-x: auto }`. This is a fixed 32px
|
|
505
|
+
decorative chip, so the art overflowing its own box made it an
|
|
506
|
+
unreachable keyboard scroll container (WCAG scrollable-region-focusable)
|
|
507
|
+
rather than something a user could ever usefully scroll. */
|
|
508
|
+
overflow: hidden;
|
|
504
509
|
}
|
|
505
510
|
.aicat-meta { display: flex; flex-direction: column; font-family: var(--ff-mono); font-size: var(--fs-xs); color: var(--fg-3); }
|
|
511
|
+
/* --mascot-deep is now theme-tuned at the token layer (paper #B81F63 /
|
|
512
|
+
ink #FF5C9E), so this needs no per-theme override. The previous
|
|
513
|
+
[data-theme="dark"],[data-theme="ink"] override missed data-theme="auto"
|
|
514
|
+
under OS-dark, which is how the kits actually render — the author name
|
|
515
|
+
fell back to the paper tone at 3.13:1 on ink. */
|
|
506
516
|
.aicat-meta .name { color: var(--mascot-deep, var(--mascot)); font-weight: 600; }
|
|
507
|
-
[data-theme="dark"] .aicat-meta .name, [data-theme="ink"] .aicat-meta .name { color: var(--mascot); }
|
|
508
517
|
|