@signalcore/ui 1.0.0-beta.1 → 1.0.0-beta.2
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 +8 -1
- package/dist/AgentAbout.js +7 -4
- package/dist/AgentAbout.js.map +1 -1
- package/dist/AgentTasks.js +2 -1
- package/dist/AgentTasks.js.map +1 -1
- package/dist/VoiceCall.js +1 -1
- package/dist/VoiceCall.js.map +1 -1
- package/dist/about.d.ts +34 -144
- package/dist/about.js +6 -8
- package/dist/about.js.map +1 -1
- package/dist/automation/cron.d.ts +1 -1
- package/dist/automation/cron.js.map +1 -1
- package/dist/automation/marks.js +7 -3
- package/dist/automation/marks.js.map +1 -1
- package/dist/automation/messages.d.ts +2 -0
- package/dist/automation/messages.js +2 -0
- package/dist/automation/messages.js.map +1 -1
- package/dist/automation/types.d.ts +25 -128
- package/dist/automation/types.js +36 -0
- package/dist/automation/types.js.map +1 -0
- package/dist/automation/when.d.ts +1 -1
- package/dist/automation/when.js +5 -1
- package/dist/automation/when.js.map +1 -1
- package/dist/blocks.d.ts +23 -1
- package/dist/browser.js +2 -2
- package/dist/browser.js.map +1 -1
- package/dist/call.d.ts +1 -1
- package/dist/call.js.map +1 -1
- package/dist/contract.d.ts +262 -0
- package/dist/credentials.js.map +1 -1
- package/dist/dev.js +1 -1
- package/dist/dev.js.map +1 -1
- package/dist/es.js +9 -1
- package/dist/es.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -1
- package/dist/kit/AudioPlayer.js +1 -1
- package/dist/kit/AudioPlayer.js.map +1 -1
- package/dist/kit/Choices.js +1 -1
- package/dist/kit/Choices.js.map +1 -1
- package/dist/kit/FileField.js +1 -1
- package/dist/kit/FileField.js.map +1 -1
- package/dist/kit/Popover.js.map +1 -1
- package/dist/kit/Tile.d.ts +1 -1
- package/dist/kit/Tile.js +1 -1
- package/dist/kit/Tile.js.map +1 -1
- package/dist/kit/messages.d.ts +13 -0
- package/dist/kit/messages.js +7 -1
- package/dist/kit/messages.js.map +1 -1
- package/dist/kit/uploads.d.ts +2 -1
- package/dist/kit/uploads.js +1 -1
- package/dist/kit/uploads.js.map +1 -1
- package/dist/kit/waveform.js.map +1 -1
- package/dist/markdown.js.map +1 -1
- package/dist/marks.js +2 -3
- package/dist/marks.js.map +1 -1
- package/dist/surface/Markdown.js +1 -1
- package/dist/surface/Markdown.js.map +1 -1
- package/dist/surface/Surface.js +43 -4
- package/dist/surface/Surface.js.map +1 -1
- package/dist/surface/scope.d.ts +1 -1
- package/dist/surface/scope.js.map +1 -1
- package/dist/surface/values.js +1 -1
- package/dist/surface/values.js.map +1 -1
- package/dist/tint.d.ts +1 -1
- package/dist/tint.js +1 -1
- package/dist/tint.js.map +1 -1
- package/dist/usePolled.js +1 -1
- package/dist/usePolled.js.map +1 -1
- package/dist/voice/webgl.js +1 -1
- package/dist/voice/webgl.js.map +1 -1
- package/dist/widget.js +14 -14
- package/package.json +1 -1
- package/src/AgentAbout.tsx +10 -3
- package/src/AgentTasks.tsx +2 -2
- package/src/VoiceCall.tsx +1 -1
- package/src/about.ts +42 -144
- package/src/automation/cron.ts +1 -1
- package/src/automation/marks.tsx +6 -2
- package/src/automation/messages.ts +4 -0
- package/src/automation/types.ts +58 -118
- package/src/automation/when.ts +3 -1
- package/src/blocks.ts +25 -0
- package/src/browser.ts +4 -4
- package/src/call.ts +1 -1
- package/src/contract.ts +1720 -0
- package/src/credentials.ts +2 -2
- package/src/dev.ts +1 -1
- package/src/es.ts +8 -0
- package/src/index.ts +1 -0
- package/src/kit/AudioPlayer.tsx +1 -1
- package/src/kit/Choices.tsx +1 -1
- package/src/kit/FileField.tsx +1 -1
- package/src/kit/Popover.tsx +1 -1
- package/src/kit/Tile.tsx +1 -1
- package/src/kit/messages.ts +20 -0
- package/src/kit/uploads.ts +2 -2
- package/src/kit/waveform.ts +1 -1
- package/src/markdown.ts +1 -1
- package/src/marks.tsx +3 -123
- package/src/styles.css +15 -0
- package/src/surface/Markdown.tsx +1 -1
- package/src/surface/Surface.tsx +53 -4
- package/src/surface/scope.ts +3 -1
- package/src/surface/values.ts +5 -5
- package/src/tint.ts +1 -1
- package/src/usePolled.ts +2 -2
- package/src/voice/webgl.ts +1 -1
package/dist/surface/Surface.js
CHANGED
|
@@ -34,6 +34,7 @@ const BLOCKS = /* @__PURE__ */ new Set([
|
|
|
34
34
|
"image",
|
|
35
35
|
"automation",
|
|
36
36
|
"file",
|
|
37
|
+
"connect",
|
|
37
38
|
"button",
|
|
38
39
|
"choice",
|
|
39
40
|
"form"
|
|
@@ -47,12 +48,12 @@ function known(blocks) {
|
|
|
47
48
|
});
|
|
48
49
|
}
|
|
49
50
|
/**
|
|
50
|
-
* Whether a screen draws its own edge: one that is a single card (or an automation,
|
|
51
|
-
* its own) needs no panel around it where it is shown.
|
|
51
|
+
* Whether a screen draws its own edge: one that is a single card (or an automation, or an app
|
|
52
|
+
* to connect, each a card of its own) needs no panel around it where it is shown.
|
|
52
53
|
*/
|
|
53
54
|
function framesItself(surface) {
|
|
54
55
|
const [only, ...more] = surface.blocks;
|
|
55
|
-
return more.length === 0 && (only?.type === "card" || only?.type === "automation");
|
|
56
|
+
return more.length === 0 && (only?.type === "card" || only?.type === "automation" || only?.type === "connect");
|
|
56
57
|
}
|
|
57
58
|
/**
|
|
58
59
|
* What the agent showed, drawn block by block with the kit's primitives (a `card` is a `Card`, a
|
|
@@ -154,6 +155,7 @@ function Draw({ block }) {
|
|
|
154
155
|
case "image": return /* @__PURE__ */ jsx(Picture, { block });
|
|
155
156
|
case "automation": return /* @__PURE__ */ jsx(AutomationShown, { block });
|
|
156
157
|
case "file": return /* @__PURE__ */ jsx(FileShown, { block });
|
|
158
|
+
case "connect": return /* @__PURE__ */ jsx(Connect, { block });
|
|
157
159
|
case "button": return /* @__PURE__ */ jsx(Pressable, { block });
|
|
158
160
|
case "choice": return /* @__PURE__ */ jsx(Choice, { block });
|
|
159
161
|
case "form": return /* @__PURE__ */ jsx(FormView, { block });
|
|
@@ -355,6 +357,43 @@ function AutomationShown({ block }) {
|
|
|
355
357
|
});
|
|
356
358
|
}
|
|
357
359
|
/**
|
|
360
|
+
* An app for the person to connect, as its connection stands: its logo and name over the line
|
|
361
|
+
* the agent wrote of what it will do with it; while it waits for them, the button that opens
|
|
362
|
+
* its consent page apart; once it works, or if it failed, a quiet word in its place. Before
|
|
363
|
+
* the API has asked for it, only the app as the agent named it.
|
|
364
|
+
*/
|
|
365
|
+
function Connect({ block }) {
|
|
366
|
+
const part = useSurfaceParts();
|
|
367
|
+
const { messages } = useKit();
|
|
368
|
+
const { connection } = block;
|
|
369
|
+
const title = connection?.title ?? block.app;
|
|
370
|
+
const pending = connection?.status === "pending" ? connection.url : null;
|
|
371
|
+
const url = pending !== null && https(pending) ? pending : null;
|
|
372
|
+
const working = connection?.status === "active";
|
|
373
|
+
const failed = connection !== null && !working && connection.status !== "pending";
|
|
374
|
+
return /* @__PURE__ */ jsx(Card, {
|
|
375
|
+
title,
|
|
376
|
+
subtitle: block.purpose,
|
|
377
|
+
image: connection?.icon ?? void 0,
|
|
378
|
+
children: url !== null ? /* @__PURE__ */ jsx(Button, {
|
|
379
|
+
tone: "primary",
|
|
380
|
+
href: url,
|
|
381
|
+
children: messages.connect.authorize(title)
|
|
382
|
+
}) : /* @__PURE__ */ jsxs("p", {
|
|
383
|
+
className: part("sc-surface__connect", ...working ? ["sc-surface__connect--done"] : []),
|
|
384
|
+
children: [working && /* @__PURE__ */ jsx(CheckIcon, {
|
|
385
|
+
"aria-hidden": true,
|
|
386
|
+
weight: "bold"
|
|
387
|
+
}), connectWords(messages.connect, working, failed)]
|
|
388
|
+
})
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
/** The quiet word an app's card says where its button isn't: working, failed, or on its way. */
|
|
392
|
+
function connectWords(words, working, failed) {
|
|
393
|
+
if (working) return words.connected;
|
|
394
|
+
return failed ? words.failed : words.preparing;
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
358
397
|
* A button that sends its action, marked once it was the one pressed; or a link that opens its
|
|
359
398
|
* page apart (it stays usable on an older turn: it changes nothing here).
|
|
360
399
|
*/
|
|
@@ -476,6 +515,6 @@ function https(url) {
|
|
|
476
515
|
return /^https:\/\//i.test(url);
|
|
477
516
|
}
|
|
478
517
|
//#endregion
|
|
479
|
-
export { Surface, framesItself, known,
|
|
518
|
+
export { Surface, framesItself, known, usePlayed };
|
|
480
519
|
|
|
481
520
|
//# sourceMappingURL=Surface.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Surface.js","names":["Host"],"sources":["../../src/surface/Surface.tsx"],"sourcesContent":["import { ArrowUpIcon, CheckIcon, PencilSimpleIcon } from \"@phosphor-icons/react\";\nimport { Component, useEffect, useRef, useState, type ComponentType, type ReactNode } from \"react\";\n\nimport { Automation } from \"../automation/Automation\";\nimport type {\n AutomationBlock,\n Block,\n ButtonBlock,\n ChoiceBlock,\n FieldsBlock,\n ImageBlock,\n ListBlock,\n Surface as Shown,\n TableBlock,\n} from \"../blocks\";\nimport { classes } from \"../classes\";\nimport { Button } from \"../kit/Button\";\nimport { formatSize } from \"../about\";\nimport { baseName } from \"../files\";\nimport { Card } from \"../kit/Card\";\nimport { Chart } from \"../kit/Chart\";\nimport { KitProvider, useKit, useParts } from \"../kit/context\";\nimport { FileMark } from \"../kit/FileMark\";\nimport { List, Row } from \"../kit/List\";\nimport { Mark } from \"../kit/Mark\";\nimport type { KitLocale, KitMessages } from \"../kit/messages\";\nimport type { FileHost } from \"../kit/uploads\";\nimport { AudioPlayer } from \"../kit/AudioPlayer\";\nimport { Note } from \"../kit/Note\";\nimport { Stat } from \"../kit/Stat\";\nimport type { Attachment, FileBlock } from \"../types\";\nimport { FormView } from \"./Form\";\nimport { Markdown } from \"./Markdown\";\nimport {\n ScopeContext,\n useScope,\n useSurfaceParts,\n type Scope,\n type SurfaceClassNames,\n type SurfaceComponents,\n} from \"./scope\";\nimport { answeredIn, chosen, FIELD_KINDS, picked, pressed, type SurfaceAction } from \"./values\";\n\nexport interface SurfaceProps {\n surface: Shown;\n onAction: (action: SurfaceAction) => void;\n /** False draws the inputs disabled: a surface already answered, or one from an older turn. */\n active?: boolean | undefined;\n /** What the person already did on it: the option picked, the button pressed, the form sent. */\n answered?: SurfaceAction | undefined;\n components?: SurfaceComponents | undefined;\n classNames?: SurfaceClassNames;\n /** The words its controls say, in the host's language; English by default. */\n messages?: Partial<KitMessages> | undefined;\n /** The language of its calendar, days, times and numbers; English by default. */\n locale?: KitLocale | undefined;\n /**\n * Where its file fields' files go (uploaded as they are picked) and how the ones sent are found\n * and opened: the conversation's. None: a file field takes no files.\n */\n files?: FileHost | undefined;\n}\n\nconst BLOCKS: ReadonlySet<string> = new Set([\n \"card\",\n \"stack\",\n \"divider\",\n \"text\",\n \"fields\",\n \"table\",\n \"list\",\n \"stat\",\n \"chart\",\n \"note\",\n \"image\",\n \"automation\",\n \"file\",\n \"button\",\n \"choice\",\n \"form\",\n]);\n\n/** Whether every block down the tree, and every field of its forms, is one this renderer draws. */\nexport function known(blocks: Block[]): boolean {\n return blocks.every((block) => {\n if (!BLOCKS.has(block.type)) return false;\n if (block.type === \"form\") return block.fields.every((field) => FIELD_KINDS.has(field.kind));\n return \"children\" in block ? known(block.children) : true;\n });\n}\n\n/**\n * Whether a screen draws its own edge: one that is a single card (or an automation, a card of\n * its own) needs no panel around it where it is shown.\n */\nexport function framesItself(surface: Shown): boolean {\n const [only, ...more] = surface.blocks;\n return more.length === 0 && (only?.type === \"card\" || only?.type === \"automation\");\n}\n\n/**\n * What the agent showed, drawn block by block with the kit's primitives (a `card` is a `Card`, a\n * `list` a `List` of `Row`s, a form's fields its controls); a block or a field kind the API has\n * grown since this renderer was written leaves the whole surface to its text twin, so nothing\n * is half-drawn. Each block's action comes back through `onAction`; the host wraps it with the\n * surface's id.\n */\nexport function Surface({\n surface,\n onAction,\n active = true,\n answered,\n components,\n classNames,\n messages,\n locale,\n files,\n}: SurfaceProps) {\n const scope: Scope = { active, answered, onAction, components, classNames, files };\n return (\n <KitProvider classNames={classNames} messages={messages} locale={locale}>\n <ScopeContext value={scope}>\n <div className={`sc ${classes(classNames, \"sc-surface\")}`}>\n <Whole twin={<p className={classes(classNames, \"sc-surface__text\")}>{surface.text}</p>}>\n {known(surface.blocks)\n ? surface.blocks.map((block, index) => <Draw key={index} block={block} />)\n : null}\n </Whole>\n </div>\n </ScopeContext>\n </KitProvider>\n );\n}\n\n/**\n * The surface drawn whole or not at all: what it can't draw (blocks it doesn't know, or one that\n * fails as it draws) leaves its text twin in its place, never a broken page.\n */\nclass Whole extends Component<{ twin: ReactNode; children: ReactNode }, { failed: boolean }> {\n override state = { failed: false };\n\n static getDerivedStateFromError(): { failed: boolean } {\n return { failed: true };\n }\n\n override render() {\n return this.state.failed || this.props.children === null\n ? this.props.twin\n : this.props.children;\n }\n}\n\n/** One block: the host's own component for its type when it gave one, else the kit's. */\nfunction Draw({ block }: { block: Block }) {\n const { active, onAction, components } = useScope();\n const part = useSurfaceParts();\n const children =\n \"children\" in block\n ? block.children.map((child, index) => <Draw key={index} block={child} />)\n : undefined;\n const Own = components?.[block.type];\n if (Own) {\n // Found by the block's own type, so its fields are the right props; the union is too wide\n // for the compiler to see that pairing.\n const Host = Own as ComponentType<Record<string, unknown>>;\n return <Host {...block} active={active} onAction={onAction} children={children} />;\n }\n switch (block.type) {\n case \"card\":\n return (\n <Card title={block.title} subtitle={block.subtitle} image={block.image}>\n {children}\n </Card>\n );\n case \"stack\":\n return (\n <div\n part=\"stack\"\n className={part(\n \"sc-surface__stack\",\n ...(block.direction === \"row\" ? ([\"sc-surface__stack--row\"] as const) : []),\n )}\n >\n {children}\n </div>\n );\n case \"divider\":\n return <hr part=\"divider\" className={part(\"sc-surface__divider\")} />;\n case \"text\":\n return (\n <div part=\"text\" className={part(\"sc-surface__text\")}>\n <Markdown text={block.text} />\n </div>\n );\n case \"fields\":\n return <Fields block={block} />;\n case \"table\":\n return <Table block={block} />;\n case \"list\":\n return <Listing block={block} />;\n case \"stat\":\n return <Stat label={block.label} value={block.value} hint={block.hint} tone={block.tone} />;\n case \"chart\":\n return <Chart kind={block.kind} points={block.points} />;\n case \"note\":\n return (\n <Note tone={block.tone}>\n <Markdown text={block.text} />\n </Note>\n );\n case \"image\":\n return <Picture block={block} />;\n case \"automation\":\n return <AutomationShown block={block} />;\n case \"file\":\n return <FileShown block={block} />;\n case \"button\":\n return <Pressable block={block} />;\n case \"choice\":\n return <Choice block={block} />;\n case \"form\":\n return <FormView block={block} />;\n }\n}\n\n/**\n * A file the agent put in front of the person, compact: its kind's mark, its title (its name\n * under it when the title is another), its size at the right; pressed, it opens whole in the\n * host's viewer (`files.open`), whatever its kind. A picture, a video or a sound plays where it\n * is instead, when the host can play it (`files.play`). Until the API has put it in the\n * conversation it is only named, quietly.\n */\nfunction FileShown({ block }: { block: FileBlock }) {\n const { files } = useScope();\n const { code } = useKit();\n const { file } = block;\n const name = file?.name ?? baseName(block.path);\n const title = block.title ?? name;\n const open = file !== null ? files?.open : undefined;\n const played = file !== null ? playedAs(file.media_type) : null;\n const src = usePlayed(file, played !== null ? files?.play : undefined);\n if (file !== null && played !== null && src !== null) {\n return (\n <Played\n kind={played}\n src={src.url}\n name={name}\n caption={block.title}\n onOpen={open ? () => open(file) : undefined}\n onLost={src.again}\n />\n );\n }\n return (\n <List>\n <Row\n mark={<FileMark name={name} type={file?.media_type} />}\n title={title}\n subtitle={title === name ? undefined : name}\n meta={file === null ? undefined : formatSize(file.size, code)}\n quiet={file === null}\n onPress={open && file !== null ? () => open(file) : undefined}\n />\n </List>\n );\n}\n\ntype PlayedKind = \"image\" | \"video\" | \"audio\";\n\n/** How a file plays in the page: a picture, a video, a sound; null for any other kind. */\nexport function playedAs(mediaType: string): PlayedKind | null {\n const family = mediaType.split(\"/\")[0];\n return family === \"image\" || family === \"video\" || family === \"audio\" ? family : null;\n}\n\n/**\n * Where `file` plays from, asked of the host once (`play`): null until it answers, and when it\n * can't. `again` asks for a new address, for a player whose address ran out.\n */\nexport function usePlayed(\n file: Attachment | null,\n play: ((file: Attachment) => Promise<string>) | undefined,\n): { url: string; again: () => void } | null {\n const [url, setUrl] = useState<string | null>(null);\n const [asked, setAsked] = useState(0);\n // The file and the host's way to play it as they are now: a block drawn again carries the\n // same file anew, and the host its functions; what is asked for changes with the file's id.\n const latest = useRef({ file, play });\n latest.current = { file, play };\n const id = file?.id;\n const playable = play !== undefined;\n useEffect(() => {\n const { file: now, play: player } = latest.current;\n if (now === null || player === undefined) return;\n let current = true;\n player(now).then(\n (address) => current && setUrl(address),\n () => current && setUrl(null),\n );\n return () => {\n current = false;\n };\n }, [id, playable, asked]);\n return url === null ? null : { url, again: () => setAsked((times) => times + 1) };\n}\n\n/**\n * A picture, a video or a sound the agent put in front of the person, where it is: the\n * browser's own player, streaming from the address it plays from; a picture pressed opens whole\n * in the host's viewer. A player whose address ran out asks for a new one, once.\n */\nfunction Played({\n kind,\n src,\n name,\n caption,\n onOpen,\n onLost,\n}: {\n kind: PlayedKind;\n src: string;\n name: string;\n caption: string | undefined;\n onOpen: (() => void) | undefined;\n onLost: () => void;\n}) {\n const part = useSurfaceParts();\n const lost = useRef(false);\n const once = () => {\n if (lost.current) return;\n lost.current = true;\n onLost();\n };\n return (\n <figure part=\"media\" className={part(\"sc-surface__media\")} data-kind={kind}>\n {kind === \"image\" ? (\n onOpen ? (\n <button type=\"button\" onClick={onOpen} aria-label={name}>\n <img src={src} alt={name} loading=\"lazy\" onError={once} />\n </button>\n ) : (\n <img src={src} alt={name} loading=\"lazy\" onError={once} />\n )\n ) : kind === \"video\" ? (\n <video src={src} controls playsInline preload=\"metadata\" aria-label={name} onError={once} />\n ) : (\n <AudioPlayer src={src} label={name} onError={once} />\n )}\n {caption !== undefined && <figcaption>{caption}</figcaption>}\n </figure>\n );\n}\n\n/** Labels and their values, a row each. */\nfunction Fields({ block }: { block: FieldsBlock }) {\n const part = useSurfaceParts();\n return (\n <dl part=\"fields\" className={part(\"sc-surface__fields\")}>\n {block.items.map((item, index) => (\n <div key={index}>\n <dt>{item.label}</dt>\n <dd>{item.value}</dd>\n </div>\n ))}\n </dl>\n );\n}\n\n/** Rows under named columns, scrolling across inside their own box when they are wide. */\nfunction Table({ block }: { block: TableBlock }) {\n const part = useSurfaceParts();\n return (\n <div part=\"table\" className={part(\"sc-surface__table\")}>\n <table>\n <thead>\n <tr>\n {block.columns.map((column, index) => (\n <th key={index} scope=\"col\">\n {column}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {block.rows.map((row, index) => (\n <tr key={index}>\n {block.columns.map((_, at) => (\n <td key={at}>{row[at] ?? \"\"}</td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n );\n}\n\n/** A list block as the kit's `List`: a `Row` each; with an action, rows to pick, the picked checked. */\nfunction Listing({ block }: { block: ListBlock }) {\n const { active, answered, onAction } = useScope();\n const { action } = block;\n const pick = action !== undefined && answered?.name === action ? answered.data.item : undefined;\n return (\n <List>\n {block.items.map((entry) => (\n <Row\n key={entry.id}\n mark={\n entry.image !== undefined && https(entry.image) ? (\n <Mark src={entry.image} photo size={40} />\n ) : undefined\n }\n title={entry.title}\n subtitle={entry.subtitle}\n meta={entry.meta}\n pressed={entry.id === pick}\n disabled={!active}\n onPress={\n action === undefined ? undefined : () => onAction(chosen({ ...block, action }, entry))\n }\n />\n ))}\n </List>\n );\n}\n\n/** A picture from an https address, never sending the page's address along, with its caption. */\nfunction Picture({ block }: { block: ImageBlock }) {\n const part = useSurfaceParts();\n if (!https(block.url)) return null;\n return (\n <figure part=\"image\" className={part(\"sc-surface__image\")}>\n <img src={block.url} alt={block.alt} loading=\"lazy\" referrerPolicy=\"no-referrer\" />\n {block.caption !== undefined && <figcaption>{block.caption}</figcaption>}\n </figure>\n );\n}\n\n/**\n * An automation, read only: the agent's page is where it is switched and its runs opened. One\n * that is not there (removed, renamed since) says so quietly.\n */\nfunction AutomationShown({ block }: { block: AutomationBlock }) {\n const part = useParts();\n const { messages } = useKit();\n // `null`: it is gone; missing: not filled in by the API, which a newer server always does.\n if (block.view == null) {\n return (\n <p className={part(\"sc-automation\", \"sc-automation--gone\")}>{messages.automation.gone}</p>\n );\n }\n return <Automation automation={block.view} runs={block.view.runs} />;\n}\n\n/**\n * A button that sends its action, marked once it was the one pressed; or a link that opens its\n * page apart (it stays usable on an older turn: it changes nothing here).\n */\nfunction Pressable({ block }: { block: ButtonBlock }) {\n const { active, answered, onAction } = useScope();\n const { action, url, tone, label } = block;\n if (url !== undefined) {\n return https(url) ? (\n <Button tone={tone} href={url}>\n {label}\n </Button>\n ) : null;\n }\n if (action === undefined) return null;\n const done = answered?.name === action && answered.label === label;\n return (\n <Button\n tone={tone}\n icon={done ? CheckIcon : undefined}\n pressed={done}\n disabled={!active}\n onClick={() => onAction(pressed({ ...block, action }))}\n >\n {label}\n </Button>\n );\n}\n\n/** A few options to pick one of at once; the one picked keeps a ring and a check. */\nfunction Choice({ block }: { block: ChoiceBlock }) {\n const { active, answered, onAction } = useScope();\n const part = useSurfaceParts();\n const mine = answered?.name === block.action ? answered.data : undefined;\n const chosenId = mine?.option;\n const listed =\n block.other === true ||\n block.options.length > SHORT_CHOICE ||\n block.options.some((option) => option.hint !== undefined || option.label.length > SHORT_LABEL);\n return (\n <div\n role=\"group\"\n part=\"choice\"\n className={part(\n \"sc-surface__choice\",\n ...(listed ? ([\"sc-surface__choice--list\"] as const) : []),\n )}\n >\n {block.options.map((option) => (\n <button\n key={option.id}\n type=\"button\"\n disabled={!active}\n aria-pressed={option.id === chosenId || undefined}\n onClick={() => onAction(picked(block, option))}\n part=\"option\"\n className={part(\"sc-surface__option\")}\n >\n <span>\n {option.label}\n {option.hint !== undefined && <small>{option.hint}</small>}\n </span>\n {option.id === chosenId && <CheckIcon aria-hidden weight=\"bold\" />}\n </button>\n ))}\n {block.other === true && (\n <Other\n active={active}\n said={typeof mine?.text === \"string\" ? mine.text : undefined}\n onSay={(text) => onAction(answeredIn(block, text))}\n />\n )}\n </div>\n );\n}\n\n/** Pills fit options this few and this short; more, longer or with a hint, they go one a row. */\nconst SHORT_CHOICE = 4;\nconst SHORT_LABEL = 28;\n\n/**\n * A choice's last row, to answer in one's own words: pressed, it turns into a box with its send\n * (Enter sends, Escape folds it back). Once said, the words stand in it, chosen.\n */\nfunction Other({\n active,\n said,\n onSay,\n}: {\n active: boolean;\n said: string | undefined;\n onSay: (text: string) => void;\n}) {\n const part = useSurfaceParts();\n const { messages } = useKit();\n const [writing, setWriting] = useState(false);\n const [text, setText] = useState(\"\");\n if (said !== undefined || !writing) {\n return (\n <button\n type=\"button\"\n disabled={!active}\n aria-pressed={said !== undefined || undefined}\n onClick={() => setWriting(true)}\n part=\"option other\"\n className={part(\"sc-surface__option\", \"sc-surface__option--other\")}\n >\n {said === undefined && <PencilSimpleIcon aria-hidden />}\n <span>{said ?? messages.other}</span>\n {said !== undefined && <CheckIcon aria-hidden weight=\"bold\" />}\n </button>\n );\n }\n const send = () => {\n const words = text.trim();\n if (words !== \"\") onSay(words);\n };\n return (\n <form\n part=\"other\"\n className={part(\"sc-surface__other\")}\n onSubmit={(event) => {\n event.preventDefault();\n send();\n }}\n >\n <input\n // A person who pressed to write wants to write.\n autoFocus\n value={text}\n aria-label={messages.other}\n placeholder={messages.otherPlaceholder}\n onChange={(event) => setText(event.target.value)}\n onKeyDown={(event) => {\n if (event.key === \"Escape\") {\n event.stopPropagation();\n setWriting(false);\n }\n }}\n />\n <button type=\"submit\" aria-label={messages.send} disabled={text.trim() === \"\"}>\n <ArrowUpIcon aria-hidden weight=\"bold\" />\n </button>\n </form>\n );\n}\n\n/** Only an https address is loaded or opened: never a script, never a local file. */\nfunction https(url: string): boolean {\n return /^https:\\/\\//i.test(url);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Surface.js","names":["Host"],"sources":["../../src/surface/Surface.tsx"],"sourcesContent":["import { ArrowUpIcon, CheckIcon, PencilSimpleIcon } from \"@phosphor-icons/react\";\nimport { Component, useEffect, useRef, useState, type ComponentType, type ReactNode } from \"react\";\n\nimport { Automation } from \"../automation/Automation\";\nimport type {\n AutomationBlock,\n Block,\n ButtonBlock,\n ChoiceBlock,\n ConnectBlock,\n FieldsBlock,\n ImageBlock,\n ListBlock,\n Surface as Shown,\n TableBlock,\n} from \"../blocks\";\nimport { classes } from \"../classes\";\nimport { Button } from \"../kit/Button\";\nimport { formatSize } from \"../about\";\nimport { baseName } from \"../files\";\nimport { Card } from \"../kit/Card\";\nimport { Chart } from \"../kit/Chart\";\nimport { KitProvider, useKit, useParts } from \"../kit/context\";\nimport { FileMark } from \"../kit/FileMark\";\nimport { List, Row } from \"../kit/List\";\nimport { Mark } from \"../kit/Mark\";\nimport type { KitLocale, KitMessages } from \"../kit/messages\";\nimport type { FileHost } from \"../kit/uploads\";\nimport { AudioPlayer } from \"../kit/AudioPlayer\";\nimport { Note } from \"../kit/Note\";\nimport { Stat } from \"../kit/Stat\";\nimport type { Attachment, FileBlock } from \"../types\";\nimport { FormView } from \"./Form\";\nimport { Markdown } from \"./Markdown\";\nimport {\n ScopeContext,\n useScope,\n useSurfaceParts,\n type Scope,\n type SurfaceClassNames,\n type SurfaceComponents,\n} from \"./scope\";\nimport { answeredIn, chosen, FIELD_KINDS, picked, pressed, type SurfaceAction } from \"./values\";\n\nexport interface SurfaceProps {\n surface: Shown;\n onAction: (action: SurfaceAction) => void;\n /** False draws the inputs disabled: a surface already answered, or one from an older turn. */\n active?: boolean | undefined;\n /** What the person already did on it: the option picked, the button pressed, the form sent. */\n answered?: SurfaceAction | undefined;\n components?: SurfaceComponents | undefined;\n classNames?: SurfaceClassNames;\n /** The words its controls say, in the host's language; English by default. */\n messages?: Partial<KitMessages> | undefined;\n /** The language of its calendar, days, times and numbers; English by default. */\n locale?: KitLocale | undefined;\n /**\n * Where its file fields' files go (uploaded as they are picked) and how the ones sent are found\n * and opened: the conversation's. None: a file field takes no files.\n */\n files?: FileHost | undefined;\n}\n\nconst BLOCKS: ReadonlySet<string> = new Set([\n \"card\",\n \"stack\",\n \"divider\",\n \"text\",\n \"fields\",\n \"table\",\n \"list\",\n \"stat\",\n \"chart\",\n \"note\",\n \"image\",\n \"automation\",\n \"file\",\n \"connect\",\n \"button\",\n \"choice\",\n \"form\",\n]);\n\n/** Whether every block down the tree, and every field of its forms, is one this renderer draws. */\nexport function known(blocks: Block[]): boolean {\n return blocks.every((block) => {\n if (!BLOCKS.has(block.type)) return false;\n if (block.type === \"form\") return block.fields.every((field) => FIELD_KINDS.has(field.kind));\n return \"children\" in block ? known(block.children) : true;\n });\n}\n\n/**\n * Whether a screen draws its own edge: one that is a single card (or an automation, or an app\n * to connect, each a card of its own) needs no panel around it where it is shown.\n */\nexport function framesItself(surface: Shown): boolean {\n const [only, ...more] = surface.blocks;\n return (\n more.length === 0 &&\n (only?.type === \"card\" || only?.type === \"automation\" || only?.type === \"connect\")\n );\n}\n\n/**\n * What the agent showed, drawn block by block with the kit's primitives (a `card` is a `Card`, a\n * `list` a `List` of `Row`s, a form's fields its controls); a block or a field kind the API has\n * grown since this renderer was written leaves the whole surface to its text twin, so nothing\n * is half-drawn. Each block's action comes back through `onAction`; the host wraps it with the\n * surface's id.\n */\nexport function Surface({\n surface,\n onAction,\n active = true,\n answered,\n components,\n classNames,\n messages,\n locale,\n files,\n}: SurfaceProps) {\n const scope: Scope = { active, answered, onAction, components, classNames, files };\n return (\n <KitProvider classNames={classNames} messages={messages} locale={locale}>\n <ScopeContext value={scope}>\n <div className={`sc ${classes(classNames, \"sc-surface\")}`}>\n <Whole twin={<p className={classes(classNames, \"sc-surface__text\")}>{surface.text}</p>}>\n {known(surface.blocks)\n ? surface.blocks.map((block, index) => <Draw key={index} block={block} />)\n : null}\n </Whole>\n </div>\n </ScopeContext>\n </KitProvider>\n );\n}\n\n/**\n * The surface drawn whole or not at all: what it can't draw (blocks it doesn't know, or one that\n * fails as it draws) leaves its text twin in its place, never a broken page.\n */\nclass Whole extends Component<{ twin: ReactNode; children: ReactNode }, { failed: boolean }> {\n override state = { failed: false };\n\n static getDerivedStateFromError(): { failed: boolean } {\n return { failed: true };\n }\n\n override render() {\n return this.state.failed || this.props.children === null\n ? this.props.twin\n : this.props.children;\n }\n}\n\n/** One block: the host's own component for its type when it gave one, else the kit's. */\nfunction Draw({ block }: { block: Block }) {\n const { active, onAction, components } = useScope();\n const part = useSurfaceParts();\n const children =\n \"children\" in block\n ? block.children.map((child, index) => <Draw key={index} block={child} />)\n : undefined;\n const Own = components?.[block.type];\n if (Own) {\n // Found by the block's own type, so its fields are the right props; the union is too wide\n // for the compiler to see that pairing.\n const Host = Own as ComponentType<Record<string, unknown>>;\n return <Host {...block} active={active} onAction={onAction} children={children} />;\n }\n switch (block.type) {\n case \"card\":\n return (\n <Card title={block.title} subtitle={block.subtitle} image={block.image}>\n {children}\n </Card>\n );\n case \"stack\":\n return (\n <div\n part=\"stack\"\n className={part(\n \"sc-surface__stack\",\n ...(block.direction === \"row\" ? ([\"sc-surface__stack--row\"] as const) : []),\n )}\n >\n {children}\n </div>\n );\n case \"divider\":\n return <hr part=\"divider\" className={part(\"sc-surface__divider\")} />;\n case \"text\":\n return (\n <div part=\"text\" className={part(\"sc-surface__text\")}>\n <Markdown text={block.text} />\n </div>\n );\n case \"fields\":\n return <Fields block={block} />;\n case \"table\":\n return <Table block={block} />;\n case \"list\":\n return <Listing block={block} />;\n case \"stat\":\n return <Stat label={block.label} value={block.value} hint={block.hint} tone={block.tone} />;\n case \"chart\":\n return <Chart kind={block.kind} points={block.points} />;\n case \"note\":\n return (\n <Note tone={block.tone}>\n <Markdown text={block.text} />\n </Note>\n );\n case \"image\":\n return <Picture block={block} />;\n case \"automation\":\n return <AutomationShown block={block} />;\n case \"file\":\n return <FileShown block={block} />;\n case \"connect\":\n return <Connect block={block} />;\n case \"button\":\n return <Pressable block={block} />;\n case \"choice\":\n return <Choice block={block} />;\n case \"form\":\n return <FormView block={block} />;\n }\n}\n\n/**\n * A file the agent put in front of the person, compact: its kind's mark, its title (its name\n * under it when the title is another), its size at the right; pressed, it opens whole in the\n * host's viewer (`files.open`), whatever its kind. A picture, a video or a sound plays where it\n * is instead, when the host can play it (`files.play`). Until the API has put it in the\n * conversation it is only named, quietly.\n */\nfunction FileShown({ block }: { block: FileBlock }) {\n const { files } = useScope();\n const { code } = useKit();\n const { file } = block;\n const name = file?.name ?? baseName(block.path);\n const title = block.title ?? name;\n const open = file !== null ? files?.open : undefined;\n const played = file !== null ? playedAs(file.media_type) : null;\n const src = usePlayed(file, played !== null ? files?.play : undefined);\n if (file !== null && played !== null && src !== null) {\n return (\n <Played\n kind={played}\n src={src.url}\n name={name}\n caption={block.title}\n onOpen={open ? () => open(file) : undefined}\n onLost={src.again}\n />\n );\n }\n return (\n <List>\n <Row\n mark={<FileMark name={name} type={file?.media_type} />}\n title={title}\n subtitle={title === name ? undefined : name}\n meta={file === null ? undefined : formatSize(file.size, code)}\n quiet={file === null}\n onPress={open && file !== null ? () => open(file) : undefined}\n />\n </List>\n );\n}\n\ntype PlayedKind = \"image\" | \"video\" | \"audio\";\n\n/** How a file plays in the page: a picture, a video, a sound; null for any other kind. */\nfunction playedAs(mediaType: string): PlayedKind | null {\n const family = mediaType.split(\"/\")[0];\n return family === \"image\" || family === \"video\" || family === \"audio\" ? family : null;\n}\n\n/**\n * Where `file` plays from, asked of the host once (`play`): null until it answers, and when it\n * can't. `again` asks for a new address, for a player whose address ran out.\n */\nexport function usePlayed(\n file: Attachment | null,\n play: ((file: Attachment) => Promise<string>) | undefined,\n): { url: string; again: () => void } | null {\n const [url, setUrl] = useState<string | null>(null);\n const [asked, setAsked] = useState(0);\n // The file and the host's way to play it as they are now: a block drawn again carries the\n // same file anew, and the host its functions; what is asked for changes with the file's id.\n const latest = useRef({ file, play });\n latest.current = { file, play };\n const id = file?.id;\n const playable = play !== undefined;\n useEffect(() => {\n const { file: now, play: player } = latest.current;\n if (now === null || player === undefined) return;\n let current = true;\n player(now).then(\n (address) => current && setUrl(address),\n () => current && setUrl(null),\n );\n return () => {\n current = false;\n };\n }, [id, playable, asked]);\n return url === null ? null : { url, again: () => setAsked((times) => times + 1) };\n}\n\n/**\n * A picture, a video or a sound the agent put in front of the person, where it is: the\n * browser's own player, streaming from the address it plays from; a picture pressed opens whole\n * in the host's viewer. A player whose address ran out asks for a new one, once.\n */\nfunction Played({\n kind,\n src,\n name,\n caption,\n onOpen,\n onLost,\n}: {\n kind: PlayedKind;\n src: string;\n name: string;\n caption: string | undefined;\n onOpen: (() => void) | undefined;\n onLost: () => void;\n}) {\n const part = useSurfaceParts();\n const lost = useRef(false);\n const once = () => {\n if (lost.current) return;\n lost.current = true;\n onLost();\n };\n return (\n <figure part=\"media\" className={part(\"sc-surface__media\")} data-kind={kind}>\n {kind === \"image\" ? (\n onOpen ? (\n <button type=\"button\" onClick={onOpen} aria-label={name}>\n <img src={src} alt={name} loading=\"lazy\" onError={once} />\n </button>\n ) : (\n <img src={src} alt={name} loading=\"lazy\" onError={once} />\n )\n ) : kind === \"video\" ? (\n <video src={src} controls playsInline preload=\"metadata\" aria-label={name} onError={once} />\n ) : (\n <AudioPlayer src={src} label={name} onError={once} />\n )}\n {caption !== undefined && <figcaption>{caption}</figcaption>}\n </figure>\n );\n}\n\n/** Labels and their values, a row each. */\nfunction Fields({ block }: { block: FieldsBlock }) {\n const part = useSurfaceParts();\n return (\n <dl part=\"fields\" className={part(\"sc-surface__fields\")}>\n {block.items.map((item, index) => (\n <div key={index}>\n <dt>{item.label}</dt>\n <dd>{item.value}</dd>\n </div>\n ))}\n </dl>\n );\n}\n\n/** Rows under named columns, scrolling across inside their own box when they are wide. */\nfunction Table({ block }: { block: TableBlock }) {\n const part = useSurfaceParts();\n return (\n <div part=\"table\" className={part(\"sc-surface__table\")}>\n <table>\n <thead>\n <tr>\n {block.columns.map((column, index) => (\n <th key={index} scope=\"col\">\n {column}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {block.rows.map((row, index) => (\n <tr key={index}>\n {block.columns.map((_, at) => (\n <td key={at}>{row[at] ?? \"\"}</td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n );\n}\n\n/** A list block as the kit's `List`: a `Row` each; with an action, rows to pick, the picked checked. */\nfunction Listing({ block }: { block: ListBlock }) {\n const { active, answered, onAction } = useScope();\n const { action } = block;\n const pick = action !== undefined && answered?.name === action ? answered.data.item : undefined;\n return (\n <List>\n {block.items.map((entry) => (\n <Row\n key={entry.id}\n mark={\n entry.image !== undefined && https(entry.image) ? (\n <Mark src={entry.image} photo size={40} />\n ) : undefined\n }\n title={entry.title}\n subtitle={entry.subtitle}\n meta={entry.meta}\n pressed={entry.id === pick}\n disabled={!active}\n onPress={\n action === undefined ? undefined : () => onAction(chosen({ ...block, action }, entry))\n }\n />\n ))}\n </List>\n );\n}\n\n/** A picture from an https address, never sending the page's address along, with its caption. */\nfunction Picture({ block }: { block: ImageBlock }) {\n const part = useSurfaceParts();\n if (!https(block.url)) return null;\n return (\n <figure part=\"image\" className={part(\"sc-surface__image\")}>\n <img src={block.url} alt={block.alt} loading=\"lazy\" referrerPolicy=\"no-referrer\" />\n {block.caption !== undefined && <figcaption>{block.caption}</figcaption>}\n </figure>\n );\n}\n\n/**\n * An automation, read only: the agent's page is where it is switched and its runs opened. One\n * that is not there (removed, renamed since) says so quietly.\n */\nfunction AutomationShown({ block }: { block: AutomationBlock }) {\n const part = useParts();\n const { messages } = useKit();\n // `null`: it is gone; missing: not filled in by the API, which a newer server always does.\n if (block.view == null) {\n return (\n <p className={part(\"sc-automation\", \"sc-automation--gone\")}>{messages.automation.gone}</p>\n );\n }\n return <Automation automation={block.view} runs={block.view.runs} />;\n}\n\n/**\n * An app for the person to connect, as its connection stands: its logo and name over the line\n * the agent wrote of what it will do with it; while it waits for them, the button that opens\n * its consent page apart; once it works, or if it failed, a quiet word in its place. Before\n * the API has asked for it, only the app as the agent named it.\n */\nfunction Connect({ block }: { block: ConnectBlock }) {\n const part = useSurfaceParts();\n const { messages } = useKit();\n const { connection } = block;\n const title = connection?.title ?? block.app;\n const pending = connection?.status === \"pending\" ? connection.url : null;\n const url = pending !== null && https(pending) ? pending : null;\n const working = connection?.status === \"active\";\n const failed = connection !== null && !working && connection.status !== \"pending\";\n return (\n <Card title={title} subtitle={block.purpose} image={connection?.icon ?? undefined}>\n {url !== null ? (\n <Button tone=\"primary\" href={url}>\n {messages.connect.authorize(title)}\n </Button>\n ) : (\n <p\n className={part(\n \"sc-surface__connect\",\n ...(working ? ([\"sc-surface__connect--done\"] as const) : []),\n )}\n >\n {working && <CheckIcon aria-hidden weight=\"bold\" />}\n {connectWords(messages.connect, working, failed)}\n </p>\n )}\n </Card>\n );\n}\n\n/** The quiet word an app's card says where its button isn't: working, failed, or on its way. */\nfunction connectWords(words: KitMessages[\"connect\"], working: boolean, failed: boolean): string {\n if (working) return words.connected;\n return failed ? words.failed : words.preparing;\n}\n\n/**\n * A button that sends its action, marked once it was the one pressed; or a link that opens its\n * page apart (it stays usable on an older turn: it changes nothing here).\n */\nfunction Pressable({ block }: { block: ButtonBlock }) {\n const { active, answered, onAction } = useScope();\n const { action, url, tone, label } = block;\n if (url !== undefined) {\n return https(url) ? (\n <Button tone={tone} href={url}>\n {label}\n </Button>\n ) : null;\n }\n if (action === undefined) return null;\n const done = answered?.name === action && answered.label === label;\n return (\n <Button\n tone={tone}\n icon={done ? CheckIcon : undefined}\n pressed={done}\n disabled={!active}\n onClick={() => onAction(pressed({ ...block, action }))}\n >\n {label}\n </Button>\n );\n}\n\n/** A few options to pick one of at once; the one picked keeps a ring and a check. */\nfunction Choice({ block }: { block: ChoiceBlock }) {\n const { active, answered, onAction } = useScope();\n const part = useSurfaceParts();\n const mine = answered?.name === block.action ? answered.data : undefined;\n const chosenId = mine?.option;\n const listed =\n block.other === true ||\n block.options.length > SHORT_CHOICE ||\n block.options.some((option) => option.hint !== undefined || option.label.length > SHORT_LABEL);\n return (\n <div\n role=\"group\"\n part=\"choice\"\n className={part(\n \"sc-surface__choice\",\n ...(listed ? ([\"sc-surface__choice--list\"] as const) : []),\n )}\n >\n {block.options.map((option) => (\n <button\n key={option.id}\n type=\"button\"\n disabled={!active}\n aria-pressed={option.id === chosenId || undefined}\n onClick={() => onAction(picked(block, option))}\n part=\"option\"\n className={part(\"sc-surface__option\")}\n >\n <span>\n {option.label}\n {option.hint !== undefined && <small>{option.hint}</small>}\n </span>\n {option.id === chosenId && <CheckIcon aria-hidden weight=\"bold\" />}\n </button>\n ))}\n {block.other === true && (\n <Other\n active={active}\n said={typeof mine?.text === \"string\" ? mine.text : undefined}\n onSay={(text) => onAction(answeredIn(block, text))}\n />\n )}\n </div>\n );\n}\n\n/** Pills fit options this few and this short; more, longer or with a hint, they go one a row. */\nconst SHORT_CHOICE = 4;\nconst SHORT_LABEL = 28;\n\n/**\n * A choice's last row, to answer in one's own words: pressed, it turns into a box with its send\n * (Enter sends, Escape folds it back). Once said, the words stand in it, chosen.\n */\nfunction Other({\n active,\n said,\n onSay,\n}: {\n active: boolean;\n said: string | undefined;\n onSay: (text: string) => void;\n}) {\n const part = useSurfaceParts();\n const { messages } = useKit();\n const [writing, setWriting] = useState(false);\n const [text, setText] = useState(\"\");\n if (said !== undefined || !writing) {\n return (\n <button\n type=\"button\"\n disabled={!active}\n aria-pressed={said !== undefined || undefined}\n onClick={() => setWriting(true)}\n part=\"option other\"\n className={part(\"sc-surface__option\", \"sc-surface__option--other\")}\n >\n {said === undefined && <PencilSimpleIcon aria-hidden />}\n <span>{said ?? messages.other}</span>\n {said !== undefined && <CheckIcon aria-hidden weight=\"bold\" />}\n </button>\n );\n }\n const send = () => {\n const words = text.trim();\n if (words !== \"\") onSay(words);\n };\n return (\n <form\n part=\"other\"\n className={part(\"sc-surface__other\")}\n onSubmit={(event) => {\n event.preventDefault();\n send();\n }}\n >\n <input\n // A person who pressed to write wants to write.\n autoFocus\n value={text}\n aria-label={messages.other}\n placeholder={messages.otherPlaceholder}\n onChange={(event) => setText(event.target.value)}\n onKeyDown={(event) => {\n if (event.key === \"Escape\") {\n event.stopPropagation();\n setWriting(false);\n }\n }}\n />\n <button type=\"submit\" aria-label={messages.send} disabled={text.trim() === \"\"}>\n <ArrowUpIcon aria-hidden weight=\"bold\" />\n </button>\n </form>\n );\n}\n\n/** Only an https address is loaded or opened: never a script, never a local file. */\nfunction https(url: string): boolean {\n return /^https:\\/\\//i.test(url);\n}\n"],"mappings":"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"}
|
package/dist/surface/scope.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { ComponentType, ReactNode } from "react";
|
|
|
8
8
|
* The surface's own parts, by the class each carries: the blocks that are not a kit primitive,
|
|
9
9
|
* and the form's layout. `classNames` takes these and the kit's (`KitPart`) under the same keys.
|
|
10
10
|
*/
|
|
11
|
-
export type SurfacePart = "sc-surface" | "sc-surface__stack" | "sc-surface__stack--row" | "sc-surface__divider" | "sc-surface__text" | "sc-surface__fields" | "sc-surface__table" | "sc-surface__image" | "sc-surface__media" | "sc-surface__choice" | "sc-surface__choice--list" | "sc-surface__option" | "sc-surface__option--other" | "sc-surface__other" | "sc-surface__form" | "sc-surface__submit" | "sc-surface__foot" | "sc-surface__sent";
|
|
11
|
+
export type SurfacePart = "sc-surface" | "sc-surface__stack" | "sc-surface__stack--row" | "sc-surface__divider" | "sc-surface__text" | "sc-surface__fields" | "sc-surface__table" | "sc-surface__image" | "sc-surface__media" | "sc-surface__choice" | "sc-surface__choice--list" | "sc-surface__option" | "sc-surface__option--other" | "sc-surface__other" | "sc-surface__form" | "sc-surface__submit" | "sc-surface__foot" | "sc-surface__sent" | "sc-surface__connect" | "sc-surface__connect--done";
|
|
12
12
|
export type SurfaceClassNames = Partial<Record<SurfacePart | KitPart, string>> | undefined;
|
|
13
13
|
/**
|
|
14
14
|
* What a host's component for a block type receives: the block itself, whether the surface still
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scope.js","names":[],"sources":["../../src/surface/scope.ts"],"sourcesContent":["import { createContext, useContext, type ComponentType, type ReactNode } from \"react\";\n\nimport type { Block } from \"../blocks\";\nimport { classes } from \"../classes\";\nimport type { KitPart } from \"../kit/context\";\nimport type { FileHost } from \"../kit/uploads\";\nimport type { SurfaceAction } from \"./values\";\n\n/**\n * The surface's own parts, by the class each carries: the blocks that are not a kit primitive,\n * and the form's layout. `classNames` takes these and the kit's (`KitPart`) under the same keys.\n */\nexport type SurfacePart =\n | \"sc-surface\"\n | \"sc-surface__stack\"\n | \"sc-surface__stack--row\"\n | \"sc-surface__divider\"\n | \"sc-surface__text\"\n | \"sc-surface__fields\"\n | \"sc-surface__table\"\n | \"sc-surface__image\"\n | \"sc-surface__media\"\n | \"sc-surface__choice\"\n | \"sc-surface__choice--list\"\n | \"sc-surface__option\"\n | \"sc-surface__option--other\"\n | \"sc-surface__other\"\n | \"sc-surface__form\"\n | \"sc-surface__submit\"\n | \"sc-surface__foot\"\n | \"sc-surface__sent\";\n\nexport type SurfaceClassNames = Partial<Record<SurfacePart | KitPart, string>> | undefined;\n\n/**\n * What a host's component for a block type receives: the block itself, whether the surface still\n * takes input, and `onAction` to send what the person did; a container's `children` come drawn.\n */\nexport type BlockProps<Type extends Block[\"type\"]> = Omit<\n Extract<Block, { type: Type }>,\n \"children\"\n> &\n (Extract<Block, { type: Type }> extends { children: Block[] }\n ? { children: ReactNode }\n : unknown) & {\n active: boolean;\n onAction: (action: SurfaceAction) => void;\n };\n\n/** The host's own drawing of a block type, in place of the package's: `{ table: TheirTable }`. */\nexport type SurfaceComponents = { [Type in Block[\"type\"]]?: ComponentType<BlockProps<Type>> };\n\n/** What every block of one surface shares: whether it takes input, what was done on it. */\nexport interface Scope {\n active: boolean;\n /** What the person already did on the surface. */\n answered: SurfaceAction | undefined;\n onAction: (action: SurfaceAction) => void;\n components: SurfaceComponents | undefined;\n classNames: SurfaceClassNames;\n /** Where its file fields' files go and are found; none: they take none. */\n files: FileHost | undefined;\n}\n\nexport const ScopeContext = createContext<Scope>({\n active: false,\n answered: undefined,\n onAction: () => undefined,\n components: undefined,\n classNames: undefined,\n files: undefined,\n});\n\nexport function useScope(): Scope {\n return useContext(ScopeContext);\n}\n\n/** The classes of the surface's own parts, with the host's own under each. */\nexport function useSurfaceParts(): (...parts: SurfacePart[]) => string {\n const { classNames } = useScope();\n return (...parts) => classes(classNames, ...parts);\n}\n"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"scope.js","names":[],"sources":["../../src/surface/scope.ts"],"sourcesContent":["import { createContext, useContext, type ComponentType, type ReactNode } from \"react\";\n\nimport type { Block } from \"../blocks\";\nimport { classes } from \"../classes\";\nimport type { KitPart } from \"../kit/context\";\nimport type { FileHost } from \"../kit/uploads\";\nimport type { SurfaceAction } from \"./values\";\n\n/**\n * The surface's own parts, by the class each carries: the blocks that are not a kit primitive,\n * and the form's layout. `classNames` takes these and the kit's (`KitPart`) under the same keys.\n */\nexport type SurfacePart =\n | \"sc-surface\"\n | \"sc-surface__stack\"\n | \"sc-surface__stack--row\"\n | \"sc-surface__divider\"\n | \"sc-surface__text\"\n | \"sc-surface__fields\"\n | \"sc-surface__table\"\n | \"sc-surface__image\"\n | \"sc-surface__media\"\n | \"sc-surface__choice\"\n | \"sc-surface__choice--list\"\n | \"sc-surface__option\"\n | \"sc-surface__option--other\"\n | \"sc-surface__other\"\n | \"sc-surface__form\"\n | \"sc-surface__submit\"\n | \"sc-surface__foot\"\n | \"sc-surface__sent\"\n | \"sc-surface__connect\"\n | \"sc-surface__connect--done\";\n\nexport type SurfaceClassNames = Partial<Record<SurfacePart | KitPart, string>> | undefined;\n\n/**\n * What a host's component for a block type receives: the block itself, whether the surface still\n * takes input, and `onAction` to send what the person did; a container's `children` come drawn.\n */\nexport type BlockProps<Type extends Block[\"type\"]> = Omit<\n Extract<Block, { type: Type }>,\n \"children\"\n> &\n (Extract<Block, { type: Type }> extends { children: Block[] }\n ? { children: ReactNode }\n : unknown) & {\n active: boolean;\n onAction: (action: SurfaceAction) => void;\n };\n\n/** The host's own drawing of a block type, in place of the package's: `{ table: TheirTable }`. */\nexport type SurfaceComponents = { [Type in Block[\"type\"]]?: ComponentType<BlockProps<Type>> };\n\n/** What every block of one surface shares: whether it takes input, what was done on it. */\nexport interface Scope {\n active: boolean;\n /** What the person already did on the surface. */\n answered: SurfaceAction | undefined;\n onAction: (action: SurfaceAction) => void;\n components: SurfaceComponents | undefined;\n classNames: SurfaceClassNames;\n /** Where its file fields' files go and are found; none: they take none. */\n files: FileHost | undefined;\n}\n\nexport const ScopeContext = createContext<Scope>({\n active: false,\n answered: undefined,\n onAction: () => undefined,\n components: undefined,\n classNames: undefined,\n files: undefined,\n});\n\nexport function useScope(): Scope {\n return useContext(ScopeContext);\n}\n\n/** The classes of the surface's own parts, with the host's own under each. */\nexport function useSurfaceParts(): (...parts: SurfacePart[]) => string {\n const { classNames } = useScope();\n return (...parts) => classes(classNames, ...parts);\n}\n"],"mappings":";;;AAkEA,MAAa,eAAe,cAAqB;CAC/C,QAAQ;CACR,UAAU,KAAA;CACV,gBAAgB,KAAA;CAChB,YAAY,KAAA;CACZ,YAAY,KAAA;CACZ,OAAO,KAAA;AACT,CAAC;AAED,SAAgB,WAAkB;CAChC,OAAO,WAAW,YAAY;AAChC;;AAGA,SAAgB,kBAAuD;CACrE,MAAM,EAAE,eAAe,SAAS;CAChC,QAAQ,GAAG,UAAU,QAAQ,YAAY,GAAG,KAAK;AACnD"}
|
package/dist/surface/values.js
CHANGED
|
@@ -232,6 +232,6 @@ function hasOption(field, id) {
|
|
|
232
232
|
return optionOf(field, id) !== void 0;
|
|
233
233
|
}
|
|
234
234
|
//#endregion
|
|
235
|
-
export { FIELD_KINDS,
|
|
235
|
+
export { FIELD_KINDS, answeredIn, chosen, filled, labelOf, picked, pressed, problemOf, problemsOf, sent, startValues };
|
|
236
236
|
|
|
237
237
|
//# sourceMappingURL=values.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"values.js","names":[],"sources":["../../src/surface/values.ts"],"sourcesContent":["/**\n * What a surface sends, as pure functions: each block's action, a form's values from its fields,\n * the words the person's answer reads as, and what is wrong with a value. The controls hold\n * values in the shapes the contract sends (`blocks.ts`); nothing here touches the page.\n */\nimport type {\n ButtonBlock,\n ChoiceBlock,\n Entry,\n FieldValue,\n FormBlock,\n FormField,\n ListBlock,\n Option,\n} from \"../blocks\";\nimport {\n DAY,\n dayAllowed,\n formatDay,\n formatMoment,\n formatTime,\n MOMENT,\n momentParts,\n slotAllowed,\n TIME,\n} from \"../kit/dates\";\nimport type { KitMessages } from \"../kit/messages\";\nimport { formatNumber, withUnit } from \"../kit/numbers\";\nimport { STARS } from \"../kit/RatingField\";\nimport type { Action, Attachment } from \"../types\";\n\n/**\n * What a block sends when the person uses it: its action's name, what it carries, and the words\n * the app shows for it (the option picked, the button, the form's answer).\n */\nexport type SurfaceAction = Omit<Action, \"surface\">;\n\n/** A form's values by field name, as its controls hold them. */\nexport type Values = Record<string, FieldValue>;\n\n/** The field kinds this renderer draws; a surface with another falls back to its text. */\nexport const FIELD_KINDS: ReadonlySet<string> = new Set([\n \"text\",\n \"textarea\",\n \"email\",\n \"phone\",\n \"url\",\n \"number\",\n \"date\",\n \"time\",\n \"datetime\",\n \"select\",\n \"multiselect\",\n \"checkbox\",\n \"range\",\n \"rating\",\n \"file\",\n \"secret\",\n]);\n\n/** Up to this many options, a select shows them all; more go in a list to search. */\nexport const FEW_OPTIONS = 5;\n/** Up to this many, a multiselect is chips; more go in a list to search. */\nexport const FEW_CHIPS = 8;\n/** What a secret reads as, anywhere it is shown. */\nexport const MASK = \"••••••••\";\n/** The longest answer label the API takes. */\nconst LABEL_MAX = 2000;\n\nconst EMAIL = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]{2,}$/;\nconst PHONE = /^\\+?[\\d\\s().-]+$/;\nconst URL_SHAPE = /^(https?:\\/\\/)?[^\\s/.]+(\\.[^\\s/.]+)+(\\/\\S*)?$/i;\n\n// --- The blocks' actions ------------------------------------------------------------------\n\n/** A button's action: its name, nothing more, under its label. */\nexport function pressed(block: ButtonBlock & { action: string }): SurfaceAction {\n return { name: block.action, data: {}, label: block.label };\n}\n\n/** A choice's action: the option's id, under its label. */\nexport function picked(block: ChoiceBlock, option: Option): SurfaceAction {\n return { name: block.action, data: { option: option.id }, label: option.label };\n}\n\n/** A choice answered in the person's own words, under them. */\nexport function answeredIn(block: ChoiceBlock, text: string): SurfaceAction {\n return { name: block.action, data: { text }, label: text };\n}\n\n/** A list's action: the entry's id, under its title. */\nexport function chosen(block: ListBlock & { action: string }, entry: Entry): SurfaceAction {\n return { name: block.action, data: { item: entry.id }, label: entry.title };\n}\n\n/**\n * A form's action: its values by field name (an empty field as null), under the words of its\n * one answer when it asks one thing (the day, the slot, the options picked, the files' names,\n * read from `files`), its submit's when it asks more.\n */\nexport function filled(\n block: FormBlock,\n values: Values,\n code: string,\n messages: KitMessages,\n files?: ReadonlyMap<string, Attachment>,\n): SurfaceAction {\n const data: Values = {};\n for (const field of block.fields) data[field.name] = sent(field, values[field.name] ?? null);\n const only = block.fields.length === 1 ? block.fields[0] : undefined;\n const answer = only ? labelOf(only, data[only.name] ?? null, code, messages, { files }) : \"\";\n return { name: block.action, data, label: clip(answer) || (block.submit ?? messages.send) };\n}\n\n/** A value as it goes: words trimmed, an empty one as null, a moment only once whole. */\nexport function sent(field: FormField, value: FieldValue): FieldValue {\n if (typeof value === \"string\") {\n const text = value.trim();\n if (field.kind === \"datetime\") return MOMENT.test(text) ? text : null;\n return text === \"\" ? null : text;\n }\n if (typeof value === \"number\") return Number.isFinite(value) ? value : null;\n return value;\n}\n\n// --- Where a form starts ------------------------------------------------------------------\n\n/**\n * Each field's first value: what the person already sent (`answered`, the form's own action's\n * data), else what the agent gave it, else empty; a value that does not fit its kind is left out.\n */\nexport function startValues(fields: FormField[], answered?: Record<string, unknown>): Values {\n const values: Values = {};\n for (const field of fields) {\n const given = answered && field.name in answered ? answered[field.name] : field.value;\n values[field.name] = fit(field, given) ?? emptyOf(field);\n }\n return values;\n}\n\n/** A field with nothing given: unticked, none picked, a slider at its least, else nothing. */\nexport function emptyOf(field: FormField): FieldValue {\n switch (field.kind) {\n case \"checkbox\":\n return false;\n case \"multiselect\":\n case \"file\":\n return [];\n case \"range\":\n return field.min ?? 0;\n default:\n return null;\n }\n}\n\n/** A value read as the field's kind holds it, or null when it does not fit. */\nexport function fit(field: FormField, raw: unknown): FieldValue {\n switch (field.kind) {\n case \"checkbox\":\n return raw === true || raw === \"true\"\n ? true\n : raw === false || raw === \"false\"\n ? false\n : null;\n case \"number\":\n case \"range\":\n case \"rating\": {\n const number = typeof raw === \"string\" && raw.trim() !== \"\" ? Number(raw) : raw;\n return typeof number === \"number\" && Number.isFinite(number) ? number : null;\n }\n case \"multiselect\": {\n const ids = Array.isArray(raw) ? raw : typeof raw === \"string\" ? [raw] : [];\n return ids.filter((id): id is string => typeof id === \"string\" && hasOption(field, id));\n }\n case \"file\": {\n const ids = Array.isArray(raw) ? raw : typeof raw === \"string\" ? [raw] : [];\n const strings = ids.filter((id): id is string => typeof id === \"string\" && id !== \"\");\n return field.multiple ? strings : strings.slice(0, 1);\n }\n case \"select\":\n return typeof raw === \"string\" && hasOption(field, raw) ? raw : null;\n case \"date\":\n return typeof raw === \"string\" && DAY.test(raw) ? raw : null;\n case \"time\":\n return typeof raw === \"string\" && TIME.test(raw) ? raw : null;\n case \"datetime\":\n return typeof raw === \"string\" && MOMENT.test(raw.slice(0, 16)) ? raw.slice(0, 16) : null;\n default:\n return typeof raw === \"string\" || typeof raw === \"number\" ? String(raw) : null;\n }\n}\n\n// --- What a value reads as ----------------------------------------------------------------\n\n/**\n * A value in words, as the person's answer reads in the chat: a day and a time in the host's\n * language (against `today`), the options' labels joined, the files' names (from `files`), a\n * number with its unit; \"\" when it is empty. A secret is never shown.\n */\nexport function labelOf(\n field: FormField,\n value: FieldValue,\n code: string,\n messages: KitMessages,\n {\n today = new Date(),\n files,\n }: { today?: Date | undefined; files?: ReadonlyMap<string, Attachment> | undefined } = {},\n): string {\n if (field.kind === \"secret\") return value === null || value === \"\" ? \"\" : MASK;\n if (field.kind === \"checkbox\") return value === true ? messages.yes : messages.no;\n if (value === null) return \"\";\n if (Array.isArray(value)) {\n const labels = value.map((id) =>\n field.kind === \"file\"\n ? (files?.get(id)?.name ?? messages.file)\n : (optionOf(field, id)?.label ?? id),\n );\n return labels.length === 0\n ? \"\"\n : new Intl.ListFormat(code, { type: \"conjunction\" }).format(labels);\n }\n if (typeof value === \"number\") {\n if (field.kind === \"rating\") return messages.rated(value, field.max ?? STARS);\n return withUnit(formatNumber(value, code), field.unit);\n }\n if (typeof value !== \"string\") return \"\";\n switch (field.kind) {\n case \"select\":\n return optionOf(field, value)?.label ?? value;\n case \"date\":\n return formatDay(value, code, today);\n case \"time\":\n return TIME.test(value) ? formatTime(value, code) : value;\n case \"datetime\":\n return formatMoment(value, code, today);\n default:\n return value.trim();\n }\n}\n\nfunction clip(text: string): string {\n return text.length > LABEL_MAX ? `${text.slice(0, LABEL_MAX - 1)}…` : text;\n}\n\n// --- What is wrong with a value -----------------------------------------------------------\n\n/** What is wrong with each field's value, by name; a field with nothing wrong is not in it. */\nexport function problemsOf(\n fields: FormField[],\n values: Values,\n code: string,\n messages: KitMessages,\n): Record<string, string> {\n const problems: Record<string, string> = {};\n for (const field of fields) {\n const problem = problemOf(field, values[field.name] ?? null, code, messages);\n if (problem !== null) problems[field.name] = problem;\n }\n return problems;\n}\n\n/**\n * What is wrong with a value, in words, or null: empty when it must be filled, a shape that is\n * not an email, a phone or an address, a number out of its bounds, a day or a time the field\n * does not offer.\n */\nexport function problemOf(\n field: FormField,\n value: FieldValue,\n code: string,\n messages: KitMessages,\n): string | null {\n if (isEmpty(field, value)) return field.required ? emptyWords(field, messages) : null;\n const text = typeof value === \"string\" ? value.trim() : \"\";\n switch (field.kind) {\n case \"email\":\n return EMAIL.test(text) ? null : messages.invalidEmail;\n case \"phone\":\n return PHONE.test(text) && text.replace(/\\D/g, \"\").length >= 6 ? null : messages.invalidPhone;\n case \"url\":\n return URL_SHAPE.test(text) ? null : messages.invalidUrl;\n case \"number\":\n case \"range\":\n case \"rating\":\n return typeof value === \"number\" && Number.isFinite(value)\n ? outOfBounds(field, value, code, messages)\n : messages.invalidNumber;\n case \"date\":\n return dayAllowed(field, text) ? null : messages.unavailableDate;\n case \"time\":\n if (!TIME.test(text)) return messages.invalidTime;\n return slotAllowed(field.times, text) ? null : messages.unavailableTime;\n case \"datetime\": {\n const [date, time] = momentParts(text);\n if (date === \"\" || time === \"\") return messages.chooseMoment;\n if (!dayAllowed(field, date)) return messages.unavailableDate;\n if (!TIME.test(time)) return messages.invalidTime;\n return slotAllowed(field.times, time) ? null : messages.unavailableTime;\n }\n case \"select\":\n return hasOption(field, text) ? null : messages.chooseOption;\n case \"multiselect\":\n return Array.isArray(value) && value.every((id) => hasOption(field, id))\n ? null\n : messages.chooseOption;\n default:\n return null;\n }\n}\n\n/** Whether nothing was given: no words, no pick, an unticked box, no stars. */\nexport function isEmpty(field: FormField, value: FieldValue): boolean {\n if (value === null) return true;\n if (field.kind === \"checkbox\") return value !== true;\n if (typeof value === \"string\") return value.trim() === \"\";\n if (Array.isArray(value)) return value.length === 0;\n return false;\n}\n\nfunction emptyWords(field: FormField, messages: KitMessages): string {\n switch (field.kind) {\n case \"date\":\n return messages.chooseDate;\n case \"time\":\n return messages.chooseTime;\n case \"datetime\":\n return messages.chooseMoment;\n case \"select\":\n case \"multiselect\":\n return messages.chooseOption;\n case \"rating\":\n return messages.chooseRating;\n case \"checkbox\":\n return messages.check;\n case \"file\":\n return messages.chooseFile;\n default:\n return messages.required;\n }\n}\n\nfunction outOfBounds(\n field: FormField,\n value: number,\n code: string,\n messages: KitMessages,\n): string | null {\n const { min, max } = field;\n if (min !== undefined && value < min) return messages.atLeast(formatNumber(min, code));\n if (max !== undefined && value > max) return messages.atMost(formatNumber(max, code));\n return null;\n}\n\nexport function optionOf(field: FormField, id: string): Option | undefined {\n return field.options?.find((option) => option.id === id);\n}\n\nfunction hasOption(field: FormField, id: string): boolean {\n return optionOf(field, id) !== undefined;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"values.js","names":[],"sources":["../../src/surface/values.ts"],"sourcesContent":["/**\n * What a surface sends, as pure functions: each block's action, a form's values from its fields,\n * the words the person's answer reads as, and what is wrong with a value. The controls hold\n * values in the shapes the contract sends (`blocks.ts`); nothing here touches the page.\n */\nimport type {\n ButtonBlock,\n ChoiceBlock,\n Entry,\n FieldValue,\n FormBlock,\n FormField,\n ListBlock,\n Option,\n} from \"../blocks\";\nimport {\n DAY,\n dayAllowed,\n formatDay,\n formatMoment,\n formatTime,\n MOMENT,\n momentParts,\n slotAllowed,\n TIME,\n} from \"../kit/dates\";\nimport type { KitMessages } from \"../kit/messages\";\nimport { formatNumber, withUnit } from \"../kit/numbers\";\nimport { STARS } from \"../kit/RatingField\";\nimport type { Action, Attachment } from \"../types\";\n\n/**\n * What a block sends when the person uses it: its action's name, what it carries, and the words\n * the app shows for it (the option picked, the button, the form's answer).\n */\nexport type SurfaceAction = Omit<Action, \"surface\">;\n\n/** A form's values by field name, as its controls hold them. */\nexport type Values = Record<string, FieldValue>;\n\n/** The field kinds this renderer draws; a surface with another falls back to its text. */\nexport const FIELD_KINDS: ReadonlySet<string> = new Set([\n \"text\",\n \"textarea\",\n \"email\",\n \"phone\",\n \"url\",\n \"number\",\n \"date\",\n \"time\",\n \"datetime\",\n \"select\",\n \"multiselect\",\n \"checkbox\",\n \"range\",\n \"rating\",\n \"file\",\n \"secret\",\n]);\n\n/** Up to this many options, a select shows them all; more go in a list to search. */\nexport const FEW_OPTIONS = 5;\n/** Up to this many, a multiselect is chips; more go in a list to search. */\nexport const FEW_CHIPS = 8;\n/** What a secret reads as, anywhere it is shown. */\nconst MASK = \"••••••••\";\n/** The longest answer label the API takes. */\nconst LABEL_MAX = 2000;\n\nconst EMAIL = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]{2,}$/;\nconst PHONE = /^\\+?[\\d\\s().-]+$/;\nconst URL_SHAPE = /^(https?:\\/\\/)?[^\\s/.]+(\\.[^\\s/.]+)+(\\/\\S*)?$/i;\n\n// --- The blocks' actions ------------------------------------------------------------------\n\n/** A button's action: its name, nothing more, under its label. */\nexport function pressed(block: ButtonBlock & { action: string }): SurfaceAction {\n return { name: block.action, data: {}, label: block.label };\n}\n\n/** A choice's action: the option's id, under its label. */\nexport function picked(block: ChoiceBlock, option: Option): SurfaceAction {\n return { name: block.action, data: { option: option.id }, label: option.label };\n}\n\n/** A choice answered in the person's own words, under them. */\nexport function answeredIn(block: ChoiceBlock, text: string): SurfaceAction {\n return { name: block.action, data: { text }, label: text };\n}\n\n/** A list's action: the entry's id, under its title. */\nexport function chosen(block: ListBlock & { action: string }, entry: Entry): SurfaceAction {\n return { name: block.action, data: { item: entry.id }, label: entry.title };\n}\n\n/**\n * A form's action: its values by field name (an empty field as null), under the words of its\n * one answer when it asks one thing (the day, the slot, the options picked, the files' names,\n * read from `files`), its submit's when it asks more.\n */\nexport function filled(\n block: FormBlock,\n values: Values,\n code: string,\n messages: KitMessages,\n files?: ReadonlyMap<string, Attachment>,\n): SurfaceAction {\n const data: Values = {};\n for (const field of block.fields) data[field.name] = sent(field, values[field.name] ?? null);\n const only = block.fields.length === 1 ? block.fields[0] : undefined;\n const answer = only ? labelOf(only, data[only.name] ?? null, code, messages, { files }) : \"\";\n return { name: block.action, data, label: clip(answer) || (block.submit ?? messages.send) };\n}\n\n/** A value as it goes: words trimmed, an empty one as null, a moment only once whole. */\nexport function sent(field: FormField, value: FieldValue): FieldValue {\n if (typeof value === \"string\") {\n const text = value.trim();\n if (field.kind === \"datetime\") return MOMENT.test(text) ? text : null;\n return text === \"\" ? null : text;\n }\n if (typeof value === \"number\") return Number.isFinite(value) ? value : null;\n return value;\n}\n\n// --- Where a form starts ------------------------------------------------------------------\n\n/**\n * Each field's first value: what the person already sent (`answered`, the form's own action's\n * data), else what the agent gave it, else empty; a value that does not fit its kind is left out.\n */\nexport function startValues(fields: FormField[], answered?: Record<string, unknown>): Values {\n const values: Values = {};\n for (const field of fields) {\n const given = answered && field.name in answered ? answered[field.name] : field.value;\n values[field.name] = fit(field, given) ?? emptyOf(field);\n }\n return values;\n}\n\n/** A field with nothing given: unticked, none picked, a slider at its least, else nothing. */\nfunction emptyOf(field: FormField): FieldValue {\n switch (field.kind) {\n case \"checkbox\":\n return false;\n case \"multiselect\":\n case \"file\":\n return [];\n case \"range\":\n return field.min ?? 0;\n default:\n return null;\n }\n}\n\n/** A value read as the field's kind holds it, or null when it does not fit. */\nfunction fit(field: FormField, raw: unknown): FieldValue {\n switch (field.kind) {\n case \"checkbox\":\n return raw === true || raw === \"true\"\n ? true\n : raw === false || raw === \"false\"\n ? false\n : null;\n case \"number\":\n case \"range\":\n case \"rating\": {\n const number = typeof raw === \"string\" && raw.trim() !== \"\" ? Number(raw) : raw;\n return typeof number === \"number\" && Number.isFinite(number) ? number : null;\n }\n case \"multiselect\": {\n const ids = Array.isArray(raw) ? raw : typeof raw === \"string\" ? [raw] : [];\n return ids.filter((id): id is string => typeof id === \"string\" && hasOption(field, id));\n }\n case \"file\": {\n const ids = Array.isArray(raw) ? raw : typeof raw === \"string\" ? [raw] : [];\n const strings = ids.filter((id): id is string => typeof id === \"string\" && id !== \"\");\n return field.multiple ? strings : strings.slice(0, 1);\n }\n case \"select\":\n return typeof raw === \"string\" && hasOption(field, raw) ? raw : null;\n case \"date\":\n return typeof raw === \"string\" && DAY.test(raw) ? raw : null;\n case \"time\":\n return typeof raw === \"string\" && TIME.test(raw) ? raw : null;\n case \"datetime\":\n return typeof raw === \"string\" && MOMENT.test(raw.slice(0, 16)) ? raw.slice(0, 16) : null;\n default:\n return typeof raw === \"string\" || typeof raw === \"number\" ? String(raw) : null;\n }\n}\n\n// --- What a value reads as ----------------------------------------------------------------\n\n/**\n * A value in words, as the person's answer reads in the chat: a day and a time in the host's\n * language (against `today`), the options' labels joined, the files' names (from `files`), a\n * number with its unit; \"\" when it is empty. A secret is never shown.\n */\nexport function labelOf(\n field: FormField,\n value: FieldValue,\n code: string,\n messages: KitMessages,\n {\n today = new Date(),\n files,\n }: { today?: Date | undefined; files?: ReadonlyMap<string, Attachment> | undefined } = {},\n): string {\n if (field.kind === \"secret\") return value === null || value === \"\" ? \"\" : MASK;\n if (field.kind === \"checkbox\") return value === true ? messages.yes : messages.no;\n if (value === null) return \"\";\n if (Array.isArray(value)) {\n const labels = value.map((id) =>\n field.kind === \"file\"\n ? (files?.get(id)?.name ?? messages.file)\n : (optionOf(field, id)?.label ?? id),\n );\n return labels.length === 0\n ? \"\"\n : new Intl.ListFormat(code, { type: \"conjunction\" }).format(labels);\n }\n if (typeof value === \"number\") {\n if (field.kind === \"rating\") return messages.rated(value, field.max ?? STARS);\n return withUnit(formatNumber(value, code), field.unit);\n }\n if (typeof value !== \"string\") return \"\";\n switch (field.kind) {\n case \"select\":\n return optionOf(field, value)?.label ?? value;\n case \"date\":\n return formatDay(value, code, today);\n case \"time\":\n return TIME.test(value) ? formatTime(value, code) : value;\n case \"datetime\":\n return formatMoment(value, code, today);\n default:\n return value.trim();\n }\n}\n\nfunction clip(text: string): string {\n return text.length > LABEL_MAX ? `${text.slice(0, LABEL_MAX - 1)}…` : text;\n}\n\n// --- What is wrong with a value -----------------------------------------------------------\n\n/** What is wrong with each field's value, by name; a field with nothing wrong is not in it. */\nexport function problemsOf(\n fields: FormField[],\n values: Values,\n code: string,\n messages: KitMessages,\n): Record<string, string> {\n const problems: Record<string, string> = {};\n for (const field of fields) {\n const problem = problemOf(field, values[field.name] ?? null, code, messages);\n if (problem !== null) problems[field.name] = problem;\n }\n return problems;\n}\n\n/**\n * What is wrong with a value, in words, or null: empty when it must be filled, a shape that is\n * not an email, a phone or an address, a number out of its bounds, a day or a time the field\n * does not offer.\n */\nexport function problemOf(\n field: FormField,\n value: FieldValue,\n code: string,\n messages: KitMessages,\n): string | null {\n if (isEmpty(field, value)) return field.required ? emptyWords(field, messages) : null;\n const text = typeof value === \"string\" ? value.trim() : \"\";\n switch (field.kind) {\n case \"email\":\n return EMAIL.test(text) ? null : messages.invalidEmail;\n case \"phone\":\n return PHONE.test(text) && text.replace(/\\D/g, \"\").length >= 6 ? null : messages.invalidPhone;\n case \"url\":\n return URL_SHAPE.test(text) ? null : messages.invalidUrl;\n case \"number\":\n case \"range\":\n case \"rating\":\n return typeof value === \"number\" && Number.isFinite(value)\n ? outOfBounds(field, value, code, messages)\n : messages.invalidNumber;\n case \"date\":\n return dayAllowed(field, text) ? null : messages.unavailableDate;\n case \"time\":\n if (!TIME.test(text)) return messages.invalidTime;\n return slotAllowed(field.times, text) ? null : messages.unavailableTime;\n case \"datetime\": {\n const [date, time] = momentParts(text);\n if (date === \"\" || time === \"\") return messages.chooseMoment;\n if (!dayAllowed(field, date)) return messages.unavailableDate;\n if (!TIME.test(time)) return messages.invalidTime;\n return slotAllowed(field.times, time) ? null : messages.unavailableTime;\n }\n case \"select\":\n return hasOption(field, text) ? null : messages.chooseOption;\n case \"multiselect\":\n return Array.isArray(value) && value.every((id) => hasOption(field, id))\n ? null\n : messages.chooseOption;\n default:\n return null;\n }\n}\n\n/** Whether nothing was given: no words, no pick, an unticked box, no stars. */\nfunction isEmpty(field: FormField, value: FieldValue): boolean {\n if (value === null) return true;\n if (field.kind === \"checkbox\") return value !== true;\n if (typeof value === \"string\") return value.trim() === \"\";\n if (Array.isArray(value)) return value.length === 0;\n return false;\n}\n\nfunction emptyWords(field: FormField, messages: KitMessages): string {\n switch (field.kind) {\n case \"date\":\n return messages.chooseDate;\n case \"time\":\n return messages.chooseTime;\n case \"datetime\":\n return messages.chooseMoment;\n case \"select\":\n case \"multiselect\":\n return messages.chooseOption;\n case \"rating\":\n return messages.chooseRating;\n case \"checkbox\":\n return messages.check;\n case \"file\":\n return messages.chooseFile;\n default:\n return messages.required;\n }\n}\n\nfunction outOfBounds(\n field: FormField,\n value: number,\n code: string,\n messages: KitMessages,\n): string | null {\n const { min, max } = field;\n if (min !== undefined && value < min) return messages.atLeast(formatNumber(min, code));\n if (max !== undefined && value > max) return messages.atMost(formatNumber(max, code));\n return null;\n}\n\nfunction optionOf(field: FormField, id: string): Option | undefined {\n return field.options?.find((option) => option.id === id);\n}\n\nfunction hasOption(field: FormField, id: string): boolean {\n return optionOf(field, id) !== undefined;\n}\n"],"mappings":"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"}
|
package/dist/tint.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/** The tints an agent's tile can take: the soft ones, never a state or the accent. */
|
|
3
3
|
export type AgentTint = "sky" | "rose" | "sand";
|
|
4
4
|
/**
|
|
5
|
-
* The tint an agent keeps wherever it appears (
|
|
5
|
+
* The tint an agent keeps wherever it appears (the agents' day, its profile, its card): picked from
|
|
6
6
|
* its id, so a rename changes nothing, and spread evenly over the three.
|
|
7
7
|
*/
|
|
8
8
|
export declare function agentTint(id: string): AgentTint;
|
package/dist/tint.js
CHANGED
|
@@ -5,7 +5,7 @@ const TINTS = [
|
|
|
5
5
|
"sand"
|
|
6
6
|
];
|
|
7
7
|
/**
|
|
8
|
-
* The tint an agent keeps wherever it appears (
|
|
8
|
+
* The tint an agent keeps wherever it appears (the agents' day, its profile, its card): picked from
|
|
9
9
|
* its id, so a rename changes nothing, and spread evenly over the three.
|
|
10
10
|
*/
|
|
11
11
|
function agentTint(id) {
|
package/dist/tint.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tint.js","names":[],"sources":["../src/tint.ts"],"sourcesContent":["/** The tints an agent's tile can take: the soft ones, never a state or the accent. */\nexport type AgentTint = \"sky\" | \"rose\" | \"sand\";\n\nconst TINTS: readonly AgentTint[] = [\"sky\", \"rose\", \"sand\"];\n\n/**\n * The tint an agent keeps wherever it appears (
|
|
1
|
+
{"version":3,"file":"tint.js","names":[],"sources":["../src/tint.ts"],"sourcesContent":["/** The tints an agent's tile can take: the soft ones, never a state or the accent. */\nexport type AgentTint = \"sky\" | \"rose\" | \"sand\";\n\nconst TINTS: readonly AgentTint[] = [\"sky\", \"rose\", \"sand\"];\n\n/**\n * The tint an agent keeps wherever it appears (the agents' day, its profile, its card): picked from\n * its id, so a rename changes nothing, and spread evenly over the three.\n */\nexport function agentTint(id: string): AgentTint {\n let hash = 0;\n for (const char of id) hash = (hash * 31 + (char.codePointAt(0) ?? 0)) >>> 0;\n return TINTS[hash % TINTS.length] ?? \"sky\";\n}\n"],"mappings":";AAGA,MAAM,QAA8B;CAAC;CAAO;CAAQ;AAAM;;;;;AAM1D,SAAgB,UAAU,IAAuB;CAC/C,IAAI,OAAO;CACX,KAAK,MAAM,QAAQ,IAAI,OAAQ,OAAO,MAAM,KAAK,YAAY,CAAC,KAAK,OAAQ;CAC3E,OAAO,MAAM,OAAO,MAAM,WAAW;AACvC"}
|
package/dist/usePolled.js
CHANGED
package/dist/usePolled.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"usePolled.js","names":[],"sources":["../src/usePolled.ts"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\n\nimport { ApiError } from \"./client\";\n\n/** What a read gave: the value once it came, why it failed, or that it is no longer there. */\
|
|
1
|
+
{"version":3,"file":"usePolled.js","names":[],"sources":["../src/usePolled.ts"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\n\nimport { ApiError } from \"./client\";\n\n/** What a read gave: the value once it came, why it failed, or that it is no longer there. */\ninterface Polled<T> {\n value: T | null;\n problem: unknown;\n gone: boolean;\n}\n\n/** How often something still moving (a run under way) is read again. */\nconst POLL_MS = 3000;\n\n/**\n * Reads `load` when `key` changes, and again every `POLL_MS` while `moving` says what it gave is\n * still moving (a run under way). What was read stays while a new read is on its way, so nothing\n * flickers; a 404 reads as gone.\n */\nexport function usePolled<T>(\n load: () => Promise<T>,\n key: string,\n moving: (value: T) => boolean,\n): Polled<T> {\n const [polled, setPolled] = useState<Polled<T> & { key: string }>({\n key,\n value: null,\n problem: null,\n gone: false,\n });\n const latest = useRef({ load, moving });\n useEffect(() => {\n latest.current = { load, moving };\n });\n\n useEffect(() => {\n let stopped = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n const read = async () => {\n try {\n const value = await latest.current.load();\n if (stopped) return;\n setPolled({ key, value, problem: null, gone: false });\n if (latest.current.moving(value)) timer = setTimeout(() => void read(), POLL_MS);\n } catch (error) {\n if (stopped) return;\n const gone = error instanceof ApiError && error.status === 404;\n setPolled((last) => ({ ...last, key, problem: gone ? null : error, gone }));\n }\n };\n void read();\n return () => {\n stopped = true;\n clearTimeout(timer);\n };\n }, [key]);\n\n // Another key's value is not this one's: until its own comes, nothing is shown.\n return polled.key === key ? polled : { value: null, problem: null, gone: false };\n}\n"],"mappings":";;;;AAYA,MAAM,UAAU;;;;;;AAOhB,SAAgB,UACd,MACA,KACA,QACW;CACX,MAAM,CAAC,QAAQ,aAAa,SAAsC;EAChE;EACA,OAAO;EACP,SAAS;EACT,MAAM;CACR,CAAC;CACD,MAAM,SAAS,OAAO;EAAE;EAAM;CAAO,CAAC;CACtC,gBAAgB;EACd,OAAO,UAAU;GAAE;GAAM;EAAO;CAClC,CAAC;CAED,gBAAgB;EACd,IAAI,UAAU;EACd,IAAI;EACJ,MAAM,OAAO,YAAY;GACvB,IAAI;IACF,MAAM,QAAQ,MAAM,OAAO,QAAQ,KAAK;IACxC,IAAI,SAAS;IACb,UAAU;KAAE;KAAK;KAAO,SAAS;KAAM,MAAM;IAAM,CAAC;IACpD,IAAI,OAAO,QAAQ,OAAO,KAAK,GAAG,QAAQ,iBAAiB,KAAK,KAAK,GAAG,OAAO;GACjF,SAAS,OAAO;IACd,IAAI,SAAS;IACb,MAAM,OAAO,iBAAiB,YAAY,MAAM,WAAW;IAC3D,WAAW,UAAU;KAAE,GAAG;KAAM;KAAK,SAAS,OAAO,OAAO;KAAO;IAAK,EAAE;GAC5E;EACF;EACA,KAAU;EACV,aAAa;GACX,UAAU;GACV,aAAa,KAAK;EACpB;CACF,GAAG,CAAC,GAAG,CAAC;CAGR,OAAO,OAAO,QAAQ,MAAM,SAAS;EAAE,OAAO;EAAM,SAAS;EAAM,MAAM;CAAM;AACjF"}
|
package/dist/voice/webgl.js
CHANGED
|
@@ -172,6 +172,6 @@ function createRenderer(canvas, fragment, dim, onLost) {
|
|
|
172
172
|
};
|
|
173
173
|
}
|
|
174
174
|
//#endregion
|
|
175
|
-
export { CUTOUT, INK, INK_UNIFORMS, createRenderer, inkOf, inkOver
|
|
175
|
+
export { CUTOUT, INK, INK_UNIFORMS, createRenderer, inkOf, inkOver };
|
|
176
176
|
|
|
177
177
|
//# sourceMappingURL=webgl.js.map
|
package/dist/voice/webgl.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webgl.js","names":[],"sources":["../../src/voice/webgl.ts"],"sourcesContent":["/**\n * The WebGL under the `wave` and `fluid-dots` orbs: one fullscreen triangle drawn by one\n * fragment shader, and its uniforms (the time, the signal, the host's colour). Nothing here knows\n * React, a call or a state; the frame loop is `motion`'s.\n */\n\n/** A style of orb drawn by a fragment shader. */\nexport interface Shader {\n /** GLSL ES 1.00 source of the fragment shader; it reads the uniforms named in `Uniforms`. */\n fragment: string;\n /**\n * How fast the shader's clock runs at an energy, 0 (idle) to 1 (listening, speaking): 1 is the\n * shader's own pace. The clock is summed frame by frame on this side, so a change of pace bends\n * the motion instead of making it jump, as a time multiplied inside the shader would.\n */\n pace(energy: number): number;\n}\n\n/** What one frame is drawn from. */\nexport interface Uniforms {\n /** `iTime`: seconds on the shader's own clock, already paced. */\n time: number;\n /** `uLevel`: how loud the microphone or the voice is, 0 to 1. */\n level: number;\n /** `uEnergy`: 0 at rest, 0.5 while thinking, 1 while listening or speaking. */\n energy: number;\n}\n\n/** A colour as the shader takes it: red, green and blue, each 0 to 1. */\nexport type Rgb = readonly [number, number, number];\n\nexport interface Renderer {\n draw(values: Uniforms): void;\n /** Paints the next frames in `ink` (`uInk`), or (null) in the shader's own colours. */\n ink(ink: Rgb | null): void;\n dispose(): void;\n}\n\nconst VERTEX = `attribute vec2 aPos; void main(){ gl_Position=vec4(aPos,0.0,1.0); }`;\n\n/**\n * The line every shader ends its `main` with: the shaders paint light on black, and an orb sits on\n * any background, so what is near black becomes transparent instead. The canvas is premultiplied,\n * so the colour goes with its alpha. There is no black square around the orb.\n */\nexport const CUTOUT = `gl_FragColor.a *= smoothstep(0.02, 0.12, max(gl_FragColor.r, max(gl_FragColor.g, gl_FragColor.b))); gl_FragColor.rgb *= gl_FragColor.a;`;\n\n/** The uniforms of the host's colour, which every shader declares beside its own. */\nexport const INK_UNIFORMS = `uniform vec3 uInk; uniform float uInked; // signalcore: the host's colour`;\n\n/**\n * The line before `CUTOUT`: with the host's colour (`uInked` 1), the shader's light is painted\n * in it, as bright as it was (its brightest channel); without, the shader's own colours stay.\n */\nexport const INK = `gl_FragColor.rgb = mix(gl_FragColor.rgb, uInk * max(gl_FragColor.r, max(gl_FragColor.g, gl_FragColor.b)), uInked);`;\n\n/**\n * What the shader paints so that, blended by exclusion over `surface`, its brightest light shows\n * as `color`: the colour itself over black, its opposite over white (exclusion turns it over),\n * and between them, channel by channel, what turns into it as far as it can; a channel of the\n * surface near the middle turns nothing over, and keeps the colour.\n */\nexport function inkOver(color: Rgb, surface: Rgb): Rgb {\n const channel = (wanted: number, under: number): number => {\n const turn = 1 - 2 * under;\n if (Math.abs(turn) < 0.2) return wanted;\n return Math.min(1, Math.max(0, (wanted - under) / turn));\n };\n return [\n channel(color[0], surface[0]),\n channel(color[1], surface[1]),\n channel(color[2], surface[2]),\n ];\n}\n\n/** A pixel to read colours off: any CSS colour, as the browser paints it. */\nlet swatch: CanvasRenderingContext2D | null | undefined;\n\n/** A CSS colour as red, green, blue and alpha, each 0 to 1; null where it can't be read. */\nexport function rgbaOf(css: string): readonly [number, number, number, number] | null {\n if (swatch === undefined) {\n swatch = document.createElement(\"canvas\").getContext(\"2d\", { willReadFrequently: true });\n }\n if (swatch === null || css === \"\") return null;\n swatch.clearRect(0, 0, 1, 1);\n swatch.fillStyle = \"#000\";\n swatch.fillStyle = css;\n swatch.fillRect(0, 0, 1, 1);\n const [r = 0, g = 0, b = 0, a = 0] = swatch.getImageData(0, 0, 1, 1).data;\n return [r / 255, g / 255, b / 255, a / 255];\n}\n\n/**\n * The host's colour for an orb's canvas, as its shader paints it, from what the stylesheet puts\n * on the canvas: the colour (`color`: `--sc-orb-color`, transparent when none) and the surface it\n * blends with (`outline-color`; a page's own, light, where the surface is see-through). Null:\n * the shader's own colours.\n */\nexport function inkOf(canvas: HTMLCanvasElement): Rgb | null {\n const style = getComputedStyle(canvas);\n const color = rgbaOf(style.color);\n if (color === null || color[3] === 0) return null;\n const surface = rgbaOf(style.outlineColor);\n const under: Rgb =\n surface === null || surface[3] < 0.5 ? [1, 1, 1] : [surface[0], surface[1], surface[2]];\n return inkOver([color[0], color[1], color[2]], under);\n}\n\nfunction compile(gl: WebGLRenderingContext, type: number, source: string): WebGLShader {\n const shader = gl.createShader(type);\n if (!shader) throw new Error(\"no shader\");\n gl.shaderSource(shader, source);\n gl.compileShader(shader);\n if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n const log = gl.getShaderInfoLog(shader);\n gl.deleteShader(shader);\n throw new Error(log ?? \"shader compile error\");\n }\n return shader;\n}\n\n/** The program of a fragment shader, linked: the triangle's vertex shader and `fragment`. */\nfunction link(gl: WebGLRenderingContext, fragment: string): WebGLProgram {\n const program = gl.createProgram();\n const vertex = compile(gl, gl.VERTEX_SHADER, VERTEX);\n const pixels = compile(gl, gl.FRAGMENT_SHADER, fragment);\n gl.attachShader(program, vertex);\n gl.attachShader(program, pixels);\n gl.linkProgram(program);\n // Linked, the program keeps what it needs: the shaders can go.\n gl.deleteShader(vertex);\n gl.deleteShader(pixels);\n if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n const log = gl.getProgramInfoLog(program);\n gl.deleteProgram(program);\n throw new Error(log ?? \"shader link error\");\n }\n return program;\n}\n\n/**\n * Sets `canvas` up to draw `fragment` on a square of `dim` pixels, compiled once: the program, the\n * triangle and the uniforms' places are kept for every frame after. Nothing when WebGL is not\n * there or the shader does not compile on this device, and `onLost` is called if the browser\n * takes the context away later (a GPU reset, too many contexts on the page).\n */\nexport function createRenderer(\n canvas: HTMLCanvasElement,\n fragment: string,\n dim: number,\n onLost: () => void,\n): Renderer | undefined {\n const gl = canvas.getContext(\"webgl\", { alpha: true, premultipliedAlpha: true });\n if (!gl) return undefined;\n\n let program: WebGLProgram;\n try {\n program = link(gl, fragment);\n } catch {\n return undefined;\n }\n gl.useProgram(program);\n\n const buffer = gl.createBuffer();\n gl.bindBuffer(gl.ARRAY_BUFFER, buffer);\n // One triangle bigger than the screen: three vertices cover it, with no seam across a quad.\n gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);\n const position = gl.getAttribLocation(program, \"aPos\");\n gl.enableVertexAttribArray(position);\n gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);\n\n canvas.width = dim;\n canvas.height = dim;\n gl.viewport(0, 0, dim, dim);\n gl.clearColor(0, 0, 0, 0);\n // A uniform a shader does not use is optimised out and has no place: setting it does nothing.\n gl.uniform2f(gl.getUniformLocation(program, \"iResolution\"), dim, dim);\n const time = gl.getUniformLocation(program, \"iTime\");\n const level = gl.getUniformLocation(program, \"uLevel\");\n const energy = gl.getUniformLocation(program, \"uEnergy\");\n const inkColor = gl.getUniformLocation(program, \"uInk\");\n const inked = gl.getUniformLocation(program, \"uInked\");\n gl.uniform1f(inked, 0);\n\n const lost = (event: Event) => {\n event.preventDefault();\n onLost();\n };\n canvas.addEventListener(\"webglcontextlost\", lost);\n\n return {\n draw(values) {\n gl.uniform1f(time, values.time);\n gl.uniform1f(level, values.level);\n gl.uniform1f(energy, values.energy);\n gl.clear(gl.COLOR_BUFFER_BIT);\n gl.drawArrays(gl.TRIANGLES, 0, 3);\n },\n ink(ink) {\n if (ink !== null) gl.uniform3f(inkColor, ink[0], ink[1], ink[2]);\n gl.uniform1f(inked, ink === null ? 0 : 1);\n },\n dispose() {\n canvas.removeEventListener(\"webglcontextlost\", lost);\n gl.deleteProgram(program);\n gl.deleteBuffer(buffer);\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"webgl.js","names":[],"sources":["../../src/voice/webgl.ts"],"sourcesContent":["/**\n * The WebGL under the `wave` and `fluid-dots` orbs: one fullscreen triangle drawn by one\n * fragment shader, and its uniforms (the time, the signal, the host's colour). Nothing here knows\n * React, a call or a state; the frame loop is `motion`'s.\n */\n\n/** A style of orb drawn by a fragment shader. */\nexport interface Shader {\n /** GLSL ES 1.00 source of the fragment shader; it reads the uniforms named in `Uniforms`. */\n fragment: string;\n /**\n * How fast the shader's clock runs at an energy, 0 (idle) to 1 (listening, speaking): 1 is the\n * shader's own pace. The clock is summed frame by frame on this side, so a change of pace bends\n * the motion instead of making it jump, as a time multiplied inside the shader would.\n */\n pace(energy: number): number;\n}\n\n/** What one frame is drawn from. */\nexport interface Uniforms {\n /** `iTime`: seconds on the shader's own clock, already paced. */\n time: number;\n /** `uLevel`: how loud the microphone or the voice is, 0 to 1. */\n level: number;\n /** `uEnergy`: 0 at rest, 0.5 while thinking, 1 while listening or speaking. */\n energy: number;\n}\n\n/** A colour as the shader takes it: red, green and blue, each 0 to 1. */\nexport type Rgb = readonly [number, number, number];\n\nexport interface Renderer {\n draw(values: Uniforms): void;\n /** Paints the next frames in `ink` (`uInk`), or (null) in the shader's own colours. */\n ink(ink: Rgb | null): void;\n dispose(): void;\n}\n\nconst VERTEX = `attribute vec2 aPos; void main(){ gl_Position=vec4(aPos,0.0,1.0); }`;\n\n/**\n * The line every shader ends its `main` with: the shaders paint light on black, and an orb sits on\n * any background, so what is near black becomes transparent instead. The canvas is premultiplied,\n * so the colour goes with its alpha. There is no black square around the orb.\n */\nexport const CUTOUT = `gl_FragColor.a *= smoothstep(0.02, 0.12, max(gl_FragColor.r, max(gl_FragColor.g, gl_FragColor.b))); gl_FragColor.rgb *= gl_FragColor.a;`;\n\n/** The uniforms of the host's colour, which every shader declares beside its own. */\nexport const INK_UNIFORMS = `uniform vec3 uInk; uniform float uInked; // signalcore: the host's colour`;\n\n/**\n * The line before `CUTOUT`: with the host's colour (`uInked` 1), the shader's light is painted\n * in it, as bright as it was (its brightest channel); without, the shader's own colours stay.\n */\nexport const INK = `gl_FragColor.rgb = mix(gl_FragColor.rgb, uInk * max(gl_FragColor.r, max(gl_FragColor.g, gl_FragColor.b)), uInked);`;\n\n/**\n * What the shader paints so that, blended by exclusion over `surface`, its brightest light shows\n * as `color`: the colour itself over black, its opposite over white (exclusion turns it over),\n * and between them, channel by channel, what turns into it as far as it can; a channel of the\n * surface near the middle turns nothing over, and keeps the colour.\n */\nexport function inkOver(color: Rgb, surface: Rgb): Rgb {\n const channel = (wanted: number, under: number): number => {\n const turn = 1 - 2 * under;\n if (Math.abs(turn) < 0.2) return wanted;\n return Math.min(1, Math.max(0, (wanted - under) / turn));\n };\n return [\n channel(color[0], surface[0]),\n channel(color[1], surface[1]),\n channel(color[2], surface[2]),\n ];\n}\n\n/** A pixel to read colours off: any CSS colour, as the browser paints it. */\nlet swatch: CanvasRenderingContext2D | null | undefined;\n\n/** A CSS colour as red, green, blue and alpha, each 0 to 1; null where it can't be read. */\nfunction rgbaOf(css: string): readonly [number, number, number, number] | null {\n if (swatch === undefined) {\n swatch = document.createElement(\"canvas\").getContext(\"2d\", { willReadFrequently: true });\n }\n if (swatch === null || css === \"\") return null;\n swatch.clearRect(0, 0, 1, 1);\n swatch.fillStyle = \"#000\";\n swatch.fillStyle = css;\n swatch.fillRect(0, 0, 1, 1);\n const [r = 0, g = 0, b = 0, a = 0] = swatch.getImageData(0, 0, 1, 1).data;\n return [r / 255, g / 255, b / 255, a / 255];\n}\n\n/**\n * The host's colour for an orb's canvas, as its shader paints it, from what the stylesheet puts\n * on the canvas: the colour (`color`: `--sc-orb-color`, transparent when none) and the surface it\n * blends with (`outline-color`; a page's own, light, where the surface is see-through). Null:\n * the shader's own colours.\n */\nexport function inkOf(canvas: HTMLCanvasElement): Rgb | null {\n const style = getComputedStyle(canvas);\n const color = rgbaOf(style.color);\n if (color === null || color[3] === 0) return null;\n const surface = rgbaOf(style.outlineColor);\n const under: Rgb =\n surface === null || surface[3] < 0.5 ? [1, 1, 1] : [surface[0], surface[1], surface[2]];\n return inkOver([color[0], color[1], color[2]], under);\n}\n\nfunction compile(gl: WebGLRenderingContext, type: number, source: string): WebGLShader {\n const shader = gl.createShader(type);\n if (!shader) throw new Error(\"no shader\");\n gl.shaderSource(shader, source);\n gl.compileShader(shader);\n if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n const log = gl.getShaderInfoLog(shader);\n gl.deleteShader(shader);\n throw new Error(log ?? \"shader compile error\");\n }\n return shader;\n}\n\n/** The program of a fragment shader, linked: the triangle's vertex shader and `fragment`. */\nfunction link(gl: WebGLRenderingContext, fragment: string): WebGLProgram {\n const program = gl.createProgram();\n const vertex = compile(gl, gl.VERTEX_SHADER, VERTEX);\n const pixels = compile(gl, gl.FRAGMENT_SHADER, fragment);\n gl.attachShader(program, vertex);\n gl.attachShader(program, pixels);\n gl.linkProgram(program);\n // Linked, the program keeps what it needs: the shaders can go.\n gl.deleteShader(vertex);\n gl.deleteShader(pixels);\n if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n const log = gl.getProgramInfoLog(program);\n gl.deleteProgram(program);\n throw new Error(log ?? \"shader link error\");\n }\n return program;\n}\n\n/**\n * Sets `canvas` up to draw `fragment` on a square of `dim` pixels, compiled once: the program, the\n * triangle and the uniforms' places are kept for every frame after. Nothing when WebGL is not\n * there or the shader does not compile on this device, and `onLost` is called if the browser\n * takes the context away later (a GPU reset, too many contexts on the page).\n */\nexport function createRenderer(\n canvas: HTMLCanvasElement,\n fragment: string,\n dim: number,\n onLost: () => void,\n): Renderer | undefined {\n const gl = canvas.getContext(\"webgl\", { alpha: true, premultipliedAlpha: true });\n if (!gl) return undefined;\n\n let program: WebGLProgram;\n try {\n program = link(gl, fragment);\n } catch {\n return undefined;\n }\n gl.useProgram(program);\n\n const buffer = gl.createBuffer();\n gl.bindBuffer(gl.ARRAY_BUFFER, buffer);\n // One triangle bigger than the screen: three vertices cover it, with no seam across a quad.\n gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);\n const position = gl.getAttribLocation(program, \"aPos\");\n gl.enableVertexAttribArray(position);\n gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);\n\n canvas.width = dim;\n canvas.height = dim;\n gl.viewport(0, 0, dim, dim);\n gl.clearColor(0, 0, 0, 0);\n // A uniform a shader does not use is optimised out and has no place: setting it does nothing.\n gl.uniform2f(gl.getUniformLocation(program, \"iResolution\"), dim, dim);\n const time = gl.getUniformLocation(program, \"iTime\");\n const level = gl.getUniformLocation(program, \"uLevel\");\n const energy = gl.getUniformLocation(program, \"uEnergy\");\n const inkColor = gl.getUniformLocation(program, \"uInk\");\n const inked = gl.getUniformLocation(program, \"uInked\");\n gl.uniform1f(inked, 0);\n\n const lost = (event: Event) => {\n event.preventDefault();\n onLost();\n };\n canvas.addEventListener(\"webglcontextlost\", lost);\n\n return {\n draw(values) {\n gl.uniform1f(time, values.time);\n gl.uniform1f(level, values.level);\n gl.uniform1f(energy, values.energy);\n gl.clear(gl.COLOR_BUFFER_BIT);\n gl.drawArrays(gl.TRIANGLES, 0, 3);\n },\n ink(ink) {\n if (ink !== null) gl.uniform3f(inkColor, ink[0], ink[1], ink[2]);\n gl.uniform1f(inked, ink === null ? 0 : 1);\n },\n dispose() {\n canvas.removeEventListener(\"webglcontextlost\", lost);\n gl.deleteProgram(program);\n gl.deleteBuffer(buffer);\n },\n };\n}\n"],"mappings":";AAsCA,MAAM,SAAS;;;;;;AAOf,MAAa,SAAS;;AAGtB,MAAa,eAAe;;;;;AAM5B,MAAa,MAAM;;;;;;;AAQnB,SAAgB,QAAQ,OAAY,SAAmB;CACrD,MAAM,WAAW,QAAgB,UAA0B;EACzD,MAAM,OAAO,IAAI,IAAI;EACrB,IAAI,KAAK,IAAI,IAAI,IAAI,IAAK,OAAO;EACjC,OAAO,KAAK,IAAI,GAAG,KAAK,IAAI,IAAI,SAAS,SAAS,IAAI,CAAC;CACzD;CACA,OAAO;EACL,QAAQ,MAAM,IAAI,QAAQ,EAAE;EAC5B,QAAQ,MAAM,IAAI,QAAQ,EAAE;EAC5B,QAAQ,MAAM,IAAI,QAAQ,EAAE;CAC9B;AACF;;AAGA,IAAI;;AAGJ,SAAS,OAAO,KAA+D;CAC7E,IAAI,WAAW,KAAA,GACb,SAAS,SAAS,cAAc,QAAQ,CAAC,CAAC,WAAW,MAAM,EAAE,oBAAoB,KAAK,CAAC;CAEzF,IAAI,WAAW,QAAQ,QAAQ,IAAI,OAAO;CAC1C,OAAO,UAAU,GAAG,GAAG,GAAG,CAAC;CAC3B,OAAO,YAAY;CACnB,OAAO,YAAY;CACnB,OAAO,SAAS,GAAG,GAAG,GAAG,CAAC;CAC1B,MAAM,CAAC,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,KAAK,OAAO,aAAa,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC;CACrE,OAAO;EAAC,IAAI;EAAK,IAAI;EAAK,IAAI;EAAK,IAAI;CAAG;AAC5C;;;;;;;AAQA,SAAgB,MAAM,QAAuC;CAC3D,MAAM,QAAQ,iBAAiB,MAAM;CACrC,MAAM,QAAQ,OAAO,MAAM,KAAK;CAChC,IAAI,UAAU,QAAQ,MAAM,OAAO,GAAG,OAAO;CAC7C,MAAM,UAAU,OAAO,MAAM,YAAY;CACzC,MAAM,QACJ,YAAY,QAAQ,QAAQ,KAAK,KAAM;EAAC;EAAG;EAAG;CAAC,IAAI;EAAC,QAAQ;EAAI,QAAQ;EAAI,QAAQ;CAAE;CACxF,OAAO,QAAQ;EAAC,MAAM;EAAI,MAAM;EAAI,MAAM;CAAE,GAAG,KAAK;AACtD;AAEA,SAAS,QAAQ,IAA2B,MAAc,QAA6B;CACrF,MAAM,SAAS,GAAG,aAAa,IAAI;CACnC,IAAI,CAAC,QAAQ,MAAM,IAAI,MAAM,WAAW;CACxC,GAAG,aAAa,QAAQ,MAAM;CAC9B,GAAG,cAAc,MAAM;CACvB,IAAI,CAAC,GAAG,mBAAmB,QAAQ,GAAG,cAAc,GAAG;EACrD,MAAM,MAAM,GAAG,iBAAiB,MAAM;EACtC,GAAG,aAAa,MAAM;EACtB,MAAM,IAAI,MAAM,OAAO,sBAAsB;CAC/C;CACA,OAAO;AACT;;AAGA,SAAS,KAAK,IAA2B,UAAgC;CACvE,MAAM,UAAU,GAAG,cAAc;CACjC,MAAM,SAAS,QAAQ,IAAI,GAAG,eAAe,MAAM;CACnD,MAAM,SAAS,QAAQ,IAAI,GAAG,iBAAiB,QAAQ;CACvD,GAAG,aAAa,SAAS,MAAM;CAC/B,GAAG,aAAa,SAAS,MAAM;CAC/B,GAAG,YAAY,OAAO;CAEtB,GAAG,aAAa,MAAM;CACtB,GAAG,aAAa,MAAM;CACtB,IAAI,CAAC,GAAG,oBAAoB,SAAS,GAAG,WAAW,GAAG;EACpD,MAAM,MAAM,GAAG,kBAAkB,OAAO;EACxC,GAAG,cAAc,OAAO;EACxB,MAAM,IAAI,MAAM,OAAO,mBAAmB;CAC5C;CACA,OAAO;AACT;;;;;;;AAQA,SAAgB,eACd,QACA,UACA,KACA,QACsB;CACtB,MAAM,KAAK,OAAO,WAAW,SAAS;EAAE,OAAO;EAAM,oBAAoB;CAAK,CAAC;CAC/E,IAAI,CAAC,IAAI,OAAO,KAAA;CAEhB,IAAI;CACJ,IAAI;EACF,UAAU,KAAK,IAAI,QAAQ;CAC7B,QAAQ;EACN;CACF;CACA,GAAG,WAAW,OAAO;CAErB,MAAM,SAAS,GAAG,aAAa;CAC/B,GAAG,WAAW,GAAG,cAAc,MAAM;CAErC,GAAG,WAAW,GAAG,cAAc,IAAI,aAAa;EAAC;EAAI;EAAI;EAAG;EAAI;EAAI;CAAC,CAAC,GAAG,GAAG,WAAW;CACvF,MAAM,WAAW,GAAG,kBAAkB,SAAS,MAAM;CACrD,GAAG,wBAAwB,QAAQ;CACnC,GAAG,oBAAoB,UAAU,GAAG,GAAG,OAAO,OAAO,GAAG,CAAC;CAEzD,OAAO,QAAQ;CACf,OAAO,SAAS;CAChB,GAAG,SAAS,GAAG,GAAG,KAAK,GAAG;CAC1B,GAAG,WAAW,GAAG,GAAG,GAAG,CAAC;CAExB,GAAG,UAAU,GAAG,mBAAmB,SAAS,aAAa,GAAG,KAAK,GAAG;CACpE,MAAM,OAAO,GAAG,mBAAmB,SAAS,OAAO;CACnD,MAAM,QAAQ,GAAG,mBAAmB,SAAS,QAAQ;CACrD,MAAM,SAAS,GAAG,mBAAmB,SAAS,SAAS;CACvD,MAAM,WAAW,GAAG,mBAAmB,SAAS,MAAM;CACtD,MAAM,QAAQ,GAAG,mBAAmB,SAAS,QAAQ;CACrD,GAAG,UAAU,OAAO,CAAC;CAErB,MAAM,QAAQ,UAAiB;EAC7B,MAAM,eAAe;EACrB,OAAO;CACT;CACA,OAAO,iBAAiB,oBAAoB,IAAI;CAEhD,OAAO;EACL,KAAK,QAAQ;GACX,GAAG,UAAU,MAAM,OAAO,IAAI;GAC9B,GAAG,UAAU,OAAO,OAAO,KAAK;GAChC,GAAG,UAAU,QAAQ,OAAO,MAAM;GAClC,GAAG,MAAM,GAAG,gBAAgB;GAC5B,GAAG,WAAW,GAAG,WAAW,GAAG,CAAC;EAClC;EACA,IAAI,KAAK;GACP,IAAI,QAAQ,MAAM,GAAG,UAAU,UAAU,IAAI,IAAI,IAAI,IAAI,IAAI,EAAE;GAC/D,GAAG,UAAU,OAAO,QAAQ,OAAO,IAAI,CAAC;EAC1C;EACA,UAAU;GACR,OAAO,oBAAoB,oBAAoB,IAAI;GACnD,GAAG,cAAc,OAAO;GACxB,GAAG,aAAa,MAAM;EACxB;CACF;AACF"}
|