work
This commit is contained in:
@@ -54,7 +54,7 @@ use uuid::Uuid;
|
||||
schemas(
|
||||
schemas::LoginRequest,
|
||||
schemas::AccessTokenResponse,
|
||||
schemas::TenantSummary,
|
||||
schemas::TenantSnippet,
|
||||
schemas::TenantSelectionResponse,
|
||||
schemas::TenantSelectionRequest,
|
||||
schemas::TenantListResponse,
|
||||
@@ -605,12 +605,6 @@ pub mod schemas {
|
||||
pub tenant: TenantSnippet,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct TenantSummary {
|
||||
pub tenant_id: Uuid,
|
||||
pub slug: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct TenantSnippet {
|
||||
pub id: Uuid,
|
||||
@@ -620,7 +614,7 @@ pub mod schemas {
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct TenantSelectionResponse {
|
||||
pub access_token: String,
|
||||
pub tenants: Vec<TenantSummary>,
|
||||
pub tenants: Vec<TenantSnippet>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
|
||||
@@ -46,12 +46,6 @@ pub struct LoginResponse {
|
||||
pub tenant: TenantSnippet,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct TenantSummary {
|
||||
pub tenant_id: Uuid,
|
||||
pub slug: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct TenantSnippet {
|
||||
pub id: Uuid,
|
||||
@@ -61,7 +55,7 @@ pub struct TenantSnippet {
|
||||
#[derive(Serialize)]
|
||||
pub struct TenantSelectionResponse {
|
||||
pub access_token: String,
|
||||
pub tenants: Vec<TenantSummary>,
|
||||
pub tenants: Vec<TenantSnippet>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -132,8 +126,8 @@ pub async fn login(
|
||||
|
||||
let tenants = memberships
|
||||
.into_iter()
|
||||
.map(|(_, tenant)| TenantSummary {
|
||||
tenant_id: tenant.id,
|
||||
.map(|(_, tenant)| TenantSnippet {
|
||||
id: tenant.id,
|
||||
slug: tenant.slug,
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
pointer-events: auto;
|
||||
cursor: grab;
|
||||
transition: transform 0.16s ease, opacity 0.2s ease, box-shadow 0.2s ease;
|
||||
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.18);
|
||||
box-shadow: 2px 2px 4px color-mix(in oklch, black 18%, transparent);
|
||||
}
|
||||
|
||||
.tag-chip--draggable:active {
|
||||
@@ -146,14 +146,14 @@
|
||||
padding: 0.25em;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
color: #fff;
|
||||
background: oklch(0.22 0.06 260deg);
|
||||
color: var(--on-accent);
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease, opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.skeuo-card__nav-button:hover:not([disabled]) {
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
background: oklch(0.18 0.06 260deg);
|
||||
}
|
||||
|
||||
.skeuo-card__nav-button:disabled {
|
||||
@@ -192,7 +192,7 @@ body.skeuo-cursor-remove * {
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.18);
|
||||
box-shadow: 0 12px 32px color-mix(in oklch, black 18%, transparent);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
+114
-1
@@ -539,6 +539,11 @@ const AppLayout = () => {
|
||||
const [searchResults, setSearchResults] = useState(null);
|
||||
const [tags, setTags] = useState([]);
|
||||
const [correspondents, setCorrespondents] = useState([]);
|
||||
const [webdavTokens, setWebdavTokens] = useState([]);
|
||||
const [webdavTokensLoading, setWebdavTokensLoading] = useState(false);
|
||||
const [creatingWebdavToken, setCreatingWebdavToken] = useState(false);
|
||||
const [deletingWebdavTokenId, setDeletingWebdavTokenId] = useState(null);
|
||||
const [webdavTokenSecret, setWebdavTokenSecret] = useState(null);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [activeTagFilters, setActiveTagFilters] = useState([]);
|
||||
const [activeCorrespondentFilters, setActiveCorrespondentFilters] = useState([]);
|
||||
@@ -743,6 +748,11 @@ const AppLayout = () => {
|
||||
setSearchResults(null);
|
||||
setTags([]);
|
||||
setCorrespondents([]);
|
||||
setWebdavTokens([]);
|
||||
setWebdavTokensLoading(false);
|
||||
setCreatingWebdavToken(false);
|
||||
setDeletingWebdavTokenId(null);
|
||||
setWebdavTokenSecret(null);
|
||||
setSearchQuery('');
|
||||
setActiveTagFilters([]);
|
||||
setDropOverlayState({ active: false, folderName: DEFAULT_FOLDER_NAME });
|
||||
@@ -1734,6 +1744,97 @@ const AppLayout = () => {
|
||||
}
|
||||
}, [api, notifyApiError]);
|
||||
|
||||
const refreshWebdavTokens = useCallback(async () => {
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
setWebdavTokensLoading(true);
|
||||
try {
|
||||
const { data } = await api.get('/profile/webdav-tokens');
|
||||
setWebdavTokens(Array.isArray(data) ? data : []);
|
||||
} catch (error) {
|
||||
notifyApiError(error, 'Failed to load WebDAV tokens.');
|
||||
} finally {
|
||||
setWebdavTokensLoading(false);
|
||||
}
|
||||
}, [api, notifyApiError, token]);
|
||||
|
||||
const createWebdavToken = useCallback(
|
||||
async ({ label, expires_at } = {}) => {
|
||||
if (creatingWebdavToken) {
|
||||
return false;
|
||||
}
|
||||
setCreatingWebdavToken(true);
|
||||
try {
|
||||
const payload = {};
|
||||
if (label) {
|
||||
payload.label = label;
|
||||
}
|
||||
if (expires_at) {
|
||||
payload.expires_at = expires_at;
|
||||
}
|
||||
const { data } = await api.post('/profile/webdav-tokens', payload);
|
||||
if (data?.token_info) {
|
||||
setWebdavTokens((previous) => {
|
||||
const filtered = previous.filter((entry) => entry.id !== data.token_info.id);
|
||||
return [data.token_info, ...filtered];
|
||||
});
|
||||
} else {
|
||||
await refreshWebdavTokens();
|
||||
}
|
||||
if (data?.token) {
|
||||
setWebdavTokenSecret(data.token);
|
||||
}
|
||||
setStatusMessage('WebDAV token created.', 'success');
|
||||
return data;
|
||||
} catch (error) {
|
||||
notifyApiError(error, 'Failed to create WebDAV token.');
|
||||
return false;
|
||||
} finally {
|
||||
setCreatingWebdavToken(false);
|
||||
}
|
||||
},
|
||||
[api, creatingWebdavToken, notifyApiError, refreshWebdavTokens, setStatusMessage],
|
||||
);
|
||||
|
||||
const deleteWebdavToken = useCallback(
|
||||
async (tokenId) => {
|
||||
if (!tokenId) {
|
||||
return false;
|
||||
}
|
||||
setDeletingWebdavTokenId(tokenId);
|
||||
try {
|
||||
await api.delete(`/profile/webdav-tokens/${tokenId}`);
|
||||
setWebdavTokens((previous) => previous.filter((entry) => entry.id !== tokenId));
|
||||
setStatusMessage('WebDAV token revoked.', 'success');
|
||||
return true;
|
||||
} catch (error) {
|
||||
notifyApiError(error, 'Failed to revoke WebDAV token.');
|
||||
return false;
|
||||
} finally {
|
||||
setDeletingWebdavTokenId(null);
|
||||
}
|
||||
},
|
||||
[api, notifyApiError, setStatusMessage],
|
||||
);
|
||||
|
||||
const dismissCreatedWebdavToken = useCallback(() => {
|
||||
setWebdavTokenSecret(null);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSettingsModalOpen) {
|
||||
return;
|
||||
}
|
||||
refreshWebdavTokens();
|
||||
}, [isSettingsModalOpen, refreshWebdavTokens]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSettingsModalOpen) {
|
||||
setWebdavTokenSecret(null);
|
||||
}
|
||||
}, [isSettingsModalOpen]);
|
||||
|
||||
const handleTagUpdate = useCallback(
|
||||
async (tagId, changes) => {
|
||||
if (!tagId) {
|
||||
@@ -5404,7 +5505,19 @@ const AppLayout = () => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<SettingsModal open={isSettingsModalOpen} onClose={closeSettingsModal} />
|
||||
<SettingsModal
|
||||
open={isSettingsModalOpen}
|
||||
onClose={closeSettingsModal}
|
||||
tokens={webdavTokens}
|
||||
loading={webdavTokensLoading}
|
||||
creating={creatingWebdavToken}
|
||||
deletingId={deletingWebdavTokenId}
|
||||
onRefresh={refreshWebdavTokens}
|
||||
onCreate={createWebdavToken}
|
||||
onDelete={deleteWebdavToken}
|
||||
createdToken={webdavTokenSecret}
|
||||
onDismissCreatedToken={dismissCreatedWebdavToken}
|
||||
/>
|
||||
</div>
|
||||
</AppShellContext.Provider>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
import React, { useMemo, useState, useCallback, useEffect } from 'react';
|
||||
|
||||
const SECTIONS = [
|
||||
{
|
||||
id: 'webdav',
|
||||
label: 'WebDAV',
|
||||
},
|
||||
];
|
||||
|
||||
const SettingsModal = ({
|
||||
open,
|
||||
onClose,
|
||||
tokens = [],
|
||||
loading = false,
|
||||
creating = false,
|
||||
deletingId = null,
|
||||
onRefresh,
|
||||
onCreate,
|
||||
onDelete,
|
||||
createdToken = null,
|
||||
onDismissCreatedToken,
|
||||
}) => {
|
||||
const defaultSection = SECTIONS[0]?.id || 'webdav';
|
||||
const [activeSection, setActiveSection] = useState(defaultSection);
|
||||
const [newTokenLabel, setNewTokenLabel] = useState('');
|
||||
const [newTokenExpires, setNewTokenExpires] = useState('');
|
||||
const [formError, setFormError] = useState(null);
|
||||
|
||||
const handleBackdropClick = useCallback(() => {
|
||||
onClose?.();
|
||||
}, [onClose]);
|
||||
|
||||
const handleInnerClick = useCallback((event) => {
|
||||
event.stopPropagation();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setActiveSection(defaultSection);
|
||||
setNewTokenLabel('');
|
||||
setNewTokenExpires('');
|
||||
setFormError(null);
|
||||
}
|
||||
}, [open, defaultSection]);
|
||||
|
||||
const formatDateTime = useCallback((value) => {
|
||||
if (!value) {
|
||||
return '—';
|
||||
}
|
||||
const timestamp = new Date(value);
|
||||
if (Number.isNaN(timestamp.getTime())) {
|
||||
return value;
|
||||
}
|
||||
return timestamp.toLocaleString();
|
||||
}, []);
|
||||
|
||||
const handleRefresh = useCallback(() => {
|
||||
onRefresh?.();
|
||||
}, [onRefresh]);
|
||||
|
||||
const handleCopyToken = useCallback(() => {
|
||||
if (!createdToken) {
|
||||
return;
|
||||
}
|
||||
if (navigator?.clipboard?.writeText) {
|
||||
navigator.clipboard.writeText(createdToken).catch(() => {});
|
||||
}
|
||||
}, [createdToken]);
|
||||
|
||||
const handleDismissSecret = useCallback(() => {
|
||||
onDismissCreatedToken?.();
|
||||
}, [onDismissCreatedToken]);
|
||||
|
||||
const handleCreateToken = useCallback(
|
||||
async (event) => {
|
||||
event.preventDefault();
|
||||
setFormError(null);
|
||||
let normalizedLabel = newTokenLabel.trim();
|
||||
if (normalizedLabel.length === 0) {
|
||||
normalizedLabel = undefined;
|
||||
}
|
||||
|
||||
let normalizedExpires;
|
||||
if (newTokenExpires) {
|
||||
const parsed = new Date(newTokenExpires);
|
||||
if (Number.isNaN(parsed.getTime())) {
|
||||
setFormError('Enter a valid expiration date.');
|
||||
return;
|
||||
}
|
||||
normalizedExpires = parsed.toISOString();
|
||||
}
|
||||
|
||||
const result = await onCreate?.({
|
||||
label: normalizedLabel,
|
||||
expires_at: normalizedExpires,
|
||||
});
|
||||
|
||||
if (result !== false) {
|
||||
setNewTokenLabel('');
|
||||
setNewTokenExpires('');
|
||||
setFormError(null);
|
||||
}
|
||||
},
|
||||
[newTokenExpires, newTokenLabel, onCreate],
|
||||
);
|
||||
|
||||
const renderWebdavSection = useMemo(() => {
|
||||
const hasTokens = Array.isArray(tokens) && tokens.length > 0;
|
||||
|
||||
return (
|
||||
<div className="settings-section">
|
||||
<div className="settings-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="secondary"
|
||||
onClick={handleRefresh}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Refreshing…' : 'Refresh'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{createdToken ? (
|
||||
<div className="settings-notice">
|
||||
<p>
|
||||
Copy this token now; you will not be able to view it again after closing this window.
|
||||
</p>
|
||||
<pre className="token-display">{createdToken}</pre>
|
||||
<div className="settings-notice__actions">
|
||||
<button type="button" className="secondary" onClick={handleCopyToken}>
|
||||
Copy token
|
||||
</button>
|
||||
<button type="button" onClick={handleDismissSecret}>
|
||||
Dismiss
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<form className="settings-form" onSubmit={handleCreateToken}>
|
||||
<div className="settings-form__field">
|
||||
<label htmlFor="webdav-token-label">Label</label>
|
||||
<input
|
||||
id="webdav-token-label"
|
||||
type="text"
|
||||
value={newTokenLabel}
|
||||
onChange={(event) => setNewTokenLabel(event.target.value)}
|
||||
placeholder="Personal WebDAV token"
|
||||
/>
|
||||
</div>
|
||||
<div className="settings-form__field">
|
||||
<label htmlFor="webdav-token-expires">Expires at</label>
|
||||
<input
|
||||
id="webdav-token-expires"
|
||||
type="datetime-local"
|
||||
value={newTokenExpires}
|
||||
onChange={(event) => setNewTokenExpires(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="settings-form__actions">
|
||||
<button type="submit" disabled={creating}>
|
||||
{creating ? 'Creating…' : 'Create token'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{formError ? <p className="settings-form__error">{formError}</p> : null}
|
||||
|
||||
{loading && !hasTokens ? (
|
||||
<p className="settings-empty">Loading tokens…</p>
|
||||
) : null}
|
||||
|
||||
{!loading && !hasTokens ? (
|
||||
<p className="settings-empty">No WebDAV tokens yet.</p>
|
||||
) : null}
|
||||
|
||||
{hasTokens ? (
|
||||
<table className="settings-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Label</th>
|
||||
<th scope="col">Created</th>
|
||||
<th scope="col">Last used</th>
|
||||
<th scope="col">Expires</th>
|
||||
<th scope="col">Scopes</th>
|
||||
<th scope="col">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{tokens.map((token) => {
|
||||
const isRevoked = Boolean(token?.revoked_at);
|
||||
const scopes = Array.isArray(token?.scopes)
|
||||
? token.scopes.join(', ')
|
||||
: '—';
|
||||
return (
|
||||
<tr key={token.id} className={isRevoked ? 'is-revoked' : undefined}>
|
||||
<td>{token.label || '—'}</td>
|
||||
<td>{formatDateTime(token.created_at)}</td>
|
||||
<td>{formatDateTime(token.last_used_at)}</td>
|
||||
<td>{formatDateTime(token.expires_at)}</td>
|
||||
<td>{scopes || '—'}</td>
|
||||
<td className="settings-table__actions">
|
||||
{isRevoked ? (
|
||||
<span className="settings-status">Revoked</span>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="danger"
|
||||
onClick={() => onDelete?.(token.id)}
|
||||
disabled={deletingId === token.id}
|
||||
>
|
||||
{deletingId === token.id ? 'Revoking…' : 'Revoke'}
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}, [
|
||||
tokens,
|
||||
loading,
|
||||
createdToken,
|
||||
creating,
|
||||
deletingId,
|
||||
newTokenLabel,
|
||||
newTokenExpires,
|
||||
formError,
|
||||
formatDateTime,
|
||||
handleCopyToken,
|
||||
handleCreateToken,
|
||||
handleRefresh,
|
||||
onDelete,
|
||||
handleDismissSecret,
|
||||
]);
|
||||
|
||||
if (!open) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="modal-backdrop" role="presentation" onClick={handleBackdropClick}>
|
||||
<div
|
||||
className="modal modal--panel settings-modal"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="settings-modal-title"
|
||||
onClick={handleInnerClick}
|
||||
>
|
||||
<div className="panel-modal__header">
|
||||
<h3 id="settings-modal-title">Settings</h3>
|
||||
<button type="button" className="secondary" onClick={onClose}>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
<div className="settings-modal__body">
|
||||
<nav className="settings-modal__sidebar" aria-label="Settings sections">
|
||||
<ul>
|
||||
{SECTIONS.map((section) => (
|
||||
<li key={section.id}>
|
||||
<button
|
||||
type="button"
|
||||
className={section.id === activeSection ? 'active' : ''}
|
||||
onClick={() => setActiveSection(section.id)}
|
||||
>
|
||||
{section.label}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
<div className="settings-modal__content">
|
||||
{activeSection === 'webdav' ? renderWebdavSection : <p>Select a settings section.</p>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsModal;
|
||||
+214
-24
@@ -36,6 +36,18 @@
|
||||
--shadow-faint: color-mix(in oklch, black 6%, transparent);
|
||||
--shadow-soft: color-mix(in oklch, black 12%, transparent);
|
||||
--shadow-medium: color-mix(in oklch, black 18%, transparent);
|
||||
--shadow-strong: color-mix(in oklch, black 28%, transparent);
|
||||
--shadow-deep: color-mix(in oklch, black 55%, transparent);
|
||||
--shadow-pop: color-mix(in oklch, black 25%, transparent);
|
||||
--outline-subtle: color-mix(in oklch, black 6%, transparent);
|
||||
--overlay-dark: color-mix(in oklch, black 55%, transparent);
|
||||
--overlay-dim: color-mix(in oklch, oklch(0.28 0.01 calc(265deg + var(--warmth))) 20%, transparent);
|
||||
--overlay-darker: color-mix(in oklch, black 75%, transparent);
|
||||
--surface-danger-subtle: color-mix(in oklch, var(--danger) 12%, transparent);
|
||||
--overlay-accent-subtle: color-mix(in oklch, var(--accent) 12%, transparent);
|
||||
--border-strong: color-mix(in oklch, var(--fg) 24%, transparent);
|
||||
--surface-hover: color-mix(in oklch, white 10%, transparent);
|
||||
--overlay-accent-strong: color-mix(in oklch, var(--accent) 24%, transparent);
|
||||
--overlay-backdrop: color-mix(in oklch, oklch(0.22 0.015 calc(265deg + var(--warmth))) 45%, transparent);
|
||||
--overlay-shadow: color-mix(in oklch, oklch(0.22 0.015 calc(265deg + var(--warmth))) 18%, transparent);
|
||||
|
||||
@@ -57,9 +69,17 @@
|
||||
--sidebar-active-bg: color-mix(in oklch, var(--selection) 16%, transparent);
|
||||
--selection-ring: oklch(0.78 0.16 calc(260deg + var(--warmth)));
|
||||
--sidebar-active-pill-border: color-mix(in oklch, var(--accent) 64%, transparent);
|
||||
--link: var(--accent);
|
||||
--link-visited: color-mix(in oklch, var(--accent) 75%, var(--fg) 25%);
|
||||
--preview-nav-bg: oklch(0.18 0.05 calc(270deg + var(--warmth)));
|
||||
--preview-nav-bg-hover: oklch(0.14 0.05 calc(270deg + var(--warmth)));
|
||||
--preview-nav-fg: var(--on-accent);
|
||||
--text-on-dark: color-mix(in oklch, white 90%, transparent);
|
||||
--surface-ink-soft: color-mix(in oklch, var(--fg) 8%, transparent);
|
||||
|
||||
font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
font-size: 15px;
|
||||
--font-mono: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
||||
|
||||
--detail-panel-width: 30em;
|
||||
--documents-grid-title-size: 0.8rem;
|
||||
@@ -86,7 +106,7 @@ a {
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: color-mix(in oklch, var(--accent) 75%, var(--fg) 25%);
|
||||
color: var(--link-visited);
|
||||
}
|
||||
|
||||
a:hover,
|
||||
@@ -244,7 +264,7 @@ button.danger:hover:not([disabled]) {
|
||||
.preview-zoom-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(15, 23, 42, 0);
|
||||
background: var(--overlay-backdrop);
|
||||
transition: background 0.25s ease, opacity 0.25s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -283,7 +303,7 @@ button.danger:hover:not([disabled]) {
|
||||
justify-content: center;
|
||||
max-width: 95vw;
|
||||
max-height: 95vh;
|
||||
box-shadow: 0 32px 120px rgba(15, 23, 42, 0.55);
|
||||
box-shadow: 0 32px 120px var(--shadow-deep);
|
||||
}
|
||||
|
||||
.preview-zoom__scroll:focus {
|
||||
@@ -322,23 +342,23 @@ button.danger:hover:not([disabled]) {
|
||||
height: 2.8rem;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
background: rgba(0, 0, 0, 0.62);
|
||||
color: #fff;
|
||||
background: var(--preview-nav-bg);
|
||||
color: var(--preview-nav-fg);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
transition: background 0.15s ease, transform 0.15s ease;
|
||||
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.28);
|
||||
box-shadow: 0 12px 28px var(--shadow-strong);
|
||||
}
|
||||
|
||||
.preview-zoom__nav-button:hover {
|
||||
background: rgba(0, 0, 0, 0.82);
|
||||
background: var(--preview-nav-bg-hover);
|
||||
}
|
||||
|
||||
.preview-zoom__nav-button:focus-visible {
|
||||
outline: 2px solid var(--accent, #4f46e5);
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
@@ -604,9 +624,9 @@ button.danger:hover:not([disabled]) {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.25);
|
||||
box-shadow: 0 6px 12px var(--shadow-pop);
|
||||
overflow: hidden;
|
||||
background-color: rgba(32, 34, 40, 0.2);
|
||||
background-color: var(--overlay-dim);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -668,7 +688,7 @@ button.danger:hover:not([disabled]) {
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
color: var(--text-on-dark);
|
||||
}
|
||||
|
||||
.document-drag-preview__count {
|
||||
@@ -676,12 +696,12 @@ button.danger:hover:not([disabled]) {
|
||||
bottom: 4px;
|
||||
right: 4px;
|
||||
background-color: var(--accent);
|
||||
color: #fff;
|
||||
color: var(--on-accent);
|
||||
border-radius: 999px;
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.25);
|
||||
box-shadow: 0 4px 8px var(--shadow-pop);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -703,7 +723,7 @@ button.danger:hover:not([disabled]) {
|
||||
.preview-workspace__info {
|
||||
background: var(--surface-subtle);
|
||||
padding: 1rem;
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.06);
|
||||
box-shadow: inset 0 0 0 1px var(--outline-subtle);
|
||||
}
|
||||
|
||||
.preview-workspace__summary {
|
||||
@@ -1152,7 +1172,7 @@ button.danger:hover:not([disabled]) {
|
||||
}
|
||||
|
||||
.sidebar__title-button:focus-visible {
|
||||
outline: 2px solid var(--accent, #2563eb);
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
@@ -1215,12 +1235,12 @@ button.danger:hover:not([disabled]) {
|
||||
.menu button.menu__item.active,
|
||||
.menu .menu__item.active {
|
||||
font-weight: 600;
|
||||
color: var(--accent, #2563eb);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.menu button.menu__item:focus-visible,
|
||||
.menu .menu__item:focus-visible {
|
||||
outline: 2px solid var(--accent, #2563eb);
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
@@ -1235,7 +1255,7 @@ button.danger:hover:not([disabled]) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--accent, #2563eb);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.menu__active-indicator {
|
||||
@@ -1888,7 +1908,7 @@ button.danger:hover:not([disabled]) {
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
color: var(--accent, #2563eb);
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
@@ -1939,7 +1959,7 @@ button.danger:hover:not([disabled]) {
|
||||
}
|
||||
|
||||
.documents-panel tbody tr.document.tag-drop-target {
|
||||
background: var(--accent-soft, rgba(63, 106, 216, 0.12));
|
||||
background: var(--accent-soft);
|
||||
box-shadow: inset 0 0 0 2px var(--accent);
|
||||
}
|
||||
|
||||
@@ -2113,7 +2133,7 @@ button.danger:hover:not([disabled]) {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-family: var(--font-mono, 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
@@ -2354,7 +2374,7 @@ button.danger:hover:not([disabled]) {
|
||||
}
|
||||
|
||||
.preview-pane__nav-button:focus-visible {
|
||||
outline: 2px solid var(--accent, #2684ff);
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
@@ -2426,7 +2446,7 @@ button.danger:hover:not([disabled]) {
|
||||
border: 1px solid var(--border-muted, var(--border));
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-family: var(--font-mono, 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
@@ -2531,7 +2551,8 @@ form.inline {
|
||||
}
|
||||
|
||||
.modal--panel {
|
||||
width: min(80vw, 960px);
|
||||
width: 80vw;
|
||||
max-width: 960px;
|
||||
max-height: 90vh;
|
||||
padding: 0;
|
||||
border-radius: 12px;
|
||||
@@ -2539,6 +2560,175 @@ form.inline {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.settings-modal__body {
|
||||
display: grid;
|
||||
grid-template-columns: 12rem 1fr;
|
||||
gap: 1.5rem;
|
||||
height: 90vh;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.settings-modal__sidebar {
|
||||
border-right: 1px solid var(--border-muted, var(--border));
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.settings-modal__sidebar ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.settings-modal__sidebar button {
|
||||
width: 100%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 0.4rem;
|
||||
border: none;
|
||||
background: none;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
padding: 0.45rem 0.6rem;
|
||||
border-radius: 0.35rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.settings-modal__sidebar button:hover,
|
||||
.settings-modal__sidebar button:focus-visible {
|
||||
background: var(--sidebar-hover-bg);
|
||||
}
|
||||
|
||||
.settings-modal__sidebar button.active {
|
||||
background: var(--surface-soft, rgba(148, 163, 184, 0.18));
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.settings-modal__content {
|
||||
overflow-y: auto;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.settings-section h4 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.settings-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.settings-actions .secondary {
|
||||
min-width: 7rem;
|
||||
}
|
||||
|
||||
.settings-form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
align-items: flex-end;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.settings-form__field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
min-width: 14rem;
|
||||
}
|
||||
|
||||
.settings-form__field input[type='text'],
|
||||
.settings-form__field input[type='datetime-local'] {
|
||||
padding: 0.45rem 0.6rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.35rem;
|
||||
background: var(--surface-soft, rgba(148, 163, 184, 0.12));
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.settings-form__actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.settings-form__error {
|
||||
color: var(--danger, #d14343);
|
||||
font-size: 0.85rem;
|
||||
margin: 0 0 0.75rem;
|
||||
}
|
||||
|
||||
.settings-empty {
|
||||
margin: 1rem 0;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.settings-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.settings-table th,
|
||||
.settings-table td {
|
||||
padding: 0.55rem 0.75rem;
|
||||
border-bottom: 1px solid var(--border-muted, var(--border));
|
||||
text-align: left;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.settings-table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.settings-table__actions {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.settings-status {
|
||||
color: var(--muted);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.settings-table tr.is-revoked {
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.settings-notice {
|
||||
border: 1px solid var(--accent);
|
||||
background: rgba(37, 99, 235, 0.08);
|
||||
padding: 0.9rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.token-display {
|
||||
background: rgba(15, 23, 42, 0.08);
|
||||
padding: 0.5rem 0.65rem;
|
||||
border-radius: 0.35rem;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.95rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.settings-notice__actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.6rem;
|
||||
}
|
||||
|
||||
.settings-notice__actions button {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.panel-modal__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user