diff --git a/app/boot-skeleton.css b/app/boot-skeleton.css
index 09908b5c4..5254a5240 100644
--- a/app/boot-skeleton.css
+++ b/app/boot-skeleton.css
@@ -95,8 +95,9 @@
/* mail */
/* List density (the density setting, data-boot-density): extra-compact rows
+ (boot-density-xc, the same variant contacts uses: drop one copy on merging)
have no avatar, compact and extra-compact ones no preview. */
-@custom-variant boot-xc (&:where([data-boot-density="extra-compact"] *));
+@custom-variant boot-density-xc (&:where([data-boot-density="extra-compact"] *));
@custom-variant boot-dense (&:where([data-boot-density="compact"] *, [data-boot-density="extra-compact"] *));
/* Message previews turned off (showPreview). */
@custom-variant boot-no-preview (&:where([data-boot-preview="off"] *));
@@ -104,3 +105,56 @@
@custom-variant boot-std (&:where(:root:not([data-interface-layout="mountain-view"]) *));
/* Mountain View's side panel shows only with calendar or contacts. */
@custom-variant boot-no-mv-side (&:where([data-boot-rail]:not([data-boot-rail~="calendar"]):not([data-boot-rail~="contacts"]) *));
+
+/* Message list rows (MailListRowsSkeleton), in short classes because a
+ skeleton draws dozens of them. Geometry as ThreadListItem's: a 1px rule,
+ the cell's density padding and gap, its minimum height on stacked rows.
+ In the components layer, so utilities on the same element still win. */
+@layer components {
+ .sk-mrow {
+ border-bottom: 1px solid var(--color-border);
+ background-color: var(--color-background);
+ }
+ .sk-mcell {
+ display: flex;
+ align-items: flex-start;
+ gap: var(--density-item-gap);
+ padding: var(--density-item-py) 0.75rem;
+ }
+ .sk-mcell-min {
+ min-height: var(--list-item-height);
+ }
+ /* The avatar's column: centred on the row. */
+ .sk-mav {
+ display: flex;
+ flex-shrink: 0;
+ align-self: stretch;
+ align-items: center;
+ }
+ /* A line of text as tall as the real one (font size and leading from its
+ classes), with its bar centred on it. */
+ .sk-ml {
+ position: relative;
+ display: block;
+ height: 1lh;
+ }
+ .sk-ml > .sk-shape {
+ position: absolute;
+ inset-block: 0;
+ inset-inline-start: 0;
+ height: 0.75rem;
+ margin-block: auto;
+ border-radius: 0.25rem;
+ }
+}
+
+/* Panels drawn in the shapes' own colour (the sidebar in dark themes, the
+ Mountain View phone header): lift the shapes off them. Below the contrast
+ rules' specificity, and not in forced colours. */
+@media not (forced-colors: active) {
+ :where(.dark) .sk-lift-dark .sk-shape,
+ :where([data-interface-layout="mountain-view"]) .sk-lift-mv .sk-shape,
+ .sk-lift .sk-shape {
+ background-color: color-mix(in oklab, var(--color-foreground) 10%, transparent);
+ }
+}
diff --git a/components/boot-skeleton/__tests__/mail-skeleton.test.tsx b/components/boot-skeleton/__tests__/mail-skeleton.test.tsx
index 27d59123b..784665b8c 100644
--- a/components/boot-skeleton/__tests__/mail-skeleton.test.tsx
+++ b/components/boot-skeleton/__tests__/mail-skeleton.test.tsx
@@ -47,7 +47,7 @@ describe('mail skeleton', () => {
it('draws every shape for the boot page and lets the boot attributes pick', () => {
const rows = html();
- expect(rows.querySelector('.boot-xc\\:hidden')).not.toBeNull();
+ expect(rows.querySelector('.boot-density-xc\\:hidden')).not.toBeNull();
expect(rows.querySelector('.boot-dense\\:hidden')).not.toBeNull();
expect(rows.querySelector('.md\\:mv\\:block')).not.toBeNull();
});
diff --git a/components/boot-skeleton/mail-skeleton.tsx b/components/boot-skeleton/mail-skeleton.tsx
index 9d270b954..ae430177e 100644
--- a/components/boot-skeleton/mail-skeleton.tsx
+++ b/components/boot-skeleton/mail-skeleton.tsx
@@ -32,7 +32,6 @@ import {
Search,
Settings,
Square,
- Star,
Users,
} from "@/components/icons";
import { cn } from "@/lib/cn";
@@ -75,7 +74,7 @@ function TextLine({ className, w, bar = "h-3" }: { className?: string; w: string
export function FolderRowsSkeleton({ count = 7 }: { count?: number }) {
return (
// On the sidebar's panel colour, which dark themes share with the shapes.
-
+
{repeat(count, (i) => (
@@ -172,7 +171,7 @@ interface RowOptions {
/** Classes for the avatar, or null where the density has none. */
function avatarClass(o: RowOptions): string | null {
- if (o.density === undefined) return "boot-xc:hidden";
+ if (o.density === undefined) return "boot-density-xc:hidden";
return o.density === "extra-compact" ? null : "";
}
@@ -182,37 +181,49 @@ function previewClass(o: RowOptions): string | null {
return o.preview === false || o.density === "compact" || o.density === "extra-compact" ? null : "";
}
+/*
+ * The rows below are drawn dozens of times, so they use the short classes in
+ * app/boot-skeleton.css (mail section) for what every row repeats: sk-mrow
+ * (the row and its rule), sk-mcell (the cell: the density's padding and gap,
+ * and its minimum height), sk-mav (the avatar's column) and sk-ml (a line of
+ * text, with its bar).
+ */
+
+/** One line of row text, as tall as the real line; `w` is the bar's width. */
+function L({ className, w }: { className?: string; w: string }) {
+ return (
+
+
+
+ );
+}
+
/** A stacked row (ThreadListItem on phones and in the split list). */
function StackedRow({ i, o }: { i: number; o: RowOptions }) {
const avatar = avatarClass(o);
const preview = previewClass(o);
return (
-
-
-
- {avatar !== null && (
-
-
+
+
+ {avatar !== null && (
+
+
+
+ )}
+
+
+
+
+
+
+ {preview !== null && (
+
+ {/* Mountain View's phone rows keep the preview to one line, beside a 32px star. */}
+
+
)}
-
-
-
-
-
-
- {preview !== null && (
-
-
- {/* Mountain View's phone rows keep the preview to one line. */}
-
-
- )}
-
-
+
);
@@ -222,25 +233,18 @@ function StackedRow({ i, o }: { i: number; o: RowOptions }) {
function OneLineRow({ i, o }: { i: number; o: RowOptions }) {
const avatar = avatarClass(o);
return (
-
-
-
- {avatar !== null && (
-
-
-
- )}
-
-
-
-
-
-
+
+
+ {avatar !== null && (
+
+
-
+ )}
+
+
+
+
+
);
@@ -249,29 +253,18 @@ function OneLineRow({ i, o }: { i: number; o: RowOptions }) {
/** Mountain View's row on a tablet or desktop: check box and star, then one 40px line. */
function MvRow({ i }: { i: number }) {
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+ {/* The check box and the star, 26px each (an 18px icon in p-1). */}
+
+
+
+
+
+
+
+
+
);
@@ -375,7 +368,7 @@ function MobileHeaderSkeleton() {
"border-b border-border bg-background px-4",
"mv:mx-2 mv:mt-2 mv:mb-1 mv:rounded-full mv:border-0 mv:bg-muted mv:px-2 mv:shadow-sm",
// Mountain View's pill is the shapes' own colour: lift them off it.
- "mv:[&_.sk-shape]:bg-foreground/10",
+ "sk-lift-mv",
)}
>
@@ -384,7 +377,7 @@ function MobileHeaderSkeleton() {
-
+
diff --git a/components/boot-skeleton/shell.tsx b/components/boot-skeleton/shell.tsx
index 1b8e0b1de..0c2369253 100644
--- a/components/boot-skeleton/shell.tsx
+++ b/components/boot-skeleton/shell.tsx
@@ -182,10 +182,6 @@ export function MvTopBarSkeleton({ search = true, className }: { search?: boolea
- {/* The account switcher. */}
-
-
-
);