@astrosheep/square 0.4.5 → 0.4.6
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/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useMemo, type ReactNode } from "react";
|
|
2
|
-
import { Platform,
|
|
2
|
+
import { Platform, Text, View, type TextStyle } from "react-native";
|
|
3
3
|
import type { PluginTheme } from "@getpaseo/plugin";
|
|
4
4
|
import { Lexer, lexer, type Token, type Tokens } from "marked";
|
|
5
5
|
|
|
@@ -10,7 +10,7 @@ function makeStyles(colors: PluginTheme["colors"]) {
|
|
|
10
10
|
block: { gap: 6 },
|
|
11
11
|
text: { color: colors.foreground, fontSize: 13, lineHeight: 19 },
|
|
12
12
|
lead: { color: colors.accent, fontWeight: "600" as const },
|
|
13
|
-
strong: { fontWeight: "
|
|
13
|
+
strong: { fontWeight: "600" as const, color: colors.foreground },
|
|
14
14
|
em: { color: colors.foregroundMuted, fontStyle: "italic" as const },
|
|
15
15
|
del: { color: colors.foregroundMuted, textDecorationLine: "line-through" as const },
|
|
16
16
|
link: { color: colors.accent, textDecorationLine: "underline" as const },
|
|
@@ -33,6 +33,10 @@ type Styles = ReturnType<typeof makeStyles>;
|
|
|
33
33
|
/** Children of a token, if it has any: `Token` is a union, so the optional field needs one cast. */
|
|
34
34
|
const children = (node: Token | { tokens?: Token[] }): Token[] => (node as { tokens?: Token[] }).tokens ?? [];
|
|
35
35
|
|
|
36
|
+
const unescape = (text: string): string =>
|
|
37
|
+
text.replace(/&(lt|gt|quot|#39|amp);/g, (_, entity: string) =>
|
|
38
|
+
entity === "lt" ? "<" : entity === "gt" ? ">" : entity === "quot" ? '"' : entity === "#39" ? "'" : "&");
|
|
39
|
+
|
|
36
40
|
function renderInlines(tokens: Token[], styles: Styles, prefix: string, extra?: TextStyle): ReactNode[] {
|
|
37
41
|
return tokens.map((token, index) => {
|
|
38
42
|
const key = `${prefix}-${index}`;
|
|
@@ -50,7 +54,7 @@ function renderInlines(tokens: Token[], styles: Styles, prefix: string, extra?:
|
|
|
50
54
|
default: {
|
|
51
55
|
const inline = token as Tokens.Generic;
|
|
52
56
|
if (inline.tokens !== undefined) return nested({});
|
|
53
|
-
return inline.text ?? inline.raw;
|
|
57
|
+
return unescape(inline.text ?? inline.raw);
|
|
54
58
|
}
|
|
55
59
|
}
|
|
56
60
|
});
|
|
@@ -149,41 +153,20 @@ export function SquareInline({ text, theme, numberOfLines, lead }: {
|
|
|
149
153
|
);
|
|
150
154
|
}
|
|
151
155
|
|
|
152
|
-
/** A paragraph that is one *emphasis* and nothing else is an action, not a line. */
|
|
153
|
-
const isAction = (token: Token): token is Tokens.Paragraph =>
|
|
154
|
-
token.type === "paragraph" && children(token).length === 1 && children(token)[0]!.type === "em";
|
|
155
|
-
|
|
156
|
-
/** A bell or an action is stage direction: a caption on a hairline across the full width. */
|
|
157
|
-
export function SquareCue({ label, theme }: { label: ReactNode; theme: PluginTheme }) {
|
|
158
|
-
const rule = { flex: 1, height: StyleSheet.hairlineWidth, backgroundColor: theme.colors.border };
|
|
159
|
-
return (
|
|
160
|
-
<View style={{ flexDirection: "row", alignItems: "center", gap: 8, marginVertical: 2 }}>
|
|
161
|
-
<View style={rule} />
|
|
162
|
-
<Text style={{ color: theme.colors.foregroundMuted, fontSize: 11.5, lineHeight: 16, letterSpacing: 0.4 }}>{label}</Text>
|
|
163
|
-
<View style={rule} />
|
|
164
|
-
</View>
|
|
165
|
-
);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
156
|
/**
|
|
169
157
|
* A body of Markdown: no envelope — the words only, at full width.
|
|
170
|
-
* `lead` runs the author into the first line
|
|
158
|
+
* `lead` runs the author into the first line.
|
|
171
159
|
*/
|
|
172
|
-
export function SquareMarkdownBody({ text, theme, lead
|
|
160
|
+
export function SquareMarkdownBody({ text, theme, lead }: {
|
|
173
161
|
text: string;
|
|
174
162
|
theme: PluginTheme;
|
|
175
163
|
lead?: string;
|
|
176
|
-
author?: string;
|
|
177
164
|
}) {
|
|
178
165
|
const styles = useMemo(() => makeStyles(theme.colors), [theme.colors]);
|
|
179
166
|
const tokens = useMemo(() => lexer(text).filter(token => token.type !== "space"), [text]);
|
|
180
167
|
let pending = lead;
|
|
181
168
|
const nodes = tokens.map((token, index) => {
|
|
182
169
|
const key = `body-${index}`;
|
|
183
|
-
if (isAction(token)) {
|
|
184
|
-
const words = renderInlines(children(children(token)[0]!), styles, key);
|
|
185
|
-
return <SquareCue key={key} theme={theme} label={author === undefined ? words : <>{author} {words}</>} />;
|
|
186
|
-
}
|
|
187
170
|
if (pending === undefined) return renderBlocks([token], styles, key);
|
|
188
171
|
const name = pending;
|
|
189
172
|
pending = undefined;
|
|
@@ -3,7 +3,7 @@ import { Icon } from "@getpaseo/plugin/client/react-native";
|
|
|
3
3
|
import { useMemo, useState } from "react";
|
|
4
4
|
import { Pressable, ScrollView, Text, View } from "react-native";
|
|
5
5
|
import { COLLAPSED_PREVIEW_MAX_CHARS, outputPreview, type SquareMessageData } from "../shared/message";
|
|
6
|
-
import { MONO,
|
|
6
|
+
import { MONO, SquareInline, SquareMarkdownBody } from "./markdown";
|
|
7
7
|
import { squarePalette } from "./palette";
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -12,7 +12,6 @@ import { squarePalette } from "./palette";
|
|
|
12
12
|
* Header (both states): ■ origin · authors … refs · how many.
|
|
13
13
|
* Collapsed: two preview lines. Expanded: the whole scene, at full width.
|
|
14
14
|
* Each structured message keeps its own author, reference and body.
|
|
15
|
-
* A badge or an *action* is stage direction, a caption on a hairline.
|
|
16
15
|
*/
|
|
17
16
|
export function SquareMessage({ item, theme, layout }: PluginTimelineItemProps<SquareMessageData>) {
|
|
18
17
|
const [expanded, setExpanded] = useState(false);
|
|
@@ -38,7 +37,7 @@ export function SquareMessage({ item, theme, layout }: PluginTimelineItemProps<S
|
|
|
38
37
|
// On web the header brightens its details under the cursor; touch keeps them quiet.
|
|
39
38
|
const detail = layout.platform === "web" && hovered ? foreground : muted;
|
|
40
39
|
const row = { flexDirection: "row" as const, alignItems: "center" as const, gap: 6 };
|
|
41
|
-
const
|
|
40
|
+
const guide = { marginLeft: 2.5, paddingLeft: palette.quote.paddingLeft, borderLeftWidth: palette.quote.borderLeftWidth, borderLeftColor: palette.quote.borderLeftColor };
|
|
42
41
|
|
|
43
42
|
return (
|
|
44
43
|
<View style={[
|
|
@@ -66,22 +65,19 @@ export function SquareMessage({ item, theme, layout }: PluginTimelineItemProps<S
|
|
|
66
65
|
<Icon name={expanded ? "ChevronUp" : "ChevronDown"} size={10} color={detail} />
|
|
67
66
|
</View>
|
|
68
67
|
{!expanded && preview !== undefined && preview !== "" && (
|
|
69
|
-
<View style={{ ...
|
|
68
|
+
<View style={{ ...guide, marginTop: 5 }}>
|
|
70
69
|
<SquareInline text={preview} theme={palette.theme} numberOfLines={2} lead={chorus && latest && "author" in latest ? latest.author : undefined} />
|
|
71
70
|
</View>
|
|
72
71
|
)}
|
|
73
72
|
</Pressable>
|
|
74
73
|
{expanded && (
|
|
75
|
-
<ScrollView style={{ maxHeight: layout.compact ? 280 : 400, marginTop:
|
|
76
|
-
<View style={{ ...
|
|
77
|
-
{items === undefined ? <SquareMarkdownBody text={text ?? ""} theme={palette.theme} /> : items.map((entry, index) =>
|
|
78
|
-
<
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
? <SquareCue theme={palette.theme} label={[[entry.author, entry.badge].filter(Boolean).join(" "), entry.origin, entry.ref].filter(Boolean).join(" · ")} />
|
|
83
|
-
: <Text style={{ color: muted, fontSize: 11, fontFamily: MONO }}>{[entry.origin, entry.ref].filter(Boolean).join(" · ")}</Text>}
|
|
84
|
-
<SquareMarkdownBody text={entry.body} theme={palette.theme} lead={chorus ? entry.author : undefined} author={entry.author} />
|
|
74
|
+
<ScrollView style={{ maxHeight: layout.compact ? 280 : 400, marginTop: 5 }} nestedScrollEnabled>
|
|
75
|
+
<View style={{ ...guide, gap: 7 }}>
|
|
76
|
+
{items === undefined ? <SquareMarkdownBody text={text ?? ""} theme={palette.theme} /> : items.map((entry, index) => (
|
|
77
|
+
<View key={index} style={index > 0 ? { paddingTop: 7, borderTopWidth: palette.card.borderWidth, borderTopColor: border } : undefined}>
|
|
78
|
+
{"text" in entry
|
|
79
|
+
? <SquareMarkdownBody text={entry.text} theme={palette.theme} />
|
|
80
|
+
: <SquareMarkdownBody text={entry.body} theme={palette.theme} lead={chorus ? entry.author : undefined} />}
|
|
85
81
|
</View>
|
|
86
82
|
))}
|
|
87
83
|
</View>
|
package/plugins/pi/square-pi.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Markdown } from '@earendil-works/pi-tui';
|
|
1
|
+
import { Box, Markdown, truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
|
|
2
2
|
import { z } from 'zod/v4';
|
|
3
3
|
|
|
4
4
|
import { Square, discover, squareClock } from '../../dist/square-api.js';
|
|
@@ -17,16 +17,90 @@ function displayCard(card) {
|
|
|
17
17
|
return isCardPresentation(card.details) ? card.details : { text: card.text };
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
const fg = (theme, name) => (text) => theme.fg?.(name, text) ?? text;
|
|
21
|
+
const style = (theme, name) => (text) => theme[name]?.(text) ?? text;
|
|
22
|
+
|
|
23
|
+
function markdownTheme(theme) {
|
|
24
|
+
const color = (name) => fg(theme, name);
|
|
25
|
+
return {
|
|
25
26
|
heading: color('mdHeading'), link: color('mdLink'), linkUrl: color('mdLinkUrl'), code: color('mdCode'),
|
|
26
27
|
codeBlock: color('mdCodeBlock'), codeBlockBorder: color('mdCodeBlockBorder'), quote: color('mdQuote'),
|
|
27
28
|
quoteBorder: color('mdQuoteBorder'), hr: color('mdHr'), listBullet: color('mdListBullet'),
|
|
28
|
-
bold: style('bold'), italic:
|
|
29
|
-
|
|
29
|
+
bold: style(theme, 'bold'), italic: (text) => fg(theme, 'muted')(style(theme, 'italic')(text)),
|
|
30
|
+
underline: style(theme, 'underline'), strikethrough: style(theme, 'strikethrough'),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Keep the short references; the long origin is the part that may ellipsize. */
|
|
35
|
+
function cardHeader(left, right, width) {
|
|
36
|
+
if (!right) return truncateToWidth(left, width);
|
|
37
|
+
const rightWidth = visibleWidth(right);
|
|
38
|
+
if (rightWidth + 4 >= width) return truncateToWidth(left, width);
|
|
39
|
+
const leftWidth = width - rightWidth - 2;
|
|
40
|
+
return `${truncateToWidth(left, leftWidth)}${' '.repeat(width - Math.min(visibleWidth(left), leftWidth) - rightWidth)}${right}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const escapeMarkdown = text => text.replace(/[!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~]/g, '\\$&');
|
|
44
|
+
const shortOrigin = origin => origin.split(/[\\/]/).filter(Boolean).at(-1) || origin;
|
|
45
|
+
|
|
46
|
+
/** The same quiet quote guide in collapsed and expanded states. */
|
|
47
|
+
function guidedMarkdown(text, theme, { preview = false, lead } = {}) {
|
|
48
|
+
const source = lead ? `**${escapeMarkdown(lead)}** ${text}` : text;
|
|
49
|
+
const markdown = new Markdown(source, 0, 0, markdownTheme(theme), { color: fg(theme, 'customMessageText') });
|
|
50
|
+
const guide = fg(theme, 'mdQuoteBorder')('│ ');
|
|
51
|
+
return {
|
|
52
|
+
invalidate() { markdown.invalidate(); },
|
|
53
|
+
render(width) {
|
|
54
|
+
const contentWidth = Math.max(1, width - 2);
|
|
55
|
+
let lines = markdown.render(contentWidth);
|
|
56
|
+
if (preview) {
|
|
57
|
+
const visible = lines.filter(line => line.trim() !== '');
|
|
58
|
+
const clipped = visible.length > 2 || text.length >= 1_000;
|
|
59
|
+
lines = visible.slice(0, 2);
|
|
60
|
+
if (clipped && lines.length) lines[lines.length - 1] = truncateToWidth(`${lines.at(-1).trimEnd()}${fg(theme, 'dim')('…')}`, contentWidth);
|
|
61
|
+
}
|
|
62
|
+
return lines.map(line => guide + line);
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** People get the producer's display facts, never the model-facing reminder envelope. */
|
|
68
|
+
export function renderSquareCard(message, { expanded, outputPad = 1 }, theme) {
|
|
69
|
+
if (typeof message.content !== 'string') return undefined;
|
|
70
|
+
const items = message.details?.items;
|
|
71
|
+
if (!Array.isArray(items) || items.length === 0
|
|
72
|
+
|| items.some(item => !isCardPresentation(item) && typeof item?.text !== 'string')) {
|
|
73
|
+
return new Markdown(message.content, outputPad, 0, markdownTheme(theme));
|
|
74
|
+
}
|
|
75
|
+
const presentations = items.filter(isCardPresentation);
|
|
76
|
+
const authors = [...new Set(presentations.flatMap(item => item.author === undefined ? [] : [item.author]))];
|
|
77
|
+
const origins = [...new Set(presentations.map(item => item.origin))];
|
|
78
|
+
const originName = origins.map(shortOrigin).join(', ') || 'Notification';
|
|
79
|
+
const badge = presentations.at(-1)?.badge;
|
|
80
|
+
const byline = [authors.join(', '), badge].filter(Boolean).join(' · ');
|
|
81
|
+
const refs = presentations.flatMap(item => item.ref === undefined ? []
|
|
82
|
+
: [origins.length > 1 ? `${shortOrigin(item.origin)} ${item.ref}` : item.ref]).join(', ');
|
|
83
|
+
const trailing = [refs, presentations.length > 1 ? String(presentations.length) : undefined].filter(Boolean).join(' · ');
|
|
84
|
+
const box = new Box(outputPad, 1, text => theme.bg?.('customMessageBg', text) ?? text);
|
|
85
|
+
const left = `${fg(theme, 'accent')('■')} ${style(theme, 'bold')(originName)}${byline ? fg(theme, 'muted')(` · ${byline}`) : ''}`;
|
|
86
|
+
const right = trailing ? fg(theme, 'dim')(trailing) : '';
|
|
87
|
+
box.addChild({ invalidate() {}, render(width) { return [cardHeader(left, right, width)]; } });
|
|
88
|
+
const bodyOf = item => isCardPresentation(item) ? item.body : item.text;
|
|
89
|
+
if (!expanded) {
|
|
90
|
+
const body = bodyOf(items.at(-1));
|
|
91
|
+
if (body.trim()) box.addChild(guidedMarkdown(body.slice(0, 1_000).replace(/[\uD800-\uDBFF]$/, ''), theme, { preview: true }));
|
|
92
|
+
return box;
|
|
93
|
+
}
|
|
94
|
+
for (const [index, item] of items.entries()) {
|
|
95
|
+
if (index > 0) box.addChild({
|
|
96
|
+
invalidate() {},
|
|
97
|
+
render(width) { return [`${fg(theme, 'mdQuoteBorder')('│ ')}${fg(theme, 'borderMuted')('─'.repeat(Math.max(1, width - 2)))}`]; },
|
|
98
|
+
});
|
|
99
|
+
box.addChild(guidedMarkdown(bodyOf(item), theme, {
|
|
100
|
+
lead: authors.length > 1 && isCardPresentation(item) ? item.author : undefined,
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
return box;
|
|
30
104
|
}
|
|
31
105
|
|
|
32
106
|
export default function squarePiExtension(pi) {
|