refactor(z-index): introduce a shared named z-index scale (#723)
* refactor(z-index): introduce a shared named z-index scale Replace scattered magic z-index numbers (100, 150, 200, 201, 250) with named layers (z-app-bar, z-app-bar-elevated, z-modal, z-menu-elevated) so intent is documented at each call site and future additions insert predictably instead of colliding. Defined via Tailwind's @utility directive in two places that mirror each other: packages/ui/src/index.css (ui:-prefixed, for components that live in the @cio/ui package) and apps/dashboard/src/app.css (unprefixed, for dashboard-only files) — a ui:-prefixed custom utility only compiles if it is also used somewhere under packages/ui/src, so dashboard-only usages need the unprefixed twin. Also fixes two pre-existing dropdown/select menus (tags-admin.svelte, mark-exercise-modal.svelte) that used ui:z-[250], which never actually compiled to any CSS rule since nothing under packages/ui/src used that exact class. Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * fix(ui): safelist the packaged z-index scale so it always compiles @utility only emits a class when Tailwind's scanner sees that exact candidate under packages/ui/src, so ui:z-app-bar-elevated and ui:z-menu-elevated were documented but silently missing from the compiled @cio/ui CSS since nothing inside the package itself used them. Safelist all four layers via @source inline() so the shipped scale always matches what's documented, regardless of which layers this package happens to reference internally. Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
This commit is contained in:
co-authored by
Rotimi Best
Cursor Agent
parent
c0f82d9573
commit
0836d9b5bb
@@ -8,6 +8,28 @@
|
||||
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
/**
|
||||
* Shared z-index scale, mirrored from `packages/ui/src/index.css`.
|
||||
*
|
||||
* Dashboard code has its own (unprefixed) Tailwind pipeline, separate from `@cio/ui`'s
|
||||
* `ui:`-prefixed one — a `ui:`-prefixed utility only compiles if that exact class string is
|
||||
* also used somewhere under `packages/ui/src`. So dashboard-only usages (page headers, the
|
||||
* course sidebar, dropdowns/selects that need to escape a sticky header or a modal) must use
|
||||
* these unprefixed names instead. Keep the two scales numerically in sync; see
|
||||
* `packages/ui/README.md` for the full write-up of each layer.
|
||||
*/
|
||||
@utility z-app-bar {
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
@utility z-app-bar-elevated {
|
||||
z-index: 150;
|
||||
}
|
||||
|
||||
@utility z-menu-elevated {
|
||||
z-index: 250;
|
||||
}
|
||||
|
||||
/* Geist variable font — self-hosted */
|
||||
@font-face {
|
||||
font-family: 'Geist';
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</script>
|
||||
|
||||
<header
|
||||
class="border-border ui:bg-background sticky top-0 z-100 flex h-12 w-full shrink-0 items-center gap-2 border-b backdrop-blur transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-8"
|
||||
class="border-border ui:bg-background z-app-bar sticky top-0 flex h-12 w-full shrink-0 items-center gap-2 border-b backdrop-blur transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-8"
|
||||
>
|
||||
<div class="flex w-full items-center gap-2 px-4">
|
||||
<Sidebar.Trigger />
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
</script>
|
||||
|
||||
<header
|
||||
class="border-border ui:bg-background sticky top-0 z-100 flex h-12 w-full shrink-0 items-center gap-2 border-b backdrop-blur transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-8"
|
||||
class="border-border ui:bg-background z-app-bar sticky top-0 flex h-12 w-full shrink-0 items-center gap-2 border-b backdrop-blur transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-8"
|
||||
>
|
||||
<div class="flex w-full items-center gap-2 px-4">
|
||||
<Sidebar.Trigger />
|
||||
|
||||
+2
-2
@@ -296,7 +296,7 @@
|
||||
>
|
||||
<EllipsisVerticalIcon class="h-5 w-5" />
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end" class="ui:z-[250]">
|
||||
<DropdownMenu.Content align="end" class="z-menu-elevated">
|
||||
<DropdownMenu.Item
|
||||
class="text-red-600"
|
||||
onclick={() => {
|
||||
@@ -366,7 +366,7 @@
|
||||
<Select.Trigger class="w-full">
|
||||
{status.text}
|
||||
</Select.Trigger>
|
||||
<Select.Content class="ui:z-[250]">
|
||||
<Select.Content class="z-menu-elevated">
|
||||
{#each SELECTABLE_STATUS as statusOption}
|
||||
<Select.Item value={statusOption.id.toString()}>
|
||||
{statusOption.text}
|
||||
|
||||
@@ -112,7 +112,7 @@
|
||||
{#if !isOrgLoaded}
|
||||
<SidebarSkeleton />
|
||||
{:else}
|
||||
<Sidebar.Root collapsible="icon" class="ui:z-150!" mobileOverlayClass="ui:z-150!">
|
||||
<Sidebar.Root collapsible="icon" class="z-app-bar-elevated!" mobileOverlayClass="z-app-bar-elevated!">
|
||||
<Sidebar.Header>
|
||||
<CourseSidebarLogo />
|
||||
</Sidebar.Header>
|
||||
|
||||
@@ -405,7 +405,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Page.Header isSticky={true} class="top-12! z-100! min-h-[36px]">
|
||||
<Page.Header isSticky={true} class="z-app-bar! top-12! min-h-[36px]">
|
||||
<Page.HeaderContent>
|
||||
<Page.Title class="flex flex-col gap-2">
|
||||
<span>{exerciseDisplayTitle}</span>
|
||||
@@ -451,7 +451,7 @@
|
||||
</Button>
|
||||
{/snippet}
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end" class="z-201!">
|
||||
<DropdownMenu.Content align="end" class="z-menu-elevated!">
|
||||
<DropdownMenu.Item onclick={addSectionFromHeader}>
|
||||
{$t('course.navItem.lessons.exercises.all_exercises.add_section')}
|
||||
</DropdownMenu.Item>
|
||||
@@ -490,7 +490,7 @@
|
||||
</Button>
|
||||
{/snippet}
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end" class="z-201!">
|
||||
<DropdownMenu.Content align="end" class="z-menu-elevated!">
|
||||
{#if hasSections($questionnaire.sections)}
|
||||
<DropdownMenu.Item onclick={() => (reorderQuestions = !reorderQuestions)}>
|
||||
{reorderQuestions
|
||||
|
||||
@@ -148,7 +148,7 @@
|
||||
<div class="hidden shrink-0 md:block md:w-(--side-panel-width)"></div>
|
||||
|
||||
<aside
|
||||
class="ui:bg-background fixed inset-y-0 right-0 z-100 flex h-screen w-full flex-col border-l md:w-(--side-panel-width)"
|
||||
class="ui:bg-background z-app-bar fixed inset-y-0 right-0 flex h-screen w-full flex-col border-l md:w-(--side-panel-width)"
|
||||
aria-label={t.get(def.titleKey)}
|
||||
>
|
||||
<button
|
||||
|
||||
@@ -383,7 +383,7 @@
|
||||
: $t('tags_admin.tag_modal.select_category')}
|
||||
</p>
|
||||
</Select.Trigger>
|
||||
<Select.Content class="ui:z-[250]">
|
||||
<Select.Content class="z-menu-elevated">
|
||||
{#each tagApi.tagGroups as group (group.id)}
|
||||
<Select.Item value={group.id}>{group.name}</Select.Item>
|
||||
{/each}
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
// dialog's own bottom padding when a sticky footer is rendered —
|
||||
// the footer's own `py-4` provides the bottom spacing instead, and
|
||||
// it pins flush with the dialog's bottom edge.
|
||||
'ui:bg-background ui:border ui:data-[state=open]:animate-in ui:data-[state=closed]:animate-out ui:data-[state=closed]:fade-out-0 ui:data-[state=open]:fade-in-0 ui:data-[state=closed]:zoom-out-95 ui:data-[state=open]:zoom-in-95 ui:fixed ui:left-[50%] ui:top-[50%] ui:z-200 ui:grid ui:w-full ui:max-w-[calc(100%-2rem)] ui:translate-x-[-50%] ui:translate-y-[-50%] ui:gap-4 ui:rounded-lg ui:p-6 ui:[&:has([data-slot=dialog-footer][data-sticky=true])]:pb-0 ui:shadow-lg ui:duration-200 ui:sm:max-w-lg',
|
||||
'ui:bg-background ui:border ui:data-[state=open]:animate-in ui:data-[state=closed]:animate-out ui:data-[state=closed]:fade-out-0 ui:data-[state=open]:fade-in-0 ui:data-[state=closed]:zoom-out-95 ui:data-[state=open]:zoom-in-95 ui:fixed ui:left-[50%] ui:top-[50%] ui:z-modal ui:grid ui:w-full ui:max-w-[calc(100%-2rem)] ui:translate-x-[-50%] ui:translate-y-[-50%] ui:gap-4 ui:rounded-lg ui:p-6 ui:[&:has([data-slot=dialog-footer][data-sticky=true])]:pb-0 ui:shadow-lg ui:duration-200 ui:sm:max-w-lg',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
bind:ref
|
||||
data-slot="dialog-overlay"
|
||||
class={cn(
|
||||
'ui:data-[state=open]:animate-in ui:data-[state=closed]:animate-out ui:data-[state=closed]:fade-out-0 ui:data-[state=open]:fade-in-0 ui:fixed ui:inset-0 ui:z-200 ui:bg-black/50',
|
||||
'ui:data-[state=open]:animate-in ui:data-[state=closed]:animate-out ui:data-[state=closed]:fade-out-0 ui:data-[state=open]:fade-in-0 ui:fixed ui:inset-0 ui:z-modal ui:bg-black/50',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
<div
|
||||
data-slot="sidebar-container"
|
||||
class={cn(
|
||||
'ui:w-(--sidebar-width) ui:fixed ui:inset-y-0 ui:z-100 ui:hidden ui:max-h-svh ui:transition-[left,right,width] ui:duration-200 ui:ease-linear ui:md:flex',
|
||||
'ui:w-(--sidebar-width) ui:fixed ui:inset-y-0 ui:z-app-bar ui:hidden ui:max-h-svh ui:transition-[left,right,width] ui:duration-200 ui:ease-linear ui:md:flex',
|
||||
side === 'left'
|
||||
? 'ui:left-0 ui:group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]'
|
||||
: 'ui:right-0 ui:group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
|
||||
|
||||
@@ -7,6 +7,53 @@
|
||||
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
/**
|
||||
* Shared z-index scale.
|
||||
*
|
||||
* Tailwind supports arbitrary integer z-index values out of the box (e.g. `z-137`), which is
|
||||
* how one-off stacking numbers end up scattered across the app and collide as new layers get
|
||||
* added. Prefer these named layers instead so intent is documented at the call site — see
|
||||
* `packages/ui/README.md` for the full write-up and when to reach for each one.
|
||||
*
|
||||
* z-app-bar (100) Sticky/fixed app chrome: top bars, sticky in-page headers, the
|
||||
* desktop sidebar rail, the side panel.
|
||||
* z-app-bar-elevated (150) A sidebar (mobile sheet or desktop) that must render above other
|
||||
* z-app-bar content while open, e.g. the course content sidebar.
|
||||
* z-modal (200) Blocking dialogs/drawers.
|
||||
* z-menu-elevated (250) Dropdown/select/popover content that must escape a z-app-bar,
|
||||
* z-app-bar-elevated, or z-modal ancestor.
|
||||
*
|
||||
* Anything not listed (tooltips, dropdowns, sheets, drawers) uses Tailwind's default `z-50`,
|
||||
* which already sits above normal page content.
|
||||
*/
|
||||
@utility z-app-bar {
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
@utility z-app-bar-elevated {
|
||||
z-index: 150;
|
||||
}
|
||||
|
||||
@utility z-modal {
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
@utility z-menu-elevated {
|
||||
z-index: 250;
|
||||
}
|
||||
|
||||
/*
|
||||
* `@utility` only emits a class when Tailwind's scanner sees that exact candidate
|
||||
* somewhere under `packages/ui/src` — it does not ship every documented layer just because
|
||||
* it's declared above. Safelist all four here so the compiled `@cio/ui` CSS always exposes
|
||||
* the full scale, regardless of which layers happen to be referenced inside this package
|
||||
* today; otherwise a consumer using e.g. `ui:z-menu-elevated` could silently get no rule.
|
||||
*/
|
||||
@source inline('ui:z-app-bar{,!}');
|
||||
@source inline('ui:z-app-bar-elevated{,!}');
|
||||
@source inline('ui:z-modal{,!}');
|
||||
@source inline('ui:z-menu-elevated{,!}');
|
||||
|
||||
:root {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user