aboutsummaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2022-06-05 23:10:51 -0300
committerSebastian <sebasjm@gmail.com>2022-06-05 23:11:38 -0300
commitabb47b60ad6aa82f68c88c10b0fa614785cd123c (patch)
tree79fa39379ece376dd106c3865ca4b2f8531dc30d /packages
parentfee5de75624a1d8e42ac695876ab188ab2d92921 (diff)
dev script without storybook
Diffstat (limited to 'packages')
-rwxr-xr-xpackages/anastasis-webui/clean_and_build.sh2
-rwxr-xr-xpackages/anastasis-webui/dev.mjs83
-rw-r--r--packages/anastasis-webui/package.json5
-rw-r--r--packages/anastasis-webui/src/pages/home/index.storiesNo.tsx80
-rw-r--r--packages/anastasis-webui/src/pages/home/index.tsx2
-rw-r--r--packages/anastasis-webui/src/stories.tsx381
-rw-r--r--packages/anastasis-webui/src/utils/index.tsx26
-rw-r--r--packages/anastasis-webui/stories.html72
8 files changed, 639 insertions, 12 deletions
diff --git a/packages/anastasis-webui/clean_and_build.sh b/packages/anastasis-webui/clean_and_build.sh
index 3da450c6b..85047b6e1 100755
--- a/packages/anastasis-webui/clean_and_build.sh
+++ b/packages/anastasis-webui/clean_and_build.sh
@@ -11,7 +11,7 @@ cp \
echo css
pnpm exec sass -I . ./src/scss/main.scss dist/main.css &
echo js
-pnpm exec esbuild --log-level=error --bundle src/main.ts --outdir=dist --target=es6 --loader:.scss=text --loader:.svg=dataurl --format=iife --sourcemap --jsx-factory=h --jsx-fragment=Fragment --platform=browser &
+pnpm exec esbuild --log-level=error --bundle src/main.ts --outdir=dist --target=es6 --loader:.svg=dataurl --format=iife --sourcemap --jsx-factory=h --jsx-fragment=Fragment --platform=browser &
wait -n
wait -n
diff --git a/packages/anastasis-webui/dev.mjs b/packages/anastasis-webui/dev.mjs
new file mode 100755
index 000000000..d6b6bf10d
--- /dev/null
+++ b/packages/anastasis-webui/dev.mjs
@@ -0,0 +1,83 @@
+#!/usr/bin/env node
+/* eslint-disable no-undef */
+import esbuild from 'esbuild'
+import fs from 'fs';
+import WebSocket from "ws";
+import chokidar from "chokidar";
+
+const devServerBroadcastDelay = 500
+const devServerPort = 8002
+const wss = new WebSocket.Server({ port: devServerPort });
+const toWatch = ["./src"]
+
+function broadcast(file, event) {
+ setTimeout(() => {
+ wss.clients.forEach((client) => {
+ if (client.readyState === WebSocket.OPEN) {
+ console.log(new Date(), file)
+ client.send(JSON.stringify(event));
+ }
+ });
+ }, devServerBroadcastDelay);
+}
+
+const watcher = chokidar
+ .watch(toWatch, {
+ persistent: true,
+ ignoreInitial: true,
+ awaitWriteFinish: {
+ stabilityThreshold: 100,
+ pollInterval: 100,
+ },
+ })
+ .on("error", (error) => console.error(error))
+ .on("change", async (file) => {
+ broadcast(file, { type: "RELOAD" });
+ })
+ .on("add", async (file) => {
+ broadcast(file, { type: "RELOAD" });
+ })
+ .on("unlink", async (file) => {
+ broadcast(file, { type: "RELOAD" });
+ });
+
+
+fs.writeFileSync("dist/stories.html", fs.readFileSync("stories.html"))
+fs.writeFileSync("dist/mocha.css", fs.readFileSync("node_modules/mocha/mocha.css"))
+fs.writeFileSync("dist/mocha.js", fs.readFileSync("node_modules/mocha/mocha.js"))
+fs.writeFileSync("dist/mocha.js.map", fs.readFileSync("node_modules/mocha/mocha.js.map"))
+
+export const buildConfig = {
+ entryPoints: ['src/stories.tsx'],
+ bundle: true,
+ outdir: 'dist',
+ minify: false,
+ loader: {
+ '.svg': 'dataurl',
+ },
+ target: [
+ 'es6'
+ ],
+ format: 'iife',
+ platform: 'browser',
+ sourcemap: true,
+ jsxFactory: 'h',
+ jsxFragment: 'Fragment',
+}
+
+const server = await esbuild
+ .serve({ servedir: 'dist' }, {
+ ...buildConfig, outdir: 'dist'
+ })
+ .catch((e) => {
+ console.log(e)
+ process.exit(1)
+ });
+
+console.log(`Dev server is ready at http://localhost:${server.port}/.
+http://localhost:${server.port}/stories.html for the components stories.
+The server is running a using websocket at ${devServerPort} to notify code change and live reload.
+`);
+
+
+
diff --git a/packages/anastasis-webui/package.json b/packages/anastasis-webui/package.json
index a855ffa94..0cdb1be71 100644
--- a/packages/anastasis-webui/package.json
+++ b/packages/anastasis-webui/package.json
@@ -29,12 +29,15 @@
"@gnu-taler/anastasis-core": "workspace:*",
"@gnu-taler/taler-util": "workspace:*",
"base64-inline-loader": "1.1.1",
+ "chokidar": "^3.5.3",
"date-fns": "2.28.0",
"jed": "1.1.1",
+ "mocha": "^9.2.0",
"preact": "^10.5.15",
"preact-render-to-string": "^5.1.19",
"preact-router": "^3.2.1",
- "qrcode-generator": "^1.4.4"
+ "qrcode-generator": "^1.4.4",
+ "ws": "7.4.5"
},
"devDependencies": {
"@creativebulma/bulma-tooltip": "^1.2.0",
diff --git a/packages/anastasis-webui/src/pages/home/index.storiesNo.tsx b/packages/anastasis-webui/src/pages/home/index.storiesNo.tsx
new file mode 100644
index 000000000..5355b6c1a
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/index.storiesNo.tsx
@@ -0,0 +1,80 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import * as a1 from "./RecoveryFinishedScreen.stories.js";
+import * as a3 from "./ContinentSelectionScreen.stories.js";
+import * as a4 from "./ReviewPoliciesScreen.stories.js";
+import * as a5 from "./authMethod/AuthMethodSmsSolve.stories.js";
+import * as a6 from "./authMethod/AuthMethodSmsSetup.stories.js";
+import * as a7 from "./authMethod/AuthMethodPostSetup.stories.js";
+import * as a8 from "./authMethod/AuthMethodEmailSetup.stories.js";
+import * as a9 from "./authMethod/AuthMethodIbanSetup.stories.js";
+import * as a10 from "./authMethod/AuthMethodQuestionSolve.stories.js";
+import * as a11 from "./authMethod/AuthMethodIbanSolve.stories.js";
+import * as a12 from "./authMethod/AuthMethodTotpSolve.stories.js";
+import * as a13 from "./authMethod/AuthMethodPostSolve.stories.js";
+import * as a14 from "./authMethod/AuthMethodTotpSetup.stories.js";
+import * as a15 from "./authMethod/AuthMethodEmailSolve.stories.js";
+import * as a16 from "./authMethod/AuthMethodQuestionSetup.stories.js";
+import * as a17 from "./ChallengePayingScreen.stories.js";
+import * as a18 from "./AuthenticationEditorScreen.stories.js";
+import * as a19 from "./SecretSelectionScreen.stories.js";
+import * as a20 from "./PoliciesPayingScreen.stories.js";
+import * as a21 from "./BackupFinishedScreen.stories.js";
+import * as a22 from "./SecretEditorScreen.stories.js";
+import * as a23 from "./AddingProviderScreen.stories.js";
+import * as a24 from "./StartScreen.stories.js";
+import * as a25 from "./ChallengeOverviewScreen.stories.js";
+import * as a26 from "./TruthsPayingScreen.stories.js";
+import * as a27 from "./EditPoliciesScreen.stories.js";
+import * as a28 from "./AttributeEntryScreen.stories.js";
+import * as a29 from "./SolveScreen.stories.js";
+
+export default [
+ a1,
+ a3,
+ a4,
+ a5,
+ a6,
+ a7,
+ a8,
+ a9,
+ a10,
+ a11,
+ a12,
+ a13,
+ a14,
+ a15,
+ a16,
+ a17,
+ a18,
+ a19,
+ a20,
+ a21,
+ a22,
+ a23,
+ a24,
+ a25,
+ a26,
+ a27,
+ a28,
+ a29,
+];
diff --git a/packages/anastasis-webui/src/pages/home/index.tsx b/packages/anastasis-webui/src/pages/home/index.tsx
index 03bf21577..47b62c7e8 100644
--- a/packages/anastasis-webui/src/pages/home/index.tsx
+++ b/packages/anastasis-webui/src/pages/home/index.tsx
@@ -157,7 +157,6 @@ export function AnastasisClientFrame(props: AnastasisClientFrameProps): VNode {
return (
<Fragment>
- <Menu title="Anastasis" />
<div class="home" onKeyPress={(e) => handleKeyPress(e)}>
<h1 class="title">{props.title}</h1>
<ErrorBanner />
@@ -195,6 +194,7 @@ const AnastasisClient: FunctionalComponent = () => {
return (
<AnastasisProvider value={reducer}>
<ErrorBoundary reducer={reducer}>
+ <Menu title="Anastasis" />
<AnastasisClientImpl />
</ErrorBoundary>
</AnastasisProvider>
diff --git a/packages/anastasis-webui/src/stories.tsx b/packages/anastasis-webui/src/stories.tsx
new file mode 100644
index 000000000..2b830766f
--- /dev/null
+++ b/packages/anastasis-webui/src/stories.tsx
@@ -0,0 +1,381 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { setupI18n } from "@gnu-taler/taler-util";
+import { ComponentChild, Fragment, h, render, VNode } from "preact";
+import { useEffect, useErrorBoundary, useState } from "preact/hooks";
+import { strings } from "./i18n/strings.js";
+import * as pages from "./pages/home/index.storiesNo.js";
+
+const url = new URL(window.location.href);
+const lang = url.searchParams.get("lang") || "en";
+
+setupI18n(lang, strings);
+
+const Page = ({ children }: any) => <div class="page">{children}</div>;
+const SideBar = ({ children }: any) => <div class="sidebar">{children}</div>;
+const Content = ({ children }: any) => <div class="content">{children}</div>;
+
+function parseExampleImport(group: string, im: any): ComponentItem {
+ const component = im.default.title;
+ const order: number = im.default.args?.order || 0;
+ return {
+ name: component,
+ order,
+ examples: Object.entries(im)
+ .filter(([k]) => k !== "default")
+ .map(
+ ([name, render]) =>
+ ({
+ group,
+ component,
+ name,
+ render,
+ } as ExampleItem),
+ ),
+ };
+}
+
+function SortStories(a: any, b: any): number {
+ return (a?.order ?? 0) - (b?.order ?? 0);
+}
+
+const allExamples = Object.entries({ pages }).map(([title, value]) => ({
+ title,
+ list: value.default
+ .map((s) => parseExampleImport(title, s))
+ .sort(SortStories),
+}));
+
+interface ComponentItem {
+ name: string;
+ order: number;
+ examples: ExampleItem[];
+}
+
+interface ExampleItem {
+ group: string;
+ component: string;
+ name: string;
+ render: {
+ (args: any): VNode;
+ args: any;
+ };
+}
+
+function findByGroupComponentName(
+ group: string,
+ component: string,
+ name: string,
+): ExampleItem | undefined {
+ const gl = allExamples.filter((e) => e.title === group);
+ if (gl.length === 0) {
+ return undefined;
+ }
+ const cl = gl[0].list.filter((l) => l.name === component);
+ if (cl.length === 0) {
+ return undefined;
+ }
+ const el = cl[0].examples.filter((c) => c.name === name);
+ if (el.length === 0) {
+ return undefined;
+ }
+ return el[0];
+}
+
+function getContentForExample(item: ExampleItem | undefined): () => VNode {
+ if (!item)
+ return function SelectExampleMessage() {
+ return <div>select example from the list on the left</div>;
+ };
+ const example = findByGroupComponentName(
+ item.group,
+ item.component,
+ item.name,
+ );
+ if (!example)
+ return function ExampleNotFoundMessage() {
+ return <div>example not found</div>;
+ };
+ return () => example.render(example.render.args);
+}
+
+function ExampleList({
+ name,
+ list,
+ selected,
+ onSelectStory,
+}: {
+ name: string;
+ list: {
+ name: string;
+ examples: ExampleItem[];
+ }[];
+ selected: ExampleItem | undefined;
+ onSelectStory: (i: ExampleItem, id: string) => void;
+}): VNode {
+ const [isOpen, setOpen] = useState(selected && selected.group === name);
+ return (
+ <ol>
+ <div onClick={() => setOpen(!isOpen)}>{name}</div>
+ <div data-hide={!isOpen}>
+ {list.map((k) => (
+ <li key={k.name}>
+ <dl>
+ <dt>{k.name}</dt>
+ {k.examples.map((r) => {
+ const e = encodeURIComponent;
+ const eId = `${e(r.group)}-${e(r.component)}-${e(r.name)}`;
+ const isSelected =
+ selected &&
+ selected.component === r.component &&
+ selected.group === r.group &&
+ selected.name === r.name;
+ return (
+ <dd id={eId} key={r.name} data-selected={isSelected}>
+ <a
+ href={`#${eId}`}
+ onClick={(e) => {
+ e.preventDefault();
+ location.hash = `#${eId}`;
+ onSelectStory(r, eId);
+ }}
+ >
+ {r.name}
+ </a>
+ </dd>
+ );
+ })}
+ </dl>
+ </li>
+ ))}
+ </div>
+ </ol>
+ );
+}
+
+// function getWrapperForGroup(group: string): FunctionComponent {
+// switch (group) {
+// case "popup":
+// return function PopupWrapper({ children }: any) {
+// return (
+// <Fragment>
+// <PopupNavBar />
+// <PopupBox>{children}</PopupBox>
+// </Fragment>
+// );
+// };
+// case "wallet":
+// return function WalletWrapper({ children }: any) {
+// return (
+// <Fragment>
+// <LogoHeader />
+// <WalletNavBar />
+// <WalletBox>{children}</WalletBox>
+// </Fragment>
+// );
+// };
+// case "cta":
+// return function WalletWrapper({ children }: any) {
+// return (
+// <Fragment>
+// <WalletBox>{children}</WalletBox>
+// </Fragment>
+// );
+// };
+// default:
+// return Fragment;
+// }
+// }
+
+function ErrorReport({
+ children,
+ selected,
+}: {
+ children: ComponentChild;
+ selected: ExampleItem | undefined;
+}): VNode {
+ const [error] = useErrorBoundary();
+ if (error) {
+ return (
+ <div class="error_report">
+ <p>Error was thrown trying to render</p>
+ {selected && (
+ <ul>
+ <li>
+ <b>group</b>: {selected.group}
+ </li>
+ <li>
+ <b>component</b>: {selected.component}
+ </li>
+ <li>
+ <b>example</b>: {selected.name}
+ </li>
+ <li>
+ <b>args</b>:{" "}
+ <pre>{JSON.stringify(selected.render.args, undefined, 2)}</pre>
+ </li>
+ </ul>
+ )}
+ <p>{error.message}</p>
+ <pre>{error.stack}</pre>
+ </div>
+ );
+ }
+ return <Fragment>{children}</Fragment>;
+}
+
+function getSelectionFromLocationHash(hash: string): ExampleItem | undefined {
+ if (!hash) return undefined;
+ const parts = hash.substring(1).split("-");
+ if (parts.length < 3) return undefined;
+ return findByGroupComponentName(
+ decodeURIComponent(parts[0]),
+ decodeURIComponent(parts[1]),
+ decodeURIComponent(parts[2]),
+ );
+}
+
+function Application(): VNode {
+ const initialSelection = getSelectionFromLocationHash(location.hash);
+ const [selected, updateSelected] = useState<ExampleItem | undefined>(
+ initialSelection,
+ );
+ useEffect(() => {
+ if (location.hash) {
+ const hash = location.hash.substring(1);
+ const found = document.getElementById(hash);
+ if (found) {
+ setTimeout(() => {
+ found.scrollIntoView({
+ block: "center",
+ });
+ }, 10);
+ }
+ }
+ }, []);
+
+ const ExampleContent = getContentForExample(selected);
+
+ // const GroupWrapper = getWrapperForGroup(selected?.group || "default");
+
+ return (
+ <Page>
+ <LiveReload />
+ <SideBar>
+ {allExamples.map((e) => (
+ <ExampleList
+ key={e.title}
+ name={e.title}
+ list={e.list}
+ selected={selected}
+ onSelectStory={(item, htmlId) => {
+ document.getElementById(htmlId)?.scrollIntoView({
+ block: "center",
+ });
+ updateSelected(item);
+ }}
+ />
+ ))}
+ <hr />
+ </SideBar>
+ <Content>
+ <ErrorReport selected={selected}>
+ {/* <GroupWrapper> */}
+ <ExampleContent />
+ {/* </GroupWrapper> */}
+ </ErrorReport>
+ </Content>
+ </Page>
+ );
+}
+
+if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", main);
+} else {
+ main();
+}
+function main(): void {
+ try {
+ const container = document.getElementById("container");
+ if (!container) {
+ throw Error("container not found, can't mount page contents");
+ }
+ render(<Application />, container);
+ } catch (e) {
+ console.error("got error", e);
+ if (e instanceof Error) {
+ document.body.innerText = `Fatal error: "${e.message}". Please report this bug at https://bugs.gnunet.org/.`;
+ }
+ }
+}
+
+let liveReloadMounted = false;
+function LiveReload({ port = 8002 }: { port?: number }): VNode {
+ const [isReloading, setIsReloading] = useState(false);
+ useEffect(() => {
+ if (!liveReloadMounted) {
+ setupLiveReload(port, () => {
+ setIsReloading(true);
+ window.location.reload();
+ });
+ liveReloadMounted = true;
+ }
+ });
+
+ if (isReloading) {
+ return (
+ <div
+ style={{
+ position: "absolute",
+ width: "100%",
+ height: "100%",
+ backgroundColor: "rgba(0,0,0,0.5)",
+ color: "white",
+ display: "flex",
+ justifyContent: "center",
+ }}
+ >
+ <h1 style={{ margin: "auto" }}>reloading...</h1>
+ </div>
+ );
+ }
+ return <Fragment />;
+}
+
+function setupLiveReload(port: number, onReload: () => void): void {
+ const protocol = location.protocol === "https:" ? "wss:" : "ws:";
+ const host = location.hostname;
+ const socketPath = `${protocol}//${host}:${port}/socket`;
+
+ const ws = new WebSocket(socketPath);
+ ws.onmessage = (message) => {
+ const event = JSON.parse(message.data);
+ if (event.type === "LOG") {
+ console.log(event.message);
+ }
+ if (event.type === "RELOAD") {
+ onReload();
+ }
+ };
+ ws.onerror = (error) => {
+ console.error(error);
+ };
+}
diff --git a/packages/anastasis-webui/src/utils/index.tsx b/packages/anastasis-webui/src/utils/index.tsx
index 4dace0934..2e502cacb 100644
--- a/packages/anastasis-webui/src/utils/index.tsx
+++ b/packages/anastasis-webui/src/utils/index.tsx
@@ -1,5 +1,8 @@
/* eslint-disable @typescript-eslint/camelcase */
import {
+ AuthenticationProviderStatus,
+ AuthenticationProviderStatusError,
+ AuthenticationProviderStatusOk,
BackupStates,
RecoveryStates,
ReducerState,
@@ -115,6 +118,7 @@ const base = {
],
authentication_providers: {
"http://localhost:8086/": {
+ status: "ok",
http_status: 200,
annual_fee: "COL:0",
business_name: "Anastasis Local",
@@ -134,11 +138,12 @@ const base = {
usage_fee: "COL:0",
},
],
- salt: "WBMDD76BR1E90YQ5AHBMKPH7GW",
+ provider_salt: "WBMDD76BR1E90YQ5AHBMKPH7GW",
storage_limit_in_megabytes: 16,
truth_upload_fee: "COL:0",
- },
+ } as AuthenticationProviderStatusOk,
"https://kudos.demo.anastasis.lu/": {
+ status: "ok",
http_status: 200,
annual_fee: "COL:0",
business_name: "Anastasis Kudo",
@@ -154,11 +159,12 @@ const base = {
usage_fee: "COL:0",
},
],
- salt: "WBMDD76BR1E90YQ5AHBMKPH7GW",
+ provider_salt: "WBMDD76BR1E90YQ5AHBMKPH7GW",
storage_limit_in_megabytes: 16,
truth_upload_fee: "COL:0",
- },
+ } as AuthenticationProviderStatusOk,
"https://anastasis.demo.taler.net/": {
+ status: "ok",
http_status: 200,
annual_fee: "COL:0",
business_name: "Anastasis Demo",
@@ -178,23 +184,23 @@ const base = {
usage_fee: "COL:0",
},
],
- salt: "WBMDD76BR1E90YQ5AHBMKPH7GW",
+ provider_salt: "WBMDD76BR1E90YQ5AHBMKPH7GW",
storage_limit_in_megabytes: 16,
truth_upload_fee: "COL:0",
- },
+ } as AuthenticationProviderStatusOk,
"http://localhost:8087/": {
code: 8414,
hint: "request to provider failed",
- },
+ } as AuthenticationProviderStatusError,
"http://localhost:8088/": {
code: 8414,
hint: "request to provider failed",
- },
+ } as AuthenticationProviderStatusError,
"http://localhost:8089/": {
code: 8414,
hint: "request to provider failed",
- },
+ } as AuthenticationProviderStatusError,
},
} as Partial<ReducerState>;
@@ -210,6 +216,7 @@ export const reducerStatesExample = {
} as ReducerState,
secretSelection: {
...base,
+ reducer_type: "recovery",
recovery_state: RecoveryStates.SecretSelecting,
} as ReducerState,
recoveryFinished: {
@@ -260,6 +267,7 @@ export const reducerStatesExample = {
authEditing: {
...base,
backup_state: BackupStates.AuthenticationsEditing,
+ reducer_type: "backup",
} as ReducerState,
backupAttributeEditing: {
...base,
diff --git a/packages/anastasis-webui/stories.html b/packages/anastasis-webui/stories.html
new file mode 100644
index 000000000..9f41fdeaf
--- /dev/null
+++ b/packages/anastasis-webui/stories.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Stories</title>
+ <style>
+ /* page css */
+ div.page {
+ margin: 0px;
+ padding: 0px;
+ font-size: 100%;
+ font-family: Arial, Helvetica, sans-serif;
+ }
+ div.page p:not([class]) {
+ margin-bottom: 1em;
+ margin-top: 1em;
+ }
+ div.page {
+ width: 100%;
+ display: flex;
+ flex-direction: row;
+ }
+ /* sidebar css */
+ div.sidebar {
+ min-width: 200px;
+ height: calc(100vh - 20px);
+ overflow-y: visible;
+ overflow-x: hidden;
+ scroll-behavior: smooth;
+ }
+ div.sidebar > ol {
+ padding: 4px;
+ }
+ div.sidebar div:first-child {
+ background-color: lightcoral;
+ cursor: pointer;
+ }
+ div.sidebar div[data-hide="true"] {
+ display: none;
+ }
+ div.sidebar dd {
+ margin-left: 1em;
+ padding: 4px;
+ cursor: pointer;
+ border-radius: 4px;
+ margin-bottom: 4px;
+ }
+ div.sidebar dd:nth-child(even) {
+ background-color: lightgray;
+ }
+ div.sidebar dd:nth-child(odd) {
+ background-color: lightblue;
+ }
+ div.sidebar a {
+ color: black;
+ }
+ div.sidebar dd[data-selected] {
+ background-color: green;
+ }
+
+ /* content css */
+ div.content {
+ width: 100%;
+ padding: 20px;
+ }
+ </style>
+ <script src="./stories.js"></script>
+ <link rel="stylesheet" href="./main.css" />
+ </head>
+ <body>
+ <taler-stories id="container"></taler-stories>
+ </body>
+</html>