Initialize configurable Quick Access module
Module Package Release / publish-packages (push) Successful in 12s
Module Package Release / publish-packages (push) Successful in 12s
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "@govoplan/quick-access-webui",
|
||||
"version": "0.1.18",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"module": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/index.ts",
|
||||
"import": "./src/index.ts"
|
||||
},
|
||||
"./styles/quick-access.css": "./src/styles/quick-access.css"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@govoplan/core-webui": "^0.1.18",
|
||||
"lucide-react": "^1.23.0",
|
||||
"react": ">=19.2.7 <20",
|
||||
"react-dom": ">=19.2.7 <20",
|
||||
"react-router": ">=8.3.0 <9"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@govoplan/core-webui": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import { apiFetch, type ApiSettings } from "@govoplan/core-webui";
|
||||
|
||||
export type QuickAccessPreference = {
|
||||
enabled?: boolean | null;
|
||||
forced?: boolean;
|
||||
order?: number | null;
|
||||
};
|
||||
|
||||
export type QuickAccessProfile = {
|
||||
scope_type: "system" | "tenant" | "user";
|
||||
tenant_id?: string | null;
|
||||
scope_id?: string | null;
|
||||
revision: number;
|
||||
etag: string;
|
||||
category_preferences: Record<string, QuickAccessPreference>;
|
||||
tool_preferences: Record<string, QuickAccessPreference>;
|
||||
stale_category_ids: string[];
|
||||
stale_tool_ids: string[];
|
||||
};
|
||||
|
||||
export type QuickAccessCategory = {
|
||||
id: string;
|
||||
label: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
order: number;
|
||||
};
|
||||
|
||||
export type QuickAccessTool = {
|
||||
id: string;
|
||||
module_id: string;
|
||||
category_id: string;
|
||||
label: string;
|
||||
description?: string | null;
|
||||
icon: string;
|
||||
surface_id: string;
|
||||
full_page_path?: string | null;
|
||||
required_all: string[];
|
||||
required_any: string[];
|
||||
order: number;
|
||||
default_enabled: boolean;
|
||||
modes: string[];
|
||||
};
|
||||
|
||||
export type QuickAccessCatalogue = {
|
||||
categories: QuickAccessCategory[];
|
||||
tools: QuickAccessTool[];
|
||||
};
|
||||
|
||||
export type EffectiveQuickAccessTool = QuickAccessTool & {
|
||||
enabled: boolean;
|
||||
forced: boolean;
|
||||
locked_by?: string | null;
|
||||
};
|
||||
|
||||
export type EffectiveQuickAccessCategory = QuickAccessCategory & {
|
||||
enabled: boolean;
|
||||
forced: boolean;
|
||||
locked_by?: string | null;
|
||||
tools: EffectiveQuickAccessTool[];
|
||||
};
|
||||
|
||||
export type EffectiveQuickAccess = {
|
||||
categories: EffectiveQuickAccessCategory[];
|
||||
diagnostics: string[];
|
||||
};
|
||||
|
||||
const profileEndpoints = {
|
||||
system: "/api/v1/quick-access/profiles/system",
|
||||
tenant: "/api/v1/quick-access/profiles/tenant",
|
||||
me: "/api/v1/quick-access/profiles/me"
|
||||
} as const;
|
||||
|
||||
export function loadQuickAccessCatalogue(
|
||||
settings: ApiSettings,
|
||||
includeAll = false
|
||||
): Promise<QuickAccessCatalogue> {
|
||||
const query = includeAll ? "?include_all=true" : "";
|
||||
return apiFetch(settings, `/api/v1/quick-access/catalogue${query}`);
|
||||
}
|
||||
|
||||
export function loadEffectiveQuickAccess(
|
||||
settings: ApiSettings,
|
||||
includeAll = false
|
||||
): Promise<EffectiveQuickAccess> {
|
||||
const query = includeAll ? "?include_all=true" : "";
|
||||
return apiFetch(settings, `/api/v1/quick-access/effective${query}`);
|
||||
}
|
||||
|
||||
export function loadQuickAccessProfile(
|
||||
settings: ApiSettings,
|
||||
scope: "system" | "tenant" | "me"
|
||||
): Promise<QuickAccessProfile> {
|
||||
return apiFetch(settings, profileEndpoints[scope]);
|
||||
}
|
||||
|
||||
export function saveQuickAccessProfile(
|
||||
settings: ApiSettings,
|
||||
scope: "system" | "tenant" | "me",
|
||||
profile: QuickAccessProfile,
|
||||
categoryPreferences: Record<string, QuickAccessPreference>,
|
||||
toolPreferences: Record<string, QuickAccessPreference>
|
||||
): Promise<QuickAccessProfile> {
|
||||
return apiFetch(settings, profileEndpoints[scope], {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"If-Match": profile.etag
|
||||
},
|
||||
body: JSON.stringify({
|
||||
base_revision: profile.revision,
|
||||
category_preferences: categoryPreferences,
|
||||
tool_preferences: toolPreferences
|
||||
})
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
import {
|
||||
CalendarDays,
|
||||
Files,
|
||||
ListChecks,
|
||||
MessagesSquare,
|
||||
Settings2,
|
||||
X,
|
||||
type LucideIcon
|
||||
} from "lucide-react";
|
||||
import { useEffect, useMemo, useRef, useState, type MouseEvent } from "react";
|
||||
import { Link, useLocation } from "react-router";
|
||||
import {
|
||||
DismissibleAlert,
|
||||
IconButton,
|
||||
LoadingFrame,
|
||||
usePlatformLanguage,
|
||||
usePlatformUiCapabilities,
|
||||
type QuickAccessRailProps,
|
||||
type QuickAccessToolsUiCapability
|
||||
} from "@govoplan/core-webui";
|
||||
import {
|
||||
loadEffectiveQuickAccess,
|
||||
type EffectiveQuickAccess,
|
||||
type EffectiveQuickAccessCategory
|
||||
} from "../api/quickAccess";
|
||||
|
||||
|
||||
const iconByCategory: Record<string, LucideIcon> = {
|
||||
work: ListChecks,
|
||||
calendar: CalendarDays,
|
||||
messages: MessagesSquare,
|
||||
files: Files
|
||||
};
|
||||
|
||||
export default function QuickAccessRail({ settings, auth, tools }: QuickAccessRailProps) {
|
||||
const [effective, setEffective] = useState<EffectiveQuickAccess | null>(null);
|
||||
const [activeCategoryId, setActiveCategoryId] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
const drawerRef = useRef<HTMLDivElement>(null);
|
||||
const triggerRef = useRef<HTMLButtonElement | null>(null);
|
||||
const location = useLocation();
|
||||
const contributions = usePlatformUiCapabilities<QuickAccessToolsUiCapability>("quickAccess.tools");
|
||||
const { translateText } = usePlatformLanguage();
|
||||
const renderers = useMemo(
|
||||
() => new Map(contributions.flatMap((entry) => entry.tools).map((tool) => [tool.id, tool])),
|
||||
[contributions]
|
||||
);
|
||||
const availableToolIds = useMemo(() => new Set(tools.map((tool) => tool.id)), [tools]);
|
||||
const categories = useMemo(
|
||||
() => (effective?.categories ?? []).map((category) => ({
|
||||
...category,
|
||||
tools: category.tools.filter((tool) => tool.enabled && availableToolIds.has(tool.id))
|
||||
})).filter((category) => category.enabled && category.tools.length > 0),
|
||||
[availableToolIds, effective]
|
||||
);
|
||||
const activeCategory = categories.find((category) => category.id === activeCategoryId) ?? null;
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
async function load() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await loadEffectiveQuickAccess(settings);
|
||||
if (!active) return;
|
||||
setEffective(result);
|
||||
setError("");
|
||||
} catch (reason) {
|
||||
if (active) setError(reason instanceof Error ? reason.message : "Quick Access could not be loaded.");
|
||||
} finally {
|
||||
if (active) setLoading(false);
|
||||
}
|
||||
}
|
||||
void load();
|
||||
const handleChanged = () => void load();
|
||||
window.addEventListener("govoplan:quick-access-changed", handleChanged);
|
||||
return () => {
|
||||
active = false;
|
||||
window.removeEventListener("govoplan:quick-access-changed", handleChanged);
|
||||
};
|
||||
}, [auth.user.account_id, settings.accessToken, settings.apiBaseUrl, settings.apiKey]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeCategoryId && !categories.some((category) => category.id === activeCategoryId)) {
|
||||
setActiveCategoryId(null);
|
||||
}
|
||||
}, [activeCategoryId, categories]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeCategoryId) return;
|
||||
function onKeyDown(event: KeyboardEvent) {
|
||||
if (event.key === "Escape") closeDrawer();
|
||||
}
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
window.requestAnimationFrame(() => {
|
||||
drawerRef.current
|
||||
?.querySelector<HTMLElement>("button, a[href], input, select, textarea")
|
||||
?.focus();
|
||||
});
|
||||
return () => window.removeEventListener("keydown", onKeyDown);
|
||||
}, [activeCategoryId]);
|
||||
|
||||
useEffect(() => {
|
||||
setActiveCategoryId(null);
|
||||
}, [location.pathname, location.search]);
|
||||
|
||||
if (!loading && categories.length === 0 && !error) return null;
|
||||
|
||||
function closeDrawer(restoreFocus = true) {
|
||||
setActiveCategoryId(null);
|
||||
if (restoreFocus) {
|
||||
window.requestAnimationFrame(() => triggerRef.current?.focus());
|
||||
}
|
||||
}
|
||||
|
||||
function toggleCategory(
|
||||
category: EffectiveQuickAccessCategory,
|
||||
event: MouseEvent<HTMLButtonElement>
|
||||
) {
|
||||
if (activeCategoryId === category.id) {
|
||||
closeDrawer();
|
||||
return;
|
||||
}
|
||||
triggerRef.current = event.currentTarget;
|
||||
setActiveCategoryId(category.id);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<aside className="quick-access-rail" aria-label="i18n:govoplan-quick-access.quick_access">
|
||||
<div className="quick-access-rail-tools">
|
||||
{loading ? <span className="quick-access-rail-loading" aria-label="i18n:govoplan-quick-access.loading" /> : null}
|
||||
{categories.map((category) => {
|
||||
const Icon = iconByCategory[category.id] ?? ListChecks;
|
||||
const label = translateText(category.label);
|
||||
return (
|
||||
<button
|
||||
key={category.id}
|
||||
type="button"
|
||||
className={`quick-access-rail-button${activeCategoryId === category.id ? " active" : ""}`}
|
||||
title={label}
|
||||
aria-label={label}
|
||||
aria-expanded={activeCategoryId === category.id}
|
||||
aria-controls="quick-access-drawer"
|
||||
onClick={(event) => toggleCategory(category, event)}
|
||||
>
|
||||
<Icon size={20} aria-hidden="true" />
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<Link
|
||||
className="quick-access-rail-button quick-access-settings-link"
|
||||
to="/settings?section=quick-access"
|
||||
title={translateText("i18n:govoplan-quick-access.configure")}
|
||||
aria-label={translateText("i18n:govoplan-quick-access.configure")}
|
||||
>
|
||||
<Settings2 size={19} aria-hidden="true" />
|
||||
</Link>
|
||||
</aside>
|
||||
|
||||
{activeCategory ? (
|
||||
<div
|
||||
id="quick-access-drawer"
|
||||
className="quick-access-drawer"
|
||||
role="dialog"
|
||||
aria-modal="false"
|
||||
aria-label={translateText(activeCategory.label)}
|
||||
ref={drawerRef}
|
||||
>
|
||||
<header className="quick-access-drawer-header">
|
||||
<div>
|
||||
<strong>{translateText(activeCategory.label)}</strong>
|
||||
<small>{translateText(activeCategory.description)}</small>
|
||||
</div>
|
||||
<IconButton label="i18n:govoplan-quick-access.close" icon={<X size={18} />} variant="ghost" onClick={() => closeDrawer()} />
|
||||
</header>
|
||||
<div className="quick-access-drawer-content">
|
||||
{error ? <DismissibleAlert tone="warning" resetKey={error}>{error}</DismissibleAlert> : null}
|
||||
<LoadingFrame loading={loading} label="i18n:govoplan-quick-access.loading">
|
||||
{activeCategory.tools.map((tool) => {
|
||||
const renderer = renderers.get(tool.id);
|
||||
return (
|
||||
<section className="quick-access-tool" key={tool.id} data-tool-id={tool.id}>
|
||||
<div className="quick-access-tool-heading">
|
||||
<div>
|
||||
<strong>{translateText(tool.label)}</strong>
|
||||
{tool.description ? <small>{translateText(tool.description)}</small> : null}
|
||||
</div>
|
||||
{tool.full_page_path ? <Link to={tool.full_page_path} onClick={() => closeDrawer(false)}>i18n:govoplan-quick-access.open_full_page</Link> : null}
|
||||
</div>
|
||||
{renderer
|
||||
? renderer.render({ settings, auth, close: () => closeDrawer(), active: true })
|
||||
: <p className="quick-access-tool-unavailable">i18n:govoplan-quick-access.compact_view_unavailable</p>}
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
</LoadingFrame>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,370 @@
|
||||
import { ArrowDown, ArrowUp, RotateCcw, Save } from "lucide-react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
DismissibleAlert,
|
||||
IconButton,
|
||||
i18nMessage,
|
||||
LoadingFrame,
|
||||
SegmentedControl,
|
||||
ToggleSwitch,
|
||||
usePlatformLanguage,
|
||||
useUnsavedDraftGuard,
|
||||
type ApiSettings,
|
||||
type AuthInfo
|
||||
} from "@govoplan/core-webui";
|
||||
import {
|
||||
loadEffectiveQuickAccess,
|
||||
loadQuickAccessCatalogue,
|
||||
loadQuickAccessProfile,
|
||||
saveQuickAccessProfile,
|
||||
type EffectiveQuickAccess,
|
||||
type QuickAccessCatalogue,
|
||||
type QuickAccessPreference,
|
||||
type QuickAccessProfile
|
||||
} from "../../api/quickAccess";
|
||||
|
||||
|
||||
type ProfileScope = "system" | "tenant" | "me";
|
||||
type Draft = {
|
||||
categories: Record<string, QuickAccessPreference>;
|
||||
tools: Record<string, QuickAccessPreference>;
|
||||
};
|
||||
type AvailabilityMode = "inherit" | "available" | "blocked" | "forced";
|
||||
|
||||
const EMPTY_DRAFT: Draft = { categories: {}, tools: {} };
|
||||
|
||||
export default function QuickAccessSettingsPanel({
|
||||
settings,
|
||||
auth,
|
||||
scope,
|
||||
canWrite
|
||||
}: {
|
||||
settings: ApiSettings;
|
||||
auth: AuthInfo;
|
||||
scope: ProfileScope;
|
||||
canWrite: boolean;
|
||||
}) {
|
||||
const [catalogue, setCatalogue] = useState<QuickAccessCatalogue | null>(null);
|
||||
const [effective, setEffective] = useState<EffectiveQuickAccess | null>(null);
|
||||
const [profile, setProfile] = useState<QuickAccessProfile | null>(null);
|
||||
const [draft, setDraft] = useState<Draft>(EMPTY_DRAFT);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [message, setMessage] = useState("");
|
||||
const [messageTone, setMessageTone] = useState<"success" | "warning">("success");
|
||||
const { translateText } = usePlatformLanguage();
|
||||
const isPersonal = scope === "me";
|
||||
const dirty = useMemo(
|
||||
() => profile !== null && JSON.stringify(draft) !== JSON.stringify({
|
||||
categories: profile.category_preferences,
|
||||
tools: profile.tool_preferences
|
||||
}),
|
||||
[draft, profile]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
void load();
|
||||
}, [auth.user.account_id, scope, settings.accessToken, settings.apiBaseUrl, settings.apiKey]);
|
||||
|
||||
useUnsavedDraftGuard({
|
||||
dirty,
|
||||
onSave: save,
|
||||
onDiscard: reset
|
||||
});
|
||||
|
||||
async function load() {
|
||||
setLoading(true);
|
||||
setMessage("");
|
||||
const includeAll = scope !== "me";
|
||||
try {
|
||||
const [nextCatalogue, nextProfile, nextEffective] = await Promise.all([
|
||||
loadQuickAccessCatalogue(settings, includeAll),
|
||||
loadQuickAccessProfile(settings, scope),
|
||||
loadEffectiveQuickAccess(settings, includeAll)
|
||||
]);
|
||||
setCatalogue(nextCatalogue);
|
||||
setProfile(nextProfile);
|
||||
setEffective(nextEffective);
|
||||
setDraft({
|
||||
categories: nextProfile.category_preferences,
|
||||
tools: nextProfile.tool_preferences
|
||||
});
|
||||
} catch (reason) {
|
||||
setMessageTone("warning");
|
||||
setMessage(reason instanceof Error ? reason.message : "i18n:govoplan-quick-access.load_failed");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function save(): Promise<boolean> {
|
||||
if (!profile || !canWrite) return false;
|
||||
setSaving(true);
|
||||
setMessage("");
|
||||
try {
|
||||
const next = await saveQuickAccessProfile(
|
||||
settings,
|
||||
scope,
|
||||
profile,
|
||||
draft.categories,
|
||||
draft.tools
|
||||
);
|
||||
setProfile(next);
|
||||
setDraft({ categories: next.category_preferences, tools: next.tool_preferences });
|
||||
setMessageTone("success");
|
||||
setMessage("i18n:govoplan-quick-access.saved");
|
||||
window.dispatchEvent(new CustomEvent("govoplan:quick-access-changed"));
|
||||
return true;
|
||||
} catch (reason) {
|
||||
setMessageTone("warning");
|
||||
setMessage(reason instanceof Error ? reason.message : "i18n:govoplan-quick-access.save_failed");
|
||||
return false;
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
if (!profile) return;
|
||||
setDraft({
|
||||
categories: profile.category_preferences,
|
||||
tools: profile.tool_preferences
|
||||
});
|
||||
}
|
||||
|
||||
function setPreference(
|
||||
group: "categories" | "tools",
|
||||
id: string,
|
||||
preference: QuickAccessPreference | null
|
||||
) {
|
||||
setDraft((current) => {
|
||||
const nextGroup = { ...current[group] };
|
||||
if (preference === null) delete nextGroup[id];
|
||||
else nextGroup[id] = preference;
|
||||
return { ...current, [group]: nextGroup };
|
||||
});
|
||||
}
|
||||
|
||||
function move(
|
||||
group: "categories" | "tools",
|
||||
ids: string[],
|
||||
id: string,
|
||||
direction: -1 | 1
|
||||
) {
|
||||
const currentIndex = ids.indexOf(id);
|
||||
const targetIndex = currentIndex + direction;
|
||||
if (currentIndex < 0 || targetIndex < 0 || targetIndex >= ids.length) return;
|
||||
const reordered = [...ids];
|
||||
[reordered[currentIndex], reordered[targetIndex]] = [reordered[targetIndex], reordered[currentIndex]];
|
||||
setDraft((current) => ({
|
||||
...current,
|
||||
[group]: Object.fromEntries(reordered.map((itemId, index) => [
|
||||
itemId,
|
||||
{ ...(current[group][itemId] ?? {}), order: (index + 1) * 10 }
|
||||
]))
|
||||
}));
|
||||
}
|
||||
|
||||
const categories = useMemo(
|
||||
() => [...(catalogue?.categories ?? [])].sort((left, right) =>
|
||||
(draft.categories[left.id]?.order ?? left.order) - (draft.categories[right.id]?.order ?? right.order)
|
||||
),
|
||||
[catalogue, draft.categories]
|
||||
);
|
||||
const tools = useMemo(
|
||||
() => [...(catalogue?.tools ?? [])].sort((left, right) => {
|
||||
if (left.category_id !== right.category_id) return left.category_id.localeCompare(right.category_id);
|
||||
return (draft.tools[left.id]?.order ?? left.order) - (draft.tools[right.id]?.order ?? right.order);
|
||||
}),
|
||||
[catalogue, draft.tools]
|
||||
);
|
||||
const lockedCategories = new Map(
|
||||
(effective?.categories ?? []).filter((item) => item.locked_by).map((item) => [item.id, item.locked_by])
|
||||
);
|
||||
const lockedTools = new Map(
|
||||
(effective?.categories ?? []).flatMap((category) => category.tools).filter((item) => item.locked_by).map((item) => [item.id, item.locked_by])
|
||||
);
|
||||
const categoryIds = categories.map((item) => item.id);
|
||||
|
||||
return (
|
||||
<div className="quick-access-settings">
|
||||
<div className="quick-access-settings-heading">
|
||||
<div>
|
||||
<h2>i18n:govoplan-quick-access.settings_title</h2>
|
||||
<p>{isPersonal ? "i18n:govoplan-quick-access.personal_help" : "i18n:govoplan-quick-access.admin_help"}</p>
|
||||
</div>
|
||||
<div className="quick-access-settings-actions">
|
||||
<Button variant="secondary" icon={<RotateCcw size={16} />} disabled={!dirty || saving} onClick={reset}>i18n:govoplan-quick-access.discard</Button>
|
||||
<Button variant="primary" icon={<Save size={16} />} disabled={!dirty || saving || !canWrite} onClick={() => void save()}>i18n:govoplan-quick-access.save</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{message ? <DismissibleAlert tone={messageTone} resetKey={message}>{message}</DismissibleAlert> : null}
|
||||
{profile && (profile.stale_category_ids.length || profile.stale_tool_ids.length) ? (
|
||||
<DismissibleAlert tone="info" resetKey={`${profile.stale_category_ids.join(",")}:${profile.stale_tool_ids.join(",")}`}>
|
||||
i18n:govoplan-quick-access.stale_preferences_retained
|
||||
</DismissibleAlert>
|
||||
) : null}
|
||||
|
||||
<LoadingFrame loading={loading} label="i18n:govoplan-quick-access.loading">
|
||||
<section className="quick-access-settings-section">
|
||||
<h3>i18n:govoplan-quick-access.categories</h3>
|
||||
<div className="quick-access-preference-list">
|
||||
{categories.map((category, index) => (
|
||||
<PreferenceRow
|
||||
key={category.id}
|
||||
id={category.id}
|
||||
label={translateText(category.label)}
|
||||
description={translateText(category.description)}
|
||||
preference={draft.categories[category.id]}
|
||||
isPersonal={isPersonal}
|
||||
lockedBy={editableConstraintSource(scope, lockedCategories.get(category.id))}
|
||||
disabled={!canWrite || saving}
|
||||
onChange={(value) => setPreference("categories", category.id, value)}
|
||||
onMoveUp={() => move("categories", categoryIds, category.id, -1)}
|
||||
onMoveDown={() => move("categories", categoryIds, category.id, 1)}
|
||||
first={index === 0}
|
||||
last={index === categories.length - 1}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="quick-access-settings-section">
|
||||
<h3>i18n:govoplan-quick-access.registered_tools</h3>
|
||||
<div className="quick-access-preference-list">
|
||||
{categories.flatMap((category) => {
|
||||
const categoryTools = tools.filter((tool) => tool.category_id === category.id);
|
||||
const toolIds = categoryTools.map((tool) => tool.id);
|
||||
return categoryTools.map((tool, index) => (
|
||||
<PreferenceRow
|
||||
key={tool.id}
|
||||
id={tool.id}
|
||||
label={translateText(tool.label)}
|
||||
description={`${translateText(category.label)} · ${tool.module_id}${tool.description ? ` · ${translateText(tool.description)}` : ""}`}
|
||||
preference={draft.tools[tool.id]}
|
||||
defaultEnabled={tool.default_enabled}
|
||||
isPersonal={isPersonal}
|
||||
lockedBy={editableConstraintSource(scope, lockedTools.get(tool.id))}
|
||||
disabled={!canWrite || saving}
|
||||
onChange={(value) => setPreference("tools", tool.id, value)}
|
||||
onMoveUp={() => move("tools", toolIds, tool.id, -1)}
|
||||
onMoveDown={() => move("tools", toolIds, tool.id, 1)}
|
||||
first={index === 0}
|
||||
last={index === categoryTools.length - 1}
|
||||
/>
|
||||
));
|
||||
})}
|
||||
{!tools.length ? <p className="quick-access-empty">i18n:govoplan-quick-access.no_registered_tools</p> : null}
|
||||
</div>
|
||||
</section>
|
||||
</LoadingFrame>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function PreferenceRow({
|
||||
id,
|
||||
label,
|
||||
description,
|
||||
preference,
|
||||
defaultEnabled = true,
|
||||
isPersonal,
|
||||
lockedBy,
|
||||
disabled,
|
||||
onChange,
|
||||
onMoveUp,
|
||||
onMoveDown,
|
||||
first,
|
||||
last
|
||||
}: {
|
||||
id: string;
|
||||
label: string;
|
||||
description: string;
|
||||
preference?: QuickAccessPreference;
|
||||
defaultEnabled?: boolean;
|
||||
isPersonal: boolean;
|
||||
lockedBy?: string | null;
|
||||
disabled: boolean;
|
||||
onChange: (value: QuickAccessPreference | null) => void;
|
||||
onMoveUp: () => void;
|
||||
onMoveDown: () => void;
|
||||
first: boolean;
|
||||
last: boolean;
|
||||
}) {
|
||||
const mode = preferenceMode(preference);
|
||||
const rowDisabled = disabled || Boolean(lockedBy);
|
||||
return (
|
||||
<div className={`quick-access-preference-row${lockedBy ? " is-locked" : ""}`} data-preference-id={id}>
|
||||
<div className="quick-access-preference-copy">
|
||||
<strong>{label}</strong>
|
||||
<small>{description}</small>
|
||||
{lockedBy ? (
|
||||
<small>
|
||||
{i18nMessage("i18n:govoplan-quick-access.locked_by_value", {
|
||||
value0: lockedBy
|
||||
})}
|
||||
</small>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="quick-access-preference-controls">
|
||||
{isPersonal ? (
|
||||
<ToggleSwitch
|
||||
label={label}
|
||||
inactiveLabel="i18n:govoplan-quick-access.hidden"
|
||||
activeLabel="i18n:govoplan-quick-access.visible"
|
||||
checked={preference?.enabled ?? defaultEnabled}
|
||||
disabled={rowDisabled}
|
||||
onChange={(enabled) => onChange({ ...preference, enabled })}
|
||||
/>
|
||||
) : (
|
||||
<SegmentedControl
|
||||
ariaLabel={`${label} availability`}
|
||||
role="group"
|
||||
value={mode}
|
||||
disabled={rowDisabled}
|
||||
onChange={(value) => onChange(preferenceForMode(value, preference))}
|
||||
options={[
|
||||
{ id: "inherit", label: "i18n:govoplan-quick-access.inherit" },
|
||||
{ id: "available", label: "i18n:govoplan-quick-access.available" },
|
||||
{ id: "blocked", label: "i18n:govoplan-quick-access.blocked" },
|
||||
{ id: "forced", label: "i18n:govoplan-quick-access.forced" }
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
<div className="quick-access-order-buttons">
|
||||
<IconButton label="i18n:govoplan-quick-access.move_up" icon={<ArrowUp size={16} />} variant="ghost" disabled={disabled || first} onClick={onMoveUp} />
|
||||
<IconButton label="i18n:govoplan-quick-access.move_down" icon={<ArrowDown size={16} />} variant="ghost" disabled={disabled || last} onClick={onMoveDown} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function preferenceMode(preference?: QuickAccessPreference): AvailabilityMode {
|
||||
if (!preference || preference.enabled === null || preference.enabled === undefined) return "inherit";
|
||||
if (preference.enabled === false) return "blocked";
|
||||
return preference.forced ? "forced" : "available";
|
||||
}
|
||||
|
||||
function editableConstraintSource(
|
||||
scope: ProfileScope,
|
||||
source: string | null | undefined
|
||||
): string | null {
|
||||
if (!source || scope === "system") return null;
|
||||
if (scope === "tenant") return source === "system" ? source : null;
|
||||
return source;
|
||||
}
|
||||
|
||||
function preferenceForMode(
|
||||
mode: AvailabilityMode,
|
||||
current?: QuickAccessPreference
|
||||
): QuickAccessPreference | null {
|
||||
if (mode === "inherit") return current?.order === undefined ? null : { order: current.order };
|
||||
if (mode === "blocked") return { ...current, enabled: false, forced: false };
|
||||
if (mode === "forced") return { ...current, enabled: true, forced: true };
|
||||
return { ...current, enabled: true, forced: false };
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import type { PlatformTranslations } from "@govoplan/core-webui";
|
||||
|
||||
export const generatedTranslations: PlatformTranslations = {
|
||||
de: {
|
||||
"i18n:govoplan-quick-access.quick_access": "Schnellzugriff",
|
||||
"i18n:govoplan-quick-access.configure": "Schnellzugriff konfigurieren",
|
||||
"i18n:govoplan-quick-access.close": "Schließen",
|
||||
"i18n:govoplan-quick-access.loading": "Schnellzugriff wird geladen",
|
||||
"i18n:govoplan-quick-access.open_full_page": "Vollständige Seite öffnen",
|
||||
"i18n:govoplan-quick-access.compact_view_unavailable": "Die kompakte Ansicht ist nicht verfügbar. Verwenden Sie die vollständige Seite.",
|
||||
"i18n:govoplan-quick-access.settings_title": "Schnellzugriff",
|
||||
"i18n:govoplan-quick-access.personal_help": "Werkzeuge auswählen und ordnen, die neben der aktuellen Arbeit verfügbar bleiben.",
|
||||
"i18n:govoplan-quick-access.admin_help": "Verfügbarkeit, erzwungene Einträge und Standardreihenfolge für nachgeordnete Ebenen festlegen.",
|
||||
"i18n:govoplan-quick-access.categories": "Kategorien",
|
||||
"i18n:govoplan-quick-access.registered_tools": "Registrierte Werkzeuge",
|
||||
"i18n:govoplan-quick-access.no_registered_tools": "Derzeit registriert kein aktiviertes Modul ein Schnellzugriffswerkzeug.",
|
||||
"i18n:govoplan-quick-access.inherit": "Erben",
|
||||
"i18n:govoplan-quick-access.available": "Verfügbar",
|
||||
"i18n:govoplan-quick-access.blocked": "Gesperrt",
|
||||
"i18n:govoplan-quick-access.forced": "Erzwungen",
|
||||
"i18n:govoplan-quick-access.hidden": "Ausgeblendet",
|
||||
"i18n:govoplan-quick-access.visible": "Sichtbar",
|
||||
"i18n:govoplan-quick-access.move_up": "Nach oben",
|
||||
"i18n:govoplan-quick-access.move_down": "Nach unten",
|
||||
"i18n:govoplan-quick-access.save": "Speichern",
|
||||
"i18n:govoplan-quick-access.discard": "Verwerfen",
|
||||
"i18n:govoplan-quick-access.saved": "Die Schnellzugriffseinstellungen wurden gespeichert.",
|
||||
"i18n:govoplan-quick-access.load_failed": "Die Schnellzugriffseinstellungen konnten nicht geladen werden.",
|
||||
"i18n:govoplan-quick-access.save_failed": "Die Schnellzugriffseinstellungen konnten nicht gespeichert werden.",
|
||||
"i18n:govoplan-quick-access.stale_preferences_retained": "Einstellungen für derzeit nicht verfügbare Modulwerkzeuge bleiben erhalten und gelten wieder, sobald die Werkzeuge zurückkehren.",
|
||||
"i18n:govoplan-quick-access.category.work": "Arbeit",
|
||||
"i18n:govoplan-quick-access.category.work_description": "Aufgaben, Freigaben, Übergaben und andere zu bearbeitende Arbeit.",
|
||||
"i18n:govoplan-quick-access.category.calendar": "Kalender",
|
||||
"i18n:govoplan-quick-access.category.calendar_description": "Termine, Verfügbarkeit und zeitgebundene Verpflichtungen.",
|
||||
"i18n:govoplan-quick-access.category.messages": "Nachrichten",
|
||||
"i18n:govoplan-quick-access.category.messages_description": "Mail, Postfach und künftige Gesprächskanäle in einer Einblendung.",
|
||||
"i18n:govoplan-quick-access.category.files": "Dateien",
|
||||
"i18n:govoplan-quick-access.category.files_description": "Aktuelle und kontextbezogene Dateien, ohne die laufende Aufgabe zu verlassen.",
|
||||
"i18n:govoplan-quick-access.locked_by_value": "Durch {value0} festgelegt"
|
||||
},
|
||||
en: {
|
||||
"i18n:govoplan-quick-access.quick_access": "Quick Access",
|
||||
"i18n:govoplan-quick-access.configure": "Configure Quick Access",
|
||||
"i18n:govoplan-quick-access.close": "Close",
|
||||
"i18n:govoplan-quick-access.loading": "Loading Quick Access",
|
||||
"i18n:govoplan-quick-access.open_full_page": "Open full page",
|
||||
"i18n:govoplan-quick-access.compact_view_unavailable": "The compact view is unavailable. Use the full page.",
|
||||
"i18n:govoplan-quick-access.settings_title": "Quick Access",
|
||||
"i18n:govoplan-quick-access.personal_help": "Choose and order the tools kept beside your current work.",
|
||||
"i18n:govoplan-quick-access.admin_help": "Set availability, forced items, and default ordering for lower scopes.",
|
||||
"i18n:govoplan-quick-access.categories": "Categories",
|
||||
"i18n:govoplan-quick-access.registered_tools": "Registered tools",
|
||||
"i18n:govoplan-quick-access.no_registered_tools": "No enabled module currently registers a Quick Access tool.",
|
||||
"i18n:govoplan-quick-access.inherit": "Inherit",
|
||||
"i18n:govoplan-quick-access.available": "Available",
|
||||
"i18n:govoplan-quick-access.blocked": "Blocked",
|
||||
"i18n:govoplan-quick-access.forced": "Forced",
|
||||
"i18n:govoplan-quick-access.hidden": "Hidden",
|
||||
"i18n:govoplan-quick-access.visible": "Visible",
|
||||
"i18n:govoplan-quick-access.move_up": "Move up",
|
||||
"i18n:govoplan-quick-access.move_down": "Move down",
|
||||
"i18n:govoplan-quick-access.save": "Save",
|
||||
"i18n:govoplan-quick-access.discard": "Discard",
|
||||
"i18n:govoplan-quick-access.saved": "Quick Access settings were saved.",
|
||||
"i18n:govoplan-quick-access.load_failed": "Quick Access settings could not be loaded.",
|
||||
"i18n:govoplan-quick-access.save_failed": "Quick Access settings could not be saved.",
|
||||
"i18n:govoplan-quick-access.stale_preferences_retained": "Preferences for currently unavailable module tools are retained and will apply if those tools return.",
|
||||
"i18n:govoplan-quick-access.category.work": "Work",
|
||||
"i18n:govoplan-quick-access.category.work_description": "Tasks, approvals, handoffs, and other work requiring attention.",
|
||||
"i18n:govoplan-quick-access.category.calendar": "Calendar",
|
||||
"i18n:govoplan-quick-access.category.calendar_description": "Schedule, availability, and time-bound commitments.",
|
||||
"i18n:govoplan-quick-access.category.messages": "Messages",
|
||||
"i18n:govoplan-quick-access.category.messages_description": "Mail, Postbox, and future conversational channels in one overlay.",
|
||||
"i18n:govoplan-quick-access.category.files": "Files",
|
||||
"i18n:govoplan-quick-access.category.files_description": "Recent and contextual files without leaving the current task.",
|
||||
"i18n:govoplan-quick-access.locked_by_value": "Set by {value0}"
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default, quickAccessModule } from "./module";
|
||||
export * from "./api/quickAccess";
|
||||
@@ -0,0 +1,98 @@
|
||||
import { createElement, lazy } from "react";
|
||||
import type {
|
||||
AdminSectionsUiCapability,
|
||||
PlatformWebModule,
|
||||
QuickAccessRuntimeUiCapability,
|
||||
SettingsSectionsUiCapability
|
||||
} from "@govoplan/core-webui";
|
||||
import QuickAccessRail from "./components/QuickAccessRail";
|
||||
import { generatedTranslations } from "./i18n/generatedTranslations";
|
||||
import "./styles/quick-access.css";
|
||||
|
||||
|
||||
const QuickAccessSettingsPanel = lazy(
|
||||
() => import("./features/settings/QuickAccessSettingsPanel")
|
||||
);
|
||||
|
||||
const runtime: QuickAccessRuntimeUiCapability = {
|
||||
Rail: QuickAccessRail
|
||||
};
|
||||
|
||||
const settingsSections: SettingsSectionsUiCapability = {
|
||||
sections: [
|
||||
{
|
||||
id: "quick-access",
|
||||
label: "i18n:govoplan-quick-access.quick_access",
|
||||
group: "ui",
|
||||
order: 30,
|
||||
surfaceId: "quick_access.settings.personal",
|
||||
allOf: ["quick_access:profile:read"],
|
||||
render: ({ settings, auth }) => createElement(QuickAccessSettingsPanel, {
|
||||
settings,
|
||||
auth,
|
||||
scope: "me",
|
||||
canWrite: auth.scopes.includes("quick_access:profile:write")
|
||||
})
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const adminSections: AdminSectionsUiCapability = {
|
||||
sections: [
|
||||
{
|
||||
id: "system-quick-access",
|
||||
moduleId: "quick_access",
|
||||
kind: "settings",
|
||||
label: "i18n:govoplan-quick-access.quick_access",
|
||||
group: "SYSTEM",
|
||||
order: 31,
|
||||
surfaceId: "quick_access.admin.system",
|
||||
allOf: ["quick_access:system:admin"],
|
||||
render: ({ settings, auth }) => createElement(QuickAccessSettingsPanel, {
|
||||
settings,
|
||||
auth,
|
||||
scope: "system",
|
||||
canWrite: true
|
||||
})
|
||||
},
|
||||
{
|
||||
id: "tenant-quick-access",
|
||||
moduleId: "quick_access",
|
||||
kind: "settings",
|
||||
label: "i18n:govoplan-quick-access.quick_access",
|
||||
group: "TENANT",
|
||||
order: 31,
|
||||
surfaceId: "quick_access.admin.tenant",
|
||||
allOf: ["quick_access:profile:admin"],
|
||||
render: ({ settings, auth }) => createElement(QuickAccessSettingsPanel, {
|
||||
settings,
|
||||
auth,
|
||||
scope: "tenant",
|
||||
canWrite: true
|
||||
})
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export const quickAccessModule: PlatformWebModule = {
|
||||
id: "quick_access",
|
||||
label: "i18n:govoplan-quick-access.quick_access",
|
||||
version: "0.1.18",
|
||||
dependencies: ["access"],
|
||||
optionalDependencies: ["views", "policy"],
|
||||
translations: generatedTranslations,
|
||||
viewSurfaces: [
|
||||
{ id: "quick_access.rail", moduleId: "quick_access", kind: "quick_access", label: "i18n:govoplan-quick-access.quick_access", order: 5, required: true },
|
||||
{ id: "quick_access.drawer", moduleId: "quick_access", kind: "quick_access", label: "i18n:govoplan-quick-access.quick_access", parentId: "quick_access.rail", order: 10, required: true },
|
||||
{ id: "quick_access.settings.personal", moduleId: "quick_access", kind: "section", label: "i18n:govoplan-quick-access.quick_access", order: 20 },
|
||||
{ id: "quick_access.admin.tenant", moduleId: "quick_access", kind: "section", label: "i18n:govoplan-quick-access.quick_access", order: 30 },
|
||||
{ id: "quick_access.admin.system", moduleId: "quick_access", kind: "section", label: "i18n:govoplan-quick-access.quick_access", order: 40 }
|
||||
],
|
||||
uiCapabilities: {
|
||||
"quickAccess.runtime": runtime,
|
||||
"settings.sections": settingsSections,
|
||||
"admin.sections": adminSections
|
||||
}
|
||||
};
|
||||
|
||||
export default quickAccessModule;
|
||||
@@ -0,0 +1,265 @@
|
||||
.quick-access-rail,
|
||||
.quick-access-drawer,
|
||||
.quick-access-rail *,
|
||||
.quick-access-drawer * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.quick-access-rail {
|
||||
position: relative;
|
||||
z-index: 36;
|
||||
width: 48px;
|
||||
min-width: 48px;
|
||||
height: 100vh;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
border-left: var(--border-line);
|
||||
background: var(--panel-header);
|
||||
}
|
||||
|
||||
.quick-access-rail-tools {
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 3px;
|
||||
overflow-y: auto;
|
||||
padding: 6px 4px;
|
||||
}
|
||||
|
||||
.quick-access-rail-button {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
color: var(--muted);
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.quick-access-rail-button:hover {
|
||||
color: var(--text-strong);
|
||||
background: var(--sidebar-hover-bg);
|
||||
}
|
||||
|
||||
.quick-access-rail-button.active {
|
||||
color: var(--accent);
|
||||
background: var(--accent-soft);
|
||||
}
|
||||
|
||||
.quick-access-settings-link {
|
||||
margin: auto 4px 6px;
|
||||
}
|
||||
|
||||
.quick-access-rail-loading {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin: 11px auto;
|
||||
border: 2px solid var(--border);
|
||||
border-top-color: var(--accent);
|
||||
border-radius: 50%;
|
||||
animation: quick-access-spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
.quick-access-drawer {
|
||||
position: fixed;
|
||||
z-index: 35;
|
||||
top: 0;
|
||||
right: 48px;
|
||||
width: min(420px, calc(100vw - 104px));
|
||||
height: 100vh;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-left: var(--border-line);
|
||||
box-shadow: -12px 0 30px rgb(0 0 0 / 18%);
|
||||
background: var(--panel);
|
||||
}
|
||||
|
||||
.quick-access-drawer-header {
|
||||
min-height: 64px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
border-bottom: var(--border-line);
|
||||
padding: 10px 12px 10px 16px;
|
||||
background: var(--panel-header);
|
||||
}
|
||||
|
||||
.quick-access-drawer-header > div:first-child,
|
||||
.quick-access-tool-heading > div:first-child,
|
||||
.quick-access-preference-copy {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.quick-access-drawer-header small,
|
||||
.quick-access-tool-heading small,
|
||||
.quick-access-preference-copy small,
|
||||
.quick-access-settings-heading p,
|
||||
.quick-access-tool-unavailable,
|
||||
.quick-access-empty {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.quick-access-drawer-content {
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.quick-access-tool {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
border-bottom: var(--border-line);
|
||||
padding: 14px 16px 16px;
|
||||
}
|
||||
|
||||
.quick-access-tool-heading,
|
||||
.quick-access-settings-heading,
|
||||
.quick-access-preference-row,
|
||||
.quick-access-preference-controls,
|
||||
.quick-access-settings-actions,
|
||||
.quick-access-order-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.quick-access-tool-heading,
|
||||
.quick-access-settings-heading,
|
||||
.quick-access-preference-row {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.quick-access-tool-heading > a {
|
||||
flex: 0 0 auto;
|
||||
color: var(--accent);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.quick-access-settings {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.quick-access-settings-heading {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.quick-access-settings-heading h2,
|
||||
.quick-access-settings-section h3 {
|
||||
margin: 0;
|
||||
color: var(--text-strong);
|
||||
}
|
||||
|
||||
.quick-access-settings-section {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.quick-access-settings-section h3 {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.quick-access-preference-list {
|
||||
border: var(--border-line);
|
||||
background: var(--panel);
|
||||
}
|
||||
|
||||
.quick-access-preference-row {
|
||||
min-height: 62px;
|
||||
padding: 8px 10px 8px 12px;
|
||||
}
|
||||
|
||||
.quick-access-preference-row + .quick-access-preference-row {
|
||||
border-top: var(--border-line);
|
||||
}
|
||||
|
||||
.quick-access-preference-row:hover {
|
||||
background: var(--sidebar-hover-bg);
|
||||
}
|
||||
|
||||
.quick-access-preference-row.is-locked {
|
||||
background: var(--panel-soft);
|
||||
}
|
||||
|
||||
.quick-access-preference-controls {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.quick-access-preference-controls .segmented-control-option {
|
||||
min-height: 30px;
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.quick-access-order-buttons {
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.quick-access-empty {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
@keyframes quick-access-spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.quick-access-rail {
|
||||
position: fixed;
|
||||
z-index: 36;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 50px;
|
||||
flex-direction: row;
|
||||
border-top: var(--border-line);
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.quick-access-rail-tools {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.quick-access-settings-link {
|
||||
margin: 4px 6px 4px auto;
|
||||
}
|
||||
|
||||
.quick-access-drawer {
|
||||
right: 0;
|
||||
bottom: 50px;
|
||||
top: auto;
|
||||
width: 100%;
|
||||
height: min(70vh, 680px);
|
||||
border-top: var(--border-line);
|
||||
border-left: 0;
|
||||
box-shadow: 0 -12px 30px rgb(0 0 0 / 18%);
|
||||
}
|
||||
|
||||
.quick-access-preference-row {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.quick-access-preference-controls {
|
||||
justify-content: space-between;
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user