{
- setConfig(config);
- toggleDetails(config, data);
- updateUrl(configToArgs(config));
- }}
- />
- ),
- },
- ];
return (
) : null}
- {renderChart(updatedSelection)}
- {showSidePanel ? (
-
-
-
- ) : null}
-
+
+ {
+ setConfig(config);
+ updateChartWithConfig(config, data);
+ updateUrl(configToArgs(config));
+ }}
+ />
+ {renderChart(updatedSelection)}
+
);
diff --git a/src/chart.tsx b/src/chart.tsx
index f1515cf..5765211 100644
--- a/src/chart.tsx
+++ b/src/chart.tsx
@@ -14,6 +14,7 @@ import {useEffect, useRef} from 'react';
import {IntlShape, useIntl} from 'react-intl';
import {
ChartHandle,
+ ChartInfo,
CircleRenderer,
createChart,
DetailedRenderer,
@@ -24,7 +25,7 @@ import {
RelativesChart,
ChartColors as TopolaChartColors,
} from 'topola';
-import {ChartColors, Ids, Sex} from './config';
+import {ChartColors, Ids, Sex} from './sidepanel/config/config';
import {Media} from './util/media';
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 {
data: JsonGedcomData;
selection: IndiInfo;
@@ -328,20 +363,18 @@ class ChartWrapper {
});
const svg = select('#chartSvg');
const parent = select('#svgContainer').node() as Element;
-
const scale = zoomTransform(parent).k;
- const zoomOutFactor = min([
- 1,
+ const extent: [number, number] = calculateScaleExtent(
+ parent,
scale,
- parent.clientWidth / chartInfo.size[0],
- parent.clientHeight / chartInfo.size[1],
- ])!;
- const extent: [number, number] = [max([0.1, zoomOutFactor])!, 2];
+ chartInfo,
+ );
this.zoomBehavior = zoom()
.scaleExtent(extent)
.translateExtent([[0, 0], chartInfo.size])
.on('zoom', (event) => zoomed(chartInfo.size, event));
+
select(parent).on('scroll', scrolled).call(this.zoomBehavior);
const scrollTopTween = (scrollTop: number) => {
diff --git a/src/index.css b/src/index.css
index 410ba08..ce21126 100644
--- a/src/index.css
+++ b/src/index.css
@@ -14,9 +14,15 @@ body, html {
#content {
flex: 1 1 auto;
- display: flex;
- overflow: hidden;
height: 100%;
+ overflow: hidden;
+}
+
+#introContent {
+ flex: 1 1 auto;
+ height: 100%;
+ overflow: hidden;
+ display: flex;
}
#svgContainer {
@@ -24,13 +30,6 @@ body, html {
overflow: auto;
}
-.sidePanel {
- flex: 0 0 320px;
- overflow: auto;
- border-left: solid #ccc 1px;
- overflow-x: hidden;
-}
-
.hidden {
display: none;
}
@@ -147,10 +146,12 @@ div.zoom {
.details {
padding: 15px 0px;
border-bottom: 1px solid rgba(34,36,38,.15);
+ overflow-y: auto;
+ height: calc(100% - 43px);
}
.details .ui.items .item .content {
- padding: 0 15px;
+ padding: 0 15px 0 25px;
}
.details .item-header {
@@ -166,8 +167,6 @@ div.zoom {
}
.details .person-image {
- max-width: 289px;
- width: 289px;
padding: 0 10px;
}
@@ -278,3 +277,85 @@ div.zoom {
#dotatsoSvgContainer .link {
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;
+ }
+}
+
+
+
diff --git a/src/intro.tsx b/src/intro.tsx
index a4512ea..b4e97d8 100644
--- a/src/intro.tsx
+++ b/src/intro.tsx
@@ -141,7 +141,7 @@ function Contents() {
/** The intro page. */
export function Intro() {
return (
-
+
diff --git a/src/config.tsx b/src/sidepanel/config/config.tsx
similarity index 100%
rename from src/config.tsx
rename to src/sidepanel/config/config.tsx
diff --git a/src/details/additional-files.tsx b/src/sidepanel/details/additional-files.tsx
similarity index 100%
rename from src/details/additional-files.tsx
rename to src/sidepanel/details/additional-files.tsx
diff --git a/src/sidepanel/details/collapsed-details.tsx b/src/sidepanel/details/collapsed-details.tsx
new file mode 100644
index 0000000..9a972f9
--- /dev/null
+++ b/src/sidepanel/details/collapsed-details.tsx
@@ -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 (
+
+ {fullName ? (
+ {fullName}
+ ) : (
+
+
+
+ )}
+
+ );
+}
diff --git a/src/details/details.tsx b/src/sidepanel/details/details.tsx
similarity index 99%
rename from src/details/details.tsx
rename to src/sidepanel/details/details.tsx
index 19c818d..7eeab17 100644
--- a/src/details/details.tsx
+++ b/src/sidepanel/details/details.tsx
@@ -10,7 +10,7 @@ import {
getImageFileEntry,
getNonImageFileEntry,
mapToSource,
-} from '../util/gedcom_util';
+} from '../../util/gedcom_util';
import {AdditionalFiles} from './additional-files';
import {ALL_SUPPORTED_EVENT_TYPES, Events} from './events';
import {MultilineText} from './multiline-text';
diff --git a/src/details/event-extras.tsx b/src/sidepanel/details/event-extras.tsx
similarity index 98%
rename from src/details/event-extras.tsx
rename to src/sidepanel/details/event-extras.tsx
index 3929ea5..b756832 100644
--- a/src/details/event-extras.tsx
+++ b/src/sidepanel/details/event-extras.tsx
@@ -9,7 +9,7 @@ import {
Popup,
Tab,
} from 'semantic-ui-react';
-import {Source} from '../util/gedcom_util';
+import {Source} from '../../util/gedcom_util';
import {AdditionalFiles, FileEntry} from './additional-files';
import {MultilineText} from './multiline-text';
import {Sources} from './sources';
diff --git a/src/details/events.tsx b/src/sidepanel/details/events.tsx
similarity index 98%
rename from src/details/events.tsx
rename to src/sidepanel/details/events.tsx
index e4d3b0c..9eabf9b 100644
--- a/src/details/events.tsx
+++ b/src/sidepanel/details/events.tsx
@@ -5,8 +5,8 @@ import {FormattedMessage, IntlShape, useIntl} from 'react-intl';
import {Link, useLocation} from 'react-router';
import {Header, Item} from 'semantic-ui-react';
import {DateOrRange, getDate} from 'topola';
-import {calcAge} from '../util/age_util';
-import {compareDates, formatDateOrRange} from '../util/date_util';
+import {calcAge} from '../../util/age_util';
+import {compareDates, formatDateOrRange} from '../../util/date_util';
import {
dereference,
GedcomData,
@@ -20,7 +20,7 @@ import {
resolveDate,
resolveType,
Source,
-} from '../util/gedcom_util';
+} from '../../util/gedcom_util';
import {FileEntry} from './additional-files';
import {EventExtras, Image} from './event-extras';
import {TranslatedTag} from './translated-tag';
diff --git a/src/details/multiline-text.tsx b/src/sidepanel/details/multiline-text.tsx
similarity index 100%
rename from src/details/multiline-text.tsx
rename to src/sidepanel/details/multiline-text.tsx
diff --git a/src/details/sources.tsx b/src/sidepanel/details/sources.tsx
similarity index 91%
rename from src/details/sources.tsx
rename to src/sidepanel/details/sources.tsx
index 3f81ea3..c563d3b 100644
--- a/src/details/sources.tsx
+++ b/src/sidepanel/details/sources.tsx
@@ -1,8 +1,8 @@
import {useIntl} from 'react-intl';
import Linkify from 'react-linkify';
import {List} from 'semantic-ui-react';
-import {formatDateOrRange} from '../util/date_util';
-import {Source} from '../util/gedcom_util';
+import {formatDateOrRange} from '../../util/date_util';
+import {Source} from '../../util/gedcom_util';
interface Props {
sources?: Source[];
diff --git a/src/details/translated-tag.tsx b/src/sidepanel/details/translated-tag.tsx
similarity index 100%
rename from src/details/translated-tag.tsx
rename to src/sidepanel/details/translated-tag.tsx
diff --git a/src/details/wrapped-image.tsx b/src/sidepanel/details/wrapped-image.tsx
similarity index 100%
rename from src/details/wrapped-image.tsx
rename to src/sidepanel/details/wrapped-image.tsx
diff --git a/src/sidepanel/side-panel.tsx b/src/sidepanel/side-panel.tsx
new file mode 100644
index 0000000..28a96e9
--- /dev/null
+++ b/src/sidepanel/side-panel.tsx
@@ -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: () => ,
+ },
+ {
+ menuItem: intl.formatMessage({
+ id: 'tab.settings',
+ defaultMessage: 'Settings',
+ }),
+ render: () => ,
+ },
+ ];
+
+ return (
+
+ );
+}