a tool for shared writing and social publishing
1"use client";
2import { ShortcutKey } from "./Layout";
3import { Media } from "./Media";
4import { Popover } from "./Popover";
5import { metaKey } from "src/utils/metaKey";
6import { useEntitySetContext } from "./EntitySetProvider";
7import { useState } from "react";
8import { ActionButton } from "components/ActionBar/ActionButton";
9import { HelpSmall } from "./Icons/HelpSmall";
10import { isMac } from "src/utils/isDevice";
11import { useIsMobile } from "src/hooks/isMobile";
12
13export const HelpPopover = (props: { noShortcuts?: boolean }) => {
14 let entity_set = useEntitySetContext();
15 let isMobile = useIsMobile();
16
17 return entity_set.permissions.write ? (
18 <Popover
19 side={isMobile ? "top" : "right"}
20 align={isMobile ? "center" : "start"}
21 asChild
22 className="max-w-xs w-full"
23 trigger={<ActionButton icon={<HelpSmall />} label="About" />}
24 >
25 <div className="flex flex-col text-sm gap-2 text-secondary">
26 {/* about links */}
27 <HelpLink text="📖 Leaflet Manual" url="https://about.leaflet.pub" />
28 <HelpLink text="💡 Make with Leaflet" url="https://make.leaflet.pub" />
29 <HelpLink
30 text="✨ Explore Publications"
31 url="https://leaflet.pub/discover"
32 />
33 <HelpLink text="📣 Newsletter" url="https://buttondown.com/leaflet" />
34 {/* contact links */}
35 <div className="columns-2 gap-2">
36 <HelpLink
37 text="🦋 Bluesky"
38 url="https://bsky.app/profile/leaflet.pub"
39 />
40 <HelpLink text="💌 Email" url="mailto:contact@leaflet.pub" />
41 </div>
42 {/* keyboard shortcuts: desktop only */}
43 <Media mobile={false}>
44 {!props.noShortcuts && (
45 <>
46 <hr className="text-border my-1" />
47 <div className="flex flex-col gap-1">
48 <Label>Text Shortcuts</Label>
49 <KeyboardShortcut name="Bold" keys={[metaKey(), "B"]} />
50 <KeyboardShortcut name="Italic" keys={[metaKey(), "I"]} />
51 <KeyboardShortcut name="Underline" keys={[metaKey(), "U"]} />
52 <KeyboardShortcut
53 name="Highlight"
54 keys={[metaKey(), isMac() ? "Ctrl" : "Meta", "H"]}
55 />
56 <KeyboardShortcut
57 name="Strikethrough"
58 keys={[metaKey(), isMac() ? "Ctrl" : "Meta", "X"]}
59 />
60 <KeyboardShortcut name="Inline Link" keys={[metaKey(), "K"]} />
61
62 <Label>Block Shortcuts</Label>
63 {/* shift + up/down arrows (or click + drag): select multiple blocks */}
64 <KeyboardShortcut
65 name="Move Block Up"
66 keys={["Shift", metaKey(), "↑"]}
67 />
68 <KeyboardShortcut
69 name="Move Block Down"
70 keys={["Shift", metaKey(), "↓"]}
71 />
72 {/* cmd/ctrl-a: first selects all text in a block; again selects all blocks on page */}
73 {/* cmd/ctrl + up/down arrows: go to beginning / end of doc */}
74
75 <Label>Canvas Shortcuts</Label>
76 <OtherShortcut name="Add Block" description="Double click" />
77 <OtherShortcut name="Select Block" description="Long press" />
78
79 <Label>Outliner Shortcuts</Label>
80 <KeyboardShortcut
81 name="Make List"
82 keys={[metaKey(), isMac() ? "Opt" : "Alt", "L"]}
83 />
84 {/* tab / shift + tab: indent / outdent */}
85 <KeyboardShortcut
86 name="Toggle Checkbox"
87 keys={[metaKey(), "Enter"]}
88 />
89 <KeyboardShortcut
90 name="Toggle Fold"
91 keys={[metaKey(), "Shift", "Enter"]}
92 />
93 <KeyboardShortcut
94 name="Fold All"
95 keys={[metaKey(), isMac() ? "Opt" : "Alt", "Shift", "↑"]}
96 />
97 <KeyboardShortcut
98 name="Unfold All"
99 keys={[metaKey(), isMac() ? "Opt" : "Alt", "Shift", "↓"]}
100 />
101 </div>
102 </>
103 )}
104 </Media>
105 {/* links: terms and privacy */}
106 <hr className="text-border my-1" />
107 {/* <HelpLink
108 text="Terms and Privacy Policy"
109 url="https://leaflet.pub/legal"
110 /> */}
111 <div>
112 <a href="https://leaflet.pub/legal" target="_blank">
113 Terms and Privacy Policy
114 </a>
115 </div>
116 </div>
117 </Popover>
118 ) : null;
119};
120
121const KeyboardShortcut = (props: { name: string; keys: string[] }) => {
122 return (
123 <div className="flex gap-2 justify-between items-center">
124 {props.name}
125 <div className="flex gap-1 items-center font-bold">
126 {props.keys.map((key, index) => {
127 return <ShortcutKey key={index}>{key}</ShortcutKey>;
128 })}
129 </div>
130 </div>
131 );
132};
133
134const OtherShortcut = (props: { name: string; description: string }) => {
135 return (
136 <div className="flex justify-between items-center">
137 <span>{props.name}</span>
138 <span>
139 <strong>{props.description}</strong>
140 </span>
141 </div>
142 );
143};
144
145const Label = (props: { children: React.ReactNode }) => {
146 return <div className="text-tertiary font-bold pt-2 ">{props.children}</div>;
147};
148
149const HelpLink = (props: { url: string; text: string }) => {
150 const [isHovered, setIsHovered] = useState(false);
151 const handleMouseEnter = () => {
152 setIsHovered(true);
153 };
154 const handleMouseLeave = () => {
155 setIsHovered(false);
156 };
157 return (
158 <a
159 href={props.url}
160 target="_blank"
161 className="py-2 px-2 rounded-md flex flex-col gap-1 bg-border-light hover:bg-border hover:no-underline"
162 style={{
163 backgroundColor: isHovered
164 ? "color-mix(in oklab, rgb(var(--accent-contrast)), rgb(var(--bg-page)) 85%)"
165 : "color-mix(in oklab, rgb(var(--accent-contrast)), rgb(var(--bg-page)) 75%)",
166 }}
167 onMouseEnter={handleMouseEnter}
168 onMouseLeave={handleMouseLeave}
169 >
170 <strong>{props.text}</strong>
171 </a>
172 );
173};