@soul-refiner/design-system 0.1.25 → 0.1.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soul-refiner/design-system",
3
- "version": "0.1.25",
3
+ "version": "0.1.26",
4
4
  "main": "src/index.ts",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -180,7 +180,8 @@ const Dropdown = ({
180
180
  >
181
181
  {React.cloneElement(
182
182
  trigger,
183
- typeof trigger.type === 'string'
183
+ typeof trigger.type === 'string' ||
184
+ (trigger.type as any)?.displayName !== 'Button'
184
185
  ? { onClick: handleOpen }
185
186
  : { isDropdownOpen: open, onClick: handleOpen }
186
187
  )}
@@ -254,7 +255,7 @@ export interface DropdownOptionProps {
254
255
  color?: string;
255
256
  disabled?: boolean;
256
257
  icon?: React.ReactElement;
257
- label: React.ReactNode;
258
+ label?: React.ReactNode;
258
259
  onClick?: (event: React.MouseEvent<HTMLDivElement>) => void | undefined;
259
260
  selected: boolean;
260
261
  sublabel?: string;
@@ -285,15 +286,17 @@ const Option = ({
285
286
  >
286
287
  {checkbox ? <Checkbox selected={selected} /> : icon}
287
288
  <Box $dir="column" $display="flex">
288
- <P
289
- $color="grey-4"
290
- $fontSize={14}
291
- $lineHeight={20}
292
- $mediumBold
293
- $textClampLines={1}
294
- >
295
- {label}
296
- </P>
289
+ {label && (
290
+ <P
291
+ $color="grey-4"
292
+ $fontSize={14}
293
+ $lineHeight={20}
294
+ $mediumBold
295
+ $textClampLines={1}
296
+ >
297
+ {label}
298
+ </P>
299
+ )}
297
300
  {sublabel && (
298
301
  <P
299
302
  $color="grey-4"
@@ -0,0 +1,288 @@
1
+ export {
2
+ ELEMENT_BLOCKQUOTE,
3
+ ELEMENT_CODE_BLOCK,
4
+ ELEMENT_CODE_LINE,
5
+ ELEMENT_CODE_SYNTAX,
6
+ ELEMENT_DEFAULT,
7
+ ELEMENT_H1,
8
+ ELEMENT_H2,
9
+ ELEMENT_H3,
10
+ ELEMENT_H4,
11
+ ELEMENT_H5,
12
+ ELEMENT_H6,
13
+ ELEMENT_HR,
14
+ ELEMENT_IMAGE,
15
+ ELEMENT_LI,
16
+ ELEMENT_LIC,
17
+ ELEMENT_LINK,
18
+ ELEMENT_MEDIA,
19
+ ELEMENT_MEDIA_EMBED,
20
+ ELEMENT_MENTION,
21
+ ELEMENT_MENTION_INPUT,
22
+ ELEMENT_OL,
23
+ ELEMENT_PARAGRAPH,
24
+ ELEMENT_TABLE,
25
+ ELEMENT_TD,
26
+ ELEMENT_TH,
27
+ ELEMENT_TODO_LI,
28
+ ELEMENT_TR,
29
+ ELEMENT_UL,
30
+ MARK_BG_COLOR,
31
+ MARK_BOLD,
32
+ MARK_CODE,
33
+ MARK_COLOR,
34
+ MARK_FONT_FAMILY,
35
+ MARK_FONT_SIZE,
36
+ MARK_FONT_WEIGHT,
37
+ MARK_HIGHLIGHT,
38
+ MARK_ITALIC,
39
+ MARK_KBD,
40
+ MARK_SEARCH_HIGHLIGHT,
41
+ MARK_STRIKETHROUGH,
42
+ MARK_SUBSCRIPT,
43
+ MARK_SUPERSCRIPT,
44
+ MARK_UNDERLINE,
45
+ } from '@udecode/plate';
46
+
47
+ export const ELEMENT_AUDIO = 'audio';
48
+ export const ELEMENT_BIBLE_QUOTE = 'bible-quote';
49
+ export const ELEMENT_BUTTON = 'button';
50
+ export const ELEMENT_EXAMPLE = 'example';
51
+ export const ELEMENT_FOOTNOTE = 'footnote';
52
+ export const ELEMENT_PAYWALL = 'paywall';
53
+ export const ELEMENT_POLL = 'poll';
54
+ export const ELEMENT_QA = 'q-n-a';
55
+ export const ELEMENT_RECIPE = 'recipe';
56
+ export const ELEMENT_VIDEO = 'video';
57
+
58
+ export const BUTTON_COMMENT = 'button-comment';
59
+ export const BUTTON_GIFT_SUBSCRIPTION = 'button-gift-subscription';
60
+ export const BUTTON_LINK = 'button-link';
61
+ export const BUTTON_SHARE = 'button-share';
62
+ export const BUTTON_SHARE_PAGE = 'button-share-page';
63
+ export const BUTTON_SIGNUP = 'signup';
64
+ export const BUTTON_SPECIAL_OFFER = 'button-special-offer';
65
+ export const BUTTON_SUBSCRIBE = 'button-subscribe';
66
+
67
+ export const COLORS = [
68
+ 'var(--color-turquoise-5)',
69
+ 'var(--color-green-5)',
70
+ 'var(--color-blue-5)',
71
+ 'var(--color-purple-5)',
72
+ 'var(--color-yellow-5)',
73
+ 'var(--color-orange-5)',
74
+ 'var(--color-red-5)',
75
+ ];
76
+
77
+ export const PLACEHOLDER = 'This is a placeholder';
78
+
79
+ export const DEFAULT_CONTENT = [
80
+ {
81
+ type: 'h1',
82
+ children: [
83
+ {
84
+ text: 'Module 1: The Warrior Identity',
85
+ bold: true,
86
+ },
87
+ ],
88
+ },
89
+ // {
90
+ // id: "video-234jlnlknsdlkfjwlkjaa",
91
+ // parentId: "course-1lk4j3k4jlkj2l4k2j34lkj",
92
+ // children: [
93
+ // {
94
+ // text: "The DNA of a Warrior",
95
+ // },
96
+ // ],
97
+ // captions: [],
98
+ // duration: 3765,
99
+ // source: "",
100
+ // synopsis:
101
+ // "Understanding the battlefield is the first key to winning the war for sexual purity. In this episode, you will discover hell's strategies against the 21st Century Church and be introduced to the four components of the noose of sexual bondage. Learn the first key steps to freedom and how to stay pure.",
102
+ // thumbnail: "video-thumbnail.jpg",
103
+ // title: "The DNA of a Warrior",
104
+ // type: "video",
105
+ // },
106
+ // {
107
+ // type: "p",
108
+ // children: [
109
+ // {
110
+ // text: "Welcome to Warpath. You’re about to embark on a journey towards lifelong healing, freedom and victory. This is a season of training. It’s sanctification on steroids, not for the faint-hearted, but for those who truly desire to live the rest of their lives for Jesus. No reserves, no retreats, no regrets.",
111
+ // },
112
+ // {
113
+ // type: "footnote",
114
+ // children: [
115
+ // {
116
+ // text: "This is a footnote",
117
+ // },
118
+ // ],
119
+ // },
120
+ // {
121
+ // text: " What was once your deepest, darkest secret will now become the word of your testimony that heals and liberates others.",
122
+ // },
123
+ // ],
124
+ // },
125
+ // {
126
+ // type: "q-n-a",
127
+ // children: [
128
+ // {
129
+ // type: "p",
130
+ // children: [
131
+ // {
132
+ // text: "Ask your question...",
133
+ // },
134
+ // ],
135
+ // },
136
+ // ],
137
+ // },
138
+ {
139
+ type: 'p',
140
+ children: [
141
+ {
142
+ text: 'In this new season, God is calling you up. He’s calling you to a higher level, a place where you’re no longer part of the problem but are becoming a part of the solution.',
143
+ },
144
+ // {
145
+ // type: "footnote",
146
+ // children: [
147
+ // {
148
+ // text: "This is another footnote",
149
+ // },
150
+ // ],
151
+ // },
152
+ {
153
+ text: '',
154
+ },
155
+ ],
156
+ },
157
+ {
158
+ type: 'blockquote',
159
+ children: [
160
+ {
161
+ text: '"Warpath isn’t about just winning for you; that’s the beginning. Warpath is about you taking the battle and winning for others so they can have their families, and their marriages, and their destinies." \n— ',
162
+ },
163
+ {
164
+ text: 'Dr. Doug Weiss',
165
+ italic: true,
166
+ },
167
+ ],
168
+ },
169
+ {
170
+ type: 'hr',
171
+ children: [
172
+ {
173
+ text: '',
174
+ },
175
+ ],
176
+ },
177
+ {
178
+ type: 'h2',
179
+ children: [
180
+ {
181
+ text: 'Lesson Overview',
182
+ bold: true,
183
+ },
184
+ ],
185
+ },
186
+ {
187
+ type: 'h3',
188
+ align: 'center',
189
+ children: [
190
+ {
191
+ text: 'THE WARRIOR IDENTITY',
192
+ bold: true,
193
+ },
194
+ ],
195
+ },
196
+ {
197
+ type: 'hr',
198
+ children: [
199
+ {
200
+ text: '',
201
+ },
202
+ ],
203
+ },
204
+ {
205
+ type: 'img',
206
+ url: 'https://source.unsplash.com/1600x900/?christian',
207
+ children: [
208
+ {
209
+ text: '',
210
+ },
211
+ ],
212
+ },
213
+ {
214
+ type: 'p',
215
+ children: [
216
+ {
217
+ text: 'Identity: You will have to grasp your true identity in Christ. You cannot look at this as a “what”, but instead about a “who”. We must become more like Christ, because He is a warrior.',
218
+ },
219
+ ],
220
+ },
221
+ {
222
+ type: 'p',
223
+ children: [
224
+ {
225
+ text: 'A different DNA: As a warrior, your spiritual genetic code will be different from those who haven’t tasted the victory and grace you’ve experienced from this battle. You’ve experienced truth as a person: Jesus; and it has forever changed you. You’ve been set apart as a “fighting man”, no longer a civilian idly consumed by the cares of this world, but a warrior who’s passionate about the Kingdom and craving some sweet revenge.',
226
+ },
227
+ ],
228
+ },
229
+ {
230
+ type: 'code_block',
231
+ lang: 'typescript',
232
+ children: [
233
+ {
234
+ type: 'code_line',
235
+ children: [
236
+ {
237
+ text: 'const variable = "DO_NOT_USE"',
238
+ },
239
+ ],
240
+ },
241
+ ],
242
+ },
243
+ // {
244
+ // type: "paywall",
245
+ // children: [
246
+ // {
247
+ // type: "h2",
248
+ // children: [
249
+ // {
250
+ // text: "Keep reading with a paid subscription",
251
+ // },
252
+ // ],
253
+ // },
254
+ // {
255
+ // type: "p",
256
+ // children: [
257
+ // {
258
+ // text: "Subscribe to ",
259
+ // },
260
+ // {
261
+ // text: "Soul Refiner",
262
+ // italic: true,
263
+ // },
264
+ // {
265
+ // text: " to keep reading this content.",
266
+ // },
267
+ // ],
268
+ // },
269
+ // {
270
+ // type: "button",
271
+ // buttonType: "button-subscribe",
272
+ // children: [
273
+ // {
274
+ // text: "Get Access",
275
+ // },
276
+ // ],
277
+ // },
278
+ // ],
279
+ // },
280
+ {
281
+ type: 'p',
282
+ children: [
283
+ {
284
+ text: '',
285
+ },
286
+ ],
287
+ },
288
+ ];
@@ -0,0 +1,56 @@
1
+ import React from 'react';
2
+ import styled from 'styled-components';
3
+ import { createPlugins, Plate, PlateProvider, TElement } from '@udecode/plate';
4
+ import { DEFAULT_CONTENT, PLACEHOLDER } from './constants';
5
+ import { EditorBar } from './navbar';
6
+ import { plateUI } from './plate';
7
+ import { blocks, inline, marks, utils } from './plugins';
8
+
9
+ const plugins = createPlugins([...blocks, ...inline, ...marks, ...utils], {
10
+ components: plateUI,
11
+ });
12
+
13
+ export interface EditorProps {
14
+ communityName: string;
15
+ editable: boolean;
16
+ onChange: (newNodes: TElement[]) => void;
17
+ }
18
+
19
+ export const Editor = ({ communityName, editable, onChange }: EditorProps) => {
20
+ return (
21
+ <PlateProvider
22
+ id="main"
23
+ initialValue={DEFAULT_CONTENT}
24
+ onChange={onChange}
25
+ plugins={plugins}
26
+ >
27
+ <Wrapper>
28
+ <EditorBar communityName={communityName} />
29
+ <Plate
30
+ id="main"
31
+ editableProps={{
32
+ readOnly: !editable,
33
+ spellCheck: true,
34
+ autoFocus: true,
35
+ placeholder: PLACEHOLDER,
36
+ }}
37
+ />
38
+ </Wrapper>
39
+ </PlateProvider>
40
+ );
41
+ };
42
+
43
+ const Wrapper = styled.div`
44
+ * {
45
+ font-family: Inter;
46
+ }
47
+
48
+ & > [data-slate-editor='true'] > * {
49
+ margin: 24px 0px;
50
+ }
51
+
52
+ position: relative;
53
+ padding: 24px;
54
+ max-width: 1200px;
55
+ margin: 98px auto 0px;
56
+ `;
@@ -0,0 +1,2 @@
1
+ export { Editor } from './editor';
2
+ export type { EditorProps } from './editor';
@@ -0,0 +1,94 @@
1
+ import React from 'react';
2
+ import {
3
+ Alignment,
4
+ AlignToolbarButton,
5
+ isCollapsed,
6
+ PlateEditor,
7
+ setAlign,
8
+ someNode,
9
+ } from '@udecode/plate';
10
+ import { Dropdown } from 'components';
11
+ import {
12
+ PlayerListPlay,
13
+ AlignCenter,
14
+ AlignJustified,
15
+ AlignLeft,
16
+ AlignRight,
17
+ } from 'icons';
18
+
19
+ export interface AlignmentDropdownProps {
20
+ editorRef: PlateEditor;
21
+ }
22
+
23
+ export const AlignmentDropdown = ({ editorRef }: AlignmentDropdownProps) => {
24
+ const handleAlign = (event: React.FormEvent) => {
25
+ const value = (event.target as HTMLElement)?.getAttribute(
26
+ 'data-value'
27
+ ) as Alignment | null;
28
+
29
+ if (!value) return;
30
+
31
+ setAlign(editorRef, {
32
+ value: value,
33
+ });
34
+ };
35
+
36
+ const alignment = (() => {
37
+ if (!isCollapsed(editorRef?.selection)) {
38
+ return 'multiple-lines';
39
+ } else if (
40
+ someNode(editorRef, { match: { align: 'center' } }) ? true : undefined
41
+ ) {
42
+ return 'center';
43
+ } else if (
44
+ someNode(editorRef, { match: { align: 'right' } }) ? true : undefined
45
+ ) {
46
+ return 'right';
47
+ } else if (
48
+ someNode(editorRef, { match: { align: 'justify' } }) ? true : undefined
49
+ ) {
50
+ return 'justify';
51
+ } else {
52
+ return 'left';
53
+ }
54
+ })();
55
+
56
+ return (
57
+ <Dropdown
58
+ closeOnBlur={false}
59
+ closeOnChange={false}
60
+ onChange={handleAlign}
61
+ maintainState={false}
62
+ trigger={<PlayerListPlay size={24} color="grey-5" />}
63
+ >
64
+ <AlignToolbarButton
65
+ as="div"
66
+ active={alignment === 'left' ? true : undefined}
67
+ data-value="left"
68
+ value="left"
69
+ icon={<AlignLeft size={24} color="grey-5" />}
70
+ />
71
+ <AlignToolbarButton
72
+ as="div"
73
+ active={alignment === 'center' ? true : undefined}
74
+ data-value="center"
75
+ value="center"
76
+ icon={<AlignCenter size={24} color="grey-5" />}
77
+ />
78
+ <AlignToolbarButton
79
+ as="div"
80
+ active={alignment === 'right' ? true : undefined}
81
+ data-value="right"
82
+ value="right"
83
+ icon={<AlignRight size={24} color="grey-5" />}
84
+ />
85
+ <AlignToolbarButton
86
+ as="div"
87
+ active={alignment === 'justify' ? true : undefined}
88
+ data-value="justify"
89
+ value="justify"
90
+ icon={<AlignJustified size={24} color="grey-5" />}
91
+ />
92
+ </Dropdown>
93
+ );
94
+ };
@@ -0,0 +1,2 @@
1
+ export { EditorBar } from './navbar';
2
+ export type { EditorBarProps } from './navbar';
@@ -0,0 +1,336 @@
1
+ import React from 'react';
2
+ import styled from 'styled-components';
3
+ import {
4
+ Alignment,
5
+ AlignToolbarButton,
6
+ focusEditor,
7
+ getPluginType,
8
+ // getPreventDefaultHandler,
9
+ ImageToolbarButton,
10
+ insertImage,
11
+ insertNodes,
12
+ isCollapsed,
13
+ LinkToolbarButton,
14
+ MarkToolbarButton,
15
+ PlateEditor,
16
+ setMarks,
17
+ setAlign,
18
+ someNode,
19
+ toggleIndentList,
20
+ toggleNodeType,
21
+ ToolbarButton,
22
+ usePlateEditorRef,
23
+ usePlateEditorState,
24
+ } from '@udecode/plate';
25
+
26
+ import { Tooltip } from 'components';
27
+ import {
28
+ Bold,
29
+ Dollar,
30
+ FileCode,
31
+ Italic,
32
+ Link,
33
+ ListOrdered,
34
+ ListUnordered,
35
+ Quote,
36
+ Redo,
37
+ Strikethrough,
38
+ Underline,
39
+ Undo,
40
+ } from 'icons';
41
+ import {
42
+ BUTTON_SUBSCRIBE,
43
+ ELEMENT_BLOCKQUOTE,
44
+ ELEMENT_BUTTON,
45
+ ELEMENT_DEFAULT,
46
+ ELEMENT_H2,
47
+ ELEMENT_PARAGRAPH,
48
+ ELEMENT_PAYWALL,
49
+ ELEMENT_QA,
50
+ MARK_BOLD,
51
+ MARK_CODE,
52
+ MARK_ITALIC,
53
+ MARK_HIGHLIGHT,
54
+ MARK_STRIKETHROUGH,
55
+ MARK_SUBSCRIPT,
56
+ MARK_SUPERSCRIPT,
57
+ MARK_UNDERLINE,
58
+ } from '../constants';
59
+ import { AlignmentDropdown } from './alignment_dropdown';
60
+
61
+ export interface EditorBarProps {
62
+ communityName: string;
63
+ }
64
+
65
+ export const EditorBar = ({ communityName }: EditorBarProps) => {
66
+ const editorRef = usePlateEditorRef('main');
67
+ const stateEditor = usePlateEditorState();
68
+ const audioRef = React.useRef<HTMLInputElement>(null);
69
+ const imageRef = React.useRef<HTMLInputElement>(null);
70
+ const videoRef = React.useRef<HTMLInputElement>(null);
71
+
72
+ const handleAudioChange = async (event: React.FormEvent) => {
73
+ const file = (event.target as HTMLInputElement)?.files?.[0];
74
+ if (!file) return;
75
+ /**
76
+ * TODO: Insert audio component
77
+ */
78
+ };
79
+
80
+ const handleBlockquote = (event: React.MouseEvent) => {
81
+ toggleNodeType(stateEditor, { activeType: ELEMENT_BLOCKQUOTE });
82
+ };
83
+
84
+ const handleImageChange = async (event: React.FormEvent) => {
85
+ const file = (event.target as HTMLInputElement)?.files?.[0];
86
+ if (!file) return;
87
+ // const imageurl = await handleUploadImage(file);
88
+ // if (imageurl) {
89
+ // insertImage(editorRef, imageurl);
90
+ // }
91
+ };
92
+
93
+ const handleFont = (value: string) => {
94
+ const type = getPluginType(editorRef, 'fontSize');
95
+ focusEditor(editorRef);
96
+ setMarks(stateEditor, { [type]: `${value}px` });
97
+ };
98
+
99
+ const handleOrderedList = (event: any) => {
100
+ event.preventDefault();
101
+ toggleIndentList(editorRef, {
102
+ listStyleType: 'decimals',
103
+ });
104
+ };
105
+
106
+ const handlePaywall = () => {
107
+ insertNodes(editorRef, [
108
+ {
109
+ type: ELEMENT_PAYWALL,
110
+ children: [
111
+ {
112
+ type: ELEMENT_H2,
113
+ children: [
114
+ {
115
+ text: 'Keep reading with a paid subscription',
116
+ },
117
+ ],
118
+ },
119
+ {
120
+ type: ELEMENT_PARAGRAPH,
121
+ children: [
122
+ {
123
+ text: 'Subscribe to ',
124
+ },
125
+ {
126
+ text: communityName,
127
+ italic: true,
128
+ },
129
+ {
130
+ text: ' to keep reading this content.',
131
+ },
132
+ ],
133
+ },
134
+ {
135
+ type: ELEMENT_BUTTON,
136
+ buttonType: BUTTON_SUBSCRIBE,
137
+ children: [
138
+ {
139
+ text: 'Get Access',
140
+ },
141
+ ],
142
+ },
143
+ ],
144
+ },
145
+ {
146
+ type: ELEMENT_DEFAULT,
147
+ children: [
148
+ {
149
+ text: '',
150
+ },
151
+ ],
152
+ },
153
+ ]);
154
+ focusEditor(editorRef);
155
+ };
156
+
157
+ const handleQA = () => {
158
+ insertNodes(editorRef, {
159
+ type: ELEMENT_QA,
160
+ children: [
161
+ {
162
+ type: ELEMENT_PARAGRAPH,
163
+ children: [
164
+ {
165
+ text: 'Ask your question here...',
166
+ },
167
+ ],
168
+ },
169
+ ],
170
+ });
171
+ };
172
+
173
+ const handleRedo = () => editorRef.redo();
174
+
175
+ const handleUndo = () => editorRef.undo();
176
+
177
+ const handleUnorderedList = (event: any) => {
178
+ event.preventDefault();
179
+ toggleIndentList(editorRef, {
180
+ listStyleType: 'disc',
181
+ });
182
+ };
183
+
184
+ const handleVideoChange = async (event: React.FormEvent) => {
185
+ const file = (event.target as HTMLInputElement)?.files?.[0];
186
+ if (!file) return;
187
+ /**
188
+ * TODO: Insert video component
189
+ */
190
+ };
191
+
192
+ return (
193
+ <Wrapper>
194
+ <Menu aria-label="editor-navigation">
195
+ <MenuSection>
196
+ <Tooltip content="Undo" place="top">
197
+ <MenuButton $active={false} onClick={handleUndo}>
198
+ <Undo size={24} color="grey-5" />
199
+ </MenuButton>
200
+ </Tooltip>
201
+ <Tooltip content="Redo" place="top">
202
+ <MenuButton $active={false} onClick={handleRedo}>
203
+ <Redo size={24} color="grey-5" />
204
+ </MenuButton>
205
+ </Tooltip>
206
+ </MenuSection>
207
+ <Divider />
208
+ <MenuSection>
209
+ <Tooltip content="Bold" place="top">
210
+ <MarkToolbarButton
211
+ as="div"
212
+ type={getPluginType(editorRef, MARK_BOLD)}
213
+ icon={
214
+ <MenuButton $active={true}>
215
+ <Bold size={24} color="grey-5" />
216
+ </MenuButton>
217
+ }
218
+ />
219
+ </Tooltip>
220
+ <Tooltip content="Italic" place="top">
221
+ <MarkToolbarButton
222
+ as="div"
223
+ type={getPluginType(editorRef, MARK_ITALIC)}
224
+ icon={
225
+ <MenuButton $active={false}>
226
+ <Italic size={24} color="grey-5" />
227
+ </MenuButton>
228
+ }
229
+ />
230
+ </Tooltip>
231
+ <Tooltip content="Underline" place="top">
232
+ <MarkToolbarButton
233
+ as="div"
234
+ type={getPluginType(editorRef, MARK_UNDERLINE)}
235
+ icon={<Underline size={24} color="grey-5" />}
236
+ />
237
+ </Tooltip>
238
+ <Tooltip content="Strikethrough" place="top">
239
+ <MarkToolbarButton
240
+ as="div"
241
+ type={getPluginType(editorRef, MARK_STRIKETHROUGH)}
242
+ icon={
243
+ <MenuButton $active={false}>
244
+ <Strikethrough size={24} color="grey-5" />
245
+ </MenuButton>
246
+ }
247
+ />
248
+ </Tooltip>
249
+ <Tooltip content="Inline code" place="top">
250
+ <MarkToolbarButton
251
+ as="div"
252
+ type={getPluginType(editorRef, MARK_CODE)}
253
+ icon={
254
+ <MenuButton $active={false}>
255
+ <FileCode size={24} color="grey-5" />
256
+ </MenuButton>
257
+ }
258
+ />
259
+ </Tooltip>
260
+ </MenuSection>
261
+ <Divider />
262
+ <MenuSection>
263
+ <MenuButton $active={false} as="div">
264
+ <AlignmentDropdown editorRef={editorRef} />
265
+ </MenuButton>
266
+ </MenuSection>
267
+ </Menu>
268
+ </Wrapper>
269
+ );
270
+ };
271
+
272
+ const Wrapper = styled.div`
273
+ position: sticky;
274
+ z-index: 2;
275
+ left: 0;
276
+ right: 0;
277
+ top: 0;
278
+ `;
279
+
280
+ const Menu = styled.nav`
281
+ display: flex;
282
+ gap: 12px;
283
+ justify-content: center;
284
+ max-width: 1200px;
285
+ width: 100%;
286
+ margin: 0 auto;
287
+ padding: 14px 24px;
288
+ background-color: var(--color-white);
289
+ `;
290
+
291
+ const MenuSection = styled.div`
292
+ display: flex;
293
+ gap: 16px;
294
+ align-items: center;
295
+ width: fit-content;
296
+ flex-shrink: 0;
297
+ `;
298
+
299
+ export const MenuButton = styled.button<{
300
+ $active?: boolean;
301
+ }>`
302
+ padding: 2px;
303
+ border: none;
304
+ outline: none;
305
+ cursor: pointer;
306
+ box-sizing: border-box;
307
+ height: 28px;
308
+ width: 28px;
309
+ border-radius: 2px;
310
+
311
+ &:hover:not(:disabled) {
312
+ padding: 1px;
313
+ border: 1px solid var(--color-grey-8);
314
+ }
315
+
316
+ &:active:not(:disabled) {
317
+ background-color: var(--color-grey-9);
318
+ }
319
+
320
+ &:disabled {
321
+ background-color: var(--color-white) !important;
322
+ cursor: initial;
323
+ svg {
324
+ fill: var(--color-grey-7) !important;
325
+ }
326
+ }
327
+
328
+ background-color: ${({ $active }) =>
329
+ $active ? 'background-color: var(--color-grey-9)' : 'transparent'};
330
+ `;
331
+
332
+ const Divider = styled.div`
333
+ width: 1px;
334
+ height: 28px;
335
+ background-color: var(--color-grey-9);
336
+ `;
@@ -0,0 +1,17 @@
1
+ import {
2
+ CodeBlockElement,
3
+ BlockquoteElement,
4
+ CodeLineElement,
5
+ CodeSyntaxLeaf,
6
+ ImageElement,
7
+ createPlateUI,
8
+ } from '@udecode/plate';
9
+ import * as Elements from './constants';
10
+
11
+ export const plateUI = createPlateUI({
12
+ [Elements.ELEMENT_BLOCKQUOTE]: BlockquoteElement,
13
+ [Elements.ELEMENT_CODE_BLOCK]: CodeBlockElement,
14
+ [Elements.ELEMENT_CODE_LINE]: CodeLineElement,
15
+ [Elements.ELEMENT_CODE_SYNTAX]: CodeSyntaxLeaf,
16
+ [Elements.ELEMENT_IMAGE]: ImageElement,
17
+ });
@@ -0,0 +1,127 @@
1
+ import React from 'react';
2
+ import styled from 'styled-components';
3
+ import {
4
+ createPluginFactory,
5
+ ELEMENT_LINK,
6
+ getAboveNode,
7
+ insertLink,
8
+ isEditorReadOnly,
9
+ setNodes,
10
+ PlateRenderElementProps,
11
+ } from '@udecode/plate';
12
+ import { Button as UIButton, P } from 'components';
13
+ import {
14
+ BUTTON_COMMENT,
15
+ BUTTON_GIFT_SUBSCRIPTION,
16
+ BUTTON_LINK,
17
+ BUTTON_SHARE,
18
+ BUTTON_SHARE_PAGE,
19
+ BUTTON_SPECIAL_OFFER,
20
+ BUTTON_SUBSCRIBE,
21
+ BUTTON_SIGNUP,
22
+ COLORS,
23
+ ELEMENT_BUTTON,
24
+ ELEMENT_PARAGRAPH,
25
+ } from '../constants';
26
+
27
+ export const Button = ({
28
+ attributes,
29
+ className,
30
+ children,
31
+ editor,
32
+ element,
33
+ }: PlateRenderElementProps) => {
34
+ const [state, setState] = React.useState('');
35
+ const { ref: _, ...htmlAttributes } = attributes;
36
+ const { buttonType, color } = element;
37
+
38
+ const currAboveNode = getAboveNode(editor, {
39
+ /**
40
+ * @todo possibly wrong
41
+ */
42
+ at: editor.prevSelection?.focus,
43
+ match: {
44
+ type: ELEMENT_BUTTON,
45
+ buttonType: buttonType,
46
+ },
47
+ });
48
+
49
+ const editable = !isEditorReadOnly(editor);
50
+
51
+ const handleColorChange = (color: string) => {
52
+ if (currAboveNode) {
53
+ setNodes(
54
+ editor,
55
+ {
56
+ color,
57
+ },
58
+ {
59
+ at: currAboveNode ? currAboveNode[1] : undefined,
60
+ }
61
+ );
62
+ }
63
+ };
64
+
65
+ const ColorMenu = () =>
66
+ !currAboveNode?.[1] || !editable ? null : (
67
+ <ColorMenuComponent contentEditable={false}>
68
+ {COLORS.map((color) => (
69
+ <ColorItem
70
+ $color={color}
71
+ contentEditable={false}
72
+ key={color}
73
+ onClick={(event: React.MouseEvent) => {
74
+ event.stopPropagation();
75
+ handleColorChange(color);
76
+ }}
77
+ />
78
+ ))}
79
+ </ColorMenuComponent>
80
+ );
81
+
82
+ return (
83
+ <ButtonWrapper>
84
+ <ColorMenu />
85
+ <UIButton>{children}</UIButton>
86
+ </ButtonWrapper>
87
+ );
88
+ };
89
+
90
+ const ColorMenuComponent = styled.div`
91
+ display: flex;
92
+ justify-content: center;
93
+ align-items: center;
94
+ gap: 16px;
95
+ z-index: 5;
96
+ position: absolute;
97
+ left: calc(50% - 148px);
98
+ border-radius: 16px;
99
+ padding: 8px 16px;
100
+ box-shadow: var(--elevation-1);
101
+ background-color: var(--color-white);
102
+ `;
103
+
104
+ const ColorItem = styled.div<{ $color: string }>`
105
+ width: 24px;
106
+ height: 24px;
107
+ border-radius: 50%;
108
+ background-color: ${({ $color }) => $color};
109
+ cursor: pointer;
110
+ `;
111
+
112
+ const ButtonWrapper = styled.div``;
113
+
114
+ export const createButtonsPlugin = createPluginFactory({
115
+ key: ELEMENT_BUTTON,
116
+ type: ELEMENT_BUTTON,
117
+ isElement: true,
118
+ component: Button,
119
+ then: (editor) => ({
120
+ inject: {
121
+ props: {
122
+ classNames: undefined,
123
+ validTypes: [ELEMENT_PARAGRAPH],
124
+ },
125
+ },
126
+ }),
127
+ });
@@ -0,0 +1,41 @@
1
+ import { KEYS_HEADING } from '@udecode/plate';
2
+ import {
3
+ ELEMENT_BLOCKQUOTE,
4
+ ELEMENT_BUTTON,
5
+ ELEMENT_CODE_BLOCK,
6
+ ELEMENT_MEDIA,
7
+ ELEMENT_MEDIA_EMBED,
8
+ ELEMENT_OL,
9
+ ELEMENT_UL,
10
+ } from '../constants';
11
+
12
+ export const exitBreakPlugin = {
13
+ options: {
14
+ rules: [
15
+ {
16
+ hotkey: 'mod+enter',
17
+ },
18
+ {
19
+ hotkey: 'mod+shift+enter',
20
+ before: true,
21
+ },
22
+ {
23
+ hotkey: 'enter',
24
+ query: {
25
+ start: true,
26
+ end: true,
27
+ allow: [
28
+ ELEMENT_BLOCKQUOTE,
29
+ ELEMENT_BUTTON,
30
+ ELEMENT_CODE_BLOCK,
31
+ ELEMENT_MEDIA,
32
+ ELEMENT_MEDIA_EMBED,
33
+ ELEMENT_OL,
34
+ ELEMENT_UL,
35
+ ...KEYS_HEADING,
36
+ ],
37
+ },
38
+ },
39
+ ],
40
+ },
41
+ };
@@ -0,0 +1,151 @@
1
+ import {
2
+ createBlockquotePlugin,
3
+ createCodeBlockPlugin,
4
+ createHeadingPlugin,
5
+ createHorizontalRulePlugin,
6
+ createIndentListPlugin,
7
+ createImagePlugin,
8
+ createMediaEmbedPlugin,
9
+ createResetNodePlugin,
10
+ createTablePlugin,
11
+ createBoldPlugin,
12
+ createCodePlugin,
13
+ createFontBackgroundColorPlugin,
14
+ createFontSizePlugin,
15
+ createFontFamilyPlugin,
16
+ createItalicPlugin,
17
+ createStrikethroughPlugin,
18
+ createSubscriptPlugin,
19
+ createSuperscriptPlugin,
20
+ createUnderlinePlugin,
21
+ createAlignPlugin,
22
+ createIndentPlugin,
23
+ createLineHeightPlugin,
24
+ createLinkPlugin,
25
+ PlateFloatingLink,
26
+ createExitBreakPlugin,
27
+ createHistoryPlugin,
28
+ createSelectOnBackspacePlugin,
29
+ createSoftBreakPlugin,
30
+ } from '@udecode/plate';
31
+
32
+ import {
33
+ ELEMENT_PARAGRAPH,
34
+ ELEMENT_H1,
35
+ ELEMENT_H2,
36
+ ELEMENT_H3,
37
+ ELEMENT_H4,
38
+ ELEMENT_H5,
39
+ ELEMENT_H6,
40
+ ELEMENT_CODE_BLOCK,
41
+ } from '../constants';
42
+
43
+ import { createButtonsPlugin } from './buttons';
44
+ import { exitBreakPlugin } from './exitBreak';
45
+ import { resetBlockTypePlugin } from './resetBlockType';
46
+ import { selectOnBackspace } from './selectOnBackspace';
47
+ import { softBreakPlugin } from './softBreak';
48
+ import {
49
+ createParagraphPlugin,
50
+ createHOnePlugin,
51
+ createHTwoPlugin,
52
+ createHThreePlugin,
53
+ createHFourPlugin,
54
+ createHFivePlugin,
55
+ createHSixPlugin,
56
+ } from './text';
57
+
58
+ export const blocks = [
59
+ createButtonsPlugin(),
60
+ createBlockquotePlugin(),
61
+ createCodeBlockPlugin(),
62
+ createHeadingPlugin(),
63
+ createHorizontalRulePlugin(),
64
+ createImagePlugin({
65
+ options: {
66
+ disableUploadInsert: true,
67
+ disableCaption: true,
68
+ disableEmbedInsert: true,
69
+ },
70
+ }),
71
+ createIndentListPlugin({
72
+ inject: {
73
+ props: {
74
+ validTypes: [
75
+ ELEMENT_PARAGRAPH,
76
+ ELEMENT_H1,
77
+ ELEMENT_H2,
78
+ ELEMENT_H3,
79
+ ELEMENT_H4,
80
+ ELEMENT_H5,
81
+ ELEMENT_H6,
82
+ ELEMENT_CODE_BLOCK,
83
+ ],
84
+ },
85
+ },
86
+ }),
87
+ // createFootnotePlugin(),
88
+ createMediaEmbedPlugin(),
89
+ createParagraphPlugin(),
90
+ createHOnePlugin(),
91
+ createHTwoPlugin(),
92
+ createHThreePlugin(),
93
+ createHFourPlugin(),
94
+ createHFivePlugin(),
95
+ createHSixPlugin(),
96
+ // createPaywallPlugin(),
97
+ // createQAPlugin(),
98
+ createResetNodePlugin(),
99
+ createTablePlugin(),
100
+ // createVideoPlugin(),
101
+ ];
102
+
103
+ export const marks = [
104
+ createBoldPlugin(),
105
+ createCodePlugin(),
106
+ createFontBackgroundColorPlugin(),
107
+ createFontSizePlugin(),
108
+ createFontFamilyPlugin(),
109
+ createItalicPlugin(),
110
+ createStrikethroughPlugin(),
111
+ createSubscriptPlugin(),
112
+ createSuperscriptPlugin(),
113
+ createUnderlinePlugin(),
114
+ ];
115
+
116
+ export const inline = [
117
+ createAlignPlugin({
118
+ inject: {
119
+ props: {
120
+ validTypes: [
121
+ ELEMENT_PARAGRAPH,
122
+ ELEMENT_H1,
123
+ ELEMENT_H2,
124
+ ELEMENT_H3,
125
+ ELEMENT_H4,
126
+ ELEMENT_H5,
127
+ ELEMENT_H6,
128
+ ],
129
+ },
130
+ },
131
+ }),
132
+ createIndentPlugin({
133
+ inject: {
134
+ props: {
135
+ validTypes: [ELEMENT_PARAGRAPH, ELEMENT_H1],
136
+ },
137
+ },
138
+ }),
139
+ createLineHeightPlugin(),
140
+ createLinkPlugin({
141
+ renderAfterEditable: PlateFloatingLink,
142
+ }),
143
+ ];
144
+
145
+ export const utils = [
146
+ createExitBreakPlugin(exitBreakPlugin),
147
+ createHistoryPlugin(),
148
+ createResetNodePlugin(resetBlockTypePlugin),
149
+ createSelectOnBackspacePlugin(selectOnBackspace),
150
+ createSoftBreakPlugin(softBreakPlugin),
151
+ ];
@@ -0,0 +1,29 @@
1
+ import { isBlockAboveEmpty, isSelectionAtBlockStart } from '@udecode/plate';
2
+
3
+ import {
4
+ ELEMENT_BLOCKQUOTE,
5
+ ELEMENT_PARAGRAPH,
6
+ ELEMENT_TODO_LI,
7
+ } from '../constants';
8
+
9
+ const resetBlockTypesCommonRule = {
10
+ types: [ELEMENT_BLOCKQUOTE, ELEMENT_TODO_LI],
11
+ defaultType: ELEMENT_PARAGRAPH,
12
+ };
13
+
14
+ export const resetBlockTypePlugin = {
15
+ options: {
16
+ rules: [
17
+ {
18
+ ...resetBlockTypesCommonRule,
19
+ hotkey: 'Enter',
20
+ predicate: isBlockAboveEmpty,
21
+ },
22
+ {
23
+ ...resetBlockTypesCommonRule,
24
+ hotkey: 'Backspace',
25
+ predicate: isSelectionAtBlockStart,
26
+ },
27
+ ],
28
+ },
29
+ };
@@ -0,0 +1,7 @@
1
+ import { ELEMENT_HR, ELEMENT_IMAGE, ELEMENT_MEDIA_EMBED } from '../constants';
2
+
3
+ export const selectOnBackspace = {
4
+ options: {
5
+ query: { allow: [ELEMENT_HR, ELEMENT_IMAGE, ELEMENT_MEDIA_EMBED] },
6
+ },
7
+ };
@@ -0,0 +1,19 @@
1
+ import {
2
+ ELEMENT_BLOCKQUOTE,
3
+ ELEMENT_CODE_BLOCK,
4
+ ELEMENT_TD,
5
+ } from '@udecode/plate';
6
+
7
+ export const softBreakPlugin = {
8
+ options: {
9
+ rules: [
10
+ { hotkey: 'shift+enter' },
11
+ {
12
+ hotkey: 'enter',
13
+ query: {
14
+ allow: [ELEMENT_CODE_BLOCK, ELEMENT_BLOCKQUOTE, ELEMENT_TD],
15
+ },
16
+ },
17
+ ],
18
+ },
19
+ };
@@ -0,0 +1,137 @@
1
+ import React from 'react';
2
+ import { PlateRenderElementProps, createPluginFactory } from '@udecode/plate';
3
+ import { H, P } from 'components';
4
+ import * as Elements from '../constants';
5
+
6
+ const ParagraphElement = ({
7
+ attributes,
8
+ nodeProps,
9
+ children,
10
+ }: PlateRenderElementProps) => {
11
+ return (
12
+ <P {...attributes} {...nodeProps}>
13
+ {children}
14
+ </P>
15
+ );
16
+ };
17
+
18
+ export const createParagraphPlugin = createPluginFactory({
19
+ key: Elements.ELEMENT_PARAGRAPH,
20
+ type: Elements.ELEMENT_PARAGRAPH,
21
+ isElement: true,
22
+ component: ParagraphElement,
23
+ });
24
+
25
+ const HOneElement = ({
26
+ attributes,
27
+ nodeProps,
28
+ children,
29
+ }: PlateRenderElementProps) => {
30
+ return (
31
+ <H.One {...attributes} {...nodeProps}>
32
+ {children}
33
+ </H.One>
34
+ );
35
+ };
36
+
37
+ export const createHOnePlugin = createPluginFactory({
38
+ key: Elements.ELEMENT_H1,
39
+ type: Elements.ELEMENT_H1,
40
+ isElement: true,
41
+ component: HOneElement,
42
+ });
43
+
44
+ const HTwoElement = ({
45
+ attributes,
46
+ nodeProps,
47
+ children,
48
+ }: PlateRenderElementProps) => {
49
+ return (
50
+ <H.Two {...attributes} {...nodeProps}>
51
+ {children}
52
+ </H.Two>
53
+ );
54
+ };
55
+
56
+ export const createHTwoPlugin = createPluginFactory({
57
+ key: Elements.ELEMENT_H2,
58
+ type: Elements.ELEMENT_H2,
59
+ isElement: true,
60
+ component: HTwoElement,
61
+ });
62
+
63
+ const HThreeElement = ({
64
+ attributes,
65
+ nodeProps,
66
+ children,
67
+ }: PlateRenderElementProps) => {
68
+ return (
69
+ <H.Three {...attributes} {...nodeProps}>
70
+ {children}
71
+ </H.Three>
72
+ );
73
+ };
74
+
75
+ export const createHThreePlugin = createPluginFactory({
76
+ key: Elements.ELEMENT_H3,
77
+ type: Elements.ELEMENT_H3,
78
+ isElement: true,
79
+ component: HThreeElement,
80
+ });
81
+
82
+ const HFourElement = ({
83
+ attributes,
84
+ nodeProps,
85
+ children,
86
+ }: PlateRenderElementProps) => {
87
+ return (
88
+ <H.Four {...attributes} {...nodeProps}>
89
+ {children}
90
+ </H.Four>
91
+ );
92
+ };
93
+
94
+ export const createHFourPlugin = createPluginFactory({
95
+ key: Elements.ELEMENT_H4,
96
+ type: Elements.ELEMENT_H4,
97
+ isElement: true,
98
+ component: HFourElement,
99
+ });
100
+
101
+ const HFiveElement = ({
102
+ attributes,
103
+ nodeProps,
104
+ children,
105
+ }: PlateRenderElementProps) => {
106
+ return (
107
+ <H.Five {...attributes} {...nodeProps}>
108
+ {children}
109
+ </H.Five>
110
+ );
111
+ };
112
+
113
+ export const createHFivePlugin = createPluginFactory({
114
+ key: Elements.ELEMENT_H5,
115
+ type: Elements.ELEMENT_H5,
116
+ isElement: true,
117
+ component: HFiveElement,
118
+ });
119
+
120
+ const HSixElement = ({
121
+ attributes,
122
+ nodeProps,
123
+ children,
124
+ }: PlateRenderElementProps) => {
125
+ return (
126
+ <H.Six {...attributes} {...nodeProps}>
127
+ {children}
128
+ </H.Six>
129
+ );
130
+ };
131
+
132
+ export const createHSixPlugin = createPluginFactory({
133
+ key: Elements.ELEMENT_H6,
134
+ type: Elements.ELEMENT_H6,
135
+ isElement: true,
136
+ component: HSixElement,
137
+ });
@@ -1,6 +1,7 @@
1
1
  export * from './actions';
2
2
  export * from './atoms';
3
3
  export * from './card';
4
+ export * from './editor';
4
5
  export * from './feedback';
5
6
  export * from './iconography';
6
7
  export * from './input';