feat: add customizable CSS variables and custom CSS to Styling settings
Adds a "CSS Variables" panel to the Styling tab with a color picker and description for every color token in globals.css (base, sidebar, design token aliases, event colors, all-day chips), plus a "Custom CSS" textarea that loads after the default stylesheet and can override any rule. Overrides are stored per-user (customCssVars, customCss) and applied on the root container's inline style, so they take precedence over themes and per-field settings. v1.113.0
This commit is contained in:
parent
4a18027068
commit
02b2e40649
12
CHANGELOG.md
Normal file
12
CHANGELOG.md
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project are documented in this file.
|
||||||
|
|
||||||
|
## [1.113.0] - 2026-07-08
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Styling settings: "CSS Variables" panel with a color picker and description for every
|
||||||
|
customizable color token in `globals.css` (base colors, settings sidebar, design token
|
||||||
|
aliases, event colors, all-day chips).
|
||||||
|
- Styling settings: "Custom CSS" textarea to inject a personal stylesheet that loads after
|
||||||
|
the app's default styles and can override any rule.
|
||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "my-weekly-todo-list",
|
"name": "my-weekly-todo-list",
|
||||||
"version": "1.112.2",
|
"version": "1.113.0",
|
||||||
"description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view",
|
"description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
3
prisma/migrations/20260708_add_custom_css/migration.sql
Normal file
3
prisma/migrations/20260708_add_custom_css/migration.sql
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
-- User: per-user CSS variable overrides and free-form custom stylesheet (Styling settings tab)
|
||||||
|
ALTER TABLE "User" ADD COLUMN IF NOT EXISTS "customCssVars" JSONB;
|
||||||
|
ALTER TABLE "User" ADD COLUMN IF NOT EXISTS "customCss" TEXT;
|
||||||
@ -116,6 +116,8 @@ model User {
|
|||||||
weatherLon Float?
|
weatherLon Float?
|
||||||
viewSettings Json?
|
viewSettings Json?
|
||||||
weatherRecentCities Json?
|
weatherRecentCities Json?
|
||||||
|
customCssVars Json?
|
||||||
|
customCss String? @db.Text
|
||||||
hasCompletedOnboarding Boolean @default(true)
|
hasCompletedOnboarding Boolean @default(true)
|
||||||
showCalendarProviderIcon Boolean @default(false)
|
showCalendarProviderIcon Boolean @default(false)
|
||||||
headerCurrentDayFormat String? @default("DDD, DD. MMMM YYYY")
|
headerCurrentDayFormat String? @default("DDD, DD. MMMM YYYY")
|
||||||
|
|||||||
@ -111,6 +111,8 @@ export async function GET(request: NextRequest) {
|
|||||||
weatherLocation: true,
|
weatherLocation: true,
|
||||||
weatherRecentCities: true,
|
weatherRecentCities: true,
|
||||||
viewSettings: true,
|
viewSettings: true,
|
||||||
|
customCssVars: true,
|
||||||
|
customCss: true,
|
||||||
hasCompletedOnboarding: true,
|
hasCompletedOnboarding: true,
|
||||||
createdAt: true
|
createdAt: true
|
||||||
}
|
}
|
||||||
@ -156,6 +158,7 @@ export async function PATCH(request: NextRequest) {
|
|||||||
showCompletedTasks, showLines, startDayOffset, weekdayFormat, weekdayCase, customWeekdayNames, weekStartDay, quoteSourceUrls, quoteLanguages,
|
showCompletedTasks, showLines, startDayOffset, weekdayFormat, weekdayCase, customWeekdayNames, weekStartDay, quoteSourceUrls, quoteLanguages,
|
||||||
kanbanStages, lightTheme, darkTheme, notificationsEnabled, mobileFontScale,
|
kanbanStages, lightTheme, darkTheme, notificationsEnabled, mobileFontScale,
|
||||||
weatherEnabled, weatherLat, weatherLon, weatherLocation, weatherRecentCities, viewSettings,
|
weatherEnabled, weatherLat, weatherLon, weatherLocation, weatherRecentCities, viewSettings,
|
||||||
|
customCssVars, customCss,
|
||||||
hasCompletedOnboarding
|
hasCompletedOnboarding
|
||||||
} = body;
|
} = body;
|
||||||
|
|
||||||
@ -256,6 +259,8 @@ export async function PATCH(request: NextRequest) {
|
|||||||
...(weatherLocation !== undefined && { weatherLocation }),
|
...(weatherLocation !== undefined && { weatherLocation }),
|
||||||
...(weatherRecentCities !== undefined && { weatherRecentCities }),
|
...(weatherRecentCities !== undefined && { weatherRecentCities }),
|
||||||
...(viewSettings !== undefined && { viewSettings }),
|
...(viewSettings !== undefined && { viewSettings }),
|
||||||
|
...(customCssVars !== undefined && { customCssVars }),
|
||||||
|
...(customCss !== undefined && { customCss }),
|
||||||
...(hasCompletedOnboarding !== undefined && { hasCompletedOnboarding }),
|
...(hasCompletedOnboarding !== undefined && { hasCompletedOnboarding }),
|
||||||
};
|
};
|
||||||
if (password && password.trim() !== "") {
|
if (password && password.trim() !== "") {
|
||||||
@ -364,6 +369,8 @@ export async function PATCH(request: NextRequest) {
|
|||||||
weatherLocation: true,
|
weatherLocation: true,
|
||||||
weatherRecentCities: true,
|
weatherRecentCities: true,
|
||||||
viewSettings: true,
|
viewSettings: true,
|
||||||
|
customCssVars: true,
|
||||||
|
customCss: true,
|
||||||
hasCompletedOnboarding: true,
|
hasCompletedOnboarding: true,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -145,6 +145,219 @@ function ExtraTimezonesEditor({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// All customizable color custom-properties defined in globals.css :root, grouped for the
|
||||||
|
// "Styling" tab's CSS Variables editor. Overrides are stored as a flat { "--var-name": "#hex" }
|
||||||
|
// map in profile.customCssVars and applied on top of containerStyle in WeeklyView.
|
||||||
|
const CSS_VARIABLE_GROUPS: { title: string; vars: { name: string; label: string; default: string }[] }[] = [
|
||||||
|
{
|
||||||
|
title: "Base Colors",
|
||||||
|
vars: [
|
||||||
|
{ name: "--weekly-teal", label: "Accent", default: "#009a9a" },
|
||||||
|
{ name: "--weekly-bg", label: "Background", default: "#ffffff" },
|
||||||
|
{ name: "--weekly-text", label: "Text", default: "#000000" },
|
||||||
|
{ name: "--weekly-text-light", label: "Text (light)", default: "#767676" },
|
||||||
|
{ name: "--weekly-border", label: "Border", default: "#cccccc" },
|
||||||
|
{ name: "--weekly-completed", label: "Completed task", default: "#cccccc" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Settings Sidebar",
|
||||||
|
vars: [
|
||||||
|
{ name: "--weekly-settings-bg", label: "Background", default: "#ffffff" },
|
||||||
|
{ name: "--weekly-settings-item-bg", label: "Item background", default: "#f8fafc" },
|
||||||
|
{ name: "--weekly-settings-label", label: "Label", default: "#475569" },
|
||||||
|
{ name: "--weekly-settings-title", label: "Title", default: "#111827" },
|
||||||
|
{ name: "--weekly-settings-input-bg", label: "Input background", default: "#ffffff" },
|
||||||
|
{ name: "--weekly-settings-input-border", label: "Input border", default: "#dddddd" },
|
||||||
|
{ name: "--weekly-settings-text", label: "Text", default: "#111827" },
|
||||||
|
{ name: "--weekly-settings-toggle-bg", label: "Toggle background", default: "#f3f4f6" },
|
||||||
|
{ name: "--weekly-settings-toggle-active-bg", label: "Toggle active background", default: "#ffffff" },
|
||||||
|
{ name: "--weekly-settings-toggle-text", label: "Toggle text", default: "#6b7280" },
|
||||||
|
{ name: "--weekly-settings-toggle-active-text", label: "Toggle active text", default: "#111827" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Design Token Aliases",
|
||||||
|
vars: [
|
||||||
|
{ name: "--bg", label: "Background (alias)", default: "#ffffff" },
|
||||||
|
{ name: "--paper", label: "Paper (alias)", default: "#ffffff" },
|
||||||
|
{ name: "--ink", label: "Text (alias)", default: "#000000" },
|
||||||
|
{ name: "--ink-2", label: "Text variant 2", default: "#2a2a28" },
|
||||||
|
{ name: "--ink-3", label: "Text light (alias)", default: "#767676" },
|
||||||
|
{ name: "--ink-4", label: "Text variant 4", default: "#a8a8a2" },
|
||||||
|
{ name: "--ink-5", label: "Text variant 5", default: "#c8c8c2" },
|
||||||
|
{ name: "--accent", label: "Accent (alias)", default: "#009a9a" },
|
||||||
|
{ name: "--line", label: "Border (alias)", default: "#cccccc" },
|
||||||
|
{ name: "--line-soft", label: "Border (soft)", default: "#f3f3f0" },
|
||||||
|
{ name: "--weekend", label: "Weekend", default: "#dc2626" },
|
||||||
|
{ name: "--tasks-bg", label: "Tasks background", default: "#f4f2ee" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Event Colors — Default",
|
||||||
|
vars: [
|
||||||
|
{ name: "--ev-default-bg", label: "Background", default: "#eef2f7" },
|
||||||
|
{ name: "--ev-default-border", label: "Border", default: "#6c87a8" },
|
||||||
|
{ name: "--ev-default-title", label: "Title", default: "#2b3f57" },
|
||||||
|
{ name: "--ev-default-meta", label: "Meta", default: "#6c87a8" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Event Colors — Family",
|
||||||
|
vars: [
|
||||||
|
{ name: "--ev-fam-bg", label: "Background", default: "#fbeef0" },
|
||||||
|
{ name: "--ev-fam-border", label: "Border", default: "#b85a6a" },
|
||||||
|
{ name: "--ev-fam-title", label: "Title", default: "#5a2530" },
|
||||||
|
{ name: "--ev-fam-meta", label: "Meta", default: "#99536a" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Event Colors — Finance",
|
||||||
|
vars: [
|
||||||
|
{ name: "--ev-fin-bg", label: "Background", default: "#f6f0e2" },
|
||||||
|
{ name: "--ev-fin-border", label: "Border", default: "#a88a3c" },
|
||||||
|
{ name: "--ev-fin-title", label: "Title", default: "#4a3a14" },
|
||||||
|
{ name: "--ev-fin-meta", label: "Meta", default: "#8a6f2a" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Event Colors — Development",
|
||||||
|
vars: [
|
||||||
|
{ name: "--ev-dev-bg", label: "Background", default: "#ecf3ed" },
|
||||||
|
{ name: "--ev-dev-border", label: "Border", default: "#5a7a4a" },
|
||||||
|
{ name: "--ev-dev-title", label: "Title", default: "#2c4527" },
|
||||||
|
{ name: "--ev-dev-meta", label: "Meta", default: "#5a7a4a" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "All-Day Chips",
|
||||||
|
vars: [
|
||||||
|
{ name: "--chip-default-bg", label: "Default", default: "#b8b8b0" },
|
||||||
|
{ name: "--chip-fam-bg", label: "Family", default: "#e88a8a" },
|
||||||
|
{ name: "--chip-special-bg", label: "Special background", default: "#f4d588" },
|
||||||
|
{ name: "--chip-special-text", label: "Special text", default: "#6b4f10" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Lets the user override any globals.css :root color variable individually. Overrides are
|
||||||
|
// merged into profile.customCssVars (a flat map) and spread onto the root container's inline
|
||||||
|
// style in WeeklyView, so they take precedence over every other color source (theme, per-field
|
||||||
|
// settings, etc.).
|
||||||
|
function CssVariablesEditor({
|
||||||
|
profile,
|
||||||
|
setProfile,
|
||||||
|
saveSetting,
|
||||||
|
}: {
|
||||||
|
profile: any;
|
||||||
|
setProfile: React.Dispatch<React.SetStateAction<any>>;
|
||||||
|
saveSetting: (key: string, value: any) => void;
|
||||||
|
}) {
|
||||||
|
const de = profile.language === "de";
|
||||||
|
const vars: Record<string, string> = profile.customCssVars || {};
|
||||||
|
const debounceTimer = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
|
const setVar = (name: string, value: string) => {
|
||||||
|
const next = { ...(profile.customCssVars || {}), [name]: value };
|
||||||
|
setProfile((p: any) => ({ ...p, customCssVars: next }));
|
||||||
|
if (debounceTimer.current) clearTimeout(debounceTimer.current);
|
||||||
|
debounceTimer.current = setTimeout(() => saveSetting("customCssVars", next), 500);
|
||||||
|
};
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
setProfile((p: any) => ({ ...p, customCssVars: {} }));
|
||||||
|
saveSetting("customCssVars", {});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: "var(--weekly-settings-item-bg)",
|
||||||
|
padding: "12px",
|
||||||
|
borderRadius: "8px",
|
||||||
|
marginBottom: "12px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "8px" }}>
|
||||||
|
<label style={{ fontSize: "0.85rem", fontWeight: 600, color: "var(--weekly-settings-label)" }}>
|
||||||
|
{de ? "CSS-Variablen" : "CSS Variables"}
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={reset}
|
||||||
|
style={{
|
||||||
|
fontSize: "0.7rem",
|
||||||
|
background: "none",
|
||||||
|
border: "1px solid var(--weekly-settings-input-border)",
|
||||||
|
borderRadius: "6px",
|
||||||
|
padding: "3px 8px",
|
||||||
|
cursor: "pointer",
|
||||||
|
color: "var(--weekly-settings-label)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{de ? "Zurücksetzen" : "Reset"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p style={{ fontSize: "0.72rem", color: "var(--weekly-settings-label)", margin: "0 0 10px", opacity: 0.8 }}>
|
||||||
|
{de
|
||||||
|
? "Passe jede Design-Farbvariable des Stylesheets einzeln an."
|
||||||
|
: "Fine-tune every design-token color used by the stylesheet."}
|
||||||
|
</p>
|
||||||
|
{CSS_VARIABLE_GROUPS.map((group) => (
|
||||||
|
<div key={group.title} style={{ marginBottom: "12px" }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: "0.7rem",
|
||||||
|
fontWeight: 600,
|
||||||
|
color: "var(--weekly-settings-title)",
|
||||||
|
marginBottom: "6px",
|
||||||
|
textTransform: "uppercase",
|
||||||
|
letterSpacing: "0.02em",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{group.title}
|
||||||
|
</div>
|
||||||
|
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "6px 12px" }}>
|
||||||
|
{group.vars.map((v) => (
|
||||||
|
<div
|
||||||
|
key={v.name}
|
||||||
|
title={v.name}
|
||||||
|
style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: "8px" }}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: "0.72rem",
|
||||||
|
color: "var(--weekly-settings-label)",
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{v.label}
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
value={vars[v.name] || v.default}
|
||||||
|
onChange={(e) => setVar(v.name, e.target.value)}
|
||||||
|
style={{
|
||||||
|
width: "32px",
|
||||||
|
height: "22px",
|
||||||
|
cursor: "pointer",
|
||||||
|
border: "1px solid var(--weekly-settings-input-border)",
|
||||||
|
borderRadius: "4px",
|
||||||
|
background: "transparent",
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Minimal ProjectIcon — resolves an icon name from the unified registry.
|
// Minimal ProjectIcon — resolves an icon name from the unified registry.
|
||||||
function ProjectIcon({ icon, size = 18, color }: { icon?: string | null; size?: number; color?: string }) {
|
function ProjectIcon({ icon, size = 18, color }: { icon?: string | null; size?: number; color?: string }) {
|
||||||
if (!icon) return <FontAwesomeIcon icon={faFolder} style={{ fontSize: size, color }} />;
|
if (!icon) return <FontAwesomeIcon icon={faFolder} style={{ fontSize: size, color }} />;
|
||||||
@ -3712,6 +3925,55 @@ function SettingsSidebar({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* CSS Variables */}
|
||||||
|
<CssVariablesEditor profile={profile} setProfile={setProfile} saveSetting={saveSetting} />
|
||||||
|
|
||||||
|
{/* Custom CSS */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: "var(--weekly-settings-item-bg)",
|
||||||
|
padding: "12px",
|
||||||
|
borderRadius: "8px",
|
||||||
|
marginBottom: "12px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
style={{
|
||||||
|
display: "block",
|
||||||
|
fontSize: "0.85rem",
|
||||||
|
fontWeight: 600,
|
||||||
|
color: "var(--weekly-settings-label)",
|
||||||
|
marginBottom: "4px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{profile.language === "de" ? "Eigenes CSS" : "Custom CSS"}
|
||||||
|
</label>
|
||||||
|
<p style={{ fontSize: "0.72rem", color: "var(--weekly-settings-label)", margin: "0 0 8px", opacity: 0.8 }}>
|
||||||
|
{profile.language === "de"
|
||||||
|
? "Wird nach dem Stylesheet geladen und kann jede Regel überschreiben."
|
||||||
|
: "Loaded after the stylesheet — can override any rule."}
|
||||||
|
</p>
|
||||||
|
<textarea
|
||||||
|
value={profile.customCss || ""}
|
||||||
|
onChange={(e) => saveFieldDebounced("customCss", e.target.value)}
|
||||||
|
rows={10}
|
||||||
|
spellCheck={false}
|
||||||
|
placeholder={".weekly-task-item.completed .weekly-task-text {\n color: #cccccc;\n}"}
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
fontFamily: "monospace",
|
||||||
|
fontSize: "0.75rem",
|
||||||
|
background: "var(--weekly-settings-input-bg)",
|
||||||
|
color: "var(--weekly-settings-text)",
|
||||||
|
border: "1px solid var(--weekly-settings-input-border)",
|
||||||
|
borderRadius: "6px",
|
||||||
|
padding: "8px",
|
||||||
|
resize: "vertical",
|
||||||
|
boxSizing: "border-box",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* All styling settings auto-save */}
|
{/* All styling settings auto-save */}
|
||||||
</div>
|
</div>
|
||||||
) : activeTab === "motivation" ? (
|
) : activeTab === "motivation" ? (
|
||||||
|
|||||||
@ -5499,6 +5499,7 @@ export default function WeeklyView() {
|
|||||||
"--weekly-past-color": activeTheme?.color8 || (darkMode
|
"--weekly-past-color": activeTheme?.color8 || (darkMode
|
||||||
? invertColor(profile.pastDayColor || "#a6a6a7")
|
? invertColor(profile.pastDayColor || "#a6a6a7")
|
||||||
: profile.pastDayColor || "#a6a6a7"),
|
: profile.pastDayColor || "#a6a6a7"),
|
||||||
|
...((profile as any).customCssVars || {}),
|
||||||
...(useLeftRail ? { paddingLeft: leftRailExpanded ? `${expandedRailWidth}px` : `${collapsedRailWidth}px`, transition: "padding-left 0.2s ease" } : {}),
|
...(useLeftRail ? { paddingLeft: leftRailExpanded ? `${expandedRailWidth}px` : `${collapsedRailWidth}px`, transition: "padding-left 0.2s ease" } : {}),
|
||||||
} as React.CSSProperties;
|
} as React.CSSProperties;
|
||||||
|
|
||||||
@ -5718,6 +5719,9 @@ export default function WeeklyView() {
|
|||||||
className={`weekly-container ${darkMode ? "dark-mode" : ""} font-size-${(profile.fontSize ?? "M").toLowerCase()} ${profile.viewStyle}-view ${profile.showTimeGrid ? "time-grid-on" : "time-grid-off"}${(() => { const hd = isMobile ? (isPortrait ? (profile.mobilePortraitHeaderDisplay || "current_day") : (profile.mobileLandscapeHeaderDisplay || profile.headerDisplay || "kw")) : (profile.headerDisplay || "kw"); return (viewDays === 1 && hd === "current_day") ? " header-current-day-single" : ""; })()}`}
|
className={`weekly-container ${darkMode ? "dark-mode" : ""} font-size-${(profile.fontSize ?? "M").toLowerCase()} ${profile.viewStyle}-view ${profile.showTimeGrid ? "time-grid-on" : "time-grid-off"}${(() => { const hd = isMobile ? (isPortrait ? (profile.mobilePortraitHeaderDisplay || "current_day") : (profile.mobileLandscapeHeaderDisplay || profile.headerDisplay || "kw")) : (profile.headerDisplay || "kw"); return (viewDays === 1 && hd === "current_day") ? " header-current-day-single" : ""; })()}`}
|
||||||
style={containerStyle}
|
style={containerStyle}
|
||||||
>
|
>
|
||||||
|
{(profile as any).customCss ? (
|
||||||
|
<style suppressHydrationWarning>{(profile as any).customCss}</style>
|
||||||
|
) : null}
|
||||||
{/* Visually hidden live region for screen reader announcements */}
|
{/* Visually hidden live region for screen reader announcements */}
|
||||||
<div
|
<div
|
||||||
role="status"
|
role="status"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user