mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
refactor(css): move the shell and sheet rules into the components layer
Unlayered, the safe-area, keyboard and sheet-clearance rules outranked every utility on the same element, so a later bg-* on the shell or a bottom-* on a sheet would have been ignored without a trace. They now sit under the utilities. The bottom sheet drops its bottom-0, which the layered keyboard rule replaces with the same zero until the keyboard opens; the keyboard rules that must win keep their !important. The toast lift stays unlayered to outrank the toaster's own rules.
This commit is contained in:
1 parent
938b5151de
commit
ef1976832b
3 files changed
+104
-59
No files matched your search
@@ -92,6 +92,37 @@ describe("the shell owns the safe area", () => {
|
||||
for (const cls of padded) expect(cls).toMatch(/\bkeyboard-open:pb-4\b/);
|
||||
});
|
||||
|
||||
it("those rules sit under the utilities, and the bottom sheet sets no bottom utility", () => {
|
||||
// Unlayered, the rules outranked every utility on the same element: a
|
||||
// `bg-*` on the shell or a `bottom-*` on a sheet would have been ignored
|
||||
// without a trace. In the components layer a utility wins, so the bottom
|
||||
// sheet must not carry one that would pin it under the keyboard.
|
||||
const css = code(read("src/app/globals.css"));
|
||||
const start = css.indexOf("@layer components {");
|
||||
expect(start).toBeGreaterThan(-1);
|
||||
let depth = 0;
|
||||
let end = start;
|
||||
for (let i = css.indexOf("{", start); i < css.length; i += 1) {
|
||||
if (css[i] === "{") depth += 1;
|
||||
else if (css[i] === "}" && --depth === 0) {
|
||||
end = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const layer = css.slice(start, end);
|
||||
expect(layer).toMatch(/\.shell-safe-area\s*\{/);
|
||||
expect(layer).toMatch(/\[data-sheet-side="bottom"\]\s*\{\s*bottom:/);
|
||||
expect(layer).toMatch(/\[data-keyboard-hide\]\s*\{\s*display:\s*none/);
|
||||
expect(layer).toMatch(/\[data-sheet-side="left"\]\s*\{\s*border-left:/);
|
||||
// Sonner's own rules are unlayered; the toast lift must stay so.
|
||||
expect(layer).not.toMatch(/data-sonner-toaster/);
|
||||
|
||||
const sheet = code(read("src/components/ui/sheet.tsx"));
|
||||
const bottomBranch = sheet.match(/side === "bottom" &&\s*"([^"]*)"/)?.[1];
|
||||
expect(bottomBranch).toBeDefined();
|
||||
expect(bottomBranch).not.toMatch(/(?<![\w-])bottom-/);
|
||||
});
|
||||
|
||||
it("the sheet primitive names its side for those rules", () => {
|
||||
expect(read("src/components/ui/sheet.tsx")).toMatch(
|
||||
/data-sheet-side=\{side\}/,
|
||||
|
||||
+69
-58
@@ -594,45 +594,77 @@
|
||||
}
|
||||
|
||||
/*
|
||||
* `.shell-safe-area` — the authenticated app shell's share of the installed
|
||||
* app's safe area (see the note in `auth-shell.tsx`). The shell takes the top
|
||||
* and side insets as padding, and the strip behind the status bar is painted
|
||||
* in the chrome colour (`--card`, the top bar and the sidebar) rather than
|
||||
* the page colour, so the status bar reads as part of the bar beneath it. A
|
||||
* zero inset makes the gradient zero tall: nothing changes in a browser tab.
|
||||
* The shell's safe area, the on-screen keyboard and the sheet clearances sit
|
||||
* in the components layer, under the utilities, so a utility on the same
|
||||
* element still wins and these rules cannot silently override one. The
|
||||
* bottom sheet takes its `bottom` from here alone for that reason (the
|
||||
* sheet primitive sets no `bottom-*`); the keyboard rules that must beat a
|
||||
* utility say `!important`, which in a layer outranks any utility.
|
||||
*/
|
||||
.shell-safe-area {
|
||||
padding-top: env(safe-area-inset-top, 0px);
|
||||
padding-right: env(safe-area-inset-right, 0px);
|
||||
padding-left: env(safe-area-inset-left, 0px);
|
||||
background: linear-gradient(var(--card), var(--card)) top / 100%
|
||||
env(safe-area-inset-top, 0px) no-repeat;
|
||||
}
|
||||
@layer components {
|
||||
/*
|
||||
* `.shell-safe-area` — the authenticated app shell's share of the installed
|
||||
* app's safe area (see the note in `auth-shell.tsx`). The shell takes the top
|
||||
* and side insets as padding, and the strip behind the status bar is painted
|
||||
* in the chrome colour (`--card`, the top bar and the sidebar) rather than
|
||||
* the page colour, so the status bar reads as part of the bar beneath it. A
|
||||
* zero inset makes the gradient zero tall: nothing changes in a browser tab.
|
||||
*/
|
||||
.shell-safe-area {
|
||||
padding-top: env(safe-area-inset-top, 0px);
|
||||
padding-right: env(safe-area-inset-right, 0px);
|
||||
padding-left: env(safe-area-inset-left, 0px);
|
||||
background: linear-gradient(var(--card), var(--card)) top / 100%
|
||||
env(safe-area-inset-top, 0px) no-repeat;
|
||||
}
|
||||
|
||||
/*
|
||||
* The on-screen keyboard. iOS Safari, and Chrome on Android by default,
|
||||
* shrink only the visual viewport when the keyboard opens: everything
|
||||
* `position: fixed` stays anchored to the layout viewport, so a bottom sheet
|
||||
* keeps its footer (Save, the Coach composer) behind the keyboard and the
|
||||
* bottom bar floats somewhere under it. `useKeyboardInset()` (mounted once
|
||||
* in the providers) publishes the covered height as `--keyboard-inset` and
|
||||
* sets `data-keyboard` on `<html>` while it is open.
|
||||
*
|
||||
* Bottom sheets then stand on the keyboard and give up the height it takes,
|
||||
* so their own scroll body (every bottom sheet is a flex column with a
|
||||
* scrolling middle) carries the form, and the bottom bar and the Coach
|
||||
* button step aside until the keyboard is gone.
|
||||
*/
|
||||
[data-sheet-side="bottom"] {
|
||||
bottom: var(--keyboard-inset, 0px);
|
||||
}
|
||||
html[data-keyboard="open"] [data-sheet-side="bottom"] {
|
||||
max-height: calc(
|
||||
100dvh - var(--keyboard-inset, 0px) - env(safe-area-inset-top, 0px) - 0.5rem
|
||||
) !important;
|
||||
}
|
||||
html[data-keyboard="open"] [data-keyboard-hide] {
|
||||
display: none !important;
|
||||
/*
|
||||
* The on-screen keyboard. iOS Safari, and Chrome on Android by default,
|
||||
* shrink only the visual viewport when the keyboard opens: everything
|
||||
* `position: fixed` stays anchored to the layout viewport, so a bottom sheet
|
||||
* keeps its footer (Save, the Coach composer) behind the keyboard and the
|
||||
* bottom bar floats somewhere under it. `useKeyboardInset()` (mounted once
|
||||
* in the providers) publishes the covered height as `--keyboard-inset` and
|
||||
* sets `data-keyboard` on `<html>` while it is open.
|
||||
*
|
||||
* Bottom sheets then stand on the keyboard and give up the height it takes,
|
||||
* so their own scroll body (every bottom sheet is a flex column with a
|
||||
* scrolling middle) carries the form, and the bottom bar and the Coach
|
||||
* button step aside until the keyboard is gone.
|
||||
*/
|
||||
[data-sheet-side="bottom"] {
|
||||
bottom: var(--keyboard-inset, 0px);
|
||||
}
|
||||
html[data-keyboard="open"] [data-sheet-side="bottom"] {
|
||||
max-height: calc(
|
||||
100dvh - var(--keyboard-inset, 0px) - env(safe-area-inset-top, 0px) -
|
||||
0.5rem
|
||||
) !important;
|
||||
}
|
||||
html[data-keyboard="open"] [data-keyboard-hide] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/*
|
||||
* Side and top sheets run the full height of the window. In an installed
|
||||
* app on a tablet the status bar sits over their first 24 px, and a phone
|
||||
* held sideways has its notch over one side, so the panel keeps its header
|
||||
* clear of both. A transparent border rather than padding: every consumer
|
||||
* sets its own padding (often `p-0`), which would override a padding here,
|
||||
* while the border adds the clearance under any of them and lets the
|
||||
* panel's own background show through it.
|
||||
*/
|
||||
[data-sheet-side="right"],
|
||||
[data-sheet-side="left"],
|
||||
[data-sheet-side="top"] {
|
||||
border-top: env(safe-area-inset-top, 0px) solid transparent;
|
||||
}
|
||||
[data-sheet-side="right"] {
|
||||
border-right: env(safe-area-inset-right, 0px) solid transparent;
|
||||
}
|
||||
[data-sheet-side="left"] {
|
||||
border-left: env(safe-area-inset-left, 0px) solid transparent;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -650,27 +682,6 @@ html [data-sonner-toaster][data-y-position="bottom"] {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Side and top sheets run the full height of the window. In an installed
|
||||
* app on a tablet the status bar sits over their first 24 px, and a phone
|
||||
* held sideways has its notch over one side, so the panel keeps its header
|
||||
* clear of both. A transparent border rather than padding: every consumer
|
||||
* sets its own padding (often `p-0`), which would override a padding here,
|
||||
* while the border adds the clearance under any of them and lets the
|
||||
* panel's own background show through it.
|
||||
*/
|
||||
[data-sheet-side="right"],
|
||||
[data-sheet-side="left"],
|
||||
[data-sheet-side="top"] {
|
||||
border-top: env(safe-area-inset-top, 0px) solid transparent;
|
||||
}
|
||||
[data-sheet-side="right"] {
|
||||
border-right: env(safe-area-inset-right, 0px) solid transparent;
|
||||
}
|
||||
[data-sheet-side="left"] {
|
||||
border-left: env(safe-area-inset-left, 0px) solid transparent;
|
||||
}
|
||||
|
||||
/*
|
||||
* `.no-scrollbar` — keeps a container scrollable but hides the visible
|
||||
* bar. Used by the mobile section strips in `<AdminShell>` and
|
||||
|
||||
@@ -66,6 +66,9 @@ function SheetContent({
|
||||
// full-height sides keep clear of the status bar and the notch.
|
||||
// Its own attribute rather than `data-slot`, which consumers
|
||||
// replace with theirs (`responsive-sheet-content`, `day-panel`).
|
||||
// A bottom sheet's `bottom` comes from that rule (0 until the
|
||||
// keyboard opens), so its branch below carries no `bottom-*`: a
|
||||
// utility would outrank the layered rule and pin it to the edge.
|
||||
data-sheet-side={side}
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
||||
@@ -76,7 +79,7 @@ function SheetContent({
|
||||
side === "top" &&
|
||||
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
|
||||
side === "bottom" &&
|
||||
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
|
||||
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 h-auto border-t",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
Reference in new issue
Block a user