tcmd: Accordion and popover md elements

This commit is contained in:
Emmaline Autumn 2024-02-28 21:41:12 -07:00
parent 2e9bfa1557
commit ff0a4280e2
22 changed files with 729 additions and 68 deletions

5
actions/temp.ts Normal file
View File

@ -0,0 +1,5 @@
"use server";
import { readFile } from "fs/promises";
export const readMD = async () => await readFile("./test.md", "utf-8");

View File

@ -8,7 +8,6 @@
}
body {
@apply dark:bg-mixed-100 bg-primary-600
/* background: linear-gradient(47deg, black, #620072, #120072); */
}
input {
@apply py-2 px-4 rounded-full dark:bg-mixed-200 dark:bg-mixed-600 placeholder:text-dark-500
@ -38,4 +37,11 @@
.btn-secondary {
@apply text-primary-500 py-4 px-6 font-bold text-lg
}
.p {
@apply py-1
}
.poppable {
@apply card bg-mixed-300 p-2 rounded-lg transition-opacity data-[visible=true]:z-10 data-[visible=true]:opacity-100 data-[visible=false]:opacity-0 -z-10 max-w-[400px] absolute
}
}

View File

@ -23,7 +23,7 @@ export default function RootLayout({
return (
<html lang="en">
<body className={inter.className + " flex min-h-[100vh]"}>
<nav className="h-[100vh] p-8 rounded-r-3xl dark:bg-mixed-300 bg-primary-400 w-max shadow-2xl">
<nav className="h-[100vh] fixed top-0 left-0 bottom-0 p-8 rounded-r-3xl dark:bg-mixed-300 bg-primary-400 w-max shadow-2xl">
<h1 className="text-lg font-bold pb-6 border-b dark:border-dark-500 border-primary-600">
Tabletop Commander
</h1>
@ -42,9 +42,10 @@ export default function RootLayout({
</li>
</ul>
</nav>
<main className="p-8 w-full">
<main className="p-8 w-full ml-64">
{children}
</main>
<div id="root-portal"></div>
</body>
</html>
);

View File

@ -1,3 +1,4 @@
import { readMD } from "@/actions/temp";
import { TCMD } from "@/components/tcmd";
import {
BookOpenIcon,
@ -16,7 +17,8 @@ export default function Home() {
<h2 className="strapline">Tabletop Commander</h2>
<h1 className="text-5xl font-bold">How does it work?</h1>
</section>
<section className="w-full my-6">
{
/* <section className="w-full my-6">
<div className="card">
<p>
Tabletop Commander (TC) is a rules-and-tools app for tabletop games
@ -135,13 +137,11 @@ export default function Home() {
will be done. If this makes it to production, tell Emma she forgot to
turn the home page into magic
</cite>
</section>
</section> */
}
<Suspense>
<TCMD
body={(async () => {
"use server";
return await readFile("./test.md", "utf-8");
})()}
body={readMD()}
/>
</Suspense>
</>

View File

@ -1,5 +1,7 @@
"use client";
import { Accordion, AccordionContent } from "@/lib/accordion";
import { Poppable } from "@/lib/poppables/components/poppable";
import { createElements } from "@/lib/tcmd";
import Link from "next/link";
import React, { FC, Fragment, ReactNode, use, useMemo } from "react";
@ -12,7 +14,9 @@ export const TCMD: FC<{ body: Promise<string> }> = ({ body }) => {
// <pre>{JSON.stringify(elements,null,2)}</pre>
// </div>
<div>
{elements.map(renderBlock)}
{elements.map((e, i) => (
<Fragment key={"tcmd-block" + i}>{renderBlock(e)}</Fragment>
))}
</div>
);
};
@ -20,7 +24,9 @@ export const TCMD: FC<{ body: Promise<string> }> = ({ body }) => {
const renderBlock = (block: BlockChildren): ReactNode => {
switch (block.type) {
case "block":
return block.children.map(renderBlock);
return block.children.map((e, i) => (
<Fragment key={"tcmd-block" + i}>{renderBlock(e)}</Fragment>
));
case "grid":
return (
<div
@ -30,14 +36,36 @@ const renderBlock = (block: BlockChildren): ReactNode => {
className="grid grid-cols-dynamic gap-x-8 gap-y-6 mb-6"
>
{block.children.map((c, i) => (
<Fragment key={"block-grid" + c.type + i}>
<div key={"block-grid" + c.type + i}>
{renderBlock(c)}
</Fragment>
</div>
))}
</div>
);
case "card":
return <div className="card">{block.children.map(renderBlock)}</div>;
return (
<div className="card">
{block.children.map((e, i) => (
<Fragment key={"card-block" + i + e.type}>
{renderBlock(e)}
</Fragment>
))}
</div>
);
case "accordion":
return (
<Accordion
title={block.metadata.title || "Expand"}
>
<AccordionContent>
{block.children.map((e, i) => (
<Fragment key={"accordion" + e.type + "i"}>
{renderBlock(e)}
</Fragment>
))}
</AccordionContent>
</Accordion>
);
default:
return (
renderParagraph(block as ParagraphToken)
@ -48,7 +76,15 @@ const renderBlock = (block: BlockChildren): ReactNode => {
const renderParagraph = (p: ParagraphToken) => {
switch (p.type) {
case "p":
return <p>{p.content.map(renderToken)}</p>;
return (
<div className="p">
{p.content.map((e, i) => (
<Fragment key={"p-paragraph" + i + e.type}>
{renderToken(e)}
</Fragment>
))}
</div>
);
case "code":
return (
<pre className="whitespace-pre-wrap">
@ -57,9 +93,9 @@ const renderParagraph = (p: ParagraphToken) => {
);
default:
return (
<p className="bg-red-600 text-white">
<div className="p bg-red-600 text-white">
Block or paragraph missing implementation: {p.type}
</p>
</div>
);
}
};
@ -68,45 +104,49 @@ const renderToken = (t: Token) => {
switch (t.type) {
case "h1":
return (
<p
<div
id={t.raw.toLowerCase().replace(/[^a-z\s]/ig, "").trim().replaceAll(
" ",
"-",
)}
className="font-bold text-2xl"
className="font-bold text-2xl p"
>
{renderInlineToken(t.line)}
</p>
</div>
);
case "h2":
return (
<p
<div
id={t.raw.toLowerCase().replace(/[^a-z\s]/ig, "").trim().replaceAll(
" ",
"-",
)}
className="font-bold text-xl"
className="font-bold text-xl p"
>
{renderInlineToken(t.line)}
</p>
</div>
);
case "h3":
return (
<p
<div
id={t.raw.toLowerCase().replace(/[^a-z\s]/ig, "").trim().replaceAll(
" ",
"-",
)}
className="font-bold text-lg"
className="font-bold text-lg p"
>
{renderInlineToken(t.line)}
</p>
</div>
);
case "p":
return (
<p>
{t.lines.map(renderToken)}
</p>
<div className="p">
{t.lines.map((e, i) => (
<Fragment key={"p-line" + i + e.type}>
{renderInlineToken(e.line)}
</Fragment>
))}
</div>
);
case "text":
return (
@ -121,10 +161,10 @@ const renderToken = (t: Token) => {
return <li className="list-disc ml-8">{renderInlineToken(t.line)}</li>;
default:
return (
<p className="text-white bg-red-500">
<div className="text-white bg-red-500 p">
Missing implementation for tcMD element `{(t as { type: string })
.type}`
</p>
</div>
);
}
};
@ -132,27 +172,44 @@ const renderToken = (t: Token) => {
const renderInlineToken = (l: Line) => {
if (typeof l === "string") return l;
return l.map((token) => {
switch (token.type) {
case "text":
return <span>{token.content}</span>;
case "bold":
return <span className="font-bold">{token.content}</span>;
case "anchor":
return (
<Link className="text-primary-600" href={token.data.href}>
{token.content}
</Link>
);
case "image":
// eslint-disable-next-line @next/next/no-img-element
return <img src={token.data.src} alt={token.content} />;
default:
return (
<span className="bg-red-500">
Inline element not implemented: {token.type}
</span>
);
}
});
return l.map((token) => (
<Fragment key={"inline_token" + token.content}>
{(() => {
switch (token.type) {
case "text":
return <span>{token.content}</span>;
case "bold":
return <span className="font-bold">{token.content}</span>;
case "anchor":
return (
<Link className="text-primary-600" href={token.data.href}>
{token.content}
</Link>
);
case "image":
// eslint-disable-next-line @next/next/no-img-element
return <img src={token.data.src} alt={token.content} />;
case "popover":
return (
<Poppable
content={token.data.popover}
preferredAlign="centered"
preferredEdge="bottom"
className="cursor-pointer"
>
<span className="border-b border-dotted border-white">
{token.content}
</span>
</Poppable>
);
default:
return (
<span className="bg-red-500">
Inline element not implemented: {token.type}
</span>
);
}
})()}
</Fragment>
));
};

12
hooks/useDebounce.ts Normal file
View File

@ -0,0 +1,12 @@
import { useEffect, useState } from 'react';
export const useDebounce = (value: any, delay: number) => {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
};

40
hooks/useInput.ts Normal file
View File

@ -0,0 +1,40 @@
import { useState, ChangeEvent } from 'react';
type InputHookConfig = {
disallowSpaces?: boolean;
spaceReplacer?: string;
}
export const useInput = <T extends string | number>(initialValue: T, config?: InputHookConfig) => {
const [value, setValue] = useState<T>(initialValue);
return {
value,
setValue,
reset: () => setValue(initialValue),
bind: {
value: value,
onChange: (event: ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
const changed: string | number = typeof initialValue === 'number' ? parseInt(event.target.value) : config?.disallowSpaces ? event.target.value.replace(' ', config.spaceReplacer || '_') : event.target.value;
setValue(changed as T);
}
}
};
};
export const useCheckbox = (initial: boolean) => {
const [value, setValue] = useState(initial);
return {
value,
setValue,
reset: () => setValue(initial),
bind: {
checked: value,
onClick: () => {
setValue(v => !v);
},
readOnly: true
}
};
};

126
hooks/useObjectState.ts Normal file
View File

@ -0,0 +1,126 @@
import React, { ChangeEvent, useCallback, useState } from "react";
import { InputBinder } from "../types/inputBinder";
type ObjectStateHookConfig = {
disallowSpaces?: boolean;
spaceReplacer?: string;
};
export const useObjectState = <T extends object>(initial: T) => {
const [state, setState] = useState<T>(initial || {} as T);
const bindProperty = useCallback(
<K extends keyof T>(property: K, config?: ObjectStateHookConfig) => ({
value: state[property] ?? "",
name: property,
onChange: (
event: ChangeEvent<
HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
>,
) =>
setState((value) => (
{
...value,
[event.target.name]: (
(typeof value[property] === "number")
? Number(event.target.value) || 0
: config?.disallowSpaces
? event.target.value.replace(" ", config.spaceReplacer || "_")
: event.target.value
),
}
)),
}),
[state],
);
const bindPropertyCheck = useCallback(<K extends keyof T>(property: K) => ({
checked: !!state[property],
name: property,
onChange: (event: ChangeEvent<HTMLInputElement>) =>
setState((value) => ({
...value,
[event.target.name]: (event.target.checked),
})),
readOnly: true,
}), [state]);
const update = useCallback(
(updates: Partial<T>) => setState((s) => ({ ...s, ...updates })),
[],
);
const reset = useCallback(() => {
setState(initial);
}, [initial]);
return {
bindProperty,
bindPropertyCheck,
update,
state,
setState,
reset,
};
};
export const useObjectStateWrapper = <T extends object>(
state: T,
setState: React.Dispatch<React.SetStateAction<T>>,
) => {
const bindProperty = useCallback(
<K extends keyof T>(
property: K,
config?: ObjectStateHookConfig,
): InputBinder => ({
value: state[property]?.toString() ?? "",
name: property.toString(),
onChange: (
event: ChangeEvent<
HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
>,
) =>
setState((value) => (
{
...value,
[event.target.name]: (
(typeof value[property] === "number")
? Number(event.target.value) || 0
: config?.disallowSpaces
? event.target.value.replace(" ", config.spaceReplacer || "_")
: event.target.value
),
}
)),
}),
[setState, state],
);
const bindPropertyCheck = useCallback(<K extends keyof T>(property: K) => ({
checked: !!state[property],
name: property,
onChange: (event: ChangeEvent<HTMLInputElement>) =>
setState((value) => ({
...value,
[event.target.name]: (event.target.checked),
})),
readOnly: true,
}), [setState, state]);
const update = useCallback(
(updates: Partial<T> | ((arg: T) => Partial<T>)) =>
setState((s) => ({
...s,
...(typeof updates === "function" ? updates(s) : updates),
})),
[setState],
);
return {
bindProperty,
bindPropertyCheck,
update,
state,
setState,
};
};

10
hooks/useQueryParams.ts Normal file
View File

@ -0,0 +1,10 @@
import { useEffect, useRef } from 'react'
import { QueryParams } from '../utils/queryParams'
export const useQueryParams = (options?: {clean: boolean}) => {
const queryParams = useRef(new QueryParams(options))
useEffect(() => {
console.log(queryParams.current.get('test'))
}, [queryParams.current])
}

19
hooks/useRefCallback.ts Normal file
View File

@ -0,0 +1,19 @@
import { ReactNode, useCallback, useRef } from 'react'
export const useRefCallback = <T = ReactNode>() : [T | null, (arg: T) => void] => {
const ref = useRef<T | null>(null);
const setRef = useCallback((val: T) => {
console.log(val);
if (ref.current) {
// does something?
}
if (val) {
// also does something?
}
ref.current = val
}, [])
return [ref.current, setRef]
}

80
lib/accordion/index.tsx Normal file
View File

@ -0,0 +1,80 @@
import { FC, PropsWithChildren, ReactNode, useCallback, useState } from "react";
interface IProps {
expandOnHover?: boolean;
expanded?: boolean;
title?: ReactNode;
}
export const Accordion: FC<PropsWithChildren<IProps>> = (
{ children, expandOnHover, expanded, title },
) => {
const [open, setOpen] = useState(false);
return (
<div
data-expanded={open || expanded}
data-expandonhover={expandOnHover}
className={(expandOnHover ? "group/hover" : "group/controlled") +
" group"}
onClick={() => !title && !expandOnHover && setOpen(!open)}
>
{!!title && (
<div
className="flex justify-between cursor-pointer"
onClick={() => !expandOnHover && setOpen(!open)}
>
{title}
<div
className={`
group-hover/hover:-rotate-180
group-data-[expanded]:-rotate-180
transition-transform
duration-500
grid
rounded-full
h-min
mr-2
mt-1
scale-y-50
`}
>
<span className="block w-2 h-2 rotate-45 border-r-2 border-b-2 place-self-center">
</span>
<span className="block w-2 h-2 rotate-45 border-r-2 border-b-2 place-self-center">
</span>
</div>
</div>
)}
{children}
</div>
);
};
export const AccordionContent: FC<PropsWithChildren> = ({ children }) => {
const [height, setHeight] = useState(0);
const updateRef = useCallback((node: HTMLDivElement | null) => {
if (node) {
setHeight(node.clientHeight);
} else {
setHeight(0);
}
}, []);
const Child = () => (
<div className="absolute bottom-0 w-full" ref={updateRef}>
{children}
</div>
);
return (
<div className="relative overflow-hidden">
{<Child />}
<span
style={{ ["--v-height" as never]: height + "px" }}
className="w-0 block h-0 group-hover/hover:h-variable group-data-[expanded]/controlled:h-variable transition-all duration-700"
/>
</div>
);
};

View File

@ -0,0 +1,170 @@
import {
Dispatch,
FC,
PropsWithChildren,
SetStateAction,
useCallback,
useEffect,
useState,
} from "react";
import { bulkRound } from "../../utils/bulkRound";
import { clamp } from "../../utils/clamp";
import { Portal } from "../../portal/components";
type edge = "top" | "bottom" | "left" | "right";
type alignment = edge | "centered";
interface IProps {
preferredEdge: edge;
preferredAlign: alignment;
relativeElement: HTMLElement;
spacing?: number;
isClosing: boolean;
isClosed: boolean;
setHover: Dispatch<SetStateAction<boolean>>;
}
type position = { top: number; left: number; width?: number };
export const PoppableContent: FC<PropsWithChildren<IProps>> = (
{
preferredAlign,
preferredEdge,
children,
relativeElement,
spacing = 10,
setHover,
isClosing,
isClosed,
},
) => {
const [popRef, setPopRef] = useState<HTMLDivElement>();
const updateRef = useCallback((node: HTMLDivElement) => {
if (!node) return;
setPopRef(node);
}, []);
const getAlignment = useCallback(
(
relX: number,
relY: number,
relWidth: number,
relHeight: number,
popWidth: number,
popHeight: number,
edge: edge,
align: alignment,
): position => {
const pos = {
top: relY,
left: relX,
};
switch (align) {
case "centered":
pos.top = relY + (relHeight / 2) - (popHeight / 2);
pos.left = relX + (relWidth / 2) - (popWidth / 2);
break;
case "top":
pos.top = relY;
break;
case "bottom":
pos.top = relY + relHeight - popHeight;
break;
case "left":
pos.left = relX;
break;
case "right":
pos.left = relX + relWidth - popWidth;
break;
}
return pos;
},
[],
);
const getPosition = useCallback(
(popWidth: number, popHeight: number, edge: edge, align: alignment) => {
const rel = relativeElement.getBoundingClientRect();
const [relX, relY, relWidth, relHeight] = bulkRound(
rel.x,
rel.y,
rel.width,
rel.height,
);
const pos: position = { top: 100, left: 100 };
const alignment = getAlignment(
relX,
relY,
relWidth,
relHeight,
popWidth,
popHeight,
edge,
align,
);
switch (edge) {
case "top":
pos.top = relY - popHeight - spacing +
document.documentElement.scrollTop;
pos.left = alignment.left;
break;
case "bottom":
pos.top = relY + relHeight + spacing +
document.documentElement.scrollTop;
pos.left = alignment.left;
break;
case "left":
pos.left = relX - popWidth - spacing;
pos.top = alignment.top + document.documentElement.scrollTop;
break;
case "right":
pos.left = relX + relWidth + spacing;
pos.top = alignment.top + document.documentElement.scrollTop;
break;
}
return pos;
},
[getAlignment, relativeElement, spacing],
);
const getClampedPosition = useCallback(() => {
if (!popRef) return { opacity: 0 };
const pop = popRef.getBoundingClientRect();
const [popWidth, popHeight] = bulkRound(pop.width, pop.height);
const pos = getPosition(popWidth, popHeight, preferredEdge, preferredAlign);
const { innerHeight, innerWidth } = window;
pos.top = ["left", "right"].includes(preferredEdge)
? clamp(pos.top, spacing, innerHeight - popHeight - spacing)
: pos.top;
pos.left = ["top", "bottom"].includes(preferredEdge)
? clamp(pos.left, spacing, innerWidth - popWidth - spacing)
: pos.left;
return pos;
}, [popRef, getPosition, preferredEdge, preferredAlign, spacing]);
return (
<Portal>
<div
ref={updateRef}
style={getClampedPosition()}
data-fading={isClosing}
data-visible={!isClosing}
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
// className="absolute w-[400px] border"
className="poppable"
>
{children}
</div>
</Portal>
);
};

View File

@ -0,0 +1,59 @@
import {
FC,
PropsWithChildren,
ReactNode,
useCallback,
useEffect,
useState,
} from "react";
import { PoppableContent } from "./poppable-content";
import { useDebounce } from "../../../hooks/useDebounce";
interface IProps {
content: ReactNode;
className?: string;
preferredEdge: "top" | "bottom" | "left" | "right";
preferredAlign: "centered" | "top" | "bottom" | "left" | "right";
spacing?: number;
}
export const Poppable: FC<PropsWithChildren<IProps>> = (
{ className, content, children, preferredEdge, preferredAlign, spacing },
) => {
const [isHovered, setIsHovered] = useState(false);
const closing = useDebounce(!isHovered, 1000);
const closed = useDebounce(closing, 300);
const [ref, setRef] = useState<HTMLElement>();
const updateRef = useCallback((node: HTMLElement) => {
if (!node) return;
setRef(node);
}, []);
return (
<>
<span
ref={updateRef}
className={className}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{children}
</span>
{!!ref && (
<PoppableContent
preferredAlign={preferredAlign}
preferredEdge={preferredEdge}
spacing={spacing}
isClosing={closing}
isClosed={closed}
relativeElement={ref}
setHover={setIsHovered}
>
{content}
</PoppableContent>
)}
</>
);
};

View File

@ -0,0 +1,27 @@
import { FC, PropsWithChildren, useEffect, useState } from "react";
import { createPortal } from "react-dom";
interface IProps {
className?: string;
el?: string;
}
export const Portal: FC<PropsWithChildren<IProps>> = (
{ children, className = "root-portal", el = "div" },
) => {
const [container] = useState(() => {
// This will be executed only on the initial render
// https://reactjs.org/docs/hooks-reference.html#lazy-initial-state
return document.getElementById("root-portal")!;
});
useEffect(() => {
container.classList.add(className);
document.body.appendChild(container);
return () => {
document.body.removeChild(container);
};
}, [className, container]);
return createPortal(children, container);
};

View File

@ -21,7 +21,6 @@ const tokenize = (body: string) => {
let openBT = blockTokens.findLast((bt) => !bt.closed);
if (block) {
if (typeof block === "string") {
console.log(block);
if (openBT) {
openBT.closed = true;
}

View File

@ -24,7 +24,7 @@ export const inlineTokens: {
},
},
{
rx: /(?<!\!)\[(.*?)\]\((.*?)\)/g,
rx: /(?<![\!\?|^])\[(.*?)\]\((.*?)\)/g,
create(content, start, end, tokens) {
const [_, label, href] = content;
tokens.push({
@ -60,4 +60,22 @@ export const inlineTokens: {
return l;
},
},
{
rx: /\^\[(.*?)\]\((.*?)\)/g,
create(content, start, end, tokens) {
const [_, text, popover] = content;
tokens.push({
content: text,
end,
start,
type: "popover",
data: {
popover,
},
});
},
replace(l) {
return l;
},
},
];

View File

@ -41,4 +41,17 @@ const blockTokens: {
};
},
},
{
rx: /\[accordion\s?([a-z\s]*)\]/,
closeRx: /\[\/accordion\]/,
create(line) {
const title = line.match(this.rx)?.at(1);
return {
type: "accordion",
metadata: { title },
children: [],
closed: false,
};
},
},
];

1
lib/utils/bulkRound.ts Normal file
View File

@ -0,0 +1 @@
export const bulkRound = (...args: number[]): number[] => args.map(n => Math.round(n));

2
lib/utils/clamp.ts Normal file
View File

@ -0,0 +1,2 @@
export const clamp = (value: number, min: number, max: number) =>
Math.max(Math.min(value, max), min)

View File

@ -4,8 +4,10 @@ const config: Config = {
content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./lib/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
backgroundImage: {
@ -42,6 +44,9 @@ const config: Config = {
gridTemplateColumns: {
dynamic: "repeat(var(--grid-cols), minmax(0,1fr))",
},
height: {
variable: "var(--v-height)",
},
},
},
plugins: [],

31
test.md
View File

@ -4,16 +4,17 @@
[[
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua. Sollicitudin tempor id eu nisl nunc
mi ipsum faucibus vitae. Lobortis elementum nibh tellus molestie nunc. Purus non
enim praesent elementum facilisis leo vel. Orci nulla pellentesque dignissim
enim sit amet venenatis. Eu feugiat pretium nibh ipsum. Gravida dictum fusce ut
placerat orci nulla pellentesque. Tincidunt vitae semper quis lectus nulla at
volutpat diam ut. Proin sed libero enim sed faucibus turpis in eu mi. Dui sapien
eget mi proin sed libero enim sed faucibus. Felis donec et odio pellentesque
diam volutpat commodo sed egestas. Massa tincidunt dui ut ornare lectus sit amet
est placerat. Auctor urna nunc id cursus metus aliquam eleifend.
Lorem ^[ipsum](This is a popover test) dolor sit amet, consectetur adipiscing
elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Sollicitudin tempor id eu nisl nunc mi ipsum faucibus vitae. Lobortis elementum
nibh tellus molestie nunc. Purus non enim praesent elementum facilisis leo vel.
Orci nulla pellentesque dignissim enim sit amet venenatis. Eu feugiat pretium
nibh ipsum. Gravida dictum fusce ut placerat orci nulla pellentesque. Tincidunt
vitae semper quis lectus nulla at volutpat diam ut. Proin sed libero enim sed
faucibus turpis in eu mi. Dui sapien eget mi proin sed libero enim sed faucibus.
Felis donec et odio pellentesque diam volutpat commodo sed egestas. Massa
tincidunt dui ut ornare lectus sit amet est placerat. Auctor urna nunc id cursus
metus aliquam eleifend.
- Lorem ipsum dolor sit amet,
- consectetur adipiscing elit, bananana banana ban anana anaba bananananana
@ -74,6 +75,16 @@ const blockTokens: {
[[
[accordion this is the title]
this is the test of a single accordion
[/accordion]
]]
[[
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua. Sollicitudin tempor id eu nisl nunc
mi ipsum faucibus vitae. Lobortis elementum nibh tellus molestie nunc. Purus non

4
types.d.ts vendored
View File

@ -1,5 +1,5 @@
type InlineToken = {
type: "text" | "bold" | "anchor" | "image";
type: "text" | "bold" | "anchor" | "image" | "popover";
content: string;
data?: any;
};
@ -42,7 +42,7 @@ type MultilineCfg = {
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
type BlockToken = {
type: "block" | "grid" | "card";
type: "block" | "grid" | "card" | "accordion";
metadata: any;
children: BlockChildren[];
parent?: string;