@shelamkoff/rector 1.0.1 → 1.0.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 (54) 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/gallery.css +6 -1
  26. package/dist/plugins/gallery/index.js +375 -354
  27. package/dist/plugins/gallery/locale/en.d.ts +8 -0
  28. package/dist/plugins/gallery/locale/en.js +11 -3
  29. package/dist/plugins/gallery/locale/ru.d.ts +8 -0
  30. package/dist/plugins/gallery/locale/ru.js +11 -3
  31. package/dist/plugins/gallery/view-empty.d.ts +2 -0
  32. package/dist/plugins/gallery/view-empty.js +21 -15
  33. package/dist/plugins/gallery/view-filled.d.ts +2 -2
  34. package/dist/plugins/gallery/view-filled.js +61 -56
  35. package/dist/plugins/image/image.css +7 -2
  36. package/dist/plugins/image/index.js +365 -345
  37. package/dist/plugins/image/locale/en.d.ts +8 -0
  38. package/dist/plugins/image/locale/en.js +11 -3
  39. package/dist/plugins/image/locale/ru.d.ts +8 -0
  40. package/dist/plugins/image/locale/ru.js +11 -3
  41. package/dist/plugins/image/view-empty.d.ts +2 -0
  42. package/dist/plugins/image/view-empty.js +21 -15
  43. package/dist/plugins/image/view-filled.d.ts +2 -2
  44. package/dist/plugins/image/view-filled.js +77 -72
  45. package/dist/plugins/shared/dropzone.d.ts +6 -0
  46. package/dist/plugins/shared/dropzone.js +47 -36
  47. package/dist/plugins/shared/layer.d.ts +13 -0
  48. package/dist/plugins/shared/layer.js +45 -0
  49. package/dist/plugins/shared/sourceEditor.css +165 -0
  50. package/dist/plugins/shared/sourceEditor.d.ts +97 -0
  51. package/dist/plugins/shared/sourceEditor.js +260 -0
  52. package/dist/renderer/renderers/carousel/index.js +24 -23
  53. package/dist/renderer/renderers/carousel/styles.css +8 -2
  54. package/package.json +32 -32
@@ -0,0 +1,165 @@
1
+ .oe-source-editor {
2
+ position: absolute;
3
+ inset: 0;
4
+ z-index: var(--oe-plugin-dialog-z-index, 1200);
5
+ box-sizing: border-box;
6
+ display: grid;
7
+ place-items: center;
8
+ min-height: 220px;
9
+ padding: 16px;
10
+ }
11
+
12
+ .oe-source-editor--preloaded {
13
+ visibility: hidden;
14
+ opacity: 0;
15
+ pointer-events: none;
16
+ }
17
+
18
+ .oe-source-editor[data-oe-source-editor='html'] {
19
+ min-height: min(26rem, calc(100vh - 2rem));
20
+ }
21
+
22
+ .oe-source-editor__backdrop {
23
+ position: absolute;
24
+ inset: 0;
25
+ width: 100%;
26
+ height: 100%;
27
+ padding: 0;
28
+ background: var(--oe-overlay-dark, rgba(0, 0, 0, 0.7));
29
+ border: 0;
30
+ border-radius: inherit;
31
+ cursor: default;
32
+ }
33
+
34
+ .oe-source-editor__panel {
35
+ position: relative;
36
+ display: grid;
37
+ gap: 12px;
38
+ box-sizing: border-box;
39
+ width: min(100%, 32rem);
40
+ max-height: calc(100vh - 2rem);
41
+ padding: 16px;
42
+ overflow-y: auto;
43
+ background: var(--oe-surface-elevated, #1e1e24);
44
+ border: 1px solid var(--oe-border-hover, #2a2a2a);
45
+ border-radius: var(--oe-radius, 8px);
46
+ box-shadow: var(--oe-toolbar-shadow, 0 8px 24px rgba(0, 0, 0, 0.4));
47
+ text-align: left;
48
+ }
49
+
50
+ .oe-source-editor[data-oe-source-editor='url'] .oe-source-editor__panel {
51
+ width: min(100%, 28rem);
52
+ }
53
+
54
+ .oe-source-editor__title {
55
+ margin: 0;
56
+ color: var(--oe-text-1, #f5f5f5);
57
+ font: inherit;
58
+ font-size: 15px;
59
+ font-weight: 650;
60
+ }
61
+
62
+ .oe-source-editor__label {
63
+ display: grid;
64
+ gap: 8px;
65
+ color: var(--oe-text-2, #c4c4c4);
66
+ font-size: 13px;
67
+ font-weight: 600;
68
+ }
69
+
70
+ .oe-source-editor__field {
71
+ box-sizing: border-box;
72
+ width: 100%;
73
+ min-height: 42px;
74
+ padding: 10px 12px;
75
+ resize: vertical;
76
+ color: var(--oe-text-1, #f5f5f5);
77
+ background: var(--oe-input-bg, #2c2c33);
78
+ border: 1px solid var(--oe-border-hover, #2a2a2a);
79
+ border-radius: var(--oe-radius-sm, 4px);
80
+ outline: none;
81
+ font: inherit;
82
+ font-size: 14px;
83
+ line-height: 1.5;
84
+ }
85
+
86
+ .oe-source-editor textarea.oe-source-editor__field {
87
+ min-height: 9rem;
88
+ max-height: min(40vh, 20rem);
89
+ }
90
+
91
+ .oe-source-editor__field:hover {
92
+ background: var(--oe-input-bg-hover, #353540);
93
+ }
94
+
95
+ .oe-source-editor .oe-source-editor__field:focus,
96
+ .oe-source-editor .oe-source-editor__field:focus-visible {
97
+ border-color: var(--oe-border-hover, #2a2a2a);
98
+ outline: none;
99
+ outline-offset: 0;
100
+ box-shadow: none;
101
+ }
102
+
103
+ .oe-source-editor__field[aria-invalid='true'] {
104
+ border-color: var(--oe-danger, #f87171);
105
+ }
106
+
107
+ .oe-source-editor__error {
108
+ color: var(--oe-danger-light, #fca5a5);
109
+ font-size: 13px;
110
+ }
111
+
112
+ .oe-source-editor__actions {
113
+ display: flex;
114
+ justify-content: flex-end;
115
+ gap: 8px;
116
+ }
117
+
118
+ .oe-source-editor__button {
119
+ min-height: 36px;
120
+ padding: 7px 14px;
121
+ border: 1px solid transparent;
122
+ border-radius: var(--oe-radius-sm, 4px);
123
+ font: inherit;
124
+ font-size: 13px;
125
+ font-weight: 650;
126
+ cursor: pointer;
127
+ }
128
+
129
+ .oe-source-editor__button--secondary {
130
+ color: var(--oe-text-1, #f5f5f5);
131
+ background: var(--oe-card, #111);
132
+ border-color: var(--oe-border-hover, #2a2a2a);
133
+ }
134
+
135
+ .oe-source-editor__button--secondary:hover {
136
+ background: var(--oe-card-hover, #191919);
137
+ }
138
+
139
+ .oe-source-editor__button--primary {
140
+ color: var(--oe-text-on-accent, #fff);
141
+ background: var(--oe-accent, #4357b4);
142
+ }
143
+
144
+ .oe-source-editor__button--primary:hover {
145
+ filter: brightness(1.08);
146
+ }
147
+
148
+ @media (max-width: 520px) {
149
+ .oe-source-editor {
150
+ padding: 10px;
151
+ }
152
+
153
+ .oe-source-editor__panel {
154
+ max-height: calc(100vh - 1.25rem);
155
+ padding: 14px;
156
+ }
157
+
158
+ .oe-source-editor__actions {
159
+ flex-direction: column-reverse;
160
+ }
161
+
162
+ .oe-source-editor__button {
163
+ width: 100%;
164
+ }
165
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * @typedef {Object} SourceEditorConfig
3
+ * @property {HTMLElement} wrapper Plugin wrapper that owns the editor.
4
+ * @property {AbortSignal} signal Plugin render lifecycle signal.
5
+ * @property {'url' | 'html'} kind Source value kind.
6
+ * @property {string} title Dialog title.
7
+ * @property {string} label Accessible field label.
8
+ * @property {string} placeholder Field placeholder.
9
+ * @property {string} submitText Submit button label.
10
+ * @property {string} cancelText Cancel button label.
11
+ * @property {string} invalidText Validation error.
12
+ * @property {(value: string) => string} normalize Sanitizes and validates input.
13
+ * @property {(value: string) => void} onSubmit Receives the normalized value.
14
+ */
15
+ /**
16
+ * Build URL/HTML forms while the owning plugin view is rendered so their
17
+ * first style and layout pass does not happen in the user's click handler.
18
+ *
19
+ * @param {HTMLElement} wrapper Plugin wrapper that owns the editor.
20
+ * @param {AbortSignal} signal Plugin render lifecycle signal.
21
+ * @param {Array<'url' | 'html'>} kinds Forms used by the current plugin view.
22
+ * @returns {void}
23
+ */
24
+ export function preloadSourceEditor(wrapper: HTMLElement, signal: AbortSignal, kinds: Array<"url" | "html">): void;
25
+ /**
26
+ * Open an editor-owned URL or HTML input surface without relying on native
27
+ * browser prompts.
28
+ *
29
+ * @param {SourceEditorConfig} config Source editor configuration.
30
+ * @returns {{ close: () => void }}
31
+ */
32
+ export function openSourceEditor(config: SourceEditorConfig): {
33
+ close: () => void;
34
+ };
35
+ export type SourceEditorConfig = {
36
+ /**
37
+ * Plugin wrapper that owns the editor.
38
+ */
39
+ wrapper: HTMLElement;
40
+ /**
41
+ * Plugin render lifecycle signal.
42
+ */
43
+ signal: AbortSignal;
44
+ /**
45
+ * Source value kind.
46
+ */
47
+ kind: "url" | "html";
48
+ /**
49
+ * Dialog title.
50
+ */
51
+ title: string;
52
+ /**
53
+ * Accessible field label.
54
+ */
55
+ label: string;
56
+ /**
57
+ * Field placeholder.
58
+ */
59
+ placeholder: string;
60
+ /**
61
+ * Submit button label.
62
+ */
63
+ submitText: string;
64
+ /**
65
+ * Cancel button label.
66
+ */
67
+ cancelText: string;
68
+ /**
69
+ * Validation error.
70
+ */
71
+ invalidText: string;
72
+ /**
73
+ * Sanitizes and validates input.
74
+ */
75
+ normalize: (value: string) => string;
76
+ /**
77
+ * Receives the normalized value.
78
+ */
79
+ onSubmit: (value: string) => void;
80
+ };
81
+ export type SourceEditorSurface = {
82
+ root: HTMLElement;
83
+ backdrop: HTMLButtonElement;
84
+ panel: HTMLFormElement;
85
+ title: HTMLHeadingElement;
86
+ labelText: HTMLSpanElement;
87
+ field: HTMLInputElement | HTMLTextAreaElement;
88
+ error: HTMLDivElement;
89
+ cancel: HTMLButtonElement;
90
+ submit: HTMLButtonElement;
91
+ signal: AbortSignal;
92
+ layer: {
93
+ open: () => void;
94
+ close: () => void;
95
+ };
96
+ destroy: () => void;
97
+ };
@@ -0,0 +1,260 @@
1
+ import { createPluginLayer } from './layer.js'
2
+
3
+ /**
4
+ * @typedef {Object} SourceEditorSurface
5
+ * @property {HTMLElement} root
6
+ * @property {HTMLButtonElement} backdrop
7
+ * @property {HTMLFormElement} panel
8
+ * @property {HTMLHeadingElement} title
9
+ * @property {HTMLSpanElement} labelText
10
+ * @property {HTMLInputElement | HTMLTextAreaElement} field
11
+ * @property {HTMLDivElement} error
12
+ * @property {HTMLButtonElement} cancel
13
+ * @property {HTMLButtonElement} submit
14
+ * @property {AbortSignal} signal
15
+ * @property {{ open: () => void, close: () => void }} layer
16
+ * @property {() => void} destroy
17
+ */
18
+
19
+ /** @type {WeakMap<HTMLElement, { close: () => void, surface: SourceEditorSurface }>} */
20
+ const activeEditors = new WeakMap()
21
+
22
+ /** @type {WeakMap<HTMLElement, Map<'url' | 'html', SourceEditorSurface>>} */
23
+ const surfaces = new WeakMap()
24
+
25
+ /**
26
+ * @typedef {Object} SourceEditorConfig
27
+ * @property {HTMLElement} wrapper Plugin wrapper that owns the editor.
28
+ * @property {AbortSignal} signal Plugin render lifecycle signal.
29
+ * @property {'url' | 'html'} kind Source value kind.
30
+ * @property {string} title Dialog title.
31
+ * @property {string} label Accessible field label.
32
+ * @property {string} placeholder Field placeholder.
33
+ * @property {string} submitText Submit button label.
34
+ * @property {string} cancelText Cancel button label.
35
+ * @property {string} invalidText Validation error.
36
+ * @property {(value: string) => string} normalize Sanitizes and validates input.
37
+ * @property {(value: string) => void} onSubmit Receives the normalized value.
38
+ */
39
+
40
+ /**
41
+ * Build URL/HTML forms while the owning plugin view is rendered so their
42
+ * first style and layout pass does not happen in the user's click handler.
43
+ *
44
+ * @param {HTMLElement} wrapper Plugin wrapper that owns the editor.
45
+ * @param {AbortSignal} signal Plugin render lifecycle signal.
46
+ * @param {Array<'url' | 'html'>} kinds Forms used by the current plugin view.
47
+ * @returns {void}
48
+ */
49
+ export function preloadSourceEditor(wrapper, signal, kinds) {
50
+ if (signal.aborted) return
51
+ for (const kind of new Set(kinds)) ensureSurface(wrapper, signal, kind)
52
+ }
53
+
54
+ /**
55
+ * Open an editor-owned URL or HTML input surface without relying on native
56
+ * browser prompts.
57
+ *
58
+ * @param {SourceEditorConfig} config Source editor configuration.
59
+ * @returns {{ close: () => void }}
60
+ */
61
+ export function openSourceEditor(config) {
62
+ if (config.signal.aborted) return { close() {} }
63
+ activeEditors.get(config.wrapper)?.close()
64
+
65
+ const surface = ensureSurface(config.wrapper, config.signal, config.kind)
66
+ if (!surface) return { close() {} }
67
+ const controller = new AbortController()
68
+ const { root, backdrop, panel, title, labelText, field, error, cancel, submit } = surface
69
+ backdrop.setAttribute('aria-label', config.cancelText)
70
+ panel.setAttribute('aria-label', config.title)
71
+ title.textContent = config.title
72
+ labelText.textContent = config.label
73
+ field.value = ''
74
+ field.placeholder = config.placeholder
75
+ field.removeAttribute('aria-invalid')
76
+ error.textContent = ''
77
+ error.hidden = true
78
+ cancel.textContent = config.cancelText
79
+ submit.textContent = config.submitText
80
+ setSurfaceVisible(surface, true)
81
+ surface.layer.open()
82
+
83
+ let closed = false
84
+ const close = () => {
85
+ if (closed) return
86
+ closed = true
87
+ if (activeEditors.get(config.wrapper)?.close === close) activeEditors.delete(config.wrapper)
88
+ controller.abort()
89
+ surface.layer.close()
90
+ setSurfaceVisible(surface, false)
91
+ }
92
+ activeEditors.set(config.wrapper, { close, surface })
93
+
94
+ config.signal.addEventListener('abort', close, { once: true, signal: controller.signal })
95
+ backdrop.addEventListener('click', close, { signal: controller.signal })
96
+ cancel.addEventListener('click', close, { signal: controller.signal })
97
+ root.addEventListener('keydown', (event) => {
98
+ if (event.key === 'Escape') {
99
+ event.preventDefault()
100
+ event.stopPropagation()
101
+ close()
102
+ }
103
+ }, { signal: controller.signal })
104
+ panel.addEventListener('submit', (event) => {
105
+ event.preventDefault()
106
+ let value = ''
107
+ try {
108
+ value = config.normalize(field.value)
109
+ } catch {
110
+ value = ''
111
+ }
112
+ if (!value) {
113
+ error.textContent = config.invalidText
114
+ error.hidden = false
115
+ field.setAttribute('aria-invalid', 'true')
116
+ field.focus()
117
+ return
118
+ }
119
+ close()
120
+ config.onSubmit(value)
121
+ }, { signal: controller.signal })
122
+ field.addEventListener('input', () => {
123
+ error.hidden = true
124
+ field.removeAttribute('aria-invalid')
125
+ }, { signal: controller.signal })
126
+
127
+ queueMicrotask(() => {
128
+ if (!closed) field.focus({ preventScroll: true })
129
+ })
130
+
131
+ return { close }
132
+ }
133
+
134
+ /**
135
+ * @param {HTMLElement} wrapper
136
+ * @param {AbortSignal} signal
137
+ * @param {'url' | 'html'} kind
138
+ * @returns {SourceEditorSurface | null}
139
+ */
140
+ function ensureSurface(wrapper, signal, kind) {
141
+ if (signal.aborted) return null
142
+
143
+ let owned = surfaces.get(wrapper)
144
+ if (!owned) {
145
+ owned = new Map()
146
+ surfaces.set(wrapper, owned)
147
+ }
148
+
149
+ const cached = owned.get(kind)
150
+ if (cached?.signal === signal && cached.root.isConnected) return cached
151
+ cached?.destroy()
152
+
153
+ const root = document.createElement('div')
154
+ root.className = 'oe-source-editor oe-source-editor--preloaded'
155
+ root.dataset.oeSourceEditor = kind
156
+ root.setAttribute('aria-hidden', 'true')
157
+ root.inert = true
158
+
159
+ const backdrop = document.createElement('button')
160
+ backdrop.type = 'button'
161
+ backdrop.className = 'oe-source-editor__backdrop'
162
+ backdrop.setAttribute('aria-label', 'Cancel')
163
+ backdrop.tabIndex = -1
164
+
165
+ const panel = document.createElement('form')
166
+ panel.className = 'oe-source-editor__panel'
167
+ panel.noValidate = true
168
+ panel.setAttribute('role', 'dialog')
169
+ panel.setAttribute('aria-modal', 'true')
170
+ panel.setAttribute('aria-label', 'Source')
171
+
172
+ const title = document.createElement('h3')
173
+ title.className = 'oe-source-editor__title'
174
+ title.textContent = 'Source'
175
+
176
+ const label = document.createElement('label')
177
+ label.className = 'oe-source-editor__label'
178
+ const labelText = document.createElement('span')
179
+ labelText.textContent = 'Source'
180
+
181
+ /** @type {HTMLInputElement | HTMLTextAreaElement} */
182
+ let field
183
+ if (kind === 'html') {
184
+ const textarea = document.createElement('textarea')
185
+ textarea.rows = 6
186
+ textarea.spellcheck = false
187
+ field = textarea
188
+ } else {
189
+ const input = document.createElement('input')
190
+ input.type = 'url'
191
+ input.inputMode = 'url'
192
+ input.setAttribute('autocomplete', 'url')
193
+ field = input
194
+ }
195
+ field.className = 'oe-source-editor__field'
196
+ field.required = true
197
+ label.append(labelText, field)
198
+
199
+ const error = document.createElement('div')
200
+ error.className = 'oe-source-editor__error'
201
+ error.setAttribute('role', 'alert')
202
+ error.hidden = true
203
+
204
+ const actions = document.createElement('div')
205
+ actions.className = 'oe-source-editor__actions'
206
+
207
+ const cancel = document.createElement('button')
208
+ cancel.type = 'button'
209
+ cancel.className = 'oe-source-editor__button oe-source-editor__button--secondary'
210
+ cancel.textContent = 'Cancel'
211
+
212
+ const submit = document.createElement('button')
213
+ submit.type = 'submit'
214
+ submit.className = 'oe-source-editor__button oe-source-editor__button--primary'
215
+ submit.textContent = 'Insert'
216
+
217
+ actions.append(cancel, submit)
218
+ panel.append(title, label, error, actions)
219
+ root.append(backdrop, panel)
220
+ wrapper.appendChild(root)
221
+
222
+ const layer = createPluginLayer(wrapper, signal)
223
+ /** @type {SourceEditorSurface} */
224
+ let surface
225
+ const destroy = () => {
226
+ const active = activeEditors.get(wrapper)
227
+ if (active?.surface === surface) active.close()
228
+ root.remove()
229
+ if (owned?.get(kind) === surface) owned.delete(kind)
230
+ if (owned?.size === 0) surfaces.delete(wrapper)
231
+ }
232
+ surface = {
233
+ root,
234
+ backdrop,
235
+ panel,
236
+ title,
237
+ labelText,
238
+ field,
239
+ error,
240
+ cancel,
241
+ submit,
242
+ signal,
243
+ layer,
244
+ destroy,
245
+ }
246
+ owned.set(kind, surface)
247
+ signal.addEventListener('abort', destroy, { once: true })
248
+ return surface
249
+ }
250
+
251
+ /**
252
+ * @param {SourceEditorSurface} surface
253
+ * @param {boolean} visible
254
+ * @returns {void}
255
+ */
256
+ function setSurfaceVisible(surface, visible) {
257
+ surface.root.classList.toggle('oe-source-editor--preloaded', !visible)
258
+ surface.root.setAttribute('aria-hidden', String(!visible))
259
+ surface.root.inert = !visible
260
+ }
@@ -13,23 +13,23 @@ import { normalizeCarouselData } from '../../../shared/carouselData.js'
13
13
  import { setSanitizedRawHtml } from '../../../shared/sanitize/sanitizeRawHtml.js'
14
14
  import { setSafeUrlAttribute } from '../../../shared/sanitize/sanitizeUrl.js'
15
15
 
16
- const styles = new URL('./styles.css', import.meta.url).href
16
+ const styles = new URL('./styles.css', import.meta.url).href
17
17
 
18
- /**
19
- * Create the mixed-media carousel renderer and own its Carousel instances.
20
- * @param {string} classPrefix
21
- * @param {Record<string, import('../../../shared/localeTypes').LocaleValue>} locale
22
- * @returns {import('../../types').BlockRenderer<import('../../types').CarouselBlock>}
23
- */
24
- export function createCarouselRenderer(classPrefix, locale) {
18
+ /**
19
+ * Create the mixed-media carousel renderer and own its Carousel instances.
20
+ * @param {string} classPrefix
21
+ * @param {Record<string, import('../../../shared/localeTypes').LocaleValue>} locale
22
+ * @returns {import('../../types').BlockRenderer<import('../../types').CarouselBlock>}
23
+ */
24
+ export function createCarouselRenderer(classPrefix, locale) {
25
25
  const p = `${classPrefix}-carousel-block`
26
26
  /** @type {WeakMap<HTMLElement, Carousel>} */
27
- const instances = new WeakMap()
28
- /** @param {string} key @param {string} fallback */
29
- const t = (key, fallback) => {
30
- const value = locale[`renderer.carousel.${key}`]
31
- return typeof value === 'string' ? value : fallback
32
- }
27
+ const instances = new WeakMap()
28
+ /** @param {string} key @param {string} fallback */
29
+ const t = (key, fallback) => {
30
+ const value = locale[`renderer.carousel.${key}`]
31
+ return typeof value === 'string' ? value : fallback
32
+ }
33
33
 
34
34
  return {
35
35
  type: 'carousel',
@@ -41,10 +41,11 @@ export function createCarouselRenderer(classPrefix, locale) {
41
41
  root.className = p
42
42
  root.setAttribute('aria-label', t('label', 'Content carousel'))
43
43
  if (data.options.aspectRatio && data.options.aspectRatio !== 'auto') {
44
+ root.classList.add(`${p}--fixed-ratio`)
44
45
  root.style.aspectRatio = data.options.aspectRatio
45
46
  }
46
47
 
47
- const slides = data.slides.map(slide => {
48
+ const slides = data.slides.map(slide => {
48
49
  const content = () => {
49
50
  const figure = document.createElement('figure')
50
51
  figure.className = `${p}__slide`
@@ -80,14 +81,14 @@ export function createCarouselRenderer(classPrefix, locale) {
80
81
  ...(slide.type === 'image' && slide.src ? { thumb: slide.src } : {}),
81
82
  ...(slide.type === 'video' && slide.poster ? { thumb: slide.poster } : {}),
82
83
  meta: { id: slide.id, type: slide.type },
83
- }
84
- })
85
-
86
- // Preserve mode may intentionally pass a normalized empty carousel.
87
- // Avoid constructing a runtime instance with no navigable slides.
88
- if (!slides.length) return root
89
-
90
- const plugins = [createKeyboard({ scope: 'container' }), createSwipe({ mouse: true })]
84
+ }
85
+ })
86
+
87
+ // Preserve mode may intentionally pass a normalized empty carousel.
88
+ // Avoid constructing a runtime instance with no navigable slides.
89
+ if (!slides.length) return root
90
+
91
+ const plugins = [createKeyboard({ scope: 'container' }), createSwipe({ mouse: true })]
91
92
  if (data.options.navigation) plugins.push(createArrows())
92
93
  if (data.options.pagination) plugins.push(createDots())
93
94
  if (data.options.thumbnails) plugins.push(createThumbnails())
@@ -1,7 +1,13 @@
1
1
  .editor-carousel-block { position: relative; width: 100%; }
2
- .editor-carousel-block .carousel__viewport { border-radius: var(--er-radius); background: var(--er-surface); }
2
+ .editor-carousel-block .carousel__viewport { border-radius: var(--er-radius); }
3
3
  .editor-carousel-block__slide { display: grid; place-items: center; box-sizing: border-box; width: 100%; min-height: 16rem; margin: 0; }
4
- .editor-carousel-block__slide img, .editor-carousel-block__slide video { display: block; width: 100%; height: 100%; max-height: 36rem; object-fit: contain; }
4
+ .editor-carousel-block__slide img, .editor-carousel-block__slide video { display: block; width: 100%; height: auto; max-height: none; object-fit: contain; }
5
+ .editor-carousel-block--fixed-ratio .carousel__viewport { height: 100%; background: var(--er-surface); }
6
+ .editor-carousel-block--fixed-ratio .carousel,
7
+ .editor-carousel-block--fixed-ratio .carousel__track,
8
+ .editor-carousel-block--fixed-ratio .editor-carousel-block__slide { height: 100%; min-height: 0; }
9
+ .editor-carousel-block--fixed-ratio .editor-carousel-block__slide img,
10
+ .editor-carousel-block--fixed-ratio .editor-carousel-block__slide video { height: 100%; max-height: 100%; object-fit: contain; }
5
11
  .editor-carousel-block__html { box-sizing: border-box; width: 100%; padding: 1rem; }
6
12
  .editor-carousel-block figcaption { box-sizing: border-box; width: 100%; padding: .625rem .875rem; color: var(--er-text-2); font-size: .8125rem; text-align: center; }
7
13
  .editor-carousel-block .carousel__arrow:focus-visible, .editor-carousel-block .carousel__dot:focus-visible, .editor-carousel-block .carousel__thumb:focus-visible { outline: 2px solid var(--er-accent); outline-offset: 2px; }