refactor: introduce dedicated identifier types for improved clarity and type safety
This commit is contained in:
+159
-160
@@ -31,6 +31,7 @@ import { getTagColorStyle } from '../utils/colors';
|
||||
import { useSidebarContext } from './SidebarContext';
|
||||
import { usePanelManager, usePanelResizeBindings } from '../app/PanelManagerContext';
|
||||
import type { StatusMessage } from '../hooks/documents/store/useDocumentsStore';
|
||||
import type { Identifier } from '../types/identifiers';
|
||||
|
||||
interface CommunityLink {
|
||||
label: string;
|
||||
@@ -60,7 +61,6 @@ const COMMUNITY_LINKS: CommunityLink[] = [
|
||||
},
|
||||
];
|
||||
|
||||
type Identifier = string | number;
|
||||
type FolderIdentifier = Identifier | 'root';
|
||||
|
||||
interface FolderTreeNode {
|
||||
@@ -528,94 +528,94 @@ const Sidebar: React.FC<SidebarProps> = ({
|
||||
const themeMenuSection =
|
||||
neutralHue != null
|
||||
? (
|
||||
<div className="menu__section">
|
||||
<div className="menu__heading menu__heading--with-actions">
|
||||
<span>Theme</span>
|
||||
<div className="menu__heading-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button"
|
||||
onClick={handleThemeModeToggle}
|
||||
aria-label={`Switch theme (next: ${nextThemeLabel})`}
|
||||
title={`Theme: ${themeModeLabel} (next: ${nextThemeLabel})`}
|
||||
>
|
||||
{themeModeIcon}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button"
|
||||
onClick={handleThemeAdjustmentsReset}
|
||||
aria-label="Reset theme adjustments"
|
||||
title="Reset theme adjustments"
|
||||
>
|
||||
<RestoreIcon size={16} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="menu__section">
|
||||
<div className="menu__heading menu__heading--with-actions">
|
||||
<span>Theme</span>
|
||||
<div className="menu__heading-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button"
|
||||
onClick={handleThemeModeToggle}
|
||||
aria-label={`Switch theme (next: ${nextThemeLabel})`}
|
||||
title={`Theme: ${themeModeLabel} (next: ${nextThemeLabel})`}
|
||||
>
|
||||
{themeModeIcon}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button"
|
||||
onClick={handleThemeAdjustmentsReset}
|
||||
aria-label="Reset theme adjustments"
|
||||
title="Reset theme adjustments"
|
||||
>
|
||||
<RestoreIcon size={16} />
|
||||
</button>
|
||||
</div>
|
||||
<label className="menu__slider" htmlFor={neutralHueInputId}>
|
||||
<span className="menu__slider-label">Hue</span>
|
||||
<input
|
||||
id={neutralHueInputId}
|
||||
type="range"
|
||||
min="0"
|
||||
max="360"
|
||||
step="1"
|
||||
value={neutralHue}
|
||||
onChange={(event) => handleNeutralHueChange(event.target.value)}
|
||||
onDoubleClick={resetNeutralHue}
|
||||
/>
|
||||
<span className="menu__slider-value">{neutralHue}°</span>
|
||||
</label>
|
||||
<label className="menu__slider" htmlFor={neutralChromaInputId}>
|
||||
<span className="menu__slider-label">Chroma</span>
|
||||
<input
|
||||
id={neutralChromaInputId}
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.01"
|
||||
value={neutralChroma}
|
||||
onChange={(event) => handleNeutralChromaChange(event.target.value)}
|
||||
onDoubleClick={resetNeutralChroma}
|
||||
/>
|
||||
<span className="menu__slider-value">{formatSliderValue(neutralChroma)}</span>
|
||||
</label>
|
||||
<label className="menu__slider" htmlFor={neutralContrastInputId}>
|
||||
<span className="menu__slider-label">Contrast</span>
|
||||
<input
|
||||
id={neutralContrastInputId}
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.01"
|
||||
value={neutralContrast}
|
||||
onChange={(event) => handleNeutralContrastChange(event.target.value)}
|
||||
onDoubleClick={resetNeutralContrast}
|
||||
/>
|
||||
<span className="menu__slider-value">{formatSliderValue(neutralContrast)}</span>
|
||||
</label>
|
||||
</div>
|
||||
)
|
||||
<label className="menu__slider" htmlFor={neutralHueInputId}>
|
||||
<span className="menu__slider-label">Hue</span>
|
||||
<input
|
||||
id={neutralHueInputId}
|
||||
type="range"
|
||||
min="0"
|
||||
max="360"
|
||||
step="1"
|
||||
value={neutralHue}
|
||||
onChange={(event) => handleNeutralHueChange(event.target.value)}
|
||||
onDoubleClick={resetNeutralHue}
|
||||
/>
|
||||
<span className="menu__slider-value">{neutralHue}°</span>
|
||||
</label>
|
||||
<label className="menu__slider" htmlFor={neutralChromaInputId}>
|
||||
<span className="menu__slider-label">Chroma</span>
|
||||
<input
|
||||
id={neutralChromaInputId}
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.01"
|
||||
value={neutralChroma}
|
||||
onChange={(event) => handleNeutralChromaChange(event.target.value)}
|
||||
onDoubleClick={resetNeutralChroma}
|
||||
/>
|
||||
<span className="menu__slider-value">{formatSliderValue(neutralChroma)}</span>
|
||||
</label>
|
||||
<label className="menu__slider" htmlFor={neutralContrastInputId}>
|
||||
<span className="menu__slider-label">Contrast</span>
|
||||
<input
|
||||
id={neutralContrastInputId}
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.01"
|
||||
value={neutralContrast}
|
||||
onChange={(event) => handleNeutralContrastChange(event.target.value)}
|
||||
onDoubleClick={resetNeutralContrast}
|
||||
/>
|
||||
<span className="menu__slider-value">{formatSliderValue(neutralContrast)}</span>
|
||||
</label>
|
||||
</div>
|
||||
)
|
||||
: null;
|
||||
|
||||
const communityMenuFooter = COMMUNITY_LINKS.length
|
||||
? (
|
||||
<div className="menu__footer">
|
||||
{COMMUNITY_LINKS.map(({ href, label, title, Icon }) => (
|
||||
<a
|
||||
key={href}
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className="menu__footer-link"
|
||||
title={title}
|
||||
>
|
||||
<Icon size={16} stroke={1.5} />
|
||||
<span>{label}</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
<div className="menu__footer">
|
||||
{COMMUNITY_LINKS.map(({ href, label, title, Icon }) => (
|
||||
<a
|
||||
key={href}
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className="menu__footer-link"
|
||||
title={title}
|
||||
>
|
||||
<Icon size={16} stroke={1.5} />
|
||||
<span>{label}</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
: null;
|
||||
|
||||
const handleSearchInputChange = useCallback(
|
||||
@@ -736,62 +736,62 @@ const Sidebar: React.FC<SidebarProps> = ({
|
||||
}
|
||||
|
||||
const tenantMenuContent = tenantMenuOpen && tenantMenuStyle
|
||||
? createPortal(
|
||||
<div
|
||||
className={menuClassName}
|
||||
ref={tenantMenuRef}
|
||||
role="menu"
|
||||
aria-label="Account menu"
|
||||
style={tenantMenuStyle}
|
||||
>
|
||||
{showTenantList ? (
|
||||
<div className="menu__section">
|
||||
<div className="menu__heading">Switch tenant</div>
|
||||
<div className="menu__wrapper">
|
||||
{tenants.map((tenant) => {
|
||||
const tenantId = tenant?.id || null;
|
||||
const isActive = tenantId === activeTenantId;
|
||||
const tenantLabel = tenant?.name || tenantId || 'Tenant';
|
||||
return (
|
||||
<button
|
||||
key={tenantId ?? tenantLabel}
|
||||
type="button"
|
||||
className={`menu__button${isActive ? ' active' : ''}`}
|
||||
onClick={() => handleTenantSelect(tenant)}
|
||||
role="menuitem"
|
||||
>
|
||||
<span className="menu__check-slot">
|
||||
{isActive ? <CheckIcon size={16} /> : null}
|
||||
</span>
|
||||
<span className="menu__label">{tenantLabel}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="menu__section">
|
||||
<div className="menu__wrapper">
|
||||
<button type="button" className="menu__button" onClick={handleSettingsFromMenu}>
|
||||
<SettingsIcon size={16} />
|
||||
Settings
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="menu__button menu__button--danger"
|
||||
onClick={handleLogoutFromMenu}
|
||||
>
|
||||
<LogoutIcon size={16} />
|
||||
Log out
|
||||
</button>
|
||||
</div>
|
||||
? createPortal(
|
||||
<div
|
||||
className={menuClassName}
|
||||
ref={tenantMenuRef}
|
||||
role="menu"
|
||||
aria-label="Account menu"
|
||||
style={tenantMenuStyle}
|
||||
>
|
||||
{showTenantList ? (
|
||||
<div className="menu__section">
|
||||
<div className="menu__heading">Switch tenant</div>
|
||||
<div className="menu__wrapper">
|
||||
{tenants.map((tenant) => {
|
||||
const tenantId = tenant?.id || null;
|
||||
const isActive = tenantId === activeTenantId;
|
||||
const tenantLabel = tenant?.name || tenantId || 'Tenant';
|
||||
return (
|
||||
<button
|
||||
key={tenantId ?? tenantLabel}
|
||||
type="button"
|
||||
className={`menu__button${isActive ? ' active' : ''}`}
|
||||
onClick={() => handleTenantSelect(tenant)}
|
||||
role="menuitem"
|
||||
>
|
||||
<span className="menu__check-slot">
|
||||
{isActive ? <CheckIcon size={16} /> : null}
|
||||
</span>
|
||||
<span className="menu__label">{tenantLabel}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{themeMenuSection}
|
||||
{communityMenuFooter}
|
||||
</div>,
|
||||
document.body,
|
||||
)
|
||||
: null;
|
||||
</div>
|
||||
) : null}
|
||||
<div className="menu__section">
|
||||
<div className="menu__wrapper">
|
||||
<button type="button" className="menu__button" onClick={handleSettingsFromMenu}>
|
||||
<SettingsIcon size={16} />
|
||||
Settings
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="menu__button menu__button--danger"
|
||||
onClick={handleLogoutFromMenu}
|
||||
>
|
||||
<LogoutIcon size={16} />
|
||||
Log out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{themeMenuSection}
|
||||
{communityMenuFooter}
|
||||
</div>,
|
||||
document.body,
|
||||
)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<aside className={sidebarClassNames.join(' ')} ref={sidebarRef} style={sidebarStyle}>
|
||||
@@ -925,9 +925,8 @@ const Sidebar: React.FC<SidebarProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`sidebar-tag-cloud${
|
||||
activeTagSet.size ? ' sidebar-tag-cloud--has-active' : ''
|
||||
}`}
|
||||
className={`sidebar-tag-cloud${activeTagSet.size ? ' sidebar-tag-cloud--has-active' : ''
|
||||
}`}
|
||||
role="list"
|
||||
>
|
||||
{untaggedFilterId ? (
|
||||
@@ -955,25 +954,25 @@ const Sidebar: React.FC<SidebarProps> = ({
|
||||
style={style || undefined}
|
||||
onClick={() => handleToggleTag(tag.id)}
|
||||
aria-pressed={isActive}
|
||||
draggable
|
||||
onDragStart={(event) => {
|
||||
try {
|
||||
const payload = JSON.stringify({
|
||||
id: tag.id,
|
||||
label: tag.label,
|
||||
color: tag.color || null,
|
||||
});
|
||||
event.dataTransfer.effectAllowed = 'copy';
|
||||
event.dataTransfer.setData('application/x-papercrate-tag', payload);
|
||||
event.dataTransfer.setData('text/papercrate-tag', payload);
|
||||
} catch (error) {
|
||||
console.warn('[sidebar] Failed to set tag drag payload', error);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{tag.label}
|
||||
</button>
|
||||
);
|
||||
draggable
|
||||
onDragStart={(event) => {
|
||||
try {
|
||||
const payload = JSON.stringify({
|
||||
id: tag.id,
|
||||
label: tag.label,
|
||||
color: tag.color || null,
|
||||
});
|
||||
event.dataTransfer.effectAllowed = 'copy';
|
||||
event.dataTransfer.setData('application/x-papercrate-tag', payload);
|
||||
event.dataTransfer.setData('text/papercrate-tag', payload);
|
||||
} catch (error) {
|
||||
console.warn('[sidebar] Failed to set tag drag payload', error);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{tag.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,8 +2,7 @@ import { useMemo } from 'react';
|
||||
import type { DragEvent } from 'react';
|
||||
import { TAG_FILTER_UNTAGGED } from '../app/workspaceUtils';
|
||||
import type { StatusMessage } from '../hooks/documents/store/useDocumentsStore';
|
||||
|
||||
type Identifier = string | number;
|
||||
import type { Identifier } from '../types/identifiers';
|
||||
|
||||
interface FolderTreeNode {
|
||||
id: Identifier;
|
||||
|
||||
Reference in New Issue
Block a user