feat(t5): Fokusring für alle, 44-px-Ziele bei Touch, 16-px-Schrift (T5a A5)
- globals.css: eine Basisregel :where(:focus-visible) mit --focus-width und --focus-offset (Sonne 3 px), Firefox-:-moz-focusring gleichgezogen; --text-xs 0.8125rem. - Fokus-Codemod (scripts/codemod-t5-focus.mjs): eigene Ringe und outline-none raus, sm:h-/sm:size- zu pointer-fine:, Label-Zeilen mit focus-ring-within; Skript-Fokus (Lücken-Liste, „Gespeichert“) bleibt. - Button/Input 44 px, kompakt nur mit Maus; Icon-Knöpfe mit unsichtbarer 44-px-Trefferfläche bei Touch, Nachbarn mit genug Abstand. - 16 px in Feldern, Labels, Auswahl, Leerzuständen, Listentiteln. - .holo-stage auf allen Beute-Bühnen, .stage-card holt den Seitenring. - Baukasten-Wächter (src/styles/baukasten-guard.spec.ts) mit den Regeln aus A3–A5, Ausnahmen mit Grund; Kalender-Auswahl als Inset-Ring. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
62ab080725
commit
ce68729233
85 files changed
+935
-139
No files matched your search
@@ -78,7 +78,7 @@ export function ColonyOverviewLinks({ overview, hrefFor }: ColonyOverviewLinksPr
|
||||
<Link
|
||||
key={row.id}
|
||||
href={hrefFor(row.id)}
|
||||
className="flex min-h-14 items-center gap-3 rounded-xl border border-border/60 px-3 py-2 hover:bg-muted/50 focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:outline-none"
|
||||
className="flex min-h-14 items-center gap-3 rounded-xl border border-border/60 px-3 py-2 hover:bg-muted/50"
|
||||
>
|
||||
<span className="flex min-w-0 flex-1 flex-col gap-1">
|
||||
<span className="flex flex-wrap items-center gap-2">
|
||||
|
||||
@@ -26,7 +26,7 @@ const PRINT_DONE = "Etiketten-PDF heruntergeladen.";
|
||||
const PROOF_DONE = "Probedruck (MUSTER) heruntergeladen. Er ist nicht zum Verkauf gedacht.";
|
||||
const CHECK_FALLBACK = "Die Pflichtangaben konnten nicht geprüft werden.";
|
||||
const LINK_CLASS =
|
||||
"inline-flex min-h-11 items-center rounded-md text-sm font-medium text-primary-ink underline underline-offset-4 focus-visible:outline-2 focus-visible:outline-ring";
|
||||
"inline-flex min-h-11 items-center rounded-md text-sm font-medium text-primary-ink underline underline-offset-4";
|
||||
|
||||
type FixTarget = "master" | "product" | "designer";
|
||||
|
||||
|
||||
@@ -192,6 +192,20 @@ describe("CalendarPage entries", () => {
|
||||
expect(link).toHaveTextContent("Wartezeit bis einschließlich 12.08. – ab 13.08. wieder erntefähig");
|
||||
});
|
||||
|
||||
it("marks the chosen day with an inset ring and keeps the focus ring inside the clipped grid", async () => {
|
||||
renderPage();
|
||||
await screen.findAllByText("Wartezeit B3 · Oxuvar", { selector: "span.truncate" });
|
||||
const day = screen.getByRole("button", { name: /Dienstag, 11\. August/ });
|
||||
await userEvent.click(day);
|
||||
|
||||
const classes = day.className.split(/\s+/);
|
||||
expect(classes).toEqual(expect.arrayContaining(["ring-2", "ring-inset", "ring-foreground"]));
|
||||
// A selection outline would replace the focus ring; the grid clips (overflow-hidden), so
|
||||
// the focus ring of every day is drawn inside the cell, next to the selection ring.
|
||||
expect(day.className).not.toMatch(/(^|\s)outline-(2|foreground)|-outline-offset-2/);
|
||||
expect(classes).toContain("focus-visible:outline-offset-[calc(-2px_-_var(--focus-width))]");
|
||||
});
|
||||
|
||||
it("counts a day with several entries in the plural", async () => {
|
||||
vi.mocked(fetchCalendarEvents).mockResolvedValue([
|
||||
{ id: "a", type: "TASK", date: "2026-08-12T00:00:00.000Z", title: "A", relatedType: "Task", relatedId: "1" },
|
||||
|
||||
@@ -146,7 +146,7 @@ function EventRow({ event, isContinuation }: { event: CalendarEvent; isContinuat
|
||||
</span>
|
||||
);
|
||||
return href ? (
|
||||
<Link href={href} className="block rounded-lg focus-visible:ring-3 focus-visible:ring-ring/50">
|
||||
<Link href={href} className="block rounded-lg">
|
||||
{content}
|
||||
</Link>
|
||||
) : (
|
||||
@@ -353,9 +353,11 @@ export default function CalendarPage() {
|
||||
aria-pressed={isSelected}
|
||||
aria-label={`${dayHeading(day)}${isToday ? ", heute" : ""}: ${entryCountText(dayEvents.length)}`}
|
||||
className={cn(
|
||||
"flex min-h-24 flex-col gap-1 border-r border-b p-1.5 text-left last:border-r-0",
|
||||
// The grid clips (overflow-hidden): the focus ring sits inside the cell,
|
||||
// next to the inset selection ring, so edge days keep it whole.
|
||||
"flex min-h-24 flex-col gap-1 border-r border-b p-1.5 text-left last:border-r-0 focus-visible:outline-offset-[calc(-2px_-_var(--focus-width))]",
|
||||
isCurrentMonth ? "bg-card" : "bg-muted/40",
|
||||
isSelected && "outline-2 -outline-offset-2 outline-foreground",
|
||||
isSelected && "ring-2 ring-inset ring-foreground",
|
||||
)}
|
||||
>
|
||||
<span
|
||||
|
||||
@@ -83,7 +83,7 @@ function ActionFooter({
|
||||
ref={errorRef}
|
||||
role="alert"
|
||||
tabIndex={-1}
|
||||
className="text-sm font-medium text-destructive outline-none"
|
||||
className="text-sm font-medium text-destructive"
|
||||
>
|
||||
{error}
|
||||
</p>
|
||||
|
||||
@@ -283,7 +283,7 @@ export function ColonyEditCard({ colony }: { colony: Colony }) {
|
||||
ref={conflictRef}
|
||||
role="alert"
|
||||
tabIndex={-1}
|
||||
className="text-sm font-medium text-destructive outline-none"
|
||||
className="text-sm font-medium text-destructive"
|
||||
>
|
||||
{conflictText}
|
||||
</p>
|
||||
|
||||
@@ -66,7 +66,7 @@ export default function HiveEditorPage() {
|
||||
);
|
||||
}
|
||||
if (!colony.data || !hive.data || !catalog) {
|
||||
return <div role="status" aria-label="Beute wird geladen" className="fixed inset-0 z-40 bg-holo-bg motion-safe:animate-pulse" />;
|
||||
return <div role="status" aria-label="Beute wird geladen" className="holo-stage fixed inset-0 z-40 bg-holo-bg motion-safe:animate-pulse" />;
|
||||
}
|
||||
return (
|
||||
<HiveEditor
|
||||
|
||||
@@ -238,7 +238,7 @@ export default function NewColonyPage() {
|
||||
ref={errorRef}
|
||||
role="alert"
|
||||
tabIndex={-1}
|
||||
className="text-sm font-medium text-destructive outline-none"
|
||||
className="text-sm font-medium text-destructive"
|
||||
>
|
||||
{submitError}
|
||||
</p>
|
||||
|
||||
@@ -39,7 +39,7 @@ function Step({ number, title, titleId, state, children }: StepProps) {
|
||||
<span
|
||||
id={titleId}
|
||||
tabIndex={titleId ? -1 : undefined}
|
||||
className="font-heading text-base font-semibold outline-none focus-visible:ring-3 focus-visible:ring-ring/50"
|
||||
className="font-heading text-base font-semibold"
|
||||
>{`${number}. ${title}`}</span>
|
||||
<span className="inline-flex items-center gap-1.5 text-sm font-medium">
|
||||
<Icon className="size-4" aria-hidden />
|
||||
|
||||
@@ -164,7 +164,7 @@ export default function NoticeDetailPage() {
|
||||
id="notice-flow-heading"
|
||||
ref={flowHeading}
|
||||
tabIndex={-1}
|
||||
className="font-heading text-lg font-semibold tracking-tight outline-none"
|
||||
className="font-heading text-lg font-semibold tracking-tight"
|
||||
>
|
||||
{flowTitle}
|
||||
</h2>
|
||||
|
||||
@@ -197,7 +197,7 @@ function CreateNoticeForm({ onDone }: { onDone: () => void }) {
|
||||
<p aria-live="polite" className="min-h-5 text-sm text-destructive">
|
||||
{showProblem ? problem : null}
|
||||
</p>
|
||||
<div ref={errorRef} tabIndex={-1} role="alert" className="outline-none">
|
||||
<div ref={errorRef} tabIndex={-1} role="alert">
|
||||
{error ? (
|
||||
<div className="flex flex-col gap-2 text-sm text-destructive">
|
||||
<p>{error.text}</p>
|
||||
|
||||
@@ -62,7 +62,7 @@ export default function NoticesPage() {
|
||||
id="notice-kind-filter"
|
||||
value={kind}
|
||||
onChange={(event) => setKind(event.target.value as KindFilter)}
|
||||
className="h-11 w-full rounded-xl border border-input bg-card px-3 text-base sm:max-w-xs md:text-sm"
|
||||
className="h-11 w-full rounded-xl border border-input bg-card px-3 text-base sm:max-w-xs"
|
||||
>
|
||||
<option value="ALL">Alle</option>
|
||||
{NOTICE_KINDS.map((entry) => (
|
||||
@@ -85,7 +85,7 @@ export default function NoticesPage() {
|
||||
aria-invalid={yearInvalid ? true : undefined}
|
||||
aria-describedby={yearInvalid ? "notice-year-problem" : undefined}
|
||||
onChange={(event) => setYearText(event.target.value)}
|
||||
className="h-11 w-28 rounded-xl border border-input bg-card px-3 text-base md:text-sm"
|
||||
className="h-11 w-28 rounded-xl border border-input bg-card px-3 text-base"
|
||||
/>
|
||||
{yearInvalid ? (
|
||||
<p id="notice-year-problem" className="text-sm text-destructive">
|
||||
@@ -131,7 +131,7 @@ function NoticeLink({ notice }: { notice: AuthorityNotice }) {
|
||||
return (
|
||||
<Link
|
||||
href={`/compliance/notices/${encodeURIComponent(notice.id)}`}
|
||||
className="flex min-h-11 flex-col gap-1 rounded-xl border border-border/60 bg-card p-3 outline-none hover:bg-muted focus-visible:ring-3 focus-visible:ring-ring/50"
|
||||
className="flex min-h-11 flex-col gap-1 rounded-xl border border-border/60 bg-card p-3 hover:bg-muted"
|
||||
>
|
||||
<span className="flex flex-wrap items-center justify-between gap-2">
|
||||
<span className="font-heading text-base font-semibold">{noticeTitle(notice)}</span>
|
||||
|
||||
@@ -17,7 +17,7 @@ const STATE_ICON: Record<ReadinessItemState, LucideIcon> = {
|
||||
};
|
||||
|
||||
const FIX_LINK_CLASS =
|
||||
"inline-flex min-h-11 shrink-0 items-center gap-1 rounded-lg px-2 font-medium text-primary-ink underline-offset-4 outline-none hover:underline focus-visible:ring-3 focus-visible:ring-ring/50";
|
||||
"inline-flex min-h-11 shrink-0 items-center gap-1 rounded-lg px-2 font-medium text-primary-ink underline-offset-4 hover:underline";
|
||||
|
||||
function FixLink({ itemKey, subject, context }: { itemKey: string; subject: ReadinessSubject | null; context: string }) {
|
||||
const { href, label, available } = fixLinkFor(itemKey, subject);
|
||||
@@ -117,7 +117,7 @@ export function ReadinessItemCard({ item, canManage }: ReadinessItemCardProps) {
|
||||
<h3
|
||||
ref={headingRef}
|
||||
tabIndex={-1}
|
||||
className="min-w-0 font-heading text-base font-semibold break-words outline-none focus-visible:ring-3 focus-visible:ring-ring/50"
|
||||
className="min-w-0 font-heading text-base font-semibold break-words"
|
||||
>
|
||||
{item.title}
|
||||
</h3>
|
||||
|
||||
@@ -46,7 +46,7 @@ function Section({
|
||||
const headingId = `restrictions-${id}`;
|
||||
return (
|
||||
<section aria-labelledby={headingId} className="flex flex-col gap-2">
|
||||
<h2 id={headingId} tabIndex={-1} className="font-heading text-lg font-semibold tracking-tight outline-none">
|
||||
<h2 id={headingId} tabIndex={-1} className="font-heading text-lg font-semibold tracking-tight">
|
||||
{`${title} (${rows.length})`}
|
||||
</h2>
|
||||
{rows.length === 0 ? (
|
||||
|
||||
@@ -244,7 +244,7 @@ export default function DashboardPage() {
|
||||
<li key={tile.label}>
|
||||
<Link
|
||||
href={tile.href}
|
||||
className="flex min-h-11 items-center gap-3 rounded-xl px-3 py-2 transition-colors hover:bg-muted focus-visible:outline-2 focus-visible:outline-ring"
|
||||
className="flex min-h-11 items-center gap-3 rounded-xl px-3 py-2 transition-colors hover:bg-muted"
|
||||
>
|
||||
<Icon
|
||||
className={`size-5 shrink-0 ${tile.attention ? "text-warning-ink" : "text-primary-ink"}`}
|
||||
|
||||
@@ -202,7 +202,7 @@ export default function NewExtractionPage() {
|
||||
data-field="harvestIds"
|
||||
aria-labelledby={labelId}
|
||||
aria-describedby={describedBy}
|
||||
className="flex flex-col gap-2 outline-none"
|
||||
className="flex flex-col gap-2"
|
||||
>
|
||||
{harvests?.map((harvest) => (
|
||||
<ToggleChip
|
||||
|
||||
@@ -253,7 +253,7 @@ export default function NewHarvestPage() {
|
||||
data-field="colonyIds"
|
||||
aria-labelledby={labelId}
|
||||
aria-describedby={describedBy}
|
||||
className="grid grid-cols-2 gap-2 outline-none"
|
||||
className="grid grid-cols-2 gap-2"
|
||||
>
|
||||
{apiaryColonies.map((colony) => {
|
||||
const withdrawal = withdrawalByColony.get(colony.id);
|
||||
|
||||
@@ -147,7 +147,7 @@ export function InspectionHiveSection({
|
||||
// A portal form inside the card (frame/part sheet) must never submit the enclosing inspection form.
|
||||
<div
|
||||
data-testid="hive-section"
|
||||
className="flex flex-col gap-4 outline-none"
|
||||
className="flex flex-col gap-4"
|
||||
onSubmit={(event) => event.stopPropagation()}
|
||||
>
|
||||
<h3 ref={headingRef} tabIndex={-1} className="sr-only">
|
||||
|
||||
@@ -85,7 +85,7 @@ export default function AppLayout({ children }: { children: ReactNode }) {
|
||||
pageContentRef.current?.focus();
|
||||
pageContentRef.current?.scrollIntoView?.({ block: "start" });
|
||||
}}
|
||||
className="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-50 focus:rounded-lg focus:bg-card focus:px-4 focus:py-3 focus:text-sm focus:font-medium focus:shadow-[var(--shadow-elevated)] focus:ring-3 focus:ring-ring/50 focus:outline-none"
|
||||
className="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-50 focus:rounded-lg focus:bg-card focus:px-4 focus:py-3 focus:text-sm focus:font-medium focus:shadow-[var(--shadow-elevated)]"
|
||||
>
|
||||
Zum Inhalt springen
|
||||
</a>
|
||||
@@ -148,7 +148,7 @@ export default function AppLayout({ children }: { children: ReactNode }) {
|
||||
id={MAIN_CONTENT_ID}
|
||||
ref={pageContentRef}
|
||||
tabIndex={-1}
|
||||
className={`flex-1 ${contentPaddingClass} outline-none sm:pb-(--safe-area-bottom)`}
|
||||
className={`flex-1 ${contentPaddingClass} sm:pb-(--safe-area-bottom)`}
|
||||
>
|
||||
<PageTransition>{children}</PageTransition>
|
||||
</div>
|
||||
|
||||
@@ -50,7 +50,7 @@ interface LotFormValues {
|
||||
|
||||
type LotFormErrors = Partial<Record<keyof LotFormValues, string>>;
|
||||
|
||||
const INPUT_CLASS = "h-11 sm:h-11";
|
||||
const INPUT_CLASS = "h-11 pointer-fine:h-11";
|
||||
|
||||
function initialValues(medicine: LotMedicine, lot?: LotView): LotFormValues {
|
||||
if (lot) {
|
||||
|
||||
@@ -129,7 +129,7 @@ function DisposalForm({ lotId, remaining, unit, onDone }: DisposalFormProps) {
|
||||
<Input
|
||||
id="disposal-quantity"
|
||||
inputMode="decimal"
|
||||
className="h-11 sm:h-11"
|
||||
className="h-11 pointer-fine:h-11"
|
||||
value={quantityText}
|
||||
onChange={(event) => setQuantityText(event.target.value)}
|
||||
/>
|
||||
@@ -141,7 +141,7 @@ function DisposalForm({ lotId, remaining, unit, onDone }: DisposalFormProps) {
|
||||
</Label>
|
||||
<Input
|
||||
id="disposal-reason"
|
||||
className="h-11 sm:h-11"
|
||||
className="h-11 pointer-fine:h-11"
|
||||
value={reason}
|
||||
maxLength={500}
|
||||
placeholder="z. B. abgelaufen, verschüttet"
|
||||
|
||||
@@ -128,7 +128,7 @@ function InventoryForm({ lotId, remaining, unit, onDone }: InventoryFormProps) {
|
||||
<Input
|
||||
id="inventory-counted"
|
||||
inputMode="decimal"
|
||||
className="h-11 sm:h-11"
|
||||
className="h-11 pointer-fine:h-11"
|
||||
value={counted}
|
||||
aria-describedby="inventory-status"
|
||||
onChange={(event) => setCounted(event.target.value)}
|
||||
|
||||
@@ -89,7 +89,7 @@ interface MedicineFormProps {
|
||||
onDone: () => void;
|
||||
}
|
||||
|
||||
const INPUT_CLASS = "h-11 sm:h-11";
|
||||
const INPUT_CLASS = "h-11 pointer-fine:h-11";
|
||||
|
||||
function MedicineForm({ medicine, prefill, onDone }: MedicineFormProps) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -72,7 +72,7 @@ export default function OfflinePage() {
|
||||
<Link
|
||||
key={stored.apiaryId}
|
||||
href={`/offline/apiary?id=${encodeURIComponent(stored.apiaryId)}`}
|
||||
className="flex min-h-14 items-center gap-3 rounded-xl border border-border/60 px-3 py-2 hover:bg-muted/50 focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:outline-none"
|
||||
className="flex min-h-14 items-center gap-3 rounded-xl border border-border/60 px-3 py-2 hover:bg-muted/50"
|
||||
>
|
||||
<span className="flex min-w-0 flex-1 flex-col">
|
||||
<span className="font-medium">{stored.pack.apiary.name}</span>
|
||||
|
||||
@@ -29,7 +29,7 @@ export function RuleCardShell({ rule, open, onToggle, children }: RuleCardShellP
|
||||
aria-expanded={open}
|
||||
aria-controls={contentId}
|
||||
onClick={onToggle}
|
||||
className="flex min-h-11 w-full items-start gap-3 rounded-xl px-4 py-1 text-left focus-visible:ring-3 focus-visible:ring-ring/50"
|
||||
className="flex min-h-11 w-full items-start gap-3 rounded-xl px-4 py-1 text-left"
|
||||
>
|
||||
<span className="flex min-w-0 flex-1 flex-col gap-1">
|
||||
<span className="flex items-center justify-between gap-2">
|
||||
|
||||
+3
-2
@@ -44,7 +44,8 @@ export function ElementPropertiesPanel({
|
||||
<p className="text-sm font-medium text-muted-foreground">
|
||||
{element.type === "text" ? "Text" : element.type === "qrCode" ? "QR-Code" : element.type === "image" ? "Bild" : "Linie"}
|
||||
</p>
|
||||
<div className="flex gap-1">
|
||||
{/* pointer-coarse:gap-4: the 44 px hit areas of the two 28 px buttons must not overlap. */}
|
||||
<div className="flex gap-1 pointer-coarse:gap-4">
|
||||
<Button type="button" variant="ghost" size="icon-sm" onClick={onDuplicate} title="Duplizieren">
|
||||
<Copy className="size-3.5" />
|
||||
</Button>
|
||||
@@ -202,7 +203,7 @@ function TextFields({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-1.5">
|
||||
<div className="flex gap-1.5 pointer-coarse:gap-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant={element.bold ? "default" : "outline"}
|
||||
|
||||
@@ -659,7 +659,7 @@ export function LabelDesigner({ initial, onSubmit, submitLabel }: LabelDesignerP
|
||||
id="designer-min-x-height"
|
||||
type="text"
|
||||
inputMode="decimal"
|
||||
className="min-h-11 sm:h-11"
|
||||
className="min-h-11 pointer-fine:h-11"
|
||||
value={minXHeightText}
|
||||
aria-invalid={minXHeightInvalid ? true : undefined}
|
||||
aria-describedby="designer-min-x-height-hint"
|
||||
@@ -786,7 +786,7 @@ export function LabelDesigner({ initial, onSubmit, submitLabel }: LabelDesignerP
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<label className="flex min-h-11 cursor-pointer items-start gap-3 rounded-xl border border-border p-3 has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-ring">
|
||||
<label className="flex min-h-11 cursor-pointer items-start gap-3 rounded-xl border border-border p-3 focus-ring-within">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mt-1 size-4 accent-[var(--primary)]"
|
||||
|
||||
@@ -282,7 +282,7 @@ export default function SuggestionsPage() {
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
data-testid="suggestion-count"
|
||||
className="min-h-5 text-sm font-medium outline-none"
|
||||
className="min-h-5 text-sm font-medium"
|
||||
>
|
||||
{countText(loaded, total)}
|
||||
</p>
|
||||
|
||||
@@ -257,7 +257,7 @@ function WinterContent() {
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="size-11 sm:size-8"
|
||||
className="size-11 pointer-fine:size-8"
|
||||
aria-label="Vorheriger Winter"
|
||||
onClick={() => setWinterYear((year) => year - 1)}
|
||||
>
|
||||
@@ -267,7 +267,7 @@ function WinterContent() {
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="size-11 sm:size-8"
|
||||
className="size-11 pointer-fine:size-8"
|
||||
aria-label="Nächster Winter"
|
||||
onClick={() => setWinterYear((year) => year + 1)}
|
||||
>
|
||||
|
||||
@@ -7,6 +7,12 @@
|
||||
@custom-variant pointer-fine (@media (pointer: fine));
|
||||
@custom-variant pointer-coarse (@media (pointer: coarse));
|
||||
|
||||
/* Type scale (spec 4.5): text-xs is 13 px (sun: 14 px), only for axes, legends and footers. */
|
||||
@theme {
|
||||
--text-xs: 0.8125rem;
|
||||
--text-xs--line-height: calc(1.125 / 0.8125);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
@@ -332,6 +338,14 @@
|
||||
.holo-stage {
|
||||
--ring: var(--holo-edge-strong);
|
||||
}
|
||||
/* A page-coloured card on the stage (the outline panel) takes the page ring back. The root
|
||||
keeps a copy of its ring for that, resolved for the current set (light, dark, sun). */
|
||||
html {
|
||||
--page-ring: var(--ring);
|
||||
}
|
||||
.holo-stage .stage-card {
|
||||
--ring: var(--page-ring);
|
||||
}
|
||||
|
||||
/* Tinted surfaces: set --tint (and --tint-ink) on the element. In sun the tint becomes the
|
||||
plain card with a 2 px border in the ink instead of a 15 % wash (spec 4.3). */
|
||||
@@ -430,7 +444,7 @@
|
||||
--queen-green: oklch(0.58 0.15 145);
|
||||
--queen-blue: oklch(0.52 0.15 255);
|
||||
--text-sm: 0.875rem;
|
||||
--text-xs: 0.75rem;
|
||||
--text-xs: 0.8125rem;
|
||||
--focus-width: 2px;
|
||||
--control-border-width: 1px;
|
||||
--comb-stroke-1: 2px;
|
||||
@@ -480,7 +494,21 @@
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
@apply border-border;
|
||||
}
|
||||
/* One focus style for everything (spec 4.6): a solid ring in --ring with a gap, 3 px in sun.
|
||||
Components add no focus classes; a row whose focus target sits inside uses
|
||||
focus-ring-within. */
|
||||
:where(:focus-visible) {
|
||||
outline: var(--focus-width) solid var(--ring);
|
||||
outline-offset: var(--focus-offset);
|
||||
}
|
||||
/* Firefox: the preflight sets `:-moz-focusring:where(:not(iframe)) { outline: auto }` (0,1,0),
|
||||
which beats the rule above. Firefox matches :-moz-focusring exactly where it matches
|
||||
:focus-visible (keyboard, not a mouse click), so restating the ring here adds nothing else. */
|
||||
:-moz-focusring:where(:not(iframe)) {
|
||||
outline: var(--focus-width) solid var(--ring);
|
||||
outline-offset: var(--focus-offset);
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
@@ -513,6 +541,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* A row whose focus target sits inside (a label with its checkbox, radio or a visually hidden
|
||||
input) also rings the whole row, so the focus is easy to spot from arm's length (spec 4.6). */
|
||||
@utility focus-ring-within {
|
||||
&:has(:focus-visible) {
|
||||
outline: var(--focus-width) solid var(--ring);
|
||||
outline-offset: var(--focus-offset);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
Motion levels (see lib/motion/motion-preference.ts)
|
||||
html[data-motion] = full | subtle | off, chosen per device in the profile.
|
||||
@@ -574,7 +611,6 @@
|
||||
height: 2.75rem;
|
||||
border-radius: 9999px;
|
||||
color: var(--primary-foreground);
|
||||
outline: none;
|
||||
}
|
||||
.honey-check::before {
|
||||
content: "";
|
||||
@@ -600,9 +636,6 @@
|
||||
border-color: var(--primary);
|
||||
background: color-mix(in oklch, var(--primary) 8%, var(--card));
|
||||
}
|
||||
.honey-check:focus-visible::before {
|
||||
box-shadow: 0 0 0 3px color-mix(in oklch, var(--ring) 45%, transparent);
|
||||
}
|
||||
.honey-check svg {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
Reference in new issue
Block a user