cambiar forma de navegación mucho
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
Cat /dev/Nulo 2023-09-07 12:19:53 -03:00
parent 230c6a8732
commit dc773b14ff
17 changed files with 219 additions and 291 deletions

View file

@ -48,6 +48,7 @@
"idb-keyval": "^6.2.1", "idb-keyval": "^6.2.1",
"nanoid": "^4.0.2", "nanoid": "^4.0.2",
"navaid": "^1.2.0", "navaid": "^1.2.0",
"prosemirror-inputrules": "^1.2.1",
"regexparam": "^2.0.1", "regexparam": "^2.0.1",
"y-indexeddb": "^9.0.10", "y-indexeddb": "^9.0.10",
"y-prosemirror": "^1.2.1", "y-prosemirror": "^1.2.1",

View file

@ -23,6 +23,9 @@ dependencies:
navaid: navaid:
specifier: ^1.2.0 specifier: ^1.2.0
version: 1.2.0 version: 1.2.0
prosemirror-inputrules:
specifier: ^1.2.1
version: 1.2.1
regexparam: regexparam:
specifier: ^2.0.1 specifier: ^2.0.1
version: 2.0.1 version: 2.0.1
@ -1317,6 +1320,13 @@ packages:
rope-sequence: 1.3.3 rope-sequence: 1.3.3
dev: true dev: true
/prosemirror-inputrules@1.2.1:
resolution: {integrity: sha512-3LrWJX1+ULRh5SZvbIQlwZafOXqp1XuV21MGBu/i5xsztd+9VD15x6OtN6mdqSFI7/8Y77gYUbQ6vwwJ4mr6QQ==}
dependencies:
prosemirror-state: 1.4.2
prosemirror-transform: 1.7.1
dev: false
/prosemirror-keymap@1.2.1: /prosemirror-keymap@1.2.1:
resolution: {integrity: sha512-kVK6WGC+83LZwuSJnuCb9PsADQnFZllt94qPP3Rx/vLcOUV65+IbBeH2nS5cFggPyEVJhGkGrgYFRrG250WhHQ==} resolution: {integrity: sha512-kVK6WGC+83LZwuSJnuCb9PsADQnFZllt94qPP3Rx/vLcOUV65+IbBeH2nS5cFggPyEVJhGkGrgYFRrG250WhHQ==}
dependencies: dependencies:

View file

@ -0,0 +1,41 @@
<script lang="ts">
import { onDestroy, onMount } from "svelte";
/* https://wicg.github.io/visual-viewport/examples/fixed-to-keyboard.html */
let barStyle = "";
function updateBar() {
const viewport = window.visualViewport!;
// Since the bar is position: fixed we need to offset it by the
// visual viewport's offset from the layout viewport origin.
const offsetY = window.innerHeight - viewport.height - viewport.offsetTop;
barStyle = `
left: ${viewport.offsetLeft}px;
bottom: ${offsetY}px;
transform: scale(${1 / viewport.scale});
`;
}
onMount(() => {
window.visualViewport!.addEventListener("resize", updateBar);
window.visualViewport!.addEventListener("scroll", updateBar);
});
onDestroy(() => {
window.visualViewport!.removeEventListener("resize", updateBar);
window.visualViewport!.removeEventListener("scroll", updateBar);
});
</script>
<div
class="transform-origin- fixed bottom-0 left-0 z-40 flex w-full flex-col"
style={barStyle}
>
<slot />
</div>
<style>
/* https://wicg.github.io/visual-viewport/examples/fixed-to-keyboard.html */
.transform-origin- {
transform-origin: left bottom;
}
</style>

View file

@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import CloseIcon from "eva-icons/fill/svg/close.svg"; import CloseIcon from "eva-icons/fill/svg/close.svg";
import bodyScroll from "../lib/bodyScroll";
export let onClose: () => void; export let onClose: () => void;
@ -10,6 +11,8 @@
function keydown(event: KeyboardEvent) { function keydown(event: KeyboardEvent) {
if (event.key === "Escape") onClose(); if (event.key === "Escape") onClose();
} }
$: $bodyScroll;
</script> </script>
<div <div
@ -20,10 +23,16 @@
on:click={click} on:click={click}
on:keydown={keydown} on:keydown={keydown}
> >
<div class="backdrop" /> <div class="fixed inset-0 bg-neutral-100/70 dark:bg-neutral-950/70" />
<div class="content-alignment" on:click={click} on:keydown={keydown}> <div
<div class="content shadow-xl"> class="fixed inset-0 z-[269] flex h-screen items-center justify-center overflow-y-auto"
on:click={click}
on:keydown={keydown}
>
<div
class="h-full w-full overflow-y-auto rounded-2xl bg-neutral-100 px-5 py-4 shadow-xl dark:bg-neutral-800 sm:h-auto sm:w-auto"
>
<div class="mb-3 flex items-center justify-between"> <div class="mb-3 flex items-center justify-between">
<h3 class="text-2xl" id="modal-title"> <h3 class="text-2xl" id="modal-title">
<slot name="title" /> <slot name="title" />
@ -109,29 +118,4 @@
position: relative; position: relative;
z-index: 169; z-index: 169;
} }
.backdrop {
position: fixed;
inset: 0;
background: var(--background);
opacity: 0.9;
}
.content-alignment {
display: flex;
height: 100vh;
position: fixed;
inset: 0;
z-index: 269;
align-items: center;
justify-content: center;
overflow-y: auto;
}
.content {
overflow-y: auto;
background: var(--background);
padding: 16px 20px;
border-radius: 16px;
}
</style> </style>

View file

@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { onDestroy, onMount, tick } from "svelte";
import type { Writable } from "svelte/store"; import type { Writable } from "svelte/store";
import type { EditorView } from "prosemirror-view"; import type { EditorView } from "prosemirror-view";
import type { EditorState } from "prosemirror-state"; import type { EditorState } from "prosemirror-state";
@ -10,6 +9,8 @@
import StrikethroughIcon from "bootstrap-icons/icons/type-strikethrough.svg"; import StrikethroughIcon from "bootstrap-icons/icons/type-strikethrough.svg";
import LinkIcon from "eva-icons/outline/svg/external-link-outline.svg"; import LinkIcon from "eva-icons/outline/svg/external-link-outline.svg";
import InternalLinkIcon from "eva-icons/outline/svg/menu-arrow-outline.svg"; import InternalLinkIcon from "eva-icons/outline/svg/menu-arrow-outline.svg";
import H2Icon from "bootstrap-icons/icons/type-h2.svg";
import H3Icon from "bootstrap-icons/icons/type-h3.svg";
import type { Command } from "./ps-utils"; import type { Command } from "./ps-utils";
import { import {
@ -23,35 +24,16 @@
import Button from "./bubblemenu/Button.svelte"; import Button from "./bubblemenu/Button.svelte";
import Modal from "../components/Modal.svelte"; import Modal from "../components/Modal.svelte";
import PagePicker from "../components/PagePicker.svelte"; import PagePicker from "../components/PagePicker.svelte";
import BottomFloatingBar from "../components/BottomFloatingBar.svelte";
import type { WorldY } from "../lib/doc"; import type { WorldY } from "../lib/doc";
import Linking from "./menubar/Linking.svelte"; import Linking from "./bubblemenu/Linking.svelte";
import HeadingButton from "./bubblemenu/HeadingButton.svelte";
export let view: EditorView; export let view: EditorView;
export let state: EditorState; export let state: EditorState;
export let worldY: WorldY; export let worldY: WorldY;
export let editingLink: Writable<false | "new" | "selection">; export let editingLink: Writable<false | "new" | "selection">;
let changingProp:
| false
| { type: "link"; url: string }
| { type: "mark" }
| { type: "mark-custom"; color: string } = false;
let linkInputEl: HTMLElement;
$: {
if (state.selection.empty) {
changingProp = false;
}
if (changingProp && changingProp.type === "link") {
tick().then(() => {
linkInputEl.focus();
});
} else {
view.focus();
}
}
function runCommand(command: Command) { function runCommand(command: Command) {
command(state, view.dispatch); command(state, view.dispatch);
} }
@ -86,30 +68,6 @@
} }
const svgStyle = "width: 100%; height: 100%"; const svgStyle = "width: 100%; height: 100%";
/* https://wicg.github.io/visual-viewport/examples/fixed-to-keyboard.html */
let barStyle = "";
function updateBar() {
const viewport = window.visualViewport!;
// Since the bar is position: fixed we need to offset it by the
// visual viewport's offset from the layout viewport origin.
const offsetY = window.innerHeight - viewport.height - viewport.offsetTop;
barStyle = `
left: ${viewport.offsetLeft}px;
bottom: ${offsetY}px;
transform: scale(${1 / viewport.scale});
`;
}
onMount(() => {
window.visualViewport!.addEventListener("resize", updateBar);
window.visualViewport!.addEventListener("scroll", updateBar);
});
onDestroy(() => {
window.visualViewport!.removeEventListener("resize", updateBar);
window.visualViewport!.removeEventListener("scroll", updateBar);
});
</script> </script>
{#if makingInternalLink} {#if makingInternalLink}
@ -119,55 +77,47 @@ transform: scale(${1 / viewport.scale});
</Modal> </Modal>
{/if} {/if}
<div class="floating z-40" style={barStyle}> <BottomFloatingBar>
<Linking {state} /> <Linking {state} />
<div class="bubble" hidden={state.selection.empty}> <div class="bubble flex items-center" hidden={state.selection.empty}>
{#if changingProp === false} <SimpleMarkItem {view} {state} type={view.state.schema.marks.strong}>
<SimpleMarkItem {view} {state} type={view.state.schema.marks.strong} <BoldIcon style={svgStyle} />
><BoldIcon style={svgStyle} /></SimpleMarkItem </SimpleMarkItem>
> <SimpleMarkItem {view} {state} type={view.state.schema.marks.em}>
<SimpleMarkItem {view} {state} type={view.state.schema.marks.em} <ItalicIcon style={svgStyle} />
><ItalicIcon style={svgStyle} /></SimpleMarkItem </SimpleMarkItem>
> <SimpleMarkItem {view} {state} type={view.state.schema.marks.underline}>
<SimpleMarkItem {view} {state} type={view.state.schema.marks.underline} <UnderlineIcon style={svgStyle} />
><UnderlineIcon style={svgStyle} /></SimpleMarkItem </SimpleMarkItem>
> <SimpleMarkItem {view} {state} type={view.state.schema.marks.strikethrough}>
<SimpleMarkItem <StrikethroughIcon style={svgStyle} />
{view} </SimpleMarkItem>
{state} <Button
type={view.state.schema.marks.strikethrough} active={markIsActive(state, view.state.schema.marks.link)}
><StrikethroughIcon style={svgStyle} /></SimpleMarkItem onClick={startEditingLink}
> >
<Button <LinkIcon style={svgStyle} />
active={markIsActive(state, view.state.schema.marks.link)} </Button>
onClick={startEditingLink}><LinkIcon style={svgStyle} /></Button <Button
> active={markIsActive(state, view.state.schema.marks.internal_link)}
<Button onClick={startMakingInternalLink}
active={markIsActive(state, view.state.schema.marks.internal_link)} >
onClick={startMakingInternalLink} <InternalLinkIcon style={svgStyle} />
><InternalLinkIcon style={svgStyle} /></Button </Button>
>
{/if} <span class="mx-2 block h-6 border-r border-neutral-400" />
<HeadingButton {view} {state} level={2}>
<H2Icon style={svgStyle} />
</HeadingButton>
<HeadingButton {view} {state} level={3}>
<H3Icon style={svgStyle} />
</HeadingButton>
</div> </div>
</div> </BottomFloatingBar>
<style> <style>
.floating {
display: flex;
flex-direction: column;
position: fixed;
left: 0px;
bottom: 0px;
padding: 0rem;
/* https://wicg.github.io/visual-viewport/examples/fixed-to-keyboard.html */
transform-origin: left bottom;
width: 100%;
}
.bubble { .bubble {
display: flex;
background: var(--background); background: var(--background);
border-top: 1px solid var(--accent-bg); border-top: 1px solid var(--accent-bg);
width: 100%; width: 100%;

View file

@ -5,6 +5,7 @@
import { dropCursor } from "prosemirror-dropcursor"; import { dropCursor } from "prosemirror-dropcursor";
import { gapCursor } from "prosemirror-gapcursor"; import { gapCursor } from "prosemirror-gapcursor";
import { history } from "prosemirror-history"; import { history } from "prosemirror-history";
import { inputRules, wrappingInputRule } from "prosemirror-inputrules";
import type { XmlFragment } from "yjs"; import type { XmlFragment } from "yjs";
import { ySyncPlugin } from "y-prosemirror"; import { ySyncPlugin } from "y-prosemirror";
@ -12,7 +13,6 @@
import { schema } from "./schema"; import { schema } from "./schema";
import BubbleMenu from "./BubbleMenu.svelte"; import BubbleMenu from "./BubbleMenu.svelte";
import MenuBar from "./MenuBar.svelte";
// import { placeholderPlugin } from "./upload"; // import { placeholderPlugin } from "./upload";
import { baseKeymap } from "./keymap"; import { baseKeymap } from "./keymap";
import type { WorldY } from "../lib/doc"; import type { WorldY } from "../lib/doc";
@ -56,6 +56,14 @@
// yCursorPlugin(doc.webrtcProvider.awareness), // yCursorPlugin(doc.webrtcProvider.awareness),
// yUndoPlugin(), // yUndoPlugin(),
keymap(baseKeymap), keymap(baseKeymap),
inputRules({
rules: [
// https://github.com/ueberdosis/tiptap/blob/6f218be6e439603c85559c6d77ec93a205003bf5/packages/extension-bullet-list/src/bullet-list.ts#L24C27-L24C43
wrappingInputRule(/^\s*([-+*])\s$/, schema.nodes.bullet_list),
wrappingInputRule(/^\s*([0-9]\.)\s$/, schema.nodes.ordered_list),
wrappingInputRule(/^>\s$/, schema.nodes.blockquote),
],
}),
// placeholderPlugin, // placeholderPlugin,
], ],
}); });
@ -73,7 +81,6 @@
<div class="editor min-h-screen"> <div class="editor min-h-screen">
{#if view} {#if view}
<MenuBar {view} state={updatedState} />
<EditLinkMenu state={updatedState} {view} {editingLink} /> <EditLinkMenu state={updatedState} {view} {editingLink} />
<LinkTooltip state={updatedState} {view} {editingLink} /> <LinkTooltip state={updatedState} {view} {editingLink} />
{/if} {/if}

View file

@ -1,35 +0,0 @@
<script lang="ts">
import type { EditorState } from "prosemirror-state";
import type { EditorView } from "prosemirror-view";
import BlockSelect from "./menubar/BlockSelect.svelte";
// import UploadItem from "./menubar/UploadItem.svelte";
import ListItem from "./menubar/ListItem.svelte";
import BlockQuoteItem from "./menubar/BlockQuoteItem.svelte";
import { ListKind } from "./utils";
export let view: EditorView;
export let state: EditorState;
</script>
<div
class="sticky top-0 z-50 flex items-center border-b border-neutral-200/60 bg-white px-4 py-2 dark:border-neutral-700 dark:bg-neutral-800"
>
<BlockSelect {view} {state} />
<!-- <UploadItem {view} {state} /> -->
<ListItem {view} {state} kind={ListKind.Unordered} />
<ListItem {view} {state} kind={ListKind.Ordered} />
<BlockQuoteItem {view} {state} />
</div>
<style lang="postcss">
div :global(button) {
@apply m-1 appearance-none rounded p-2 leading-none transition-colors hover:bg-neutral-600/20 dark:hover:bg-neutral-600/40;
}
div :global(button.active) {
@apply bg-neutral-300 dark:bg-neutral-700;
}
div :global(button svg) {
@apply h-6 w-6;
}
</style>

View file

@ -0,0 +1,29 @@
<script lang="ts">
import type { EditorState } from "prosemirror-state";
import type { EditorView } from "prosemirror-view";
import { commandListener } from "../ps-utils";
import Button from "./Button.svelte";
import { chainCommands, setBlockType } from "prosemirror-commands";
export let view: EditorView;
export let state: EditorState;
export let level: number;
$: type = state.schema.nodes.heading;
$: paragraphType = state.schema.nodes.paragraph;
$: isActive =
state.selection.to <= state.selection.$from.end() &&
state.selection.$from.parent.type === type &&
state.selection.$from.parent.attrs.level === level;
$: listener = commandListener(
view,
chainCommands(setBlockType(type, { level }), setBlockType(paragraphType)),
);
</script>
<Button active={isActive} onClick={listener}>
<slot />
</Button>

View file

@ -15,7 +15,7 @@ export function floatingUi(
options?: Partial<ComputePositionConfig>, options?: Partial<ComputePositionConfig>,
): Readable<Style> { ): Readable<Style> {
return { return {
subscribe(run, invalidate) { subscribe(run) {
return autoUpdate(refEl, tooltipEl, () => { return autoUpdate(refEl, tooltipEl, () => {
computePosition(refEl, tooltipEl, options).then(({ x, y }) => { computePosition(refEl, tooltipEl, options).then(({ x, y }) => {
run(`left: ${x}px; top: ${y}px`); run(`left: ${x}px; top: ${y}px`);

View file

@ -1,27 +0,0 @@
<script lang="ts">
import { wrapIn } from "prosemirror-commands";
import type { EditorState } from "prosemirror-state";
import type { EditorView } from "prosemirror-view";
import BlockquoteIcon from "bootstrap-icons/icons/blockquote-left.svg";
import { commandListener, nodeIsActiveFn } from "../ps-utils";
export let view: EditorView;
export let state: EditorState;
const type = state.schema.nodes.blockquote;
$: isActive = nodeIsActiveFn(type, null, true);
$: command = wrapIn(type);
$: isPossible = command(state);
$: actionListener = commandListener(view, command);
</script>
<button
type="button"
class:active={isActive(state)}
on:mousedown={actionListener}
disabled={!isPossible}><BlockquoteIcon /></button
>

View file

@ -1,49 +0,0 @@
<script lang="ts">
import type { EditorState } from "prosemirror-state";
import type { EditorView } from "prosemirror-view";
import { setBlockType } from "prosemirror-commands";
export let view: EditorView;
export let state: EditorState;
const paragraphType = state.schema.nodes.paragraph;
const headingType = state.schema.nodes.heading;
$: currentValue =
state.selection.to <= state.selection.$from.end() &&
(state.selection.$from.parent.type == headingType
? `heading:${state.selection.$from.parent.attrs.level}`
: state.selection.$from.parent.type == paragraphType
? "paragraph"
: null);
const onChange = (
event: Event & { currentTarget: EventTarget & HTMLSelectElement }
) => {
event.preventDefault();
const [type, param] = event.currentTarget.value.split(":");
if (type === "paragraph") {
setBlockType(paragraphType, {
align: state.selection.$from.parent.attrs.align,
})(state, view.dispatch);
} else if (type === "heading") {
setBlockType(headingType, {
level: parseInt(param),
align: state.selection.$from.parent.attrs.align,
})(state, view.dispatch);
} else {
console.error(`¡type no es heading ni paragraph! Es`, type);
}
};
</script>
<select value={currentValue} on:change={onChange} class="relative flex items-center justify-between py-3 pl-3 pr-10 text-left bg-white dark:bg-neutral-800 border rounded-md shadow-sm cursor-default border-neutral-200/70 dark:border-neutral-700/70 text-sm">
<option value="paragraph">Párrafo</option>
<option value="heading:1">Titulo grande</option>
<option value="heading:2">Titulo mediano</option>
<option value="heading:3">Subtitulo</option>
<option value="heading:4">Subsubtitulo</option>
<option value="heading:5">Subsubsubtitulo</option>
<option value="heading:6">Subsubsubsubtitulo</option>
</select>

View file

@ -1,44 +0,0 @@
<script lang="ts">
import { chainCommands } from "prosemirror-commands";
import { wrapInList } from "prosemirror-schema-list";
import { liftListItem } from "prosemirror-schema-list";
import type { EditorState } from "prosemirror-state";
import type { EditorView } from "prosemirror-view";
import UlIcon from "bootstrap-icons/icons/list-ul.svg";
import OlIcon from "bootstrap-icons/icons/list-ol.svg";
import { ListKind } from "../utils";
import { commandListener, nodeIsActiveFn } from "../ps-utils";
export let view: EditorView;
export let state: EditorState;
export let kind: ListKind;
$: type =
kind === ListKind.Unordered
? state.schema.nodes.bullet_list
: kind === ListKind.Ordered
? state.schema.nodes.ordered_list
: (null as never);
const listItemType = state.schema.nodes.list_item;
$: iconComponent =
kind === ListKind.Unordered
? UlIcon
: kind === ListKind.Ordered
? OlIcon
: (console.error("adsfadsf"), UlIcon);
$: isActive = nodeIsActiveFn(type, null, true);
$: command = chainCommands(liftListItem(listItemType), wrapInList(type));
$: isPossible = command(state);
$: actionListener = commandListener(view, command);
</script>
<button
type="button"
class:active={isActive(state)}
on:mousedown={actionListener}
disabled={!isPossible}><svelte:component this={iconComponent} /></button
>

24
src/lib/bodyScroll.ts Normal file
View file

@ -0,0 +1,24 @@
import type { Readable } from "svelte/store";
// weird way to stop scroll in the body
// when the store is mounted, scroll stops in the body
class BodyScroll implements Readable<boolean> {
num: number = 0;
refresh() {
if (this.num > 0) document.body.style.overflow = "hidden";
else document.body.style.overflow = "";
}
subscribe() {
this.num++;
this.refresh();
return () => {
this.num--;
this.refresh();
};
}
}
const bodyScroll = new BodyScroll();
export default bodyScroll;

View file

@ -1,11 +1,6 @@
import { import { derived, type Readable } from "svelte/store";
derived,
type Readable,
type Subscriber,
type Unsubscriber,
} from "svelte/store";
import type { Doc, Transaction, XmlFragment } from "yjs"; import type { Doc, Transaction, XmlFragment } from "yjs";
import { loadWorlds, worldsStore } from "./worldStorage"; import { worldsStore } from "./worldStorage";
import { getWorldPage, getWorldY } from "./doc"; import { getWorldPage, getWorldY } from "./doc";
export function makeYdocStore<T>( export function makeYdocStore<T>(

View file

@ -1,11 +1,12 @@
<script lang="ts"> <script lang="ts">
import type { XmlFragment } from "yjs"; import type { XmlFragment } from "yjs";
import { inject } from "regexparam"; import { inject } from "regexparam";
import ArrowBackIcon from "eva-icons/fill/svg/arrow-back.svg";
import Editor from "../editor/Editor.svelte"; import Editor from "../editor/Editor.svelte";
import Breadcrumbs from "./Page/Breadcrumbs.svelte";
import { getWorldPage, getWorldY, type WorldY } from "../lib/doc"; import { getWorldPage, getWorldY, type WorldY } from "../lib/doc";
import { routes } from "../lib/routes"; import { routes } from "../lib/routes";
import { loadWorlds } from "../lib/worldStorage"; import { loadWorlds } from "../lib/worldStorage";
import Breadcrumbs from "./Page/Breadcrumbs.svelte";
export let worldId: string; export let worldId: string;
export let pageId: string; export let pageId: string;
@ -33,9 +34,13 @@
}) })
.catch((error) => (state = { error })); .catch((error) => (state = { error }));
} }
function pop() {
history.back();
}
</script> </script>
<nav class="px-4 py-2"> <div class="px-4">
<details> <details>
<summary>Opciones</summary> <summary>Opciones</summary>
<ul> <ul>
@ -47,7 +52,13 @@
</li> </li>
</ul> </ul>
</details> </details>
</div>
<nav
class="sticky top-0 z-10 flex h-10 items-stretch gap-4 bg-white text-neutral-700 shadow dark:bg-neutral-800 dark:text-neutral-200"
>
<button title="Ir a la página anterior" on:click={pop}>
<ArrowBackIcon class="w-10 shrink-0 fill-current pl-2" />
</button>
<Breadcrumbs {pageId} {worldId} /> <Breadcrumbs {pageId} {worldId} />
</nav> </nav>

View file

@ -1,13 +1,14 @@
<script lang="ts"> <script lang="ts">
import { onMount, tick } from "svelte"; import { onMount, tick } from "svelte";
import ChevronRight from "eva-icons/fill/svg/chevron-right.svg"; import ChevronRight from "eva-icons/fill/svg/chevron-right.svg";
import ArrowDown from "eva-icons/fill/svg/arrow-down.svg";
import { inject } from "regexparam"; import { inject } from "regexparam";
import breadcrumbs from "../../lib/breadcrumbs"; import breadcrumbs from "../../lib/breadcrumbs";
import { pageStore } from "../../lib/makeYdocStore"; import { pageStore } from "../../lib/makeYdocStore";
import { derived } from "svelte/store"; import { derived } from "svelte/store";
import { getTitle } from "../../lib/getTitle"; import { getTitle } from "../../lib/getTitle";
import { routes } from "../../lib/routes"; import { routes } from "../../lib/routes";
import type { HTMLOlAttributes } from "svelte/elements"; import Modal from "../../components/Modal.svelte";
export let worldId: string; export let worldId: string;
export let pageId: string; export let pageId: string;
@ -26,6 +27,7 @@
).subscribe(set); ).subscribe(set);
}, },
); );
$: currentTitle = $crumbsTitles[$crumbsTitles.length - 1];
let breadcrumbsEl: HTMLDivElement; let breadcrumbsEl: HTMLDivElement;
let breadcrumbsListEl: HTMLOListElement; let breadcrumbsListEl: HTMLOListElement;
@ -49,22 +51,51 @@
resizeObserver.observe(breadcrumbsEl); resizeObserver.observe(breadcrumbsEl);
return () => resizeObserver.disconnect(); return () => resizeObserver.disconnect();
}); });
let breadcrumbsModalOpen = false;
</script> </script>
<button
class="flex items-center overflow-hidden text-lg sm:hidden"
on:click={() => (breadcrumbsModalOpen = true)}
>
<span class="overflow-hidden text-ellipsis whitespace-nowrap"
>{currentTitle}</span
>
<ArrowDown class="h-6 w-6 flex-shrink-0 fill-current" /></button
>
{#if breadcrumbsModalOpen}
<Modal onClose={() => (breadcrumbsModalOpen = false)}>
<ol class="h-full w-full">
{#each $pageBreadcrumbs as crumb, index}
<li>
<a
href={inject(routes.Page, { worldId, pageId: crumb })}
class="flex items-center text-ellipsis whitespace-nowrap p-4"
class:active-breadcrumb={crumb === pageId}
>{$crumbsTitles[index] || crumb}</a
>
</li>
{/each}
</ol>
</Modal>
{/if}
<!-- https://devdojo.com/pines/docs/breadcrumbs --> <!-- https://devdojo.com/pines/docs/breadcrumbs -->
<div <div
class="flex justify-between overflow-x-auto rounded-md border border-neutral-200/60 px-3.5 py-1 dark:border-neutral-700" class="hidden justify-between overflow-x-hidden leading-none sm:flex"
bind:this={breadcrumbsEl} bind:this={breadcrumbsEl}
> >
<ol <ol
class="inline-flex items-center space-x-1 text-xs text-neutral-500 dark:text-neutral-300 sm:mb-0 [&_.active-breadcrumb]:font-medium [&_.active-breadcrumb]:text-neutral-600 dark:[&_.active-breadcrumb]:text-neutral-200" class="mb-0 inline-flex items-center space-x-1 text-ellipsis text-sm text-neutral-500 dark:text-neutral-300 [&_.active-breadcrumb]:font-medium [&_.active-breadcrumb]:text-neutral-600 dark:[&_.active-breadcrumb]:text-neutral-200"
bind:this={breadcrumbsListEl} bind:this={breadcrumbsListEl}
> >
{#each $pageBreadcrumbs as crumb, index} {#each $pageBreadcrumbs as crumb, index}
<li> <li>
<a <a
href={inject(routes.Page, { worldId, pageId: crumb })} href={inject(routes.Page, { worldId, pageId: crumb })}
class="font-norma inline-flex items-center text-ellipsis whitespace-nowrap py-1 focus:outline-none" class="inline-flex items-center text-ellipsis whitespace-nowrap py-1 font-normal focus:outline-none"
class:active-breadcrumb={crumb === pageId} class:active-breadcrumb={crumb === pageId}
>{$crumbsTitles[index] || crumb}</a >{$crumbsTitles[index] || crumb}</a
> >