@cloudcannon/editable-regions 0.0.10 → 0.0.11

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.
@@ -93,7 +93,16 @@ export default class EditableArrayItemControls extends EditableComponentControls
93
93
 
94
94
  this.dragHandle = document.createElement("button");
95
95
  this.dragHandle.type = "button";
96
+
97
+ if (this.usePopoverAPI) {
98
+ //@ts-expect-error: Typescript doesn't yet support the popover API
99
+ this.dragHandle.style.anchorName = this.anchorName;
100
+ }
101
+
96
102
  this.dragHandle.onclick = (e) => {
103
+ if (this.hasAttribute("open")) {
104
+ return;
105
+ }
97
106
  e.stopPropagation();
98
107
  e.preventDefault();
99
108
  this.toggleContextMenu();
@@ -6,13 +6,28 @@ export default class EditableComponentControls extends HTMLElement {
6
6
  protected buttonRow?: HTMLDivElement;
7
7
 
8
8
  private editButton?: HTMLButtonElement;
9
+ protected usePopoverAPI = false;
10
+ protected contextMenuId =
11
+ `context-menu-popover-${Math.random().toString(36).substring(2, 11)}`;
12
+ protected anchorName = `--${this.contextMenuId}`;
13
+ protected hostAnchorName =
14
+ `--cc-editable-host-${Math.random().toString(36).substring(2, 11)}`;
9
15
 
10
16
  toggleContextMenu() {
11
- if (this.contextMenu?.classList.contains("open")) {
12
- this.contextMenu?.classList.remove("open");
17
+ if (!this.contextMenu || this.contextMenu.childElementCount === 0) {
18
+ return;
19
+ }
20
+
21
+ if (this.usePopoverAPI) {
22
+ this.contextMenu.togglePopover();
23
+ return;
24
+ }
25
+
26
+ if (this.contextMenu.classList.contains("open")) {
27
+ this.contextMenu.classList.remove("open");
13
28
  this.removeAttribute("open");
14
- } else if (this.contextMenu && this.contextMenu.childElementCount > 0) {
15
- this.contextMenu?.classList.add("open");
29
+ } else {
30
+ this.contextMenu.classList.add("open");
16
31
  this.setAttribute("open", "true");
17
32
  }
18
33
  }
@@ -28,6 +43,13 @@ export default class EditableComponentControls extends HTMLElement {
28
43
 
29
44
  this.contextMenu = document.createElement("ul");
30
45
  this.contextMenu.classList.add("context-menu");
46
+
47
+ if (this.usePopoverAPI) {
48
+ this.contextMenu.setAttribute("popover", "auto");
49
+ //@ts-expect-error: Typescript doesn't yet support the popover API
50
+ this.contextMenu.style.positionAnchor = this.anchorName;
51
+ }
52
+
31
53
  shadow.appendChild(this.contextMenu);
32
54
 
33
55
  this.editButton = document.createElement("button");
@@ -40,17 +62,32 @@ export default class EditableComponentControls extends HTMLElement {
40
62
 
41
63
  this.onclick = (e) => {
42
64
  e.preventDefault();
43
- this.contextMenu?.classList.remove("open");
44
65
  this.removeAttribute("open");
45
- };
46
66
 
47
- this.onblur = (e) => {
48
- if ((e.relatedTarget as HTMLElement)?.tagName === "A") {
49
- return;
67
+ if (this.usePopoverAPI) {
68
+ this.contextMenu?.hidePopover();
69
+ } else {
70
+ this.contextMenu?.classList.remove("open");
50
71
  }
51
- this.contextMenu?.classList.remove("open");
52
- this.removeAttribute("open");
53
72
  };
73
+
74
+ if (this.usePopoverAPI && this.contextMenu) {
75
+ this.contextMenu.ontoggle = (e: ToggleEvent) => {
76
+ if (e.newState === "open") {
77
+ this.setAttribute("open", "true");
78
+ } else {
79
+ this.removeAttribute("open");
80
+ }
81
+ };
82
+ } else {
83
+ this.onblur = (e) => {
84
+ if ((e.relatedTarget as HTMLElement)?.tagName === "A") {
85
+ return;
86
+ }
87
+ this.removeAttribute("open");
88
+ this.contextMenu?.classList.remove("open");
89
+ };
90
+ }
54
91
  }
55
92
 
56
93
  connectedCallback() {
@@ -58,8 +95,27 @@ export default class EditableComponentControls extends HTMLElement {
58
95
  return;
59
96
  }
60
97
 
98
+ this.usePopoverAPI =
99
+ "popover" in HTMLElement.prototype && CSS.supports("anchor-name", "--a");
100
+
61
101
  this.shadow = this.attachShadow({ mode: "open" });
62
102
  this.render(this.shadow);
103
+
104
+ if (this.usePopoverAPI && this.parentElement) {
105
+ const existing = getComputedStyle(this.parentElement)
106
+ .getPropertyValue("anchor-name")
107
+ .trim();
108
+
109
+ if (existing && existing !== "none") {
110
+ this.hostAnchorName = existing.split(",")[0].trim();
111
+ } else {
112
+ // @ts-expect-error
113
+ this.parentElement.style.anchorName = this.hostAnchorName;
114
+ }
115
+
116
+ // @ts-expect-error
117
+ this.style.positionAnchor = this.hostAnchorName;
118
+ }
63
119
  }
64
120
  }
65
121
 
package/helpers/checks.ts CHANGED
@@ -46,7 +46,11 @@ export const isEditableElement = (el: unknown): boolean => {
46
46
  );
47
47
  };
48
48
 
49
- export const areEqualEditables = (a: HTMLElement, b: HTMLElement) => {
49
+ export const areEqualEditables = (a: Element, b: Element) => {
50
+ if (!(a instanceof HTMLElement) || !(b instanceof HTMLElement)) {
51
+ return false;
52
+ }
53
+
50
54
  if (getEditableType(a) !== getEditableType(b)) {
51
55
  return false;
52
56
  }
@@ -55,7 +55,6 @@ export const apiLoadedPromise = new Promise((resolve) => {
55
55
  export const addEditableComponentRenderer = (key, renderer) => {
56
56
  extendedWindow.cc_components = extendedWindow.cc_components || {};
57
57
  extendedWindow.cc_components[key] = renderer;
58
- document.dispatchEvent(new CustomEvent(`editable-regions:registered-${key}`));
59
58
  };
60
59
 
61
60
  /**
@@ -67,7 +66,6 @@ export const addEditableComponentRenderer = (key, renderer) => {
67
66
  export const addEditableSnippetRenderer = (key, renderer) => {
68
67
  extendedWindow.cc_snippets = extendedWindow.cc_snippets || {};
69
68
  extendedWindow.cc_snippets[key] = renderer;
70
- document.dispatchEvent(new CustomEvent(`editable-regions:registered-${key}`));
71
69
  };
72
70
 
73
71
  /**
@@ -4,6 +4,33 @@ import { fileURLToPath } from "node:url";
4
4
  /** @type{string[]} */
5
5
  const SUPPORTED_VIRTUAL_MODULES = ["assets", "content"];
6
6
 
7
+ /**
8
+ * @param {*} original
9
+ * @returns
10
+ */
11
+ function wrapTransform(original) {
12
+ /**
13
+ * @this {*}
14
+ * @param {*} source
15
+ * @param {*} id
16
+ * @param {*} options
17
+ */
18
+ return function (source, id, options) {
19
+ if (this.environment?.name === "client") {
20
+ const proxy = new Proxy(this, {
21
+ get(target, prop) {
22
+ if (prop === "environment") {
23
+ return { ...target.environment, name: "ssr" };
24
+ }
25
+ return Reflect.get(target, prop);
26
+ },
27
+ });
28
+ return original.call(proxy, source, id, { ...options, ssr: true });
29
+ }
30
+ return original.call(this, source, id, options);
31
+ };
32
+ }
33
+
7
34
  /**
8
35
  * @return {import("astro").AstroIntegration}
9
36
  */
@@ -14,70 +41,96 @@ export default () => {
14
41
  "astro:config:setup": ({ updateConfig }) => {
15
42
  updateConfig({
16
43
  vite: {
17
- define: {
18
- ENV_CLIENT: false,
19
- },
20
- },
21
- });
22
- },
23
- "astro:build:setup": async ({ target, vite }) => {
24
- if (target === "client") {
25
- vite.plugins ??= [];
26
- vite.define ??= {};
27
- vite.define.ENV_CLIENT = true;
44
+ plugins: [
45
+ {
46
+ enforce: "pre",
47
+ name: "vite-plugin-editable-regions",
48
+ applyToEnvironment(environment) {
49
+ return environment.name === "client";
50
+ },
51
+ config(config) {
52
+ config.environments ??= {};
28
53
 
29
- const flatPlugins = vite.plugins?.flat(10);
30
- const astroBuildPlugin = flatPlugins?.find((obj) => {
31
- return (
32
- obj &&
33
- typeof obj === "object" &&
34
- "name" in obj &&
35
- obj.name === "astro:build"
36
- );
37
- });
54
+ config.environments.ssr ??= {};
55
+ config.environments.ssr.define ??= {};
56
+ config.environments.ssr.define.ENV_CLIENT = false;
38
57
 
39
- if (
40
- astroBuildPlugin &&
41
- "transform" in astroBuildPlugin &&
42
- typeof astroBuildPlugin.transform === "function"
43
- ) {
44
- const original = astroBuildPlugin.transform;
45
- astroBuildPlugin.transform = function (source, id, options) {
46
- return original.bind(this)(source, id, { ...options, ssr: true });
47
- };
48
- }
58
+ config.environments.astro ??= {};
59
+ config.environments.astro.define ??= {};
60
+ config.environments.astro.define.ENV_CLIENT = false;
49
61
 
50
- vite.plugins.unshift({
51
- name: "vite-plugin-editable-regions",
52
- enforce: "pre",
62
+ config.environments.prerender ??= {};
63
+ config.environments.prerender.define ??= {};
64
+ config.environments.prerender.define.ENV_CLIENT = false;
53
65
 
54
- resolveId(id) {
55
- if (id.startsWith("astro:")) {
56
- const type = id
57
- .replace("astro:", "")
58
- .replace("/client", "")
59
- .replace("/server", "");
66
+ config.environments.client ??= {};
67
+ config.environments.client.define ??= {};
68
+ config.environments.client.define.ENV_CLIENT = true;
69
+ },
70
+ configResolved(config) {
71
+ const flatPlugins = config.plugins?.flat(10);
72
+ const astroBuildPlugin = flatPlugins?.find((obj) => {
73
+ return (
74
+ obj &&
75
+ typeof obj === "object" &&
76
+ "name" in obj &&
77
+ obj.name === "astro:build"
78
+ );
79
+ });
60
80
 
61
- let dir = "";
62
- if (typeof __dirname !== "undefined") {
63
- dir = __dirname;
64
- } else {
65
- dir = dirname(fileURLToPath(import.meta.url));
66
- }
81
+ if (
82
+ astroBuildPlugin &&
83
+ "transform" in astroBuildPlugin &&
84
+ typeof astroBuildPlugin.transform === "object" &&
85
+ astroBuildPlugin.transform &&
86
+ "handler" in astroBuildPlugin.transform &&
87
+ typeof astroBuildPlugin.transform.handler === "function"
88
+ ) {
89
+ astroBuildPlugin.transform.handler = wrapTransform(
90
+ astroBuildPlugin.transform.handler,
91
+ );
92
+ } else if (
93
+ astroBuildPlugin &&
94
+ "transform" in astroBuildPlugin &&
95
+ typeof astroBuildPlugin.transform === "function"
96
+ ) {
97
+ astroBuildPlugin.transform = wrapTransform(
98
+ astroBuildPlugin.transform,
99
+ );
100
+ }
101
+ },
102
+ resolveId: {
103
+ order: "pre",
104
+ handler(id) {
105
+ if (id.startsWith("astro:")) {
106
+ const type = id
107
+ .replace("astro:", "")
108
+ .replace("/client", "")
109
+ .replace("/server", "");
67
110
 
68
- if (type === "env" && id.endsWith("/server")) {
69
- return join(dir, "modules", "secrets.js");
70
- }
111
+ let dir = "";
112
+ if (typeof __dirname !== "undefined") {
113
+ dir = __dirname;
114
+ } else {
115
+ dir = dirname(fileURLToPath(import.meta.url));
116
+ }
71
117
 
72
- if (!SUPPORTED_VIRTUAL_MODULES.includes(type)) {
73
- return;
74
- }
118
+ if (type === "env" && id.endsWith("/server")) {
119
+ return join(dir, "modules", "secrets.js");
120
+ }
75
121
 
76
- return join(dir, "modules", `${type}.js`);
77
- }
78
- },
79
- });
80
- }
122
+ if (!SUPPORTED_VIRTUAL_MODULES.includes(type)) {
123
+ return;
124
+ }
125
+
126
+ return join(dir, "modules", `${type}.js`);
127
+ }
128
+ },
129
+ },
130
+ },
131
+ ],
132
+ },
133
+ });
81
134
  },
82
135
  },
83
136
  };
@@ -131,31 +131,44 @@ export const registerAstroComponent = (key, component) => {
131
131
  props,
132
132
  resolve: () => "editable-region-placeholder",
133
133
  /**
134
- * @param {*} astroGlobal
135
- * @param {*} props
136
- * @param {*} slots
134
+ * @param {*} args
137
135
  */
138
- createAstro(astroGlobal, props, slots) {
136
+ createAstro(...args) {
137
+ if (args.length < 2 || args.length > 3) {
138
+ console.warn(
139
+ `[CloudCannon] createAstro called with unexpected number of arguments (${args.length})`,
140
+ );
141
+ }
142
+
143
+ let astroGlobal = SSRResult;
144
+ let componentProps, componentSlots;
145
+
146
+ if (args.length === 2) {
147
+ [componentProps, componentSlots] = args;
148
+ } else {
149
+ [astroGlobal, componentProps, componentSlots] = args;
150
+ }
151
+
139
152
  const astroSlots = {
140
153
  /**
141
154
  * @param {string} name
142
155
  * @returns boolean
143
156
  */
144
157
  has: (name) => {
145
- if (!slots) return false;
146
- return Boolean(slots[name]);
158
+ if (!componentSlots) return false;
159
+ return Boolean(componentSlots[name]);
147
160
  },
148
161
  /**
149
162
  * @param {string} name
150
163
  * @returns string
151
164
  */
152
165
  render: (name) => {
153
- return renderSlotToString(SSRResult, slots[name]);
166
+ return renderSlotToString(SSRResult, componentSlots[name]);
154
167
  },
155
168
  };
156
169
  return {
157
170
  __proto__: astroGlobal,
158
- props,
171
+ props: componentProps,
159
172
  slots: astroSlots,
160
173
  request: new Request(window.location.href),
161
174
  };
@@ -171,100 +171,112 @@ export default class EditableComponent extends Editable {
171
171
  targetNode?: ChildNode | null,
172
172
  renderNode?: ChildNode | null,
173
173
  ): void {
174
- let targetChild: ChildNode | null | undefined =
174
+ let nextTargetChild: ChildNode | null | undefined =
175
175
  targetNode?.firstChild ?? undefined;
176
- let renderChild: ChildNode | null | undefined =
176
+ let nextRenderChild: ChildNode | null | undefined =
177
177
  renderNode?.firstChild ?? undefined;
178
- while (renderChild || targetChild) {
179
- const nextTargetChild: ChildNode | null | undefined =
180
- targetChild?.nextSibling ?? undefined;
181
- const nextRenderChild: ChildNode | null | undefined =
182
- renderChild?.nextSibling ?? undefined;
183
178
 
184
- if (
185
- targetChild instanceof Element &&
186
- renderChild &&
187
- !(renderChild instanceof Element)
188
- ) {
179
+ while (nextRenderChild || nextTargetChild) {
180
+ const targetChild: ChildNode | null | undefined = nextTargetChild;
181
+ const renderChild: ChildNode | null | undefined = nextRenderChild;
182
+
183
+ nextTargetChild = targetChild?.nextSibling ?? undefined;
184
+ nextRenderChild = renderChild?.nextSibling ?? undefined;
185
+
186
+ // There is an existing node in the DOM but no rendered node in it's place
187
+ if (!renderChild && targetChild) {
188
+ targetNode?.removeChild(targetChild);
189
+ continue;
190
+ }
191
+
192
+ // There is a node to be rendered, but no existing node in the DOM in it's place
193
+ if (renderChild && !targetChild) {
194
+ targetNode?.appendChild(renderChild);
195
+ continue;
196
+ }
197
+
198
+ if (!targetChild || !renderChild) {
199
+ throw new Error("Illegal rendering state, both children should exist ");
200
+ }
201
+
202
+ // The existing child is an element and the rendered child is a node (i.e. some text)
203
+ if (targetChild instanceof Element && !(renderChild instanceof Element)) {
204
+ // Render the (text) node before the element and skip to the next rendered child
189
205
  targetChild.before(renderChild);
190
- renderChild = nextRenderChild;
206
+ nextTargetChild = targetChild;
191
207
  continue;
192
208
  }
193
209
 
194
- if (
195
- renderChild instanceof Element &&
196
- targetChild &&
197
- !(targetChild instanceof Element)
198
- ) {
210
+ // The existing child is a node (i.e. some text) and the rendered child is an element
211
+ if (renderChild instanceof Element && !(targetChild instanceof Element)) {
212
+ // Delete the (text) node and skip to the next existing child
199
213
  targetChild.remove();
200
- targetChild = nextTargetChild;
214
+ nextRenderChild = renderChild;
201
215
  continue;
202
216
  }
203
217
 
218
+ // Both existing and rendered children are nodes (i.e. some text)
204
219
  if (
205
- renderChild &&
206
- targetChild &&
207
220
  !(renderChild instanceof Element) &&
208
221
  !(targetChild instanceof Element)
209
222
  ) {
223
+ // Render the offscreen node's content into the existing node.
210
224
  targetChild.nodeValue = renderChild.nodeValue;
211
- } else if (
212
- targetChild instanceof HTMLElement &&
213
- renderChild instanceof HTMLElement &&
214
- isEditableElement(renderChild) &&
215
- isEditableElement(targetChild)
225
+ continue;
226
+ }
227
+
228
+ // Both children are elements but are different types
229
+ if (renderChild.nodeName !== targetChild.nodeName) {
230
+ targetChild.replaceWith(renderChild);
231
+ continue;
232
+ }
233
+
234
+ // Both existing and rendered children are the same kind of element, and neither is editable
235
+ if (!isEditableElement(renderChild) && !isEditableElement(targetChild)) {
236
+ // Update the existing element to match the rendered element and recurse their subtrees
237
+ this.updateNode(targetChild, renderChild);
238
+ this.updateTree(targetChild, renderChild);
239
+ continue;
240
+ }
241
+
242
+ if (
243
+ !(renderChild instanceof HTMLElement) ||
244
+ !(targetChild instanceof HTMLElement)
216
245
  ) {
217
- if (!areEqualEditables(renderChild, targetChild)) {
218
- targetChild.replaceWith(renderChild);
219
- } else if (isEditableText(renderChild) && isEditableText(targetChild)) {
220
- if (
221
- hasEditableText(targetChild) &&
222
- !targetChild.editable.focused &&
223
- !targetChild?.isEqualNode(renderChild) &&
224
- hasEditable(renderChild)
225
- ) {
226
- targetChild.replaceWith(renderChild);
227
- for (let i = 0; i < this.listeners.length; i++) {
228
- const listener = this.listeners[i];
229
- if (listener.editable.element === targetChild) {
230
- listener.editable.element = renderChild;
231
- renderChild.editable.pushValue(
232
- this.value,
233
- this.specialProps,
234
- listener,
235
- {
236
- ...this.contexts,
237
- __base_context: this.contextBase ?? {},
238
- },
239
- renderChild,
240
- );
241
- }
242
- }
243
- } else if (hasEditable(targetChild)) {
244
- this.updateEditable(renderChild, targetChild);
245
- }
246
- } else if (hasEditable(targetChild)) {
247
- this.updateEditable(renderChild, targetChild);
248
- }
249
- } else if (renderChild && targetChild) {
250
- if (
251
- renderChild.nodeName !== targetChild.nodeName ||
252
- isEditableElement(renderChild) ||
253
- isEditableElement(targetChild)
254
- ) {
255
- targetChild.replaceWith(renderChild);
256
- } else {
257
- this.updateNode(targetChild, renderChild);
258
- this.updateTree(targetChild, renderChild);
259
- }
260
- } else if (renderChild) {
261
- targetNode?.appendChild(renderChild);
262
- } else if (targetChild) {
263
- targetNode?.removeChild(targetChild);
246
+ throw new Error("Illegal state, both children should be elements");
247
+ }
248
+
249
+ // The existing and rendered child either aren't both editable or are different kinds of editables
250
+ if (!areEqualEditables(renderChild, targetChild)) {
251
+ targetChild.replaceWith(renderChild);
252
+ continue;
253
+ }
254
+
255
+ if (!isEditableElement(renderChild) || !isEditableElement(targetChild)) {
256
+ throw new Error("Illegal state, both children should be editable");
257
+ }
258
+
259
+ // Both elements are editable but the existing element hasn't hydrated
260
+ if (!hasEditable(targetChild)) {
261
+ targetChild.replaceWith(renderChild);
262
+ continue;
263
+ }
264
+
265
+ // The existing and rendered child are the same kind of non-text editable.
266
+ if (!isEditableText(targetChild)) {
267
+ this.updateEditable(renderChild, targetChild);
268
+ continue;
264
269
  }
265
270
 
266
- targetChild = nextTargetChild;
267
- renderChild = nextRenderChild;
271
+ // The existing element is currently focused or is already the same as the rendered element
272
+ if (
273
+ (hasEditableText(targetChild) && targetChild.editable.focused) ||
274
+ targetChild?.isEqualNode(renderChild)
275
+ ) {
276
+ this.updateEditable(renderChild, targetChild);
277
+ } else {
278
+ targetChild.replaceWith(renderChild);
279
+ }
268
280
  }
269
281
  }
270
282
 
@@ -351,23 +363,6 @@ export default class EditableComponent extends Editable {
351
363
  );
352
364
  }
353
365
 
354
- setupListeners(): void {
355
- super.setupListeners();
356
- const key = this.element.dataset.component;
357
- if (!key) {
358
- return;
359
- }
360
-
361
- const component = this.getComponents()?.[key];
362
- if (!component) {
363
- document.addEventListener(
364
- `editable-regions:registered-${key}`,
365
- () => this.update(),
366
- { once: true },
367
- );
368
- }
369
- }
370
-
371
366
  mount(): void {
372
367
  if (!this.controlsElement) {
373
368
  let editPath: string | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudcannon/editable-regions",
3
- "version": "0.0.10",
3
+ "version": "0.0.11",
4
4
  "type": "module",
5
5
  "description": "Visual Editing for the CloudCannon CMS.",
6
6
  "keywords": [
@@ -53,7 +53,7 @@
53
53
  "./internal/styles": "./styles/index.js"
54
54
  },
55
55
  "devDependencies": {
56
- "@biomejs/biome": "2.3.6",
56
+ "@biomejs/biome": "2.4.4",
57
57
  "@cloudcannon/javascript-api": "0.0.10",
58
58
  "@types/js-beautify": "1.14.3",
59
59
  "@types/react": "19.2.8",
@@ -9,7 +9,9 @@ editable-array-item {
9
9
  will-change: contents;
10
10
  }
11
11
 
12
- editable-array-item,
13
- [data-editable="array-item"] {
14
- position: relative;
12
+ @supports not (anchor-name: --a) {
13
+ editable-array-item,
14
+ [data-editable="array-item"] {
15
+ position: relative;
16
+ }
15
17
  }
@@ -9,7 +9,9 @@ editable-component {
9
9
  will-change: contents;
10
10
  }
11
11
 
12
- editable-component,
13
- [data-editable="component"] {
14
- position: relative;
12
+ @supports not (anchor-name: --a) {
13
+ editable-component,
14
+ [data-editable="component"] {
15
+ position: relative;
16
+ }
15
17
  }
@@ -90,6 +90,27 @@ button {
90
90
  }
91
91
  }
92
92
 
93
+ .context-menu:popover-open {
94
+ display: block;
95
+ pointer-events: auto;
96
+ inset: auto;
97
+ margin: 4px 4px 0 0;
98
+ position: fixed;
99
+ top: anchor(bottom);
100
+ right: anchor(right);
101
+ position-try-fallbacks: flip-block;
102
+ }
103
+
104
+ @supports (anchor-name: --a) {
105
+ :host {
106
+ position: fixed;
107
+ top: anchor(top);
108
+ right: anchor(right);
109
+ margin-top: var(--ccve-editable-outline-width);
110
+ margin-right: var(--ccve-editable-outline-width);
111
+ }
112
+ }
113
+
93
114
  @keyframes fade-in-up {
94
115
  from {
95
116
  opacity: 0.5;