canon-ds 0.1.0
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/LICENSE +21 -0
- package/README.md +153 -0
- package/THIRD_PARTY_NOTICES.md +55 -0
- package/bin/canon.js +6 -0
- package/lib/build-manifest.mjs +54 -0
- package/lib/build.mjs +69 -0
- package/lib/cli.mjs +273 -0
- package/lib/color.js +160 -0
- package/lib/components/_shared.mjs +93 -0
- package/lib/components/accordion.mjs +136 -0
- package/lib/components/account-card.mjs +166 -0
- package/lib/components/activity-feed.mjs +176 -0
- package/lib/components/activity-gauge.mjs +98 -0
- package/lib/components/agent-presence.mjs +156 -0
- package/lib/components/alert.mjs +154 -0
- package/lib/components/app-store-button.mjs +108 -0
- package/lib/components/avatar-group.mjs +128 -0
- package/lib/components/avatar.mjs +225 -0
- package/lib/components/badge-group.mjs +55 -0
- package/lib/components/badge.mjs +174 -0
- package/lib/components/banner.mjs +116 -0
- package/lib/components/breadcrumb.mjs +170 -0
- package/lib/components/button-group.mjs +114 -0
- package/lib/components/button.mjs +197 -0
- package/lib/components/card-header.mjs +115 -0
- package/lib/components/card.mjs +106 -0
- package/lib/components/carousel.mjs +182 -0
- package/lib/components/chart-frame.mjs +101 -0
- package/lib/components/checkbox.mjs +163 -0
- package/lib/components/close-button.mjs +49 -0
- package/lib/components/code.mjs +123 -0
- package/lib/components/color-picker.mjs +122 -0
- package/lib/components/combobox.mjs +210 -0
- package/lib/components/command-palette.mjs +226 -0
- package/lib/components/content-divider.mjs +104 -0
- package/lib/components/counter.mjs +86 -0
- package/lib/components/credit-card.mjs +126 -0
- package/lib/components/date-picker.mjs +202 -0
- package/lib/components/description-list.mjs +88 -0
- package/lib/components/dialog.mjs +140 -0
- package/lib/components/divider.mjs +115 -0
- package/lib/components/drawer.mjs +150 -0
- package/lib/components/empty-state.mjs +112 -0
- package/lib/components/featured-card.mjs +124 -0
- package/lib/components/featured-icon.mjs +67 -0
- package/lib/components/field.mjs +122 -0
- package/lib/components/file-dropzone.mjs +166 -0
- package/lib/components/filter-bar.mjs +179 -0
- package/lib/components/header-navigation.mjs +94 -0
- package/lib/components/icon-button.mjs +179 -0
- package/lib/components/index.mjs +52 -0
- package/lib/components/inline-cta.mjs +139 -0
- package/lib/components/input-group.mjs +164 -0
- package/lib/components/input.mjs +197 -0
- package/lib/components/kanban.mjs +179 -0
- package/lib/components/kbd.mjs +70 -0
- package/lib/components/kicker.mjs +79 -0
- package/lib/components/link.mjs +108 -0
- package/lib/components/list.mjs +117 -0
- package/lib/components/media-frame.mjs +119 -0
- package/lib/components/menu.mjs +190 -0
- package/lib/components/message.mjs +249 -0
- package/lib/components/metric-group.mjs +210 -0
- package/lib/components/mobile-header.mjs +173 -0
- package/lib/components/multi-select.mjs +218 -0
- package/lib/components/notification.mjs +175 -0
- package/lib/components/number-input.mjs +149 -0
- package/lib/components/page-header.mjs +68 -0
- package/lib/components/pagination.mjs +236 -0
- package/lib/components/pin-input.mjs +120 -0
- package/lib/components/popover.mjs +126 -0
- package/lib/components/progress-circle.mjs +115 -0
- package/lib/components/progress.mjs +93 -0
- package/lib/components/prose.mjs +96 -0
- package/lib/components/radio.mjs +160 -0
- package/lib/components/rating.mjs +93 -0
- package/lib/components/rich-text-editor.mjs +178 -0
- package/lib/components/section-header.mjs +62 -0
- package/lib/components/segmented-control.mjs +121 -0
- package/lib/components/select.mjs +163 -0
- package/lib/components/sidebar-nav.mjs +172 -0
- package/lib/components/sidebar.mjs +124 -0
- package/lib/components/skeleton.mjs +66 -0
- package/lib/components/slider.mjs +122 -0
- package/lib/components/social-button.mjs +119 -0
- package/lib/components/spinner.mjs +69 -0
- package/lib/components/stat.mjs +120 -0
- package/lib/components/stepper.mjs +228 -0
- package/lib/components/switch.mjs +159 -0
- package/lib/components/table.mjs +212 -0
- package/lib/components/tabs.mjs +207 -0
- package/lib/components/tag.mjs +196 -0
- package/lib/components/tags-input.mjs +146 -0
- package/lib/components/textarea.mjs +117 -0
- package/lib/components/timeline.mjs +135 -0
- package/lib/components/toast.mjs +115 -0
- package/lib/components/tooltip.mjs +86 -0
- package/lib/components/topbar.mjs +207 -0
- package/lib/components/tree-view.mjs +156 -0
- package/lib/components/video-player.mjs +189 -0
- package/lib/connect.mjs +110 -0
- package/lib/design-files.mjs +175 -0
- package/lib/distribution.mjs +109 -0
- package/lib/editor.js +940 -0
- package/lib/engine.js +339 -0
- package/lib/generators/agents.mjs +68 -0
- package/lib/generators/connection.md +58 -0
- package/lib/generators/css.mjs +2 -0
- package/lib/generators/designmd.mjs +324 -0
- package/lib/generators/documentation.css +82 -0
- package/lib/generators/documentation.html +81 -0
- package/lib/generators/documentation.js +96 -0
- package/lib/generators/documentation.mjs +35 -0
- package/lib/generators/dtcg.mjs +60 -0
- package/lib/generators/preview.css +279 -0
- package/lib/generators/preview.mjs +225 -0
- package/lib/generators/react.mjs +162 -0
- package/lib/generators/tailwind.mjs +114 -0
- package/lib/install.mjs +108 -0
- package/lib/lint.mjs +538 -0
- package/lib/mcp.mjs +268 -0
- package/lib/open.mjs +7 -0
- package/lib/patterns/_app.mjs +56 -0
- package/lib/patterns/app-shell.mjs +25 -0
- package/lib/patterns/audit-log.mjs +37 -0
- package/lib/patterns/auth-pages.mjs +44 -0
- package/lib/patterns/billing-page.mjs +39 -0
- package/lib/patterns/calendar-page.mjs +55 -0
- package/lib/patterns/dashboard-page.mjs +28 -0
- package/lib/patterns/files-page.mjs +30 -0
- package/lib/patterns/first-run.mjs +31 -0
- package/lib/patterns/form-layout.mjs +24 -0
- package/lib/patterns/inbox-page.mjs +58 -0
- package/lib/patterns/index.mjs +29 -0
- package/lib/patterns/list-detail-page.mjs +24 -0
- package/lib/patterns/mobile-app.mjs +46 -0
- package/lib/patterns/modal-flows.mjs +32 -0
- package/lib/patterns/onboarding-page.mjs +42 -0
- package/lib/patterns/permissions-page.mjs +38 -0
- package/lib/patterns/profile-page.mjs +32 -0
- package/lib/patterns/settings-page.mjs +24 -0
- package/lib/project.mjs +28 -0
- package/lib/serve.mjs +279 -0
- package/lib/system.mjs +210 -0
- package/lib/tokens/base.js +251 -0
- package/lib/tokens/canon-preset.mjs +67 -0
- package/lib/tokens/index.mjs +1 -0
- package/lib/tokens/legacy-canon.json +559 -0
- package/lib/tokens/presets.mjs +161 -0
- package/lib/tokens/resolve.mjs +1 -0
- package/lib/types.mjs +4 -0
- package/lib/version.mjs +2 -0
- package/package.json +54 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { FOCUS_RING, TRANSITION_COLORS, ex, typeStyle } from "./_shared.mjs";
|
|
2
|
+
// Activity feed: a column of "who did what to which object, when" rows joined by a
|
|
3
|
+
// 2px connector under 32px avatars, with optional quoted comments, file attachments
|
|
4
|
+
// and a comment composer at the end. The compact density is the notification-feed
|
|
5
|
+
// version (24px avatars, one line per event).
|
|
6
|
+
const HAIRLINE = '{border.width.thin} solid {color.border-default}';
|
|
7
|
+
const FILE = '<svg class="cn-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4.5 2.5h4.5l3 3v8a.5.5 0 01-.5.5h-7a.5.5 0 01-.5-.5v-11z"/><path d="M9 2.5v3h3"/></svg>';
|
|
8
|
+
const avatar = (initials, name) => `<span class="cn-activity-feed__avatar" role="img" aria-label="${name}">${initials}</span>`;
|
|
9
|
+
const comment = (text) => `<div class="cn-activity-feed__comment">${text}</div>`;
|
|
10
|
+
const attachment = (name, size) => `<a href="#" class="cn-activity-feed__attachment"><span class="cn-activity-feed__attachment-icon" aria-hidden="true">${FILE}</span><span class="cn-activity-feed__attachment-name">${name}</span><span class="cn-activity-feed__attachment-size">${size}</span></a>`;
|
|
11
|
+
const item = (o) => `<article class="cn-activity-feed__item">${avatar(o.initials, o.actor)}<span class="cn-activity-feed__line" aria-hidden="true"></span><div class="cn-activity-feed__content"><div class="cn-activity-feed__header"><span class="cn-activity-feed__actor">${o.actor}</span><span class="cn-activity-feed__action">${o.action}</span>${o.target ? `<span class="cn-activity-feed__target">${o.target}</span>` : ''}<time class="cn-activity-feed__time" datetime="${o.datetime}">${o.time}</time></div>${o.extra ?? ''}</div></article>`;
|
|
12
|
+
const composer = `<div class="cn-activity-feed__composer">${avatar('LF', 'Lucas Ferreira')}<div class="cn-activity-feed__content"><div class="cn-textarea" data-size="sm" data-resize="none"><textarea class="cn-textarea__field" rows="2" placeholder="Leave a comment…" aria-label="Comment"></textarea></div><div class="cn-activity-feed__composer-actions"><button type="button" class="cn-button" data-variant="primary" data-size="sm"><span class="cn-button__label">Comment</span></button></div></div></div>`;
|
|
13
|
+
const feed = (variant, inner, width = '560px') => `<div class="cn-activity-feed" data-variant="${variant}" role="feed" aria-label="Activity" aria-busy="false" style="width:100%;max-width:${width}">${inner}</div>`;
|
|
14
|
+
export const activityFeed = {
|
|
15
|
+
name: 'ActivityFeed',
|
|
16
|
+
slug: 'activity-feed',
|
|
17
|
+
category: 'data-display',
|
|
18
|
+
description: 'A vertical feed of events on one object or workspace: a 32px avatar, "actor did action to target" in one line with a subtle timestamp, joined by a 2px connector. Events can carry a quoted comment, a file attachment, and the feed can end with a comment composer.',
|
|
19
|
+
usage: 'Use on a record page (a deal, a project, a document) or a workspace home to show what people and agents did, newest first or oldest first per screen. For a process with known steps use Timeline; for chat use Message; for system notifications a person can act on use Notification.',
|
|
20
|
+
anatomy: [
|
|
21
|
+
{ part: 'root', element: 'div', description: 'The feed column (role="feed"). Flex column; width comes from the layout.' },
|
|
22
|
+
{ part: 'item', element: 'article', description: 'One event: avatar on the left, content on the right, relative so the connector can hang under the avatar. 12px vertical padding.' },
|
|
23
|
+
{ part: 'avatar', element: 'span', description: '32px circle with initials (24px in compact). role="img" + aria-label with the actor name, or a real Avatar image.' },
|
|
24
|
+
{ part: 'line', element: 'span', description: 'The 2px connector from this avatar to the next one, border-default. aria-hidden; hidden automatically on the last item.' },
|
|
25
|
+
{ part: 'content', element: 'div', description: 'Column next to the avatar: header line first, then optional comment, attachment or composer fields.' },
|
|
26
|
+
{ part: 'header', element: 'div', description: 'The one-line sentence: actor, action, target, time. Wraps on narrow widths.' },
|
|
27
|
+
{ part: 'actor', element: 'span', description: 'Who did it, label-sm semibold in fg-default ("Maya Chen").' },
|
|
28
|
+
{ part: 'action', element: 'span', description: 'The verb phrase in body-sm muted ("commented on", "uploaded").' },
|
|
29
|
+
{ part: 'target', element: 'span', description: 'The object acted on, label-sm in fg-default ("Q3 launch plan"). Optional when the action is self-contained.', optional: true },
|
|
30
|
+
{ part: 'time', element: 'time', description: 'When, body-xs in fg-subtle, after the sentence. Always a <time datetime>.' },
|
|
31
|
+
{ part: 'comment', element: 'div', description: 'Quoted comment text in a bg-subtle card (radius lg, 12px padding, body-sm).', optional: true },
|
|
32
|
+
{ part: 'attachment', element: 'a', description: 'A file row: icon, name and size in a hairline card. A link to the file.', optional: true },
|
|
33
|
+
{ part: 'attachment-icon', element: 'span', description: '32px bg-subtle square holding a 16px file icon.', optional: true },
|
|
34
|
+
{ part: 'attachment-name', element: 'span', description: 'File name, label-sm, truncated.', optional: true },
|
|
35
|
+
{ part: 'attachment-size', element: 'span', description: 'Size or type, body-xs fg-subtle ("2.4 MB").', optional: true },
|
|
36
|
+
{ part: 'composer', element: 'div', description: 'The comment box at the end of the feed: the current user\'s avatar next to a Textarea and a Comment button.', optional: true },
|
|
37
|
+
{ part: 'composer-actions', element: 'div', description: 'Right-aligned row under the textarea holding the primary sm "Comment" button.', optional: true },
|
|
38
|
+
],
|
|
39
|
+
props: {
|
|
40
|
+
variant: {
|
|
41
|
+
values: ['simple', 'with-comments', 'compact', 'with-composer'],
|
|
42
|
+
default: 'simple',
|
|
43
|
+
description: 'simple = one sentence per event with a 32px avatar (record pages). with-comments = the same rows plus quoted comment cards and file attachments (discussion on a document). compact = 24px avatars, tighter rows, for a notification-style feed in a side panel or popover. with-composer = the feed ends with a Textarea + Comment button so people can reply in place.',
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
states: {
|
|
47
|
+
busy: { selector: '[aria-busy="true"]', description: 'The feed is loading more events: content dims slightly. Set while fetching, clear when done.', markup: 'aria-busy="true" on the root' },
|
|
48
|
+
attachmentHover: { selector: ' .cn-activity-feed__attachment:hover', description: 'Pointer over a file row (lives on the attachment, styled in extraCss): subtle fill and a stronger border.', markup: 'native :hover on .cn-activity-feed__attachment' },
|
|
49
|
+
attachmentFocus: { selector: ' .cn-activity-feed__attachment:focus-visible', description: 'Keyboard focus on a file row shows the focus ring (extraCss).', markup: 'native :focus-visible on .cn-activity-feed__attachment' },
|
|
50
|
+
},
|
|
51
|
+
base: {
|
|
52
|
+
root: { display: 'flex', 'flex-direction': 'column', width: '100%', 'min-width': '0', color: '{color.fg-default}' },
|
|
53
|
+
item: { position: 'relative', display: 'flex', 'align-items': 'flex-start', gap: '{space.3}', 'padding-block': '{space.3}', 'min-width': '0' },
|
|
54
|
+
avatar: {
|
|
55
|
+
position: 'relative',
|
|
56
|
+
'z-index': '{z.raised}',
|
|
57
|
+
display: 'inline-flex',
|
|
58
|
+
'align-items': 'center',
|
|
59
|
+
'justify-content': 'center',
|
|
60
|
+
'flex-shrink': '0',
|
|
61
|
+
width: '{space.8}',
|
|
62
|
+
height: '{space.8}',
|
|
63
|
+
'border-radius': '{radius.full}',
|
|
64
|
+
'background-color': '{color.bg-subtle}',
|
|
65
|
+
color: '{color.fg-muted}',
|
|
66
|
+
...typeStyle('label-xs'),
|
|
67
|
+
'letter-spacing': '{font.letterSpacing.wide}',
|
|
68
|
+
'text-transform': 'uppercase',
|
|
69
|
+
'user-select': 'none',
|
|
70
|
+
overflow: 'hidden',
|
|
71
|
+
},
|
|
72
|
+
line: {
|
|
73
|
+
position: 'absolute',
|
|
74
|
+
'inset-inline-start': 'calc({space.4} - {space.px})',
|
|
75
|
+
top: 'calc({space.3} + {space.8} + {space.1})',
|
|
76
|
+
bottom: 'calc(-1 * {space.2})',
|
|
77
|
+
width: '{border.width.medium}',
|
|
78
|
+
'border-radius': '{radius.full}',
|
|
79
|
+
'background-color': '{color.border-default}',
|
|
80
|
+
},
|
|
81
|
+
content: { flex: '1 1 auto', 'min-width': '0', display: 'flex', 'flex-direction': 'column', gap: '{space.2}', 'padding-top': '{space.1.5}' },
|
|
82
|
+
header: { display: 'flex', 'flex-wrap': 'wrap', 'align-items': 'baseline', 'column-gap': '{space.1}', 'row-gap': '0', ...typeStyle('body-sm'), color: '{color.fg-muted}' },
|
|
83
|
+
actor: { ...typeStyle('label-sm'), 'font-weight': '{font.weight.semibold}', 'line-height': '{font.lineHeight.normal}', color: '{color.fg-default}' },
|
|
84
|
+
action: { color: '{color.fg-muted}' },
|
|
85
|
+
target: { ...typeStyle('label-sm'), 'line-height': '{font.lineHeight.normal}', color: '{color.fg-default}' },
|
|
86
|
+
time: { ...typeStyle('body-xs'), color: '{color.fg-subtle}', 'margin-inline-start': '{space.1}', 'white-space': 'nowrap' },
|
|
87
|
+
comment: { 'background-color': '{color.bg-subtle}', 'border-radius': '{radius.lg}', padding: '{space.3}', ...typeStyle('body-sm'), color: '{color.fg-default}', 'overflow-wrap': 'anywhere' },
|
|
88
|
+
attachment: {
|
|
89
|
+
display: 'inline-flex',
|
|
90
|
+
'align-items': 'center',
|
|
91
|
+
gap: '{space.3}',
|
|
92
|
+
'align-self': 'flex-start',
|
|
93
|
+
'max-width': '100%',
|
|
94
|
+
padding: '{space.2.5} {space.3}',
|
|
95
|
+
'background-color': '{color.bg-surface}',
|
|
96
|
+
border: HAIRLINE,
|
|
97
|
+
'border-radius': '{radius.lg}',
|
|
98
|
+
color: '{color.fg-default}',
|
|
99
|
+
'text-decoration': 'none',
|
|
100
|
+
...TRANSITION_COLORS,
|
|
101
|
+
},
|
|
102
|
+
'attachment-icon': { display: 'inline-flex', 'align-items': 'center', 'justify-content': 'center', 'flex-shrink': '0', width: '{space.8}', height: '{space.8}', 'border-radius': '{radius.md}', 'background-color': '{color.bg-subtle}', color: '{color.fg-muted}' },
|
|
103
|
+
'attachment-name': { ...typeStyle('label-sm'), 'line-height': '{font.lineHeight.normal}', color: '{color.fg-default}', 'min-width': '0', overflow: 'hidden', 'text-overflow': 'ellipsis', 'white-space': 'nowrap' },
|
|
104
|
+
'attachment-size': { ...typeStyle('body-xs'), color: '{color.fg-subtle}', 'white-space': 'nowrap' },
|
|
105
|
+
composer: { display: 'flex', 'align-items': 'flex-start', gap: '{space.3}', 'padding-block': '{space.3}', 'min-width': '0' },
|
|
106
|
+
'composer-actions': { display: 'flex', 'justify-content': 'flex-end', 'align-items': 'center', gap: '{space.2}', 'margin-top': '{space.2}' },
|
|
107
|
+
'@states': {
|
|
108
|
+
busy: { content: { opacity: '{opacity.muted}' } },
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
variants: {
|
|
112
|
+
variant: {
|
|
113
|
+
simple: { root: {} },
|
|
114
|
+
'with-comments': { root: {} },
|
|
115
|
+
compact: {
|
|
116
|
+
item: { gap: '{space.2}', 'padding-block': '{space.2}' },
|
|
117
|
+
avatar: { width: '{space.6}', height: '{space.6}', 'font-size': '{font.size.2xs}' },
|
|
118
|
+
line: { 'inset-inline-start': 'calc({space.3} - {space.px})', top: 'calc({space.2} + {space.6} + {space.1})' },
|
|
119
|
+
content: { 'padding-top': '{space.0.5}', gap: '{space.1.5}' },
|
|
120
|
+
comment: { padding: '{space.2} {space.2.5}' },
|
|
121
|
+
},
|
|
122
|
+
'with-composer': { root: {} },
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
extraCss: `
|
|
126
|
+
.cn-activity-feed__item:last-child .cn-activity-feed__line { display: none; }
|
|
127
|
+
.cn-activity-feed__composer > .cn-activity-feed__content { padding-top: 0; }
|
|
128
|
+
.cn-activity-feed__attachment-icon .cn-icon { width: {size.icon.md}; height: {size.icon.md}; }
|
|
129
|
+
.cn-activity-feed__attachment:hover { background-color: {color.bg-subtle}; border-color: {color.border-control-hover}; }
|
|
130
|
+
.cn-activity-feed__attachment:focus-visible { outline: none; box-shadow: ${FOCUS_RING['box-shadow']}; }
|
|
131
|
+
.cn-activity-feed__comment p + p { margin-top: {space.2}; }`,
|
|
132
|
+
examples: [
|
|
133
|
+
ex('Simple', feed('simple', [
|
|
134
|
+
item({ initials: 'MC', actor: 'Maya Chen', action: 'commented on', target: 'Q3 launch plan', time: '2h ago', datetime: '2026-09-11T07:40' }),
|
|
135
|
+
item({ initials: 'DC', actor: 'Daniel Costa', action: 'uploaded', target: 'brand-guidelines-v3.pdf', time: '4h ago', datetime: '2026-09-11T05:12' }),
|
|
136
|
+
item({ initials: 'SA', actor: 'Sofia Almeida', action: 'assigned', target: 'Homepage redesign to Lucas Ferreira', time: 'Yesterday', datetime: '2026-09-10T16:05' }),
|
|
137
|
+
item({ initials: 'AK', actor: 'Aisha Khan', action: 'marked', target: 'Invoice INV-3066 as paid', time: 'Sep 9', datetime: '2026-09-09T11:30' }),
|
|
138
|
+
].join('')), 'One sentence per event. The actor and the target are emphasized; the verb and the time are quiet.'),
|
|
139
|
+
ex('With comments and an attachment', feed('with-comments', [
|
|
140
|
+
item({ initials: 'MC', actor: 'Maya Chen', action: 'commented on', target: 'Q3 launch plan', time: '2h ago', datetime: '2026-09-11T07:40', extra: comment('Can we move the pricing page to the second sprint? Legal still needs to review the new tiers.') }),
|
|
141
|
+
item({ initials: 'DC', actor: 'Daniel Costa', action: 'replied', time: '1h ago', datetime: '2026-09-11T08:32', extra: comment('Works for me. I attached the updated timeline.') + attachment('q3-launch-timeline.xlsx', '312 KB') }),
|
|
142
|
+
item({ initials: 'NB', actor: 'Noah Berg', action: 'approved', target: 'Q3 launch plan', time: '20m ago', datetime: '2026-09-11T09:20' }),
|
|
143
|
+
].join('')), 'Quoted comments sit in a subtle card; files are hairline rows that open the file.'),
|
|
144
|
+
ex('Compact (notification feed)', feed('compact', [
|
|
145
|
+
item({ initials: 'EL', actor: 'Elena Rossi', action: 'mentioned you in', target: 'Brand refresh', time: '5m', datetime: '2026-09-11T09:35' }),
|
|
146
|
+
item({ initials: 'TS', actor: 'Tomás Silva', action: 'requested access to', target: 'Finance dashboard', time: '32m', datetime: '2026-09-11T09:08' }),
|
|
147
|
+
item({ initials: 'AK', actor: 'Aisha Khan', action: 'closed', target: 'Ticket #4821', time: '1h', datetime: '2026-09-11T08:40' }),
|
|
148
|
+
item({ initials: 'SA', actor: 'Sofia Almeida', action: 'shared', target: 'Onboarding checklist', time: '3h', datetime: '2026-09-11T06:40' }),
|
|
149
|
+
item({ initials: 'LF', actor: 'Lucas Ferreira', action: 'joined', target: 'Lumen Design', time: 'Yesterday', datetime: '2026-09-10T14:00' }),
|
|
150
|
+
].join(''), '420px'), '24px avatars and 8px vertical padding for side panels and popovers.'),
|
|
151
|
+
ex('With composer', feed('with-composer', [
|
|
152
|
+
item({ initials: 'MC', actor: 'Maya Chen', action: 'commented on', target: 'Pricing page copy', time: '1h ago', datetime: '2026-09-11T08:40', extra: comment('The second headline reads better. Can we A/B test both?') }),
|
|
153
|
+
item({ initials: 'AK', actor: 'Aisha Khan', action: 'replied', time: '40m ago', datetime: '2026-09-11T09:00', extra: comment('Yes — I set up the test for Monday.') }),
|
|
154
|
+
composer,
|
|
155
|
+
].join('')), 'The composer reuses the item layout so the connector runs into the current user\'s avatar.'),
|
|
156
|
+
],
|
|
157
|
+
rules: [
|
|
158
|
+
'Every row is one sentence in the same shape: actor, verb, target, time. Actor and target are emphasized; the verb and the time stay quiet.',
|
|
159
|
+
'Verbs are past tense and specific ("uploaded", "approved", "mentioned you in"); never "updated the record".',
|
|
160
|
+
'Pick one order per screen: newest first for a home feed, oldest first on a record where the story matters.',
|
|
161
|
+
'Timestamps are relative under 24h ("2h ago"), absolute after ("Sep 9"), always in a <time datetime>.',
|
|
162
|
+
'Comments are quoted verbatim in the subtle card and truncated after four lines with a "Show more" link Button; the feed never renders an entire email.',
|
|
163
|
+
'Attachments show name and size; the icon is the file type, not a thumbnail. One attachment row per file.',
|
|
164
|
+
'Use compact only in side panels, popovers and notification drawers; on a page the default size keeps comments readable.',
|
|
165
|
+
'The composer goes last, after the newest event, and uses a sm Textarea with a single primary "Comment" button; no toolbar.',
|
|
166
|
+
'Long feeds paginate with a "Show 20 earlier events" link Button at the end, not infinite scroll inside a card.',
|
|
167
|
+
],
|
|
168
|
+
a11y: [
|
|
169
|
+
'Root has role="feed" with an aria-label; each event is an <article> so screen readers can jump between them. Set aria-busy="true" on the root while loading more.',
|
|
170
|
+
'Initials avatars carry role="img" and aria-label with the full name; the connector line is aria-hidden.',
|
|
171
|
+
'The sentence must read correctly in DOM order: actor, action, target, time. Do not move the time before the actor with CSS.',
|
|
172
|
+
'Attachments are real links with the file name as the accessible name; the size is extra text, not the only label.',
|
|
173
|
+
'The composer textarea has a label ("Comment") and the button is a real <button>; Cmd/Ctrl+Enter may submit but is never the only way.',
|
|
174
|
+
],
|
|
175
|
+
related: ['timeline', 'avatar', 'textarea', 'message', 'notification', 'list'],
|
|
176
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { ex, typeStyle } from "./_shared.mjs";
|
|
2
|
+
// Two or three concentric rings, one per metric, in brand.600 / brand.400 / brand.200 from the
|
|
3
|
+
// outside in, with a legend beside them. Same geometry rules as ProgressCircle: viewBox 0 0 100 100,
|
|
4
|
+
// stroke-width 8, radii 42 / 32 / 22 → circumferences 263.89 / 201.06 / 138.23.
|
|
5
|
+
const RINGS = [
|
|
6
|
+
{ r: 42, c: 263.89 },
|
|
7
|
+
{ r: 32, c: 201.06 },
|
|
8
|
+
{ r: 22, c: 138.23 },
|
|
9
|
+
];
|
|
10
|
+
const RING_COLOR = ['{brand.600}', '{brand.400}', '{brand.200}'];
|
|
11
|
+
const gauge = (o) => {
|
|
12
|
+
const rings = o.metrics.map((m, i) => {
|
|
13
|
+
const { r, c } = RINGS[i];
|
|
14
|
+
return `<circle class="cn-activity-gauge__ring" data-ring="${i + 1}" data-track cx="50" cy="50" r="${r}"/><circle class="cn-activity-gauge__ring" data-ring="${i + 1}" cx="50" cy="50" r="${r}" style="stroke-dasharray:${c};stroke-dashoffset:${(c * (1 - m.value / 100)).toFixed(1)}"/>`;
|
|
15
|
+
}).join('');
|
|
16
|
+
const legend = o.metrics.map((m, i) => `<li class="cn-activity-gauge__legend-item"><span class="cn-activity-gauge__swatch" data-ring="${i + 1}"></span><span class="cn-activity-gauge__label">${m.label}</span><span class="cn-activity-gauge__label" data-value>${m.text ?? `${m.value}%`}</span></li>`).join('');
|
|
17
|
+
return `<div class="cn-activity-gauge" data-size="${o.size ?? 'md'}" role="img" aria-label="${o.label}"><svg class="cn-activity-gauge__rings" viewBox="0 0 100 100" aria-hidden="true">${rings}</svg><ul class="cn-activity-gauge__legend">${legend}</ul></div>`;
|
|
18
|
+
};
|
|
19
|
+
const OUTREACH = [
|
|
20
|
+
{ label: 'Emails sent', value: 72 },
|
|
21
|
+
{ label: 'Replies', value: 48 },
|
|
22
|
+
{ label: 'Meetings booked', value: 91 },
|
|
23
|
+
];
|
|
24
|
+
export const activityGauge = {
|
|
25
|
+
name: 'ActivityGauge',
|
|
26
|
+
slug: 'activity-gauge',
|
|
27
|
+
category: 'data-display',
|
|
28
|
+
description: 'Two or three concentric rings that each show a share of a goal, from the outer ring in the strongest brand tint to the inner one in the lightest, with a legend beside them that names each ring and its value.',
|
|
29
|
+
usage: 'Use on a dashboard card to compare 2–3 related goals at a glance (sent / replied / booked; storage / bandwidth / seats). For one value use ProgressCircle; for more than three metrics or exact comparison use a bar chart in a ChartFrame; for a trend use a line chart.',
|
|
30
|
+
anatomy: [
|
|
31
|
+
{ part: 'root', element: 'div', description: 'Row (inline-flex, align center) with the rings on the left and the legend on the right. role="img" with an aria-label that reads every metric. Carries data-size.' },
|
|
32
|
+
{ part: 'rings', element: 'svg', description: 'The square svg, viewBox 0 0 100 100, holding a track and a value ring per metric.' },
|
|
33
|
+
{ part: 'ring', element: 'circle', description: 'One ring. data-ring="1|2|3" sets the color (brand.600 / 400 / 200, outside in); data-track marks the grey background ring. Value rings set stroke-dasharray (circumference) and stroke-dashoffset inline.' },
|
|
34
|
+
{ part: 'legend', element: 'ul', description: 'Stacked list of metrics next to the rings, gap space.2, no bullets.' },
|
|
35
|
+
{ part: 'legend-item', element: 'li', description: 'One row: swatch, label, value (label with data-value).' },
|
|
36
|
+
{ part: 'swatch', element: 'span', description: '8px round dot in the ring color (data-ring).' },
|
|
37
|
+
{ part: 'label', element: 'span', description: 'body-sm text: the metric name in fg-muted; with data-value, the number pushed to the end in fg-default, tabular.' },
|
|
38
|
+
],
|
|
39
|
+
props: {
|
|
40
|
+
size: { values: ['sm', 'md', 'lg'], default: 'md', description: 'Ring box 96 / 128 / 160px. sm for a stat tile, md for a dashboard card, lg for a page section with a heading.' },
|
|
41
|
+
},
|
|
42
|
+
states: {},
|
|
43
|
+
base: {
|
|
44
|
+
root: { display: 'inline-flex', 'flex-wrap': 'wrap', 'align-items': 'center', gap: '{space.6}', 'max-width': '100%', color: '{color.fg-default}', 'vertical-align': 'middle' },
|
|
45
|
+
rings: { display: 'block', 'flex-shrink': '0', width: '{space.32}', height: '{space.32}', overflow: 'visible' },
|
|
46
|
+
ring: {
|
|
47
|
+
fill: 'none',
|
|
48
|
+
stroke: '{brand.600}',
|
|
49
|
+
'stroke-width': '8',
|
|
50
|
+
'stroke-linecap': 'round',
|
|
51
|
+
transform: 'rotate(-90deg)',
|
|
52
|
+
'transform-origin': '50% 50%',
|
|
53
|
+
'transition-property': 'stroke-dashoffset',
|
|
54
|
+
'transition-duration': '{motion.duration.slow}',
|
|
55
|
+
'transition-timing-function': '{motion.easing.standard}',
|
|
56
|
+
},
|
|
57
|
+
legend: { display: 'flex', 'flex-direction': 'column', gap: '{space.2}', margin: '0', padding: '0', 'list-style': 'none', 'min-width': '{space.40}' },
|
|
58
|
+
'legend-item': { display: 'flex', 'align-items': 'center', gap: '{space.2}', ...typeStyle('body-sm') },
|
|
59
|
+
swatch: { width: '{space.2}', height: '{space.2}', 'flex-shrink': '0', 'border-radius': '{radius.full}', 'background-color': '{brand.600}' },
|
|
60
|
+
label: { color: '{color.fg-muted}', 'min-width': '0', overflow: 'hidden', 'text-overflow': 'ellipsis', 'white-space': 'nowrap' },
|
|
61
|
+
},
|
|
62
|
+
variants: {
|
|
63
|
+
size: {
|
|
64
|
+
sm: { root: { gap: '{space.4}' }, rings: { width: '{space.24}', height: '{space.24}' }, 'legend-item': { 'font-size': '{font.size.xs}' }, legend: { 'min-width': '{space.32}' } },
|
|
65
|
+
md: { rings: { width: '{space.32}', height: '{space.32}' } },
|
|
66
|
+
lg: { root: { gap: '{space.8}' }, rings: { width: '{space.40}', height: '{space.40}' }, 'legend-item': { 'font-size': '{font.size.md}' }, legend: { gap: '{space.3}' } },
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
extraCss: `
|
|
70
|
+
.cn-activity-gauge .cn-activity-gauge__ring[data-track] { stroke: {color.bg-subtle}; stroke-linecap: butt; }
|
|
71
|
+
.cn-activity-gauge .cn-activity-gauge__ring[data-ring="1"]:not([data-track]), .cn-activity-gauge .cn-activity-gauge__swatch[data-ring="1"] { stroke: ${RING_COLOR[0]}; background-color: ${RING_COLOR[0]}; }
|
|
72
|
+
.cn-activity-gauge .cn-activity-gauge__ring[data-ring="2"]:not([data-track]), .cn-activity-gauge .cn-activity-gauge__swatch[data-ring="2"] { stroke: ${RING_COLOR[1]}; background-color: ${RING_COLOR[1]}; }
|
|
73
|
+
.cn-activity-gauge .cn-activity-gauge__ring[data-ring="3"]:not([data-track]), .cn-activity-gauge .cn-activity-gauge__swatch[data-ring="3"] { stroke: ${RING_COLOR[2]}; background-color: ${RING_COLOR[2]}; }
|
|
74
|
+
.cn-activity-gauge .cn-activity-gauge__label[data-value] { margin-inline-start: auto; color: {color.fg-default}; font-weight: {font.weight.medium}; font-variant-numeric: tabular-nums; flex-shrink: 0; }`,
|
|
75
|
+
examples: [
|
|
76
|
+
ex('Three metrics (md)', gauge({ metrics: OUTREACH, label: 'Outreach this week: emails sent 72%, replies 48%, meetings booked 91%' }), 'Outer ring = first metric in brand.600, then brand.400, then brand.200. Offsets: 263.89 × 0.28 = 73.9, 201.06 × 0.52 = 104.6, 138.23 × 0.09 = 12.4.'),
|
|
77
|
+
ex('Two metrics', gauge({ metrics: [{ label: 'Storage', value: 64, text: '64 GB of 100' }, { label: 'Bandwidth', value: 37, text: '37%' }], label: 'Usage: storage 64 GB of 100, bandwidth 37%' }), 'With two metrics only the outer two radii are used; the values can be counts.'),
|
|
78
|
+
ex('Sizes', `<div style="display:flex;flex-wrap:wrap;align-items:center;gap:var(--cn-space-8)">${gauge({ size: 'sm', metrics: OUTREACH, label: 'Outreach: emails sent 72%, replies 48%, meetings booked 91%' })}${gauge({ size: 'lg', metrics: OUTREACH, label: 'Outreach: emails sent 72%, replies 48%, meetings booked 91%' })}</div>`, '96px (sm) and 160px (lg) rings with body-xs / body-md legends.'),
|
|
79
|
+
],
|
|
80
|
+
recipes: [
|
|
81
|
+
ex('Dashboard card', `<section class="cn-card" data-variant="default" data-padding="md" style="max-width:420px"><header class="cn-card__header"><div><h3 class="cn-card__title">Outreach this week</h3><p class="cn-card__description">Share of the weekly goal, Sep 7–11</p></div></header><div class="cn-card__body">${gauge({ metrics: OUTREACH, label: 'Outreach this week: emails sent 72%, replies 48%, meetings booked 91%' })}</div></section>`, 'Title names the goal, description the period; the gauge is the whole body.'),
|
|
82
|
+
],
|
|
83
|
+
rules: [
|
|
84
|
+
'Two or three rings, never one (ProgressCircle) or four. Order metrics by importance: the first is the outer ring in the strongest tint.',
|
|
85
|
+
'Geometry is fixed: viewBox 0 0 100 100, stroke 8, radii 42 / 32 / 22 with circumferences 263.89 / 201.06 / 138.23. Each value ring sets stroke-dasharray to its circumference and stroke-dashoffset = circumference × (1 − value / 100) inline.',
|
|
86
|
+
'Every value ring has a track ring of the same radius behind it (data-track) so an empty metric still reads as a ring.',
|
|
87
|
+
'Colors are the three brand tints only; the rings compare shares of goals, not statuses. Use a bar chart when the metrics need distinct hues.',
|
|
88
|
+
'The legend is mandatory and lists the metrics in ring order, each with its value; the number may be a count ("64 GB of 100") when the percentage alone is unclear.',
|
|
89
|
+
'Values are capped at 100%; show "100%" and say the goal was exceeded in the caption or the card description.',
|
|
90
|
+
'One gauge per card. sm in stat tiles, md in dashboard cards, lg in a page section.',
|
|
91
|
+
],
|
|
92
|
+
a11y: [
|
|
93
|
+
'The root is role="img" with an aria-label that reads every metric and value in ring order; the svg is aria-hidden.',
|
|
94
|
+
'The legend repeats the same information as visible text, so the rings are never the only source.',
|
|
95
|
+
'Swatches are decorative; the pairing between ring and legend is also carried by order (outer to inner, top to bottom).',
|
|
96
|
+
],
|
|
97
|
+
related: ['progress-circle', 'progress', 'stat', 'chart-frame', 'card'],
|
|
98
|
+
};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { ex, ICON, typeStyle } from "./_shared.mjs";
|
|
2
|
+
// status → dot fill, pill bg, pill fg
|
|
3
|
+
const STATUS = {
|
|
4
|
+
working: { dot: '{color.bg-accent}', pillBg: '{color.bg-accent-subtle}', pillFg: '{color.fg-accent}', label: 'Working' },
|
|
5
|
+
waiting: { dot: '{color.bg-warning}', pillBg: '{color.bg-warning-subtle}', pillFg: '{color.fg-warning}', label: 'Waiting for you' },
|
|
6
|
+
idle: { dot: '{color.fg-subtle}', pillBg: '{color.bg-subtle}', pillFg: '{color.fg-muted}', label: 'Idle' },
|
|
7
|
+
offline: { dot: '{color.bg-muted}', pillBg: '{color.bg-subtle}', pillFg: '{color.fg-subtle}', label: 'Offline' },
|
|
8
|
+
};
|
|
9
|
+
const avatar = (initials) => `<span class="cn-agent-presence__avatar">${initials ?? ICON.spark}<span class="cn-agent-presence__dot"></span></span>`;
|
|
10
|
+
export const agentPresence = {
|
|
11
|
+
name: 'AgentPresence',
|
|
12
|
+
slug: 'agent-presence',
|
|
13
|
+
category: 'data-display',
|
|
14
|
+
description: 'Shows an AI agent as a teammate: avatar with a status dot, the name, a state pill and a live line saying what it is doing right now. The accent color appears only here and only when an agent is working.',
|
|
15
|
+
usage: 'Use wherever a user needs to know whether an agent is active and on what: the team rail, a conversation header, a run list, a kanban card. Not for human users (use Avatar) and not for static labels (use Badge). The activity line must be live; if there is nothing current, show the last run time instead.',
|
|
16
|
+
anatomy: [
|
|
17
|
+
{ part: 'root', element: 'div', description: 'The element. Chip is inline; row and card are a grid: avatar | name + state | time on the first line, activity on the second.' },
|
|
18
|
+
{ part: 'avatar', element: 'span', description: '24px circle (20px in sm) in the accent wash with the agent’s initials or the spark icon in fg-accent. Contains the dot.' },
|
|
19
|
+
{ part: 'dot', element: 'span', description: '8px status dot at the avatar’s bottom-right with a 2px surface ring. Pulses (halo) only when working.' },
|
|
20
|
+
{ part: 'name', element: 'span', description: 'label-sm, the agent’s name ("Vera", "Iris"). Never a role or a model name.' },
|
|
21
|
+
{ part: 'state', element: 'span', description: 'Kicker-style pill: "Working", "Waiting for you", "Idle", "Offline". Hidden in chip.', optional: true },
|
|
22
|
+
{ part: 'activity', element: 'span', description: 'body-xs muted, truncated, the live "now" line: "Reading 3 replies from Nakamura…". Hidden in chip.', optional: true },
|
|
23
|
+
{ part: 'time', element: 'span', description: 'code-sm subtle, how long the current state has lasted ("2m", "40m"). Hidden in chip.', optional: true },
|
|
24
|
+
],
|
|
25
|
+
props: {
|
|
26
|
+
variant: {
|
|
27
|
+
values: ['chip', 'row', 'card'],
|
|
28
|
+
default: 'row',
|
|
29
|
+
description: 'chip = inline pill with avatar, name and dot only (inside text, headers, kanban cards); row = one line in a list with the state pill, activity and time (default); card = the row on a bordered surface with padding, for a team grid or a dashboard.',
|
|
30
|
+
},
|
|
31
|
+
size: {
|
|
32
|
+
values: ['sm', 'md'],
|
|
33
|
+
default: 'md',
|
|
34
|
+
description: 'md = 24px avatar, label-sm name (default); sm = 20px avatar, label-xs name, for dense tables and chips inside text.',
|
|
35
|
+
},
|
|
36
|
+
status: {
|
|
37
|
+
values: ['working', 'waiting', 'idle', 'offline'],
|
|
38
|
+
default: 'idle',
|
|
39
|
+
description: 'working = the agent is acting right now: accent dot with a pulse and an accent pill; waiting = it needs the user (approval, a file): warning dot and pill; idle = available, nothing running: neutral; offline = paused or disconnected: muted dot and the whole element at 70% opacity.',
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
states: {},
|
|
43
|
+
base: {
|
|
44
|
+
root: {
|
|
45
|
+
display: 'grid',
|
|
46
|
+
'grid-template-columns': 'auto auto 1fr auto',
|
|
47
|
+
'grid-template-areas': '"avatar name state time" "avatar activity activity time"',
|
|
48
|
+
'align-items': 'center',
|
|
49
|
+
'column-gap': '{space.2}',
|
|
50
|
+
'row-gap': '{space.0.5}',
|
|
51
|
+
'min-width': '0',
|
|
52
|
+
color: '{color.fg-default}',
|
|
53
|
+
...typeStyle('body-sm'),
|
|
54
|
+
},
|
|
55
|
+
avatar: {
|
|
56
|
+
'grid-area': 'avatar',
|
|
57
|
+
position: 'relative',
|
|
58
|
+
display: 'inline-flex',
|
|
59
|
+
'align-items': 'center',
|
|
60
|
+
'justify-content': 'center',
|
|
61
|
+
'flex-shrink': '0',
|
|
62
|
+
width: '{size.icon.xl}',
|
|
63
|
+
height: '{size.icon.xl}',
|
|
64
|
+
'margin-inline-end': '{space.1}',
|
|
65
|
+
'border-radius': '{radius.full}',
|
|
66
|
+
'background-color': '{color.bg-accent-subtle}',
|
|
67
|
+
color: '{color.fg-accent}',
|
|
68
|
+
...typeStyle('label-xs'),
|
|
69
|
+
'font-size': '{font.size.2xs}',
|
|
70
|
+
'text-transform': 'uppercase',
|
|
71
|
+
'letter-spacing': '{font.letterSpacing.wide}',
|
|
72
|
+
},
|
|
73
|
+
dot: {
|
|
74
|
+
position: 'absolute',
|
|
75
|
+
right: '-1px',
|
|
76
|
+
bottom: '-1px',
|
|
77
|
+
width: '8px',
|
|
78
|
+
height: '8px',
|
|
79
|
+
'border-radius': '{radius.full}',
|
|
80
|
+
'background-color': '{color.fg-subtle}',
|
|
81
|
+
'box-shadow': '0 0 0 2px {color.bg-surface}',
|
|
82
|
+
},
|
|
83
|
+
name: { 'grid-area': 'name', ...typeStyle('label-sm'), color: '{color.fg-default}', 'white-space': 'nowrap' },
|
|
84
|
+
state: {
|
|
85
|
+
'grid-area': 'state',
|
|
86
|
+
'justify-self': 'start',
|
|
87
|
+
display: 'inline-flex',
|
|
88
|
+
'align-items': 'center',
|
|
89
|
+
height: '{size.icon.lg}',
|
|
90
|
+
'padding-inline': '{space.2}',
|
|
91
|
+
'border-radius': '{radius.full}',
|
|
92
|
+
...typeStyle('kicker'),
|
|
93
|
+
'text-transform': 'uppercase',
|
|
94
|
+
'background-color': '{color.bg-subtle}',
|
|
95
|
+
color: '{color.fg-muted}',
|
|
96
|
+
'white-space': 'nowrap',
|
|
97
|
+
},
|
|
98
|
+
activity: { 'grid-area': 'activity', ...typeStyle('body-xs'), color: '{color.fg-muted}', 'min-width': '0', overflow: 'hidden', 'text-overflow': 'ellipsis', 'white-space': 'nowrap' },
|
|
99
|
+
time: { 'grid-area': 'time', 'align-self': 'start', ...typeStyle('code-sm'), color: '{color.fg-subtle}', 'white-space': 'nowrap', 'padding-top': '{space.0.5}' },
|
|
100
|
+
},
|
|
101
|
+
variants: {
|
|
102
|
+
variant: {
|
|
103
|
+
chip: {
|
|
104
|
+
root: { display: 'inline-flex', 'align-items': 'center', gap: '{space.1.5}', height: '{size.control.xs}', 'padding-inline': '{space.1} {space.2.5}', 'border-radius': '{radius.full}', border: '{border.width.thin} solid {color.border-default}', 'background-color': '{color.bg-surface}', 'vertical-align': 'middle' },
|
|
105
|
+
avatar: { width: '{size.icon.lg}', height: '{size.icon.lg}', 'margin-inline-end': '0' },
|
|
106
|
+
dot: { width: '6px', height: '6px' },
|
|
107
|
+
state: { display: 'none' },
|
|
108
|
+
activity: { display: 'none' },
|
|
109
|
+
time: { display: 'none' },
|
|
110
|
+
},
|
|
111
|
+
row: { root: { padding: '{space.2} 0' } },
|
|
112
|
+
card: { root: { padding: '{space.4}', 'background-color': '{color.bg-surface}', border: '{border.width.thin} solid {color.border-default}', 'border-radius': '{radius.xl}' } },
|
|
113
|
+
},
|
|
114
|
+
size: {
|
|
115
|
+
sm: { avatar: { width: '{size.icon.lg}', height: '{size.icon.lg}' }, name: { ...typeStyle('label-xs') }, state: { height: '{size.icon.md}', 'font-size': '9px' } },
|
|
116
|
+
md: { root: {} },
|
|
117
|
+
},
|
|
118
|
+
status: Object.fromEntries(Object.entries(STATUS).map(([s, v]) => [s, {
|
|
119
|
+
dot: { 'background-color': v.dot, ...(s === 'working' ? { animation: 'cn-agent-presence-pulse 1.8s {motion.easing.standard} infinite' } : {}) },
|
|
120
|
+
state: { 'background-color': v.pillBg, color: v.pillFg },
|
|
121
|
+
...(s === 'offline' ? { root: { opacity: '{opacity.muted}' } } : {}),
|
|
122
|
+
}])),
|
|
123
|
+
},
|
|
124
|
+
extraCss: `
|
|
125
|
+
@keyframes cn-agent-presence-pulse { 0% { box-shadow: 0 0 0 2px {color.bg-surface}, 0 0 0 2px color-mix(in srgb, {color.bg-accent} 45%, transparent); } 100% { box-shadow: 0 0 0 2px {color.bg-surface}, 0 0 0 8px transparent; } }
|
|
126
|
+
.cn-agent-presence .cn-agent-presence__avatar .cn-icon { width: {size.icon.sm}; height: {size.icon.sm}; }
|
|
127
|
+
.cn-agent-presence[data-variant="chip"] .cn-agent-presence__avatar .cn-icon { width: {size.icon.xs}; height: {size.icon.xs}; }`,
|
|
128
|
+
examples: [
|
|
129
|
+
ex('Row, working', `<div class="cn-agent-presence" data-variant="row" data-size="md" data-status="working" style="width:420px">${avatar()}<span class="cn-agent-presence__name">Vera</span><span class="cn-agent-presence__state">Working</span><span class="cn-agent-presence__activity">Reading 3 replies from Nakamura Trading Co.…</span><span class="cn-agent-presence__time">2m</span></div>`),
|
|
130
|
+
ex('Row, waiting for you', `<div class="cn-agent-presence" data-variant="row" data-size="md" data-status="waiting" style="width:420px">${avatar('IR')}<span class="cn-agent-presence__name">Iris</span><span class="cn-agent-presence__state">Waiting for you</span><span class="cn-agent-presence__activity">2 drafts ready for approval · Hoffmann GmbH, Lagos Fresh</span><span class="cn-agent-presence__time">14m</span></div>`),
|
|
131
|
+
ex('Row, idle', `<div class="cn-agent-presence" data-variant="row" data-size="md" data-status="idle" style="width:420px">${avatar('TH')}<span class="cn-agent-presence__name">Theo</span><span class="cn-agent-presence__state">Idle</span><span class="cn-agent-presence__activity">Last run 40 min ago · verified 38 companies</span><span class="cn-agent-presence__time">40m</span></div>`),
|
|
132
|
+
ex('Row, offline', `<div class="cn-agent-presence" data-variant="row" data-size="md" data-status="offline" style="width:420px">${avatar('KA')}<span class="cn-agent-presence__name">Kai</span><span class="cn-agent-presence__state">Offline</span><span class="cn-agent-presence__activity">Paused until Monday 08:00</span><span class="cn-agent-presence__time">2d</span></div>`),
|
|
133
|
+
ex('Chips', `<span class="cn-agent-presence" data-variant="chip" data-size="md" data-status="working">${avatar()}<span class="cn-agent-presence__name">Vera</span></span> <span class="cn-agent-presence" data-variant="chip" data-size="md" data-status="waiting">${avatar('IR')}<span class="cn-agent-presence__name">Iris</span></span> <span class="cn-agent-presence" data-variant="chip" data-size="sm" data-status="idle">${avatar('TH')}<span class="cn-agent-presence__name">Theo</span></span>`, 'Chips carry only avatar, dot and name. Use them inline: “Assigned to [Vera]”.'),
|
|
134
|
+
ex('Card, working, sm', `<div class="cn-agent-presence" data-variant="card" data-size="sm" data-status="working" style="width:320px">${avatar()}<span class="cn-agent-presence__name">Vera</span><span class="cn-agent-presence__state">Working</span><span class="cn-agent-presence__activity">Drafting a reply to Marisco del Sur in Spanish…</span><span class="cn-agent-presence__time">45s</span></div>`),
|
|
135
|
+
],
|
|
136
|
+
recipes: [
|
|
137
|
+
ex('Team rail', `<div style="display:flex;flex-direction:column;width:420px;border:1px solid var(--cn-color-border-default);border-radius:var(--cn-radius-xl);background:var(--cn-color-bg-surface);padding:var(--cn-space-2) var(--cn-space-4)"><div class="cn-agent-presence" data-variant="row" data-size="md" data-status="working">${avatar()}<span class="cn-agent-presence__name">Vera</span><span class="cn-agent-presence__state">Working</span><span class="cn-agent-presence__activity">Searching importers in South Korea…</span><span class="cn-agent-presence__time">6m</span></div><div class="cn-agent-presence" data-variant="row" data-size="md" data-status="waiting">${avatar('IR')}<span class="cn-agent-presence__name">Iris</span><span class="cn-agent-presence__state">Waiting for you</span><span class="cn-agent-presence__activity">Needs the price list for Hoffmann GmbH</span><span class="cn-agent-presence__time">1h</span></div><div class="cn-agent-presence" data-variant="row" data-size="md" data-status="idle">${avatar('TH')}<span class="cn-agent-presence__name">Theo</span><span class="cn-agent-presence__state">Idle</span><span class="cn-agent-presence__activity">Last run today 07:00</span><span class="cn-agent-presence__time">3h</span></div></div>`, 'Rows stack with no divider; waiting agents sort above working ones because they need the user.'),
|
|
138
|
+
],
|
|
139
|
+
rules: [
|
|
140
|
+
'Orchid/accent means "an agent is working". This is the only place accent is used as a status; never use it for human users or for success.',
|
|
141
|
+
'The activity line is live and concrete: verb + object + count ("Reading 3 replies from Nakamura…"). It updates in place without re-rendering the row.',
|
|
142
|
+
'When nothing is happening, the activity line shows the last run ("Last run 40 min ago · verified 38 companies"), never "Nothing to do".',
|
|
143
|
+
'Agents have a name and a task, not a personality: no "I", no emoji, no "thinking…", no model names.',
|
|
144
|
+
'State pill text is fixed: Working / Waiting for you / Idle / Offline. Do not invent states; put details in the activity line.',
|
|
145
|
+
'waiting sorts above working in any list: it is the one status that needs the user.',
|
|
146
|
+
'Chips carry no activity; if the reader needs to know what the agent is doing, use a row.',
|
|
147
|
+
'The pulse is the only motion: 1.8s halo on the dot while working, off under prefers-reduced-motion.',
|
|
148
|
+
],
|
|
149
|
+
a11y: [
|
|
150
|
+
'The status is text (the pill) and never only the dot color; chips keep the status in an aria-label on the root ("Vera, working").',
|
|
151
|
+
'The activity line is a polite live region (aria-live="polite" on the list, not on each row) so screen readers hear changes without interruption.',
|
|
152
|
+
'Time is relative and short ("2m"); pair it with a title attribute carrying the absolute time.',
|
|
153
|
+
'Initials in the avatar are aria-hidden when the name is visible next to them.',
|
|
154
|
+
],
|
|
155
|
+
related: ['avatar', 'badge', 'progress', 'card'],
|
|
156
|
+
};
|