Skip to content

Commit 696a80c

Browse files
lukemeliaclaude
andcommitted
Add preview pane for non-module files in Module Inspector
When a non-module file (e.g., .png, .css, .json non-card) is selected in CodeMode, the Module Inspector now renders a FilePreviewPanel showing the file's FileDef instance with a format toggle (isolated/embedded/fitted/atom) instead of the "No tools available" message. Make it a template-only component Unify CardRendererPanel and FilePreviewPanel into PreviewPanel Merge the separate FilePreviewPanel into a single PreviewPanel component that handles both card instances and file defs. The component now accepts BaseDef instead of CardDef, guards card-specific features (edit, menu items, open in interact mode) behind isCardInstance checks, and filters out the edit format for non-card files. Enrich existing code-submode tests to verify that file previews (FileDef) don't show edit format or edit button, while card previews do. - Change fileDefResource and fileDefInstance from BaseDef to FileDef for better type safety since store.get with 'file-meta' returns FileDef. - Add fileDefError getter and show a meaningful error message in the incompatibility fallback when the file preview fails to load. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 38d8042 commit 696a80c

File tree

5 files changed

+170
-39
lines changed

5 files changed

+170
-39
lines changed

packages/host/app/components/operator-mode/code-submode/module-inspector.gts

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import window from 'ember-window-mock';
1919

2020
import { TrackedObject } from 'tracked-built-ins';
2121

22+
import { LoadingIndicator } from '@cardstack/boxel-ui/components';
2223
import { eq } from '@cardstack/boxel-ui/helpers';
2324

2425
import type {
@@ -33,6 +34,7 @@ import {
3334
type CardResourceMeta,
3435
isFieldDef,
3536
isSpecCard,
37+
isCardErrorJSONAPI,
3638
internalKeyFor,
3739
GetCardsContextName,
3840
GetCardContextName,
@@ -45,7 +47,6 @@ import {
4547

4648
import CreateSpecCommand from '@cardstack/host/commands/create-specs';
4749
import CardError from '@cardstack/host/components/operator-mode/card-error';
48-
import CardRendererPanel from '@cardstack/host/components/operator-mode/card-renderer-panel/index';
4950
import Playground from '@cardstack/host/components/operator-mode/code-submode/playground/playground';
5051

5152
import SchemaEditor from '@cardstack/host/components/operator-mode/code-submode/schema-editor';
@@ -54,6 +55,7 @@ import SpecPreview from '@cardstack/host/components/operator-mode/code-submode/s
5455
import SpecPreviewBadge from '@cardstack/host/components/operator-mode/code-submode/spec-preview-badge';
5556

5657
import ToggleButton from '@cardstack/host/components/operator-mode/code-submode/toggle-button';
58+
import PreviewPanel from '@cardstack/host/components/operator-mode/preview-panel/index';
5759
import SyntaxErrorDisplay from '@cardstack/host/components/operator-mode/syntax-error-display';
5860
import consumeContext from '@cardstack/host/helpers/consume-context';
5961

@@ -158,6 +160,53 @@ export default class ModuleInspector extends Component<ModuleInspectorSignature>
158160
return state;
159161
});
160162

163+
@use private fileDefResource = resource(() => {
164+
let state = new TrackedObject<{
165+
value: FileDef | undefined;
166+
isLoading: boolean;
167+
error: unknown;
168+
}>({
169+
value: undefined,
170+
isLoading: false,
171+
error: undefined,
172+
});
173+
if (!this.args.isIncompatibleFile || !this.args.readyFile?.url) {
174+
return state;
175+
}
176+
let fileUrl = this.args.readyFile.url;
177+
state.isLoading = true;
178+
(async () => {
179+
try {
180+
let result = await this.store.get(fileUrl, { type: 'file-meta' });
181+
if (isCardErrorJSONAPI(result)) {
182+
state.error = result;
183+
state.value = undefined;
184+
} else {
185+
state.value = result as unknown as FileDef;
186+
state.error = undefined;
187+
}
188+
} catch (e) {
189+
state.error = e;
190+
state.value = undefined;
191+
} finally {
192+
state.isLoading = false;
193+
}
194+
})();
195+
return state;
196+
});
197+
198+
private get fileDefInstance(): FileDef | undefined {
199+
return this.fileDefResource?.value;
200+
}
201+
202+
private get fileDefError(): boolean {
203+
return this.fileDefResource?.error != null;
204+
}
205+
206+
private get isFileDefLoading(): boolean {
207+
return this.fileDefResource?.isLoading ?? false;
208+
}
209+
161210
private get selectedDeclarationIsSpec() {
162211
return this.isSpecResource?.value ?? false;
163212
}
@@ -261,6 +310,9 @@ export default class ModuleInspector extends Component<ModuleInspectorSignature>
261310

262311
private get fileIncompatibilityMessage() {
263312
if (this.args.isIncompatibleFile) {
313+
if (this.fileDefError) {
314+
return `Unable to load file preview. Choose a file representing a card instance or module.`;
315+
}
264316
return `No tools are available to be used with this file type. Choose a file representing a card instance or module.`;
265317
}
266318
return null;
@@ -495,6 +547,16 @@ export default class ModuleInspector extends Component<ModuleInspectorSignature>
495547
'File is empty - tools like schema inspector, and file preview, are unavailable.'
496548
}}
497549
</div>
550+
{{else if this.fileDefInstance}}
551+
<PreviewPanel
552+
@card={{this.fileDefInstance}}
553+
@format={{@previewFormat}}
554+
@setFormat={{@setPreviewFormat}}
555+
/>
556+
{{else if this.isFileDefLoading}}
557+
<div class='file-loading' data-test-file-preview-loading>
558+
<LoadingIndicator />
559+
</div>
498560
{{else if this.fileIncompatibilityMessage}}
499561
<div
500562
class='file-incompatible-message'
@@ -602,7 +664,7 @@ export default class ModuleInspector extends Component<ModuleInspectorSignature>
602664
/>
603665
</section>
604666
{{else if @card}}
605-
<CardRendererPanel
667+
<PreviewPanel
606668
@card={{@card}}
607669
@format={{@previewFormat}}
608670
@setFormat={{@setPreviewFormat}}
@@ -676,6 +738,14 @@ export default class ModuleInspector extends Component<ModuleInspectorSignature>
676738
padding: var(--boxel-sp-xl);
677739
}
678740
741+
.file-loading {
742+
display: flex;
743+
align-items: center;
744+
justify-content: center;
745+
height: 100%;
746+
background-color: var(--boxel-200);
747+
}
748+
679749
.non-preview-panel-content {
680750
padding: var(--boxel-sp-xs);
681751
background-color: var(--code-mode-panel-background-color);

packages/host/app/components/operator-mode/code-submode/playground/playground-preview.gts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
} from '@cardstack/runtime-common';
1212

1313
import CardRenderer from '@cardstack/host/components/card-renderer';
14-
import FittedFormatGallery from '@cardstack/host/components/operator-mode/card-renderer-panel/fitted-format-gallery';
14+
import FittedFormatGallery from '@cardstack/host/components/operator-mode/preview-panel/fitted-format-gallery';
1515
import type { EnhancedRealmInfo } from '@cardstack/host/services/realm';
1616

1717
import type {

packages/host/app/components/operator-mode/card-renderer-panel/fitted-format-gallery.gts renamed to packages/host/app/components/operator-mode/preview-panel/fitted-format-gallery.gts

File renamed without changes.

packages/host/app/components/operator-mode/card-renderer-panel/index.gts renamed to packages/host/app/components/operator-mode/preview-panel/index.gts

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@ import { Eye, IconCode } from '@cardstack/boxel-ui/icons';
2020
import {
2121
cardTypeDisplayName,
2222
cardTypeIcon,
23+
formats as allFormats,
2324
getMenuItems,
2425
identifyCard,
26+
isCardInstance,
2527
isResolvedCodeRef,
2628
} from '@cardstack/runtime-common';
2729

@@ -30,8 +32,6 @@ import { CardContextName } from '@cardstack/runtime-common';
3032
import CardRenderer from '@cardstack/host/components/card-renderer';
3133
import Overlays from '@cardstack/host/components/operator-mode/overlays';
3234

33-
import { urlForRealmLookup } from '@cardstack/host/lib/utils';
34-
3535
import ElementTracker, {
3636
type RenderedCardForOverlayActions,
3737
} from '@cardstack/host/resources/element-tracker';
@@ -41,6 +41,7 @@ import type OperatorModeStateService from '@cardstack/host/services/operator-mod
4141
import type RealmService from '@cardstack/host/services/realm';
4242

4343
import type {
44+
BaseDef,
4445
CardContext,
4546
CardDef,
4647
Format,
@@ -54,14 +55,14 @@ import FittedFormatGallery from './fitted-format-gallery';
5455
interface Signature {
5556
Element: HTMLElement;
5657
Args: {
57-
card: CardDef;
58+
card: BaseDef;
5859
format?: Format; // defaults to 'isolated'
5960
setFormat: (format: Format) => void;
6061
viewCard?: ViewCardFn;
6162
};
6263
}
6364

64-
export default class CardRendererPanel extends Component<Signature> {
65+
export default class PreviewPanel extends Component<Signature> {
6566
@consume(CardContextName) declare private cardContext: CardContext;
6667
@service declare private commandService: CommandService;
6768
@service declare private operatorModeStateService: OperatorModeStateService;
@@ -83,15 +84,23 @@ export default class CardRendererPanel extends Component<Signature> {
8384
return this.args.format ?? 'isolated';
8485
}
8586

87+
private get cardId(): string | undefined {
88+
return (this.args.card as CardDef).id;
89+
}
90+
91+
private get isCard(): boolean {
92+
return isCardInstance(this.args.card);
93+
}
94+
8695
private openInInteractMode = () => {
87-
this.operatorModeStateService.openCardInInteractMode(this.args.card.id);
96+
if (this.cardId) {
97+
this.operatorModeStateService.openCardInInteractMode(this.cardId);
98+
}
8899
};
89100

90101
private editTemplate = () => {
91-
// Get the card definition using identifyCard
92102
const type = identifyCard(this.args.card.constructor as any);
93103
if (type && isResolvedCodeRef(type)) {
94-
// Construct the GTS file URL
95104
const gtsFileUrl = type.module.endsWith('.gts')
96105
? type.module
97106
: `${type.module}.gts`;
@@ -100,20 +109,19 @@ export default class CardRendererPanel extends Component<Signature> {
100109
};
101110

102111
private get realmInfo() {
103-
let url = this.args.card ? urlForRealmLookup(this.args.card) : undefined;
104-
if (!url) {
112+
if (!this.cardId) {
105113
return undefined;
106114
}
107-
return this.realm.info(url);
115+
return this.realm.info(this.cardId);
108116
}
109117

110118
private get contextMenuItems() {
111-
if (!this.args.card) {
119+
if (!this.args.card || !(getMenuItems in this.args.card)) {
112120
return [];
113121
}
114122
return toMenuItems(
115-
this.args.card[getMenuItems]({
116-
canEdit: this.realm.canWrite(this.args.card.id),
123+
(this.args.card as CardDef)[getMenuItems]({
124+
canEdit: this.cardId ? this.realm.canWrite(this.cardId) : false,
117125
cardCrudFunctions: {},
118126
menuContext: 'code-mode-preview',
119127
commandContext: this.commandService.commandContext,
@@ -123,10 +131,27 @@ export default class CardRendererPanel extends Component<Signature> {
123131

124132
private get canEditCard() {
125133
return Boolean(
126-
this.format !== 'edit' && this.realm.canWrite(this.args.card.id),
134+
this.isCard &&
135+
this.format !== 'edit' &&
136+
this.cardId &&
137+
this.realm.canWrite(this.cardId),
127138
);
128139
}
129140

141+
private get cardTitle(): string | undefined {
142+
if (this.isCard) {
143+
return (this.args.card as CardDef).cardTitle;
144+
}
145+
return (this.args.card as any).name;
146+
}
147+
148+
private get availableFormats() {
149+
if (this.isCard) {
150+
return allFormats;
151+
}
152+
return allFormats.filter((f) => f !== 'edit');
153+
}
154+
130155
@provide(CardContextName)
131156
// @ts-ignore context is used via provider
132157
private get context(): CardContext {
@@ -191,7 +216,7 @@ export default class CardRendererPanel extends Component<Signature> {
191216
class='card-renderer-header'
192217
@cardTypeDisplayName={{cardTypeDisplayName @card}}
193218
@cardTypeIcon={{cardTypeIcon @card}}
194-
@cardTitle={{@card.cardTitle}}
219+
@cardTitle={{this.cardTitle}}
195220
@realmInfo={{this.realmInfo}}
196221
@onEdit={{if this.canEditCard (fn @setFormat 'edit')}}
197222
@onFinishEditing={{if
@@ -200,7 +225,7 @@ export default class CardRendererPanel extends Component<Signature> {
200225
}}
201226
@isTopCard={{true}}
202227
@moreOptionsMenuItems={{this.contextMenuItems}}
203-
data-test-code-mode-card-renderer-header={{@card.id}}
228+
data-test-code-mode-card-renderer-header={{this.cardId}}
204229
...attributes
205230
/>
206231
{{#if (eq this.format 'fitted')}}
@@ -223,7 +248,11 @@ export default class CardRendererPanel extends Component<Signature> {
223248
</div>
224249

225250
<div class='card-renderer-format-chooser'>
226-
<FormatChooser @format={{this.format}} @setFormat={{@setFormat}} />
251+
<FormatChooser
252+
@format={{this.format}}
253+
@setFormat={{@setFormat}}
254+
@formats={{this.availableFormats}}
255+
/>
227256
</div>
228257

229258
<style scoped>

0 commit comments

Comments
 (0)