bunnyquery 1.8.3 → 1.8.5
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 +38 -39
- package/bunnyquery.css +108 -2
- package/bunnyquery.js +1531 -269
- package/dist/engine.cjs +1276 -209
- package/dist/engine.cjs.map +1 -1
- package/dist/engine.d.mts +812 -53
- package/dist/engine.d.ts +812 -53
- package/dist/engine.mjs +1254 -210
- package/dist/engine.mjs.map +1 -1
- package/package.json +1 -1
- package/src/engine/budget.ts +11 -11
- package/src/engine/history.ts +23 -6
- package/src/engine/host.ts +63 -5
- package/src/engine/image_preview.ts +0 -0
- package/src/engine/index.ts +12 -0
- package/src/engine/indexing_groups.ts +323 -6
- package/src/engine/link_markup.ts +124 -0
- package/src/engine/links.ts +159 -26
- package/src/engine/prompts/chat_system_prompt.ts +24 -14
- package/src/engine/prompts/indexing_system_prompt.ts +19 -11
- package/src/engine/prompts/indexing_user_message.ts +32 -22
- package/src/engine/requests.ts +281 -14
- package/src/engine/session.ts +1158 -143
- package/src/engine/viewport_fill.ts +51 -4
- package/styles/chat.css +108 -2
package/README.md
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
# BunnyQuery
|
|
2
2
|
|
|
3
3
|
An embeddable, dependency-free AI chat widget for [Skapi](https://www.skapi.com)-powered
|
|
4
|
-
projects. Drop it into any web page and your users get a full chat experience
|
|
4
|
+
projects. Drop it into any web page and your users get a full chat experience:
|
|
5
5
|
account login/signup, conversation history, file & folder uploads, and a settings
|
|
6
|
-
panel
|
|
6
|
+
panel, all talking to your project's **BunnyQuery** AI agent.
|
|
7
7
|
|
|
8
8
|
BunnyQuery is a standalone vanilla-JS port of the BunnyQuery (www.bunnyquery.com) agent
|
|
9
9
|
chatbox. The **widget** ships as a single IIFE that exposes `window.BunnyQuery` plus one
|
|
10
|
-
stylesheet
|
|
10
|
+
stylesheet. Drop it in via `<script>`, no build step or framework required.
|
|
11
11
|
|
|
12
12
|
The package also exports the **framework-agnostic chat engine** that powers it
|
|
13
|
-
(`bunnyquery/engine`)
|
|
13
|
+
(`bunnyquery/engine`), the same DOM-free core the Skapi admin chatbox consumes, so
|
|
14
14
|
you can build your own chat UI on top of it. See
|
|
15
15
|
[Importing the chat engine](#importing-the-chat-engine).
|
|
16
16
|
|
|
17
17
|
## Features
|
|
18
18
|
|
|
19
19
|
- **AI chat** against your project's configured agent (Claude or OpenAI under the
|
|
20
|
-
hood), with
|
|
21
|
-
- **Authentication
|
|
20
|
+
hood), with animated pending indicators and a background indexing queue.
|
|
21
|
+
- **Authentication**: email/password login, optional signup, password change,
|
|
22
22
|
email verification, account recovery, and "Sign in with Google".
|
|
23
|
-
- **Conversation history
|
|
23
|
+
- **Conversation history**: paginated, with "Fetching history…" indicators on
|
|
24
24
|
first load and on scroll-up.
|
|
25
|
-
- **Attachments
|
|
25
|
+
- **Attachments**: drag-and-drop files and folders, per-file upload status
|
|
26
26
|
(uploading / failed / indexed), overflow collapsing for large batches, and a
|
|
27
27
|
prompt when an upload hits a file that already exists (skip / reindex only /
|
|
28
28
|
overwrite, with "apply to all remaining"). Images are read with vision/OCR,
|
|
@@ -33,12 +33,12 @@ you can build your own chat UI on top of it. See
|
|
|
33
33
|
across as many passes as it takes. A file's passes collapse into a single
|
|
34
34
|
status row in the chat that can be expanded, and stopped: "Stop" cancels every
|
|
35
35
|
queued and running pass at once and ends the continuation chain.
|
|
36
|
-
- **Attachment parser plugins
|
|
36
|
+
- **Attachment parser plugins**: register a client-side parser so the widget
|
|
37
37
|
extracts text in the browser from formats the model can't otherwise read, and
|
|
38
38
|
indexes it directly. See [Attachment parser plugins](#attachment-parser-plugins).
|
|
39
|
-
- **Settings panel
|
|
39
|
+
- **Settings panel**, in place inside the chat: light/dark theme, account details,
|
|
40
40
|
newsletter subscription, clear history, and remove account.
|
|
41
|
-
- **Theming
|
|
41
|
+
- **Theming**: light and dark modes via CSS custom properties; the choice is
|
|
42
42
|
remembered in `localStorage` and falls back to the OS preference.
|
|
43
43
|
|
|
44
44
|
## Requirements
|
|
@@ -82,7 +82,7 @@ call `BunnyQuery.init()`:
|
|
|
82
82
|
</html>
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
-
That's it
|
|
85
|
+
That's it. BunnyQuery takes over the `#chatbox` element and renders the login or
|
|
86
86
|
chat view depending on the user's session.
|
|
87
87
|
|
|
88
88
|
## What's in the package
|
|
@@ -91,11 +91,11 @@ chat view depending on the user's session.
|
|
|
91
91
|
| ----------------------------- | -------------------------------------------------------------------------------- |
|
|
92
92
|
| `bunnyquery.js` | The widget IIFE. Exposes the global `window.BunnyQuery`. CDN / `<script>` drop-in. |
|
|
93
93
|
| `bunnyquery.css` | The widget's full stylesheet, scoped under `.bq-agent` / `[data-bq-theme]`. |
|
|
94
|
-
| `bunnyquery/engine` | The framework-agnostic chat engine
|
|
94
|
+
| `bunnyquery/engine` | The framework-agnostic chat engine. Ships as ESM + CJS with TypeScript types. |
|
|
95
95
|
| `bunnyquery/styles/chat.css` | The shared chat-surface styles (bubbles, markdown, links) for an engine-built UI. |
|
|
96
96
|
|
|
97
97
|
The two widget files can be hosted yourself (same origin recommended) or loaded from a
|
|
98
|
-
CDN
|
|
98
|
+
CDN, no npm needed. The `engine` / `styles` subpaths are for bundler consumers
|
|
99
99
|
(`npm install bunnyquery`); see [Importing the chat engine](#importing-the-chat-engine).
|
|
100
100
|
|
|
101
101
|
## API
|
|
@@ -123,6 +123,7 @@ Mounts the widget. Returns the `BunnyQuery` object.
|
|
|
123
123
|
| `mcpBaseUrl` | `string` | `null` | Override the MCP OAuth server base URL entirely (advanced). |
|
|
124
124
|
| `hostDomain` | `string` | `null` | db-CDN host for temporary file URLs. Defaults to `skapi.app` (dev) / `skapi.com` (prod). |
|
|
125
125
|
| `attachmentParsers` | `array` | `null` | Client-side attachment parsers. See [Attachment parser plugins](#attachment-parser-plugins). |
|
|
126
|
+
| `windowedIndexing` | `boolean` | `true` | Server-driven windowed indexing for text and grid files (see [file types](#supported-file-types)). Pass `false` to fall back to agent-driven paging, which keeps the traversal inside the model's turn budget and the tab open. |
|
|
126
127
|
|
|
127
128
|
### Methods
|
|
128
129
|
|
|
@@ -142,9 +143,9 @@ BunnyQuery.toggleTheme();
|
|
|
142
143
|
BunnyQuery.logout();
|
|
143
144
|
```
|
|
144
145
|
|
|
145
|
-
> `init()` is idempotent
|
|
146
|
+
> `init()` is idempotent: calling it twice logs a warning and returns the existing
|
|
146
147
|
> instance rather than re-mounting. On a successful mount it logs its version, e.g.
|
|
147
|
-
> `[bunnyquery] v1.
|
|
148
|
+
> `[bunnyquery] v1.8.3`.
|
|
148
149
|
|
|
149
150
|
See [HISTORY.md](HISTORY.md) for the release-by-release changelog.
|
|
150
151
|
|
|
@@ -209,13 +210,13 @@ upload: a 5MB `.txt` indexed 4.0% of its content, a 4.8MB `.json` 4.2%, a
|
|
|
209
210
|
because the agent received a plausible-looking document with no way to know most
|
|
210
211
|
of it was missing.
|
|
211
212
|
|
|
212
|
-
Two drivers exist for this loop.
|
|
213
|
-
the `readFileContent` tool. With the engine's `windowedIndexing` option enabled,
|
|
213
|
+
Two drivers exist for this loop. With `windowedIndexing` (the widget's default)
|
|
214
214
|
the **worker** reads a window per request and continues from the reader's own
|
|
215
|
-
cursor, so the traversal
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
215
|
+
cursor, so the traversal neither has to fit inside the model's turn budget nor
|
|
216
|
+
depends on the tab staying open. Without it the agent pages the file itself with
|
|
217
|
+
the `readFileContent` tool. Pass `windowedIndexing: false` to `init()` to opt
|
|
218
|
+
out; engine consumers opt in via `configureChatEngine` (see
|
|
219
|
+
[Importing the chat engine](#importing-the-chat-engine)).
|
|
219
220
|
|
|
220
221
|
### 4. Everything else extractable: inlined as text server-side
|
|
221
222
|
|
|
@@ -223,8 +224,10 @@ The skapi proxy downloads the file, extracts its text **server-side**, and
|
|
|
223
224
|
inlines that text into the request, so the model reads it directly with no
|
|
224
225
|
fetching. This keeps indexing consistent across model providers.
|
|
225
226
|
|
|
226
|
-
**Office & e-book** (binary/zip, parsed
|
|
227
|
-
|
|
227
|
+
**Office & e-book** (binary/zip, parsed; includes legacy binary `.doc`/`.xls`/`.ppt`
|
|
228
|
+
and the macro-enabled `.docm`/`.xlsm`/`.pptm`):
|
|
229
|
+
`.doc` · `.docx` · `.docm` · `.xls` · `.xlsx` · `.xlsm` · `.ppt` · `.pptx` · `.pptm`
|
|
230
|
+
· `.hwp` · `.hwpx` · `.ods` · `.odt` · `.odp` · `.epub`
|
|
228
231
|
|
|
229
232
|
**Text, data, markup & source code** (decoded as text; `.html`/`.htm` have their
|
|
230
233
|
tags stripped):
|
|
@@ -261,13 +264,9 @@ via `web_search` (external web access is enabled).
|
|
|
261
264
|
|
|
262
265
|
### Caveats
|
|
263
266
|
|
|
264
|
-
- **
|
|
265
|
-
`.xlsm` `.pptm` (macro-enabled) have no reliable server-side reader. They
|
|
266
|
-
upload fine but are indexed from **metadata only**; re-save as
|
|
267
|
-
`.docx` / `.xlsx` / `.pptx` (or PDF) to capture their contents.
|
|
268
|
-
- **Anything else** — a format covered by none of the above is indexed from its
|
|
267
|
+
- **Anything else**: a format covered by none of the above is indexed from its
|
|
269
268
|
metadata. To support it, register your own
|
|
270
|
-
[Attachment parser plugin](#attachment-parser-plugins)
|
|
269
|
+
[Attachment parser plugin](#attachment-parser-plugins), which runs in the browser
|
|
271
270
|
and feeds parsed text straight into indexing.
|
|
272
271
|
|
|
273
272
|
### Re-indexing an existing file
|
|
@@ -311,7 +310,7 @@ interface AttachmentParser {
|
|
|
311
310
|
```
|
|
312
311
|
|
|
313
312
|
The first parser whose `match` returns `true` wins. A parser that throws or
|
|
314
|
-
returns nothing is ignored
|
|
313
|
+
returns nothing is ignored, and the file falls back to its normal path. Output is
|
|
315
314
|
capped (~200k chars) before it is inlined.
|
|
316
315
|
|
|
317
316
|
### Example
|
|
@@ -370,13 +369,13 @@ The active theme is saved to `localStorage`, so a returning user keeps their cho
|
|
|
370
369
|
## Importing the chat engine
|
|
371
370
|
|
|
372
371
|
`bunnyquery.js` is the ready-made widget. Under it sits a **framework-agnostic,
|
|
373
|
-
DOM-free chat engine
|
|
372
|
+
DOM-free chat engine**, the same core that powers both this widget and the Skapi
|
|
374
373
|
admin chatbox. Import it from `bunnyquery/engine` when you want to build your own chat
|
|
375
374
|
UI (React, Vue, Svelte, vanilla…) while reusing the engine's message/queue/typewriter/
|
|
376
375
|
cache state machine, request builders, markdown-message composition, and prompts.
|
|
377
376
|
|
|
378
|
-
Install the package, plus the `skapi-js` SDK (for the transport) and
|
|
379
|
-
already have one
|
|
377
|
+
Install the package, plus the `skapi-js` SDK (for the transport) and, if you don't
|
|
378
|
+
already have one, a markdown renderer such as `marked`:
|
|
380
379
|
|
|
381
380
|
```bash
|
|
382
381
|
npm install bunnyquery skapi-js marked
|
|
@@ -412,12 +411,12 @@ session.dispatchComposedMessage('Hello!'); // send a message
|
|
|
412
411
|
|
|
413
412
|
The engine owns chat **state and logic** and calls back into your view through the
|
|
414
413
|
`ChatHost` interface (render, scroll, identity, cancel/refresh). It has **no bundled
|
|
415
|
-
runtime dependencies
|
|
414
|
+
runtime dependencies**: you inject the skapi transport via `configureChatEngine()` and
|
|
416
415
|
render markdown yourself (e.g. with `marked`). Everything is fully typed: `ChatSession`,
|
|
417
416
|
`ChatHost`, `ChatMessage`, `ChatIdentity`, `ChatState`, `composeUserMessage`, the request
|
|
418
417
|
builders (`callClaudeWithPublicMcp` / `callOpenAIWithPublicMcp`, `getChatHistory`,
|
|
419
418
|
`notifyAgentSaveAttachment`), the prompt builders, and the token-budget / link / history
|
|
420
|
-
helpers
|
|
419
|
+
helpers. See the `.d.ts` shipped with `bunnyquery/engine`.
|
|
421
420
|
|
|
422
421
|
`configureChatEngine` options:
|
|
423
422
|
|
|
@@ -426,9 +425,9 @@ helpers — see the `.d.ts` shipped with `bunnyquery/engine`.
|
|
|
426
425
|
| `clientSecretRequest` | `function` | `skapi.clientSecretRequest`, bound to your Skapi instance. **Required.** |
|
|
427
426
|
| `clientSecretRequestHistory` | `function` | `skapi.clientSecretRequestHistory`, bound to your Skapi instance. **Required.** |
|
|
428
427
|
| `mcpBaseUrl` | `string` | MCP server base URL (you resolve prod vs dev). **Required.** |
|
|
429
|
-
| `poll` | `number?` | Value attached as `poll` on every request. Omit it if your `clientSecretRequest` already resolves with the final body; pass `0` for the deployed `skapi-js@latest` (needed for the early ack + a manual `.poll()` handle that powers queued-send cancel
|
|
428
|
+
| `poll` | `number?` | Value attached as `poll` on every request. Omit it if your `clientSecretRequest` already resolves with the final body; pass `0` for the deployed `skapi-js@latest` (needed for the early ack + a manual `.poll()` handle that powers queued-send cancel, the widget's case). |
|
|
430
429
|
| `attachmentParsers` | `array?` | Client-side attachment parsers, registered at configure time. More can be added later with `registerAttachmentParser()`. See [Attachment parser plugins](#attachment-parser-plugins). |
|
|
431
|
-
| `windowedIndexing` | `boolean?` | Opt in to **server-driven** windowed indexing for text and grid files (see [file types](#supported-file-types)). Off by default
|
|
430
|
+
| `windowedIndexing` | `boolean?` | Opt in to **server-driven** windowed indexing for text and grid files (see [file types](#supported-file-types)). Off by default in the engine; the widget passes it as `true`. The deployed skapi workers support it; only leave it off against a self-hosted worker that does not yet strip the `_skapi_window` directive, where it would reach the provider as an unknown body field and fail the call terminally with no retry. |
|
|
432
431
|
|
|
433
432
|
### Display and paging helpers
|
|
434
433
|
|
|
@@ -464,7 +463,7 @@ never progress.
|
|
|
464
463
|
BunnyQuery connects to your AI agent through an MCP OAuth server
|
|
465
464
|
(`mcp.broadwayinc.computer` in production, `mcp-dev.broadwayinc.computer` when
|
|
466
465
|
`dev: true`). After authorization, the OAuth server redirects back to **the current
|
|
467
|
-
host page
|
|
466
|
+
host page**: BunnyQuery reads the `?code=…&state=…` parameters, completes the
|
|
468
467
|
exchange, and cleans them from the URL automatically. No dedicated callback page is
|
|
469
468
|
needed; just make sure the page that hosts the widget is a stable, reachable URL.
|
|
470
469
|
|
package/bunnyquery.css
CHANGED
|
@@ -1119,7 +1119,17 @@
|
|
|
1119
1119
|
border: 1px solid var(--bq-line);
|
|
1120
1120
|
background: rgba(127, 127, 127, 0.05);
|
|
1121
1121
|
}
|
|
1122
|
+
/* Three states, three colours, and the row is readable at a glance without
|
|
1123
|
+
opening it:
|
|
1124
|
+
yellow working on it now (.is-active)
|
|
1125
|
+
green indexed, confirmed (.is-indexed)
|
|
1126
|
+
grey not known yet (.is-resolving, and the base look)
|
|
1127
|
+
Grey is the DEFAULT rather than a colour of its own, so anything that has not
|
|
1128
|
+
earned green or yellow reads as "no claim" — which is also what a cancelled
|
|
1129
|
+
row should look like, and what an unrecognised future state would fall back
|
|
1130
|
+
to. Red stays reserved for a failure. */
|
|
1122
1131
|
.bq-index-group.is-active { border-color: var(--bq-warning-border); background: var(--bq-warning-bg); }
|
|
1132
|
+
.bq-index-group.is-indexed { border-color: var(--bq-success-border); background: var(--bq-success-bg); }
|
|
1123
1133
|
.bq-index-group.is-error { border-color: var(--bq-danger); background: var(--bq-danger-bg); }
|
|
1124
1134
|
|
|
1125
1135
|
.bq-index-head {
|
|
@@ -1142,7 +1152,12 @@
|
|
|
1142
1152
|
}
|
|
1143
1153
|
.bq-index-head:hover { background: rgba(127, 127, 127, 0.08); color: var(--bq-muted); }
|
|
1144
1154
|
.bq-index-group.is-active .bq-index-head { color: var(--bq-warning); }
|
|
1155
|
+
.bq-index-group.is-indexed .bq-index-head { color: var(--bq-success); }
|
|
1145
1156
|
.bq-index-group.is-error .bq-index-head { color: var(--bq-danger); }
|
|
1157
|
+
/* Stated rather than inherited: a waiting row keeping the muted default is a
|
|
1158
|
+
DECISION (it is not claiming a state), not an oversight, and writing it here
|
|
1159
|
+
means adding a colour to the base rule cannot silently give it one. */
|
|
1160
|
+
.bq-index-group.is-resolving .bq-index-head { color: var(--bq-muted); }
|
|
1146
1161
|
|
|
1147
1162
|
/* Status glyph. The active one is a circular-arrow SVG the consumer inlines
|
|
1148
1163
|
(never an icon font: agent.vue's Material Symbols build gates glyphs behind
|
|
@@ -1158,8 +1173,17 @@
|
|
|
1158
1173
|
.bq-index-icon svg { width: 100%; height: 100%; display: block; }
|
|
1159
1174
|
.bq-index-group.is-active .bq-index-icon svg { animation: bq-index-spin 1.1s linear infinite; }
|
|
1160
1175
|
@keyframes bq-index-spin { to { transform: rotate(360deg); } }
|
|
1176
|
+
/* Waiting to find out (a run whose earlier passes are still being paged in, a
|
|
1177
|
+
file whose queue state has not been answered). Deliberately NOT the active
|
|
1178
|
+
look: no warning colour and no spin, because a spinning glyph on a chat row is
|
|
1179
|
+
read as "this file is being worked on right now" and that is the one thing this
|
|
1180
|
+
state does not know. A slow fade says "pending" without claiming progress. */
|
|
1181
|
+
.bq-index-group.is-resolving .bq-index-icon svg { animation: bq-index-fade 1.6s ease-in-out infinite; }
|
|
1182
|
+
@keyframes bq-index-fade { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
|
|
1161
1183
|
@media (prefers-reduced-motion: reduce) {
|
|
1162
1184
|
.bq-index-group.is-active .bq-index-icon svg { animation-duration: 3s; }
|
|
1185
|
+
/* A fade has no end state to settle on, so it stops rather than slows. */
|
|
1186
|
+
.bq-index-group.is-resolving .bq-index-icon svg { animation: none; opacity: 0.7; }
|
|
1163
1187
|
}
|
|
1164
1188
|
|
|
1165
1189
|
.bq-index-label {
|
|
@@ -1218,8 +1242,28 @@
|
|
|
1218
1242
|
border-left: 2px solid var(--bq-line);
|
|
1219
1243
|
}
|
|
1220
1244
|
.bq-message.bq-index-pass .bq-bubble { font-size: 0.78rem; }
|
|
1221
|
-
/* Close the run: the next ordinary message needs its normal breathing room.
|
|
1222
|
-
.bq-message
|
|
1245
|
+
/* Close the run: the next ordinary message needs its normal breathing room.
|
|
1246
|
+
NOT scoped to .bq-message: the trailing loader below is a run member too, and
|
|
1247
|
+
requiring .bq-message would silently drop the gap whenever it is the last one. */
|
|
1248
|
+
.bq-index-pass + :not(.bq-index-pass) { margin-top: 0.55rem; }
|
|
1249
|
+
/* Trailing loader: an open row keeps a "still working" mark of its own for as long
|
|
1250
|
+
as the file's indexing is not confirmed over. It sits on the same rail as the
|
|
1251
|
+
passes so it reads as the run continuing rather than as a message of its own —
|
|
1252
|
+
which is why the rail is repeated here instead of reusing the rule above, whose
|
|
1253
|
+
selector requires .bq-message. */
|
|
1254
|
+
.bq-index-tail {
|
|
1255
|
+
margin-bottom: 0.3rem;
|
|
1256
|
+
margin-left: 0.7rem;
|
|
1257
|
+
padding: 0.1rem 0 0.1rem 0.7rem;
|
|
1258
|
+
border-left: 2px solid var(--bq-line);
|
|
1259
|
+
font-size: 0.78rem;
|
|
1260
|
+
color: var(--bq-muted);
|
|
1261
|
+
}
|
|
1262
|
+
/* An open row animates for as long as the file is indexing, which can be minutes.
|
|
1263
|
+
Readers who ask for less motion get the static end state. */
|
|
1264
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1265
|
+
.bq-index-tail .bq-loader::after { animation: none; content: '...'; }
|
|
1266
|
+
}
|
|
1223
1267
|
.bq-index-note {
|
|
1224
1268
|
padding: 0 0.6rem 0.45rem;
|
|
1225
1269
|
font-size: 0.7rem;
|
|
@@ -1253,6 +1297,68 @@
|
|
|
1253
1297
|
/* While the temporary URL is being re-resolved the click is swallowed — show a
|
|
1254
1298
|
busy cursor and dim it so it reads as "working, don't click again". */
|
|
1255
1299
|
.bq-link-button.is-refreshing { cursor: progress; opacity: 0.6; }
|
|
1300
|
+
/* The client asked for a url for this file and did not get one, so the chip is
|
|
1301
|
+
emitted with NO href (it cannot navigate) and with ✕ in place of ↗. Paint it
|
|
1302
|
+
as the dead text it now is: muted, no underline, default cursor, and no hover
|
|
1303
|
+
response, since the base rule above still fires on hover and would otherwise
|
|
1304
|
+
promising a link. */
|
|
1305
|
+
.bq-link-button.is-unavailable,
|
|
1306
|
+
.bq-link-button.is-unavailable:hover {
|
|
1307
|
+
color: var(--bq-muted);
|
|
1308
|
+
text-decoration: none;
|
|
1309
|
+
cursor: default;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
/* ---- inline image previews ----------------------------------------------*/
|
|
1313
|
+
/* The anchor IS the preview: picture on top, the ordinary chip underneath as a
|
|
1314
|
+
caption. The caption is the FALLBACK, not decoration: when the url dies or the
|
|
1315
|
+
file is gone the <img> hides itself and what is left is exactly the text chip
|
|
1316
|
+
this feature replaced, still clickable. */
|
|
1317
|
+
.bq-link-button.is-image-preview {
|
|
1318
|
+
display: inline-block;
|
|
1319
|
+
max-width: 100%;
|
|
1320
|
+
white-space: normal;
|
|
1321
|
+
overflow: visible;
|
|
1322
|
+
text-overflow: clip;
|
|
1323
|
+
vertical-align: top;
|
|
1324
|
+
margin: 0.25em 0;
|
|
1325
|
+
}
|
|
1326
|
+
/* Qualified with .bq-md because `.bq-md img` below already sets max-width and
|
|
1327
|
+
would otherwise out-specify a bare .bq-img-preview. */
|
|
1328
|
+
.bq-md img.bq-img-preview {
|
|
1329
|
+
display: block;
|
|
1330
|
+
/* Bounded on BOTH axes so a 12000px panorama and a 40x9000 sliver each land
|
|
1331
|
+
inside the bubble; width/height auto keep the aspect ratio. The bubble cap
|
|
1332
|
+
differs per client, so the preview carries its own. */
|
|
1333
|
+
max-width: min(100%, 320px);
|
|
1334
|
+
max-height: 320px;
|
|
1335
|
+
width: auto;
|
|
1336
|
+
height: auto;
|
|
1337
|
+
border-radius: 4px;
|
|
1338
|
+
border: 1px solid var(--bq-line);
|
|
1339
|
+
/* A transparent PNG is invisible on paper without this. */
|
|
1340
|
+
background: rgba(127, 127, 127, 0.08);
|
|
1341
|
+
}
|
|
1342
|
+
/* Keyed on the MISSING src, not on a state class: the element is emitted with no
|
|
1343
|
+
src at all (the url has to be minted first) and a src-less <img> would
|
|
1344
|
+
otherwise paint the browser's broken-image glyph. */
|
|
1345
|
+
.bq-md img.bq-img-preview:not([src]),
|
|
1346
|
+
.bq-md img.bq-img-preview[data-bq-img-state="error"] { display: none; }
|
|
1347
|
+
/* The dot trail covers the mint round trip, then gets out of the way. */
|
|
1348
|
+
.bq-md img.bq-img-preview[src] ~ [data-bq-img-loader],
|
|
1349
|
+
.bq-md img.bq-img-preview[data-bq-img-state="error"] ~ [data-bq-img-loader] { display: none; }
|
|
1350
|
+
.bq-img-preview-caption {
|
|
1351
|
+
display: inline-block;
|
|
1352
|
+
max-width: 100%;
|
|
1353
|
+
overflow: hidden;
|
|
1354
|
+
text-overflow: ellipsis;
|
|
1355
|
+
white-space: nowrap;
|
|
1356
|
+
vertical-align: bottom;
|
|
1357
|
+
}
|
|
1358
|
+
/* A collapsed indexing row is a single line and its label is the indexed file's
|
|
1359
|
+
own path, which is frequently an image. Never grow that row into a picture. */
|
|
1360
|
+
.bq-index-label .bq-md img.bq-img-preview,
|
|
1361
|
+
.bq-index-label [data-bq-img-loader] { display: none; }
|
|
1256
1362
|
|
|
1257
1363
|
/* ---- rendered-markdown body ----------------------------------------------*/
|
|
1258
1364
|
/* Overrides the bubble's `white-space: pre-wrap` since marked already produces
|