From 9eab0c6f17cfdd26aa3c428cf8bcf454742bc98f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 02:01:18 +0000 Subject: [PATCH 01/12] feat(app-shell): Studio item header keeps the item name readable The title keeps its first twelve characters readable; on a narrower row the identity chips and the item tools fold into the item's more menu (the pillar's own, or one the header draws), so the menu trigger stays inside the column. The pillar rails draw the customization mark as its glyph alone. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01QsR2jUYXmZ6DQBWc6piyyF --- .../src/views/item-tools/StudioItemTools.tsx | 75 ++++-- .../views/studio-design/CustomizationMark.tsx | 33 +++ .../views/studio-design/FlowItemMoreMenu.tsx | 3 + .../studio-design/StudioDesignSurface.tsx | 20 +- .../views/studio-design/StudioItemHeader.tsx | 250 +++++++++++++++++- .../StudioRailCustomizations.tsx | 7 +- 6 files changed, 358 insertions(+), 30 deletions(-) diff --git a/ui/packages/app-shell/src/views/item-tools/StudioItemTools.tsx b/ui/packages/app-shell/src/views/item-tools/StudioItemTools.tsx index 001639c3984..d1cca5cfe41 100644 --- a/ui/packages/app-shell/src/views/item-tools/StudioItemTools.tsx +++ b/ui/packages/app-shell/src/views/item-tools/StudioItemTools.tsx @@ -22,6 +22,10 @@ * (`source.onChange`) and the item is not read-only, so an edit there is * saved by the pillar's autosave like any other. Without a buffer it shows * the item's effective body, read-only. + * + * objectstack#29693 — when the item header folds (the title would drop below + * its readable width), the five buttons leave the row and the same tools read + * as items of the header's ⋯ menu, opening the same sheets. */ import * as React from 'react'; @@ -44,6 +48,7 @@ import { ReferencesPanel, useReferencesScan } from './ReferencesPanel.js'; import { HistoryPanel } from './ResourceHistoryPage.js'; import { AuditPanel } from './AuditPanel.js'; import { JsonSourceEditor } from './JsonSourceEditor.js'; +import { useStudioItemHeaderFold, type StudioFoldedTool } from '../studio-design/StudioItemHeader.js'; /** The tool a sheet holds. */ export type StudioItemTool = 'layers' | 'references' | 'history' | 'audit' | 'source'; @@ -123,6 +128,31 @@ function StudioItemToolsFor({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [open]); + // objectstack#29693 — the tools' menu form, handed to the header's fold. + const fold = useStudioItemHeaderFold(); + const setFoldedTools = fold?.setTools; + const refsCount = refs.state.status === 'loaded' ? refs.state.items.length : undefined; + React.useEffect(() => { + if (!setFoldedTools) return; + const tool = (id: StudioItemTool, label: string, icon: React.ElementType, count?: number): StudioFoldedTool => ({ + id, + label, + icon, + count, + onSelect: () => setOpen(id), + }); + setFoldedTools([ + tool('layers', t('engine.edit.layers', locale), Layers3), + tool('references', t('engine.edit.references', locale), Link2, refsCount), + ...(canReadTrail + ? [tool('history', t('engine.edit.history', locale), History), tool('audit', t('engine.edit.auditTab', locale), ShieldCheck)] + : []), + tool('source', t('engine.edit.inspector.source', locale), FileCode2), + ]); + return () => setFoldedTools(null); + }, [setFoldedTools, canReadTrail, refsCount, locale]); + const folded = fold?.folded ?? false; + const canRollback = !readOnly && canAuthor; const sourceEditable = !readOnly && !!source?.onChange; const close = (o: boolean) => { @@ -181,27 +211,30 @@ function StudioItemToolsFor({ return ( <> -
- {toolButton('layers', t('engine.edit.layers', locale), Layers3)} - {toolButton( - 'references', - t('engine.edit.references', locale), - Link2, - refs.state.status === 'loaded' && refs.state.items.length > 0 ? ( - - {refs.state.items.length} - - ) : undefined, - )} - {canReadTrail && toolButton('history', t('engine.edit.history', locale), History)} - {canReadTrail && toolButton('audit', t('engine.edit.auditTab', locale), ShieldCheck)} - {toolButton('source', t('engine.edit.inspector.source', locale), FileCode2)} -
+ {!folded && ( +
+ {toolButton('layers', t('engine.edit.layers', locale), Layers3)} + {toolButton( + 'references', + t('engine.edit.references', locale), + Link2, + refs.state.status === 'loaded' && refs.state.items.length > 0 ? ( + + {refs.state.items.length} + + ) : undefined, + )} + {canReadTrail && toolButton('history', t('engine.edit.history', locale), History)} + {canReadTrail && toolButton('audit', t('engine.edit.auditTab', locale), ShieldCheck)} + {toolButton('source', t('engine.edit.inspector.source', locale), FileCode2)} +
+ )} {sheet('layers', t('engine.edit.layers', locale),
{layersBody}
)} diff --git a/ui/packages/app-shell/src/views/studio-design/CustomizationMark.tsx b/ui/packages/app-shell/src/views/studio-design/CustomizationMark.tsx index de7f2baede2..81a44c74c20 100644 --- a/ui/packages/app-shell/src/views/studio-design/CustomizationMark.tsx +++ b/ui/packages/app-shell/src/views/studio-design/CustomizationMark.tsx @@ -15,6 +15,7 @@ */ import * as React from 'react'; +import { Layers3, PencilRuler, Plus } from 'lucide-react'; import { cn } from '@object-ui/components'; import { customizationPackageId } from '@objectstack/spec/kernel'; import { t, tFormat } from '../metadata-admin/i18n.js'; @@ -35,12 +36,26 @@ const KIND_CLASS: Record = { customizationOf: 'bg-muted text-muted-foreground', }; +/** objectstack#29693 — the glyph the compact mark draws in place of its text. */ +const KIND_ICON: Record = { + customized: PencilRuler, + added: Plus, + customizationOf: Layers3, +}; + +/** + * objectstack#29693 — `compact` (the pillar rails): the mark is its glyph + * alone, so it never takes a row's name ("已定制(ObjectStack Showcase)" left a + * selected flow row no room for its label). Its text is its accessible name + * and leads its tooltip, before the sentence naming the pair. + */ export function CustomizationMark({ kind, baseName, pairId, locale, className, + compact = false, }: { kind: CustomizationMarkChipKind; /** The base's display name ("customized (HotCRM)"); needed by `customized` and `customizationOf`. */ @@ -49,6 +64,8 @@ export function CustomizationMark({ pairId?: string; locale: string; className?: string; + /** Draw the glyph alone, the text in its accessible name and tooltip. */ + compact?: boolean; }): React.ReactElement { const base = baseName ?? ''; const label = @@ -58,6 +75,22 @@ export function CustomizationMark({ const title = pairId ? tFormat(`engine.customizationMark.${kind}Title`, locale, { baseName: base, pair: pairId }) : undefined; + if (compact) { + const Icon = KIND_ICON[kind]; + return ( + + + ); + } return ( + {/* objectstack#29693 — what the flow header's title row folded away, on a narrow row. */} + {newSimilar && ( - + + {/* objectstack#29693 — what the title row folded away, on a narrow row. */} + setDefinitionOpen(true)}>