Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 0 additions & 62 deletions packages/host/app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,71 +32,9 @@
<meta data-boxel-head-start />
<meta data-boxel-head-end />

<style scoped>
#host-loading {
background-color: #686283;
display: flex;
align-items: center;
justify-items: center;
height: 100vh;
}

.loading-container {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}

@keyframes spin {
to {
transform: rotate(360deg);
}
}

@media (prefers-reduced-motion: no-preference) {
.loading-indicator svg {
width: 20px;
height: 20px;
animation: spin 6s linear infinite;
}
}

.loading-text {
color: #fff;
font-size: 12px;
font-weight: 600;
}
</style>
</head>
<body>
<script type="x/boundary" id="boxel-isolated-start"></script>
<div id="host-loading">
<div class="loading-container">
<div class="loading-indicator">
<svg
xmlns="http://www.w3.org/2000/svg"
width="15.5"
height="15.5"
viewBox="0 0 15.5 15.5"
>
<g
fill="none"
stroke="#00FFBA"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
>
<path
d="M7.75.75v2.8M7.75 11.95v2.8M2.801 2.801l1.981 1.981M10.718 10.718l1.981 1.981M.75 7.75h2.8M11.95 7.75h2.8M2.801 12.699l1.981-1.981M10.718 4.782l1.981-1.981"
/>
</g>
</svg>
</div>
<div class="loading-text">Loading…</div>
</div>
</div>
<script type="x/boundary" id="boxel-isolated-end"></script>

<!-- in case embercli's hooks insn't run,
Expand Down
12 changes: 12 additions & 0 deletions packages/host/app/routes/index.gts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { action } from '@ember/object';
import Route from '@ember/routing/route';
import type RouterService from '@ember/routing/router-service';
import type Transition from '@ember/routing/transition';
Expand Down Expand Up @@ -55,6 +56,17 @@ export default class Card extends Route {
@service declare realmServer: RealmServerService;

didMatrixServiceStart = false;
initialLoading = true;

@action
loading(transition: Transition) {
transition.finally(() => {
// The loading template will be shown only during the initial load of the app
this.initialLoading = false;
});

return this.initialLoading;
Comment on lines +61 to +68

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Ensure loading banner can't reappear after fast boot

The initialLoading flag is only flipped to false from the loading action, which only runs when a transition actually enters the loading substate. If the initial app boot completes quickly (no loading substate), initialLoading never changes and later slow transitions will return true here, showing the global loading template even though the app is already running. That contradicts the intent in the comment and can reintroduce the “spinner covering errors” problem on later slow transitions.

Useful? React with 👍 / 👎.

}

// WARNING! Make sure we are _very_ careful with our async in this model. This
// model hook is called _every_ time
Expand Down
46 changes: 46 additions & 0 deletions packages/host/app/templates/loading.gts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { TemplateOnlyComponent } from '@ember/component/template-only';

import RouteTemplate from 'ember-route-template';

import { LoadingIndicator } from '@cardstack/boxel-ui/components';

const Loading: TemplateOnlyComponent = <template>
<div id='host-loading' data-test-host-loading>
<div class='loading-container'>
<div class='loading-indicator'>
<LoadingIndicator @color='#00FFBA' />
</div>
<div class='loading-text'>Loading...</div>
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The loading text uses three periods (...) instead of the Unicode ellipsis character (…) that was used in the original index.html. For consistency with the original implementation and better typography, consider using the Unicode ellipsis character.

Suggested change
<div class='loading-text'>Loading...</div>
<div class='loading-text'>Loading</div>

Copilot uses AI. Check for mistakes.
</div>
</div>

<style scoped>
#host-loading {
background-color: #686283;
display: flex;
align-items: center;
justify-items: center;
height: 100vh;
}
.loading-container {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.loading-indicator {
--boxel-loading-indicator-size: 20px;
}
.loading-text {
color: #fff;
font-size: 12px;
font-weight: 600;
}
</style>
</template>;

export default RouteTemplate(Loading);
22 changes: 21 additions & 1 deletion packages/host/tests/acceptance/host-mode-test.gts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ import { getPageTitle } from 'ember-page-title/test-support';
import { module, test } from 'qunit';

import { baseRealm } from '@cardstack/runtime-common';
import { Deferred } from '@cardstack/runtime-common';

import HostModeService from '@cardstack/host/services/host-mode-service';
import type StoreService from '@cardstack/host/services/store';

import {
percySnapshot,
Expand Down Expand Up @@ -251,15 +253,33 @@ module('Acceptance | host mode tests', function (hooks) {
});

test('visiting a non-existent card shows an error', async function (assert) {
await visit('/test/Pet/non-existent.json');
let store = getService('store') as StoreService;
let originalGet = store.get.bind(store);
let gate = new Deferred<void>();
let targetId = `${testHostModeRealmURL}Pet/non-existent.json`;
store.get = async (id: string, ...rest: unknown[]) => {
if (id === targetId) {
await gate.promise;
}
return originalGet(id, ...(rest as []));
};

let visitPromise = visit('/test/Pet/non-existent.json');
await waitFor('[data-test-host-loading]');
assert.dom('[data-test-host-loading]').exists();
gate.fulfill();

await visitPromise;
assert
.dom('[data-test-error="not-found"]')
.hasText(`Card not found: ${testHostModeRealmURL}Pet/non-existent`);
assert.strictEqual(
getPageTitle(),
`Card not found: ${testHostModeRealmURL}Pet/non-existent`,
);
assert.dom('[data-test-host-loading]').doesNotExist();

store.get = originalGet;
});

test('invoking viewCard from a card stacks the linked card', async function (assert) {
Expand Down