@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.
- package/README.md +14 -0
- package/dist/appLinks.d.ts +42 -0
- package/dist/appLinks.js +120 -0
- package/dist/conversationDesign.d.ts +167 -0
- package/dist/conversationDesign.js +125 -0
- package/dist/flag.d.ts +2 -0
- package/dist/flag.js +7 -0
- package/dist/homeDesign.d.ts +242 -0
- package/dist/homeDesign.js +99 -0
- package/dist/index.d.ts +29 -4
- package/dist/index.js +35 -16
- package/dist/proofOfTrustDesign.d.ts +350 -0
- package/dist/proofOfTrustDesign.js +264 -0
- package/dist/reducer.d.ts +3 -1
- package/dist/reducer.js +17 -1
- package/dist/types.d.ts +7 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,5 +10,19 @@ and sync. The browser UI in this repository and the Ayni app both build on it; n
|
|
|
10
10
|
pnpm add @ayni.org/ayni-ui-core @ayni.org/ayni-agui-profile
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
## One design, two renderers
|
|
14
|
+
|
|
15
|
+
The browser draws with CSS and the Ayni app with React Native styles. What the two must share is written here once:
|
|
16
|
+
|
|
17
|
+
| Surface | Design values and colour roles | What it shows |
|
|
18
|
+
| --- | --- | --- |
|
|
19
|
+
| A conversation | `CONVERSATION_DESIGN`, `CONVERSATION_COLORS` | `replyQuoteOf`, `reactionPillOf`, `plainText` |
|
|
20
|
+
| The home page (rail and column) | `HOME_DESIGN`, `HOME_COLORS` | `TRUST_MARK`, `CONTROLLER_KIND_ICON`, `formatTime` |
|
|
21
|
+
| The Proof of Trust | `PROOF_OF_TRUST_DESIGN`, `PROOF_OF_TRUST_COLORS` | `verdictOf`, `identityStepOf`, `accreditationLine`, `otherCredentialLine`, `claimRows`, and its English wording (`PROOF_OF_TRUST_COPY`) |
|
|
22
|
+
|
|
23
|
+
The browser's stylesheet is the reference: tests in this repository compare it, and the English translation file,
|
|
24
|
+
with these values, so a change made on one side only fails. The app builds its styles from the same values. A change
|
|
25
|
+
of look or wording on one of these surfaces starts here.
|
|
26
|
+
|
|
13
27
|
Published to npm from this repository by the `npm-publish` workflow whenever `package.json` carries a version that is
|
|
14
28
|
not on npm yet.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App links (AG-UI profile 3.1 §10, UI-LINK-001..003, D38): HTTPS links on the network's app-link domain, which is
|
|
3
|
+
* the web UI's own host. Parsing reads only the profile's parameters (`q`, a DID, a segment, `to`, `amount`, `memo`,
|
|
4
|
+
* `oob`) and ignores every other one; the UI opens the matching web route in place when one exists and shows
|
|
5
|
+
* "on your phone" with the destination named otherwise. Links navigate, never act.
|
|
6
|
+
*/
|
|
7
|
+
export type AppLinkDestination = {
|
|
8
|
+
path: 'home';
|
|
9
|
+
controller?: string;
|
|
10
|
+
} | {
|
|
11
|
+
path: 'recent';
|
|
12
|
+
segment?: 'feed';
|
|
13
|
+
} | {
|
|
14
|
+
path: 'scan';
|
|
15
|
+
} | {
|
|
16
|
+
path: 'explore';
|
|
17
|
+
q?: string;
|
|
18
|
+
create?: boolean;
|
|
19
|
+
} | {
|
|
20
|
+
path: 'wallet';
|
|
21
|
+
segment?: 'documents' | 'history' | 'card' | 'send';
|
|
22
|
+
id?: string;
|
|
23
|
+
} | {
|
|
24
|
+
path: 'service';
|
|
25
|
+
did: string;
|
|
26
|
+
} | {
|
|
27
|
+
path: 'settings';
|
|
28
|
+
section?: 'backup' | 'devices';
|
|
29
|
+
} | {
|
|
30
|
+
path: 'connect';
|
|
31
|
+
oob?: string;
|
|
32
|
+
} | {
|
|
33
|
+
path: 'unknown';
|
|
34
|
+
};
|
|
35
|
+
/** Whether `href` is an app link of this UI's app-link domain (`origin` is the UI's own origin). */
|
|
36
|
+
export declare function isAppLink(href: string, origin: string): boolean;
|
|
37
|
+
/** The destination an app link names; `unknown` for a path outside §10. Stray parameters change nothing. */
|
|
38
|
+
export declare function parseAppLink(href: string, origin: string): AppLinkDestination;
|
|
39
|
+
/** The web routes of UI-LINK-001; every other destination is shown as "on your phone". */
|
|
40
|
+
export declare function hasWebRoute(destination: AppLinkDestination): boolean;
|
|
41
|
+
/** A stable key for the destination's label (`link.destination.<key>` in the UI's strings). */
|
|
42
|
+
export declare function destinationKey(destination: AppLinkDestination): string;
|
package/dist/appLinks.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App links (AG-UI profile 3.1 §10, UI-LINK-001..003, D38): HTTPS links on the network's app-link domain, which is
|
|
3
|
+
* the web UI's own host. Parsing reads only the profile's parameters (`q`, a DID, a segment, `to`, `amount`, `memo`,
|
|
4
|
+
* `oob`) and ignores every other one; the UI opens the matching web route in place when one exists and shows
|
|
5
|
+
* "on your phone" with the destination named otherwise. Links navigate, never act.
|
|
6
|
+
*/
|
|
7
|
+
/** Whether `href` is an app link of this UI's app-link domain (`origin` is the UI's own origin). */
|
|
8
|
+
export function isAppLink(href, origin) {
|
|
9
|
+
try {
|
|
10
|
+
const url = new URL(href, origin);
|
|
11
|
+
return url.origin === origin && (url.pathname === '/app' || url.pathname.startsWith('/app/'));
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** The destination an app link names; `unknown` for a path outside §10. Stray parameters change nothing. */
|
|
18
|
+
export function parseAppLink(href, origin) {
|
|
19
|
+
let url;
|
|
20
|
+
try {
|
|
21
|
+
url = new URL(href, origin);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return { path: 'unknown' };
|
|
25
|
+
}
|
|
26
|
+
const parts = url.pathname
|
|
27
|
+
.replace(/\/+$/, '')
|
|
28
|
+
.split('/')
|
|
29
|
+
.filter(Boolean)
|
|
30
|
+
.map((p) => decodeURIComponent(p));
|
|
31
|
+
if (parts[0] !== 'app')
|
|
32
|
+
return { path: 'unknown' };
|
|
33
|
+
const [, head, second, third] = parts;
|
|
34
|
+
const params = url.searchParams;
|
|
35
|
+
switch (head) {
|
|
36
|
+
case 'home':
|
|
37
|
+
return parts.length <= 3 ? { path: 'home', ...(second ? { controller: second } : {}) } : { path: 'unknown' };
|
|
38
|
+
case 'recent':
|
|
39
|
+
if (parts.length === 2)
|
|
40
|
+
return { path: 'recent' };
|
|
41
|
+
return parts.length === 3 && second === 'feed' ? { path: 'recent', segment: 'feed' } : { path: 'unknown' };
|
|
42
|
+
case 'scan':
|
|
43
|
+
return parts.length === 2 ? { path: 'scan' } : { path: 'unknown' };
|
|
44
|
+
case 'explore': {
|
|
45
|
+
if (parts.length === 3 && second === 'create')
|
|
46
|
+
return { path: 'explore', create: true };
|
|
47
|
+
if (parts.length !== 2)
|
|
48
|
+
return { path: 'unknown' };
|
|
49
|
+
const q = params.get('q')?.trim();
|
|
50
|
+
return q ? { path: 'explore', q } : { path: 'explore' };
|
|
51
|
+
}
|
|
52
|
+
case 'wallet': {
|
|
53
|
+
if (parts.length === 2)
|
|
54
|
+
return { path: 'wallet' };
|
|
55
|
+
if (parts.length === 3 && (second === 'documents' || second === 'history' || second === 'send'))
|
|
56
|
+
return { path: 'wallet', segment: second };
|
|
57
|
+
if (parts.length === 4 && second === 'card' && third)
|
|
58
|
+
return { path: 'wallet', segment: 'card', id: third };
|
|
59
|
+
return { path: 'unknown' };
|
|
60
|
+
}
|
|
61
|
+
case 'service':
|
|
62
|
+
return parts.length === 3 && second?.startsWith('did:') ? { path: 'service', did: second } : { path: 'unknown' };
|
|
63
|
+
case 'settings':
|
|
64
|
+
if (parts.length === 2)
|
|
65
|
+
return { path: 'settings' };
|
|
66
|
+
return parts.length === 3 && (second === 'backup' || second === 'devices')
|
|
67
|
+
? { path: 'settings', section: second }
|
|
68
|
+
: { path: 'unknown' };
|
|
69
|
+
case 'connect': {
|
|
70
|
+
if (parts.length !== 2)
|
|
71
|
+
return { path: 'unknown' };
|
|
72
|
+
const oob = params.get('oob')?.trim();
|
|
73
|
+
return oob ? { path: 'connect', oob } : { path: 'connect' };
|
|
74
|
+
}
|
|
75
|
+
default:
|
|
76
|
+
return { path: 'unknown' };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** The web routes of UI-LINK-001; every other destination is shown as "on your phone". */
|
|
80
|
+
export function hasWebRoute(destination) {
|
|
81
|
+
switch (destination.path) {
|
|
82
|
+
case 'home':
|
|
83
|
+
case 'service':
|
|
84
|
+
case 'connect':
|
|
85
|
+
return true;
|
|
86
|
+
case 'explore':
|
|
87
|
+
return !destination.create;
|
|
88
|
+
case 'settings':
|
|
89
|
+
return destination.section !== 'backup';
|
|
90
|
+
default:
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** A stable key for the destination's label (`link.destination.<key>` in the UI's strings). */
|
|
95
|
+
export function destinationKey(destination) {
|
|
96
|
+
switch (destination.path) {
|
|
97
|
+
case 'recent':
|
|
98
|
+
return destination.segment === 'feed' ? 'recentFeed' : 'recent';
|
|
99
|
+
case 'wallet':
|
|
100
|
+
return destination.segment === 'documents'
|
|
101
|
+
? 'walletDocuments'
|
|
102
|
+
: destination.segment === 'history'
|
|
103
|
+
? 'walletHistory'
|
|
104
|
+
: destination.segment === 'card'
|
|
105
|
+
? 'walletCard'
|
|
106
|
+
: destination.segment === 'send'
|
|
107
|
+
? 'walletSend'
|
|
108
|
+
: 'wallet';
|
|
109
|
+
case 'explore':
|
|
110
|
+
return destination.create ? 'exploreCreate' : 'explore';
|
|
111
|
+
case 'settings':
|
|
112
|
+
return destination.section === 'backup'
|
|
113
|
+
? 'settingsBackup'
|
|
114
|
+
: destination.section === 'devices'
|
|
115
|
+
? 'settingsDevices'
|
|
116
|
+
: 'settings';
|
|
117
|
+
default:
|
|
118
|
+
return destination.path;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The design of a conversation, written once for the two renderers of the Ayni UI: the browser (CSS) and the Ayni
|
|
3
|
+
* app (React Native styles). The numbers are CSS pixels, which React Native reads as density-independent points.
|
|
4
|
+
*
|
|
5
|
+
* The browser's stylesheet is the reference: a test in this repository compares it with these values, so the two
|
|
6
|
+
* cannot drift apart. The app builds its styles from them. A change of the conversation's look starts here.
|
|
7
|
+
*/
|
|
8
|
+
export declare const CONVERSATION_DESIGN: {
|
|
9
|
+
readonly transcript: {
|
|
10
|
+
readonly paddingTop: 16;
|
|
11
|
+
readonly paddingX: 20;
|
|
12
|
+
readonly paddingBottom: 20;
|
|
13
|
+
readonly gap: 18;
|
|
14
|
+
};
|
|
15
|
+
readonly bubble: {
|
|
16
|
+
/** a bubble never grows wider than this, nor than its row */
|
|
17
|
+
readonly maxWidth: 640;
|
|
18
|
+
readonly paddingTop: 9;
|
|
19
|
+
readonly paddingX: 14;
|
|
20
|
+
readonly paddingBottom: 8;
|
|
21
|
+
readonly radius: 16;
|
|
22
|
+
/** the one tighter corner: bottom right on the person's messages, top left on the agent's */
|
|
23
|
+
readonly tailRadius: 4;
|
|
24
|
+
/** between the quote, the body and the meta line */
|
|
25
|
+
readonly gap: 4;
|
|
26
|
+
/** room for the reaction pill, which overlaps the bubble's bottom edge */
|
|
27
|
+
readonly reactedPaddingBottom: 16;
|
|
28
|
+
readonly reactedMarginBottom: 12;
|
|
29
|
+
};
|
|
30
|
+
/** a system line: centred, no bubble */
|
|
31
|
+
readonly system: {
|
|
32
|
+
readonly fontSize: 13;
|
|
33
|
+
};
|
|
34
|
+
/** the time and "interrupted", at the end of the bubble */
|
|
35
|
+
readonly meta: {
|
|
36
|
+
readonly fontSize: 11;
|
|
37
|
+
readonly gap: 6;
|
|
38
|
+
readonly userTextOpacity: 0.78;
|
|
39
|
+
};
|
|
40
|
+
/** the message a reply answers, at the top of the bubble */
|
|
41
|
+
readonly quote: {
|
|
42
|
+
readonly paddingY: 5;
|
|
43
|
+
readonly paddingX: 10;
|
|
44
|
+
readonly borderWidth: 3;
|
|
45
|
+
readonly radius: 6;
|
|
46
|
+
readonly fontSize: 13;
|
|
47
|
+
readonly gap: 1;
|
|
48
|
+
readonly marginBottom: 2;
|
|
49
|
+
readonly whoWeight: 600;
|
|
50
|
+
readonly textOpacity: 0.9;
|
|
51
|
+
/** the share of the text colour mixed into a transparent background */
|
|
52
|
+
readonly tint: 0.07;
|
|
53
|
+
/** on the person's bubble: the share of the contrast colour in the background and in the border */
|
|
54
|
+
readonly userTint: 0.16;
|
|
55
|
+
readonly userBorderOpacity: 0.7;
|
|
56
|
+
};
|
|
57
|
+
/** the reactions of a message: one pill on the inner bottom corner, the same on both sides (UI-CONV-011) */
|
|
58
|
+
readonly reactionPill: {
|
|
59
|
+
readonly height: 26;
|
|
60
|
+
readonly paddingX: 8;
|
|
61
|
+
readonly radius: 13;
|
|
62
|
+
readonly borderWidth: 1;
|
|
63
|
+
readonly fontSize: 14;
|
|
64
|
+
readonly gap: 3;
|
|
65
|
+
/** how far the pill hangs below the bubble, and how far it steps out sideways */
|
|
66
|
+
readonly overlap: 13;
|
|
67
|
+
readonly outset: 6;
|
|
68
|
+
/** the share of the accent mixed into the background of a pill that holds the person's own reaction */
|
|
69
|
+
readonly mineTint: 0.12;
|
|
70
|
+
};
|
|
71
|
+
/** the button that returns to the latest message */
|
|
72
|
+
readonly latestButton: {
|
|
73
|
+
readonly size: 36;
|
|
74
|
+
readonly right: 20;
|
|
75
|
+
readonly bottom: 12;
|
|
76
|
+
readonly awayBeyond: 320;
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
export type ConversationDesign = typeof CONVERSATION_DESIGN;
|
|
80
|
+
/** The colour roles both palettes define, in the browser as custom properties and in the app as theme colours. */
|
|
81
|
+
export type ColorRole = 'bg' | 'surface' | 'text' | 'muted' | 'border' | 'accent' | 'accentContrast' | 'warning' | 'ok' | 'danger';
|
|
82
|
+
/** Which colour role paints what in a conversation. */
|
|
83
|
+
export declare const CONVERSATION_COLORS: {
|
|
84
|
+
readonly userBubble: {
|
|
85
|
+
readonly background: "accent";
|
|
86
|
+
readonly text: "accentContrast";
|
|
87
|
+
};
|
|
88
|
+
readonly assistantBubble: {
|
|
89
|
+
readonly background: "surface";
|
|
90
|
+
readonly text: "text";
|
|
91
|
+
};
|
|
92
|
+
readonly system: {
|
|
93
|
+
readonly text: "muted";
|
|
94
|
+
};
|
|
95
|
+
readonly meta: {
|
|
96
|
+
readonly text: "muted";
|
|
97
|
+
};
|
|
98
|
+
readonly interrupted: {
|
|
99
|
+
readonly text: "warning";
|
|
100
|
+
};
|
|
101
|
+
readonly quote: {
|
|
102
|
+
readonly border: "accent";
|
|
103
|
+
readonly who: "accent";
|
|
104
|
+
};
|
|
105
|
+
readonly reactionPill: {
|
|
106
|
+
readonly background: "bg";
|
|
107
|
+
readonly border: "border";
|
|
108
|
+
readonly text: "text";
|
|
109
|
+
readonly mineBorder: "accent";
|
|
110
|
+
};
|
|
111
|
+
readonly latestButton: {
|
|
112
|
+
readonly background: "bg";
|
|
113
|
+
readonly border: "border";
|
|
114
|
+
readonly icon: "text";
|
|
115
|
+
};
|
|
116
|
+
};
|
|
117
|
+
/** The side a message sits on, and the corner its bubble tightens. */
|
|
118
|
+
export declare function bubbleSide(role: 'user' | 'assistant' | 'system'): 'mine' | 'theirs' | 'system';
|
|
119
|
+
/** The quick row of the reaction picker (UI-CONV-017): the first six, with their English names. */
|
|
120
|
+
export declare const QUICK_REACTIONS: readonly [{
|
|
121
|
+
readonly e: "❤️";
|
|
122
|
+
readonly name: "heart";
|
|
123
|
+
}, {
|
|
124
|
+
readonly e: "👍";
|
|
125
|
+
readonly name: "thumbs up";
|
|
126
|
+
}, {
|
|
127
|
+
readonly e: "👎";
|
|
128
|
+
readonly name: "thumbs down";
|
|
129
|
+
}, {
|
|
130
|
+
readonly e: "😂";
|
|
131
|
+
readonly name: "laughing";
|
|
132
|
+
}, {
|
|
133
|
+
readonly e: "😮";
|
|
134
|
+
readonly name: "surprised";
|
|
135
|
+
}, {
|
|
136
|
+
readonly e: "😢";
|
|
137
|
+
readonly name: "sad";
|
|
138
|
+
}];
|
|
139
|
+
/** Markdown markers stripped: for the clipboard and for the quote of a reply. */
|
|
140
|
+
export declare function plainText(markdown: string): string;
|
|
141
|
+
/** The longest quote a reply carries. */
|
|
142
|
+
export declare const REPLY_QUOTE_LENGTH = 200;
|
|
143
|
+
/** The quote a reply carries (PROF-RUN-009): the message as plain text, shortened. */
|
|
144
|
+
export declare function replyQuoteOf(message: {
|
|
145
|
+
id: string;
|
|
146
|
+
role: 'user' | 'assistant' | 'system';
|
|
147
|
+
content: string;
|
|
148
|
+
}): {
|
|
149
|
+
id: string;
|
|
150
|
+
text: string;
|
|
151
|
+
role: 'user' | 'assistant';
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
154
|
+
* The reactions of a message as its one pill shows them (UI-CONV-011): the person's own first, then the others in
|
|
155
|
+
* the order they came. `mine` is the person's emoji, when they reacted: the pill is then a control that changes it.
|
|
156
|
+
*/
|
|
157
|
+
export declare function reactionPillOf(reactions: readonly {
|
|
158
|
+
emoji: string;
|
|
159
|
+
by: string;
|
|
160
|
+
}[] | undefined, me: string | undefined): {
|
|
161
|
+
emojis: {
|
|
162
|
+
emoji: string;
|
|
163
|
+
by: string;
|
|
164
|
+
own: boolean;
|
|
165
|
+
}[];
|
|
166
|
+
mine?: string;
|
|
167
|
+
} | null;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The design of a conversation, written once for the two renderers of the Ayni UI: the browser (CSS) and the Ayni
|
|
3
|
+
* app (React Native styles). The numbers are CSS pixels, which React Native reads as density-independent points.
|
|
4
|
+
*
|
|
5
|
+
* The browser's stylesheet is the reference: a test in this repository compares it with these values, so the two
|
|
6
|
+
* cannot drift apart. The app builds its styles from them. A change of the conversation's look starts here.
|
|
7
|
+
*/
|
|
8
|
+
export const CONVERSATION_DESIGN = {
|
|
9
|
+
transcript: { paddingTop: 16, paddingX: 20, paddingBottom: 20, gap: 18 },
|
|
10
|
+
bubble: {
|
|
11
|
+
/** a bubble never grows wider than this, nor than its row */
|
|
12
|
+
maxWidth: 640,
|
|
13
|
+
paddingTop: 9,
|
|
14
|
+
paddingX: 14,
|
|
15
|
+
paddingBottom: 8,
|
|
16
|
+
radius: 16,
|
|
17
|
+
/** the one tighter corner: bottom right on the person's messages, top left on the agent's */
|
|
18
|
+
tailRadius: 4,
|
|
19
|
+
/** between the quote, the body and the meta line */
|
|
20
|
+
gap: 4,
|
|
21
|
+
/** room for the reaction pill, which overlaps the bubble's bottom edge */
|
|
22
|
+
reactedPaddingBottom: 16,
|
|
23
|
+
reactedMarginBottom: 12,
|
|
24
|
+
},
|
|
25
|
+
/** a system line: centred, no bubble */
|
|
26
|
+
system: { fontSize: 13 },
|
|
27
|
+
/** the time and "interrupted", at the end of the bubble */
|
|
28
|
+
meta: { fontSize: 11, gap: 6, userTextOpacity: 0.78 },
|
|
29
|
+
/** the message a reply answers, at the top of the bubble */
|
|
30
|
+
quote: {
|
|
31
|
+
paddingY: 5,
|
|
32
|
+
paddingX: 10,
|
|
33
|
+
borderWidth: 3,
|
|
34
|
+
radius: 6,
|
|
35
|
+
fontSize: 13,
|
|
36
|
+
gap: 1,
|
|
37
|
+
marginBottom: 2,
|
|
38
|
+
whoWeight: 600,
|
|
39
|
+
textOpacity: 0.9,
|
|
40
|
+
/** the share of the text colour mixed into a transparent background */
|
|
41
|
+
tint: 0.07,
|
|
42
|
+
/** on the person's bubble: the share of the contrast colour in the background and in the border */
|
|
43
|
+
userTint: 0.16,
|
|
44
|
+
userBorderOpacity: 0.7,
|
|
45
|
+
},
|
|
46
|
+
/** the reactions of a message: one pill on the inner bottom corner, the same on both sides (UI-CONV-011) */
|
|
47
|
+
reactionPill: {
|
|
48
|
+
height: 26,
|
|
49
|
+
paddingX: 8,
|
|
50
|
+
radius: 13,
|
|
51
|
+
borderWidth: 1,
|
|
52
|
+
fontSize: 14,
|
|
53
|
+
gap: 3,
|
|
54
|
+
/** how far the pill hangs below the bubble, and how far it steps out sideways */
|
|
55
|
+
overlap: 13,
|
|
56
|
+
outset: 6,
|
|
57
|
+
/** the share of the accent mixed into the background of a pill that holds the person's own reaction */
|
|
58
|
+
mineTint: 0.12,
|
|
59
|
+
},
|
|
60
|
+
/** the button that returns to the latest message */
|
|
61
|
+
latestButton: { size: 36, right: 20, bottom: 12, awayBeyond: 320 },
|
|
62
|
+
};
|
|
63
|
+
/** Which colour role paints what in a conversation. */
|
|
64
|
+
export const CONVERSATION_COLORS = {
|
|
65
|
+
userBubble: { background: 'accent', text: 'accentContrast' },
|
|
66
|
+
assistantBubble: { background: 'surface', text: 'text' },
|
|
67
|
+
system: { text: 'muted' },
|
|
68
|
+
meta: { text: 'muted' },
|
|
69
|
+
interrupted: { text: 'warning' },
|
|
70
|
+
quote: { border: 'accent', who: 'accent' },
|
|
71
|
+
reactionPill: { background: 'bg', border: 'border', text: 'text', mineBorder: 'accent' },
|
|
72
|
+
latestButton: { background: 'bg', border: 'border', icon: 'text' },
|
|
73
|
+
};
|
|
74
|
+
/** The side a message sits on, and the corner its bubble tightens. */
|
|
75
|
+
export function bubbleSide(role) {
|
|
76
|
+
return role === 'user' ? 'mine' : role === 'assistant' ? 'theirs' : 'system';
|
|
77
|
+
}
|
|
78
|
+
/** The quick row of the reaction picker (UI-CONV-017): the first six, with their English names. */
|
|
79
|
+
export const QUICK_REACTIONS = [
|
|
80
|
+
{ e: '❤️', name: 'heart' },
|
|
81
|
+
{ e: '👍', name: 'thumbs up' },
|
|
82
|
+
{ e: '👎', name: 'thumbs down' },
|
|
83
|
+
{ e: '😂', name: 'laughing' },
|
|
84
|
+
{ e: '😮', name: 'surprised' },
|
|
85
|
+
{ e: '😢', name: 'sad' },
|
|
86
|
+
];
|
|
87
|
+
/** Markdown markers stripped: for the clipboard and for the quote of a reply. */
|
|
88
|
+
export function plainText(markdown) {
|
|
89
|
+
return markdown
|
|
90
|
+
.replace(/<\/?(u|b|i|em|strong|s)>/g, '')
|
|
91
|
+
.replace(/\*\*\*([^*]+)\*\*\*/g, '$1')
|
|
92
|
+
.replace(/\*\*([^*]+)\*\*/g, '$1')
|
|
93
|
+
.replace(/\*([^*\n]+)\*/g, '$1')
|
|
94
|
+
.replace(/~~([^~]+)~~/g, '$1')
|
|
95
|
+
.replace(/`([^`]+)`/g, '$1')
|
|
96
|
+
.replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g, '$1 ($2)')
|
|
97
|
+
.replace(/^#{1,6}\s+/gm, '')
|
|
98
|
+
.replace(/^>\s?/gm, '')
|
|
99
|
+
.replace(/^\s*([-*_])\s*\1\s*\1[\s\-*_]*$/gm, '')
|
|
100
|
+
.replace(/^[-*+]\s+/gm, '• ');
|
|
101
|
+
}
|
|
102
|
+
/** The longest quote a reply carries. */
|
|
103
|
+
export const REPLY_QUOTE_LENGTH = 200;
|
|
104
|
+
/** The quote a reply carries (PROF-RUN-009): the message as plain text, shortened. */
|
|
105
|
+
export function replyQuoteOf(message) {
|
|
106
|
+
return {
|
|
107
|
+
id: message.id,
|
|
108
|
+
text: plainText(message.content).slice(0, REPLY_QUOTE_LENGTH),
|
|
109
|
+
role: message.role === 'user' ? 'user' : 'assistant',
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The reactions of a message as its one pill shows them (UI-CONV-011): the person's own first, then the others in
|
|
114
|
+
* the order they came. `mine` is the person's emoji, when they reacted: the pill is then a control that changes it.
|
|
115
|
+
*/
|
|
116
|
+
export function reactionPillOf(reactions, me) {
|
|
117
|
+
if (!reactions?.length)
|
|
118
|
+
return null;
|
|
119
|
+
const own = reactions.find((r) => r.by === me);
|
|
120
|
+
const others = reactions.filter((r) => r.by !== me);
|
|
121
|
+
return {
|
|
122
|
+
emojis: [...(own ? [{ ...own, own: true }] : []), ...others.map((r) => ({ ...r, own: false }))],
|
|
123
|
+
mine: own?.emoji,
|
|
124
|
+
};
|
|
125
|
+
}
|
package/dist/flag.d.ts
ADDED
package/dist/flag.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** The flag emoji of an ISO 3166-1 alpha-2 code, or an empty string when the code is not one. */
|
|
2
|
+
export function flagOf(countryCode) {
|
|
3
|
+
const code = (countryCode ?? '').trim().toUpperCase();
|
|
4
|
+
if (!/^[A-Z]{2}$/.test(code))
|
|
5
|
+
return '';
|
|
6
|
+
return String.fromCodePoint(...[...code].map((c) => 0x1f1e6 + c.charCodeAt(0) - 65));
|
|
7
|
+
}
|