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 <[email protected]>

* 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 <[email protected]>

---------

Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Rotimi Best <[email protected]>
This commit is contained in:
Rotimi Best
2026-07-11 08:25:11 +01:00
committed by GitHub
co-authored by Rotimi Best Cursor Agent
parent c0f82d9573
commit 0836d9b5bb
13 changed files with 83 additions and 14 deletions
@@ -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}
+1 -1
View File
@@ -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)]',
+47
View File
@@ -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