From 6fdd41bd8b907db071b0dc98b431623978c16199 Mon Sep 17 00:00:00 2001 From: Sheng Kun Chang Date: Sat, 4 Jul 2026 06:07:04 +0800 Subject: [PATCH 1/2] feat(service-logcat,segmented-button): initialQuery seed, i18n labels, option badge ServiceLogcat gains initialQuery (seed the text filter on mount, e.g. a deep link) and an optional labels prop covering every string it renders, so callers outside English can localize it. SegmentedButton options gain an optional badge (small count pill anchored to the option's corner). Co-Authored-By: Claude Sonnet 5 --- package.json | 2 +- .../ui/dev/service-logcat/ServiceLogcat.tsx | 99 ++++++++++++++----- .../ui/dev/service-logcat/useLogcat.ts | 4 +- .../segmented-button/SegmentedButton.tsx | 13 ++- 4 files changed, 92 insertions(+), 26 deletions(-) diff --git a/package.json b/package.json index 56f1cc28d..45038dc57 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@mirrorstack-ai/web-ui-kit", "packageManager": "pnpm@10.29.3", - "version": "0.6.1", + "version": "0.6.2", "main": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { diff --git a/src/components/ui/dev/service-logcat/ServiceLogcat.tsx b/src/components/ui/dev/service-logcat/ServiceLogcat.tsx index 21a8c0c84..e06b15d1f 100644 --- a/src/components/ui/dev/service-logcat/ServiceLogcat.tsx +++ b/src/components/ui/dev/service-logcat/ServiceLogcat.tsx @@ -23,6 +23,53 @@ export const meta: ComponentMeta = { "Service log console with severity-floor filter, text filter, live-tail toggle, copy, load-older paging, and an expandable request/response detail per line", }; +/** Every user-facing string ServiceLogcat renders, for callers that need it + * in a locale other than English — all optional, defaulting to the current + * English copy, so passing nothing is fully backward compatible. */ +export interface ServiceLogcatLabels { + /** Default: "Logcat" */ + title?: string; + /** Live-tail toggle when on. Default: "Live" */ + live?: string; + /** Live-tail toggle when off. Default: "Paused" */ + paused?: string; + /** Severity-floor option: no filter. Default: "All" */ + floorAll?: string; + /** Severity-floor option: warn and above. Default: "Warn+" */ + floorWarn?: string; + /** Severity-floor option: errors only. Default: "Errors" */ + floorError?: string; + /** aria-label on the severity-floor segmented control. Default: "Filter by level" */ + filterAriaLabel?: string; + /** Text-filter input label. Default: "Filter logs" */ + filterPlaceholder?: string; + /** aria-label on the clear-filter button. Default: "Clear filter" */ + clearFilterAriaLabel?: string; + /** aria-label on the copy-logs button. Default: "Copy logs" */ + copyAriaLabel?: string; + /** Shown when the filter matches nothing. Default: "No matching log entries." */ + noMatchingEntries?: string; + /** Expanded-row request section heading. Default: "Request" */ + request?: string; + /** Expanded-row response section heading. Default: "Response" */ + response?: string; + /** Expanded-row body when neither request nor response is present. + * Default: "No request or response body." */ + noRequestResponseBody?: string; + /** Load-older-entries row label. Default: "Load older entries" */ + loadOlderEntries?: string; + /** Footer entry-count line, e.g. "42 entries" or "5 of 42" plus the active + * query. Called with (filteredCount, total, query). */ + entriesLabel?: (filteredCount: number, total: number, query: string) => string; + /** Footer status when tailing. Default: "tailing" */ + tailingStatus?: string; + /** Footer status when paused. Default: "paused" */ + pausedStatus?: string; +} + +const defaultEntriesLabel = (filteredCount: number, total: number, query: string) => + `${filteredCount === total ? `${total} entries` : `${filteredCount} of ${total}`}${query ? ` · "${query}"` : ""}`; + export interface ServiceLogcatProps { /** Log entries, chronological (oldest first); the console reverses them so the newest line sits at the top. */ logs: LogEntry[]; @@ -32,6 +79,12 @@ export interface ServiceLogcatProps { hasOlder?: boolean; /** Older page currently loading — shows the row in a loading state and suppresses re-fire. */ loadingOlder?: boolean; + /** Seeds the text filter box on mount (e.g. a module slug, to land pre-filtered + * from a deep link) — after that, the input is normal uncontrolled state. */ + initialQuery?: string; + /** Override any/all of the component's own strings for i18n. Unset fields + * fall back to the English default. */ + labels?: ServiceLogcatLabels; } const LEVEL_BADGE: Record = { @@ -48,12 +101,6 @@ const LEVEL_TEXT: Record = { error: "text-error", }; -const FLOOR_OPTIONS: readonly { value: LevelFloor; label: string }[] = [ - { value: "all", label: "All" }, - { value: "warn", label: "Warn+" }, - { value: "error", label: "Errors" }, -]; - // Compact single-line timestamp: "2026-06-30T16:23:45.225373081Z" -> // "2026-06-30 16:23:45.225" (drop the T, nanoseconds, and trailing Z). function fmtTs(ts: string): string { @@ -101,9 +148,16 @@ export function ServiceLogcat({ onLoadOlder, hasOlder = false, loadingOlder = false, + initialQuery, + labels, }: ServiceLogcatProps) { + const floorOptions: readonly { value: LevelFloor; label: string }[] = [ + { value: "all", label: labels?.floorAll ?? "All" }, + { value: "warn", label: labels?.floorWarn ?? "Warn+" }, + { value: "error", label: labels?.floorError ?? "Errors" }, + ]; const { query, setQuery, floor, setFloor, tailing, setTailing, filtered, total } = - useLogcat(logs); + useLogcat(logs, initialQuery); const rowKeys = useMemo(() => rowKeysFor(filtered), [filtered]); const scrollRef = useRef(null); const pinnedRef = useRef(true); // is the view currently pinned at the top? @@ -190,7 +244,7 @@ export function ServiceLogcat({ return (
- Logcat + {labels?.title ?? "Logcat"}
{filtered.length === 0 ? ( -

No matching log entries.

+

{labels?.noMatchingEntries ?? "No matching log entries."}

) : (
{filtered.map((l, i) => { @@ -294,10 +348,12 @@ export function ServiceLogcat({
{open && (
- {l.req_body ? : null} - {l.res_body ? : null} + {l.req_body ? : null} + {l.res_body ? : null} {!l.req_body && !l.res_body ? ( -

No request or response body.

+

+ {labels?.noRequestResponseBody ?? "No request or response body."} +

) : null}
)} @@ -315,7 +371,7 @@ export function ServiceLogcat({ onClick={onLoadOlder} className="mt-1" > - Load older entries + {labels?.loadOlderEntries ?? "Load older entries"} )}
@@ -323,10 +379,9 @@ export function ServiceLogcat({
- {filtered.length === total ? `${total} entries` : `${filtered.length} of ${total}`} - {query && ` · “${query}”`} + {(labels?.entriesLabel ?? defaultEntriesLabel)(filtered.length, total, query)} - {tailing ? "tailing" : "paused"} + {tailing ? (labels?.tailingStatus ?? "tailing") : (labels?.pausedStatus ?? "paused")}
); diff --git a/src/components/ui/dev/service-logcat/useLogcat.ts b/src/components/ui/dev/service-logcat/useLogcat.ts index 4761c6df7..5531ef832 100644 --- a/src/components/ui/dev/service-logcat/useLogcat.ts +++ b/src/components/ui/dev/service-logcat/useLogcat.ts @@ -32,8 +32,8 @@ export interface UseLogcatResult { * first); the hook reverses them to newest-first so the console shows the * latest line at the top. */ -export function useLogcat(logs: LogEntry[]): UseLogcatResult { - const [query, setQuery] = useState(""); +export function useLogcat(logs: LogEntry[], initialQuery = ""): UseLogcatResult { + const [query, setQuery] = useState(initialQuery); const [floor, setFloor] = useState("all"); const [tailing, setTailing] = useState(true); diff --git a/src/components/ui/inputs/segmented-button/SegmentedButton.tsx b/src/components/ui/inputs/segmented-button/SegmentedButton.tsx index 23aea82c6..a71f499af 100644 --- a/src/components/ui/inputs/segmented-button/SegmentedButton.tsx +++ b/src/components/ui/inputs/segmented-button/SegmentedButton.tsx @@ -21,6 +21,9 @@ export interface SegmentedButtonOption { readonly icon?: string; /** Visual tone for the unselected state. Defaults to "default". */ readonly tone?: SegmentedButtonOptionTone; + /** Small red count badge anchored to the option's top-right corner (e.g. an + * open-items count). Omit for no badge. */ + readonly badge?: string | number; } export type SegmentedButtonSize = "sm" | "md"; @@ -139,7 +142,7 @@ export function SegmentedButton({ disabled={disabled} aria-pressed={value === opt.value} className={cn( - "font-medium transition-colors inline-flex items-center justify-center gap-1.5", + "relative font-medium transition-colors inline-flex items-center justify-center gap-1.5", isBoxed ? boxedSizeStyles[size] : sizeStyles[size], isBoxed ? boxedButtonStyle(value === opt.value, inverse) @@ -151,6 +154,14 @@ export function SegmentedButton({ > {opt.icon && } {opt.label} + {opt.badge != null && ( + + )} ))} From 3d9063bcec48fe4105367b44c23463a86c5f4285 Mon Sep 17 00:00:00 2001 From: Sheng Kun Chang Date: Sat, 4 Jul 2026 06:39:55 +0800 Subject: [PATCH 2/2] docs(service-logcat,segmented-button): add stories for initialQuery, labels, badge Co-Authored-By: Claude Sonnet 5 --- .../service-logcat/ServiceLogcat.stories.tsx | 36 +++++++++++++++++++ .../SegmentedButton.stories.tsx | 12 +++++++ 2 files changed, 48 insertions(+) diff --git a/src/components/ui/dev/service-logcat/ServiceLogcat.stories.tsx b/src/components/ui/dev/service-logcat/ServiceLogcat.stories.tsx index 15484caf9..b3b828d6c 100644 --- a/src/components/ui/dev/service-logcat/ServiceLogcat.stories.tsx +++ b/src/components/ui/dev/service-logcat/ServiceLogcat.stories.tsx @@ -67,6 +67,42 @@ export const Empty: Story = { args: { logs: [] }, }; +/** Seeds the text filter box on mount — e.g. landing pre-filtered from a deep link. */ +export const WithInitialQuery: Story = { + args: { logs: SAMPLE_LOGS, initialQuery: "users" }, +}; + +/** Every user-facing string overridden — e.g. for a non-English locale. */ +export const Localized: Story = { + args: { + logs: SAMPLE_LOGS, + labels: { + title: "日誌", + live: "即時", + paused: "已暫停", + floorAll: "全部", + floorWarn: "警告以上", + floorError: "錯誤", + filterAriaLabel: "依層級篩選", + filterPlaceholder: "篩選紀錄", + clearFilterAriaLabel: "清除篩選", + copyAriaLabel: "複製紀錄", + noMatchingEntries: "沒有符合的紀錄。", + request: "請求", + response: "回應", + noRequestResponseBody: "沒有請求或回應內容。", + loadOlderEntries: "載入較舊的紀錄", + entriesLabel: (filteredCount, total, query) => { + const base = + filteredCount === total ? `共 ${total} 筆` : `${total} 筆中的 ${filteredCount} 筆`; + return query ? `${base} · "${query}"` : base; + }, + tailingStatus: "即時追蹤中", + pausedStatus: "已暫停", + }, + }, +}; + // Older pages are generated on demand, seq-keyed, and appended at the OLD end // (the bottom). Scroll near the bottom or click the row to load a page. const PAGE_SIZE = 20; diff --git a/src/components/ui/inputs/segmented-button/SegmentedButton.stories.tsx b/src/components/ui/inputs/segmented-button/SegmentedButton.stories.tsx index a2b86fe95..1ac125490 100644 --- a/src/components/ui/inputs/segmented-button/SegmentedButton.stories.tsx +++ b/src/components/ui/inputs/segmented-button/SegmentedButton.stories.tsx @@ -82,6 +82,18 @@ export const Disabled: Story = { }, }; +/** A small red count badge anchored to an option's corner (e.g. an open-items count) */ +export const WithBadge: Story = { + args: { + options: [ + { value: "list", label: "List" }, + { value: "issues", label: "Issues", badge: 4 }, + ], + value: "list", + "aria-label": "Select view", + }, +}; + /** Boxed/connected track with an inset selected pill */ export const Boxed: Story = { render: (args) => {