mirror of
https://github.com/PeWu/topola-viewer.git
synced 2026-02-18 02:55:48 +00:00
Sidebar improvements (Collapse/Expand, Mobile view) (#215)
* Extract sidebar to new component * Add sidebar toggle * Fix scrollbars sometimes appearing even at maximum zoom-out
This commit is contained in:
parent
0f0a75a5ec
commit
2a0c963789
90
src/app.tsx
90
src/app.tsx
@ -3,9 +3,14 @@ import queryString from 'query-string';
|
|||||||
import {useEffect, useState} from 'react';
|
import {useEffect, useState} from 'react';
|
||||||
import {FormattedMessage, useIntl} from 'react-intl';
|
import {FormattedMessage, useIntl} from 'react-intl';
|
||||||
import {Navigate, Route, Routes, useLocation, useNavigate} from 'react-router';
|
import {Navigate, Route, Routes, useLocation, useNavigate} from 'react-router';
|
||||||
import {Loader, Message, Portal, Tab} from 'semantic-ui-react';
|
import {
|
||||||
|
Loader,
|
||||||
|
Message,
|
||||||
|
Portal,
|
||||||
|
SidebarPushable,
|
||||||
|
SidebarPusher,
|
||||||
|
} from 'semantic-ui-react';
|
||||||
import {IndiInfo} from 'topola';
|
import {IndiInfo} from 'topola';
|
||||||
import {Changelog} from './changelog';
|
|
||||||
import {
|
import {
|
||||||
Chart,
|
Chart,
|
||||||
ChartType,
|
ChartType,
|
||||||
@ -14,15 +19,6 @@ import {
|
|||||||
downloadSvg,
|
downloadSvg,
|
||||||
printChart,
|
printChart,
|
||||||
} from './chart';
|
} from './chart';
|
||||||
import {
|
|
||||||
argsToConfig,
|
|
||||||
Config,
|
|
||||||
ConfigPanel,
|
|
||||||
configToArgs,
|
|
||||||
DEFALUT_CONFIG,
|
|
||||||
Ids,
|
|
||||||
Sex,
|
|
||||||
} from './config';
|
|
||||||
import {DataSourceEnum, SourceSelection} from './datasource/data_source';
|
import {DataSourceEnum, SourceSelection} from './datasource/data_source';
|
||||||
import {EmbeddedDataSource, EmbeddedSourceSpec} from './datasource/embedded';
|
import {EmbeddedDataSource, EmbeddedSourceSpec} from './datasource/embedded';
|
||||||
import {
|
import {
|
||||||
@ -38,14 +34,21 @@ import {
|
|||||||
WikiTreeDataSource,
|
WikiTreeDataSource,
|
||||||
WikiTreeSourceSpec,
|
WikiTreeSourceSpec,
|
||||||
} from './datasource/wikitree';
|
} from './datasource/wikitree';
|
||||||
import {Details} from './details/details';
|
|
||||||
import {DonatsoChart} from './donatso-chart';
|
import {DonatsoChart} from './donatso-chart';
|
||||||
import {Intro} from './intro';
|
import {Intro} from './intro';
|
||||||
import {TopBar} from './menu/top_bar';
|
import {TopBar} from './menu/top_bar';
|
||||||
|
import {
|
||||||
|
argsToConfig,
|
||||||
|
Config,
|
||||||
|
configToArgs,
|
||||||
|
DEFALUT_CONFIG,
|
||||||
|
Ids,
|
||||||
|
Sex,
|
||||||
|
} from './sidepanel/config/config';
|
||||||
|
import {SidePanel} from './sidepanel/side-panel';
|
||||||
import {analyticsEvent} from './util/analytics';
|
import {analyticsEvent} from './util/analytics';
|
||||||
import {getI18nMessage} from './util/error_i18n';
|
import {getI18nMessage} from './util/error_i18n';
|
||||||
import {idToIndiMap, TopolaData} from './util/gedcom_util';
|
import {idToIndiMap, TopolaData} from './util/gedcom_util';
|
||||||
import {Media} from './util/media';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load GEDCOM URL from VITE_STATIC_URL environment variable.
|
* Load GEDCOM URL from VITE_STATIC_URL environment variable.
|
||||||
@ -241,7 +244,7 @@ export function App() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleDetails(config: Config, data: TopolaData | undefined) {
|
function updateChartWithConfig(config: Config, data: TopolaData | undefined) {
|
||||||
if (data === undefined) {
|
if (data === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -254,6 +257,14 @@ export function App() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onToggleSidePanel() {
|
||||||
|
const newShowSidePanel = !showSidePanel;
|
||||||
|
setShowSidePanel(newShowSidePanel);
|
||||||
|
updateUrl({
|
||||||
|
sidePanel: newShowSidePanel ? 'true' : 'false',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/** Sets error message after data load failure. */
|
/** Sets error message after data load failure. */
|
||||||
function setErrorMessage(message: string) {
|
function setErrorMessage(message: string) {
|
||||||
setError(message);
|
setError(message);
|
||||||
@ -362,7 +373,7 @@ export function App() {
|
|||||||
const data = await loadData(args.sourceSpec, args.selection);
|
const data = await loadData(args.sourceSpec, args.selection);
|
||||||
// Set state with data.
|
// Set state with data.
|
||||||
setData(data);
|
setData(data);
|
||||||
toggleDetails(args.config, data);
|
updateChartWithConfig(args.config, data);
|
||||||
setShowSidePanel(args.showSidePanel);
|
setShowSidePanel(args.showSidePanel);
|
||||||
setState(AppState.SHOWING_CHART);
|
setState(AppState.SHOWING_CHART);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@ -510,33 +521,6 @@ export function App() {
|
|||||||
case AppState.SHOWING_CHART:
|
case AppState.SHOWING_CHART:
|
||||||
case AppState.LOADING_MORE:
|
case AppState.LOADING_MORE:
|
||||||
const updatedSelection = getSelection(data!.chartData, selection);
|
const updatedSelection = getSelection(data!.chartData, selection);
|
||||||
const sidePanelTabs = [
|
|
||||||
{
|
|
||||||
menuItem: intl.formatMessage({
|
|
||||||
id: 'tab.info',
|
|
||||||
defaultMessage: 'Info',
|
|
||||||
}),
|
|
||||||
render: () => (
|
|
||||||
<Details gedcom={data!.gedcom} indi={updatedSelection.id} />
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
menuItem: intl.formatMessage({
|
|
||||||
id: 'tab.settings',
|
|
||||||
defaultMessage: 'Settings',
|
|
||||||
}),
|
|
||||||
render: () => (
|
|
||||||
<ConfigPanel
|
|
||||||
config={config}
|
|
||||||
onChange={(config) => {
|
|
||||||
setConfig(config);
|
|
||||||
toggleDetails(config, data);
|
|
||||||
updateUrl(configToArgs(config));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
return (
|
return (
|
||||||
<div id="content">
|
<div id="content">
|
||||||
<ErrorPopup
|
<ErrorPopup
|
||||||
@ -547,13 +531,21 @@ export function App() {
|
|||||||
{state === AppState.LOADING_MORE ? (
|
{state === AppState.LOADING_MORE ? (
|
||||||
<Loader active size="small" className="loading-more" />
|
<Loader active size="small" className="loading-more" />
|
||||||
) : null}
|
) : null}
|
||||||
{renderChart(updatedSelection)}
|
<SidebarPushable>
|
||||||
{showSidePanel ? (
|
<SidePanel
|
||||||
<Media greaterThanOrEqual="large" className="sidePanel">
|
data={data!}
|
||||||
<Tab panes={sidePanelTabs} />
|
selectedIndiId={updatedSelection.id}
|
||||||
</Media>
|
config={config}
|
||||||
) : null}
|
expanded={showSidePanel}
|
||||||
<Changelog />
|
onToggle={onToggleSidePanel}
|
||||||
|
onConfigChange={(config) => {
|
||||||
|
setConfig(config);
|
||||||
|
updateChartWithConfig(config, data);
|
||||||
|
updateUrl(configToArgs(config));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<SidebarPusher>{renderChart(updatedSelection)}</SidebarPusher>
|
||||||
|
</SidebarPushable>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import {useEffect, useRef} from 'react';
|
|||||||
import {IntlShape, useIntl} from 'react-intl';
|
import {IntlShape, useIntl} from 'react-intl';
|
||||||
import {
|
import {
|
||||||
ChartHandle,
|
ChartHandle,
|
||||||
|
ChartInfo,
|
||||||
CircleRenderer,
|
CircleRenderer,
|
||||||
createChart,
|
createChart,
|
||||||
DetailedRenderer,
|
DetailedRenderer,
|
||||||
@ -24,7 +25,7 @@ import {
|
|||||||
RelativesChart,
|
RelativesChart,
|
||||||
ChartColors as TopolaChartColors,
|
ChartColors as TopolaChartColors,
|
||||||
} from 'topola';
|
} from 'topola';
|
||||||
import {ChartColors, Ids, Sex} from './config';
|
import {ChartColors, Ids, Sex} from './sidepanel/config/config';
|
||||||
import {Media} from './util/media';
|
import {Media} from './util/media';
|
||||||
import {usePrevious} from './util/previous-hook';
|
import {usePrevious} from './util/previous-hook';
|
||||||
|
|
||||||
@ -252,6 +253,40 @@ function getRendererType(chartType: ChartType) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns the element’s usable width and height by subtracting the assumed scrollbar size. */
|
||||||
|
function getScrollbarAwareSize(
|
||||||
|
element: Element,
|
||||||
|
scrollbarSize = 20,
|
||||||
|
): [number, number] {
|
||||||
|
const htmlElement = element as HTMLElement;
|
||||||
|
return [
|
||||||
|
htmlElement.clientWidth - scrollbarSize,
|
||||||
|
htmlElement.clientHeight - scrollbarSize,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the allowed zoom scale range.
|
||||||
|
* Sets the minimum scale so the chart cannot zoom out beyond full visibility,
|
||||||
|
* and fixes the maximum scale at 2.
|
||||||
|
*/
|
||||||
|
function calculateScaleExtent(
|
||||||
|
parent: Element,
|
||||||
|
scale: number,
|
||||||
|
chartInfo: ChartInfo,
|
||||||
|
): [number, number] {
|
||||||
|
const [availWidth, availHeight] = getScrollbarAwareSize(parent);
|
||||||
|
|
||||||
|
const zoomOutFactor = min([
|
||||||
|
1,
|
||||||
|
scale,
|
||||||
|
availWidth / chartInfo.size[0],
|
||||||
|
availHeight / chartInfo.size[1],
|
||||||
|
])!;
|
||||||
|
|
||||||
|
return [max([0.1, zoomOutFactor])!, 2];
|
||||||
|
}
|
||||||
|
|
||||||
export interface ChartProps {
|
export interface ChartProps {
|
||||||
data: JsonGedcomData;
|
data: JsonGedcomData;
|
||||||
selection: IndiInfo;
|
selection: IndiInfo;
|
||||||
@ -328,20 +363,18 @@ class ChartWrapper {
|
|||||||
});
|
});
|
||||||
const svg = select('#chartSvg');
|
const svg = select('#chartSvg');
|
||||||
const parent = select('#svgContainer').node() as Element;
|
const parent = select('#svgContainer').node() as Element;
|
||||||
|
|
||||||
const scale = zoomTransform(parent).k;
|
const scale = zoomTransform(parent).k;
|
||||||
const zoomOutFactor = min([
|
const extent: [number, number] = calculateScaleExtent(
|
||||||
1,
|
parent,
|
||||||
scale,
|
scale,
|
||||||
parent.clientWidth / chartInfo.size[0],
|
chartInfo,
|
||||||
parent.clientHeight / chartInfo.size[1],
|
);
|
||||||
])!;
|
|
||||||
const extent: [number, number] = [max([0.1, zoomOutFactor])!, 2];
|
|
||||||
|
|
||||||
this.zoomBehavior = zoom()
|
this.zoomBehavior = zoom()
|
||||||
.scaleExtent(extent)
|
.scaleExtent(extent)
|
||||||
.translateExtent([[0, 0], chartInfo.size])
|
.translateExtent([[0, 0], chartInfo.size])
|
||||||
.on('zoom', (event) => zoomed(chartInfo.size, event));
|
.on('zoom', (event) => zoomed(chartInfo.size, event));
|
||||||
|
|
||||||
select(parent).on('scroll', scrolled).call(this.zoomBehavior);
|
select(parent).on('scroll', scrolled).call(this.zoomBehavior);
|
||||||
|
|
||||||
const scrollTopTween = (scrollTop: number) => {
|
const scrollTopTween = (scrollTop: number) => {
|
||||||
|
|||||||
105
src/index.css
105
src/index.css
@ -14,9 +14,15 @@ body, html {
|
|||||||
|
|
||||||
#content {
|
#content {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
display: flex;
|
|
||||||
overflow: hidden;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#introContent {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
#svgContainer {
|
#svgContainer {
|
||||||
@ -24,13 +30,6 @@ body, html {
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidePanel {
|
|
||||||
flex: 0 0 320px;
|
|
||||||
overflow: auto;
|
|
||||||
border-left: solid #ccc 1px;
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hidden {
|
.hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@ -147,10 +146,12 @@ div.zoom {
|
|||||||
.details {
|
.details {
|
||||||
padding: 15px 0px;
|
padding: 15px 0px;
|
||||||
border-bottom: 1px solid rgba(34,36,38,.15);
|
border-bottom: 1px solid rgba(34,36,38,.15);
|
||||||
|
overflow-y: auto;
|
||||||
|
height: calc(100% - 43px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.details .ui.items .item .content {
|
.details .ui.items .item .content {
|
||||||
padding: 0 15px;
|
padding: 0 15px 0 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.details .item-header {
|
.details .item-header {
|
||||||
@ -166,8 +167,6 @@ div.zoom {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.details .person-image {
|
.details .person-image {
|
||||||
max-width: 289px;
|
|
||||||
width: 289px;
|
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -278,3 +277,85 @@ div.zoom {
|
|||||||
#dotatsoSvgContainer .link {
|
#dotatsoSvgContainer .link {
|
||||||
stroke: black;
|
stroke: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#content .pusher {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#content #sidebar.wide ~ .pusher {
|
||||||
|
width: calc(100% - 350px);
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#content #sidebar.very.thin ~ .pusher {
|
||||||
|
width: calc(100% - 60px);
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar {
|
||||||
|
overflow: hidden;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sideTabs {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sideTabs > .ui.tabular.menu{
|
||||||
|
padding-left: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sideToggle {
|
||||||
|
position: absolute;
|
||||||
|
background: inherit;
|
||||||
|
top: 0;
|
||||||
|
height: 42px;
|
||||||
|
border-radius: 0;
|
||||||
|
width: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sideToggle:hover {
|
||||||
|
background-color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsed-details {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsed-details .vertical-name {
|
||||||
|
writing-mode: vertical-rl;
|
||||||
|
transform: rotate(180deg);
|
||||||
|
transform-origin: center;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
#sidebar.wide {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar.very.thin {
|
||||||
|
width: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#content #sidebar.wide ~ .pusher {
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#content #sidebar.very.thin ~ .pusher {
|
||||||
|
width: calc(100% - 35px);
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar.very.thin #sideToggle {
|
||||||
|
width: 35px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -141,7 +141,7 @@ function Contents() {
|
|||||||
/** The intro page. */
|
/** The intro page. */
|
||||||
export function Intro() {
|
export function Intro() {
|
||||||
return (
|
return (
|
||||||
<div id="content">
|
<div id="introContent">
|
||||||
<div className="backgroundImage" />
|
<div className="backgroundImage" />
|
||||||
<Card className="intro">
|
<Card className="intro">
|
||||||
<Card.Content as={Media} greaterThanOrEqual="large">
|
<Card.Content as={Media} greaterThanOrEqual="large">
|
||||||
|
|||||||
26
src/sidepanel/details/collapsed-details.tsx
Normal file
26
src/sidepanel/details/collapsed-details.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import {FormattedMessage} from 'react-intl';
|
||||||
|
import {GedcomData} from '../../util/gedcom_util';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
gedcom: GedcomData;
|
||||||
|
indi: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CollapsedDetails(props: Props) {
|
||||||
|
const entries = props.gedcom.indis[props.indi].tree;
|
||||||
|
const nameEntry = entries.find((entry) => entry.tag === 'NAME');
|
||||||
|
|
||||||
|
const fullName = nameEntry?.data.replaceAll('/', '') ?? '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="collapsed-details">
|
||||||
|
{fullName ? (
|
||||||
|
<span className="vertical-name">{fullName}</span>
|
||||||
|
) : (
|
||||||
|
<span className="vertical-name">
|
||||||
|
<FormattedMessage id="name.unknown_name" defaultMessage="N.N." />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -10,7 +10,7 @@ import {
|
|||||||
getImageFileEntry,
|
getImageFileEntry,
|
||||||
getNonImageFileEntry,
|
getNonImageFileEntry,
|
||||||
mapToSource,
|
mapToSource,
|
||||||
} from '../util/gedcom_util';
|
} from '../../util/gedcom_util';
|
||||||
import {AdditionalFiles} from './additional-files';
|
import {AdditionalFiles} from './additional-files';
|
||||||
import {ALL_SUPPORTED_EVENT_TYPES, Events} from './events';
|
import {ALL_SUPPORTED_EVENT_TYPES, Events} from './events';
|
||||||
import {MultilineText} from './multiline-text';
|
import {MultilineText} from './multiline-text';
|
||||||
@ -9,7 +9,7 @@ import {
|
|||||||
Popup,
|
Popup,
|
||||||
Tab,
|
Tab,
|
||||||
} from 'semantic-ui-react';
|
} from 'semantic-ui-react';
|
||||||
import {Source} from '../util/gedcom_util';
|
import {Source} from '../../util/gedcom_util';
|
||||||
import {AdditionalFiles, FileEntry} from './additional-files';
|
import {AdditionalFiles, FileEntry} from './additional-files';
|
||||||
import {MultilineText} from './multiline-text';
|
import {MultilineText} from './multiline-text';
|
||||||
import {Sources} from './sources';
|
import {Sources} from './sources';
|
||||||
@ -5,8 +5,8 @@ import {FormattedMessage, IntlShape, useIntl} from 'react-intl';
|
|||||||
import {Link, useLocation} from 'react-router';
|
import {Link, useLocation} from 'react-router';
|
||||||
import {Header, Item} from 'semantic-ui-react';
|
import {Header, Item} from 'semantic-ui-react';
|
||||||
import {DateOrRange, getDate} from 'topola';
|
import {DateOrRange, getDate} from 'topola';
|
||||||
import {calcAge} from '../util/age_util';
|
import {calcAge} from '../../util/age_util';
|
||||||
import {compareDates, formatDateOrRange} from '../util/date_util';
|
import {compareDates, formatDateOrRange} from '../../util/date_util';
|
||||||
import {
|
import {
|
||||||
dereference,
|
dereference,
|
||||||
GedcomData,
|
GedcomData,
|
||||||
@ -20,7 +20,7 @@ import {
|
|||||||
resolveDate,
|
resolveDate,
|
||||||
resolveType,
|
resolveType,
|
||||||
Source,
|
Source,
|
||||||
} from '../util/gedcom_util';
|
} from '../../util/gedcom_util';
|
||||||
import {FileEntry} from './additional-files';
|
import {FileEntry} from './additional-files';
|
||||||
import {EventExtras, Image} from './event-extras';
|
import {EventExtras, Image} from './event-extras';
|
||||||
import {TranslatedTag} from './translated-tag';
|
import {TranslatedTag} from './translated-tag';
|
||||||
@ -1,8 +1,8 @@
|
|||||||
import {useIntl} from 'react-intl';
|
import {useIntl} from 'react-intl';
|
||||||
import Linkify from 'react-linkify';
|
import Linkify from 'react-linkify';
|
||||||
import {List} from 'semantic-ui-react';
|
import {List} from 'semantic-ui-react';
|
||||||
import {formatDateOrRange} from '../util/date_util';
|
import {formatDateOrRange} from '../../util/date_util';
|
||||||
import {Source} from '../util/gedcom_util';
|
import {Source} from '../../util/gedcom_util';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
sources?: Source[];
|
sources?: Source[];
|
||||||
63
src/sidepanel/side-panel.tsx
Normal file
63
src/sidepanel/side-panel.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import {useIntl} from 'react-intl';
|
||||||
|
import {Button, Icon, Sidebar, Tab} from 'semantic-ui-react';
|
||||||
|
import {TopolaData} from '../util/gedcom_util';
|
||||||
|
import {Config, ConfigPanel} from './config/config';
|
||||||
|
import {CollapsedDetails} from './details/collapsed-details';
|
||||||
|
import {Details} from './details/details';
|
||||||
|
|
||||||
|
interface SidePanelProps {
|
||||||
|
data: TopolaData;
|
||||||
|
selectedIndiId: string;
|
||||||
|
config: Config;
|
||||||
|
onConfigChange: (config: Config) => void;
|
||||||
|
expanded: boolean;
|
||||||
|
onToggle: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SidePanel({
|
||||||
|
data,
|
||||||
|
selectedIndiId,
|
||||||
|
config,
|
||||||
|
onConfigChange,
|
||||||
|
expanded,
|
||||||
|
onToggle,
|
||||||
|
}: SidePanelProps) {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{
|
||||||
|
menuItem: intl.formatMessage({
|
||||||
|
id: 'tab.info',
|
||||||
|
defaultMessage: 'Info',
|
||||||
|
}),
|
||||||
|
render: () => <Details gedcom={data.gedcom} indi={selectedIndiId} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
menuItem: intl.formatMessage({
|
||||||
|
id: 'tab.settings',
|
||||||
|
defaultMessage: 'Settings',
|
||||||
|
}),
|
||||||
|
render: () => <ConfigPanel config={config} onChange={onConfigChange} />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sidebar
|
||||||
|
id="sidebar"
|
||||||
|
animation="overlay"
|
||||||
|
icon="labeled"
|
||||||
|
width={expanded ? 'wide' : 'very thin'}
|
||||||
|
direction="right"
|
||||||
|
visible={true}
|
||||||
|
>
|
||||||
|
{expanded ? (
|
||||||
|
<Tab id="sideTabs" panes={tabs} />
|
||||||
|
) : (
|
||||||
|
<CollapsedDetails gedcom={data.gedcom} indi={selectedIndiId} />
|
||||||
|
)}
|
||||||
|
<Button id="sideToggle" icon size="mini" onClick={() => onToggle()}>
|
||||||
|
<Icon size="large" name={expanded ? 'arrow right' : 'arrow left'} />
|
||||||
|
</Button>
|
||||||
|
</Sidebar>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user