Archived
Merge pull request #28 from TheOneironaut/chat-bidi-rtl-support
Add BiDi support to chat text
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@jmfederico/pi-web": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Improve chat, prompt, and session text rendering for RTL and mixed-direction content.
|
||||||
@@ -55,6 +55,7 @@ export class CodeViewer extends LitElement {
|
|||||||
EditorView.editable.of(false),
|
EditorView.editable.of(false),
|
||||||
EditorView.lineWrapping,
|
EditorView.lineWrapping,
|
||||||
viewerTheme,
|
viewerTheme,
|
||||||
|
...bidiTextExtensions(this.language),
|
||||||
...languageExtensions(this.language),
|
...languageExtensions(this.language),
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
@@ -97,6 +98,19 @@ const viewerTheme = EditorView.theme({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const bidiTextTheme = EditorView.theme({
|
||||||
|
".cm-content": {
|
||||||
|
textAlign: "start",
|
||||||
|
},
|
||||||
|
".cm-line": {
|
||||||
|
unicodeBidi: "plaintext",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function bidiTextExtensions(language: string | undefined): Extension[] {
|
||||||
|
return language === "markdown" ? [EditorView.contentAttributes.of({ dir: "auto" }), bidiTextTheme] : [];
|
||||||
|
}
|
||||||
|
|
||||||
function languageExtensions(language: string | undefined): Extension[] {
|
function languageExtensions(language: string | undefined): Extension[] {
|
||||||
if (language === undefined) return [];
|
if (language === undefined) return [];
|
||||||
switch (language) {
|
switch (language) {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export class FormattedText extends LitElement {
|
|||||||
@property() text = "";
|
@property() text = "";
|
||||||
|
|
||||||
override render() {
|
override render() {
|
||||||
return html`<div class="formatted" @click=${this.onFormattedClick}>${unsafeHTML(toSafeMarkdownHtml(this.text))}</div>`;
|
return html`<div class="formatted" dir="auto" @click=${this.onFormattedClick}>${unsafeHTML(toSafeMarkdownHtml(this.text))}</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
override updated(): void {
|
override updated(): void {
|
||||||
|
|||||||
@@ -500,6 +500,7 @@ const proseInputAssistanceAttributes: Record<string, string> = {
|
|||||||
autocorrect: "on",
|
autocorrect: "on",
|
||||||
autocapitalize: "sentences",
|
autocapitalize: "sentences",
|
||||||
writingsuggestions: "true",
|
writingsuggestions: "true",
|
||||||
|
dir: "auto",
|
||||||
};
|
};
|
||||||
|
|
||||||
const codeLikeInputAssistanceAttributes: Record<string, string> = {
|
const codeLikeInputAssistanceAttributes: Record<string, string> = {
|
||||||
@@ -507,6 +508,7 @@ const codeLikeInputAssistanceAttributes: Record<string, string> = {
|
|||||||
autocorrect: "off",
|
autocorrect: "off",
|
||||||
autocapitalize: "off",
|
autocapitalize: "off",
|
||||||
writingsuggestions: "false",
|
writingsuggestions: "false",
|
||||||
|
dir: "auto",
|
||||||
};
|
};
|
||||||
|
|
||||||
function inputAssistanceContentAttributes(draftBeforeCursor: string): Record<string, string> {
|
function inputAssistanceContentAttributes(draftBeforeCursor: string): Record<string, string> {
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ export class SessionList extends LitElement implements KeyboardNavigableSection
|
|||||||
const selectedTitle = this.selected?.path ?? selectedSummary;
|
const selectedTitle = this.selected?.path ?? selectedSummary;
|
||||||
return html`
|
return html`
|
||||||
<h2>
|
<h2>
|
||||||
<button class="section-toggle" aria-expanded=${String(!this.collapsed)} @click=${() => { this.onToggleCollapsed?.(); }}><span class="section-title"><span class="section-name">${this.collapsed ? "▸" : "▾"} Sessions</span>${this.collapsed ? html`<small class="section-selected" title=${selectedTitle}>${selectedSummary}</small>` : null}</span></button>
|
<button class="section-toggle" aria-expanded=${String(!this.collapsed)} @click=${() => { this.onToggleCollapsed?.(); }}><span class="section-title"><span class="section-name">${this.collapsed ? "▸" : "▾"} Sessions</span>${this.collapsed ? html`<small class="section-selected" dir="auto" title=${selectedTitle}>${selectedSummary}</small>` : null}</span></button>
|
||||||
${this.renderCurrentSelectionButton(currentSessions)}
|
${this.renderCurrentSelectionButton(currentSessions)}
|
||||||
<small class="section-count">${sessionCount}</small>
|
<small class="section-count">${sessionCount}</small>
|
||||||
<button ?disabled=${!this.canStart} @click=${(event: MouseEvent) => { event.stopPropagation(); this.onStart?.(); }}>+</button>
|
<button ?disabled=${!this.canStart} @click=${(event: MouseEvent) => { event.stopPropagation(); this.onStart?.(); }}>+</button>
|
||||||
@@ -213,7 +213,7 @@ export class SessionList extends LitElement implements KeyboardNavigableSection
|
|||||||
>
|
>
|
||||||
<div class="action-main ${selectionActive ? "selecting" : ""}">
|
<div class="action-main ${selectionActive ? "selecting" : ""}">
|
||||||
${showsCheckbox ? html`<input class="session-checkbox" type="checkbox" aria-label=${`Select ${sessionLabel(session)}`} .checked=${bulkSelected} @click=${(event: MouseEvent) => { event.stopPropagation(); }} @change=${() => { this.toggleSelected(session.id); }}>` : null}
|
${showsCheckbox ? html`<input class="session-checkbox" type="checkbox" aria-label=${`Select ${sessionLabel(session)}`} .checked=${bulkSelected} @click=${(event: MouseEvent) => { event.stopPropagation(); }} @change=${() => { this.toggleSelected(session.id); }}>` : null}
|
||||||
<span class="action-name">${row.depth > 0 ? html`<span class="tree-marker">↳</span>` : null}${sessionLabel(session)}${row.depth > 2 ? html` <span class="badge">depth ${row.depth}</span>` : null}${row.hasMissingParent ? html` <span class="badge">parent unavailable</span>` : null}</span><small>${this.renderSessionMetaPrefix(session)}${String(session.messageCount)} messages</small>
|
<span class="action-name" dir="auto">${row.depth > 0 ? html`<span class="tree-marker">↳</span>` : null}${sessionLabel(session)}${row.depth > 2 ? html` <span class="badge">depth ${row.depth}</span>` : null}${row.hasMissingParent ? html` <span class="badge">parent unavailable</span>` : null}</span><small>${this.renderSessionMetaPrefix(session)}${String(session.messageCount)} messages</small>
|
||||||
${this.renderActivity(session)}
|
${this.renderActivity(session)}
|
||||||
</div>
|
</div>
|
||||||
<div class="action-menu">
|
<div class="action-menu">
|
||||||
@@ -375,6 +375,7 @@ export class SessionList extends LitElement implements KeyboardNavigableSection
|
|||||||
.bulk-row { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin: 0 0 6px; }
|
.bulk-row { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin: 0 0 6px; }
|
||||||
.bulk-row button { padding: 5px 7px; font-size: 12px; }
|
.bulk-row button { padding: 5px 7px; font-size: 12px; }
|
||||||
.bulk-row small { display: inline; min-width: 0; color: var(--pi-muted); }
|
.bulk-row small { display: inline; min-width: 0; color: var(--pi-muted); }
|
||||||
|
.action-name, .section-selected { text-align: start; unicode-bidi: plaintext; }
|
||||||
.bulk-row .capability-hint { flex: 1 0 100%; color: var(--pi-warning); }
|
.bulk-row .capability-hint { flex: 1 0 100%; color: var(--pi-warning); }
|
||||||
.bulk-row.selecting { padding: 6px; border: 1px solid var(--pi-border-muted); border-radius: 8px; background: color-mix(in srgb, var(--pi-surface) 65%, transparent); }
|
.bulk-row.selecting { padding: 6px; border: 1px solid var(--pi-border-muted); border-radius: 8px; background: color-mix(in srgb, var(--pi-surface) 65%, transparent); }
|
||||||
button.danger, .action-menu-panel button.danger { color: var(--pi-danger); }
|
button.danger, .action-menu-panel button.danger { color: var(--pi-danger); }
|
||||||
|
|||||||
@@ -345,6 +345,7 @@ export const chatStyles = css`
|
|||||||
.msg-meta:focus::before, .msg-meta.expanded::before { content: ""; }
|
.msg-meta:focus::before, .msg-meta.expanded::before { content: ""; }
|
||||||
}
|
}
|
||||||
formatted-text.part { display: block; }
|
formatted-text.part { display: block; }
|
||||||
|
formatted-text.part, .queued-message formatted-text { text-align: start; unicode-bidi: plaintext; }
|
||||||
.part { max-width: 100%; min-width: 0; box-sizing: border-box; overflow: visible; }
|
.part { max-width: 100%; min-width: 0; box-sizing: border-box; overflow: visible; }
|
||||||
.part + .part { margin-top: 10px; }
|
.part + .part { margin-top: 10px; }
|
||||||
.tool-line { color: var(--pi-warning); }
|
.tool-line { color: var(--pi-warning); }
|
||||||
@@ -355,22 +356,22 @@ export const chatStyles = css`
|
|||||||
.skill-invocation > summary, .skill-read > strong { color: var(--pi-purple); }
|
.skill-invocation > summary, .skill-read > strong { color: var(--pi-purple); }
|
||||||
.skill-invocation > small, .skill-read > small { display: block; margin: 6px 0 0; color: var(--pi-muted); }
|
.skill-invocation > small, .skill-read > small { display: block; margin: 6px 0 0; color: var(--pi-muted); }
|
||||||
summary { cursor: pointer; color: var(--pi-muted); }
|
summary { cursor: pointer; color: var(--pi-muted); }
|
||||||
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; direction: ltr; text-align: left; unicode-bidi: isolate; }
|
||||||
.shell-output { color: var(--pi-text); font: 13px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; line-height: 1.45; }
|
.shell-output { color: var(--pi-text); font: 13px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; line-height: 1.45; direction: ltr; text-align: left; unicode-bidi: isolate; }
|
||||||
@keyframes pulse { 0%, 100% { transform: scale(.75); opacity: .55; } 50% { transform: scale(1.2); opacity: 1; } }
|
@keyframes pulse { 0%, 100% { transform: scale(.75); opacity: .55; } 50% { transform: scale(1.2); opacity: 1; } }
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const formattedTextStyles = css`
|
export const formattedTextStyles = css`
|
||||||
:host { display: block; }
|
:host { display: block; }
|
||||||
.formatted { white-space: normal; overflow-wrap: anywhere; line-height: 1.45; }
|
.formatted { white-space: normal; overflow-wrap: anywhere; line-height: 1.45; text-align: start; unicode-bidi: plaintext; }
|
||||||
p, ul, ol, pre, blockquote, table, .code-block-wrapper { margin: 0 0 10px; }
|
p, ul, ol, pre, blockquote, table, .code-block-wrapper { margin: 0 0 10px; }
|
||||||
:is(p, ul, ol, pre, blockquote, table, .code-block-wrapper):last-child { margin-bottom: 0; }
|
:is(p, ul, ol, pre, blockquote, table, .code-block-wrapper):last-child { margin-bottom: 0; }
|
||||||
ul, ol { padding-left: 22px; }
|
ul, ol { padding-left: 22px; }
|
||||||
li + li { margin-top: 3px; }
|
li + li { margin-top: 3px; }
|
||||||
code { border: 1px solid var(--pi-border); border-radius: 4px; background: var(--pi-bg); padding: 1px 4px; font: 13px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
code { border: 1px solid var(--pi-border); border-radius: 4px; background: var(--pi-bg); padding: 1px 4px; font: 13px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; direction: ltr; text-align: left; unicode-bidi: isolate; }
|
||||||
.code-block-wrapper { position: relative; }
|
.code-block-wrapper { position: relative; }
|
||||||
.code-block-wrapper pre { margin: 0; padding-right: 40px; }
|
.code-block-wrapper pre { margin: 0; padding-right: 40px; }
|
||||||
pre { border: 1px solid var(--pi-border); border-radius: 8px; background: var(--pi-bg); padding: 10px; overflow-x: auto; overflow-y: hidden; }
|
pre { border: 1px solid var(--pi-border); border-radius: 8px; background: var(--pi-bg); padding: 10px; overflow-x: auto; overflow-y: hidden; direction: ltr; text-align: left; unicode-bidi: isolate; }
|
||||||
pre code { border: 0; padding: 0; background: transparent; }
|
pre code { border: 0; padding: 0; background: transparent; }
|
||||||
.code-copy-button { position: absolute; top: 6px; right: 6px; z-index: 1; display: inline-grid; place-items: center; width: 24px; height: 24px; border: 1px solid var(--pi-border); border-radius: 6px; background: var(--pi-surface); color: var(--pi-muted); padding: 0; font: 14px system-ui, sans-serif; line-height: 1; cursor: pointer; }
|
.code-copy-button { position: absolute; top: 6px; right: 6px; z-index: 1; display: inline-grid; place-items: center; width: 24px; height: 24px; border: 1px solid var(--pi-border); border-radius: 6px; background: var(--pi-surface); color: var(--pi-muted); padding: 0; font: 14px system-ui, sans-serif; line-height: 1; cursor: pointer; }
|
||||||
.code-copy-button:hover, .code-copy-button:focus { color: var(--pi-text); border-color: var(--pi-accent); }
|
.code-copy-button:hover, .code-copy-button:focus { color: var(--pi-text); border-color: var(--pi-accent); }
|
||||||
@@ -466,8 +467,8 @@ export const promptEditorStyles = css`
|
|||||||
textarea, .markdown-editor .cm-editor { box-sizing: border-box; width: 100%; min-height: 54px; max-height: 220px; resize: none; overflow: hidden; border-radius: 8px; border: 1px solid var(--pi-border); background: var(--pi-bg); color: var(--pi-text); font: 16px/1.4 system-ui, sans-serif; }
|
textarea, .markdown-editor .cm-editor { box-sizing: border-box; width: 100%; min-height: 54px; max-height: 220px; resize: none; overflow: hidden; border-radius: 8px; border: 1px solid var(--pi-border); background: var(--pi-bg); color: var(--pi-text); font: 16px/1.4 system-ui, sans-serif; }
|
||||||
textarea { overflow-y: auto; padding: 8px; }
|
textarea { overflow-y: auto; padding: 8px; }
|
||||||
.markdown-editor .cm-scroller { max-height: 220px; overflow-y: auto; font-family: system-ui, sans-serif; line-height: 1.4; }
|
.markdown-editor .cm-scroller { max-height: 220px; overflow-y: auto; font-family: system-ui, sans-serif; line-height: 1.4; }
|
||||||
.markdown-editor .cm-content { min-height: 38px; padding: 8px 44px 8px 8px; caret-color: var(--pi-text); }
|
.markdown-editor .cm-content { min-height: 38px; padding: 8px 44px 8px 8px; caret-color: var(--pi-text); text-align: start; unicode-bidi: plaintext; }
|
||||||
.markdown-editor .cm-line { padding: 0; }
|
.markdown-editor .cm-line { padding: 0; unicode-bidi: plaintext; }
|
||||||
.markdown-editor .cm-placeholder { color: var(--pi-dim); }
|
.markdown-editor .cm-placeholder { color: var(--pi-dim); }
|
||||||
.markdown-editor .cm-focused { outline: none; }
|
.markdown-editor .cm-focused { outline: none; }
|
||||||
.shell-mode textarea, .shell-mode .markdown-editor .cm-editor { border-color: var(--pi-success); box-shadow: 0 0 0 1px var(--pi-success-ring); }
|
.shell-mode textarea, .shell-mode .markdown-editor .cm-editor { border-color: var(--pi-success); box-shadow: 0 0 0 1px var(--pi-success-ring); }
|
||||||
|
|||||||
Reference in New Issue
Block a user