@signalcore/ui 1.0.0-beta.2 → 1.0.0-beta.4
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 +126 -21
- package/dist/AgentAbout.js +6 -2
- package/dist/AgentAbout.js.map +1 -1
- package/dist/AgentChat.d.ts +25 -3
- package/dist/AgentChat.js +302 -28
- package/dist/AgentChat.js.map +1 -1
- package/dist/AgentSpace.d.ts +25 -4
- package/dist/AgentSpace.js +55 -11
- package/dist/AgentSpace.js.map +1 -1
- package/dist/Threads.d.ts +73 -0
- package/dist/Threads.js +140 -0
- package/dist/Threads.js.map +1 -0
- package/dist/View.d.ts +48 -0
- package/dist/View.js +101 -0
- package/dist/View.js.map +1 -0
- package/dist/automation/marks.js +7 -3
- package/dist/automation/marks.js.map +1 -1
- package/dist/automation/messages.d.ts +4 -0
- package/dist/automation/messages.js +5 -1
- package/dist/automation/messages.js.map +1 -1
- package/dist/automation/when.js +8 -0
- package/dist/automation/when.js.map +1 -1
- package/dist/blocks.d.ts +83 -2
- package/dist/client.d.ts +23 -5
- package/dist/client.js +8 -2
- package/dist/client.js.map +1 -1
- package/dist/contract.d.ts +9 -4
- package/dist/es.d.ts +2 -0
- package/dist/es.js +34 -3
- package/dist/es.js.map +1 -1
- package/dist/frames.d.ts +7 -1
- package/dist/frames.js.map +1 -1
- package/dist/index.d.ts +14 -7
- package/dist/index.js +11 -4
- package/dist/kit/Agenda.d.ts +25 -0
- package/dist/kit/Agenda.js +119 -0
- package/dist/kit/Agenda.js.map +1 -0
- package/dist/kit/Combobox.js +1 -1
- package/dist/kit/DateField.js +1 -1
- package/dist/kit/Kanban.d.ts +17 -0
- package/dist/kit/Kanban.js +157 -0
- package/dist/kit/Kanban.js.map +1 -0
- package/dist/kit/TimeField.js +1 -1
- package/dist/kit/Timeline.d.ts +14 -0
- package/dist/kit/Timeline.js +71 -0
- package/dist/kit/Timeline.js.map +1 -0
- package/dist/kit/context.d.ts +1 -1
- package/dist/kit/context.js.map +1 -1
- package/dist/kit/messages.d.ts +22 -0
- package/dist/kit/messages.js +10 -0
- package/dist/kit/messages.js.map +1 -1
- package/dist/surface/Form.js +1 -1
- package/dist/surface/Placed.d.ts +20 -0
- package/dist/surface/Placed.js +70 -0
- package/dist/surface/Placed.js.map +1 -0
- package/dist/surface/Surface.d.ts +4 -2
- package/dist/surface/Surface.js +78 -2
- package/dist/surface/Surface.js.map +1 -1
- package/dist/surface/places.d.ts +16 -0
- package/dist/surface/places.js +15 -0
- package/dist/surface/places.js.map +1 -0
- package/dist/surface/scope.d.ts +17 -1
- package/dist/surface/scope.js +1 -0
- package/dist/surface/scope.js.map +1 -1
- package/dist/types.d.ts +23 -0
- package/dist/types.js.map +1 -1
- package/dist/useConversation.d.ts +2 -0
- package/dist/useConversation.js +16 -0
- package/dist/useConversation.js.map +1 -1
- package/dist/widget.js +17 -16
- package/package.json +1 -1
- package/src/AgentAbout.tsx +4 -0
- package/src/AgentChat.tsx +417 -34
- package/src/AgentSpace.tsx +106 -14
- package/src/Threads.tsx +218 -0
- package/src/View.tsx +117 -0
- package/src/automation/marks.tsx +7 -0
- package/src/automation/messages.ts +8 -0
- package/src/automation/when.ts +4 -0
- package/src/blocks.ts +100 -2
- package/src/client.ts +42 -10
- package/src/contract.ts +404 -29
- package/src/es.ts +30 -0
- package/src/frames.ts +3 -1
- package/src/index.ts +36 -3
- package/src/kit/Agenda.tsx +161 -0
- package/src/kit/Kanban.tsx +182 -0
- package/src/kit/Timeline.tsx +58 -0
- package/src/kit/context.tsx +45 -1
- package/src/kit/messages.ts +31 -0
- package/src/styles.css +825 -66
- package/src/surface/Placed.tsx +98 -0
- package/src/surface/Surface.tsx +92 -1
- package/src/surface/places.ts +19 -0
- package/src/surface/scope.ts +24 -1
- package/src/types.ts +24 -0
- package/src/useConversation.ts +10 -0
- package/src/widget/element.tsx +54 -7
- package/src/widget/widget.css +13 -0
package/README.md
CHANGED
|
@@ -185,6 +185,62 @@ couldn't begin again), `close`, `agent` (its name when the face can't be read),
|
|
|
185
185
|
(`--text`, `--voice`; `aria-checked` on the one on), `sc-space__caption` (`--person`, `--agent`),
|
|
186
186
|
`sc-space__status`.
|
|
187
187
|
|
|
188
|
+
### Panels, modals and your own blocks
|
|
189
|
+
|
|
190
|
+
What the agent shows can stand apart from the conversation (`show` and `ask_person` with
|
|
191
|
+
`where`, v3/UI.md, Surfaces): in a **panel** beside the chat (over it, where the space is narrow:
|
|
192
|
+
the rep's week while they talk), or in a **modal** over the page (something to answer now; once
|
|
193
|
+
used it closes and the chat goes on). Each opens as it comes, and a conversation that opens
|
|
194
|
+
waiting on one opens it; the reply keeps a quiet line, "Ray abrió tu semana" (`opened`), that
|
|
195
|
+
opens it again. Your page decides what it allows: `surfaces={{ panel: true, modal: false }}`
|
|
196
|
+
(both by default); a place left out shows in the conversation, as on a channel. `AgentChat`
|
|
197
|
+
takes the same `surfaces`.
|
|
198
|
+
|
|
199
|
+
`blocks` registers your own blocks by the name each was declared with (`PUT /v1/blocks/{name}`):
|
|
200
|
+
`blocks={{ "demoprep.deal-card": DealCard }}`. The agent gives only props, which the API checked
|
|
201
|
+
against your schema; your component gets `{props, text, active, onAction}` (`HostBlockProps`),
|
|
202
|
+
and a block it has no component for shows its words in a quiet card (`sc-surface__host`).
|
|
203
|
+
|
|
204
|
+
```tsx
|
|
205
|
+
function DealCard({ props }: HostBlockProps<{ title: string; amount: number }>) {
|
|
206
|
+
return <YourDealCard title={props.title} amount={props.amount} />;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
<AgentSpace
|
|
210
|
+
client={client}
|
|
211
|
+
agentId="agt_…"
|
|
212
|
+
surfaces={{ panel: true, modal: true }}
|
|
213
|
+
blocks={{ "demoprep.deal-card": DealCard }}
|
|
214
|
+
/>;
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Words: `opened(name, title)`, `closePlaced`. Parts: `sc-chat__placed` (`--open`), `sc-chat--placed`
|
|
218
|
+
(a panel is open), `sc-placed` (`--panel`, `--modal`, the latter a `<dialog>`),
|
|
219
|
+
`sc-placed__head`, `sc-placed__title`, `sc-placed__body`.
|
|
220
|
+
|
|
221
|
+
### Threads, unread, and several yeses at once
|
|
222
|
+
|
|
223
|
+
A person keeps several conversations (threads) with the agent, each with its title: one your
|
|
224
|
+
backend opened for them (`conversations.start` with `contact`, `title`, `say`), one they began.
|
|
225
|
+
`AgentSpace` shows a line over the chat, **Conversaciones**, with a dot while another thread has
|
|
226
|
+
something for them; it lists them by title with how many of the agent's messages are unread, to
|
|
227
|
+
go to one or start another (`threads={false}` turns it off). The chat reads what comes as it
|
|
228
|
+
shows it on a person's page (`POST /conversations/{id}/read`), so `unread` follows; the widget's
|
|
229
|
+
launcher wears a dot while something waits, for someone who talked before.
|
|
230
|
+
|
|
231
|
+
For your own layout: `useUnread(client, agentId)` (`{threads, unread, waiting, refresh}`, read
|
|
232
|
+
every 20 s and when the page comes back into sight), `<Threads threads current onSelect onNew
|
|
233
|
+
onBack />` and `<Conversation client id … />` (the chat of one thread, `AgentChat`'s props).
|
|
234
|
+
Words: `THREAD_MESSAGES`, `THREAD_MESSAGES_ES`. Parts: `sc-threads`, `sc-threads__head`,
|
|
235
|
+
`sc-threads__title`, `sc-threads__list`, `sc-threads__item` (`--current`), `sc-threads__name`,
|
|
236
|
+
`sc-threads__badge`, `sc-threads__dot`, `sc-space__threads`, `sc-space__dot`.
|
|
237
|
+
|
|
238
|
+
When one reply waits on the person's yes for several actions, they come in one card
|
|
239
|
+
(`sc-chat__requests`): a box ticked for each, what each would take under it, editable, and one
|
|
240
|
+
button, **Aprobar todo** (or as many as are ticked), sending a yes for each ticked (with what was
|
|
241
|
+
changed: `data.input`) and a no for the rest, in one `actions` frame. Words: `approvals`,
|
|
242
|
+
`approveAll`, `approveSome`, `declineAll`.
|
|
243
|
+
|
|
188
244
|
## The widget
|
|
189
245
|
|
|
190
246
|
`<signalcore-chat>` for pages without React: one script, React and the stylesheet inside it,
|
|
@@ -234,6 +290,10 @@ flow at the element's size (560px tall unless your CSS says). A visitor's conver
|
|
|
234
290
|
| `stylesheet` | your own CSS for the widget: the CSS itself (anything with a `{`) or its address; adopted inside the shadow root, after ours. Also the element's `stylesheet` property |
|
|
235
291
|
| `unstyled` | our look's defaults drop away (grounds, lines, shadows, corners, every ink but the page's text), the layout stays: for a host that styles everything |
|
|
236
292
|
| `shadow` | `none`: no shadow root; the widget draws in the page (our stylesheet goes first in `<head>`, so your CSS wins), for a host whose global CSS should apply directly. Read once, as the element connects |
|
|
293
|
+
| `surfaces` | where the agent may put what it shows besides the conversation: `panel`, `modal`, both (`"panel modal"`, the default) or none (`""`) |
|
|
294
|
+
|
|
295
|
+
Your own blocks go as the element's `blocks` property, React components the widget's React
|
|
296
|
+
draws: `document.querySelector("signalcore-chat").blocks = { "demoprep.deal-card": DealCard }`.
|
|
237
297
|
|
|
238
298
|
By default the widget takes your page's type and ink: the font, the base size and the text colour
|
|
239
299
|
the element inherits come through the shadow root and stand in for our defaults (read as it draws,
|
|
@@ -317,23 +377,26 @@ from its credentials: `"business"` (our app's cookie) or `"person"` (a token, a
|
|
|
317
377
|
visitor); the chat draws a yes the business decides by it, and shows a person none of the
|
|
318
378
|
agent's steps (`steps`).
|
|
319
379
|
|
|
320
|
-
| Method | Does
|
|
321
|
-
| --------------------------------------------------------------------------- |
|
|
322
|
-
| `agents.face(id)` | what a person may know of the agent (`GET /agents/{id}/face`): `AgentFace {id, name, role, icon, voice: {enabled}}`
|
|
323
|
-
| `agents.about(id)` | what the agent says about itself (`about`, null until written; `about_at`, null once stale) and its `facts`
|
|
324
|
-
| `agents.describe(id)` | has the agent write about itself anew (a model call, a few seconds); answers like `about`
|
|
325
|
-
| `agents.files(id)` | the agent's own files (`StoredFile`: `id, path, media_type, size, searchable, created_at`), by path
|
|
326
|
-
| `agents.putFile(id, path, blob, searchable)` | stores the blob's bytes under the path (replacing a file of that name), its type as the file's; `searchable` makes it knowledge `search_files` reads (text, Markdown, JSON, YAML, PDF). 50 MB at most, 5 GB per organization
|
|
327
|
-
| `agents.deleteFile(id, path)` | removes one of the agent's own files
|
|
328
|
-
| `agents.memories(id)` | what the agent remembered (`MemoryFact`: `id, text, created_at`), newest first
|
|
329
|
-
| `agents.forget(id, memoryId)` | takes one memory from it
|
|
330
|
-
| `agents.call(id)` |
|
|
331
|
-
| `files.list()` · `files.put(path, blob, searchable)` · `files.delete(path)` | the organization's files, shared with every agent: the same three as the agent's own
|
|
332
|
-
| `conversations.get(id)` | one conversation with its messages, who answers it (`handled_by`: `agent`, `person` or `nobody`) and every change of it (`handovers`); on a channel, `channel`, `contact` (`muted`) and each message's `author`, `delivery` and `decision`
|
|
333
|
-
| `conversations.
|
|
334
|
-
| `conversations.
|
|
335
|
-
| `
|
|
336
|
-
| `
|
|
380
|
+
| Method | Does |
|
|
381
|
+
| --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
382
|
+
| `agents.face(id)` | what a person may know of the agent (`GET /agents/{id}/face`): `AgentFace {id, name, role, icon, voice: {enabled}}` |
|
|
383
|
+
| `agents.about(id)` | what the agent says about itself (`about`, null until written; `about_at`, null once stale) and its `facts` |
|
|
384
|
+
| `agents.describe(id)` | has the agent write about itself anew (a model call, a few seconds); answers like `about` |
|
|
385
|
+
| `agents.files(id)` | the agent's own files (`StoredFile`: `id, path, media_type, size, searchable, created_at`), by path |
|
|
386
|
+
| `agents.putFile(id, path, blob, searchable)` | stores the blob's bytes under the path (replacing a file of that name), its type as the file's; `searchable` makes it knowledge `search_files` reads (text, Markdown, JSON, YAML, PDF). 50 MB at most, 5 GB per organization |
|
|
387
|
+
| `agents.deleteFile(id, path)` | removes one of the agent's own files |
|
|
388
|
+
| `agents.memories(id)` | what the agent remembered (`MemoryFact`: `id, text, created_at`), newest first |
|
|
389
|
+
| `agents.forget(id, memoryId)` | takes one memory from it |
|
|
390
|
+
| `agents.call(id)` | **app only** (`POST /agents/{id}/voice/token` is `/api`'s, 404 on `/v1`): the owner's call with the agent from our app, `CallToken {url, token, room, call_id, conversation_id}`, LiveKit's address and a token for the call's room, and the call's conversation (a test), made now. A host's page uses `conversations.call(agentId)` |
|
|
391
|
+
| `files.list()` · `files.put(path, blob, searchable)` · `files.delete(path)` | the organization's files, shared with every agent: the same three as the agent's own |
|
|
392
|
+
| `conversations.get(id)` | one conversation with its messages, who answers it (`handled_by`: `agent`, `person` or `nobody`) and every change of it (`handovers`); on a channel, `channel`, `contact` (`muted`) and each message's `author`, `delivery` and `decision` |
|
|
393
|
+
| `conversations.list(agentId)` | the agent's conversations, newest first (a person's own), each with `title`, `unread` and `waiting_on_person` |
|
|
394
|
+
| `conversations.start(agentId, { title })` | a new conversation with an agent, titled when given |
|
|
395
|
+
| `conversations.read(id)` | how far the person has read it: all of it, now (the chat calls it as it shows what comes) |
|
|
396
|
+
| `conversations.rename(id, title)` | its title, as the person gives it |
|
|
397
|
+
| `conversations.call(agentId)` | a call with the agent for whoever may talk to it, a person's as theirs (`POST /agents/{id}/calls`): a `CallToken`, what `VoiceCall`'s `connect` answers, with the call's own conversation, made now, where what is said lands |
|
|
398
|
+
| `channels.list(agentId?)` | the organization's channels, or one agent's (`Channel`: `id, agent_id, kind, provider, name, external_id, status, detail, settings, created_at`), the disconnected ones too |
|
|
399
|
+
| `liveUrl(conversationId, { fresh? })` | the socket's address, signed in (a promise): `ws:` beside `http:` and `wss:` beside `https:`, with `?token=` (and `&visitor=`) but in our app; `fresh` asks `getToken` for a new token first |
|
|
337
400
|
|
|
338
401
|
## The frames
|
|
339
402
|
|
|
@@ -378,15 +441,15 @@ from how the call's promise settled).
|
|
|
378
441
|
the page and its answer goes up (each call once; one told again is answered again with what it
|
|
379
442
|
gave; the turn stays under way until its step settles); without it each is refused with
|
|
380
443
|
`NO_TOOL`. It answers `{ messages, draft, pending,
|
|
381
|
-
awaiting, open, loading, error, say(text), act(action), stop(), browser,
|
|
444
|
+
awaiting, open, loading, error, say(text), act(action), actAll(actions), stop(), browser,
|
|
382
445
|
watchScreen(listener), control(to), input(event), holder, handovers }`. The history, read each
|
|
383
446
|
time the socket opens and kept live by its frames;
|
|
384
447
|
`pending` from the person's message to `done`, `stopped` or a turn error, `draft` the reply so
|
|
385
448
|
far. What `step`, `ui` and `request` frames say lands on the reply's steps, so a message is
|
|
386
449
|
the one place a step is drawn from, live or read back. `awaiting` is the step the turn waits
|
|
387
450
|
on: a surface to answer, or an action waiting for the person's leave (the step's `request`,
|
|
388
|
-
`decides: "person"`; `act` on its id with `approve` or `decline
|
|
389
|
-
act. A yes the business gives (`decides: "business"`) waits on its step and holds nothing; the
|
|
451
|
+
`decides: "person"`; `act` on its id with `approve` or `decline`, `actAll` on several at once,
|
|
452
|
+
a yes's `data.input` changing what it runs with); `say` is refused until they act. A yes the business gives (`decides: "business"`) waits on its step and holds nothing; the
|
|
390
453
|
person's answer on one, refused `business`, puts it back as the business's. `onError(code)`
|
|
391
454
|
hears the socket's codes and `load` (the
|
|
392
455
|
history could not be read): the host words them, the hook shows no text; `tool` (an answer of
|
|
@@ -509,7 +572,7 @@ an agent, the one a customer has on the phone, through LiveKit (`livekit-client`
|
|
|
509
572
|
when it mounts and ends when it unmounts (key it on the attempt for another). It opens the
|
|
510
573
|
microphone first (no call is made for someone who can't be heard), then asks `connect()` where to
|
|
511
574
|
join (`{url, token}`: our API's `client.conversations.call(agentId)` answers both, with the
|
|
512
|
-
call's conversation; `client.agents.call(id)` for the owner's test), joins the room and publishes the microphone; the agent's voice plays as it comes,
|
|
575
|
+
call's conversation; `client.agents.call(id)` for the owner's test in our own app, never on `/v1`), joins the room and publishes the microphone; the agent's voice plays as it comes,
|
|
513
576
|
and the call is on once the agent joins (its time counts from then).
|
|
514
577
|
|
|
515
578
|
What it draws, centred: the `Orb`, large (`orbSize`, 280 px; `orb`: `"wave"`, the default, or
|
|
@@ -705,6 +768,48 @@ works, a check and "Conectada" in its place (`sc-surface__connect--done`), and a
|
|
|
705
768
|
it failed. It changes as the connection does, from the `ui` frames the API sends again on the
|
|
706
769
|
same id; before the API has asked for it (`connection` null), only the app as the agent named it.
|
|
707
770
|
|
|
771
|
+
The kit's blocks of time and work: a `calendar` (`Agenda`: a day's events, or a week's from its
|
|
772
|
+
`start`, at their hours; one with a `url` opens its page, with the calendar's `action` the rest
|
|
773
|
+
send `{event}`; narrow, the days go one under another, an empty one a quiet line; 680px and up,
|
|
774
|
+
the week stands as seven columns), a `timeline` (`Timeline`: dated things the newest first, each
|
|
775
|
+
a dot on a hairline, in its `tone` when it has one, its `status` a quiet word), a `board`
|
|
776
|
+
(`Kanban`: cards in columns; with its `action`, a card dragged to another column, or moved from
|
|
777
|
+
its button's list of the others, goes there at once and sends `{card, column}`, labelled
|
|
778
|
+
`board.moved`). A `host` block is one of yours, drawn by the component `blocks` registers under
|
|
779
|
+
its `name` (a component that fails as it draws leaves its words, and the rest draws). Words:
|
|
780
|
+
`calendar` (`allDay`, `empty`), `board` (`move`, `moveTo`, `moved`, `empty`). Parts:
|
|
781
|
+
`sc-agenda` (`--day`), `sc-agenda__days`, `sc-agenda__day` (`--today`, `--empty`),
|
|
782
|
+
`sc-agenda__date`, `sc-agenda__weekday`, `sc-agenda__number`, `sc-agenda__events`,
|
|
783
|
+
`sc-agenda__event`, `sc-agenda__time`, `sc-agenda__what`, `sc-agenda__title`,
|
|
784
|
+
`sc-agenda__detail`, `sc-agenda__empty`; `sc-timeline`, `sc-timeline__item` (`--info`,
|
|
785
|
+
`--success`, `--warning`, `--danger`), `sc-timeline__dot`, `sc-timeline__body`,
|
|
786
|
+
`sc-timeline__head`, `sc-timeline__title`, `sc-timeline__status`, `sc-timeline__when`,
|
|
787
|
+
`sc-timeline__text`; `sc-board`, `sc-board__column` (`--over`), `sc-board__head`,
|
|
788
|
+
`sc-board__title`, `sc-board__count`, `sc-board__cards`, `sc-board__card` (`--dragging`),
|
|
789
|
+
`sc-board__card-title`, `sc-board__card-subtitle`, `sc-board__card-meta`, `sc-board__move`,
|
|
790
|
+
`sc-board__menu`, `sc-board__empty`.
|
|
791
|
+
|
|
792
|
+
## View
|
|
793
|
+
|
|
794
|
+
`<View client name contact? blocks? components? classNames? messages? locale? onAction? />` draws
|
|
795
|
+
an agent's view where your page puts it: what the agent keeps on show for each person (their
|
|
796
|
+
next meeting, their week), a dashboard's spec rendered for one contact with their own rows
|
|
797
|
+
(`GET /views/{name}/render`). With a person's token it is theirs; for the business, `contact`
|
|
798
|
+
names whom (their id). It is read when shown and again every `VIEW_POLL_MS` (30 s), so it shows
|
|
799
|
+
what the rows hold now; while it is first read an empty box holds its place, and a view that
|
|
800
|
+
isn't there (or isn't the person's) draws nothing. `useView(client, name, contact?)` gives
|
|
801
|
+
`{view, problem, gone}` for your own layout (`view.title`, `view.blocks`, `view.text`).
|
|
802
|
+
|
|
803
|
+
```tsx
|
|
804
|
+
<View
|
|
805
|
+
client={client}
|
|
806
|
+
name="proxima_reunion"
|
|
807
|
+
blocks={{ "demoprep.deal-card": DealCard }}
|
|
808
|
+
messages={KIT_MESSAGES_ES}
|
|
809
|
+
locale={es}
|
|
810
|
+
/>
|
|
811
|
+
```
|
|
812
|
+
|
|
708
813
|
## AgentAbout
|
|
709
814
|
|
|
710
815
|
`<AgentAbout client agentId name messages? classNames? controls? apps? locale? view? onViewChange? onOpenFile? onOpenConversation? />`: Qué puede, made to sit
|
package/dist/AgentAbout.js
CHANGED
|
@@ -45,7 +45,9 @@ const ABOUT_MESSAGES = {
|
|
|
45
45
|
schedule: "At a time",
|
|
46
46
|
webhook: "When called",
|
|
47
47
|
event: "On an event",
|
|
48
|
-
platform: "On a message"
|
|
48
|
+
platform: "On a message",
|
|
49
|
+
manual: "When started",
|
|
50
|
+
after: "After another"
|
|
49
51
|
},
|
|
50
52
|
topics: {
|
|
51
53
|
apps: "Apps",
|
|
@@ -576,7 +578,9 @@ const KIND_ICONS = {
|
|
|
576
578
|
schedule: "clock",
|
|
577
579
|
webhook: "code",
|
|
578
580
|
event: "bell",
|
|
579
|
-
platform: "message"
|
|
581
|
+
platform: "message",
|
|
582
|
+
manual: "hand",
|
|
583
|
+
after: "check"
|
|
580
584
|
};
|
|
581
585
|
/** An app's mark: the host's logo when it draws one, else the provider's, bare. */
|
|
582
586
|
function appMark(app, apps, size) {
|