diff --git a/.cursor/rules/skeleton-width-sync.mdc b/.cursor/rules/skeleton-width-sync.mdc new file mode 100644 index 000000000..a5be61969 --- /dev/null +++ b/.cursor/rules/skeleton-width-sync.mdc @@ -0,0 +1,31 @@ +--- +description: Keep skeleton placeholder widths in sync with English (en-US) copy +globs: "**/_locales/en-US.json" +alwaysApply: false +--- + +# Skeleton widths follow English copy + +en-US is the reference locale for placeholder sizing. When you add or change a +user-facing string here, update the paired `*Skeleton` for the component that +renders it so its width still roughly matches the English text. Skeleton +primitives (`TextSkeleton`, `HeadingSkeleton`) take width via the caller's +`className` (e.g. `w-16`, `w-24`, `w-80`, or `w-full`); they do not default to a +width, so the placeholder must carry one. + +Pairing convention: a component `Foo` has a sibling `FooSkeleton` that reuses the +same layout (see `contrib/claude/ui.md` and `contrib/claude/app-arborescence.md`). + +```jsonc +// en-US.json — label lengthened from "Subprocessors" +"subprocessors": "Sub-processors & vendors" +``` + +```tsx +// TopBarSkeleton.tsx — bump the matching nav bar so it still mirrors the label +{ key: "subprocessors", width: "w-40" }, // was w-24 +``` + +This is a best-effort visual convention, not a mechanically enforced link: match +the placeholder width to the English string length so loading states stay +harmonious. Other locales do not drive skeleton sizing. diff --git a/apps/compliance-portal/src/components/Hero/HeroSkeleton.tsx b/apps/compliance-portal/src/components/Hero/HeroSkeleton.tsx index 5feb14088..673024184 100644 --- a/apps/compliance-portal/src/components/Hero/HeroSkeleton.tsx +++ b/apps/compliance-portal/src/components/Hero/HeroSkeleton.tsx @@ -19,7 +19,13 @@ import { HeaderBand } from "#/components/HeaderBand/HeaderBand"; import { hero, organizationContactInfo } from "./variants"; -const CONTACT_ITEM_KEYS = ["website", "email", "location"] as const; +// Width per contact item, roughly sized to its typical content (hostname / +// email / address). See .cursor/rules/skeleton-width-sync.mdc. +const CONTACT_ITEMS = [ + { key: "website", width: "w-28" }, + { key: "email", width: "w-40" }, + { key: "location", width: "w-36" }, +] as const; // Loading placeholder paired with Hero: reuses the same layout slots with // skeleton primitives. Imports no Relay, so it renders instantly. @@ -31,13 +37,13 @@ export function HeroSkeleton() {
- +
- {CONTACT_ITEM_KEYS.map(key => ( -
- + {CONTACT_ITEMS.map(contact => ( +
+
))}
diff --git a/apps/compliance-portal/src/components/TopBar/TopBarSkeleton.tsx b/apps/compliance-portal/src/components/TopBar/TopBarSkeleton.tsx index 88470af70..2b1552ba6 100644 --- a/apps/compliance-portal/src/components/TopBar/TopBarSkeleton.tsx +++ b/apps/compliance-portal/src/components/TopBar/TopBarSkeleton.tsx @@ -18,7 +18,14 @@ import { TextSkeleton } from "@probo/ui/src/v2/typography/TextSkeleton"; import { topBar } from "./variants"; -const NAV_ITEM_KEYS = ["documents", "subprocessors", "updates", "requests"] as const; +// Width per nav item, sized to its English label so the placeholder row reads +// like the real nav. See .cursor/rules/skeleton-width-sync.mdc. +const NAV_ITEMS = [ + { key: "documents", width: "w-16" }, + { key: "subprocessors", width: "w-24" }, + { key: "updates", width: "w-12" }, + { key: "requests", width: "w-14" }, +] as const; // Loading placeholder paired with TopBar: reuses the same layout slots with // skeleton primitives. Imports no Relay / Base UI, so it renders instantly. @@ -30,14 +37,14 @@ export function TopBarSkeleton() {
- +
diff --git a/packages/ui/src/v2/typography/HeadingSkeleton.tsx b/packages/ui/src/v2/typography/HeadingSkeleton.tsx index 49d44d4d1..8917213fb 100644 --- a/packages/ui/src/v2/typography/HeadingSkeleton.tsx +++ b/packages/ui/src/v2/typography/HeadingSkeleton.tsx @@ -20,7 +20,8 @@ import { headingSkeleton } from "./variants"; export type HeadingSkeletonProps = Omit, "children"> & VariantProps; // Loading placeholder paired with Heading: a pulse block matching a heading -// line at the given size. Defaults to full width; constrain with a width class. +// line at the given size. Supply a width class (e.g. w-80, or w-full for a full +// line); with none the block collapses to its content. export function HeadingSkeleton(props: HeadingSkeletonProps) { const { size, className, ...rest } = props; diff --git a/packages/ui/src/v2/typography/Text.stories.tsx b/packages/ui/src/v2/typography/Text.stories.tsx index edb365506..35aa98035 100644 --- a/packages/ui/src/v2/typography/Text.stories.tsx +++ b/packages/ui/src/v2/typography/Text.stories.tsx @@ -104,7 +104,7 @@ export const Skeleton: Story = { render: () => (
- +
), diff --git a/packages/ui/src/v2/typography/TextSkeleton.tsx b/packages/ui/src/v2/typography/TextSkeleton.tsx index 1dab13452..55d28ef1d 100644 --- a/packages/ui/src/v2/typography/TextSkeleton.tsx +++ b/packages/ui/src/v2/typography/TextSkeleton.tsx @@ -20,7 +20,8 @@ import { textSkeleton } from "./variants"; export type TextSkeletonProps = Omit, "children"> & VariantProps; // Loading placeholder paired with Text: a pulse block matching a line of text -// at the given size. Defaults to full width; constrain with a width class. +// at the given size. Supply a width class (e.g. w-24, or w-full for a full +// line); with none the block collapses to its content. export function TextSkeleton(props: TextSkeletonProps) { const { size, className, ...rest } = props; diff --git a/packages/ui/src/v2/typography/variants.ts b/packages/ui/src/v2/typography/variants.ts index ae5877973..225b5892f 100644 --- a/packages/ui/src/v2/typography/variants.ts +++ b/packages/ui/src/v2/typography/variants.ts @@ -97,7 +97,7 @@ export const heading = tv({ }, }); -const skeletonBase = "inline-block w-full animate-pulse select-none rounded-2 bg-sand-3 text-transparent"; +const skeletonBase = "inline-block animate-pulse select-none rounded-2 bg-sand-3 text-transparent"; export const textSkeleton = tv({ base: skeletonBase,