@ayni.org/ayni-ui-core 0.1.0 → 0.3.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.
@@ -0,0 +1,242 @@
1
+ /** The text every surface starts from: its size, and its line height as a multiple of the size. */
2
+ export declare const BASE_TEXT: {
3
+ readonly fontSize: 15;
4
+ readonly lineHeight: 1.45;
5
+ };
6
+ export declare const HOME_DESIGN: {
7
+ readonly rail: {
8
+ readonly width: 64;
9
+ readonly paddingY: 10;
10
+ readonly gap: 6;
11
+ /** a controller's tile, and the bordered tiles that are not controllers (add, settings) */
12
+ readonly tile: {
13
+ readonly size: 48;
14
+ readonly radius: 14;
15
+ readonly initialsFontSize: 15;
16
+ readonly initialsWeight: 600;
17
+ readonly inactiveOpacity: 0.5;
18
+ };
19
+ readonly ghost: {
20
+ readonly borderWidth: 1;
21
+ readonly iconSize: 22;
22
+ };
23
+ /** the bar to the left of the selected tile */
24
+ readonly selectedBar: {
25
+ readonly left: -8;
26
+ readonly inset: 10;
27
+ readonly width: 4;
28
+ readonly radius: 2;
29
+ };
30
+ /** unread messages, on the tile's top right corner */
31
+ readonly badge: {
32
+ readonly top: 2;
33
+ readonly right: 2;
34
+ readonly minWidth: 18;
35
+ readonly height: 18;
36
+ readonly paddingX: 5;
37
+ readonly radius: 9;
38
+ readonly fontSize: 11;
39
+ readonly fontWeight: 600;
40
+ };
41
+ /** something waits for the person, on the tile's bottom right corner */
42
+ readonly dot: {
43
+ readonly bottom: 4;
44
+ readonly right: 4;
45
+ readonly size: 9;
46
+ readonly borderWidth: 2;
47
+ };
48
+ /** the line under the pinned tiles, with its pin */
49
+ readonly separator: {
50
+ readonly width: 48;
51
+ readonly height: 14;
52
+ readonly lineWidth: 1;
53
+ readonly pinSize: 11;
54
+ readonly pinPaddingX: 3;
55
+ };
56
+ };
57
+ readonly column: {
58
+ readonly header: {
59
+ readonly height: 64;
60
+ readonly paddingLeft: 12;
61
+ readonly paddingRight: 8;
62
+ readonly gap: 8;
63
+ readonly fontSize: 16;
64
+ readonly iconSize: 18;
65
+ readonly flagFontSize: 16;
66
+ };
67
+ /** one agent: its logo, its name with the trust mark, its links, its menu */
68
+ readonly agentRow: {
69
+ readonly gap: 10;
70
+ readonly paddingTop: 8;
71
+ readonly paddingRight: 6;
72
+ readonly paddingBottom: 8;
73
+ readonly paddingLeft: 10;
74
+ };
75
+ readonly agentAvatar: {
76
+ readonly size: 40;
77
+ readonly radius: 12;
78
+ readonly initialsFontSize: 14;
79
+ };
80
+ readonly agentText: {
81
+ readonly gap: 1;
82
+ };
83
+ readonly agentTitle: {
84
+ readonly gap: 6;
85
+ readonly fontWeight: 600;
86
+ };
87
+ readonly trustMark: {
88
+ readonly size: 24;
89
+ readonly radius: 6;
90
+ readonly iconSize: 16;
91
+ };
92
+ readonly links: {
93
+ readonly gap: 10;
94
+ readonly fontSize: 11;
95
+ readonly lineHeight: 1.2;
96
+ };
97
+ /** the small icon buttons of a card: its menu, new conversation, search */
98
+ readonly smallButton: {
99
+ readonly size: 36;
100
+ readonly radius: 12;
101
+ readonly iconSize: 18;
102
+ };
103
+ /** the open card: the link state with the tools, then the conversations */
104
+ readonly body: {
105
+ readonly paddingX: 8;
106
+ readonly paddingBottom: 8;
107
+ readonly gap: 6;
108
+ };
109
+ readonly meta: {
110
+ readonly paddingX: 4;
111
+ readonly gap: 8;
112
+ readonly fontSize: 12;
113
+ readonly stateGap: 5;
114
+ readonly iconSize: 12;
115
+ readonly toolsGap: 2;
116
+ };
117
+ readonly conversationRow: {
118
+ readonly paddingY: 10;
119
+ readonly paddingLeft: 12;
120
+ /** room for the row's menu button */
121
+ readonly paddingRight: 40;
122
+ readonly minHeight: 44;
123
+ readonly radius: 10;
124
+ readonly rowGap: 2;
125
+ readonly columnGap: 8;
126
+ readonly titleWeight: 600;
127
+ readonly previewFontSize: 13;
128
+ readonly timeFontSize: 12;
129
+ /** the share of the accent mixed into the background of the selected row */
130
+ readonly selectedTint: 0.12;
131
+ readonly menuButtonSize: 36;
132
+ readonly menuIconSize: 16;
133
+ };
134
+ /** unread messages of a conversation, and of a closed card */
135
+ readonly count: {
136
+ readonly radius: 9;
137
+ readonly fontSize: 11;
138
+ readonly paddingY: 1;
139
+ readonly paddingX: 6;
140
+ };
141
+ readonly sectionLabel: {
142
+ readonly fontSize: 12;
143
+ readonly letterSpacing: 0.04;
144
+ readonly gap: 6;
145
+ readonly paddingTop: 4;
146
+ readonly paddingX: 8;
147
+ readonly iconSize: 12;
148
+ };
149
+ readonly empty: {
150
+ readonly fontSize: 13;
151
+ readonly paddingY: 6;
152
+ readonly paddingX: 8;
153
+ };
154
+ };
155
+ };
156
+ export type HomeDesign = typeof HOME_DESIGN;
157
+ /** Which colour role paints what on the home page. */
158
+ export declare const HOME_COLORS: {
159
+ readonly rail: {
160
+ readonly background: "surface";
161
+ readonly border: "border";
162
+ readonly icon: "muted";
163
+ };
164
+ readonly tileInitials: {
165
+ readonly background: "border";
166
+ readonly text: "text";
167
+ };
168
+ readonly ghost: {
169
+ readonly background: "bg";
170
+ readonly border: "border";
171
+ };
172
+ readonly selectedBar: {
173
+ readonly background: "accent";
174
+ };
175
+ readonly badge: {
176
+ readonly background: "danger";
177
+ };
178
+ readonly dot: {
179
+ readonly background: "warning";
180
+ readonly border: "surface";
181
+ };
182
+ readonly separator: {
183
+ readonly line: "border";
184
+ readonly pin: "muted";
185
+ readonly pinBackground: "surface";
186
+ };
187
+ readonly column: {
188
+ readonly background: "bg";
189
+ readonly border: "border";
190
+ };
191
+ readonly header: {
192
+ readonly icon: "muted";
193
+ };
194
+ readonly selectedCard: {
195
+ readonly background: "surface";
196
+ };
197
+ readonly links: {
198
+ readonly text: "muted";
199
+ };
200
+ readonly meta: {
201
+ readonly text: "muted";
202
+ };
203
+ readonly conversationRow: {
204
+ readonly preview: "muted";
205
+ readonly time: "muted";
206
+ readonly selectedTitle: "accent";
207
+ };
208
+ readonly count: {
209
+ readonly background: "accent";
210
+ readonly text: "accentContrast";
211
+ };
212
+ readonly sectionLabel: {
213
+ readonly text: "muted";
214
+ };
215
+ };
216
+ /** The trust mark of an agent, and the verdict of its Proof of Trust: one icon and one colour per result. */
217
+ export declare const TRUST_MARK: {
218
+ readonly trusted: {
219
+ readonly icon: "shield-check";
220
+ readonly color: "ok";
221
+ };
222
+ readonly untrusted: {
223
+ readonly icon: "shield-alert";
224
+ readonly color: "danger";
225
+ };
226
+ readonly unverified: {
227
+ readonly icon: "shield-question";
228
+ readonly color: "warning";
229
+ };
230
+ };
231
+ export type TrustResult = keyof typeof TRUST_MARK;
232
+ /** The icon before a controller's name: what stands behind its agents. */
233
+ export declare const CONTROLLER_KIND_ICON: {
234
+ readonly organization: "building";
235
+ readonly persona: "user";
236
+ readonly unverified: "shield-question";
237
+ };
238
+ /**
239
+ * The time on a message and on a conversation row: the hour today, the day and the hour before. An unreadable
240
+ * date gives an empty string.
241
+ */
242
+ export declare function formatTime(iso: string, locale: string, now?: Date): string;
@@ -0,0 +1,99 @@
1
+ /** The text every surface starts from: its size, and its line height as a multiple of the size. */
2
+ export const BASE_TEXT = { fontSize: 15, lineHeight: 1.45 };
3
+ export const HOME_DESIGN = {
4
+ rail: {
5
+ width: 64,
6
+ paddingY: 10,
7
+ gap: 6,
8
+ /** a controller's tile, and the bordered tiles that are not controllers (add, settings) */
9
+ tile: { size: 48, radius: 14, initialsFontSize: 15, initialsWeight: 600, inactiveOpacity: 0.5 },
10
+ ghost: { borderWidth: 1, iconSize: 22 },
11
+ /** the bar to the left of the selected tile */
12
+ selectedBar: { left: -8, inset: 10, width: 4, radius: 2 },
13
+ /** unread messages, on the tile's top right corner */
14
+ badge: { top: 2, right: 2, minWidth: 18, height: 18, paddingX: 5, radius: 9, fontSize: 11, fontWeight: 600 },
15
+ /** something waits for the person, on the tile's bottom right corner */
16
+ dot: { bottom: 4, right: 4, size: 9, borderWidth: 2 },
17
+ /** the line under the pinned tiles, with its pin */
18
+ separator: { width: 48, height: 14, lineWidth: 1, pinSize: 11, pinPaddingX: 3 },
19
+ },
20
+ column: {
21
+ header: { height: 64, paddingLeft: 12, paddingRight: 8, gap: 8, fontSize: 16, iconSize: 18, flagFontSize: 16 },
22
+ /** one agent: its logo, its name with the trust mark, its links, its menu */
23
+ agentRow: { gap: 10, paddingTop: 8, paddingRight: 6, paddingBottom: 8, paddingLeft: 10 },
24
+ agentAvatar: { size: 40, radius: 12, initialsFontSize: 14 },
25
+ agentText: { gap: 1 },
26
+ agentTitle: { gap: 6, fontWeight: 600 },
27
+ trustMark: { size: 24, radius: 6, iconSize: 16 },
28
+ links: { gap: 10, fontSize: 11, lineHeight: 1.2 },
29
+ /** the small icon buttons of a card: its menu, new conversation, search */
30
+ smallButton: { size: 36, radius: 12, iconSize: 18 },
31
+ /** the open card: the link state with the tools, then the conversations */
32
+ body: { paddingX: 8, paddingBottom: 8, gap: 6 },
33
+ meta: { paddingX: 4, gap: 8, fontSize: 12, stateGap: 5, iconSize: 12, toolsGap: 2 },
34
+ conversationRow: {
35
+ paddingY: 10,
36
+ paddingLeft: 12,
37
+ /** room for the row's menu button */
38
+ paddingRight: 40,
39
+ minHeight: 44,
40
+ radius: 10,
41
+ rowGap: 2,
42
+ columnGap: 8,
43
+ titleWeight: 600,
44
+ previewFontSize: 13,
45
+ timeFontSize: 12,
46
+ /** the share of the accent mixed into the background of the selected row */
47
+ selectedTint: 0.12,
48
+ menuButtonSize: 36,
49
+ menuIconSize: 16,
50
+ },
51
+ /** unread messages of a conversation, and of a closed card */
52
+ count: { radius: 9, fontSize: 11, paddingY: 1, paddingX: 6 },
53
+ sectionLabel: { fontSize: 12, letterSpacing: 0.04, gap: 6, paddingTop: 4, paddingX: 8, iconSize: 12 },
54
+ empty: { fontSize: 13, paddingY: 6, paddingX: 8 },
55
+ },
56
+ };
57
+ /** Which colour role paints what on the home page. */
58
+ export const HOME_COLORS = {
59
+ rail: { background: 'surface', border: 'border', icon: 'muted' },
60
+ tileInitials: { background: 'border', text: 'text' },
61
+ ghost: { background: 'bg', border: 'border' },
62
+ selectedBar: { background: 'accent' },
63
+ badge: { background: 'danger' },
64
+ dot: { background: 'warning', border: 'surface' },
65
+ separator: { line: 'border', pin: 'muted', pinBackground: 'surface' },
66
+ column: { background: 'bg', border: 'border' },
67
+ header: { icon: 'muted' },
68
+ selectedCard: { background: 'surface' },
69
+ links: { text: 'muted' },
70
+ meta: { text: 'muted' },
71
+ conversationRow: { preview: 'muted', time: 'muted', selectedTitle: 'accent' },
72
+ count: { background: 'accent', text: 'accentContrast' },
73
+ sectionLabel: { text: 'muted' },
74
+ };
75
+ /** The trust mark of an agent, and the verdict of its Proof of Trust: one icon and one colour per result. */
76
+ export const TRUST_MARK = {
77
+ trusted: { icon: 'shield-check', color: 'ok' },
78
+ untrusted: { icon: 'shield-alert', color: 'danger' },
79
+ unverified: { icon: 'shield-question', color: 'warning' },
80
+ };
81
+ /** The icon before a controller's name: what stands behind its agents. */
82
+ export const CONTROLLER_KIND_ICON = {
83
+ organization: 'building',
84
+ persona: 'user',
85
+ unverified: 'shield-question',
86
+ };
87
+ /**
88
+ * The time on a message and on a conversation row: the hour today, the day and the hour before. An unreadable
89
+ * date gives an empty string.
90
+ */
91
+ export function formatTime(iso, locale, now = new Date()) {
92
+ const d = new Date(iso);
93
+ if (Number.isNaN(d.getTime()))
94
+ return '';
95
+ const today = now.toDateString() === d.toDateString();
96
+ return new Intl.DateTimeFormat(locale, today
97
+ ? { hour: '2-digit', minute: '2-digit' }
98
+ : { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' }).format(d);
99
+ }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,11 @@
1
1
  export * from './agents.js';
2
+ export * from './appLinks.js';
3
+ export * from './conversationDesign.js';
2
4
  export * from './directory.js';
5
+ export * from './flag.js';
6
+ export * from './homeDesign.js';
3
7
  export * from './profileClient.js';
8
+ export * from './proofOfTrustDesign.js';
4
9
  export * from './reducer.js';
5
10
  export * from './sse.js';
6
11
  export * from './store.js';
@@ -9,7 +14,17 @@ export * from './trust.js';
9
14
  export * from './types.js';
10
15
  export declare const CLIENT: {
11
16
  readonly name: "ayni-ui";
12
- readonly version: "3.0.0";
17
+ readonly version: "3.1.0";
18
+ };
19
+ /**
20
+ * The client description sent at session creation (PROF-SESS-014): a 3.1 agent gets `platform: web` and the
21
+ * browser as `description`; a 3.0 agent gets the browser as the free-text platform it expects.
22
+ */
23
+ export declare function clientDescription(profileVersion: string | undefined, browser: string): {
24
+ name: string;
25
+ version: string;
26
+ platform: string;
27
+ description?: string;
13
28
  };
14
29
  export declare const CLOSURE_REASON_KEYS: Record<string, string>;
15
30
  /** Rail order (UI-SHELL-011): pinned in the person's order, then by most recent message, then not linked. */
@@ -47,13 +62,23 @@ export declare function controllerOf(agent: {
47
62
  logoRef?: string;
48
63
  countryCode?: string;
49
64
  };
50
- /** The flag emoji of an ISO 3166-1 alpha-2 code, or an empty string when the code is not one. */
51
- export declare function flagOf(countryCode?: string): string;
52
65
  /** Rail order for controllers (UI-SHELL-011): pinned in the person's order, then by activity, then unverified. */
66
+ /**
67
+ * The stable rail (UI-SHELL-011, D37): pinned controllers in the person's order, then the identified ones (an
68
+ * agent of theirs issued the person a credential), then the others, both in connection order, then the unverified
69
+ * tiles. Nothing here depends on activity: a message never moves a tile.
70
+ */
53
71
  export declare function orderControllers<T extends {
54
72
  did: string;
55
73
  kind: string;
56
74
  pinned: boolean;
57
75
  pinOrder: number;
58
- }>(controllers: T[], lastMessageAt: (c: T) => string): T[];
76
+ identifiedAt?: string;
77
+ }>(controllers: T[], connectedAt: (c: T) => string): T[];
78
+ /** Whether a transcript records a credential issued to the person: a completed `issue` flow card (D37). */
79
+ export declare function hasIssuedCredential(messages: {
80
+ role: string;
81
+ activityType?: string;
82
+ content?: unknown;
83
+ }[]): boolean;
59
84
  export * from './trustDetails.js';
package/dist/index.js CHANGED
@@ -1,13 +1,27 @@
1
1
  export * from './agents.js';
2
+ export * from './appLinks.js';
3
+ export * from './conversationDesign.js';
2
4
  export * from './directory.js';
5
+ export * from './flag.js';
6
+ export * from './homeDesign.js';
3
7
  export * from './profileClient.js';
8
+ export * from './proofOfTrustDesign.js';
4
9
  export * from './reducer.js';
5
10
  export * from './sse.js';
6
11
  export * from './store.js';
7
12
  export * from './sync.js';
8
13
  export * from './trust.js';
9
14
  export * from './types.js';
10
- export const CLIENT = { name: 'ayni-ui', version: '3.0.0' };
15
+ export const CLIENT = { name: 'ayni-ui', version: '3.1.0' };
16
+ /**
17
+ * The client description sent at session creation (PROF-SESS-014): a 3.1 agent gets `platform: web` and the
18
+ * browser as `description`; a 3.0 agent gets the browser as the free-text platform it expects.
19
+ */
20
+ export function clientDescription(profileVersion, browser) {
21
+ const [major, minor] = (profileVersion ?? '3.0').split('.').map(Number);
22
+ const supportsDescription = major > 3 || (major === 3 && (minor ?? 0) >= 1);
23
+ return supportsDescription ? { ...CLIENT, platform: 'web', description: browser } : { ...CLIENT, platform: browser };
24
+ }
11
25
  export const CLOSURE_REASON_KEYS = {
12
26
  user: 'closure.user',
13
27
  supersede: 'closure.supersede',
@@ -49,23 +63,28 @@ export function controllerOf(agent) {
49
63
  }
50
64
  return { did: agent.did, kind: 'unverified', name: agent.name, logoRef: agent.logoRef };
51
65
  }
52
- /** The flag emoji of an ISO 3166-1 alpha-2 code, or an empty string when the code is not one. */
53
- export function flagOf(countryCode) {
54
- const code = (countryCode ?? '').trim().toUpperCase();
55
- if (!/^[A-Z]{2}$/.test(code))
56
- return '';
57
- return String.fromCodePoint(...[...code].map((c) => 0x1f1e6 + c.charCodeAt(0) - 65));
58
- }
59
66
  /** Rail order for controllers (UI-SHELL-011): pinned in the person's order, then by activity, then unverified. */
60
- export function orderControllers(controllers, lastMessageAt) {
67
+ /**
68
+ * The stable rail (UI-SHELL-011, D37): pinned controllers in the person's order, then the identified ones (an
69
+ * agent of theirs issued the person a credential), then the others, both in connection order, then the unverified
70
+ * tiles. Nothing here depends on activity: a message never moves a tile.
71
+ */
72
+ export function orderControllers(controllers, connectedAt) {
73
+ const byConnection = (a, b) => connectedAt(a).localeCompare(connectedAt(b)) || a.did.localeCompare(b.did);
61
74
  const pinned = controllers.filter((c) => c.pinned).sort((a, b) => a.pinOrder - b.pinOrder);
62
75
  const rest = controllers.filter((c) => !c.pinned);
63
- const verified = rest
64
- .filter((c) => c.kind !== 'unverified')
65
- .sort((a, b) => lastMessageAt(b).localeCompare(lastMessageAt(a)));
66
- const unverified = rest
67
- .filter((c) => c.kind === 'unverified')
68
- .sort((a, b) => lastMessageAt(b).localeCompare(lastMessageAt(a)));
69
- return [...pinned, ...verified, ...unverified];
76
+ const identified = rest.filter((c) => c.kind !== 'unverified' && c.identifiedAt).sort(byConnection);
77
+ const others = rest.filter((c) => c.kind !== 'unverified' && !c.identifiedAt).sort(byConnection);
78
+ const unverified = rest.filter((c) => c.kind === 'unverified').sort(byConnection);
79
+ return [...pinned, ...identified, ...others, ...unverified];
80
+ }
81
+ /** Whether a transcript records a credential issued to the person: a completed `issue` flow card (D37). */
82
+ export function hasIssuedCredential(messages) {
83
+ return messages.some((m) => {
84
+ if (m.role !== 'activity' || m.activityType !== 'ayni.flow')
85
+ return false;
86
+ const c = (m.content ?? {});
87
+ return c.kind === 'issue' && c.state === 'completed';
88
+ });
70
89
  }
71
90
  export * from './trustDetails.js';