opacacms 0.1.1 → 0.1.2

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.
Files changed (212) hide show
  1. package/package.json +36 -1
  2. package/bun.lock +0 -34
  3. package/global.d.ts +0 -11
  4. package/src/admin/api-client.ts +0 -63
  5. package/src/admin/auth-client.ts +0 -40
  6. package/src/admin/custom-field.ts +0 -179
  7. package/src/admin/index.ts +0 -15
  8. package/src/admin/react.tsx +0 -72
  9. package/src/admin/router.ts +0 -9
  10. package/src/admin/stores/admin-queries.ts +0 -121
  11. package/src/admin/stores/auth.ts +0 -61
  12. package/src/admin/stores/column-visibility.ts +0 -67
  13. package/src/admin/stores/config.ts +0 -15
  14. package/src/admin/stores/media.ts +0 -95
  15. package/src/admin/stores/query.ts +0 -13
  16. package/src/admin/stores/ui.ts +0 -29
  17. package/src/admin/ui/admin-client.tsx +0 -283
  18. package/src/admin/ui/admin-layout.tsx +0 -276
  19. package/src/admin/ui/components/ColumnVisibilityToggle.tsx +0 -141
  20. package/src/admin/ui/components/DataDetailSheet.tsx +0 -141
  21. package/src/admin/ui/components/DataDetailView.tsx +0 -175
  22. package/src/admin/ui/components/Table.tsx +0 -67
  23. package/src/admin/ui/components/fields/ArrayField.tsx +0 -166
  24. package/src/admin/ui/components/fields/BlocksField.tsx +0 -202
  25. package/src/admin/ui/components/fields/BooleanField.tsx +0 -50
  26. package/src/admin/ui/components/fields/CollapsibleField.tsx +0 -75
  27. package/src/admin/ui/components/fields/DateField.tsx +0 -45
  28. package/src/admin/ui/components/fields/FileField.tsx +0 -322
  29. package/src/admin/ui/components/fields/GroupField.tsx +0 -50
  30. package/src/admin/ui/components/fields/JoinField.tsx +0 -23
  31. package/src/admin/ui/components/fields/NumberField.tsx +0 -46
  32. package/src/admin/ui/components/fields/RadioField.tsx +0 -62
  33. package/src/admin/ui/components/fields/RelationshipField.tsx +0 -278
  34. package/src/admin/ui/components/fields/RowField.tsx +0 -40
  35. package/src/admin/ui/components/fields/SelectField.tsx +0 -59
  36. package/src/admin/ui/components/fields/TabsField.tsx +0 -101
  37. package/src/admin/ui/components/fields/TextAreaField.tsx +0 -54
  38. package/src/admin/ui/components/fields/TextField.tsx +0 -49
  39. package/src/admin/ui/components/fields/VirtualField.tsx +0 -53
  40. package/src/admin/ui/components/fields/index.tsx +0 -371
  41. package/src/admin/ui/components/fields/richtext-editor/index.tsx +0 -211
  42. package/src/admin/ui/components/fields/richtext-editor/nodes/ImageComponent.tsx +0 -142
  43. package/src/admin/ui/components/fields/richtext-editor/nodes/ImageNode.tsx +0 -95
  44. package/src/admin/ui/components/fields/richtext-editor/plugins/ComponentPickerPlugin.tsx +0 -226
  45. package/src/admin/ui/components/fields/richtext-editor/plugins/EditableSyncPlugin.tsx +0 -16
  46. package/src/admin/ui/components/fields/richtext-editor/plugins/NotionToolbarPlugin.tsx +0 -184
  47. package/src/admin/ui/components/fields/richtext-editor/plugins/SimpleToolbarPlugin.tsx +0 -240
  48. package/src/admin/ui/components/fields/richtext-editor/plugins/ValueSyncPlugin.tsx +0 -40
  49. package/src/admin/ui/components/fields/utils.ts +0 -1
  50. package/src/admin/ui/components/link.tsx +0 -41
  51. package/src/admin/ui/components/media/AssetManagerModal.tsx +0 -334
  52. package/src/admin/ui/components/toast.tsx +0 -72
  53. package/src/admin/ui/components/ui/accordion.tsx +0 -51
  54. package/src/admin/ui/components/ui/alert-dialog.tsx +0 -98
  55. package/src/admin/ui/components/ui/blocks.tsx +0 -32
  56. package/src/admin/ui/components/ui/breadcrumbs.tsx +0 -59
  57. package/src/admin/ui/components/ui/button.tsx +0 -26
  58. package/src/admin/ui/components/ui/collapsible.tsx +0 -124
  59. package/src/admin/ui/components/ui/dialog.tsx +0 -79
  60. package/src/admin/ui/components/ui/group.tsx +0 -20
  61. package/src/admin/ui/components/ui/index.ts +0 -17
  62. package/src/admin/ui/components/ui/input.tsx +0 -12
  63. package/src/admin/ui/components/ui/join.tsx +0 -53
  64. package/src/admin/ui/components/ui/label.tsx +0 -11
  65. package/src/admin/ui/components/ui/radio-group.tsx +0 -75
  66. package/src/admin/ui/components/ui/relationship-detail-sheet.tsx +0 -122
  67. package/src/admin/ui/components/ui/relationship.tsx +0 -58
  68. package/src/admin/ui/components/ui/scroll-area.tsx +0 -19
  69. package/src/admin/ui/components/ui/select.tsx +0 -187
  70. package/src/admin/ui/components/ui/separator.tsx +0 -21
  71. package/src/admin/ui/components/ui/sheet.tsx +0 -106
  72. package/src/admin/ui/components/ui/tabs.tsx +0 -116
  73. package/src/admin/ui/components/ui/utils.ts +0 -3
  74. package/src/admin/ui/hooks/use-debounce.ts +0 -15
  75. package/src/admin/ui/styles/_locale-switcher.scss +0 -33
  76. package/src/admin/ui/styles/accordion.scss +0 -60
  77. package/src/admin/ui/styles/animations.scss +0 -41
  78. package/src/admin/ui/styles/asset-manager.scss +0 -547
  79. package/src/admin/ui/styles/badge.scss +0 -13
  80. package/src/admin/ui/styles/base.scss +0 -22
  81. package/src/admin/ui/styles/button.scss +0 -161
  82. package/src/admin/ui/styles/card.scss +0 -13
  83. package/src/admin/ui/styles/collapsible.scss +0 -75
  84. package/src/admin/ui/styles/data-detail.scss +0 -92
  85. package/src/admin/ui/styles/dialog.scss +0 -102
  86. package/src/admin/ui/styles/empty-state.scss +0 -22
  87. package/src/admin/ui/styles/group.scss +0 -19
  88. package/src/admin/ui/styles/index.scss +0 -33
  89. package/src/admin/ui/styles/input.scss +0 -80
  90. package/src/admin/ui/styles/label.scss +0 -12
  91. package/src/admin/ui/styles/layout.scss +0 -56
  92. package/src/admin/ui/styles/lexical.scss +0 -469
  93. package/src/admin/ui/styles/loading.scss +0 -102
  94. package/src/admin/ui/styles/media-registry.scss +0 -597
  95. package/src/admin/ui/styles/pagination.scss +0 -20
  96. package/src/admin/ui/styles/radio-group.scss +0 -66
  97. package/src/admin/ui/styles/row.scss +0 -17
  98. package/src/admin/ui/styles/scrollbar.scss +0 -36
  99. package/src/admin/ui/styles/select.scss +0 -121
  100. package/src/admin/ui/styles/separator.scss +0 -14
  101. package/src/admin/ui/styles/sheet.scss +0 -152
  102. package/src/admin/ui/styles/sidebar.scss +0 -148
  103. package/src/admin/ui/styles/switch.scss +0 -59
  104. package/src/admin/ui/styles/table.scss +0 -207
  105. package/src/admin/ui/styles/tabs.scss +0 -62
  106. package/src/admin/ui/styles/toast.scss +0 -45
  107. package/src/admin/ui/styles/variables.scss +0 -24
  108. package/src/admin/ui/views/collection-list-view.tsx +0 -720
  109. package/src/admin/ui/views/dashboard-view.tsx +0 -263
  110. package/src/admin/ui/views/document-edit-view.tsx +0 -384
  111. package/src/admin/ui/views/global-edit-view.tsx +0 -226
  112. package/src/admin/ui/views/init-view.tsx +0 -182
  113. package/src/admin/ui/views/login-view.tsx +0 -123
  114. package/src/admin/ui/views/media-registry-view.tsx +0 -1104
  115. package/src/admin/ui/views/settings-view.tsx +0 -729
  116. package/src/admin/webcomponent.tsx +0 -15
  117. package/src/auth/index.ts +0 -194
  118. package/src/auth/migrations.ts +0 -87
  119. package/src/auth/premissions.ts +0 -46
  120. package/src/cli/commands/generate-types.ts +0 -116
  121. package/src/cli/commands/init.ts +0 -95
  122. package/src/cli/commands/migrate-commands.ts +0 -160
  123. package/src/cli/commands/seed-command.ts +0 -11
  124. package/src/cli/d1-mock.ts +0 -101
  125. package/src/cli/index.test.ts +0 -84
  126. package/src/cli/index.ts +0 -183
  127. package/src/cli/r2-mock.ts +0 -217
  128. package/src/cli/seeding.ts +0 -409
  129. package/src/client.ts +0 -181
  130. package/src/config-utils.ts +0 -102
  131. package/src/config.ts +0 -49
  132. package/src/db/adapter.ts +0 -53
  133. package/src/db/better-sqlite.ts +0 -630
  134. package/src/db/bun-sqlite.ts +0 -646
  135. package/src/db/d1.ts +0 -711
  136. package/src/db/index.ts +0 -2
  137. package/src/db/kysely/data-mapper.ts +0 -142
  138. package/src/db/kysely/field-mapper.ts +0 -148
  139. package/src/db/kysely/migration-generator.ts +0 -223
  140. package/src/db/kysely/query-builder.ts +0 -92
  141. package/src/db/kysely/schema-builder.ts +0 -439
  142. package/src/db/kysely/sql-utils.ts +0 -13
  143. package/src/db/migration.ts +0 -40
  144. package/src/db/postgres.ts +0 -621
  145. package/src/db/sqlite.ts +0 -658
  146. package/src/db/system-schema.ts +0 -121
  147. package/src/index.ts +0 -11
  148. package/src/runtimes/README.md +0 -59
  149. package/src/runtimes/bun.ts +0 -49
  150. package/src/runtimes/cloudflare-workers.ts +0 -38
  151. package/src/runtimes/next.ts +0 -26
  152. package/src/runtimes/node.ts +0 -52
  153. package/src/schema/collection.ts +0 -184
  154. package/src/schema/fields/base.ts +0 -164
  155. package/src/schema/fields/index.ts +0 -427
  156. package/src/schema/global.ts +0 -145
  157. package/src/schema/index.ts +0 -4
  158. package/src/schema/infer.ts +0 -72
  159. package/src/server/admin-router.ts +0 -20
  160. package/src/server/admin.ts +0 -142
  161. package/src/server/assets.ts +0 -306
  162. package/src/server/collection-router.ts +0 -55
  163. package/src/server/handlers.ts +0 -722
  164. package/src/server/middlewares/admin.ts +0 -27
  165. package/src/server/middlewares/auth.ts +0 -89
  166. package/src/server/middlewares/context.ts +0 -17
  167. package/src/server/middlewares/cors.ts +0 -24
  168. package/src/server/middlewares/database-init.ts +0 -74
  169. package/src/server/middlewares/rate-limit.ts +0 -71
  170. package/src/server/router.ts +0 -47
  171. package/src/server/setup-middlewares.ts +0 -58
  172. package/src/server/system-router.ts +0 -35
  173. package/src/server.ts +0 -9
  174. package/src/storage/adapters/cloudflare-r2.ts +0 -136
  175. package/src/storage/adapters/local.ts +0 -146
  176. package/src/storage/adapters/s3.ts +0 -186
  177. package/src/storage/errors.ts +0 -46
  178. package/src/storage/index.ts +0 -6
  179. package/src/storage/types.ts +0 -39
  180. package/src/types.ts +0 -605
  181. package/src/utils/lexical.ts +0 -37
  182. package/src/utils/logger.ts +0 -73
  183. package/src/validation.ts +0 -429
  184. package/src/validator.ts +0 -179
  185. package/test/admin-custom-field.test.ts +0 -162
  186. package/test/admin-react-field.test.tsx +0 -134
  187. package/test/api-features.test.ts +0 -78
  188. package/test/api.test.ts +0 -178
  189. package/test/auth.test.ts +0 -62
  190. package/test/cli-integration.test.ts +0 -148
  191. package/test/cli.test.ts +0 -25
  192. package/test/db/postgres.test.ts +0 -95
  193. package/test/db/sqlite-filter.test.ts +0 -53
  194. package/test/db/sqlite.test.ts +0 -82
  195. package/test/engine-features.test.ts +0 -79
  196. package/test/globals.test.ts +0 -74
  197. package/test/integration-tmp/db-app/opacacms.config.ts +0 -15
  198. package/test/integration-tmp/my-sqlite-app/opacacms.config.ts +0 -25
  199. package/test/integration-tmp/my-test-app/index.ts +0 -8
  200. package/test/integration-tmp/my-test-app/opacacms.config.ts +0 -16
  201. package/test/integration-tmp/my-test-app/package.json +0 -12
  202. package/test/populate.test.ts +0 -79
  203. package/test/runtimes.test.ts +0 -43
  204. package/test/schema-builder.test.ts +0 -107
  205. package/test/schema-features.test.ts +0 -63
  206. package/test/seeding.test.ts +0 -68
  207. package/test/storage/local.test.ts +0 -72
  208. package/test/storage/s3.test.ts +0 -60
  209. package/test/structural-data.test.ts +0 -100
  210. package/test/test-setup.ts +0 -11
  211. package/test/validation.test.ts +0 -162
  212. package/tsconfig.json +0 -42
@@ -1,142 +0,0 @@
1
- import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
2
- import { useLexicalNodeSelection } from "@lexical/react/useLexicalNodeSelection";
3
- import { $getNodeByKey } from "lexical";
4
- import { Trash2 } from "lucide-react";
5
- import type * as React from "react";
6
- import { useCallback, useEffect, useRef, useState } from "react";
7
-
8
- // To avoid circular dependency, we'll check for the type property directly if needed
9
- // or we can just trust the nodeKey refers to an ImageNode.
10
-
11
- export default function ImageComponent({
12
- src,
13
- altText,
14
- nodeKey,
15
- width,
16
- height,
17
- }: {
18
- src: string;
19
- altText: string;
20
- nodeKey: string;
21
- width?: number;
22
- height?: number;
23
- }) {
24
- const [editor] = useLexicalComposerContext();
25
- const [isSelected, setSelected] = useLexicalNodeSelection(nodeKey);
26
- const [isResizing, setIsResizing] = useState(false);
27
- const imageRef = useRef<HTMLImageElement>(null);
28
- const [resizingWidth, setResizingWidth] = useState<number | undefined>(width);
29
-
30
- const onDelete = useCallback(() => {
31
- editor.update(() => {
32
- const node = $getNodeByKey(nodeKey);
33
- if (node) {
34
- node.remove();
35
- }
36
- });
37
- }, [editor, nodeKey]);
38
-
39
- const onResizeStart = (e: React.MouseEvent) => {
40
- e.preventDefault();
41
- e.stopPropagation();
42
- setIsResizing(true);
43
- };
44
-
45
- useEffect(() => {
46
- if (!isResizing) return;
47
-
48
- const onMouseMove = (e: MouseEvent) => {
49
- if (imageRef.current) {
50
- const { left } = imageRef.current.getBoundingClientRect();
51
- const newWidth = Math.max(50, e.clientX - left);
52
- setResizingWidth(newWidth);
53
- }
54
- };
55
-
56
- const onMouseUp = () => {
57
- setIsResizing(false);
58
- if (resizingWidth !== undefined) {
59
- editor.update(() => {
60
- const node = $getNodeByKey(nodeKey);
61
- // @ts-expect-error - node has setWidth if it is an ImageNode
62
- if (node && typeof node.setWidth === "function") {
63
- // @ts-expect-error
64
- node.setWidth(resizingWidth);
65
- }
66
- });
67
- }
68
- };
69
-
70
- document.addEventListener("mousemove", onMouseMove);
71
- document.addEventListener("mouseup", onMouseUp);
72
-
73
- return () => {
74
- document.removeEventListener("mousemove", onMouseMove);
75
- document.removeEventListener("mouseup", onMouseUp);
76
- };
77
- }, [isResizing, resizingWidth, editor, nodeKey]);
78
-
79
- useEffect(() => {
80
- setResizingWidth(width);
81
- }, [width]);
82
-
83
- return (
84
- <div
85
- role="button"
86
- tabIndex={-1}
87
- className={`editor-image-wrapper ${isSelected ? "is-selected" : ""}`}
88
- onClick={(e) => {
89
- e.preventDefault();
90
- e.stopPropagation();
91
- setSelected(!isSelected);
92
- }}
93
- onKeyDown={(e) => {
94
- if (e.key === "Enter" || e.key === " ") {
95
- e.preventDefault();
96
- setSelected(!isSelected);
97
- }
98
- }}
99
- style={{
100
- display: "inline-block",
101
- position: "relative",
102
- cursor: "default",
103
- lineHeight: 0,
104
- zIndex: isSelected ? 10 : 1,
105
- }}
106
- >
107
- <img
108
- ref={imageRef}
109
- src={src}
110
- alt={altText}
111
- style={{
112
- width: resizingWidth || (width ? `${width}px` : "auto"),
113
- height: height ? `${height}px` : "auto",
114
- maxWidth: "100%",
115
- display: "block",
116
- }}
117
- className="editor-image-img"
118
- />
119
- {isSelected && (
120
- <>
121
- <button
122
- type="button"
123
- className="editor-image-resizer"
124
- onMouseDown={onResizeStart}
125
- aria-label="Resize image"
126
- />
127
- <button
128
- type="button"
129
- className="editor-image-delete"
130
- onClick={(e) => {
131
- e.stopPropagation();
132
- onDelete();
133
- }}
134
- aria-label="Delete image"
135
- >
136
- <Trash2 size={14} />
137
- </button>
138
- </>
139
- )}
140
- </div>
141
- );
142
- }
@@ -1,95 +0,0 @@
1
- import { DecoratorNode, type NodeKey, type SerializedLexicalNode } from "lexical";
2
- import type React from "react";
3
- import ImageComponent from "./ImageComponent";
4
-
5
- export interface SerializedImageNode extends SerializedLexicalNode {
6
- src: string;
7
- altText: string;
8
- height?: number;
9
- width?: number;
10
- type: "image";
11
- }
12
-
13
- export class ImageNode extends DecoratorNode<React.ReactElement> {
14
- __src: string;
15
- __altText: string;
16
- __height?: number;
17
- __width?: number;
18
-
19
- static override getType(): string {
20
- return "image";
21
- }
22
-
23
- static override clone(node: ImageNode): ImageNode {
24
- return new ImageNode(node.__src, node.__altText, node.__height, node.__width, node.__key);
25
- }
26
-
27
- constructor(src: string, altText: string, height?: number, width?: number, key?: NodeKey) {
28
- super(key);
29
- this.__src = src;
30
- this.__altText = altText;
31
- this.__height = height;
32
- this.__width = width;
33
- }
34
-
35
- override exportJSON(): SerializedImageNode {
36
- return {
37
- altText: this.__altText,
38
- height: this.__height,
39
- src: this.__src,
40
- type: "image",
41
- version: 1,
42
- width: this.__width,
43
- };
44
- }
45
-
46
- static override importJSON(serializedNode: SerializedImageNode): ImageNode {
47
- const { altText, height, width, src } = serializedNode;
48
- return $createImageNode(src, altText, height, width);
49
- }
50
-
51
- override createDOM(_config: unknown): HTMLElement {
52
- const span = document.createElement("span");
53
- span.className = "editor-image";
54
- return span;
55
- }
56
-
57
- override updateDOM(): false {
58
- return false;
59
- }
60
-
61
- setWidth(width: number): void {
62
- const writable = this.getWritable();
63
- writable.__width = width;
64
- }
65
-
66
- setHeight(height: number): void {
67
- const writable = this.getWritable();
68
- writable.__height = height;
69
- }
70
-
71
- override decorate(): React.ReactElement {
72
- return (
73
- <ImageComponent
74
- src={this.__src}
75
- altText={this.__altText}
76
- nodeKey={this.__key}
77
- width={this.__width}
78
- height={this.__height}
79
- />
80
- );
81
- }
82
- }
83
-
84
- export function $createImageNode(
85
- src: string,
86
- altText: string,
87
- height?: number,
88
- width?: number,
89
- ): ImageNode {
90
- return new ImageNode(src, altText, height, width);
91
- }
92
-
93
- export function $isImageNode(node: unknown): node is ImageNode {
94
- return node instanceof ImageNode;
95
- }
@@ -1,226 +0,0 @@
1
- import { $createCodeNode } from "@lexical/code";
2
- import { INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND } from "@lexical/list";
3
- import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
4
- import {
5
- LexicalTypeaheadMenuPlugin,
6
- MenuOption,
7
- useBasicTypeaheadTriggerMatch,
8
- } from "@lexical/react/LexicalTypeaheadMenuPlugin";
9
- import { $createHeadingNode, $createQuoteNode } from "@lexical/rich-text";
10
- import { $setBlocksType } from "@lexical/selection";
11
- import { $getSelection, $insertNodes, $isRangeSelection, type TextNode } from "lexical";
12
- import {
13
- Code,
14
- Heading1,
15
- Heading2,
16
- Image as ImageIcon,
17
- List,
18
- ListOrdered,
19
- Quote,
20
- Type,
21
- } from "lucide-react";
22
- import type * as React from "react";
23
- import { useCallback, useMemo, useState } from "react";
24
- import * as ReactDOM from "react-dom";
25
- import { getCurrentBaseURL } from "../../../../../api-client";
26
- import { AssetManagerModal } from "../../../media/AssetManagerModal";
27
- import { ScrollArea } from "../../../ui/scroll-area";
28
- import { $createImageNode } from "../nodes/ImageNode";
29
-
30
- class ComponentPickerOption extends MenuOption {
31
- title: string;
32
- icon: React.ReactNode;
33
- description: string;
34
- onSelect: (queryString: string) => void;
35
-
36
- constructor(
37
- title: string,
38
- options: {
39
- icon: React.ReactNode;
40
- description: string;
41
- onSelect: (queryString: string) => void;
42
- },
43
- ) {
44
- super(title);
45
- this.title = title;
46
- this.icon = options.icon;
47
- this.description = options.description;
48
- this.onSelect = options.onSelect;
49
- }
50
- }
51
-
52
- export function ComponentPickerPlugin() {
53
- const [editor] = useLexicalComposerContext();
54
- const [queryString, setQueryString] = useState<string | null>(null);
55
- const [isModalOpen, setIsModalOpen] = useState(false);
56
-
57
- const checkForTriggerMatch = useBasicTypeaheadTriggerMatch("/", {
58
- minLength: 0,
59
- });
60
-
61
- const handleInsertImage = (asset: any) => {
62
- const url = asset.url || `${getCurrentBaseURL()}/api/assets/${asset.id || asset.assetId}/view`;
63
- editor.update(() => {
64
- const imageNode = $createImageNode(url, asset.filename || "Image");
65
- $insertNodes([imageNode]);
66
- });
67
- setIsModalOpen(false);
68
- };
69
-
70
- const options = useMemo(() => {
71
- const baseOptions = [
72
- new ComponentPickerOption("Paragraph", {
73
- icon: <Type size={18} />,
74
- description: "Just start typing with plain text.",
75
- onSelect: () => {
76
- editor.update(() => {
77
- const selection = $getSelection();
78
- if ($isRangeSelection(selection)) {
79
- $setBlocksType(selection, () => $createHeadingNode("h1")); // Default logic often involves reset, simplify
80
- $setBlocksType(selection, () => $createHeadingNode("h1")); // placeholder
81
- }
82
- });
83
- },
84
- }),
85
- new ComponentPickerOption("Heading 1", {
86
- icon: <Heading1 size={18} />,
87
- description: "Large section heading.",
88
- onSelect: () => {
89
- editor.update(() => {
90
- const selection = $getSelection();
91
- if ($isRangeSelection(selection)) {
92
- $setBlocksType(selection, () => $createHeadingNode("h1"));
93
- }
94
- });
95
- },
96
- }),
97
- new ComponentPickerOption("Heading 2", {
98
- icon: <Heading2 size={18} />,
99
- description: "Medium section heading.",
100
- onSelect: () => {
101
- editor.update(() => {
102
- const selection = $getSelection();
103
- if ($isRangeSelection(selection)) {
104
- $setBlocksType(selection, () => $createHeadingNode("h2"));
105
- }
106
- });
107
- },
108
- }),
109
- new ComponentPickerOption("Bullet List", {
110
- icon: <List size={18} />,
111
- description: "Create a simple bullet list.",
112
- onSelect: () => {
113
- editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined);
114
- },
115
- }),
116
- new ComponentPickerOption("Numbered List", {
117
- icon: <ListOrdered size={18} />,
118
- description: "Create a list with numbering.",
119
- onSelect: () => {
120
- editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined);
121
- },
122
- }),
123
- new ComponentPickerOption("Quote", {
124
- icon: <Quote size={18} />,
125
- description: "Capture a quotation.",
126
- onSelect: () => {
127
- editor.update(() => {
128
- const selection = $getSelection();
129
- if ($isRangeSelection(selection)) {
130
- $setBlocksType(selection, () => $createQuoteNode());
131
- }
132
- });
133
- },
134
- }),
135
- new ComponentPickerOption("Code Block", {
136
- icon: <Code size={18} />,
137
- description: "Write code snippets.",
138
- onSelect: () => {
139
- editor.update(() => {
140
- const selection = $getSelection();
141
- if ($isRangeSelection(selection)) {
142
- $setBlocksType(selection, () => $createCodeNode());
143
- }
144
- });
145
- },
146
- }),
147
- new ComponentPickerOption("Image", {
148
- icon: <ImageIcon size={18} />,
149
- description: "Insert an image from media library.",
150
- onSelect: () => {
151
- setIsModalOpen(true);
152
- },
153
- }),
154
- ];
155
-
156
- if (!queryString) return baseOptions;
157
-
158
- return baseOptions.filter((option) =>
159
- option.title.toLowerCase().includes(queryString.toLowerCase()),
160
- );
161
- }, [editor, queryString]);
162
-
163
- const onSelectOption = useCallback(
164
- (
165
- selectedOption: ComponentPickerOption,
166
- nodeToRemove: TextNode | null,
167
- closeMenu: () => void,
168
- ) => {
169
- editor.update(() => {
170
- if (nodeToRemove) {
171
- nodeToRemove.remove();
172
- }
173
- selectedOption.onSelect(queryString || "");
174
- closeMenu();
175
- });
176
- },
177
- [editor, queryString],
178
- );
179
-
180
- return (
181
- <>
182
- <LexicalTypeaheadMenuPlugin<ComponentPickerOption>
183
- onQueryChange={setQueryString}
184
- onSelectOption={onSelectOption}
185
- triggerFn={checkForTriggerMatch}
186
- options={options}
187
- menuRenderFn={(
188
- anchorElementRef,
189
- { selectedIndex, selectOptionAndCleanUp, setHighlightedIndex },
190
- ) =>
191
- anchorElementRef.current && options.length > 0
192
- ? ReactDOM.createPortal(
193
- <ScrollArea className="opaca-slash-menu" maxHeight="300px">
194
- {options.map((option, i) => (
195
- <button
196
- key={option.key}
197
- className={`opaca-slash-menu-item ${
198
- selectedIndex === i ? "is-selected" : ""
199
- }`}
200
- onClick={() => selectOptionAndCleanUp(option)}
201
- onMouseEnter={() => setHighlightedIndex(i)}
202
- >
203
- <div className="opaca-slash-menu-icon">{option.icon}</div>
204
- <div className="opaca-slash-menu-text">
205
- <span className="opaca-slash-menu-title">{option.title}</span>
206
- <span className="opaca-slash-menu-desc">{option.description}</span>
207
- </div>
208
- </button>
209
- ))}
210
- </ScrollArea>,
211
- anchorElementRef.current,
212
- )
213
- : null
214
- }
215
- />
216
-
217
- {isModalOpen && (
218
- <AssetManagerModal
219
- bucket={"default"}
220
- onClose={() => setIsModalOpen(false)}
221
- onSelect={handleInsertImage}
222
- />
223
- )}
224
- </>
225
- );
226
- }
@@ -1,16 +0,0 @@
1
- import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
2
- import { useEffect } from "react";
3
-
4
- interface EditableSyncPluginProps {
5
- isEditable: boolean;
6
- }
7
-
8
- export function EditableSyncPlugin({ isEditable }: EditableSyncPluginProps) {
9
- const [editor] = useLexicalComposerContext();
10
-
11
- useEffect(() => {
12
- editor.setEditable(isEditable);
13
- }, [editor, isEditable]);
14
-
15
- return null;
16
- }
@@ -1,184 +0,0 @@
1
- import { TOGGLE_LINK_COMMAND } from "@lexical/link";
2
- import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
3
- import type { RangeSelection } from "lexical";
4
- import {
5
- $getSelection,
6
- $insertNodes,
7
- $isRangeSelection,
8
- COMMAND_PRIORITY_CRITICAL,
9
- FORMAT_TEXT_COMMAND,
10
- SELECTION_CHANGE_COMMAND,
11
- } from "lexical";
12
- import { Bold, Image as ImageIcon, Italic, LinkIcon, Strikethrough } from "lucide-react";
13
- import * as React from "react";
14
- import { useCallback, useEffect, useRef, useState } from "react";
15
- import * as ReactDOM from "react-dom";
16
- import { getCurrentBaseURL } from "../../../../../api-client";
17
- import { AssetManagerModal } from "../../../media/AssetManagerModal";
18
- import { $createImageNode } from "../nodes/ImageNode";
19
-
20
- export function NotionToolbarPlugin() {
21
- const [isModalOpen, setIsModalOpen] = useState(false);
22
- const [editor] = useLexicalComposerContext();
23
- const [isBold, setIsBold] = useState(false);
24
- const [isItalic, setIsItalic] = useState(false);
25
- const [isStrikethrough, setIsStrikethrough] = useState(false);
26
- const [showToolbar, setShowToolbar] = useState(false);
27
- const toolbarRef = useRef<HTMLDivElement>(null);
28
-
29
- const [position, setPosition] = useState({ top: 0, left: 0 });
30
-
31
- const updateToolbar = useCallback(() => {
32
- const selection = $getSelection();
33
- if ($isRangeSelection(selection)) {
34
- setIsBold(selection.hasFormat("bold"));
35
- setIsItalic(selection.hasFormat("italic"));
36
- setIsStrikethrough(selection.hasFormat("strikethrough"));
37
-
38
- const nativeSelection = window.getSelection();
39
- const rootElement = editor.getRootElement();
40
-
41
- if (
42
- nativeSelection !== null &&
43
- !nativeSelection.isCollapsed &&
44
- rootElement !== null &&
45
- rootElement.contains(nativeSelection.anchorNode)
46
- ) {
47
- const range = nativeSelection.getRangeAt(0);
48
- const rect = range.getBoundingClientRect();
49
-
50
- // Calculate position relative to the document or a parent container
51
- // To keep it simple, we use fixed positioning for the portal if needed,
52
- // but here we'll try to position it above the selection.
53
- setPosition({
54
- top: rect.top - 45, // 45px above the selection
55
- left: rect.left + rect.width / 2,
56
- });
57
-
58
- setShowToolbar(true);
59
- } else {
60
- setShowToolbar(false);
61
- }
62
- } else {
63
- setShowToolbar(false);
64
- }
65
- }, [editor]);
66
-
67
- useEffect(() => {
68
- return editor.registerUpdateListener(({ editorState }) => {
69
- editorState.read(() => {
70
- updateToolbar();
71
- });
72
- });
73
- }, [editor, updateToolbar]);
74
-
75
- useEffect(() => {
76
- return editor.registerCommand(
77
- SELECTION_CHANGE_COMMAND,
78
- (_payload) => {
79
- updateToolbar();
80
- return false;
81
- },
82
- COMMAND_PRIORITY_CRITICAL,
83
- );
84
- }, [editor, updateToolbar]);
85
-
86
- // Also update on scroll/resize to keep it attached (optional but good)
87
- useEffect(() => {
88
- const handleUpdate = () => {
89
- if (showToolbar) {
90
- updateToolbar();
91
- }
92
- };
93
- window.addEventListener("resize", handleUpdate);
94
- window.addEventListener("scroll", handleUpdate);
95
- return () => {
96
- window.removeEventListener("resize", handleUpdate);
97
- window.removeEventListener("scroll", handleUpdate);
98
- };
99
- }, [showToolbar, updateToolbar]);
100
-
101
- const handleInsertImage = (asset: any) => {
102
- const url = asset.url || `${getCurrentBaseURL()}/api/assets/${asset.id || asset.assetId}/view`;
103
- editor.update(() => {
104
- const imageNode = $createImageNode(url, asset.filename || "Image");
105
- $insertNodes([imageNode]);
106
- });
107
- setIsModalOpen(false);
108
- };
109
-
110
- if (!showToolbar && !isModalOpen) return null;
111
-
112
- return (
113
- <>
114
- {showToolbar &&
115
- ReactDOM.createPortal(
116
- <div
117
- ref={toolbarRef}
118
- className="opaca-lexical-bubble-menu"
119
- style={{
120
- position: "fixed",
121
- top: position.top - (typeof window !== "undefined" ? window.scrollY : 0),
122
- left: position.left,
123
- transform: "translateX(-50%)",
124
- zIndex: 1000,
125
- pointerEvents: "auto",
126
- }}
127
- >
128
- <button
129
- type="button"
130
- onClick={() => {
131
- editor.dispatchCommand(FORMAT_TEXT_COMMAND, "bold");
132
- }}
133
- className={`opaca-lexical-btn ${isBold ? "is-active" : ""}`}
134
- >
135
- <Bold size={16} />
136
- </button>
137
- <button
138
- type="button"
139
- onClick={() => {
140
- editor.dispatchCommand(FORMAT_TEXT_COMMAND, "italic");
141
- }}
142
- className={`opaca-lexical-btn ${isItalic ? "is-active" : ""}`}
143
- >
144
- <Italic size={16} />
145
- </button>
146
- <button
147
- type="button"
148
- onClick={() => {
149
- editor.dispatchCommand(FORMAT_TEXT_COMMAND, "strikethrough");
150
- }}
151
- className={`opaca-lexical-btn ${isStrikethrough ? "is-active" : ""}`}
152
- >
153
- <Strikethrough size={16} />
154
- </button>
155
- <button
156
- type="button"
157
- onClick={() => {
158
- editor.dispatchCommand(TOGGLE_LINK_COMMAND, "https://");
159
- }}
160
- className={`opaca-lexical-btn`}
161
- >
162
- <LinkIcon size={16} />
163
- </button>
164
- <button
165
- type="button"
166
- onClick={() => setIsModalOpen(true)}
167
- className={`opaca-lexical-btn`}
168
- >
169
- <ImageIcon size={16} />
170
- </button>
171
- </div>,
172
- document.body,
173
- )}
174
-
175
- {isModalOpen && (
176
- <AssetManagerModal
177
- bucket={"default"}
178
- onClose={() => setIsModalOpen(false)}
179
- onSelect={handleInsertImage}
180
- />
181
- )}
182
- </>
183
- );
184
- }