Archived
Improve chat rendering
This commit is contained in:
Generated
+30
-5
@@ -12,7 +12,8 @@
|
|||||||
"@fastify/websocket": "^11.2.0",
|
"@fastify/websocket": "^11.2.0",
|
||||||
"@mariozechner/pi-coding-agent": "^0.73.0",
|
"@mariozechner/pi-coding-agent": "^0.73.0",
|
||||||
"fastify": "^5.6.1",
|
"fastify": "^5.6.1",
|
||||||
"lit": "^3.3.1"
|
"lit": "^3.3.1",
|
||||||
|
"marked": "^18.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
@@ -1825,6 +1826,18 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@mariozechner/pi-coding-agent/node_modules/marked": {
|
||||||
|
"version": "15.0.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz",
|
||||||
|
"integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"marked": "bin/marked.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@mariozechner/pi-tui": {
|
"node_modules/@mariozechner/pi-tui": {
|
||||||
"version": "0.73.0",
|
"version": "0.73.0",
|
||||||
"resolved": "https://registry.npmjs.org/@mariozechner/pi-tui/-/pi-tui-0.73.0.tgz",
|
"resolved": "https://registry.npmjs.org/@mariozechner/pi-tui/-/pi-tui-0.73.0.tgz",
|
||||||
@@ -1844,6 +1857,18 @@
|
|||||||
"koffi": "^2.9.0"
|
"koffi": "^2.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@mariozechner/pi-tui/node_modules/marked": {
|
||||||
|
"version": "15.0.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz",
|
||||||
|
"integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"marked": "bin/marked.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@mistralai/mistralai": {
|
"node_modules/@mistralai/mistralai": {
|
||||||
"version": "2.2.1",
|
"version": "2.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.1.tgz",
|
||||||
@@ -4389,15 +4414,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/marked": {
|
"node_modules/marked": {
|
||||||
"version": "15.0.12",
|
"version": "18.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz",
|
"resolved": "https://registry.npmjs.org/marked/-/marked-18.0.3.tgz",
|
||||||
"integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==",
|
"integrity": "sha512-7VT90JOkDeaRWpfjOReRGPEKn0ecdARBkDGL+tT1wZY0efPPqkUxLUSmzy/C7TIylQYJC9STISEsCHrqb/7VIA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"marked": "bin/marked.js"
|
"marked": "bin/marked.js"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 18"
|
"node": ">= 20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mime": {
|
"node_modules/mime": {
|
||||||
|
|||||||
+2
-1
@@ -15,7 +15,8 @@
|
|||||||
"@fastify/websocket": "^11.2.0",
|
"@fastify/websocket": "^11.2.0",
|
||||||
"@mariozechner/pi-coding-agent": "^0.73.0",
|
"@mariozechner/pi-coding-agent": "^0.73.0",
|
||||||
"fastify": "^5.6.1",
|
"fastify": "^5.6.1",
|
||||||
"lit": "^3.3.1"
|
"lit": "^3.3.1",
|
||||||
|
"marked": "^18.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import type { ChatLine, ChatPart } from "./components/shared";
|
||||||
|
|
||||||
|
export function normalizeMessages(messages: any[]): ChatLine[] {
|
||||||
|
return messages.flatMap(normalizeMessage).filter((message) => message.parts.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function textMessage(role: ChatLine["role"], text: string): ChatLine {
|
||||||
|
return { role, parts: [{ type: "text", text }] };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function appendText(messages: ChatLine[], role: ChatLine["role"], text: string): ChatLine[] {
|
||||||
|
const last = messages.at(-1);
|
||||||
|
const lastPart = last?.parts.at(-1);
|
||||||
|
if (last?.role === role && lastPart?.type === "text") {
|
||||||
|
return [
|
||||||
|
...messages.slice(0, -1),
|
||||||
|
{ ...last, parts: [...last.parts.slice(0, -1), { ...lastPart, text: lastPart.text + text }] },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return [...messages, textMessage(role, text)];
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeMessage(message: any): ChatLine[] {
|
||||||
|
const role = normalizeRole(message?.role);
|
||||||
|
const parts = normalizeContent(message?.content, message);
|
||||||
|
if (role === "tool") return [{ role, parts }];
|
||||||
|
|
||||||
|
const visible = parts.filter((part) => part.type !== "empty");
|
||||||
|
return visible.length ? [{ role, parts: visible }] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeRole(role: unknown): ChatLine["role"] {
|
||||||
|
if (role === "assistant") return "assistant";
|
||||||
|
if (role === "user") return "user";
|
||||||
|
if (role === "toolResult") return "tool";
|
||||||
|
return "system";
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeContent(content: unknown, message: any): ChatPart[] {
|
||||||
|
if (typeof content === "string") return content ? [{ type: "text", text: content }] : [];
|
||||||
|
if (!Array.isArray(content)) return objectFallback(content);
|
||||||
|
|
||||||
|
return content.flatMap((part: any): ChatPart[] => {
|
||||||
|
if (part?.type === "text") return part.text ? [{ type: "text", text: part.text }] : [];
|
||||||
|
if (part?.type === "thinking") return part.thinking || part.text ? [{ type: "thinking", text: part.thinking ?? part.text }] : [];
|
||||||
|
if (part?.type === "toolCall") return [{ type: "toolCall", toolName: part.name ?? "tool", summary: summarizeArgs(part.arguments) }];
|
||||||
|
if (part?.type === "image") return [{ type: "text", text: "[image]" }];
|
||||||
|
return objectFallback(part);
|
||||||
|
}).map((part) => part.type === "text" && message?.role === "toolResult"
|
||||||
|
? { type: "toolResult", toolName: message.toolName ?? "tool", text: part.text, isError: !!message.isError }
|
||||||
|
: part);
|
||||||
|
}
|
||||||
|
|
||||||
|
function objectFallback(value: unknown): ChatPart[] {
|
||||||
|
if (value == null) return [];
|
||||||
|
if (typeof value === "object") return [{ type: "text", text: summarizeArgs(value) }];
|
||||||
|
return [{ type: "text", text: String(value) }];
|
||||||
|
}
|
||||||
|
|
||||||
|
function summarizeArgs(args: any): string {
|
||||||
|
if (!args || typeof args !== "object") return args == null ? "" : String(args);
|
||||||
|
if (typeof args.command === "string") return args.command;
|
||||||
|
if (typeof args.path === "string") return args.path;
|
||||||
|
if (typeof args.oldText === "string" && typeof args.newText === "string") return "edit text replacement";
|
||||||
|
if (Array.isArray(args.edits)) return `${args.edits.length} edit${args.edits.length === 1 ? "" : "s"}`;
|
||||||
|
const entries = Object.entries(args).filter(([, value]) => value != null).slice(0, 3);
|
||||||
|
return entries.map(([key, value]) => `${key}: ${shortValue(value)}`).join(" · ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function shortValue(value: unknown): string {
|
||||||
|
if (typeof value === "string") return value.length > 80 ? `${value.slice(0, 77)}…` : value;
|
||||||
|
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
||||||
|
if (Array.isArray(value)) return `${value.length} item${value.length === 1 ? "" : "s"}`;
|
||||||
|
if (typeof value === "object" && value) return "object";
|
||||||
|
return "";
|
||||||
|
}
|
||||||
@@ -1,19 +1,63 @@
|
|||||||
import { LitElement, html } from "lit";
|
import { LitElement, html } from "lit";
|
||||||
import { customElement, property } from "lit/decorators.js";
|
import { customElement, property, query, state } from "lit/decorators.js";
|
||||||
import type { ChatLine } from "./shared";
|
import type { ChatLine, ChatPart } from "./shared";
|
||||||
import { chatStyles } from "./shared";
|
import { chatStyles } from "./shared";
|
||||||
|
import "./FormattedText";
|
||||||
|
|
||||||
@customElement("chat-view")
|
@customElement("chat-view")
|
||||||
export class ChatView extends LitElement {
|
export class ChatView extends LitElement {
|
||||||
@property({ attribute: false }) messages: ChatLine[] = [];
|
@property({ attribute: false }) messages: ChatLine[] = [];
|
||||||
|
@query(".chat") private chat?: HTMLDivElement;
|
||||||
|
@state() private pinnedToBottom = true;
|
||||||
|
|
||||||
|
protected willUpdate(): void {
|
||||||
|
this.pinnedToBottom = this.isNearBottom();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(): void {
|
||||||
|
if (this.pinnedToBottom) this.scrollToBottom();
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return html`
|
return html`
|
||||||
<div class="chat">
|
<div class="chat" @scroll=${this.onScroll}>
|
||||||
${this.messages.map((message) => html`<div class="msg ${message.role}"><b>${message.role}</b><pre>${message.text}</pre></div>`)}
|
${this.messages.map((message) => html`
|
||||||
|
<article class="msg ${message.role}">
|
||||||
|
<b class="label">${message.role}</b>
|
||||||
|
${message.parts.map((part) => this.renderPart(part))}
|
||||||
|
</article>
|
||||||
|
`)}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private renderPart(part: ChatPart) {
|
||||||
|
if (part.type === "text") return html`<formatted-text class="part" .text=${part.text}></formatted-text>`;
|
||||||
|
if (part.type === "thinking") return html`<details class="part"><summary>thinking</summary><formatted-text .text=${part.text}></formatted-text></details>`;
|
||||||
|
if (part.type === "toolCall") return html`<div class="part tool-line">▶ ${part.toolName}<span class="summary">${part.summary}</span></div>`;
|
||||||
|
if (part.type === "toolResult") return html`
|
||||||
|
<details class="part" ?open=${part.isError}>
|
||||||
|
<summary>${part.isError ? "✖" : "✓"} ${part.toolName} result</summary>
|
||||||
|
<formatted-text .text=${part.text}></formatted-text>
|
||||||
|
</details>
|
||||||
|
`;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private onScroll() {
|
||||||
|
this.pinnedToBottom = this.isNearBottom();
|
||||||
|
}
|
||||||
|
|
||||||
|
private isNearBottom(): boolean {
|
||||||
|
const chat = this.chat;
|
||||||
|
if (!chat) return true;
|
||||||
|
return chat.scrollHeight - chat.scrollTop - chat.clientHeight < 48;
|
||||||
|
}
|
||||||
|
|
||||||
|
private scrollToBottom() {
|
||||||
|
const chat = this.chat;
|
||||||
|
if (chat) chat.scrollTop = chat.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
static styles = chatStyles;
|
static styles = chatStyles;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { LitElement, html } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators.js";
|
||||||
|
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
||||||
|
import { marked } from "marked";
|
||||||
|
import { formattedTextStyles } from "./shared";
|
||||||
|
|
||||||
|
@customElement("formatted-text")
|
||||||
|
export class FormattedText extends LitElement {
|
||||||
|
@property() text = "";
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`<div class="formatted">${unsafeHTML(toSafeMarkdownHtml(this.text))}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = formattedTextStyles;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toSafeMarkdownHtml(text: string): string {
|
||||||
|
const html = marked.parse(escapeHtml(text), { async: false, breaks: true, gfm: true }) as string;
|
||||||
|
return sanitizeHtml(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(text: string): string {
|
||||||
|
return text
|
||||||
|
.replaceAll("&", "&")
|
||||||
|
.replaceAll("<", "<")
|
||||||
|
.replaceAll(">", ">");
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeHtml(html: string): string {
|
||||||
|
const template = document.createElement("template");
|
||||||
|
template.innerHTML = html;
|
||||||
|
template.content.querySelectorAll("script, style, iframe, object, embed").forEach((node) => node.remove());
|
||||||
|
template.content.querySelectorAll("*").forEach((element) => {
|
||||||
|
for (const attribute of [...element.attributes]) {
|
||||||
|
const name = attribute.name.toLowerCase();
|
||||||
|
if (name.startsWith("on")) element.removeAttribute(attribute.name);
|
||||||
|
if ((name === "href" || name === "src") && !isSafeUrl(attribute.value)) element.removeAttribute(attribute.name);
|
||||||
|
}
|
||||||
|
if (element.tagName === "A") {
|
||||||
|
element.setAttribute("target", "_blank");
|
||||||
|
element.setAttribute("rel", "noreferrer noopener");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return template.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSafeUrl(url: string): boolean {
|
||||||
|
if (url.startsWith("#") || url.startsWith("/")) return true;
|
||||||
|
try {
|
||||||
|
return ["http:", "https:", "mailto:"].includes(new URL(url).protocol);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import "./WorkspaceList";
|
|||||||
import "./SessionList";
|
import "./SessionList";
|
||||||
import "./ChatView";
|
import "./ChatView";
|
||||||
import "./Composer";
|
import "./Composer";
|
||||||
|
import { normalizeMessages, appendText, textMessage } from "../chatMessages";
|
||||||
import { appStyles, type ChatLine } from "./shared";
|
import { appStyles, type ChatLine } from "./shared";
|
||||||
|
|
||||||
@customElement("pi-web-poc")
|
@customElement("pi-web-poc")
|
||||||
@@ -112,23 +113,19 @@ export class PiWebApp extends LitElement {
|
|||||||
|
|
||||||
private applyEvent(event: SessionUiEvent) {
|
private applyEvent(event: SessionUiEvent) {
|
||||||
if (event.type === "assistant.delta") {
|
if (event.type === "assistant.delta") {
|
||||||
const lines = [...this.messages];
|
this.messages = appendText(this.messages, "assistant", event.text);
|
||||||
const last = lines.at(-1);
|
|
||||||
if (last?.role === "assistant") last.text += event.text;
|
|
||||||
else lines.push({ role: "assistant", text: event.text });
|
|
||||||
this.messages = lines;
|
|
||||||
} else if (event.type === "tool.start") {
|
} else if (event.type === "tool.start") {
|
||||||
this.messages = [...this.messages, { role: "tool", text: `▶ ${event.toolName}` }];
|
this.messages = [...this.messages, { role: "tool", parts: [{ type: "toolCall", toolName: event.toolName, summary: "" }] }];
|
||||||
} else if (event.type === "tool.end") {
|
} else if (event.type === "tool.end") {
|
||||||
this.messages = [...this.messages, { role: "tool", text: `${event.isError ? "✖" : "✓"} ${event.toolName}` }];
|
this.messages = [...this.messages, textMessage("tool", `${event.isError ? "✖" : "✓"} ${event.toolName}`)];
|
||||||
} else if (event.type === "session.error") {
|
} else if (event.type === "session.error") {
|
||||||
this.messages = [...this.messages, { role: "system", text: event.message }];
|
this.messages = [...this.messages, textMessage("system", event.message)];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async send(text: string) {
|
private async send(text: string) {
|
||||||
if (!this.selectedSession) return;
|
if (!this.selectedSession) return;
|
||||||
this.messages = [...this.messages, { role: "user", text }];
|
this.messages = [...this.messages, textMessage("user", text)];
|
||||||
try {
|
try {
|
||||||
await api.prompt(this.selectedSession.id, text);
|
await api.prompt(this.selectedSession.id, text);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -182,10 +179,3 @@ export class PiWebApp extends LitElement {
|
|||||||
|
|
||||||
static styles = appStyles;
|
static styles = appStyles;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeMessages(messages: any[]): ChatLine[] {
|
|
||||||
return messages.map((message) => ({
|
|
||||||
role: message.role === "assistant" ? "assistant" : message.role === "user" ? "user" : "system",
|
|
||||||
text: typeof message.content === "string" ? message.content : JSON.stringify(message.content, null, 2),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
import { css } from "lit";
|
import { css } from "lit";
|
||||||
|
|
||||||
|
export type ChatPart =
|
||||||
|
| { type: "text"; text: string }
|
||||||
|
| { type: "thinking"; text: string }
|
||||||
|
| { type: "toolCall"; toolName: string; summary: string }
|
||||||
|
| { type: "toolResult"; toolName: string; text: string; isError: boolean }
|
||||||
|
| { type: "empty" };
|
||||||
|
|
||||||
export interface ChatLine {
|
export interface ChatLine {
|
||||||
role: "user" | "assistant" | "tool" | "system";
|
role: "user" | "assistant" | "tool" | "system";
|
||||||
text: string;
|
parts: ChatPart[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const appStyles = css`
|
export const appStyles = css`
|
||||||
@@ -35,12 +42,42 @@ export const chatStyles = css`
|
|||||||
:host { display: block; min-height: 0; color: #e6edf3; font: 14px system-ui, sans-serif; }
|
:host { display: block; min-height: 0; color: #e6edf3; font: 14px system-ui, sans-serif; }
|
||||||
.chat { height: 100%; overflow: auto; padding: 16px; box-sizing: border-box; }
|
.chat { height: 100%; overflow: auto; padding: 16px; box-sizing: border-box; }
|
||||||
.msg { margin: 0 0 14px; padding: 12px; border: 1px solid #30363d; border-radius: 10px; background: #161b22; }
|
.msg { margin: 0 0 14px; padding: 12px; border: 1px solid #30363d; border-radius: 10px; background: #161b22; }
|
||||||
.msg.user { border-color: #2f81f7; }
|
.msg.user { border-color: #2f81f7; background: #0d2847; }
|
||||||
.msg.tool { color: #d29922; }
|
.msg.tool { border-color: #6e5200; background: #1f1a10; color: #d29922; }
|
||||||
.msg.system { color: #ff7b72; }
|
.msg.system { color: #ff7b72; }
|
||||||
|
.label { display: block; margin-bottom: 8px; color: #8b949e; font-size: 12px; text-transform: uppercase; }
|
||||||
|
formatted-text.part { display: block; }
|
||||||
|
.part + .part { margin-top: 10px; }
|
||||||
|
.tool-line { color: #d29922; }
|
||||||
|
.summary { color: #8b949e; margin-left: 6px; }
|
||||||
|
details { border-top: 1px solid #30363d; padding-top: 8px; }
|
||||||
|
summary { cursor: pointer; color: #8b949e; }
|
||||||
pre { margin: 6px 0 0; white-space: pre-wrap; overflow-wrap: anywhere; font: inherit; }
|
pre { margin: 6px 0 0; white-space: pre-wrap; overflow-wrap: anywhere; font: inherit; }
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const formattedTextStyles = css`
|
||||||
|
:host { display: block; }
|
||||||
|
.formatted { white-space: normal; overflow-wrap: anywhere; line-height: 1.45; }
|
||||||
|
p, ul, ol, pre, blockquote, table { margin: 0 0 10px; }
|
||||||
|
:is(p, ul, ol, pre, blockquote, table):last-child { margin-bottom: 0; }
|
||||||
|
ul, ol { padding-left: 22px; }
|
||||||
|
li + li { margin-top: 3px; }
|
||||||
|
code { border: 1px solid #30363d; border-radius: 4px; background: #0d1117; padding: 1px 4px; font: 13px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
||||||
|
pre { border: 1px solid #30363d; border-radius: 8px; background: #0d1117; padding: 10px; overflow: auto; }
|
||||||
|
pre code { border: 0; padding: 0; background: transparent; }
|
||||||
|
blockquote { border-left: 3px solid #30363d; padding-left: 10px; color: #8b949e; }
|
||||||
|
a { color: #58a6ff; }
|
||||||
|
h1, h2, h3, h4 { margin: 14px 0 8px; line-height: 1.2; }
|
||||||
|
h1:first-child, h2:first-child, h3:first-child, h4:first-child { margin-top: 0; }
|
||||||
|
h1 { font-size: 20px; }
|
||||||
|
h2 { font-size: 17px; }
|
||||||
|
h3 { font-size: 15px; }
|
||||||
|
h4 { font-size: 14px; }
|
||||||
|
table { border-collapse: collapse; display: block; overflow: auto; }
|
||||||
|
th, td { border: 1px solid #30363d; padding: 4px 8px; }
|
||||||
|
th { background: #161b22; }
|
||||||
|
`;
|
||||||
|
|
||||||
export const composerStyles = css`
|
export const composerStyles = css`
|
||||||
:host { display: block; color: #e6edf3; font: 14px system-ui, sans-serif; }
|
:host { display: block; color: #e6edf3; font: 14px system-ui, sans-serif; }
|
||||||
footer { display: grid; grid-template-columns: 1fr auto auto; gap: 8px; padding: 12px; border-top: 1px solid #30363d; }
|
footer { display: grid; grid-template-columns: 1fr auto auto; gap: 8px; padding: 12px; border-top: 1px solid #30363d; }
|
||||||
|
|||||||
Reference in New Issue
Block a user