From 0328d2be55c801d01f728a9cd0ac9dfa796be6fc Mon Sep 17 00:00:00 2001 From: Fridemn <702625325@qq.com> Date: Mon, 27 Jul 2026 19:08:09 +0800 Subject: [PATCH 1/3] feat(markdown): add link safety controls and improve link styling --- .../src/components/content/Markdown.test.tsx | 8 ++++++ dashboard/src/components/content/content.scss | 26 ++++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/dashboard/src/components/content/Markdown.test.tsx b/dashboard/src/components/content/Markdown.test.tsx index f884e145..113e3632 100644 --- a/dashboard/src/components/content/Markdown.test.tsx +++ b/dashboard/src/components/content/Markdown.test.tsx @@ -25,6 +25,14 @@ describe('Streamdown Markdown renderer', () => { expect(html).not.toContain('markdown-body--streaming'); }); + it('marks external links as link-safety controls', () => { + const html = renderToStaticMarkup(); + + expect(html).toContain('AstrBot'); + }); + it('defers rich controls while content is still streaming', () => { const html = renderToStaticMarkup(); diff --git a/dashboard/src/components/content/content.scss b/dashboard/src/components/content/content.scss index f82c308f..10382560 100644 --- a/dashboard/src/components/content/content.scss +++ b/dashboard/src/components/content/content.scss @@ -93,11 +93,35 @@ .markdown-body a, .markdown-body [data-streamdown='link'] { - color: var(--astrbot-text); + display: inline; + margin: 0; + padding: 0; + border: 0; + appearance: none; + background: transparent; + color: var(--astrbot-primary); + cursor: pointer; + font: inherit; + line-height: inherit; + text-align: inherit; + text-decoration: none; + vertical-align: baseline; +} + +.markdown-body a:hover, +.markdown-body [data-streamdown='link']:hover { + text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 3px; } +.markdown-body a:focus-visible, +.markdown-body [data-streamdown='link']:focus-visible { + border-radius: 3px; + outline: 2px solid color-mix(in srgb, var(--astrbot-primary) 38%, transparent); + outline-offset: 2px; +} + .markdown-body [data-streamdown='strong'] { font-weight: 700; } From 8250ddec7a6157ea43f6fc0ed7f475980dcbcc53 Mon Sep 17 00:00:00 2001 From: Fridemn <702625325@qq.com> Date: Mon, 27 Jul 2026 19:21:06 +0800 Subject: [PATCH 2/3] feat(dashboard): add DisclosureButton component and update styles for disclosure controls --- .../src/components/ui/DisclosureButton.tsx | 51 ++++++++++++++++ .../src/components/ui/primitives.test.tsx | 27 +++++++++ .../src/i18n/locales/en-US/core/actions.json | 1 + .../src/i18n/locales/ru-RU/core/actions.json | 1 + .../src/i18n/locales/zh-CN/core/actions.json | 1 + .../src/routes/configuration/PlatformPage.tsx | 16 ++--- .../routes/extensions/ExtensionSections.tsx | 37 +++++++++--- .../src/styles/components/_primitives.scss | 59 +++++++++++++++++++ .../src/styles/features/_extensions.scss | 8 +-- dashboard/src/styles/features/_personas.scss | 3 +- dashboard/src/styles/features/_platforms.scss | 5 +- 11 files changed, 187 insertions(+), 22 deletions(-) create mode 100644 dashboard/src/components/ui/DisclosureButton.tsx diff --git a/dashboard/src/components/ui/DisclosureButton.tsx b/dashboard/src/components/ui/DisclosureButton.tsx new file mode 100644 index 00000000..2b77f2fa --- /dev/null +++ b/dashboard/src/components/ui/DisclosureButton.tsx @@ -0,0 +1,51 @@ +import { forwardRef, type ButtonHTMLAttributes } from 'react'; + +import { MdiIcon } from '@/components/icons/MdiIcon'; + +export type DisclosureButtonProps = Omit< + ButtonHTMLAttributes, + 'aria-expanded' | 'aria-label' | 'children' +> & { + collapseLabel: string; + compact?: boolean; + direction?: 'down' | 'right'; + expanded: boolean; + expandLabel: string; + label?: string; +}; + +export const DisclosureButton = forwardRef(function DisclosureButton( + { + className = '', + collapseLabel, + compact = false, + direction = 'down', + expanded, + expandLabel, + label = '', + title, + type = 'button', + ...props + }, + ref, +) { + const actionLabel = expanded ? collapseLabel : expandLabel; + const accessibleLabel = label ? `${actionLabel}: ${label}` : actionLabel; + + return ( + + + + ); +}); diff --git a/dashboard/src/components/ui/primitives.test.tsx b/dashboard/src/components/ui/primitives.test.tsx index 73e23e21..45c41e9f 100644 --- a/dashboard/src/components/ui/primitives.test.tsx +++ b/dashboard/src/components/ui/primitives.test.tsx @@ -5,6 +5,7 @@ import { describe, expect, it } from 'vitest'; import { Button } from './Button'; import { DataTable } from './DataTable'; +import { DisclosureButton } from './DisclosureButton'; import { DialogActions } from './DialogActions'; import { Pagination } from './Pagination'; import { SearchField } from './SearchField'; @@ -59,6 +60,32 @@ describe('shared UI primitives', () => { expect(markup).toContain('ui-status-chip--success'); }); + it('keeps disclosure controls centered and exposes their current state', () => { + const collapsed = renderToStaticMarkup( + , + ); + const expanded = renderToStaticMarkup( + , + ); + + expect(collapsed).toContain('aria-expanded="false"'); + expect(collapsed).toContain('aria-label="Expand: Config file"'); + expect(collapsed).toContain('ui-disclosure-button__icon'); + expect(expanded).toContain('aria-expanded="true"'); + expect(expanded).toContain('aria-label="Collapse: Tool details"'); + expect(expanded).toContain('ui-disclosure-button--compact'); + expect(expanded).toContain('ui-disclosure-button--tree'); + expect(expanded).toContain('mdi-chevron-right'); + expect(primitiveStyles).toContain(".ui-disclosure-button[aria-expanded='true']"); + }); + it('shares table selection, empty state and pagination structure', () => { const table = renderToStaticMarkup( - setShowConfigSection((current) => !current)} - type="button" - > - - + /> {showConfigSection && ( - + ) => t(`features.command.${key}`, options); const u = (key: string, options?: Record) => t(`features.tooluse.${key}`, options); const e = (key: string) => t(`features.extension.${key}`); + const disclosureLabels = { + collapse: t('core.actions.collapse'), + expand: t('core.actions.expand'), + }; const [commands, setCommands] = useState([]); const [tools, setTools] = useState([]); const [summary, setSummary] = useState({ conflicts: 0, disabled: 0 }); @@ -394,6 +399,7 @@ export function ComponentsSection() { expanded={expandedGroups.has(recordId(item, 'handler_full_name'))} item={item} key={recordId(item, 'handler_full_name') || index} + labels={disclosureLabels} onDetails={setDetails} onPermission={commandPermission} onRename={(command) => @@ -413,6 +419,7 @@ export function ComponentsSection() { expanded={expandedTools.has(recordId(item, 'name'))} item={item} key={recordId(item, 'name') || index} + labels={disclosureLabels} onPermission={toolPermission} onToggle={toggleTool} onToggleExpand={(tool) => toggleSet(setExpandedTools, recordId(tool, 'name'))} @@ -615,6 +622,7 @@ function CommandFilters({ function CommandRow({ expanded, item, + labels, onDetails, onPermission, onRename, @@ -624,6 +632,7 @@ function CommandRow({ }: { expanded: boolean; item: JsonObject; + labels: { collapse: string; expand: string }; onDetails: (item: JsonObject) => void; onPermission: (item: JsonObject, value: 'admin' | 'member') => Promise; onRename: (item: JsonObject) => void; @@ -650,9 +659,15 @@ function CommandRow({ {isGroup && subCommands.length ? ( - onToggleExpand(item)} type="button"> - - + onToggleExpand(item)} + /> ) : type === 'sub_command' ? ( ) : null} @@ -722,6 +737,7 @@ function CommandRow({ function ToolRow({ expanded, item, + labels, onPermission, onToggle, onToggleExpand, @@ -729,6 +745,7 @@ function ToolRow({ }: { expanded: boolean; item: JsonObject; + labels: { collapse: string; expand: string }; onPermission: (item: JsonObject, value: 'admin' | 'member') => Promise; onToggle: (item: JsonObject) => Promise; onToggleExpand: (item: JsonObject) => void; @@ -744,9 +761,15 @@ function ToolRow({ <> - onToggleExpand(item)} type="button"> - - + onToggleExpand(item)} + /> @@ -900,7 +923,7 @@ function RenameCommandDialog({ /> - setAliasesOpen((value) => !value)} type="button"> + setAliasesOpen((value) => !value)} type="button"> {t('dialogs.rename.aliases')} diff --git a/dashboard/src/styles/components/_primitives.scss b/dashboard/src/styles/components/_primitives.scss index a726e709..b64dcf27 100644 --- a/dashboard/src/styles/components/_primitives.scss +++ b/dashboard/src/styles/components/_primitives.scss @@ -133,6 +133,65 @@ font-size: 19px; } +.ui-disclosure-button { + position: relative; + display: grid; + width: 40px; + height: 40px; + flex: 0 0 40px; + padding: 0; + border: 0; + border-radius: var(--astrbot-radius-control); + background: transparent; + color: color-mix(in srgb, var(--astrbot-text) 68%, transparent); + cursor: pointer; + place-items: center; +} + +.ui-disclosure-button::before { + position: absolute; + inset: -2px; + content: ''; +} + +.ui-disclosure-button:hover { + background: var(--astrbot-hover); + color: var(--astrbot-primary); +} + +.ui-disclosure-button:focus-visible { + outline: 2px solid color-mix(in srgb, var(--astrbot-primary) 38%, transparent); + outline-offset: 2px; +} + +.ui-disclosure-button__icon { + font-size: 20px; + line-height: 1; + transition: transform .18s ease; +} + +.ui-disclosure-button[aria-expanded='true'] .ui-disclosure-button__icon { + transform: rotate(180deg); +} + +.ui-disclosure-button--tree[aria-expanded='true'] .ui-disclosure-button__icon { + transform: rotate(90deg); +} + +.ui-disclosure-button--compact { + width: 32px; + height: 32px; + flex-basis: 32px; +} + +.ui-disclosure-button--compact::before { + inset: -6px; +} + +.ui-disclosure-button--compact .ui-disclosure-button__icon { + font-size: 18px; +} + .ui-search-field { display: flex; min-width: 180px; diff --git a/dashboard/src/styles/features/_extensions.scss b/dashboard/src/styles/features/_extensions.scss index b265fe04..2eb7794e 100644 --- a/dashboard/src/styles/features/_extensions.scss +++ b/dashboard/src/styles/features/_extensions.scss @@ -121,9 +121,9 @@ .component-panel__table > footer button { display: grid; width: 32px; height: 32px; place-items: center; border: 0; border-radius: 6px; background: transparent; color: var(--astrbot-text); cursor: pointer; } .component-panel__table > footer button:disabled { opacity: .3; } .component-command-name { display: flex; align-items: center; } -.component-command-name button, -.component-expand { display: grid; width: 28px; height: 28px; flex: 0 0 auto; place-items: center; border: 0; background: transparent; color: var(--astrbot-text); cursor: pointer; } -.component-command-indent { width: 28px; } +.component-command-name .ui-disclosure-button, +.component-expand { margin: -2px 0; } +.component-command-indent { width: 32px; } .component-command-name code, .component-details code { padding: 3px 7px; border-radius: 5px; background: color-mix(in srgb, var(--astrbot-primary) 10%, transparent); color: var(--astrbot-text); white-space: nowrap; } .is-subcommand .component-command-name code { background: color-mix(in srgb, var(--astrbot-secondary) 10%, transparent); color: var(--astrbot-secondary); } @@ -168,6 +168,7 @@ .component-rename input { min-height: 42px; padding: 8px 11px; border: 1px solid var(--astrbot-border); border-radius: 8px; background: var(--astrbot-surface); color: var(--astrbot-text); font: inherit; } .component-rename > section { overflow: hidden; border: 1px solid var(--astrbot-border); border-radius: 9px; } .component-rename > section > button { display: flex; width: 100%; min-height: 46px; align-items: center; justify-content: space-between; padding: 8px 13px; border: 0; background: transparent; color: var(--astrbot-text); font: inherit; } +.component-rename > section > button > .mdi { flex: 0 0 auto; margin-left: 12px; font-size: 19px; line-height: 1; } .component-rename > section > div { display: grid; gap: 9px; padding: 12px; border-top: 1px solid var(--astrbot-border); } .component-rename > section > div label { display: flex; gap: 8px; } .component-rename > section > div label input { flex: 1; } @@ -551,4 +552,3 @@ .knowledge-retrieval { display: grid; gap: 10px; } .knowledge-chunk-preview { max-width: 700px; white-space: pre-wrap; overflow-wrap: anywhere; } .knowledge-chunk-content { max-height: 65vh; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; } - diff --git a/dashboard/src/styles/features/_personas.scss b/dashboard/src/styles/features/_personas.scss index 44b256f5..674548fd 100644 --- a/dashboard/src/styles/features/_personas.scss +++ b/dashboard/src/styles/features/_personas.scss @@ -99,8 +99,10 @@ .persona-choice { margin: 0; overflow: hidden; border: 1px solid var(--astrbot-border); border-radius: 10px; background: var(--astrbot-surface); box-shadow: 0 2px 7px rgb(0 0 0 / 6%); } .persona-choice__header { display: flex; width: 100%; min-height: 52px; align-items: center; justify-content: space-between; padding: 13px 16px; border: 0; background: transparent; color: inherit; cursor: pointer; text-align: left; } .persona-choice__header:hover { background: color-mix(in srgb, var(--astrbot-text) 3%, transparent); } +.persona-choice__header:focus-visible { outline: 2px solid color-mix(in srgb, var(--astrbot-primary) 38%, transparent); outline-offset: -2px; } .persona-choice__header > span { display: flex; min-width: 0; align-items: center; gap: 8px; } .persona-choice__header > span > .mdi { font-size: 20px; } +.persona-choice__header > .mdi { flex: 0 0 auto; margin-left: 12px; font-size: 20px; line-height: 1; } .persona-choice__header strong { font-size: 17px; } .persona-choice__header small { display: grid; min-width: 23px; height: 23px; padding: 0 6px; border-radius: 999px; background: color-mix(in srgb, var(--astrbot-primary) 13%, transparent); color: var(--astrbot-primary); font-size: 12px; place-items: center; } .persona-choice__body { padding: 0 20px 18px; animation: motion-expand-content-in .2s ease-out both; } @@ -172,4 +174,3 @@ @media (max-width: 480px) { .persona-grid { grid-template-columns: 1fr; } .persona-toolbar > div { flex-wrap: wrap; } } .headless-dialog__content:has(.json-editor--dialog) { width: min(900px, calc(100vw - 32px)); } - diff --git a/dashboard/src/styles/features/_platforms.scss b/dashboard/src/styles/features/_platforms.scss index 5c7d62d5..c9a24a3f 100644 --- a/dashboard/src/styles/features/_platforms.scss +++ b/dashboard/src/styles/features/_platforms.scss @@ -88,10 +88,9 @@ .platform-editor__config { margin: 18px 0 0 40px; } .platform-editor__config .dynamic-config { padding: 0; border: 0; } .platform-editor__step--config { margin-top: 30px; } -.platform-editor__step-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 18px; } +.platform-editor__step-heading { display: flex; align-items: center; justify-content: space-between; gap: 18px; } +.platform-editor__step-heading > div { min-width: 0; flex: 1; } .platform-editor__step-heading h3 small { margin-left: 6px; padding: 3px 8px; border-radius: 5px; background: color-mix(in srgb, var(--astrbot-primary) 10%, transparent); color: var(--astrbot-primary); font-size: 11px; font-weight: 500; } -.platform-editor__step-heading button { display: grid; width: 36px; height: 36px; flex: 0 0 auto; place-items: center; border: 0; border-radius: 50%; background: transparent; color: inherit; cursor: pointer; } -.platform-editor__step-heading button:hover { background: var(--astrbot-hover); } .platform-editor__profiles { display: grid; gap: 14px; margin-top: 22px; } .platform-editor__profiles > label { display: flex; align-items: center; gap: 10px; font-size: 16px; } .platform-editor__profiles input[type='radio'] { width: 20px; height: 20px; accent-color: var(--astrbot-text); } From f91d9351a3b5f551e49cc8fbb5ab7f7ee8168329 Mon Sep 17 00:00:00 2001 From: Fridemn <702625325@qq.com> Date: Mon, 27 Jul 2026 19:25:13 +0800 Subject: [PATCH 3/3] fix(dashboard): improve handling of enum values in ConfigGroup component --- .../config/DynamicConfigForm.test.tsx | 53 +++++++++++++++++++ .../components/config/DynamicConfigForm.tsx | 2 +- 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/dashboard/src/components/config/DynamicConfigForm.test.tsx b/dashboard/src/components/config/DynamicConfigForm.test.tsx index 66841c1b..f7407c20 100644 --- a/dashboard/src/components/config/DynamicConfigForm.test.tsx +++ b/dashboard/src/components/config/DynamicConfigForm.test.tsx @@ -45,6 +45,59 @@ describe('DynamicConfigForm', () => { expect(markup).not.toContain('dynamic-object__manage'); }); + it('does not add a blank option when the current enum value is valid', () => { + const markup = renderToStaticMarkup( + + undefined} + translationPath="config" + value={{ segment_mode: 'regex' }} + variant="inline" + /> + , + ); + + expect(markup).not.toContain(''); + expect(markup).toContain('Regular expression'); + expect(markup).toContain('Word list'); + }); + + it('keeps a hidden placeholder for an unmatched enum value', () => { + const markup = renderToStaticMarkup( + + undefined} + translationPath="config" + value={{ segment_mode: 'legacy_mode' }} + variant="inline" + /> + , + ); + + expect(markup).toContain(''); + }); + it('renders the embedding dimension detector for special metadata', () => { const markup = renderToStaticMarkup( diff --git a/dashboard/src/components/config/DynamicConfigForm.tsx b/dashboard/src/components/config/DynamicConfigForm.tsx index df7ff62b..f7fbd24f 100644 --- a/dashboard/src/components/config/DynamicConfigForm.tsx +++ b/dashboard/src/components/config/DynamicConfigForm.tsx @@ -599,7 +599,7 @@ function ConfigControl({ onChange={(event) => onChange(metadata.options?.[Number(event.target.value)])} value={selectedIndex < 0 ? '' : selectedIndex} > - + {selectedIndex < 0 && } {metadata.options.map((option, index) => ( {String(labels[index] ?? option)}