bunnyquery 1.2.2 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,8 +6,13 @@ account login/signup, conversation history, file & folder uploads, and a setting
6
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
- chatbox. It ships as a single IIFE that exposes `window.BunnyQuery`, plus one
10
- stylesheet. No build step, no framework, no npm install.
9
+ chatbox. The **widget** ships as a single IIFE that exposes `window.BunnyQuery` plus one
10
+ stylesheet drop it in via `<script>`, no build step or framework required.
11
+
12
+ The package also exports the **framework-agnostic chat engine** that powers it
13
+ (`bunnyquery/engine`) — the same DOM-free core the Skapi admin chatbox consumes — so
14
+ you can build your own chat UI on top of it. See
15
+ [Importing the chat engine](#importing-the-chat-engine).
11
16
 
12
17
  ## Features
13
18
 
@@ -68,14 +73,18 @@ call `BunnyQuery.init()`:
68
73
  That's it — BunnyQuery takes over the `#chatbox` element and renders the login or
69
74
  chat view depending on the user's session.
70
75
 
71
- ## Files
76
+ ## What's in the package
72
77
 
73
- | File | Purpose |
74
- | --------------- | ------------------------------------------------------------- |
75
- | `bunnyquery.js` | The widget. Exposes the global `window.BunnyQuery`. |
76
- | `bunnyquery.css` | All styles, scoped under `.bq-agent` / `[data-bq-theme]`. |
78
+ | Path | Purpose |
79
+ | ----------------------------- | -------------------------------------------------------------------------------- |
80
+ | `bunnyquery.js` | The widget IIFE. Exposes the global `window.BunnyQuery`. CDN / `<script>` drop-in. |
81
+ | `bunnyquery.css` | The widget's full stylesheet, scoped under `.bq-agent` / `[data-bq-theme]`. |
82
+ | `bunnyquery/engine` | The framework-agnostic chat engine — ships as ESM + CJS with TypeScript types. |
83
+ | `bunnyquery/styles/chat.css` | The shared chat-surface styles (bubbles, markdown, links) for an engine-built UI. |
77
84
 
78
- Host them yourself (same origin recommended) or from a CDN.
85
+ The two widget files can be hosted yourself (same origin recommended) or loaded from a
86
+ CDN — no npm needed. The `engine` / `styles` subpaths are for bundler consumers
87
+ (`npm install bunnyquery`); see [Importing the chat engine](#importing-the-chat-engine).
79
88
 
80
89
  ## API
81
90
 
@@ -138,6 +147,67 @@ To customize colors, override the variables in your own stylesheet **after**
138
147
 
139
148
  The active theme is saved to `localStorage`, so a returning user keeps their choice.
140
149
 
150
+ ## Importing the chat engine
151
+
152
+ `bunnyquery.js` is the ready-made widget. Under it sits a **framework-agnostic,
153
+ DOM-free chat engine** — the same core that powers both this widget and the Skapi
154
+ admin chatbox. Import it from `bunnyquery/engine` when you want to build your own chat
155
+ UI (React, Vue, Svelte, vanilla…) while reusing the engine's message/queue/typewriter/
156
+ cache state machine, request builders, markdown-message composition, and prompts.
157
+
158
+ Install the package, plus the `skapi-js` SDK (for the transport) and — if you don't
159
+ already have one — a markdown renderer such as `marked`:
160
+
161
+ ```bash
162
+ npm install bunnyquery skapi-js marked
163
+ ```
164
+
165
+ ```ts
166
+ import {
167
+ configureChatEngine,
168
+ ChatSession,
169
+ composeUserMessage,
170
+ type ChatHost,
171
+ } from 'bunnyquery/engine';
172
+
173
+ // Shared chat-surface styles (message bubbles, rendered markdown, links).
174
+ // Pair it with your own container/layout CSS and the --bq-* design tokens.
175
+ import 'bunnyquery/styles/chat.css';
176
+
177
+ // 1. Inject the skapi transport + MCP endpoint ONCE at startup.
178
+ configureChatEngine({
179
+ clientSecretRequest: (opts) => skapi.clientSecretRequest(opts),
180
+ clientSecretRequestHistory: (params, fetchOptions) =>
181
+ skapi.clientSecretRequestHistory(params, fetchOptions),
182
+ mcpBaseUrl: 'https://mcp.broadwayinc.computer',
183
+ poll: 0, // see the note below
184
+ });
185
+
186
+ // 2. Implement a ChatHost (identity, render/scroll hooks, the skapi
187
+ // cancel/refresh surface) for your view, then drive a ChatSession.
188
+ const session = new ChatSession(host); // host: ChatHost
189
+ await session.loadHistory();
190
+ session.dispatchComposedMessage('Hello!'); // send a message
191
+ ```
192
+
193
+ The engine owns chat **state and logic** and calls back into your view through the
194
+ `ChatHost` interface (render, scroll, identity, cancel/refresh). It has **no bundled
195
+ runtime dependencies** — you inject the skapi transport via `configureChatEngine()` and
196
+ render markdown yourself (e.g. with `marked`). Everything is fully typed: `ChatSession`,
197
+ `ChatHost`, `ChatMessage`, `ChatIdentity`, `ChatState`, `composeUserMessage`, the request
198
+ builders (`callClaudeWithPublicMcp` / `callOpenAIWithPublicMcp`, `getChatHistory`,
199
+ `notifyAgentSaveAttachment`), the prompt builders, and the token-budget / link / history
200
+ helpers — see the `.d.ts` shipped with `bunnyquery/engine`.
201
+
202
+ `configureChatEngine` options:
203
+
204
+ | Option | Type | Description |
205
+ | ----------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------ |
206
+ | `clientSecretRequest` | `function` | `skapi.clientSecretRequest`, bound to your Skapi instance. **Required.** |
207
+ | `clientSecretRequestHistory` | `function` | `skapi.clientSecretRequestHistory`, bound to your Skapi instance. **Required.** |
208
+ | `mcpBaseUrl` | `string` | MCP server base URL (you resolve prod vs dev). **Required.** |
209
+ | `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). |
210
+
141
211
  ## OAuth & redirects
142
212
 
143
213
  BunnyQuery connects to your AI agent through an MCP OAuth server
package/bunnyquery.css CHANGED
@@ -1,3 +1,8 @@
1
+ /* GENERATED by scripts/build-css.mjs — DO NOT EDIT.
2
+ * = src/widget.css (widget chrome + tokens + chat container)
3
+ * + styles/chat.css (shared chat surface, also bunnyquery/styles/chat.css).
4
+ * Edit those sources, then run `npm run build`. */
5
+
1
6
  /* ============================================================================
2
7
  * BunnyQuery — embeddable AI chat widget
3
8
  * Ported from the bunnyquery (www.skapi.com) agent.vue chatbox + login views.
@@ -608,170 +613,13 @@
608
613
  font-weight: 600;
609
614
  }
610
615
 
611
- /* messages + bubbles */
612
- .bq-message {
613
- display: flex;
614
- margin-bottom: 0.5rem;
615
- }
616
- .bq-message.is-user { justify-content: flex-end; }
617
- .bq-message.is-assistant { justify-content: flex-start; }
618
-
619
- .bq-bubble {
620
- max-width: 85%;
621
- padding: 0.55rem 0.7rem;
622
- border: 1px solid var(--bq-line);
623
- white-space: pre-wrap;
624
- line-height: 1.45;
625
- font-size: 0.83rem;
626
- background: var(--bq-paper);
627
- }
628
- .bq-message.is-user .bq-bubble { background: var(--bq-paper); }
629
- .bq-message.is-assistant:not(.is-error) .bq-bubble {
630
- background: var(--bq-success-bg);
631
- border-color: var(--bq-success-border);
632
- }
633
- .bq-message.is-error .bq-bubble {
634
- background: var(--bq-danger-bg);
635
- border-color: var(--bq-danger);
636
- color: var(--bq-danger);
637
- }
638
- .bq-message.is-cancelled .bq-bubble {
639
- background: var(--bq-danger-bg);
640
- border-color: var(--bq-danger);
641
- color: var(--bq-danger);
642
- opacity: 0.65;
643
- }
644
- .bq-message.is-pending-older .bq-bubble {
645
- background: var(--bq-warning-bg);
646
- border-color: var(--bq-warning-border);
647
- color: var(--bq-warning);
648
- transition: opacity 0.25s ease;
649
- }
650
- .bq-message.is-sending-to-server .bq-bubble { opacity: 0.4; }
651
-
652
- .bq-empty-greeting .bq-bubble {
653
- color: var(--bq-muted);
654
- }
655
-
656
- .bq-pending-note {
657
- display: block;
658
- margin-top: 0.35rem;
659
- font-size: 0.7rem;
660
- color: var(--bq-warning);
661
- font-style: italic;
662
- }
663
- .bq-cancel-error {
664
- display: block;
665
- margin-top: 0.3rem;
666
- font-size: 0.68rem;
667
- color: var(--bq-danger);
668
- font-style: italic;
669
- clear: both;
670
- }
671
- .bq-cancel-queue-btn {
672
- float: right;
673
- margin: -0.25rem -0.25rem 0.2rem 0.4rem;
674
- width: 1.1rem;
675
- height: 1.1rem;
676
- display: inline-flex;
677
- align-items: center;
678
- justify-content: center;
679
- padding: 0;
680
- border: 1px solid var(--bq-warning-border);
681
- background: transparent;
682
- color: var(--bq-warning);
683
- font-size: 0.85rem;
684
- line-height: 1;
685
- cursor: pointer;
686
- flex-shrink: 0;
687
- }
688
- .bq-cancel-queue-btn:hover:not(.is-disabled) { background: var(--bq-warning-bg); }
689
- .bq-cancel-queue-btn.is-disabled { opacity: 0.3; cursor: not-allowed; pointer-events: none; }
690
-
691
- /* in-bubble file/link anchors */
692
- .bq-file-download,
693
- .bq-link-button {
694
- color: var(--bq-main);
695
- text-decoration: none;
696
- cursor: pointer;
697
- background: transparent;
698
- border: 0;
699
- padding: 0;
700
- font: inherit;
701
- }
702
- .bq-file-download:hover,
703
- .bq-link-button:hover { text-decoration: underline; }
704
- .bq-link-button {
705
- display: inline-block;
706
- max-width: 100%;
707
- overflow: hidden;
708
- text-overflow: ellipsis;
709
- white-space: nowrap;
710
- vertical-align: bottom;
711
- }
712
-
713
- /* ---- markdown body -------------------------------------------------------*/
714
- .bq-md { white-space: normal; word-break: break-word; }
715
- .bq-md > :first-child { margin-top: 0; }
716
- .bq-md > :last-child { margin-bottom: 0; }
717
- .bq-md p { margin: 0.4em 0; }
718
- .bq-md h1 { font-size: 1.3em; }
719
- .bq-md h2 { font-size: 1.18em; }
720
- .bq-md h3 { font-size: 1.08em; }
721
- .bq-md h4, .bq-md h5, .bq-md h6 { font-size: 1em; }
722
- .bq-md h1, .bq-md h2, .bq-md h3, .bq-md h4, .bq-md h5, .bq-md h6 {
723
- margin: 0.8em 0 0.35em;
724
- line-height: 1.25;
725
- font-weight: 600;
726
- }
727
- .bq-md ul, .bq-md ol { margin: 0.4em 0; padding-left: 1.4em; }
728
- .bq-md li { margin: 0.15em 0; }
729
- .bq-md blockquote {
730
- margin: 0.5em 0;
731
- padding: 0.2em 0.8em;
732
- border-left: 3px solid var(--bq-line);
733
- color: var(--bq-muted);
734
- background: rgba(127, 127, 127, 0.08);
735
- }
736
- .bq-md hr { border: none; border-top: 1px solid var(--bq-line); margin: 0.8em 0; }
737
- .bq-md a { color: var(--bq-main); text-decoration: underline; }
738
- .bq-md a:hover { text-decoration: none; }
739
- .bq-md code {
740
- font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
741
- font-size: 0.9em;
742
- padding: 0.1em 0.35em;
743
- background: rgba(127, 127, 127, 0.16);
744
- white-space: pre-wrap;
745
- word-break: break-word;
746
- }
747
- .bq-md pre {
748
- margin: 0.5em 0;
749
- padding: 0.6em 0.8em;
750
- background: var(--bq-code-bg);
751
- color: var(--bq-code-fg);
752
- overflow-x: auto;
753
- white-space: pre;
754
- }
755
- .bq-md pre code {
756
- background: transparent;
757
- color: inherit;
758
- padding: 0;
759
- font-size: 0.85em;
760
- word-break: normal;
761
- }
762
- .bq-md table {
763
- border-collapse: collapse;
764
- margin: 0.5em 0;
765
- font-size: 0.95em;
766
- display: block;
767
- overflow-x: auto;
768
- }
769
- .bq-md th, .bq-md td { border: 1px solid var(--bq-line); padding: 0.3em 0.55em; text-align: left; }
770
- .bq-md th { background: rgba(127, 127, 127, 0.1); font-weight: 600; }
771
- .bq-md img { max-width: 100%; height: auto; border-radius: 4px; }
772
- .bq-md strong { font-weight: 600; }
773
- .bq-md em { font-style: italic; }
774
- .bq-md del { text-decoration: line-through; }
616
+ /* messages + bubbles: the CORE conversation styling (bubble padding/colors,
617
+ rendered markdown, in-bubble link anchors, queue/cancel affordances) is
618
+ authored ONCE in @skapi/chat-engine/styles/chat.css and concatenated onto
619
+ this file by the build (scripts/build-css.mjs). Only the widget-specific
620
+ container bits stay here. */
621
+ .bq-bubble { max-width: 85%; }
622
+ .bq-empty-greeting .bq-bubble { color: var(--bq-muted); }
775
623
 
776
624
  /* ---- composer / input row ------------------------------------------------*/
777
625
  .bq-input-row {
@@ -860,7 +708,8 @@
860
708
  position: absolute;
861
709
  inset: 0 auto 0 0;
862
710
  width: var(--att-progress, 0%);
863
- background: rgba(240, 98, 146, 0.18);
711
+ /* theme-reactive tint (matches agent.vue's fade()→color-mix() change) */
712
+ background: color-mix(in srgb, var(--bq-main) 18%, transparent);
864
713
  transition: width 0.15s linear;
865
714
  z-index: 0;
866
715
  pointer-events: none;
@@ -996,6 +845,19 @@
996
845
  color: var(--bq-paper);
997
846
  }
998
847
 
848
+ /* "apply to all remaining files" checkbox in the overwrite/reindex prompt */
849
+ .bq-overwrite-applyall {
850
+ display: flex;
851
+ align-items: center;
852
+ gap: 0.4rem;
853
+ margin: -0.5rem 0 1.25rem;
854
+ font-size: 0.82rem;
855
+ color: var(--bq-ink);
856
+ cursor: pointer;
857
+ user-select: none;
858
+ }
859
+ .bq-overwrite-applyall input { cursor: pointer; margin: 0; }
860
+
999
861
  /* ============================================================================
1000
862
  * RESPONSIVE
1001
863
  * ==========================================================================*/
@@ -1015,3 +877,198 @@
1015
877
  .bq-modal-root[data-bq-theme="dark"] .bq-icon-invertible {
1016
878
  filter: invert(var(--bq-icon-invert));
1017
879
  }
880
+
881
+ /**
882
+ * @skapi/chat-engine — shared chat-display styles.
883
+ *
884
+ * The conversation surface (message bubbles, rendered markdown, in-bubble file/
885
+ * link anchors, the queue/cancel affordances) must look IDENTICAL in agent.vue
886
+ * (the Vue admin chatbox) and the BunnyQuery widget. These rules are authored
887
+ * ONCE here and consumed by both:
888
+ * - agent.vue imports this file as a NON-scoped <style> block (so the rules
889
+ * also reach the v-html `.bq-md` markdown, which has no scope attribute).
890
+ * - the widget concatenates it into the shipped bunnyquery.css at build time.
891
+ *
892
+ * Each consumer supplies the `--bq-*` design tokens (light + dark) and keeps its
893
+ * OWN container/layout rules — `.bq-chat`, `.bq-messages` padding, the
894
+ * `.bq-history-loading` treatment, the empty-greeting, and the bubble max-width
895
+ * — which legitimately differ between an embedded admin panel and a standalone
896
+ * widget. Colors here are theme-reactive (tokens + rgba(127,127,127,…) neutral
897
+ * transparency) so the surface works in both light and dark schemes.
898
+ *
899
+ * Token contract (the consumer must define these): --bq-paper, --bq-line,
900
+ * --bq-muted, --bq-main, --bq-success-bg, --bq-success-border, --bq-danger,
901
+ * --bq-danger-bg, --bq-warning, --bq-warning-bg, --bq-warning-border,
902
+ * --bq-code-bg, --bq-code-fg.
903
+ */
904
+
905
+ /* ---- messages + bubbles --------------------------------------------------*/
906
+ .bq-message { display: flex; margin-bottom: 0.5rem; }
907
+ .bq-message.is-user { justify-content: flex-end; }
908
+ .bq-message.is-assistant { justify-content: flex-start; }
909
+
910
+ .bq-bubble {
911
+ padding: 0.55rem 0.7rem;
912
+ border: 1px solid var(--bq-line);
913
+ white-space: pre-wrap;
914
+ line-height: 1.45;
915
+ font-size: 0.83rem;
916
+ background: var(--bq-paper);
917
+ }
918
+ .bq-bubble span { word-break: break-word; }
919
+ .bq-message.is-user .bq-bubble { background: var(--bq-paper); }
920
+ .bq-message.is-assistant:not(.is-error) .bq-bubble {
921
+ background: var(--bq-success-bg);
922
+ border-color: var(--bq-success-border);
923
+ }
924
+ .bq-message.is-error .bq-bubble {
925
+ background: var(--bq-danger-bg);
926
+ border-color: var(--bq-danger);
927
+ color: var(--bq-danger);
928
+ }
929
+ .bq-message.is-cancelled .bq-bubble {
930
+ background: var(--bq-danger-bg);
931
+ border-color: var(--bq-danger);
932
+ color: var(--bq-danger);
933
+ opacity: 0.65;
934
+ }
935
+ .bq-message.is-pending-older .bq-bubble {
936
+ background: var(--bq-warning-bg);
937
+ border-color: var(--bq-warning-border);
938
+ color: var(--bq-warning);
939
+ transition: opacity 0.25s ease;
940
+ }
941
+ .bq-message.is-sending-to-server .bq-bubble { opacity: 0.4; }
942
+
943
+ /* ---- queue / cancel affordances -----------------------------------------*/
944
+ .bq-pending-note {
945
+ display: block;
946
+ margin-top: 0.35rem;
947
+ font-size: 0.7rem;
948
+ color: var(--bq-warning);
949
+ font-style: italic;
950
+ }
951
+ .bq-cancel-error {
952
+ display: block;
953
+ margin-top: 0.3rem;
954
+ font-size: 0.68rem;
955
+ color: var(--bq-danger);
956
+ font-style: italic;
957
+ clear: both;
958
+ }
959
+ .bq-cancel-queue-btn {
960
+ float: right;
961
+ margin: -0.25rem -0.25rem 0.2rem 0.4rem;
962
+ width: 1.1rem;
963
+ height: 1.1rem;
964
+ min-height: 0;
965
+ display: inline-flex;
966
+ align-items: center;
967
+ justify-content: center;
968
+ padding: 0;
969
+ border: 1px solid var(--bq-warning-border);
970
+ background: transparent;
971
+ color: var(--bq-warning);
972
+ font-size: 0.85rem;
973
+ line-height: 1;
974
+ cursor: pointer;
975
+ box-shadow: none;
976
+ border-radius: 0;
977
+ flex-shrink: 0;
978
+ }
979
+ .bq-cancel-queue-btn:hover:not(.is-disabled) { background: var(--bq-warning-bg); color: var(--bq-warning); }
980
+ .bq-cancel-queue-btn.is-disabled { opacity: 0.3; cursor: not-allowed; pointer-events: none; }
981
+
982
+ /* ---- in-bubble file / link anchors --------------------------------------*/
983
+ .bq-file-download,
984
+ .bq-link-button {
985
+ color: var(--bq-main);
986
+ text-decoration: none;
987
+ cursor: pointer;
988
+ background: transparent;
989
+ border: 0;
990
+ padding: 0;
991
+ font: inherit;
992
+ }
993
+ .bq-file-download:hover,
994
+ .bq-link-button:hover { color: var(--bq-main); text-decoration: underline; background: transparent; }
995
+ .bq-link-button {
996
+ display: inline-block;
997
+ max-width: 100%;
998
+ overflow: hidden;
999
+ text-overflow: ellipsis;
1000
+ white-space: nowrap;
1001
+ vertical-align: bottom;
1002
+ }
1003
+ /* While the temporary URL is being re-resolved the click is swallowed — show a
1004
+ busy cursor and dim it so it reads as "working, don't click again". */
1005
+ .bq-link-button.is-refreshing { cursor: progress; opacity: 0.6; }
1006
+
1007
+ /* ---- rendered-markdown body ----------------------------------------------*/
1008
+ /* Overrides the bubble's `white-space: pre-wrap` since marked already produces
1009
+ structural HTML; pre/code reinstate pre-wrap for their own content. */
1010
+ .bq-md { white-space: normal; word-break: break-word; }
1011
+ .bq-md > :first-child { margin-top: 0; }
1012
+ .bq-md > :last-child { margin-bottom: 0; }
1013
+ .bq-md p { margin: 0.4em 0; }
1014
+ .bq-md h1 { font-size: 1.3em; }
1015
+ .bq-md h2 { font-size: 1.18em; }
1016
+ .bq-md h3 { font-size: 1.08em; }
1017
+ .bq-md h4, .bq-md h5, .bq-md h6 { font-size: 1em; }
1018
+ .bq-md h1, .bq-md h2, .bq-md h3, .bq-md h4, .bq-md h5, .bq-md h6 {
1019
+ margin: 0.8em 0 0.35em;
1020
+ line-height: 1.25;
1021
+ font-weight: 600;
1022
+ }
1023
+ .bq-md ul, .bq-md ol { margin: 0.4em 0; padding-left: 1.4em; }
1024
+ .bq-md li { margin: 0.15em 0; }
1025
+ .bq-md li > p { margin: 0.2em 0; }
1026
+ .bq-md blockquote {
1027
+ margin: 0.5em 0;
1028
+ padding: 0.2em 0.8em;
1029
+ border-left: 3px solid var(--bq-line);
1030
+ color: var(--bq-muted);
1031
+ background: rgba(127, 127, 127, 0.08);
1032
+ }
1033
+ .bq-md hr { border: none; border-top: 1px solid var(--bq-line); margin: 0.8em 0; }
1034
+ .bq-md a { color: var(--bq-main); text-decoration: underline; }
1035
+ .bq-md a:hover { text-decoration: none; }
1036
+ .bq-md code {
1037
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
1038
+ font-size: 0.9em;
1039
+ padding: 0.1em 0.35em;
1040
+ background: rgba(127, 127, 127, 0.16);
1041
+ border-radius: 0;
1042
+ white-space: pre-wrap;
1043
+ word-break: break-word;
1044
+ }
1045
+ .bq-md pre {
1046
+ margin: 0.5em 0;
1047
+ padding: 0.6em 0.8em;
1048
+ background: var(--bq-code-bg);
1049
+ color: var(--bq-code-fg);
1050
+ border-radius: 0;
1051
+ overflow-x: auto;
1052
+ white-space: pre;
1053
+ }
1054
+ .bq-md pre code {
1055
+ background: transparent;
1056
+ color: inherit;
1057
+ padding: 0;
1058
+ font-size: 0.85em;
1059
+ white-space: pre;
1060
+ word-break: normal;
1061
+ }
1062
+ .bq-md table {
1063
+ border-collapse: collapse;
1064
+ margin: 0.5em 0;
1065
+ font-size: 0.95em;
1066
+ display: block;
1067
+ overflow-x: auto;
1068
+ }
1069
+ .bq-md th, .bq-md td { border: 1px solid var(--bq-line); padding: 0.3em 0.55em; text-align: left; }
1070
+ .bq-md th { background: rgba(127, 127, 127, 0.1); font-weight: 600; }
1071
+ .bq-md img { max-width: 100%; height: auto; border-radius: 4px; }
1072
+ .bq-md strong { font-weight: 600; }
1073
+ .bq-md em { font-style: italic; }
1074
+ .bq-md del { text-decoration: line-through; }