wings-plugin-ui 0.3.0 → 0.4.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 +3 -2
- package/emoji.js +1632 -0
- package/markdown.js +13 -3
- package/package.json +2 -1
- package/plugin-ui.css +26 -0
- package/ui.d.ts +7 -1
- package/ui.js +37 -5
package/markdown.js
CHANGED
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
// it, and the only URLs it lets through are https links and https or small data:image images.
|
|
3
3
|
//
|
|
4
4
|
// Blocks: headings, paragraphs, lists (flat, with an indent depth, and task items), fenced code, quotes, rules and
|
|
5
|
-
// GFM tables. Inline: code, bold, italics, strikethrough, links, images
|
|
5
|
+
// GFM tables. Inline: code, bold, italics, strikethrough, links, images, bare https URLs and emoji shortcodes
|
|
6
|
+
// (`:white_check_mark:`), which stay literal inside code and when the name is unknown.
|
|
7
|
+
|
|
8
|
+
import { EMOJI } from "./emoji.js";
|
|
6
9
|
|
|
7
10
|
/** A data: image longer than this, in characters, is shown as its alt text. */
|
|
8
11
|
export const MAX_DATA_IMAGE = 8000;
|
|
@@ -142,7 +145,11 @@ export function parseBlocks(src) {
|
|
|
142
145
|
// Every scan is bounded and stops at a newline, so a long run of brackets or markers costs a bounded step per start
|
|
143
146
|
// rather than a pass over the rest of the text.
|
|
144
147
|
const inlinePattern =
|
|
145
|
-
/\[!\[([^\[\]\n]{0,200})\]\(([^)\s]{0,2000})\)\]\(([^)\s]{1,2000})\)|!\[([^\[\]\n]{0,200})\]\(([^)\s]{0,2000})\)|\[([^\[\]\n]{1,200})\]\(([^)\s]{1,2000})\)|`([^`\n]{1,2000})`|\*\*([^*\n]{1,2000})\*\*|~~([^~\n]{1,2000})~~|\*([^*\s][^*\n]{0,2000})\*|(https:\/\/[^\s<>"'`)\]]{1,2000})
|
|
148
|
+
/\[!\[([^\[\]\n]{0,200})\]\(([^)\s]{0,2000})\)\]\(([^)\s]{1,2000})\)|!\[([^\[\]\n]{0,200})\]\(([^)\s]{0,2000})\)|\[([^\[\]\n]{1,200})\]\(([^)\s]{1,2000})\)|`([^`\n]{1,2000})`|\*\*([^*\n]{1,2000})\*\*|~~([^~\n]{1,2000})~~|\*([^*\s][^*\n]{0,2000})\*|(https:\/\/[^\s<>"'`)\]]{1,2000})|:([a-z0-9_+-]{1,40}):/gi;
|
|
149
|
+
|
|
150
|
+
/** `text` with its known `:shortcodes:` as emoji, for spans that are plain text, like a bold or a link's text. */
|
|
151
|
+
export const emojify = (text) =>
|
|
152
|
+
text.includes(":") ? text.replace(/:([a-z0-9_+-]{1,40}):/gi, (all, name) => (Object.hasOwn(EMOJI, name.toLowerCase()) ? EMOJI[name.toLowerCase()] : all)) : text;
|
|
146
153
|
|
|
147
154
|
const sentenceEnd = ".,;:!?";
|
|
148
155
|
|
|
@@ -172,7 +179,10 @@ export function parseInline(src) {
|
|
|
172
179
|
else if (m[9] !== undefined) out.push({ kind: "strong", text: m[9] });
|
|
173
180
|
else if (m[10] !== undefined) out.push({ kind: "del", text: m[10] });
|
|
174
181
|
else if (m[11] !== undefined) out.push({ kind: "em", text: m[11] });
|
|
175
|
-
else {
|
|
182
|
+
else if (m[13] !== undefined) {
|
|
183
|
+
const emoji = Object.hasOwn(EMOJI, m[13].toLowerCase()) ? EMOJI[m[13].toLowerCase()] : null;
|
|
184
|
+
out.push({ kind: "text", text: emoji ?? m[0] });
|
|
185
|
+
} else {
|
|
176
186
|
// A bare URL, without the punctuation that ends a sentence.
|
|
177
187
|
let end = m[12].length;
|
|
178
188
|
while (end > 0 && sentenceEnd.includes(m[12][end - 1])) end--;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wings-plugin-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "React components and the stylesheet for Wings plugin pages, on Base UI like the Wings app's shadcn/ui components.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"files": [
|
|
16
16
|
"ui.js",
|
|
17
17
|
"markdown.js",
|
|
18
|
+
"emoji.js",
|
|
18
19
|
"ui.d.ts",
|
|
19
20
|
"plugin-ui.css",
|
|
20
21
|
"README.md",
|
package/plugin-ui.css
CHANGED
|
@@ -744,6 +744,32 @@ textarea::placeholder {
|
|
|
744
744
|
}
|
|
745
745
|
|
|
746
746
|
/* Status: a dot, a spinner and a badge. */
|
|
747
|
+
.w-avatar {
|
|
748
|
+
display: inline-flex;
|
|
749
|
+
flex: none;
|
|
750
|
+
align-items: center;
|
|
751
|
+
justify-content: center;
|
|
752
|
+
width: 24px;
|
|
753
|
+
height: 24px;
|
|
754
|
+
border-radius: 50%;
|
|
755
|
+
background: hsl(var(--w-hue) 45% 32%);
|
|
756
|
+
color: hsl(var(--w-hue) 70% 90%);
|
|
757
|
+
font-size: 10px;
|
|
758
|
+
font-weight: 600;
|
|
759
|
+
line-height: 1;
|
|
760
|
+
letter-spacing: 0.02em;
|
|
761
|
+
user-select: none;
|
|
762
|
+
}
|
|
763
|
+
.w-avatar[data-size="small"] {
|
|
764
|
+
width: 18px;
|
|
765
|
+
height: 18px;
|
|
766
|
+
font-size: 8px;
|
|
767
|
+
}
|
|
768
|
+
.w-avatar[data-size="large"] {
|
|
769
|
+
width: 32px;
|
|
770
|
+
height: 32px;
|
|
771
|
+
font-size: 12px;
|
|
772
|
+
}
|
|
747
773
|
.w-dot {
|
|
748
774
|
flex: none;
|
|
749
775
|
width: 6px;
|
package/ui.d.ts
CHANGED
|
@@ -174,6 +174,11 @@ export function Dot(
|
|
|
174
174
|
label?: string;
|
|
175
175
|
},
|
|
176
176
|
): ReactElement;
|
|
177
|
+
/**
|
|
178
|
+
* A round avatar with the initials of `name` (up to two letters) on a colour that depends only on the name, so a person
|
|
179
|
+
* looks the same everywhere. Decoration next to the name unless given a `label`, which names it for assistive tech.
|
|
180
|
+
*/
|
|
181
|
+
export function Avatar(props: Props<"span"> & { name: string; size?: "small" | "large"; label?: string }): ReactElement;
|
|
177
182
|
/** A spinner, 16px like the app's, in the colour of the text around it. With a `label` it's announced; otherwise it's decoration. */
|
|
178
183
|
export function Spinner(props: Props<"span"> & { size?: "large"; label?: string }): ReactElement;
|
|
179
184
|
/** shadcn/ui's Badge variants. */
|
|
@@ -339,7 +344,8 @@ export function Disclosure(
|
|
|
339
344
|
|
|
340
345
|
/**
|
|
341
346
|
* Markdown as React elements: headings, paragraphs, lists (with read-only task checkboxes), fenced code, quotes,
|
|
342
|
-
* rules, GFM tables, bold, italics, strikethrough, inline code, links, bare https URLs
|
|
347
|
+
* rules, GFM tables, bold, italics, strikethrough, inline code, links, bare https URLs, images and emoji shortcodes like
|
|
348
|
+
* `:white_check_mark:` (unknown names and anything in code stay as typed). No HTML is ever
|
|
343
349
|
* parsed. Links open through `wings.openUrl` and only when they are https, so the manifest needs
|
|
344
350
|
* `permissions.openUrl: ["https://"]` for them to work. Images load only from https or `data:image/` (png, jpeg, gif,
|
|
345
351
|
* webp) URLs of up to 8,000 characters; anything else shows its alt text. A remote https image shows as a "Load image from <host>" button and loads only when clicked, since text an agent wrote could otherwise leak data in the URL; a data: image shows at once. `inline` renders only the inline parts, in
|
package/ui.js
CHANGED
|
@@ -17,7 +17,7 @@ import { ToggleGroup as BaseToggleGroup } from "@base-ui/react/toggle-group";
|
|
|
17
17
|
import { createContext, createElement as h, useContext, useEffect, useId, memo, useLayoutEffect, useRef, useState } from "react";
|
|
18
18
|
import { createRoot } from "react-dom/client";
|
|
19
19
|
|
|
20
|
-
import { MAX_DATA_IMAGE, parseBlocks, parseInline } from "./markdown.js";
|
|
20
|
+
import { MAX_DATA_IMAGE, emojify, parseBlocks, parseInline } from "./markdown.js";
|
|
21
21
|
|
|
22
22
|
export { MAX_DATA_IMAGE };
|
|
23
23
|
|
|
@@ -365,6 +365,38 @@ export function Dot({ tone, color, label, className, style, ...rest }) {
|
|
|
365
365
|
});
|
|
366
366
|
}
|
|
367
367
|
|
|
368
|
+
/** Up to two initials of a name or login: "Ada Lovelace" is AL, "ada-bot[bot]" is AB, "ada" is A. */
|
|
369
|
+
const initialsOf = (name) => {
|
|
370
|
+
const words = name.replace(/\[bot\]$/i, "").split(/[^\p{L}\p{N}]+/u).filter(Boolean);
|
|
371
|
+
return (words.length > 1 ? words[0][0] + words[1][0] : (words[0] ?? "?").slice(0, 1)).toUpperCase();
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
/** A hue from 0 to 359 that depends only on the name, so a person has the same colour everywhere. */
|
|
375
|
+
const hueOf = (name) => {
|
|
376
|
+
let hash = 0;
|
|
377
|
+
for (let i = 0; i < name.length; i++) hash = (hash * 31 + name.charCodeAt(i)) >>> 0;
|
|
378
|
+
return hash % 360;
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* A round avatar with the initials of a `name` on a colour taken from it. Decoration next to the name by default;
|
|
383
|
+
* with a `label` it's announced.
|
|
384
|
+
*/
|
|
385
|
+
export function Avatar({ name, size, label, className, style, ...rest }) {
|
|
386
|
+
return h(
|
|
387
|
+
"span",
|
|
388
|
+
{
|
|
389
|
+
...rest,
|
|
390
|
+
className: cx("w-avatar", className),
|
|
391
|
+
// The hue comes from the name, as data: plugin-ui.css paints the avatar with it.
|
|
392
|
+
style: { ...style, "--w-hue": hueOf(name) },
|
|
393
|
+
"data-size": size,
|
|
394
|
+
...(label ? { role: "img", "aria-label": label } : { "aria-hidden": true }),
|
|
395
|
+
},
|
|
396
|
+
initialsOf(name),
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
368
400
|
/** A spinner. With a `label` it's announced. */
|
|
369
401
|
export function Spinner({ size, label, className, ...rest }) {
|
|
370
402
|
return h("span", { ...rest, className: cx("w-spinner", className), "data-size": size, ...(label ? { role: "img", "aria-label": label } : { "aria-hidden": true }) });
|
|
@@ -795,15 +827,15 @@ function MdInline({ text }) {
|
|
|
795
827
|
case "code":
|
|
796
828
|
return h("code", { key: i, className: "w-md-code" }, part.text);
|
|
797
829
|
case "strong":
|
|
798
|
-
return h("strong", { key: i }, part.text);
|
|
830
|
+
return h("strong", { key: i }, emojify(part.text));
|
|
799
831
|
case "em":
|
|
800
|
-
return h("em", { key: i }, part.text);
|
|
832
|
+
return h("em", { key: i }, emojify(part.text));
|
|
801
833
|
case "del":
|
|
802
|
-
return h("del", { key: i }, part.text);
|
|
834
|
+
return h("del", { key: i }, emojify(part.text));
|
|
803
835
|
case "image":
|
|
804
836
|
return h(MdImage, { key: i, src: part.src, alt: part.alt });
|
|
805
837
|
case "link":
|
|
806
|
-
return h("a", { key: i, className: "w-md-link", href: part.href, onClick: (e) => openLink(e, part.href) }, part.text);
|
|
838
|
+
return h("a", { key: i, className: "w-md-link", href: part.href, onClick: (e) => openLink(e, part.href) }, emojify(part.text));
|
|
807
839
|
default:
|
|
808
840
|
return part.text;
|
|
809
841
|
}
|