@gogitcms/editor 0.23.0
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/app/index.html +38 -0
- package/app/src/App.tsx +2665 -0
- package/app/src/apollo.ts +119 -0
- package/app/src/auth.ts +248 -0
- package/app/src/config.ts +88 -0
- package/app/src/main.tsx +18 -0
- package/app/src/media.ts +151 -0
- package/app/src/navigation.tsx +321 -0
- package/app/src/plugins.ts +11 -0
- package/app/src/previewHost.tsx +263 -0
- package/app/src/previewToken.ts +68 -0
- package/app/src/queries.ts +591 -0
- package/app/src/virtual-cms-plugins.d.ts +6 -0
- package/app/vendor/analytics/src/__tests__/config.test.ts +91 -0
- package/app/vendor/analytics/src/config.ts +94 -0
- package/app/vendor/analytics/src/events.ts +56 -0
- package/app/vendor/analytics/src/index.ts +23 -0
- package/app/vendor/analytics/src/provider.tsx +196 -0
- package/app/vendor/design-system/src/ThemeProvider.tsx +86 -0
- package/app/vendor/design-system/src/__tests__/ApplyChangesModal.test.tsx +148 -0
- package/app/vendor/design-system/src/__tests__/BranchImport.test.tsx +46 -0
- package/app/vendor/design-system/src/__tests__/Button.test.tsx +45 -0
- package/app/vendor/design-system/src/__tests__/ChangeRequestSummary.test.tsx +57 -0
- package/app/vendor/design-system/src/__tests__/ContentBrowser.changes.test.tsx +611 -0
- package/app/vendor/design-system/src/__tests__/ContentBrowser.collab.test.tsx +322 -0
- package/app/vendor/design-system/src/__tests__/ContentBrowser.collabsync.test.tsx +264 -0
- package/app/vendor/design-system/src/__tests__/ContentBrowser.contentslot.test.tsx +53 -0
- package/app/vendor/design-system/src/__tests__/ContentBrowser.discriminator.test.tsx +142 -0
- package/app/vendor/design-system/src/__tests__/ContentBrowser.drafts.test.tsx +271 -0
- package/app/vendor/design-system/src/__tests__/ContentBrowser.fields.test.tsx +117 -0
- package/app/vendor/design-system/src/__tests__/ContentBrowser.media.test.tsx +140 -0
- package/app/vendor/design-system/src/__tests__/ContentBrowser.mixedcollab.test.tsx +63 -0
- package/app/vendor/design-system/src/__tests__/ContentBrowser.mixedvalues.test.tsx +38 -0
- package/app/vendor/design-system/src/__tests__/ContentBrowser.pagination.test.tsx +62 -0
- package/app/vendor/design-system/src/__tests__/ContentBrowser.previewtab.test.tsx +212 -0
- package/app/vendor/design-system/src/__tests__/ContentBrowser.reorder.test.tsx +45 -0
- package/app/vendor/design-system/src/__tests__/ContentBrowser.search.test.tsx +135 -0
- package/app/vendor/design-system/src/__tests__/ContentBrowser.selectvalue.test.tsx +185 -0
- package/app/vendor/design-system/src/__tests__/ContentBrowser.staged.test.tsx +132 -0
- package/app/vendor/design-system/src/__tests__/ContentBrowser.usermenu.test.tsx +56 -0
- package/app/vendor/design-system/src/__tests__/MediaBrowser.test.tsx +353 -0
- package/app/vendor/design-system/src/__tests__/MediaField.test.tsx +185 -0
- package/app/vendor/design-system/src/__tests__/Notifications.test.tsx +69 -0
- package/app/vendor/design-system/src/__tests__/Onboarding.test.tsx +287 -0
- package/app/vendor/design-system/src/__tests__/cssTokens.test.ts +201 -0
- package/app/vendor/design-system/src/__tests__/fieldComponents.test.ts +43 -0
- package/app/vendor/design-system/src/__tests__/reorder.test.ts +58 -0
- package/app/vendor/design-system/src/components/ApplyChangesModal.tsx +348 -0
- package/app/vendor/design-system/src/components/BranchImport.tsx +157 -0
- package/app/vendor/design-system/src/components/BranchMenu.tsx +192 -0
- package/app/vendor/design-system/src/components/Button.tsx +131 -0
- package/app/vendor/design-system/src/components/ChangeDetail.tsx +472 -0
- package/app/vendor/design-system/src/components/ChangeRequestSummary.tsx +173 -0
- package/app/vendor/design-system/src/components/CollabField.tsx +388 -0
- package/app/vendor/design-system/src/components/ContentBrowser.tsx +5073 -0
- package/app/vendor/design-system/src/components/Icon.tsx +28 -0
- package/app/vendor/design-system/src/components/Icon.web.tsx +31 -0
- package/app/vendor/design-system/src/components/Input.tsx +106 -0
- package/app/vendor/design-system/src/components/MediaBrowser.tsx +766 -0
- package/app/vendor/design-system/src/components/MediaField.tsx +670 -0
- package/app/vendor/design-system/src/components/MediaPreview.tsx +91 -0
- package/app/vendor/design-system/src/components/MediaPreview.web.tsx +169 -0
- package/app/vendor/design-system/src/components/NavRow.tsx +105 -0
- package/app/vendor/design-system/src/components/Notifications.tsx +301 -0
- package/app/vendor/design-system/src/components/Onboarding.tsx +751 -0
- package/app/vendor/design-system/src/components/ProjectMenu.tsx +124 -0
- package/app/vendor/design-system/src/components/Segment.tsx +87 -0
- package/app/vendor/design-system/src/components/Skeleton.tsx +216 -0
- package/app/vendor/design-system/src/components/Spinner.tsx +44 -0
- package/app/vendor/design-system/src/components/Text.tsx +85 -0
- package/app/vendor/design-system/src/components/documentDrafts.ts +213 -0
- package/app/vendor/design-system/src/components/layout.tsx +284 -0
- package/app/vendor/design-system/src/components/primitives.tsx +143 -0
- package/app/vendor/design-system/src/components/reorder.ts +40 -0
- package/app/vendor/design-system/src/fieldComponents.ts +63 -0
- package/app/vendor/design-system/src/icons.ts +102 -0
- package/app/vendor/design-system/src/index.ts +198 -0
- package/app/vendor/design-system/src/media.ts +229 -0
- package/app/vendor/design-system/src/theme.ts +116 -0
- package/app/vendor/design-system/src/web/Button.tsx +110 -0
- package/app/vendor/design-system/src/web/Icon.tsx +52 -0
- package/app/vendor/design-system/src/web/Input.tsx +39 -0
- package/app/vendor/design-system/src/web/index.ts +43 -0
- package/app/vendor/design-system/src/web/primitives.tsx +119 -0
- package/app/vendor/markdown-editor/src/MarkdownEditor.tsx +248 -0
- package/app/vendor/markdown-editor/src/Toolbar.tsx +157 -0
- package/app/vendor/markdown-editor/src/field.tsx +67 -0
- package/app/vendor/markdown-editor/src/flavors/commonmark/index.ts +27 -0
- package/app/vendor/markdown-editor/src/flavors/gfm/index.ts +30 -0
- package/app/vendor/markdown-editor/src/flavors/gfm/parser.ts +73 -0
- package/app/vendor/markdown-editor/src/flavors/gfm/schema.ts +70 -0
- package/app/vendor/markdown-editor/src/flavors/gfm/serializer.ts +82 -0
- package/app/vendor/markdown-editor/src/flavors/gfm/taskList.ts +39 -0
- package/app/vendor/markdown-editor/src/flavors/registry.ts +13 -0
- package/app/vendor/markdown-editor/src/flavors/shared/inputrules.ts +79 -0
- package/app/vendor/markdown-editor/src/flavors/shared/keymap.ts +89 -0
- package/app/vendor/markdown-editor/src/flavors/shared/placeholder.ts +24 -0
- package/app/vendor/markdown-editor/src/flavors/shared/plugins.ts +58 -0
- package/app/vendor/markdown-editor/src/flavors/types.ts +22 -0
- package/app/vendor/markdown-editor/src/formats/registry.ts +45 -0
- package/app/vendor/markdown-editor/src/icons.tsx +120 -0
- package/app/vendor/markdown-editor/src/index.ts +23 -0
- package/app/vendor/markdown-editor/src/theme.ts +249 -0
- package/app/vendor/markdown-editor/src/types.ts +58 -0
- package/app/vendor/plugin-sdk/src/__tests__/documentActions.test.ts +108 -0
- package/app/vendor/plugin-sdk/src/__tests__/loader.test.ts +75 -0
- package/app/vendor/plugin-sdk/src/__tests__/registry.test.ts +148 -0
- package/app/vendor/plugin-sdk/src/index.ts +33 -0
- package/app/vendor/plugin-sdk/src/loader.ts +45 -0
- package/app/vendor/plugin-sdk/src/navkeys.ts +17 -0
- package/app/vendor/plugin-sdk/src/react.tsx +155 -0
- package/app/vendor/plugin-sdk/src/registry.ts +337 -0
- package/app/vendor/plugin-sdk/src/types.ts +230 -0
- package/app/vendor/realtime/src/__tests__/pure.test.ts +58 -0
- package/app/vendor/realtime/src/__tests__/seeded-room.base64 +1 -0
- package/app/vendor/realtime/src/__tests__/seeding.test.ts +70 -0
- package/app/vendor/realtime/src/collab.ts +308 -0
- package/app/vendor/realtime/src/hooks.ts +57 -0
- package/app/vendor/realtime/src/index.ts +12 -0
- package/app/vendor/realtime/src/pure.ts +43 -0
- package/app/vite.config.mjs +59 -0
- package/bin/gogitcms-editor.mjs +168 -0
- package/npm-shrinkwrap.json +5696 -0
- package/package.json +74 -0
- package/src/commands/build.mjs +100 -0
- package/src/commands/dev.mjs +186 -0
- package/src/commands/init.mjs +144 -0
- package/src/commands/login.mjs +116 -0
- package/src/commands/status.mjs +68 -0
- package/src/lib/api.mjs +122 -0
- package/src/lib/appRoot.mjs +51 -0
- package/src/lib/baked.json +3 -0
- package/src/lib/config.mjs +108 -0
- package/src/lib/credentials.mjs +91 -0
- package/src/lib/defaults.mjs +26 -0
- package/src/lib/graphql.mjs +28 -0
- package/src/lib/localServer.mjs +206 -0
- package/src/lib/open.mjs +23 -0
- package/src/lib/plugins.mjs +164 -0
- package/src/lib/tokenBroker.mjs +85 -0
- package/src/tui/prompts.mjs +92 -0
- package/src/tui/screen.mjs +282 -0
- package/src/tui/theme.mjs +40 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
// Self-contained CSS for the editor. Injected once into the document head by
|
|
2
|
+
// <MarkdownEditor>. Colors are driven by CSS variables scoped to the editor
|
|
3
|
+
// root and switched by a `data-theme` attribute — the host passes "light"/"dark"
|
|
4
|
+
// as a prop (theme context can't cross the Expo DOM-component bridge).
|
|
5
|
+
|
|
6
|
+
export const EDITOR_STYLE_ID = "cms-editor-styles";
|
|
7
|
+
|
|
8
|
+
export const EDITOR_CSS = `
|
|
9
|
+
.cms-editor {
|
|
10
|
+
--cms-ed-bg: #ffffff;
|
|
11
|
+
--cms-ed-fg: #1a1a1a;
|
|
12
|
+
--cms-ed-muted: #6b7280;
|
|
13
|
+
--cms-ed-border: #e2e4e8;
|
|
14
|
+
--cms-ed-accent: #2563eb;
|
|
15
|
+
--cms-ed-code-bg: #f3f4f6;
|
|
16
|
+
--cms-ed-selection: #cfe0ff;
|
|
17
|
+
position: relative;
|
|
18
|
+
color: var(--cms-ed-fg);
|
|
19
|
+
background: var(--cms-ed-bg);
|
|
20
|
+
font: 14px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
21
|
+
border: 1px solid var(--cms-ed-border);
|
|
22
|
+
border-radius: 8px;
|
|
23
|
+
overflow: hidden;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/* Formatting toolbar — pinned to the top of the editor (and the pane in focus
|
|
27
|
+
mode) so it stays visible while the content scrolls. */
|
|
28
|
+
.cms-editor__toolbar {
|
|
29
|
+
position: sticky;
|
|
30
|
+
top: 0;
|
|
31
|
+
z-index: 4;
|
|
32
|
+
display: flex;
|
|
33
|
+
align-items: center;
|
|
34
|
+
gap: 2px;
|
|
35
|
+
flex-wrap: nowrap;
|
|
36
|
+
padding: 5px 8px;
|
|
37
|
+
border-bottom: 1px solid var(--cms-ed-border);
|
|
38
|
+
background: var(--cms-ed-bg);
|
|
39
|
+
}
|
|
40
|
+
.cms-editor__toolbar-sep {
|
|
41
|
+
width: 1px;
|
|
42
|
+
align-self: stretch;
|
|
43
|
+
margin: 4px 4px;
|
|
44
|
+
background: var(--cms-ed-border);
|
|
45
|
+
}
|
|
46
|
+
.cms-editor__tool {
|
|
47
|
+
display: inline-flex;
|
|
48
|
+
align-items: center;
|
|
49
|
+
justify-content: center;
|
|
50
|
+
width: 28px;
|
|
51
|
+
height: 28px;
|
|
52
|
+
padding: 0;
|
|
53
|
+
border: none;
|
|
54
|
+
border-radius: 6px;
|
|
55
|
+
background: transparent;
|
|
56
|
+
color: var(--cms-ed-muted);
|
|
57
|
+
cursor: pointer;
|
|
58
|
+
transition: background 120ms ease, color 120ms ease;
|
|
59
|
+
}
|
|
60
|
+
.cms-editor__tool:hover:not(:disabled) {
|
|
61
|
+
background: var(--cms-ed-code-bg);
|
|
62
|
+
color: var(--cms-ed-fg);
|
|
63
|
+
}
|
|
64
|
+
.cms-editor__tool[aria-pressed="true"] {
|
|
65
|
+
background: var(--cms-ed-code-bg);
|
|
66
|
+
color: var(--cms-ed-accent);
|
|
67
|
+
}
|
|
68
|
+
.cms-editor__tool:disabled {
|
|
69
|
+
opacity: 0.4;
|
|
70
|
+
cursor: default;
|
|
71
|
+
}
|
|
72
|
+
/* The expand toggle sits at the far right of the toolbar. */
|
|
73
|
+
.cms-editor__tool--expand {
|
|
74
|
+
margin-left: auto;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* Focus mode: take over the viewport, not the pane the editor sits in. */
|
|
78
|
+
.cms-editor--expanded {
|
|
79
|
+
position: fixed;
|
|
80
|
+
inset: 0;
|
|
81
|
+
z-index: 1000;
|
|
82
|
+
height: 100%;
|
|
83
|
+
max-height: none;
|
|
84
|
+
border: none;
|
|
85
|
+
border-radius: 0;
|
|
86
|
+
overflow-y: auto;
|
|
87
|
+
}
|
|
88
|
+
.cms-editor--expanded .ProseMirror {
|
|
89
|
+
max-width: 720px;
|
|
90
|
+
margin: 0 auto;
|
|
91
|
+
min-height: calc(100% - 40px);
|
|
92
|
+
padding: 32px 24px 25vh;
|
|
93
|
+
}
|
|
94
|
+
.cms-editor[data-theme="dark"] {
|
|
95
|
+
--cms-ed-bg: #16181d;
|
|
96
|
+
--cms-ed-fg: #e6e6e6;
|
|
97
|
+
--cms-ed-muted: #9aa1ad;
|
|
98
|
+
--cms-ed-border: #2c2f36;
|
|
99
|
+
--cms-ed-accent: #6ea8ff;
|
|
100
|
+
--cms-ed-code-bg: #22252c;
|
|
101
|
+
--cms-ed-selection: #24406b;
|
|
102
|
+
}
|
|
103
|
+
.cms-editor .ProseMirror {
|
|
104
|
+
padding: 12px 16px;
|
|
105
|
+
min-height: 120px;
|
|
106
|
+
outline: none;
|
|
107
|
+
white-space: pre-wrap;
|
|
108
|
+
word-wrap: break-word;
|
|
109
|
+
}
|
|
110
|
+
.cms-editor .ProseMirror[contenteditable="false"] { opacity: 0.85; }
|
|
111
|
+
.cms-editor .ProseMirror p { margin: 0 0 0.75em; }
|
|
112
|
+
.cms-editor .ProseMirror > :last-child { margin-bottom: 0; }
|
|
113
|
+
.cms-editor .ProseMirror h1,
|
|
114
|
+
.cms-editor .ProseMirror h2,
|
|
115
|
+
.cms-editor .ProseMirror h3,
|
|
116
|
+
.cms-editor .ProseMirror h4 { line-height: 1.25; margin: 1em 0 0.5em; font-weight: 600; }
|
|
117
|
+
.cms-editor .ProseMirror h1 { font-size: 1.7em; }
|
|
118
|
+
.cms-editor .ProseMirror h2 { font-size: 1.4em; }
|
|
119
|
+
.cms-editor .ProseMirror h3 { font-size: 1.2em; }
|
|
120
|
+
.cms-editor .ProseMirror a { color: var(--cms-ed-accent); }
|
|
121
|
+
.cms-editor .ProseMirror code {
|
|
122
|
+
background: var(--cms-ed-code-bg);
|
|
123
|
+
border-radius: 4px;
|
|
124
|
+
padding: 0.1em 0.35em;
|
|
125
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
126
|
+
font-size: 0.9em;
|
|
127
|
+
}
|
|
128
|
+
.cms-editor .ProseMirror pre {
|
|
129
|
+
background: var(--cms-ed-code-bg);
|
|
130
|
+
border-radius: 6px;
|
|
131
|
+
padding: 10px 12px;
|
|
132
|
+
overflow-x: auto;
|
|
133
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
134
|
+
font-size: 0.9em;
|
|
135
|
+
}
|
|
136
|
+
.cms-editor .ProseMirror pre code { background: none; padding: 0; }
|
|
137
|
+
.cms-editor .ProseMirror blockquote {
|
|
138
|
+
border-left: 3px solid var(--cms-ed-border);
|
|
139
|
+
margin: 0 0 0.75em;
|
|
140
|
+
padding-left: 12px;
|
|
141
|
+
color: var(--cms-ed-muted);
|
|
142
|
+
}
|
|
143
|
+
.cms-editor .ProseMirror hr {
|
|
144
|
+
border: none;
|
|
145
|
+
border-top: 1px solid var(--cms-ed-border);
|
|
146
|
+
margin: 1em 0;
|
|
147
|
+
}
|
|
148
|
+
.cms-editor .ProseMirror ul,
|
|
149
|
+
.cms-editor .ProseMirror ol { padding-left: 1.4em; margin: 0 0 0.75em; }
|
|
150
|
+
|
|
151
|
+
/* Task lists */
|
|
152
|
+
.cms-editor .ProseMirror li.task-list-item { list-style: none; position: relative; }
|
|
153
|
+
.cms-editor .ProseMirror li.task-list-item .task-list-item-checkbox {
|
|
154
|
+
position: absolute;
|
|
155
|
+
left: -1.5em;
|
|
156
|
+
top: 0.2em;
|
|
157
|
+
width: 1em;
|
|
158
|
+
height: 1em;
|
|
159
|
+
border: 1px solid var(--cms-ed-muted);
|
|
160
|
+
border-radius: 3px;
|
|
161
|
+
background: var(--cms-ed-bg);
|
|
162
|
+
cursor: pointer;
|
|
163
|
+
box-sizing: border-box;
|
|
164
|
+
user-select: none;
|
|
165
|
+
}
|
|
166
|
+
.cms-editor .ProseMirror li.task-list-item[data-checked="true"] .task-list-item-checkbox {
|
|
167
|
+
background: var(--cms-ed-accent);
|
|
168
|
+
border-color: var(--cms-ed-accent);
|
|
169
|
+
}
|
|
170
|
+
.cms-editor .ProseMirror li.task-list-item[data-checked="true"] .task-list-item-checkbox::after {
|
|
171
|
+
content: "";
|
|
172
|
+
position: absolute;
|
|
173
|
+
left: 0.28em;
|
|
174
|
+
top: 0.08em;
|
|
175
|
+
width: 0.28em;
|
|
176
|
+
height: 0.55em;
|
|
177
|
+
border: solid #fff;
|
|
178
|
+
border-width: 0 2px 2px 0;
|
|
179
|
+
transform: rotate(45deg);
|
|
180
|
+
}
|
|
181
|
+
.cms-editor .ProseMirror li.task-list-item .task-list-item-content { display: inline; }
|
|
182
|
+
|
|
183
|
+
/* Tables */
|
|
184
|
+
.cms-editor .ProseMirror table {
|
|
185
|
+
border-collapse: collapse;
|
|
186
|
+
width: 100%;
|
|
187
|
+
margin: 0 0 0.75em;
|
|
188
|
+
overflow: hidden;
|
|
189
|
+
}
|
|
190
|
+
.cms-editor .ProseMirror th,
|
|
191
|
+
.cms-editor .ProseMirror td {
|
|
192
|
+
border: 1px solid var(--cms-ed-border);
|
|
193
|
+
padding: 6px 10px;
|
|
194
|
+
vertical-align: top;
|
|
195
|
+
text-align: left;
|
|
196
|
+
}
|
|
197
|
+
.cms-editor .ProseMirror th { background: var(--cms-ed-code-bg); font-weight: 600; }
|
|
198
|
+
.cms-editor .ProseMirror .selectedCell::after {
|
|
199
|
+
content: "";
|
|
200
|
+
position: absolute;
|
|
201
|
+
inset: 0;
|
|
202
|
+
background: var(--cms-ed-selection);
|
|
203
|
+
opacity: 0.3;
|
|
204
|
+
pointer-events: none;
|
|
205
|
+
}
|
|
206
|
+
.cms-editor .ProseMirror .column-resize-handle {
|
|
207
|
+
position: absolute;
|
|
208
|
+
right: -2px; top: 0; bottom: 0;
|
|
209
|
+
width: 4px;
|
|
210
|
+
background: var(--cms-ed-accent);
|
|
211
|
+
cursor: col-resize;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/* Placeholder (empty doc) */
|
|
215
|
+
.cms-editor .ProseMirror .cms-editor-placeholder::before {
|
|
216
|
+
content: attr(data-placeholder);
|
|
217
|
+
color: var(--cms-ed-muted);
|
|
218
|
+
pointer-events: none;
|
|
219
|
+
height: 0;
|
|
220
|
+
float: left;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/* Gap cursor (prosemirror-gapcursor) */
|
|
224
|
+
.cms-editor .ProseMirror-gapcursor {
|
|
225
|
+
display: none;
|
|
226
|
+
pointer-events: none;
|
|
227
|
+
position: absolute;
|
|
228
|
+
}
|
|
229
|
+
.cms-editor .ProseMirror-gapcursor:after {
|
|
230
|
+
content: "";
|
|
231
|
+
display: block;
|
|
232
|
+
position: absolute;
|
|
233
|
+
top: -2px;
|
|
234
|
+
width: 20px;
|
|
235
|
+
border-top: 1px solid var(--cms-ed-fg);
|
|
236
|
+
animation: cms-ed-blink 1.1s steps(2, start) infinite;
|
|
237
|
+
}
|
|
238
|
+
@keyframes cms-ed-blink { to { visibility: hidden; } }
|
|
239
|
+
.cms-editor .ProseMirror-focused .ProseMirror-gapcursor { display: block; }
|
|
240
|
+
`;
|
|
241
|
+
|
|
242
|
+
// Inject the stylesheet once per document (safe to call repeatedly).
|
|
243
|
+
export function ensureEditorStyles(doc: Document | undefined): void {
|
|
244
|
+
if (!doc || doc.getElementById(EDITOR_STYLE_ID)) return;
|
|
245
|
+
const style = doc.createElement("style");
|
|
246
|
+
style.id = EDITOR_STYLE_ID;
|
|
247
|
+
style.textContent = EDITOR_CSS;
|
|
248
|
+
doc.head.appendChild(style);
|
|
249
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { EditorFormat, MarkdownFlavor } from "./formats/registry";
|
|
2
|
+
|
|
3
|
+
export type { EditorFormat, MarkdownFlavor };
|
|
4
|
+
|
|
5
|
+
// Where the edited value comes from — a frontmatter field or the file body.
|
|
6
|
+
// Carried through for host symmetry; the editor itself treats both the same.
|
|
7
|
+
export type EditorSource = "field" | "body";
|
|
8
|
+
|
|
9
|
+
// Light/dark selection. Passed explicitly because React context (and the
|
|
10
|
+
// design-system theme) cannot cross the Expo DOM-component WebView bridge.
|
|
11
|
+
export type EditorTheme = "light" | "dark";
|
|
12
|
+
|
|
13
|
+
// Props for <MarkdownEditor>. Every prop is JSON-serializable and `onChange` is
|
|
14
|
+
// an async "native action" so the same component works both inline on the web
|
|
15
|
+
// and inside a WebView on native (Expo DOM components).
|
|
16
|
+
export interface MarkdownEditorProps {
|
|
17
|
+
// Markdown source (controlled).
|
|
18
|
+
value: string;
|
|
19
|
+
// Called with serialized markdown after edits (debounced). Async on native.
|
|
20
|
+
onChange?: (markdown: string) => void | Promise<void>;
|
|
21
|
+
format?: EditorFormat;
|
|
22
|
+
flavor?: MarkdownFlavor;
|
|
23
|
+
source?: EditorSource;
|
|
24
|
+
readOnly?: boolean;
|
|
25
|
+
placeholder?: string;
|
|
26
|
+
theme?: EditorTheme;
|
|
27
|
+
// Milliseconds to debounce serialization + onChange. Default 200.
|
|
28
|
+
debounceMs?: number;
|
|
29
|
+
// Show the expand button that toggles full-panel focus mode. Default true.
|
|
30
|
+
expandable?: boolean;
|
|
31
|
+
// Show the formatting toolbar at the top of the editor. Default true.
|
|
32
|
+
toolbar?: boolean;
|
|
33
|
+
// Expo DOM-component WebView props on native; ignored on web. Typed loosely so
|
|
34
|
+
// the package need not depend on expo/react-native types.
|
|
35
|
+
dom?: unknown;
|
|
36
|
+
// Live collaboration binding (web/desktop only — the Yjs objects are not
|
|
37
|
+
// serializable across the native DOM bridge). When present the editor mirrors
|
|
38
|
+
// a shared Y.XmlFragment instead of the local `value`, shows remote cursors,
|
|
39
|
+
// and uses per-client collaborative undo. `ydoc`/`awareness` are typed loosely
|
|
40
|
+
// (unknown) so this package needn't re-export yjs types to callers.
|
|
41
|
+
collab?: EditorCollab;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type EditorCollab = {
|
|
45
|
+
ydoc: unknown; // Y.Doc
|
|
46
|
+
awareness: unknown; // y-protocols Awareness
|
|
47
|
+
// Top-level Y.XmlFragment name for this body field, e.g. "body:content".
|
|
48
|
+
fragmentName: string;
|
|
49
|
+
// Called with true/false as the editor gains/loses focus, so the host can
|
|
50
|
+
// publish presence (which field this user is editing).
|
|
51
|
+
onFocusChange?: (focused: boolean) => void;
|
|
52
|
+
// Whether the provider has completed its initial sync with the server room,
|
|
53
|
+
// plus a change subscription. When present, the editor defers its seed-the-
|
|
54
|
+
// fragment decision until sync — an unsynced local doc always looks empty,
|
|
55
|
+
// which is not evidence the room is. Optional for hosts without the notion.
|
|
56
|
+
synced?: () => boolean;
|
|
57
|
+
subscribeSynced?: (cb: () => void) => () => void;
|
|
58
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { PluginRegistry } from "../registry";
|
|
2
|
+
import type { PluginDocument } from "../types";
|
|
3
|
+
|
|
4
|
+
const doc = (over: Partial<PluginDocument> = {}): PluginDocument => ({
|
|
5
|
+
id: "doc-1",
|
|
6
|
+
collection: "posts",
|
|
7
|
+
path: "content/blog/a.md",
|
|
8
|
+
fields: { slug: "a" },
|
|
9
|
+
...over,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe("addDocumentAction", () => {
|
|
13
|
+
let registry: PluginRegistry;
|
|
14
|
+
let errors: string[];
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
registry = new PluginRegistry();
|
|
18
|
+
errors = [];
|
|
19
|
+
jest.spyOn(console, "error").mockImplementation((m: unknown) => void errors.push(String(m)));
|
|
20
|
+
jest.spyOn(console, "warn").mockImplementation(() => {});
|
|
21
|
+
});
|
|
22
|
+
afterEach(() => jest.restoreAllMocks());
|
|
23
|
+
|
|
24
|
+
const action = (over: Record<string, unknown> = {}) => ({
|
|
25
|
+
id: "preview",
|
|
26
|
+
label: "Preview",
|
|
27
|
+
onSelect: () => {},
|
|
28
|
+
...over,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("registers an action and attributes it to the plugin", () => {
|
|
32
|
+
registry.api("@acme/preview").addDocumentAction(action());
|
|
33
|
+
const [a] = registry.documentActionsFor(doc());
|
|
34
|
+
expect(a).toMatchObject({ id: "preview", label: "Preview", pluginId: "@acme/preview" });
|
|
35
|
+
expect(a.key).toBe("@acme/preview:preview");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("keys actions by plugin, so two plugins may both add a 'preview'", () => {
|
|
39
|
+
registry.api("@acme/one").addDocumentAction(action());
|
|
40
|
+
registry.api("@acme/two").addDocumentAction(action());
|
|
41
|
+
expect(registry.documentActionsFor(doc()).map((a) => a.key)).toEqual([
|
|
42
|
+
"@acme/one:preview",
|
|
43
|
+
"@acme/two:preview",
|
|
44
|
+
]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("ignores a duplicate id within one plugin", () => {
|
|
48
|
+
const api = registry.api("@acme/preview");
|
|
49
|
+
api.addDocumentAction(action());
|
|
50
|
+
api.addDocumentAction(action({ label: "Second" }));
|
|
51
|
+
expect(registry.documentActionsFor(doc())).toHaveLength(1);
|
|
52
|
+
expect(errors.join()).toMatch(/duplicate id/);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it.each([
|
|
56
|
+
["a missing id", { id: "" }],
|
|
57
|
+
["an id with spaces", { id: "my action" }],
|
|
58
|
+
["a missing label", { label: "" }],
|
|
59
|
+
["a non-function onSelect", { onSelect: "nope" }],
|
|
60
|
+
])("rejects %s without throwing into the loader", (_label, bad) => {
|
|
61
|
+
registry.api("@acme/preview").addDocumentAction(action(bad));
|
|
62
|
+
expect(registry.documentActionsFor(doc())).toHaveLength(0);
|
|
63
|
+
expect(errors).toHaveLength(1);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("filtering", () => {
|
|
67
|
+
it("shows an unrestricted action on every collection", () => {
|
|
68
|
+
registry.api("p").addDocumentAction(action());
|
|
69
|
+
expect(registry.documentActionsFor(doc({ collection: "pages" }))).toHaveLength(1);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("restricts to the declared collections", () => {
|
|
73
|
+
// The behaviour the preview plugin depends on: collections with no URL
|
|
74
|
+
// template must not get a Preview button.
|
|
75
|
+
registry.api("p").addDocumentAction(action({ collections: ["posts"] }));
|
|
76
|
+
expect(registry.documentActionsFor(doc({ collection: "posts" }))).toHaveLength(1);
|
|
77
|
+
expect(registry.documentActionsFor(doc({ collection: "pages" }))).toHaveLength(0);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("honours the available predicate", () => {
|
|
81
|
+
registry.api("p").addDocumentAction(
|
|
82
|
+
action({ available: (d: PluginDocument) => Boolean(d.fields.slug) })
|
|
83
|
+
);
|
|
84
|
+
expect(registry.documentActionsFor(doc())).toHaveLength(1);
|
|
85
|
+
expect(registry.documentActionsFor(doc({ fields: {} }))).toHaveLength(0);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("hides an action whose available() throws, and keeps the others", () => {
|
|
89
|
+
const api = registry.api("p");
|
|
90
|
+
api.addDocumentAction(action({ id: "bad", available: () => { throw new Error("boom"); } }));
|
|
91
|
+
api.addDocumentAction(action({ id: "good" }));
|
|
92
|
+
expect(registry.documentActionsFor(doc()).map((a) => a.id)).toEqual(["good"]);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("bumps the version so hosts re-render", () => {
|
|
97
|
+
const before = registry.getVersion();
|
|
98
|
+
registry.api("p").addDocumentAction(action());
|
|
99
|
+
expect(registry.getVersion()).toBeGreaterThan(before);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("copies the collections array so a plugin cannot mutate it later", () => {
|
|
103
|
+
const collections = ["posts"];
|
|
104
|
+
registry.api("p").addDocumentAction(action({ collections }));
|
|
105
|
+
collections.push("pages");
|
|
106
|
+
expect(registry.documentActionsFor(doc({ collection: "pages" }))).toHaveLength(0);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { loadPlugins } from "../loader";
|
|
2
|
+
import { PluginRegistry } from "../registry";
|
|
3
|
+
import type { CmsPluginApi } from "../types";
|
|
4
|
+
|
|
5
|
+
let errorSpy: jest.SpyInstance;
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
errorSpy = jest.spyOn(console, "error").mockImplementation(() => {});
|
|
8
|
+
});
|
|
9
|
+
afterEach(() => errorSpy.mockRestore());
|
|
10
|
+
|
|
11
|
+
test("loads plugins, runs setup with a scoped api and options", async () => {
|
|
12
|
+
const reg = new PluginRegistry();
|
|
13
|
+
const seen: Array<{ id: string; options: unknown }> = [];
|
|
14
|
+
await loadPlugins(
|
|
15
|
+
[
|
|
16
|
+
{
|
|
17
|
+
id: "@acme/a",
|
|
18
|
+
options: { palette: "brand" },
|
|
19
|
+
load: async () => ({
|
|
20
|
+
default: (cms: CmsPluginApi, options: unknown) => {
|
|
21
|
+
seen.push({ id: cms.id, options });
|
|
22
|
+
cms.addSidebarLink({ label: "A", to: "a" });
|
|
23
|
+
},
|
|
24
|
+
}),
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
reg
|
|
28
|
+
);
|
|
29
|
+
expect(seen).toEqual([{ id: "@acme/a", options: { palette: "brand" } }]);
|
|
30
|
+
expect(reg.status("@acme/a")).toBe("ready");
|
|
31
|
+
expect(reg.sidebar()).toHaveLength(1);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("a plugin that fails to import is failed without affecting others", async () => {
|
|
35
|
+
const reg = new PluginRegistry();
|
|
36
|
+
await loadPlugins(
|
|
37
|
+
[
|
|
38
|
+
{ id: "bad", load: async () => { throw new Error("network"); } },
|
|
39
|
+
{ id: "good", load: async () => ({ default: () => {} }) },
|
|
40
|
+
],
|
|
41
|
+
reg
|
|
42
|
+
);
|
|
43
|
+
expect(reg.status("bad")).toBe("failed");
|
|
44
|
+
expect(reg.plugins().find((p) => p.id === "bad")?.error).toBe("network");
|
|
45
|
+
expect(reg.status("good")).toBe("ready");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("a module without a default-exported function fails", async () => {
|
|
49
|
+
const reg = new PluginRegistry();
|
|
50
|
+
await loadPlugins([{ id: "no-default", load: async () => ({}) }], reg);
|
|
51
|
+
expect(reg.status("no-default")).toBe("failed");
|
|
52
|
+
expect(reg.plugins()[0].error).toMatch(/no default-exported setup function/);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("a setup function that throws (sync or async) fails the plugin", async () => {
|
|
56
|
+
const reg = new PluginRegistry();
|
|
57
|
+
await loadPlugins(
|
|
58
|
+
[
|
|
59
|
+
{ id: "sync-throw", load: async () => ({ default: () => { throw new Error("sync"); } }) },
|
|
60
|
+
{ id: "async-throw", load: async () => ({ default: async () => { throw new Error("async"); } }) },
|
|
61
|
+
],
|
|
62
|
+
reg
|
|
63
|
+
);
|
|
64
|
+
expect(reg.status("sync-throw")).toBe("failed");
|
|
65
|
+
expect(reg.status("async-throw")).toBe("failed");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("duplicate ids load once", async () => {
|
|
69
|
+
const reg = new PluginRegistry();
|
|
70
|
+
let calls = 0;
|
|
71
|
+
const entry = { id: "dup", load: async () => ({ default: () => { calls++; } }) };
|
|
72
|
+
await loadPlugins([entry, entry], reg);
|
|
73
|
+
expect(calls).toBe(1);
|
|
74
|
+
expect(reg.plugins()).toHaveLength(1);
|
|
75
|
+
});
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { PluginRegistry } from "../registry";
|
|
2
|
+
|
|
3
|
+
// Registrations that are rejected report via console.error; keep tests quiet
|
|
4
|
+
// and let them assert on the spy.
|
|
5
|
+
let errorSpy: jest.SpyInstance;
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
errorSpy = jest.spyOn(console, "error").mockImplementation(() => {});
|
|
8
|
+
});
|
|
9
|
+
afterEach(() => errorSpy.mockRestore());
|
|
10
|
+
|
|
11
|
+
const loader = async () => ({ default: () => null });
|
|
12
|
+
|
|
13
|
+
test("registrations are scoped to the plugin that made them", () => {
|
|
14
|
+
const reg = new PluginRegistry();
|
|
15
|
+
reg.api("a").addRoute({ path: "dash", component: loader });
|
|
16
|
+
reg.api("b").addRoute({ path: "dash", component: loader });
|
|
17
|
+
expect(reg.routesFor("a")).toHaveLength(1);
|
|
18
|
+
expect(reg.routesFor("b")).toHaveLength(1);
|
|
19
|
+
expect(reg.routesFor("a")[0].pluginId).toBe("a");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("duplicate route paths within one plugin are ignored", () => {
|
|
23
|
+
const reg = new PluginRegistry();
|
|
24
|
+
const api = reg.api("a");
|
|
25
|
+
api.addRoute({ path: "/dash/", component: loader });
|
|
26
|
+
api.addRoute({ path: "dash", component: loader }); // same after normalization
|
|
27
|
+
expect(reg.routesFor("a")).toHaveLength(1);
|
|
28
|
+
expect(errorSpy).toHaveBeenCalled();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("field components register under plugin: and first registration wins", () => {
|
|
32
|
+
const reg = new PluginRegistry();
|
|
33
|
+
const first = async () => ({ default: () => null });
|
|
34
|
+
reg.api("a").registerFieldComponent({ name: "color", types: ["string"], component: first });
|
|
35
|
+
reg.api("b").registerFieldComponent({ name: "color", types: ["string"], component: loader });
|
|
36
|
+
const rec = reg.fieldComponent("plugin:color");
|
|
37
|
+
expect(rec?.pluginId).toBe("a");
|
|
38
|
+
expect(rec?.component).toBe(first);
|
|
39
|
+
expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('already registered by plugin a'));
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("field component names outside the allowed shape are rejected", () => {
|
|
43
|
+
const reg = new PluginRegistry();
|
|
44
|
+
reg.api("a").registerFieldComponent({ name: "has space", types: ["string"], component: loader });
|
|
45
|
+
reg.api("a").registerFieldComponent({ name: "", types: ["string"], component: loader });
|
|
46
|
+
expect(reg.fieldComponent("plugin:has space")).toBeUndefined();
|
|
47
|
+
expect(errorSpy).toHaveBeenCalledTimes(2);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("user menu links require exactly one of to/onSelect", () => {
|
|
51
|
+
const reg = new PluginRegistry();
|
|
52
|
+
const api = reg.api("a");
|
|
53
|
+
api.addUserMenuLink({ label: "Both", to: "x", onSelect: () => {} });
|
|
54
|
+
api.addUserMenuLink({ label: "Neither" });
|
|
55
|
+
api.addUserMenuLink({ label: "Good", to: "settings" });
|
|
56
|
+
expect(reg.userMenu()).toHaveLength(1);
|
|
57
|
+
expect(reg.userMenu()[0].label).toBe("Good");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("sidebar links default to the Plugins section and keep plugin attribution", () => {
|
|
61
|
+
const reg = new PluginRegistry();
|
|
62
|
+
reg.api("a").addSidebarLink({ label: "Analytics", to: "/dash" });
|
|
63
|
+
reg.api("b").addSidebarLink({ label: "SEO", to: "audit", section: "Tools" });
|
|
64
|
+
expect(reg.sidebar()).toEqual([
|
|
65
|
+
expect.objectContaining({ pluginId: "a", section: "Plugins", to: "dash" }),
|
|
66
|
+
expect.objectContaining({ pluginId: "b", section: "Tools", to: "audit" }),
|
|
67
|
+
]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("matchRoute", () => {
|
|
71
|
+
const reg = new PluginRegistry();
|
|
72
|
+
const api = reg.api("a");
|
|
73
|
+
api.addRoute({ path: "reports/:kind", component: loader });
|
|
74
|
+
api.addRoute({ path: "reports/summary", component: loader });
|
|
75
|
+
api.addRoute({ path: "dash", component: loader });
|
|
76
|
+
|
|
77
|
+
test("captures :params", () => {
|
|
78
|
+
const m = reg.matchRoute("a", "reports/weekly");
|
|
79
|
+
expect(m?.record.path).toBe("reports/:kind");
|
|
80
|
+
expect(m?.params).toEqual({ kind: "weekly" });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("prefers the route with more static segments", () => {
|
|
84
|
+
expect(reg.matchRoute("a", "reports/summary")?.record.path).toBe("reports/summary");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("does not match across plugins or lengths", () => {
|
|
88
|
+
expect(reg.matchRoute("b", "dash")).toBeNull();
|
|
89
|
+
expect(reg.matchRoute("a", "dash/extra")).toBeNull();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("load state: pending → ready/failed, settled(), plugins()", () => {
|
|
94
|
+
const reg = new PluginRegistry();
|
|
95
|
+
reg.markPending("a");
|
|
96
|
+
reg.markPending("b");
|
|
97
|
+
expect(reg.settled()).toBe(false);
|
|
98
|
+
reg.markReady("a");
|
|
99
|
+
reg.markFailed("b", new Error("boom"));
|
|
100
|
+
expect(reg.settled()).toBe(true);
|
|
101
|
+
expect(reg.plugins()).toEqual([
|
|
102
|
+
{ id: "a", status: "ready", error: undefined },
|
|
103
|
+
{ id: "b", status: "failed", error: "boom" },
|
|
104
|
+
]);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("subscribers are notified and version is monotonic", () => {
|
|
108
|
+
const reg = new PluginRegistry();
|
|
109
|
+
const seen: number[] = [];
|
|
110
|
+
const unsub = reg.subscribe(() => seen.push(reg.getVersion()));
|
|
111
|
+
reg.api("a").addSidebarLink({ label: "X", to: "x" });
|
|
112
|
+
reg.markPending("a");
|
|
113
|
+
unsub();
|
|
114
|
+
reg.markReady("a");
|
|
115
|
+
expect(seen).toEqual([1, 2]);
|
|
116
|
+
expect(reg.getVersion()).toBe(3);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("host capabilities", () => {
|
|
120
|
+
it("passes a preview-token request to the host, scoped to the repository", async () => {
|
|
121
|
+
const asked: string[] = [];
|
|
122
|
+
const registry = new PluginRegistry({
|
|
123
|
+
getPreviewToken: async (repositoryId) => {
|
|
124
|
+
asked.push(repositoryId);
|
|
125
|
+
return "preview-token";
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const token = await registry.api("@acme/preview").getPreviewToken("repo-1");
|
|
130
|
+
|
|
131
|
+
expect(token).toBe("preview-token");
|
|
132
|
+
expect(asked).toEqual(["repo-1"]);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("tells a plugin plainly when the host cannot mint one", async () => {
|
|
136
|
+
// Otherwise this surfaces as the preview service refusing a session, which
|
|
137
|
+
// sends whoever is debugging it to the wrong end entirely.
|
|
138
|
+
const registry = new PluginRegistry();
|
|
139
|
+
await expect(registry.api("@acme/preview").getPreviewToken("repo-1")).rejects.toThrow(
|
|
140
|
+
/does not provide preview tokens/,
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("refuses an empty repository id rather than minting something unscoped", async () => {
|
|
145
|
+
const registry = new PluginRegistry({ getPreviewToken: async () => "t" });
|
|
146
|
+
await expect(registry.api("@acme/preview").getPreviewToken("")).rejects.toThrow(/repositoryId is required/);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
CmsPluginApi,
|
|
3
|
+
PluginComponentLoader,
|
|
4
|
+
PluginDocument,
|
|
5
|
+
PluginDocumentAction,
|
|
6
|
+
PluginDocumentActionContext,
|
|
7
|
+
PluginFieldComponent,
|
|
8
|
+
PluginFieldProps,
|
|
9
|
+
PluginFieldType,
|
|
10
|
+
PluginLoadEntry,
|
|
11
|
+
PluginRoute,
|
|
12
|
+
PluginScreenProps,
|
|
13
|
+
PluginSetup,
|
|
14
|
+
PluginSidebarLink,
|
|
15
|
+
PluginStatus,
|
|
16
|
+
PluginTabContext,
|
|
17
|
+
PluginTabSpec,
|
|
18
|
+
PluginUserMenuLink,
|
|
19
|
+
} from "./types";
|
|
20
|
+
export {
|
|
21
|
+
PLUGIN_COMPONENT_PREFIX,
|
|
22
|
+
PluginRegistry,
|
|
23
|
+
type PluginDocumentActionRecord,
|
|
24
|
+
type PluginFieldComponentRecord,
|
|
25
|
+
type PluginInfo,
|
|
26
|
+
type PluginRouteRecord,
|
|
27
|
+
type PluginSidebarLinkRecord,
|
|
28
|
+
type PluginUserMenuLinkRecord,
|
|
29
|
+
type RouteMatch,
|
|
30
|
+
} from "./registry";
|
|
31
|
+
export { loadPlugins } from "./loader";
|
|
32
|
+
export { pluginNavKey, parsePluginNavKey } from "./navkeys";
|
|
33
|
+
export { PluginScreenHost, createPluginRenderField, getLazyComponent, usePluginRegistry } from "./react";
|