@shelamkoff/rector 1.0.1 → 1.0.3

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 (63) hide show
  1. package/README.md +25 -24
  2. package/dist/core/clipboard/Clipboard.d.ts +2 -0
  3. package/dist/core/clipboard/Clipboard.js +52 -24
  4. package/dist/core/index.js +195 -194
  5. package/dist/core/locale/en.d.ts +1 -0
  6. package/dist/core/locale/en.js +1 -0
  7. package/dist/core/locale/ru.d.ts +1 -0
  8. package/dist/core/locale/ru.js +1 -0
  9. package/dist/core/themes/light.css +1 -1
  10. package/dist/core/themes/variables.css +72 -10
  11. package/dist/locale/en.d.ts +41 -0
  12. package/dist/locale/ru.d.ts +41 -0
  13. package/dist/plugins/attaches/attaches.css +16 -14
  14. package/dist/plugins/attaches/index.js +997 -920
  15. package/dist/plugins/attaches/locale/en.d.ts +9 -0
  16. package/dist/plugins/attaches/locale/en.js +11 -2
  17. package/dist/plugins/attaches/locale/ru.d.ts +9 -0
  18. package/dist/plugins/attaches/locale/ru.js +11 -2
  19. package/dist/plugins/carousel/carousel.css +213 -49
  20. package/dist/plugins/carousel/index.js +918 -858
  21. package/dist/plugins/carousel/locale/en.d.ts +15 -0
  22. package/dist/plugins/carousel/locale/en.js +18 -3
  23. package/dist/plugins/carousel/locale/ru.d.ts +15 -0
  24. package/dist/plugins/carousel/locale/ru.js +18 -3
  25. package/dist/plugins/gallery/README.md +92 -92
  26. package/dist/plugins/gallery/README.ru.md +17 -17
  27. package/dist/plugins/gallery/gallery.css +44 -19
  28. package/dist/plugins/gallery/index.js +375 -354
  29. package/dist/plugins/gallery/locale/en.d.ts +8 -0
  30. package/dist/plugins/gallery/locale/en.js +11 -3
  31. package/dist/plugins/gallery/locale/ru.d.ts +8 -0
  32. package/dist/plugins/gallery/locale/ru.js +11 -3
  33. package/dist/plugins/gallery/settings.js +15 -10
  34. package/dist/plugins/gallery/view-empty.d.ts +2 -0
  35. package/dist/plugins/gallery/view-empty.js +21 -15
  36. package/dist/plugins/gallery/view-filled.d.ts +2 -2
  37. package/dist/plugins/gallery/view-filled.js +84 -72
  38. package/dist/plugins/image/image.css +7 -2
  39. package/dist/plugins/image/index.js +365 -345
  40. package/dist/plugins/image/locale/en.d.ts +8 -0
  41. package/dist/plugins/image/locale/en.js +11 -3
  42. package/dist/plugins/image/locale/ru.d.ts +8 -0
  43. package/dist/plugins/image/locale/ru.js +11 -3
  44. package/dist/plugins/image/view-empty.d.ts +2 -0
  45. package/dist/plugins/image/view-empty.js +21 -15
  46. package/dist/plugins/image/view-filled.d.ts +2 -2
  47. package/dist/plugins/image/view-filled.js +77 -72
  48. package/dist/plugins/shared/dropzone.d.ts +6 -0
  49. package/dist/plugins/shared/dropzone.js +47 -36
  50. package/dist/plugins/shared/layer.d.ts +13 -0
  51. package/dist/plugins/shared/layer.js +45 -0
  52. package/dist/plugins/shared/sourceEditor.css +165 -0
  53. package/dist/plugins/shared/sourceEditor.d.ts +97 -0
  54. package/dist/plugins/shared/sourceEditor.js +260 -0
  55. package/dist/renderer/renderers/carousel/index.js +24 -23
  56. package/dist/renderer/renderers/carousel/styles.css +8 -2
  57. package/dist/renderer/renderers/gallery/README.md +41 -41
  58. package/dist/renderer/renderers/gallery/README.ru.md +41 -41
  59. package/dist/renderer/renderers/gallery/index.js +100 -85
  60. package/dist/renderer/renderers/gallery/styles.css +417 -401
  61. package/dist/shared/galleryMasonry.d.ts +34 -0
  62. package/dist/shared/galleryMasonry.js +152 -0
  63. package/package.json +29 -28
@@ -6,6 +6,14 @@ declare const _default: {
6
6
  'plugin.gallery.dropzone': string;
7
7
  'plugin.gallery.dropzoneUpload': string;
8
8
  'plugin.gallery.dropzoneText': string;
9
+ 'plugin.gallery.dropzoneUrlPrefix': string;
10
+ 'plugin.gallery.dropzoneUrl': string;
11
+ 'plugin.gallery.urlEditorTitle': string;
12
+ 'plugin.gallery.urlEditorLabel': string;
13
+ 'plugin.gallery.urlEditorPlaceholder': string;
14
+ 'plugin.gallery.sourceSubmit': string;
15
+ 'plugin.gallery.sourceCancel': string;
16
+ 'plugin.gallery.invalidUrl': string;
9
17
  'plugin.gallery.addMore': string;
10
18
  'plugin.gallery.delete': string;
11
19
  'plugin.gallery.deleteAll': string;
@@ -6,6 +6,14 @@ export default {
6
6
  'plugin.gallery.dropzone': 'or drag and drop files here',
7
7
  'plugin.gallery.dropzoneUpload': 'Upload',
8
8
  'plugin.gallery.dropzoneText': 'images from your device or drag and drop them here',
9
+ 'plugin.gallery.dropzoneUrlPrefix': 'or',
10
+ 'plugin.gallery.dropzoneUrl': 'insert by URL',
11
+ 'plugin.gallery.urlEditorTitle': 'Insert image by URL',
12
+ 'plugin.gallery.urlEditorLabel': 'Image URL',
13
+ 'plugin.gallery.urlEditorPlaceholder': 'https://example.com/image.jpg',
14
+ 'plugin.gallery.sourceSubmit': 'Insert',
15
+ 'plugin.gallery.sourceCancel': 'Cancel',
16
+ 'plugin.gallery.invalidUrl': 'Enter a valid image URL.',
9
17
  'plugin.gallery.addMore': 'Add more',
10
18
  'plugin.gallery.delete': 'Delete',
11
19
  'plugin.gallery.deleteAll': 'Delete all',
@@ -30,6 +38,6 @@ export default {
30
38
  'plugin.gallery.layoutAuto': 'Automatic layout',
31
39
  'plugin.gallery.layoutMasonry': 'Masonry layout',
32
40
  'plugin.gallery.layoutTriptych': 'Triptych layout',
33
- 'plugin.gallery.layoutTemplate': 'Layout template',
34
- 'plugin.gallery.emptyReadonly': 'No images',
35
- }
41
+ 'plugin.gallery.layoutTemplate': 'Layout template',
42
+ 'plugin.gallery.emptyReadonly': 'No images',
43
+ }
@@ -6,6 +6,14 @@ declare const _default: {
6
6
  'plugin.gallery.dropzone': string;
7
7
  'plugin.gallery.dropzoneUpload': string;
8
8
  'plugin.gallery.dropzoneText': string;
9
+ 'plugin.gallery.dropzoneUrlPrefix': string;
10
+ 'plugin.gallery.dropzoneUrl': string;
11
+ 'plugin.gallery.urlEditorTitle': string;
12
+ 'plugin.gallery.urlEditorLabel': string;
13
+ 'plugin.gallery.urlEditorPlaceholder': string;
14
+ 'plugin.gallery.sourceSubmit': string;
15
+ 'plugin.gallery.sourceCancel': string;
16
+ 'plugin.gallery.invalidUrl': string;
9
17
  'plugin.gallery.addMore': string;
10
18
  'plugin.gallery.delete': string;
11
19
  'plugin.gallery.deleteAll': string;
@@ -6,6 +6,14 @@ export default {
6
6
  'plugin.gallery.dropzone': 'или перетащите файлы сюда',
7
7
  'plugin.gallery.dropzoneUpload': 'Загрузите',
8
8
  'plugin.gallery.dropzoneText': 'изображения с устройства или перетащите их сюда',
9
+ 'plugin.gallery.dropzoneUrlPrefix': 'или',
10
+ 'plugin.gallery.dropzoneUrl': 'вставьте по URL',
11
+ 'plugin.gallery.urlEditorTitle': 'Вставка изображения по URL',
12
+ 'plugin.gallery.urlEditorLabel': 'URL изображения',
13
+ 'plugin.gallery.urlEditorPlaceholder': 'https://example.com/image.jpg',
14
+ 'plugin.gallery.sourceSubmit': 'Вставить',
15
+ 'plugin.gallery.sourceCancel': 'Отмена',
16
+ 'plugin.gallery.invalidUrl': 'Введите корректный URL изображения.',
9
17
  'plugin.gallery.addMore': 'Добавить ещё',
10
18
  'plugin.gallery.delete': 'Удалить',
11
19
  'plugin.gallery.deleteAll': 'Удалить всё',
@@ -30,6 +38,6 @@ export default {
30
38
  'plugin.gallery.layoutAuto': 'Автоматический макет',
31
39
  'plugin.gallery.layoutMasonry': 'Кладочный макет',
32
40
  'plugin.gallery.layoutTriptych': 'Триптих',
33
- 'plugin.gallery.layoutTemplate': 'Шаблон макета',
34
- 'plugin.gallery.emptyReadonly': 'Нет изображений',
35
- }
41
+ 'plugin.gallery.layoutTemplate': 'Шаблон макета',
42
+ 'plugin.gallery.emptyReadonly': 'Нет изображений',
43
+ }
@@ -153,20 +153,25 @@ function buildStylesGroup(wrapper, state, deps, signal) {
153
153
  const group = createGroup(deps.t('styles', 'Styles'))
154
154
  const styles = state.data.styles
155
155
 
156
- const makeStyleInput = (/** @type {string} */ key, /** @type {string} */ value) => {
157
- const input = document.createElement('input')
158
- input.type = 'text'
159
- input.className = CSS.styleInput
160
- input.value = value || ''
156
+ const makeStyleInput = (/** @type {string} */ key, /** @type {string} */ value) => {
157
+ const input = document.createElement('input')
158
+ input.type = 'text'
159
+ input.className = CSS.styleInput
160
+ input.value = value || ''
161
161
  input.addEventListener('input', () => {
162
162
  deps.mutate(() => {
163
163
  if (input.value) styles[key] = input.value
164
164
  else delete styles[key]
165
- applyGalleryStyles(wrapper, state)
166
- })
167
- }, { signal })
168
- return input
169
- }
165
+ applyGalleryStyles(wrapper, state)
166
+ })
167
+ }, { signal })
168
+ if (key === 'gap') {
169
+ input.addEventListener('change', () => {
170
+ if (state.data.layout === 'masonry') deps.reRender()
171
+ }, { signal })
172
+ }
173
+ return input
174
+ }
170
175
 
171
176
  const makeRow = (/** @type {string} */ label, /** @type {HTMLElement} */ input) => {
172
177
  const row = document.createElement('div')
@@ -3,6 +3,7 @@
3
3
  * @property {(key: string, fallback: string) => string} t
4
4
  * @property {boolean} readOnly
5
5
  * @property {() => void} onUploadClick
6
+ * @property {() => void} onOpenUrlEditor
6
7
  * @property {Array<{ icon?: string, label: string, handler: (context: { signal: AbortSignal }) => Promise<Array<{url: string, alt?: string}> | null> }>} customActions
7
8
  * @property {(handler: (context: { signal: AbortSignal }) => Promise<Array<{url: string, alt?: string}> | null>) => Promise<void>} runCustomAction
8
9
  * @property {(files: File[]) => void} onFilesDropped
@@ -21,6 +22,7 @@ export type EmptyViewDeps = {
21
22
  t: (key: string, fallback: string) => string;
22
23
  readOnly: boolean;
23
24
  onUploadClick: () => void;
25
+ onOpenUrlEditor: () => void;
24
26
  customActions: Array<{
25
27
  icon?: string;
26
28
  label: string;
@@ -1,13 +1,14 @@
1
- import { renderDropzone } from '../shared/dropzone.js'
2
- import { isSupportedImageFile } from '../shared/fileInput.js'
1
+ import { renderDropzone } from '../shared/dropzone.js'
2
+ import { isSupportedImageFile } from '../shared/fileInput.js'
3
3
  import { CSS } from './css.js'
4
4
  import { ICON_SELECT } from './icons.js'
5
5
 
6
6
  /**
7
- * @typedef {Object} EmptyViewDeps
8
- * @property {(key: string, fallback: string) => string} t
9
- * @property {boolean} readOnly
7
+ * @typedef {Object} EmptyViewDeps
8
+ * @property {(key: string, fallback: string) => string} t
9
+ * @property {boolean} readOnly
10
10
  * @property {() => void} onUploadClick
11
+ * @property {() => void} onOpenUrlEditor
11
12
  * @property {Array<{ icon?: string, label: string, handler: (context: { signal: AbortSignal }) => Promise<Array<{url: string, alt?: string}> | null> }>} customActions
12
13
  * @property {(handler: (context: { signal: AbortSignal }) => Promise<Array<{url: string, alt?: string}> | null>) => Promise<void>} runCustomAction
13
14
  * @property {(files: File[]) => void} onFilesDropped
@@ -17,11 +18,11 @@ import { ICON_SELECT } from './icons.js'
17
18
  * Render the empty-state dropzone for the Gallery plugin.
18
19
  * Replaces wrapper contents and removes the `filled` class.
19
20
  *
20
- * @param {HTMLElement} wrapper
21
- * @param {import('./state.js').GalleryState} state
22
- * @param {EmptyViewDeps} deps
23
- * @returns {void}
24
- */
21
+ * @param {HTMLElement} wrapper
22
+ * @param {import('./state.js').GalleryState} state
23
+ * @param {EmptyViewDeps} deps
24
+ * @returns {void}
25
+ */
25
26
  export function renderEmptyView(wrapper, state, deps) {
26
27
  state.resetTransient()
27
28
 
@@ -38,18 +39,23 @@ export function renderEmptyView(wrapper, state, deps) {
38
39
  filled: CSS.filled,
39
40
  }, {
40
41
  iconHtml: ICON_SELECT,
41
- uploadText: deps.t('dropzoneUpload', 'Upload'),
42
- afterText: deps.t('dropzoneText', 'images from your device or drag and drop them here'),
43
- readOnly: deps.readOnly,
44
- emptyText: deps.t('emptyReadonly', 'No images'),
42
+ uploadText: deps.t('dropzoneUpload', 'Upload'),
43
+ afterText: deps.t('dropzoneText', 'images from your device or drag and drop them here'),
44
+ readOnly: deps.readOnly,
45
+ emptyText: deps.t('emptyReadonly', 'No images'),
45
46
  onUploadClick: deps.onUploadClick,
47
+ inlineActions: [{
48
+ prefix: deps.t('dropzoneUrlPrefix', 'or'),
49
+ label: deps.t('dropzoneUrl', 'insert by URL'),
50
+ onSelect: deps.onOpenUrlEditor,
51
+ }],
46
52
  actions: deps.customActions.map(action => ({
47
53
  icon: action.icon,
48
54
  label: action.label,
49
55
  onSelect: () => { void deps.runCustomAction(action.handler) },
50
56
  })),
51
57
  onDrop: (dt) => {
52
- const files = [...(dt.files || [])].filter(isSupportedImageFile)
58
+ const files = [...(dt.files || [])].filter(isSupportedImageFile)
53
59
  if (files.length > 0) deps.onFilesDropped(files)
54
60
  },
55
61
  })
@@ -9,7 +9,7 @@
9
9
  * @property {(operation: () => void) => void} mutate
10
10
  * @property {(files: File[]) => void} onFilesDropped
11
11
  * @property {() => void} onTriggerFileInput
12
- * @property {() => void} onPromptUrl
12
+ * @property {() => void} onOpenUrlEditor
13
13
  * @property {() => void} onDeleteAll
14
14
  * @property {Array<{ icon?: string, label: string, handler: (context: { signal: AbortSignal }) => Promise<Array<{url: string, alt?: string}> | null> }>} customActions
15
15
  * @property {(handler: (context: { signal: AbortSignal }) => Promise<Array<{url: string, alt?: string}> | null>) => Promise<void>} runCustomAction
@@ -34,7 +34,7 @@ export type FilledViewDeps = {
34
34
  mutate: (operation: () => void) => void;
35
35
  onFilesDropped: (files: File[]) => void;
36
36
  onTriggerFileInput: () => void;
37
- onPromptUrl: () => void;
37
+ onOpenUrlEditor: () => void;
38
38
  onDeleteAll: () => void;
39
39
  customActions: Array<{
40
40
  icon?: string;
@@ -10,14 +10,16 @@ import {
10
10
  } from './layout.js'
11
11
  import { applyGalleryStyles } from './styles.js'
12
12
  import { buildSettingsPanel } from './settings.js'
13
- import {
14
- attachExternalDrop, createEmptySlot, createFilledSlot, createOverflowItem,
15
- } from './slot.js'
13
+ import {
14
+ attachExternalDrop, createEmptySlot, createFilledSlot, createOverflowItem,
15
+ } from './slot.js'
16
+ import { createPluginLayer } from '../shared/layer.js'
17
+ import { mountGalleryMasonry } from '../../shared/galleryMasonry.js'
16
18
 
17
19
  /**
18
- * @typedef {Object} FilledViewDeps
19
- * @property {(key: string, fallback: string) => string} t
20
- * @property {boolean} readOnly
20
+ * @typedef {Object} FilledViewDeps
21
+ * @property {(key: string, fallback: string) => string} t
22
+ * @property {boolean} readOnly
21
23
  * @property {() => void} syncCaptions
22
24
  * @property {() => import('./state.js').GalleryState | undefined} getState
23
25
  * @property {() => void} reRender
@@ -25,7 +27,7 @@ import {
25
27
  * @property {(operation: () => void) => void} mutate
26
28
  * @property {(files: File[]) => void} onFilesDropped
27
29
  * @property {() => void} onTriggerFileInput
28
- * @property {() => void} onPromptUrl
30
+ * @property {() => void} onOpenUrlEditor
29
31
  * @property {() => void} onDeleteAll
30
32
  * @property {Array<{ icon?: string, label: string, handler: (context: { signal: AbortSignal }) => Promise<Array<{url: string, alt?: string}> | null> }>} customActions
31
33
  * @property {(handler: (context: { signal: AbortSignal }) => Promise<Array<{url: string, alt?: string}> | null>) => Promise<void>} runCustomAction
@@ -35,11 +37,11 @@ import {
35
37
  * Render the filled-state Gallery view: template grid (or masonry) +
36
38
  * optional overflow row + action bar.
37
39
  *
38
- * @param {HTMLElement} wrapper
39
- * @param {import('./state.js').GalleryState} state
40
- * @param {FilledViewDeps} deps
41
- * @returns {void}
42
- */
40
+ * @param {HTMLElement} wrapper
41
+ * @param {import('./state.js').GalleryState} state
42
+ * @param {FilledViewDeps} deps
43
+ * @returns {void}
44
+ */
43
45
  export function renderFilledView(wrapper, state, deps) {
44
46
  state.resetTransient()
45
47
  wrapper.innerHTML = ''
@@ -54,7 +56,7 @@ export function renderFilledView(wrapper, state, deps) {
54
56
  }
55
57
 
56
58
  applyGalleryStyles(wrapper, state)
57
- if (!deps.readOnly) wrapper.appendChild(renderActions(wrapper, state, deps, signal))
59
+ if (!deps.readOnly) wrapper.appendChild(renderActions(wrapper, state, deps, signal))
58
60
  }
59
61
 
60
62
  /**
@@ -65,21 +67,27 @@ export function renderFilledView(wrapper, state, deps) {
65
67
  * @param {FilledViewDeps} deps
66
68
  * @param {AbortSignal} signal
67
69
  */
68
- function renderMasonry(wrapper, state, deps, signal) {
69
- const grid = document.createElement('div')
70
- grid.className = `${CSS.grid} eg--masonry`
71
-
72
- const slotDeps = makeSlotDeps(deps)
73
-
74
- state.data.images.forEach((img, i) => {
75
- const slot = createFilledSlot(img, i, signal, slotDeps)
76
- slot.dataset.index = String(i)
77
- grid.appendChild(slot)
78
- })
79
-
70
+ function renderMasonry(wrapper, state, deps, signal) {
71
+ const grid = document.createElement('div')
72
+ grid.className = `${CSS.grid} eg--masonry`
73
+
74
+ const slotDeps = makeSlotDeps(deps)
75
+ /** @type {HTMLElement[]} */
76
+ const slots = []
77
+
78
+ state.data.images.forEach((img, i) => {
79
+ const slot = createFilledSlot(img, i, signal, slotDeps)
80
+ slot.dataset.index = String(i)
81
+ const image = /** @type {HTMLImageElement | null} */ (slot.querySelector(`.${CSS.slotImg}`))
82
+ if (image) image.loading = 'eager'
83
+ slots.push(slot)
84
+ grid.appendChild(slot)
85
+ })
86
+
80
87
  if (!deps.readOnly) attachExternalDrop(grid, signal, deps.onFilesDropped)
81
- wrapper.appendChild(grid)
82
- }
88
+ wrapper.appendChild(grid)
89
+ mountGalleryMasonry(grid, slots, { signal })
90
+ }
83
91
 
84
92
  /**
85
93
  * Slot-based: fixed template + optional overflow row.
@@ -125,20 +133,20 @@ function renderSlotBased(wrapper, state, deps, signal) {
125
133
  const slot = createFilledSlot(img, idx, signal, slotDeps)
126
134
  slot.dataset.slot = String(idx)
127
135
 
128
- const imgEl = /** @type {HTMLImageElement | null} */ (slot.querySelector(`.${CSS.slotImg}`))
129
- if (imgEl) {
130
- let ready = false
131
- const onReady = () => {
132
- if (ready) return
133
- ready = true
134
- orientations[idx] = classifyOrientation(imgEl.naturalWidth, imgEl.naturalHeight)
135
- loadedCount++
136
- if (loadedCount >= slotCount) onAllLoaded()
137
- }
138
- imgEl.addEventListener('load', onReady, { once: true, signal })
139
- imgEl.addEventListener('error', onReady, { once: true, signal })
140
- if (imgEl.complete) queueMicrotask(onReady)
141
- }
136
+ const imgEl = /** @type {HTMLImageElement | null} */ (slot.querySelector(`.${CSS.slotImg}`))
137
+ if (imgEl) {
138
+ let ready = false
139
+ const onReady = () => {
140
+ if (ready) return
141
+ ready = true
142
+ orientations[idx] = classifyOrientation(imgEl.naturalWidth, imgEl.naturalHeight)
143
+ loadedCount++
144
+ if (loadedCount >= slotCount) onAllLoaded()
145
+ }
146
+ imgEl.addEventListener('load', onReady, { once: true, signal })
147
+ imgEl.addEventListener('error', onReady, { once: true, signal })
148
+ if (imgEl.complete) queueMicrotask(onReady)
149
+ }
142
150
  grid.appendChild(slot)
143
151
  }
144
152
  } else {
@@ -155,7 +163,7 @@ function renderSlotBased(wrapper, state, deps, signal) {
155
163
  }
156
164
  }
157
165
 
158
- if (!deps.readOnly) attachExternalDrop(grid, signal, deps.onFilesDropped)
166
+ if (!deps.readOnly) attachExternalDrop(grid, signal, deps.onFilesDropped)
159
167
  wrapper.appendChild(grid)
160
168
 
161
169
  if (overflowImages.length > 0) {
@@ -167,7 +175,7 @@ function renderSlotBased(wrapper, state, deps, signal) {
167
175
  overflow.appendChild(createOverflowItem(img, globalIdx, signal, slotDeps))
168
176
  })
169
177
 
170
- if (!deps.readOnly) attachExternalDrop(overflow, signal, deps.onFilesDropped)
178
+ if (!deps.readOnly) attachExternalDrop(overflow, signal, deps.onFilesDropped)
171
179
  wrapper.appendChild(overflow)
172
180
  }
173
181
  }
@@ -180,9 +188,9 @@ function renderSlotBased(wrapper, state, deps, signal) {
180
188
  * @returns {import('./slot.js').SlotDeps}
181
189
  */
182
190
  function makeSlotDeps(deps) {
183
- return {
184
- t: deps.t,
185
- readOnly: deps.readOnly,
191
+ return {
192
+ t: deps.t,
193
+ readOnly: deps.readOnly,
186
194
  syncCaptions: deps.syncCaptions,
187
195
  getState: deps.getState,
188
196
  onRemoveImage: (index) => {
@@ -229,37 +237,41 @@ function renderActions(wrapper, state, deps, signal) {
229
237
  dropdown.className = CSS.dropdown
230
238
 
231
239
  const settingsBtn = document.createElement('button')
232
- settingsBtn.type = 'button'
233
- settingsBtn.className = CSS.actionBtn
234
- settingsBtn.innerHTML = `${ICON_SETTINGS} ${deps.t('settings', 'Settings')}`
235
- settingsBtn.setAttribute('aria-haspopup', 'true')
236
- settingsBtn.setAttribute('aria-expanded', 'false')
237
-
238
- const panel = buildSettingsPanel(wrapper, state, settingsDeps)
239
- panel.setAttribute('role', 'group')
240
- panel.setAttribute('aria-label', deps.t('settings', 'Settings'))
240
+ settingsBtn.type = 'button'
241
+ settingsBtn.className = CSS.actionBtn
242
+ settingsBtn.innerHTML = `${ICON_SETTINGS} ${deps.t('settings', 'Settings')}`
243
+ settingsBtn.setAttribute('aria-haspopup', 'true')
244
+ settingsBtn.setAttribute('aria-expanded', 'false')
245
+
246
+ const panel = buildSettingsPanel(wrapper, state, settingsDeps)
247
+ panel.setAttribute('role', 'group')
248
+ panel.setAttribute('aria-label', deps.t('settings', 'Settings'))
249
+ const settingsLayer = createPluginLayer(wrapper, signal)
250
+ const setSettingsOpen = (open) => {
251
+ dropdown.classList.toggle(CSS.dropdownOpen, open)
252
+ settingsBtn.setAttribute('aria-expanded', String(open))
253
+ if (open) settingsLayer.open()
254
+ else settingsLayer.close()
255
+ }
241
256
 
242
257
  settingsBtn.addEventListener('click', (e) => {
243
258
  e.stopPropagation()
244
- const open = dropdown.classList.toggle(CSS.dropdownOpen)
245
- settingsBtn.setAttribute('aria-expanded', String(open))
259
+ setSettingsOpen(!dropdown.classList.contains(CSS.dropdownOpen))
246
260
  }, { signal })
247
261
 
248
262
  dropdown.append(settingsBtn, panel)
249
263
 
250
264
  document.addEventListener('click', (e) => {
251
265
  if (!dropdown.contains(/** @type {Node} */ (e.target))) {
252
- dropdown.classList.remove(CSS.dropdownOpen)
253
- settingsBtn.setAttribute('aria-expanded', 'false')
254
- }
255
- }, { signal })
256
- dropdown.addEventListener('keydown', (event) => {
257
- if (event.key !== 'Escape' || !dropdown.classList.contains(CSS.dropdownOpen)) return
258
- event.preventDefault()
259
- dropdown.classList.remove(CSS.dropdownOpen)
260
- settingsBtn.setAttribute('aria-expanded', 'false')
261
- settingsBtn.focus()
262
- }, { signal })
266
+ setSettingsOpen(false)
267
+ }
268
+ }, { signal })
269
+ dropdown.addEventListener('keydown', (event) => {
270
+ if (event.key !== 'Escape' || !dropdown.classList.contains(CSS.dropdownOpen)) return
271
+ event.preventDefault()
272
+ setSettingsOpen(false)
273
+ settingsBtn.focus()
274
+ }, { signal })
263
275
 
264
276
  // Main view container (for drill-down hide/show)
265
277
  const mainView = document.createElement('div')
@@ -283,9 +295,9 @@ function renderActions(wrapper, state, deps, signal) {
283
295
  // Delete all
284
296
  const deleteAllBtn = document.createElement('button')
285
297
  deleteAllBtn.type = 'button'
286
- deleteAllBtn.className = `${CSS.actionBtn} ${CSS.actionBtnDanger}`
287
- deleteAllBtn.innerHTML = ICON_TRASH
288
- deleteAllBtn.setAttribute('aria-label', deps.t('deleteAll', 'Delete all'))
298
+ deleteAllBtn.className = `${CSS.actionBtn} ${CSS.actionBtnDanger}`
299
+ deleteAllBtn.innerHTML = ICON_TRASH
300
+ deleteAllBtn.setAttribute('aria-label', deps.t('deleteAll', 'Delete all'))
289
301
  deleteAllBtn.addEventListener('mousedown', (e) => e.preventDefault(), { signal })
290
302
  deleteAllBtn.addEventListener('click', (e) => {
291
303
  e.stopPropagation()
@@ -335,7 +347,7 @@ function showAddView(actions, mainView, deps, signal) {
335
347
 
336
348
  view.appendChild(makeActionBtn(
337
349
  `${ICON_URL} URL`,
338
- () => { deps.onPromptUrl(); restore() },
350
+ () => { deps.onOpenUrlEditor(); restore() },
339
351
  signal,
340
352
  ))
341
353
 
@@ -1,4 +1,5 @@
1
1
  .oe-image {
2
+ position: relative;
2
3
  border-radius: 0.5rem;
3
4
  overflow-anchor: none;
4
5
  text-align: center;
@@ -239,7 +240,7 @@
239
240
  border-radius: 8px;
240
241
  box-shadow: var(--oe-toolbar-shadow, 0 8px 24px rgba(0, 0, 0, 0.4));
241
242
  padding: 12px;
242
- z-index: 100;
243
+ z-index: var(--oe-plugin-panel-z-index, 1100);
243
244
  }
244
245
 
245
246
  .oe-image__dropdown--open .oe-image__dropdown-panel {
@@ -377,7 +378,7 @@
377
378
  border: 1px solid var(--oe-border-hover, #2a2a2a);
378
379
  border-radius: 6px;
379
380
  box-shadow: var(--oe-toolbar-shadow, 0 8px 24px rgba(0, 0, 0, 0.4));
380
- z-index: 110;
381
+ z-index: var(--oe-plugin-popover-z-index, 1120);
381
382
  padding: 3px;
382
383
  max-height: 10rem;
383
384
  overflow-y: auto;
@@ -516,6 +517,10 @@
516
517
  color: var(--oe-text-1, #1a1a1a);
517
518
  }
518
519
 
520
+ .oe-theme-light .oe-image__action-btn:hover {
521
+ color: var(--oe-text-on-accent, #fff);
522
+ }
523
+
519
524
  .oe-theme-light .oe-image__dropdown-panel {
520
525
  background: var(--oe-card, #fff);
521
526
  border-color: var(--oe-border-hover, #d4d4d4);