tcmd: Accordion and popover md elements
This commit is contained in:
parent
2e9bfa1557
commit
ff0a4280e2
5
actions/temp.ts
Normal file
5
actions/temp.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { readFile } from "fs/promises";
|
||||||
|
|
||||||
|
export const readMD = async () => await readFile("./test.md", "utf-8");
|
@ -8,7 +8,6 @@
|
|||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
@apply dark:bg-mixed-100 bg-primary-600
|
@apply dark:bg-mixed-100 bg-primary-600
|
||||||
/* background: linear-gradient(47deg, black, #620072, #120072); */
|
|
||||||
}
|
}
|
||||||
input {
|
input {
|
||||||
@apply py-2 px-4 rounded-full dark:bg-mixed-200 dark:bg-mixed-600 placeholder:text-dark-500
|
@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 {
|
.btn-secondary {
|
||||||
@apply text-primary-500 py-4 px-6 font-bold text-lg
|
@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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,7 @@ export default function RootLayout({
|
|||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body className={inter.className + " flex min-h-[100vh]"}>
|
<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">
|
<h1 className="text-lg font-bold pb-6 border-b dark:border-dark-500 border-primary-600">
|
||||||
Tabletop Commander
|
Tabletop Commander
|
||||||
</h1>
|
</h1>
|
||||||
@ -42,9 +42,10 @@ export default function RootLayout({
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
<main className="p-8 w-full">
|
<main className="p-8 w-full ml-64">
|
||||||
{children}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
|
<div id="root-portal"></div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
12
app/page.tsx
12
app/page.tsx
@ -1,3 +1,4 @@
|
|||||||
|
import { readMD } from "@/actions/temp";
|
||||||
import { TCMD } from "@/components/tcmd";
|
import { TCMD } from "@/components/tcmd";
|
||||||
import {
|
import {
|
||||||
BookOpenIcon,
|
BookOpenIcon,
|
||||||
@ -16,7 +17,8 @@ export default function Home() {
|
|||||||
<h2 className="strapline">Tabletop Commander</h2>
|
<h2 className="strapline">Tabletop Commander</h2>
|
||||||
<h1 className="text-5xl font-bold">How does it work?</h1>
|
<h1 className="text-5xl font-bold">How does it work?</h1>
|
||||||
</section>
|
</section>
|
||||||
<section className="w-full my-6">
|
{
|
||||||
|
/* <section className="w-full my-6">
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<p>
|
<p>
|
||||||
Tabletop Commander (TC) is a rules-and-tools app for tabletop games
|
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
|
will be done. If this makes it to production, tell Emma she forgot to
|
||||||
turn the home page into magic
|
turn the home page into magic
|
||||||
</cite>
|
</cite>
|
||||||
</section>
|
</section> */
|
||||||
|
}
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<TCMD
|
<TCMD
|
||||||
body={(async () => {
|
body={readMD()}
|
||||||
"use server";
|
|
||||||
return await readFile("./test.md", "utf-8");
|
|
||||||
})()}
|
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</>
|
</>
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { Accordion, AccordionContent } from "@/lib/accordion";
|
||||||
|
import { Poppable } from "@/lib/poppables/components/poppable";
|
||||||
import { createElements } from "@/lib/tcmd";
|
import { createElements } from "@/lib/tcmd";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import React, { FC, Fragment, ReactNode, use, useMemo } from "react";
|
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>
|
// <pre>{JSON.stringify(elements,null,2)}</pre>
|
||||||
// </div>
|
// </div>
|
||||||
<div>
|
<div>
|
||||||
{elements.map(renderBlock)}
|
{elements.map((e, i) => (
|
||||||
|
<Fragment key={"tcmd-block" + i}>{renderBlock(e)}</Fragment>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -20,7 +24,9 @@ export const TCMD: FC<{ body: Promise<string> }> = ({ body }) => {
|
|||||||
const renderBlock = (block: BlockChildren): ReactNode => {
|
const renderBlock = (block: BlockChildren): ReactNode => {
|
||||||
switch (block.type) {
|
switch (block.type) {
|
||||||
case "block":
|
case "block":
|
||||||
return block.children.map(renderBlock);
|
return block.children.map((e, i) => (
|
||||||
|
<Fragment key={"tcmd-block" + i}>{renderBlock(e)}</Fragment>
|
||||||
|
));
|
||||||
case "grid":
|
case "grid":
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -30,14 +36,36 @@ const renderBlock = (block: BlockChildren): ReactNode => {
|
|||||||
className="grid grid-cols-dynamic gap-x-8 gap-y-6 mb-6"
|
className="grid grid-cols-dynamic gap-x-8 gap-y-6 mb-6"
|
||||||
>
|
>
|
||||||
{block.children.map((c, i) => (
|
{block.children.map((c, i) => (
|
||||||
<Fragment key={"block-grid" + c.type + i}>
|
<div key={"block-grid" + c.type + i}>
|
||||||
{renderBlock(c)}
|
{renderBlock(c)}
|
||||||
</Fragment>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
case "card":
|
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:
|
default:
|
||||||
return (
|
return (
|
||||||
renderParagraph(block as ParagraphToken)
|
renderParagraph(block as ParagraphToken)
|
||||||
@ -48,7 +76,15 @@ const renderBlock = (block: BlockChildren): ReactNode => {
|
|||||||
const renderParagraph = (p: ParagraphToken) => {
|
const renderParagraph = (p: ParagraphToken) => {
|
||||||
switch (p.type) {
|
switch (p.type) {
|
||||||
case "p":
|
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":
|
case "code":
|
||||||
return (
|
return (
|
||||||
<pre className="whitespace-pre-wrap">
|
<pre className="whitespace-pre-wrap">
|
||||||
@ -57,9 +93,9 @@ const renderParagraph = (p: ParagraphToken) => {
|
|||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<p className="bg-red-600 text-white">
|
<div className="p bg-red-600 text-white">
|
||||||
Block or paragraph missing implementation: {p.type}
|
Block or paragraph missing implementation: {p.type}
|
||||||
</p>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -68,45 +104,49 @@ const renderToken = (t: Token) => {
|
|||||||
switch (t.type) {
|
switch (t.type) {
|
||||||
case "h1":
|
case "h1":
|
||||||
return (
|
return (
|
||||||
<p
|
<div
|
||||||
id={t.raw.toLowerCase().replace(/[^a-z\s]/ig, "").trim().replaceAll(
|
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)}
|
{renderInlineToken(t.line)}
|
||||||
</p>
|
</div>
|
||||||
);
|
);
|
||||||
case "h2":
|
case "h2":
|
||||||
return (
|
return (
|
||||||
<p
|
<div
|
||||||
id={t.raw.toLowerCase().replace(/[^a-z\s]/ig, "").trim().replaceAll(
|
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)}
|
{renderInlineToken(t.line)}
|
||||||
</p>
|
</div>
|
||||||
);
|
);
|
||||||
case "h3":
|
case "h3":
|
||||||
return (
|
return (
|
||||||
<p
|
<div
|
||||||
id={t.raw.toLowerCase().replace(/[^a-z\s]/ig, "").trim().replaceAll(
|
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)}
|
{renderInlineToken(t.line)}
|
||||||
</p>
|
</div>
|
||||||
);
|
);
|
||||||
case "p":
|
case "p":
|
||||||
return (
|
return (
|
||||||
<p>
|
<div className="p">
|
||||||
{t.lines.map(renderToken)}
|
{t.lines.map((e, i) => (
|
||||||
</p>
|
<Fragment key={"p-line" + i + e.type}>
|
||||||
|
{renderInlineToken(e.line)}
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
case "text":
|
case "text":
|
||||||
return (
|
return (
|
||||||
@ -121,10 +161,10 @@ const renderToken = (t: Token) => {
|
|||||||
return <li className="list-disc ml-8">{renderInlineToken(t.line)}</li>;
|
return <li className="list-disc ml-8">{renderInlineToken(t.line)}</li>;
|
||||||
default:
|
default:
|
||||||
return (
|
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 })
|
Missing implementation for tcMD element `{(t as { type: string })
|
||||||
.type}`
|
.type}`
|
||||||
</p>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -132,27 +172,44 @@ const renderToken = (t: Token) => {
|
|||||||
const renderInlineToken = (l: Line) => {
|
const renderInlineToken = (l: Line) => {
|
||||||
if (typeof l === "string") return l;
|
if (typeof l === "string") return l;
|
||||||
|
|
||||||
return l.map((token) => {
|
return l.map((token) => (
|
||||||
switch (token.type) {
|
<Fragment key={"inline_token" + token.content}>
|
||||||
case "text":
|
{(() => {
|
||||||
return <span>{token.content}</span>;
|
switch (token.type) {
|
||||||
case "bold":
|
case "text":
|
||||||
return <span className="font-bold">{token.content}</span>;
|
return <span>{token.content}</span>;
|
||||||
case "anchor":
|
case "bold":
|
||||||
return (
|
return <span className="font-bold">{token.content}</span>;
|
||||||
<Link className="text-primary-600" href={token.data.href}>
|
case "anchor":
|
||||||
{token.content}
|
return (
|
||||||
</Link>
|
<Link className="text-primary-600" href={token.data.href}>
|
||||||
);
|
{token.content}
|
||||||
case "image":
|
</Link>
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
);
|
||||||
return <img src={token.data.src} alt={token.content} />;
|
case "image":
|
||||||
default:
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
return (
|
return <img src={token.data.src} alt={token.content} />;
|
||||||
<span className="bg-red-500">
|
case "popover":
|
||||||
Inline element not implemented: {token.type}
|
return (
|
||||||
</span>
|
<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
12
hooks/useDebounce.ts
Normal 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
40
hooks/useInput.ts
Normal 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
126
hooks/useObjectState.ts
Normal 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
10
hooks/useQueryParams.ts
Normal 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
19
hooks/useRefCallback.ts
Normal 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
80
lib/accordion/index.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
170
lib/poppables/components/poppable-content.tsx
Normal file
170
lib/poppables/components/poppable-content.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
59
lib/poppables/components/poppable.tsx
Normal file
59
lib/poppables/components/poppable.tsx
Normal 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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
27
lib/portal/components/index.ts
Normal file
27
lib/portal/components/index.ts
Normal 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);
|
||||||
|
};
|
@ -21,7 +21,6 @@ const tokenize = (body: string) => {
|
|||||||
let openBT = blockTokens.findLast((bt) => !bt.closed);
|
let openBT = blockTokens.findLast((bt) => !bt.closed);
|
||||||
if (block) {
|
if (block) {
|
||||||
if (typeof block === "string") {
|
if (typeof block === "string") {
|
||||||
console.log(block);
|
|
||||||
if (openBT) {
|
if (openBT) {
|
||||||
openBT.closed = true;
|
openBT.closed = true;
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@ export const inlineTokens: {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rx: /(?<!\!)\[(.*?)\]\((.*?)\)/g,
|
rx: /(?<![\!\?|^])\[(.*?)\]\((.*?)\)/g,
|
||||||
create(content, start, end, tokens) {
|
create(content, start, end, tokens) {
|
||||||
const [_, label, href] = content;
|
const [_, label, href] = content;
|
||||||
tokens.push({
|
tokens.push({
|
||||||
@ -60,4 +60,22 @@ export const inlineTokens: {
|
|||||||
return l;
|
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;
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
@ -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
1
lib/utils/bulkRound.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const bulkRound = (...args: number[]): number[] => args.map(n => Math.round(n));
|
2
lib/utils/clamp.ts
Normal file
2
lib/utils/clamp.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export const clamp = (value: number, min: number, max: number) =>
|
||||||
|
Math.max(Math.min(value, max), min)
|
@ -4,8 +4,10 @@ const config: Config = {
|
|||||||
content: [
|
content: [
|
||||||
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
"./lib/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
],
|
],
|
||||||
|
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
backgroundImage: {
|
backgroundImage: {
|
||||||
@ -42,6 +44,9 @@ const config: Config = {
|
|||||||
gridTemplateColumns: {
|
gridTemplateColumns: {
|
||||||
dynamic: "repeat(var(--grid-cols), minmax(0,1fr))",
|
dynamic: "repeat(var(--grid-cols), minmax(0,1fr))",
|
||||||
},
|
},
|
||||||
|
height: {
|
||||||
|
variable: "var(--v-height)",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [],
|
||||||
|
31
test.md
31
test.md
@ -4,16 +4,17 @@
|
|||||||
|
|
||||||
[[
|
[[
|
||||||
|
|
||||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
|
Lorem ^[ipsum](This is a popover test) dolor sit amet, consectetur adipiscing
|
||||||
incididunt ut labore et dolore magna aliqua. Sollicitudin tempor id eu nisl nunc
|
elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
|
||||||
mi ipsum faucibus vitae. Lobortis elementum nibh tellus molestie nunc. Purus non
|
Sollicitudin tempor id eu nisl nunc mi ipsum faucibus vitae. Lobortis elementum
|
||||||
enim praesent elementum facilisis leo vel. Orci nulla pellentesque dignissim
|
nibh tellus molestie nunc. Purus non enim praesent elementum facilisis leo vel.
|
||||||
enim sit amet venenatis. Eu feugiat pretium nibh ipsum. Gravida dictum fusce ut
|
Orci nulla pellentesque dignissim enim sit amet venenatis. Eu feugiat pretium
|
||||||
placerat orci nulla pellentesque. Tincidunt vitae semper quis lectus nulla at
|
nibh ipsum. Gravida dictum fusce ut placerat orci nulla pellentesque. Tincidunt
|
||||||
volutpat diam ut. Proin sed libero enim sed faucibus turpis in eu mi. Dui sapien
|
vitae semper quis lectus nulla at volutpat diam ut. Proin sed libero enim sed
|
||||||
eget mi proin sed libero enim sed faucibus. Felis donec et odio pellentesque
|
faucibus turpis in eu mi. Dui sapien eget mi proin sed libero enim sed faucibus.
|
||||||
diam volutpat commodo sed egestas. Massa tincidunt dui ut ornare lectus sit amet
|
Felis donec et odio pellentesque diam volutpat commodo sed egestas. Massa
|
||||||
est placerat. Auctor urna nunc id cursus metus aliquam eleifend.
|
tincidunt dui ut ornare lectus sit amet est placerat. Auctor urna nunc id cursus
|
||||||
|
metus aliquam eleifend.
|
||||||
|
|
||||||
- Lorem ipsum dolor sit amet,
|
- Lorem ipsum dolor sit amet,
|
||||||
- consectetur adipiscing elit, bananana banana ban anana anaba bananananana
|
- 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
|
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
|
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
|
mi ipsum faucibus vitae. Lobortis elementum nibh tellus molestie nunc. Purus non
|
||||||
|
4
types.d.ts
vendored
4
types.d.ts
vendored
@ -1,5 +1,5 @@
|
|||||||
type InlineToken = {
|
type InlineToken = {
|
||||||
type: "text" | "bold" | "anchor" | "image";
|
type: "text" | "bold" | "anchor" | "image" | "popover";
|
||||||
content: string;
|
content: string;
|
||||||
data?: any;
|
data?: any;
|
||||||
};
|
};
|
||||||
@ -42,7 +42,7 @@ type MultilineCfg = {
|
|||||||
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
|
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
|
||||||
|
|
||||||
type BlockToken = {
|
type BlockToken = {
|
||||||
type: "block" | "grid" | "card";
|
type: "block" | "grid" | "card" | "accordion";
|
||||||
metadata: any;
|
metadata: any;
|
||||||
children: BlockChildren[];
|
children: BlockChildren[];
|
||||||
parent?: string;
|
parent?: string;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user