@oxygen-cms/ui 1.9.1 → 2.1.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/package.json +1 -1
- package/src/CrudApi.js +39 -0
- package/src/PagesApi.js +2 -0
- package/src/PartialsApi.js +7 -0
- package/src/components/ContentResourceEdit.vue +980 -0
- package/src/components/{PageEdit.vue → PageEditWysiwyg.vue} +1 -1
- package/src/components/ResourceList.vue +8 -3
- package/src/components/VersionsDrawer.vue +386 -0
- package/src/components/content/PartialNodeView.vue +1 -1
- package/src/components/pages/CreatePageDropdown.vue +5 -5
- package/src/components/pages/PageActions.vue +67 -0
- package/src/components/pages/PageChooseParent.vue +170 -0
- package/src/components/pages/PageEdit.vue +149 -0
- package/src/components/pages/PageList.vue +1 -1
- package/src/components/{PageNestedRow.vue → pages/PageNestedRow.vue} +3 -3
- package/src/components/{PageStatusIcon.vue → pages/PageStatusIcon.vue} +1 -1
- package/src/components/{PageTable.vue → pages/PageTable.vue} +5 -5
- package/src/components/partials/CreatePartialDropdown.vue +1 -1
- package/src/components/{PartialActions.vue → partials/PartialActions.vue} +1 -1
- package/src/components/partials/PartialEdit.vue +49 -0
- package/src/components/{PartialList.vue → partials/PartialList.vue} +3 -3
- package/src/components/{PartialStatusIcon.vue → partials/PartialStatusIcon.vue} +1 -1
- package/src/components/{PartialTable.vue → partials/PartialTable.vue} +1 -1
- package/src/icons.js +11 -2
- package/src/modules/PagesPartials.js +10 -27
- package/src/components/LegacyPage.vue +0 -263
- package/src/components/PageActions.vue +0 -151
- /package/src/components/{PageNestedPagination.vue → pages/PageNestedPagination.vue} +0 -0
|
@@ -0,0 +1,980 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div v-hotkey="keymap" class="edit-container is-flex is-flex-direction-column" :class="{ 'is-fullscreen': isFullscreen }">
|
|
3
|
+
|
|
4
|
+
<!-- Version Warning Banner -->
|
|
5
|
+
<div v-if="!isFullscreen && !loading && editingNonHead" :class="(editOverrideConfirmed ? 'has-background-info-light' : 'has-background-warning') + ' px-4 py-4'">
|
|
6
|
+
<div class="is-flex is-align-items-center">
|
|
7
|
+
<b-icon icon="exclamation-triangle" class="mr-2"></b-icon>
|
|
8
|
+
You're viewing a version from {{ formatDate(model.updatedAt) }}. This is not the current version.
|
|
9
|
+
<b-button class="ml-2" type="is-text is-warning" size="is-small" icon-left="pencil-alt" @click="navigateToHeadVersion">Edit Latest</b-button>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<!-- Trashed Warning Banner -->
|
|
14
|
+
<div v-if="!isFullscreen && !loading && isTrashed" class="has-background-grey-light px-4 py-4">
|
|
15
|
+
<div class="is-flex is-align-items-center">
|
|
16
|
+
<b-icon icon="trash" class="mr-2"></b-icon>
|
|
17
|
+
This {{ displayName.toLowerCase() }} is in the trash. Editing trashed items is supported, but not recommended.
|
|
18
|
+
<b-button rounded class="ml-2" type="is-dark" size="is-small" icon-left="recycle" @click="restoreResource">Restore</b-button>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<!-- Header Bar -->
|
|
23
|
+
<div v-if="!isFullscreen" class="header-bar px-4 py-3 is-flex is-align-items-center" style="border-bottom: 1px solid #dbdbdb;">
|
|
24
|
+
<b-button rounded icon-left="angle-left" @click="goBack">Back</b-button>
|
|
25
|
+
|
|
26
|
+
<div class="mx-4 is-flex-grow-1">
|
|
27
|
+
<transition name="fade" mode="out-in">
|
|
28
|
+
<b-skeleton v-if="loading" key="skeleton" width="200px"></b-skeleton>
|
|
29
|
+
<div v-else-if="!editingTitle" key="display" class="title-display">
|
|
30
|
+
<span class="title is-4">{{ model.title || ('Untitled ' + displayName) }}</span>
|
|
31
|
+
<b-button rounded size="is-small" type="is-light" icon-left="pencil-alt" @click="startEditingTitle"></b-button>
|
|
32
|
+
<b-tag v-if="isHeadVersion" type="is-success" size="is-small" class="ml-2" icon="star">
|
|
33
|
+
Latest version
|
|
34
|
+
</b-tag>
|
|
35
|
+
<!-- SLOT: Resource-specific title tags (e.g., "Sent" tag for emails) -->
|
|
36
|
+
<slot
|
|
37
|
+
name="title-tags"
|
|
38
|
+
:model="model"
|
|
39
|
+
:is-dirty="isDirty"
|
|
40
|
+
:loading="loading"
|
|
41
|
+
></slot>
|
|
42
|
+
<b-tag v-if="isDirty" type="is-info">Unsaved changes</b-tag>
|
|
43
|
+
</div>
|
|
44
|
+
<b-field v-else key="editing" expanded class="title-editing">
|
|
45
|
+
<b-input
|
|
46
|
+
ref="titleInput"
|
|
47
|
+
v-model="editingTitleValue"
|
|
48
|
+
:placeholder="displayName + ' Title'"
|
|
49
|
+
expanded
|
|
50
|
+
class="mr-2"
|
|
51
|
+
@keyup.enter.native="finishEditingTitle"
|
|
52
|
+
@keyup.esc.native="cancelEditingTitle"
|
|
53
|
+
/>
|
|
54
|
+
<b-field>
|
|
55
|
+
<p class="control">
|
|
56
|
+
<b-button type="is-primary" @click="finishEditingTitle">Done</b-button>
|
|
57
|
+
</p>
|
|
58
|
+
<p class="control">
|
|
59
|
+
<b-button @click="cancelEditingTitle">Cancel</b-button>
|
|
60
|
+
</p>
|
|
61
|
+
</b-field>
|
|
62
|
+
</b-field>
|
|
63
|
+
</transition>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<div class="is-flex" style="gap: 0.5rem;">
|
|
67
|
+
<b-button
|
|
68
|
+
v-if="!loading && hasPublish"
|
|
69
|
+
:disabled="isPublished"
|
|
70
|
+
:type="isPublished ? '' : 'is-success'"
|
|
71
|
+
icon-left="globe-asia"
|
|
72
|
+
@click="isPublished ? null : publish()"
|
|
73
|
+
>
|
|
74
|
+
{{ isPublished ? 'Published' : 'Publish' }}
|
|
75
|
+
</b-button>
|
|
76
|
+
|
|
77
|
+
<!-- SLOT: Resource-specific top-bar actions (e.g., send button for emails) -->
|
|
78
|
+
<slot
|
|
79
|
+
name="top-bar-actions"
|
|
80
|
+
:model="model"
|
|
81
|
+
:is-published="isPublished"
|
|
82
|
+
:is-dirty="isDirty"
|
|
83
|
+
:loading="loading"
|
|
84
|
+
></slot>
|
|
85
|
+
|
|
86
|
+
<b-button
|
|
87
|
+
v-if="!loading"
|
|
88
|
+
icon-left="eye"
|
|
89
|
+
:disabled="isInViewMode"
|
|
90
|
+
@click="viewFullscreen"
|
|
91
|
+
>
|
|
92
|
+
View
|
|
93
|
+
</b-button>
|
|
94
|
+
<b-field>
|
|
95
|
+
<p class="control">
|
|
96
|
+
<b-button icon-left="cog" @click="openSettings">{{ settingsLabel || (displayName + ' Settings') }}</b-button>
|
|
97
|
+
</p>
|
|
98
|
+
<p v-if="hasVersions" class="control">
|
|
99
|
+
<b-button icon-left="history" @click="openVersionDrawer">Versions</b-button>
|
|
100
|
+
</p>
|
|
101
|
+
<p class="control">
|
|
102
|
+
<b-dropdown position="is-bottom-left" aria-role="menu">
|
|
103
|
+
<template #trigger>
|
|
104
|
+
<b-button icon-left="bars"></b-button>
|
|
105
|
+
</template>
|
|
106
|
+
|
|
107
|
+
<!-- SLOT: Resource-specific dropdown items (e.g., "View on Site" for pages) -->
|
|
108
|
+
<slot
|
|
109
|
+
name="dropdown-actions"
|
|
110
|
+
:model="model"
|
|
111
|
+
:is-published="isPublished"
|
|
112
|
+
></slot>
|
|
113
|
+
|
|
114
|
+
<b-dropdown-item v-if="hasVersions" aria-role="menuitem" @click="saveAsNewVersion">
|
|
115
|
+
<b-icon icon="plus"></b-icon>
|
|
116
|
+
New Version
|
|
117
|
+
</b-dropdown-item>
|
|
118
|
+
<b-dropdown-item v-if="!isTrashed" aria-role="menuitem" @click="deleteResource">
|
|
119
|
+
<b-icon icon="trash"></b-icon>
|
|
120
|
+
Delete
|
|
121
|
+
</b-dropdown-item>
|
|
122
|
+
<b-dropdown-item v-if="isTrashed" aria-role="menuitem" @click="forceDeleteResource">
|
|
123
|
+
<b-icon icon="trash"></b-icon>
|
|
124
|
+
Delete Forever
|
|
125
|
+
</b-dropdown-item>
|
|
126
|
+
<b-dropdown-item v-if="isTrashed" aria-role="menuitem" @click="restoreResource">
|
|
127
|
+
<b-icon icon="recycle"></b-icon>
|
|
128
|
+
Restore
|
|
129
|
+
</b-dropdown-item>
|
|
130
|
+
</b-dropdown>
|
|
131
|
+
</p>
|
|
132
|
+
</b-field>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<!-- SLOT: Inline settings (shown between toolbar and editor) -->
|
|
137
|
+
<slot
|
|
138
|
+
v-if="!isFullscreen"
|
|
139
|
+
name="inline-settings"
|
|
140
|
+
:model="model"
|
|
141
|
+
:is-dirty="isDirty"
|
|
142
|
+
:server-model="serverModel"
|
|
143
|
+
:loading="loading"
|
|
144
|
+
></slot>
|
|
145
|
+
|
|
146
|
+
<!-- Editor Toolbar -->
|
|
147
|
+
<div class="has-background-white-bis px-4 py-3 is-flex is-align-items-center" style="border-bottom: 1px solid #dbdbdb; gap: 1rem;">
|
|
148
|
+
<b-field class="mb-0">
|
|
149
|
+
<p class="control">
|
|
150
|
+
<b-button
|
|
151
|
+
:type="editorMode === 'code' ? 'is-dark' : ''"
|
|
152
|
+
:disabled="loading"
|
|
153
|
+
@click="switchEditorMode('code')"
|
|
154
|
+
>
|
|
155
|
+
Code
|
|
156
|
+
</b-button></p>
|
|
157
|
+
<p class="control">
|
|
158
|
+
<b-button
|
|
159
|
+
:type="editorMode === 'split' ? 'is-dark' : ''"
|
|
160
|
+
:disabled="loading"
|
|
161
|
+
@click="switchEditorMode('split')"
|
|
162
|
+
>
|
|
163
|
+
Split
|
|
164
|
+
</b-button></p>
|
|
165
|
+
<p class="control">
|
|
166
|
+
<b-button
|
|
167
|
+
:type="editorMode === 'preview' ? 'is-dark' : ''"
|
|
168
|
+
:disabled="loading"
|
|
169
|
+
@click="switchEditorMode('preview')"
|
|
170
|
+
>
|
|
171
|
+
Preview
|
|
172
|
+
</b-button></p>
|
|
173
|
+
</b-field>
|
|
174
|
+
|
|
175
|
+
<b-button icon-left="image" :disabled="loading || editorMode === 'preview'" @click="isMediaModalActive = true">
|
|
176
|
+
Insert Photo or File
|
|
177
|
+
</b-button>
|
|
178
|
+
|
|
179
|
+
<b-switch v-if="hasFullPagePreview" :value="renderLayout" :disabled="loading" size="is-small" @input="updateQueryParam('fullPage', $event)">
|
|
180
|
+
Preview full page
|
|
181
|
+
</b-switch>
|
|
182
|
+
|
|
183
|
+
<b-switch
|
|
184
|
+
:value="isFullscreen" :disabled="loading" size="is-small" @input="updateQueryParam('fullscreen', $event)"
|
|
185
|
+
>Focus</b-switch>
|
|
186
|
+
|
|
187
|
+
<div class="is-flex-grow-1"></div>
|
|
188
|
+
|
|
189
|
+
<b-field>
|
|
190
|
+
<p class="control">
|
|
191
|
+
<b-dropdown position="is-bottom-left" aria-role="menu" :disabled="loading">
|
|
192
|
+
<template #trigger>
|
|
193
|
+
<b-button :label="versionStrategyLabel" icon-right="caret-down" :disabled="loading || !isDirty" />
|
|
194
|
+
</template>
|
|
195
|
+
<b-dropdown-item aria-role="menuitem" @click="versionStrategy = 'guess'">
|
|
196
|
+
Create New Version if Needed
|
|
197
|
+
</b-dropdown-item>
|
|
198
|
+
<b-dropdown-item aria-role="menuitem" @click="versionStrategy = 'new'">
|
|
199
|
+
Save as New Version
|
|
200
|
+
</b-dropdown-item>
|
|
201
|
+
<b-dropdown-item aria-role="menuitem" @click="versionStrategy = 'overwrite'">
|
|
202
|
+
Overwrite Current Version
|
|
203
|
+
</b-dropdown-item>
|
|
204
|
+
</b-dropdown>
|
|
205
|
+
</p>
|
|
206
|
+
<p class="control">
|
|
207
|
+
<b-button
|
|
208
|
+
type="is-primary"
|
|
209
|
+
icon-left="save"
|
|
210
|
+
:loading="saving"
|
|
211
|
+
:disabled="loading || !isDirty"
|
|
212
|
+
@click="save"
|
|
213
|
+
>
|
|
214
|
+
Save
|
|
215
|
+
</b-button>
|
|
216
|
+
</p>
|
|
217
|
+
</b-field>
|
|
218
|
+
</div>
|
|
219
|
+
|
|
220
|
+
<!-- Fullscreen Loading -->
|
|
221
|
+
<b-loading :active="loading" :is-full-page="true"></b-loading>
|
|
222
|
+
|
|
223
|
+
<!-- Editor Area -->
|
|
224
|
+
<div class="editor-area is-flex-grow-1 is-relative">
|
|
225
|
+
<!-- Code Mode -->
|
|
226
|
+
<CodeEditor
|
|
227
|
+
v-if="editorMode === 'code'"
|
|
228
|
+
:key="'code-' + resourceId"
|
|
229
|
+
ref="codeEditor"
|
|
230
|
+
v-model="model.content"
|
|
231
|
+
lang="twig"
|
|
232
|
+
height="100%"
|
|
233
|
+
/>
|
|
234
|
+
|
|
235
|
+
<!-- Preview Mode -->
|
|
236
|
+
<iframe
|
|
237
|
+
v-if="editorMode === 'preview'"
|
|
238
|
+
ref="previewIframe"
|
|
239
|
+
:srcdoc="previewHtml"
|
|
240
|
+
class="preview-iframe"
|
|
241
|
+
frameborder="0"
|
|
242
|
+
></iframe>
|
|
243
|
+
|
|
244
|
+
<!-- Split Mode -->
|
|
245
|
+
<div v-if="editorMode === 'split'" class="split-mode is-flex">
|
|
246
|
+
<div class="split-left" style="flex: 1; border-right: 1px solid #dbdbdb;">
|
|
247
|
+
<CodeEditor
|
|
248
|
+
:key="'split-' + resourceId"
|
|
249
|
+
ref="splitCodeEditor"
|
|
250
|
+
v-model="model.content"
|
|
251
|
+
lang="twig"
|
|
252
|
+
height="100%"
|
|
253
|
+
/>
|
|
254
|
+
</div>
|
|
255
|
+
<div class="split-right" style="flex: 1;">
|
|
256
|
+
<iframe
|
|
257
|
+
ref="splitPreviewIframe"
|
|
258
|
+
:srcdoc="previewHtml"
|
|
259
|
+
class="preview-iframe"
|
|
260
|
+
frameborder="0"
|
|
261
|
+
></iframe>
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
<!-- Settings Drawer -->
|
|
268
|
+
<transition name="slide-right">
|
|
269
|
+
<div v-if="editSettingsModalActive" class="settings-drawer">
|
|
270
|
+
<div class="drawer-overlay" @click="updateQueryParam('settings', false)"></div>
|
|
271
|
+
<div class="drawer-content" :style="{ width: settingsDrawerWidth }">
|
|
272
|
+
<div class="drawer-header px-4 py-3 has-background-light" style="border-bottom: 1px solid #dbdbdb;">
|
|
273
|
+
<div class="is-flex is-align-items-center" style="gap: 0.5rem;">
|
|
274
|
+
<h2 class="title is-5 mb-0 is-flex-grow-1">{{ settingsLabel || (displayName + ' Settings') }}</h2>
|
|
275
|
+
<b-button
|
|
276
|
+
type="is-primary"
|
|
277
|
+
icon-left="save"
|
|
278
|
+
size="is-small"
|
|
279
|
+
:loading="saving"
|
|
280
|
+
:disabled="!isDirty"
|
|
281
|
+
@click="save"
|
|
282
|
+
>
|
|
283
|
+
Save
|
|
284
|
+
</b-button>
|
|
285
|
+
<b-button icon-left="times" size="is-small" @click="updateQueryParam('settings', false)">Close</b-button>
|
|
286
|
+
</div>
|
|
287
|
+
</div>
|
|
288
|
+
|
|
289
|
+
<div class="drawer-body is-relative">
|
|
290
|
+
<b-loading :active="loading" :is-full-page="false"></b-loading>
|
|
291
|
+
<div v-if="!loading" class="px-4 py-4">
|
|
292
|
+
<!-- SLOT: Resource-specific settings fields (e.g., slug for pages, key for partials) -->
|
|
293
|
+
<slot
|
|
294
|
+
name="settings-drawer-fields"
|
|
295
|
+
:model="model"
|
|
296
|
+
:is-dirty="isDirty"
|
|
297
|
+
:server-model="serverModel"
|
|
298
|
+
></slot>
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
302
|
+
</div>
|
|
303
|
+
</transition>
|
|
304
|
+
|
|
305
|
+
<!-- Media Insert Modal -->
|
|
306
|
+
<MediaInsertModal
|
|
307
|
+
:active="isMediaModalActive"
|
|
308
|
+
:multiselect-allowed="false"
|
|
309
|
+
@close="isMediaModalActive = false"
|
|
310
|
+
@select="onMediaInserted"
|
|
311
|
+
/>
|
|
312
|
+
|
|
313
|
+
<!-- Versions Drawer -->
|
|
314
|
+
<VersionsDrawer
|
|
315
|
+
v-if="hasVersions"
|
|
316
|
+
ref="versionsDrawer"
|
|
317
|
+
:active="isVersionDrawerActive"
|
|
318
|
+
:resource-id="resourceId"
|
|
319
|
+
:current-version-id="model ? model.id : null"
|
|
320
|
+
:resource-api="resourceApi"
|
|
321
|
+
:published-stage="publishedStage"
|
|
322
|
+
:has-version-actions="hasVersionActions"
|
|
323
|
+
:has-publish="hasPublish"
|
|
324
|
+
@update:active="updateQueryParam('versions', $event)"
|
|
325
|
+
@navigate="navigateToVersion"
|
|
326
|
+
@publish="onVersionPublish"
|
|
327
|
+
@make-head="onMakeHeadVersion"
|
|
328
|
+
>
|
|
329
|
+
<!-- Pass through the version-dropdown-actions slot from parent -->
|
|
330
|
+
<template #version-dropdown-actions="slotProps">
|
|
331
|
+
<slot name="version-dropdown-actions" v-bind="slotProps"></slot>
|
|
332
|
+
</template>
|
|
333
|
+
</VersionsDrawer>
|
|
334
|
+
</div>
|
|
335
|
+
</template>
|
|
336
|
+
|
|
337
|
+
<script>
|
|
338
|
+
import CodeEditor from './CodeEditor.vue';
|
|
339
|
+
import MediaInsertModal from './media/MediaInsertModal.vue';
|
|
340
|
+
import VersionsDrawer from './VersionsDrawer.vue';
|
|
341
|
+
import { morphToNotification, getApiHost } from '../api.js';
|
|
342
|
+
import { checkForUnsavedChanges } from '../unsavedChanges.js';
|
|
343
|
+
|
|
344
|
+
export default {
|
|
345
|
+
name: "ContentResourceEdit",
|
|
346
|
+
components: { CodeEditor, MediaInsertModal, VersionsDrawer },
|
|
347
|
+
beforeRouteLeave(to, from, next) {
|
|
348
|
+
checkForUnsavedChanges(this.model, this.serverModel, this.$buefy, next);
|
|
349
|
+
},
|
|
350
|
+
props: {
|
|
351
|
+
// API instance
|
|
352
|
+
resourceApi: {
|
|
353
|
+
type: Object,
|
|
354
|
+
required: true,
|
|
355
|
+
validator: (api) => {
|
|
356
|
+
// Ensure it has the CrudApi interface
|
|
357
|
+
return typeof api.get === 'function' &&
|
|
358
|
+
typeof api.update === 'function' &&
|
|
359
|
+
typeof api.listVersions === 'function';
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
|
|
363
|
+
// Display/routing configuration (flat props like ResourceList.vue)
|
|
364
|
+
displayName: {
|
|
365
|
+
type: String,
|
|
366
|
+
required: true // e.g., 'Page', 'Partial'
|
|
367
|
+
},
|
|
368
|
+
settingsLabel: {
|
|
369
|
+
type: String,
|
|
370
|
+
required: false, // e.g., 'TryBooking Settings' - defaults to '{displayName} Settings'
|
|
371
|
+
default: null
|
|
372
|
+
},
|
|
373
|
+
routePrefix: {
|
|
374
|
+
type: String,
|
|
375
|
+
required: true // e.g., 'pages', 'partials'
|
|
376
|
+
},
|
|
377
|
+
listRouteName: {
|
|
378
|
+
type: String,
|
|
379
|
+
required: true // e.g., 'pages.list'
|
|
380
|
+
},
|
|
381
|
+
trashRouteName: {
|
|
382
|
+
type: String,
|
|
383
|
+
required: true // e.g., 'pages.trash'
|
|
384
|
+
},
|
|
385
|
+
|
|
386
|
+
// Stage configuration
|
|
387
|
+
publishedStage: {
|
|
388
|
+
type: Number,
|
|
389
|
+
required: false, // The stage value that means "published" (optional if no publish)
|
|
390
|
+
default: null
|
|
391
|
+
},
|
|
392
|
+
|
|
393
|
+
// Feature flags (only for structural features that affect template)
|
|
394
|
+
hasSlug: {
|
|
395
|
+
type: Boolean,
|
|
396
|
+
default: false
|
|
397
|
+
},
|
|
398
|
+
hasHierarchy: {
|
|
399
|
+
type: Boolean,
|
|
400
|
+
default: false
|
|
401
|
+
},
|
|
402
|
+
hasVersionActions: {
|
|
403
|
+
type: Boolean,
|
|
404
|
+
default: false // Whether versions have resource-specific actions (e.g., "View on Site")
|
|
405
|
+
},
|
|
406
|
+
hasPublish: {
|
|
407
|
+
type: Boolean,
|
|
408
|
+
default: true // Whether this resource type supports publish/stage functionality
|
|
409
|
+
},
|
|
410
|
+
hasVersions: {
|
|
411
|
+
type: Boolean,
|
|
412
|
+
default: true // Whether this resource type supports versioning
|
|
413
|
+
},
|
|
414
|
+
hasFullPagePreview: {
|
|
415
|
+
type: Boolean,
|
|
416
|
+
default: false // Whether "Preview full page" toggle makes sense (Pages, Emails)
|
|
417
|
+
},
|
|
418
|
+
settingsDrawerWidth: {
|
|
419
|
+
type: String,
|
|
420
|
+
default: '500px' // Width of settings drawer (e.g., '1000px' for wider panels)
|
|
421
|
+
}
|
|
422
|
+
},
|
|
423
|
+
data() {
|
|
424
|
+
return {
|
|
425
|
+
loading: true,
|
|
426
|
+
saving: false,
|
|
427
|
+
model: {
|
|
428
|
+
content: '',
|
|
429
|
+
title: '',
|
|
430
|
+
slug: '',
|
|
431
|
+
slugPart: '',
|
|
432
|
+
description: '',
|
|
433
|
+
tags: [],
|
|
434
|
+
meta: '',
|
|
435
|
+
options: '{}',
|
|
436
|
+
stage: 0,
|
|
437
|
+
headVersion: null,
|
|
438
|
+
deletedAt: null
|
|
439
|
+
},
|
|
440
|
+
serverModel: null,
|
|
441
|
+
isMediaModalActive: false,
|
|
442
|
+
versions: [],
|
|
443
|
+
versionsLoading: false,
|
|
444
|
+
versionStrategy: 'guess',
|
|
445
|
+
editingNonHead: false,
|
|
446
|
+
editOverrideConfirmed: false,
|
|
447
|
+
previewHtml: '',
|
|
448
|
+
previewDebounceTimer: null,
|
|
449
|
+
editingTitle: false,
|
|
450
|
+
editingTitleValue: '',
|
|
451
|
+
isLoadingFromServer: false
|
|
452
|
+
}
|
|
453
|
+
},
|
|
454
|
+
computed: {
|
|
455
|
+
resourceId() {
|
|
456
|
+
return parseInt(this.$route.params.id);
|
|
457
|
+
},
|
|
458
|
+
isHeadVersion() {
|
|
459
|
+
// headVersion is null for head versions, or an object/ID for historical versions
|
|
460
|
+
if (!this.model) return false;
|
|
461
|
+
const hv = this.model.headVersion;
|
|
462
|
+
return hv === null || hv === undefined;
|
|
463
|
+
},
|
|
464
|
+
isPublished() {
|
|
465
|
+
if (!this.hasPublish || this.publishedStage === null) {
|
|
466
|
+
return false; // No publish functionality
|
|
467
|
+
}
|
|
468
|
+
return this.model && this.model.stage === this.publishedStage;
|
|
469
|
+
},
|
|
470
|
+
isTrashed() {
|
|
471
|
+
return this.model && this.model.deletedAt !== null && this.model.deletedAt !== undefined;
|
|
472
|
+
},
|
|
473
|
+
isDirty() {
|
|
474
|
+
// Delegate to the API class which knows which fields to check
|
|
475
|
+
return this.resourceApi.constructor.isDirty(this.model, this.serverModel);
|
|
476
|
+
},
|
|
477
|
+
needsEditConfirmation() {
|
|
478
|
+
return !this.isHeadVersion && !this.editOverrideConfirmed;
|
|
479
|
+
},
|
|
480
|
+
versionStrategyLabel() {
|
|
481
|
+
switch(this.versionStrategy) {
|
|
482
|
+
case 'new': return 'Save as New Version';
|
|
483
|
+
case 'overwrite': return 'Overwrite Current Version';
|
|
484
|
+
default: return 'Create New Version if Needed';
|
|
485
|
+
}
|
|
486
|
+
},
|
|
487
|
+
versionStrategyIcon() {
|
|
488
|
+
switch(this.versionStrategy) {
|
|
489
|
+
case 'new': return 'plus';
|
|
490
|
+
case 'overwrite': return 'save';
|
|
491
|
+
default: return 'caret-up';
|
|
492
|
+
}
|
|
493
|
+
},
|
|
494
|
+
parentSlug() {
|
|
495
|
+
if (!this.model) return '';
|
|
496
|
+
// If parent exists, return its slug, otherwise return empty string (root level)
|
|
497
|
+
return this.model.parent ? this.model.parent.slug : '';
|
|
498
|
+
},
|
|
499
|
+
defaultEditorMode() {
|
|
500
|
+
// Get from user preferences, default to 'split'
|
|
501
|
+
const preferredMode = this.$store.getters.userPreferences.get('editor.defaultMode');
|
|
502
|
+
if (preferredMode) {
|
|
503
|
+
return preferredMode;
|
|
504
|
+
}
|
|
505
|
+
return 'split';
|
|
506
|
+
},
|
|
507
|
+
isInViewMode() {
|
|
508
|
+
// View mode is: preview + fullPage + fullscreen
|
|
509
|
+
return this.editorMode === 'preview' && this.renderLayout === true && this.fullscreen;
|
|
510
|
+
},
|
|
511
|
+
// Query-string-based computed properties (single source of truth)
|
|
512
|
+
editorMode() {
|
|
513
|
+
const mode = this.$route.query.mode;
|
|
514
|
+
const validModes = ['code', 'split', 'preview'];
|
|
515
|
+
if (validModes.includes(mode)) {
|
|
516
|
+
return mode;
|
|
517
|
+
}
|
|
518
|
+
return this.defaultEditorMode;
|
|
519
|
+
},
|
|
520
|
+
renderLayout() {
|
|
521
|
+
return this.$route.query.fullPage === 'true';
|
|
522
|
+
},
|
|
523
|
+
isFullscreen() {
|
|
524
|
+
return this.$route.query.fullscreen === 'true';
|
|
525
|
+
},
|
|
526
|
+
isVersionDrawerActive() {
|
|
527
|
+
return this.$route.query.versions === 'true';
|
|
528
|
+
},
|
|
529
|
+
editSettingsModalActive() {
|
|
530
|
+
return this.$route.query.settings === 'true';
|
|
531
|
+
},
|
|
532
|
+
keymap() {
|
|
533
|
+
return {
|
|
534
|
+
'ctrl+s': this.saveHotkey,
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
},
|
|
538
|
+
watch: {
|
|
539
|
+
'model.content': {
|
|
540
|
+
handler(newVal) {
|
|
541
|
+
console.log('model.content changed, length:', newVal ? newVal.length : 0, 'mode:', this.editorMode, 'isLoadingFromServer:', this.isLoadingFromServer);
|
|
542
|
+
// Skip refresh if we're loading from server - the query watcher or mounted hook will handle it
|
|
543
|
+
if (this.isLoadingFromServer) {
|
|
544
|
+
console.log('Skipping refresh - loading from server');
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
if (this.editorMode === 'split' || this.editorMode === 'preview') {
|
|
548
|
+
this.debouncedRefreshPreview();
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
},
|
|
552
|
+
'$route.params.id'() {
|
|
553
|
+
// Reload when route changes (switching between versions)
|
|
554
|
+
this.editOverrideConfirmed = false; // Reset edit confirmation when switching
|
|
555
|
+
this.fetchData();
|
|
556
|
+
},
|
|
557
|
+
'$route.query': {
|
|
558
|
+
handler(newQuery, oldQuery) {
|
|
559
|
+
// Refresh preview when mode or fullPage changes (e.g., via browser back/forward)
|
|
560
|
+
const modeChanged = newQuery.mode !== oldQuery.mode;
|
|
561
|
+
const fullPageChanged = newQuery.fullPage !== oldQuery.fullPage;
|
|
562
|
+
|
|
563
|
+
if (modeChanged || fullPageChanged) {
|
|
564
|
+
if (this.editorMode === 'split' || this.editorMode === 'preview') {
|
|
565
|
+
this.refreshPreview();
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
},
|
|
569
|
+
deep: true
|
|
570
|
+
}
|
|
571
|
+
},
|
|
572
|
+
mounted() {
|
|
573
|
+
this.fetchData();
|
|
574
|
+
},
|
|
575
|
+
beforeDestroy() {
|
|
576
|
+
if (this.previewDebounceTimer) {
|
|
577
|
+
clearTimeout(this.previewDebounceTimer);
|
|
578
|
+
}
|
|
579
|
+
},
|
|
580
|
+
methods: {
|
|
581
|
+
async saveHotkey(event) {
|
|
582
|
+
event.preventDefault();
|
|
583
|
+
if (this.isDirty) {
|
|
584
|
+
await this.save();
|
|
585
|
+
}
|
|
586
|
+
},
|
|
587
|
+
async fetchData() {
|
|
588
|
+
this.loading = true;
|
|
589
|
+
this.isLoadingFromServer = true;
|
|
590
|
+
try {
|
|
591
|
+
const response = await this.resourceApi.get(this.resourceId);
|
|
592
|
+
this.setModel(response.item);
|
|
593
|
+
this.editingNonHead = !this.isHeadVersion;
|
|
594
|
+
// Trigger initial preview refresh after data loads
|
|
595
|
+
if (this.editorMode === 'split' || this.editorMode === 'preview') {
|
|
596
|
+
this.refreshPreview();
|
|
597
|
+
}
|
|
598
|
+
} catch (error) {
|
|
599
|
+
console.error('Failed to fetch resource:', error);
|
|
600
|
+
} finally {
|
|
601
|
+
this.isLoadingFromServer = false;
|
|
602
|
+
}
|
|
603
|
+
},
|
|
604
|
+
setModel(model) {
|
|
605
|
+
this.loading = false;
|
|
606
|
+
this.model = { ...model };
|
|
607
|
+
this.serverModel = { ...model };
|
|
608
|
+
},
|
|
609
|
+
async save() {
|
|
610
|
+
this.saving = true;
|
|
611
|
+
try {
|
|
612
|
+
// Strip parent field for hierarchical resources - it should only be updated via move operation
|
|
613
|
+
const saveData = { ...this.model };
|
|
614
|
+
if (this.hasHierarchy && saveData.parent !== undefined) {
|
|
615
|
+
delete saveData.parent;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const response = await this.resourceApi.update(saveData);
|
|
619
|
+
this.setModel(response.item);
|
|
620
|
+
this.$buefy.toast.open(morphToNotification(response));
|
|
621
|
+
} catch (error) {
|
|
622
|
+
console.error('Save failed:', error);
|
|
623
|
+
} finally {
|
|
624
|
+
this.saving = false;
|
|
625
|
+
}
|
|
626
|
+
},
|
|
627
|
+
async publish() {
|
|
628
|
+
if (!this.hasPublish) {
|
|
629
|
+
console.warn('Publish called but hasPublish is false');
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
try {
|
|
633
|
+
const updatedModel = await this.resourceApi.publish(this.model.id);
|
|
634
|
+
this.setModel(updatedModel);
|
|
635
|
+
// Refresh versions list if drawer is open
|
|
636
|
+
if (this.$refs.versionsDrawer && this.isVersionDrawerActive) {
|
|
637
|
+
await this.$refs.versionsDrawer.loadVersions();
|
|
638
|
+
}
|
|
639
|
+
} catch (error) {
|
|
640
|
+
console.error('Publish failed:', error);
|
|
641
|
+
}
|
|
642
|
+
},
|
|
643
|
+
async refreshPreview() {
|
|
644
|
+
if (!this.model) return;
|
|
645
|
+
|
|
646
|
+
try {
|
|
647
|
+
// Use the API endpoint for content preview
|
|
648
|
+
const url = getApiHost() + 'oxygen/api/' + this.routePrefix + '/content/' + this.model.id;
|
|
649
|
+
const response = await this.resourceApi.request('post')
|
|
650
|
+
.withJson({
|
|
651
|
+
content: this.model.content,
|
|
652
|
+
renderLayout: this.renderLayout
|
|
653
|
+
})
|
|
654
|
+
.fetchRaw(url);
|
|
655
|
+
|
|
656
|
+
this.previewHtml = await response.text();
|
|
657
|
+
} catch (error) {
|
|
658
|
+
console.error('Preview refresh failed:', error);
|
|
659
|
+
this.previewHtml = '<div style="padding: 2rem; color: red;">Preview failed to load</div>';
|
|
660
|
+
}
|
|
661
|
+
},
|
|
662
|
+
debouncedRefreshPreview() {
|
|
663
|
+
if (this.previewDebounceTimer) {
|
|
664
|
+
clearTimeout(this.previewDebounceTimer);
|
|
665
|
+
}
|
|
666
|
+
this.previewDebounceTimer = setTimeout(() => {
|
|
667
|
+
console.log('Debounced preview refresh triggered');
|
|
668
|
+
this.refreshPreview();
|
|
669
|
+
}, 1000);
|
|
670
|
+
},
|
|
671
|
+
onMediaInserted(files) {
|
|
672
|
+
if (files.length === 0) return;
|
|
673
|
+
|
|
674
|
+
const file = files[0];
|
|
675
|
+
const snippet = `{{ media('${file.slug}') }}`;
|
|
676
|
+
|
|
677
|
+
// Get the appropriate editor ref based on mode
|
|
678
|
+
const editorRef = this.editorMode === 'split' ? this.$refs.splitCodeEditor : this.$refs.codeEditor;
|
|
679
|
+
|
|
680
|
+
if (editorRef && editorRef.$refs.ace) {
|
|
681
|
+
const editor = editorRef.$refs.ace.editor;
|
|
682
|
+
editor.insert(snippet);
|
|
683
|
+
editor.focus();
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
this.isMediaModalActive = false;
|
|
687
|
+
},
|
|
688
|
+
toggleFullscreen() {
|
|
689
|
+
this.updateQueryParam('fullscreen', !this.isFullscreen);
|
|
690
|
+
},
|
|
691
|
+
openVersionDrawer() {
|
|
692
|
+
this.updateQueryParam('versions', true);
|
|
693
|
+
},
|
|
694
|
+
viewFullscreen() {
|
|
695
|
+
// Update query params to enter fullscreen preview mode with full page layout
|
|
696
|
+
const query = {
|
|
697
|
+
...this.$route.query,
|
|
698
|
+
fullscreen: 'true',
|
|
699
|
+
fullPage: 'true',
|
|
700
|
+
mode: 'preview',
|
|
701
|
+
versions: 'false'
|
|
702
|
+
};
|
|
703
|
+
this.$router.replace({ query }).catch(() => {});
|
|
704
|
+
},
|
|
705
|
+
saveAsNewVersion() {
|
|
706
|
+
this.versionStrategy = 'new';
|
|
707
|
+
this.save();
|
|
708
|
+
},
|
|
709
|
+
async deleteResource() {
|
|
710
|
+
// Soft delete - no confirmation needed
|
|
711
|
+
await this.resourceApi.deleteAndNotify(this.model.id);
|
|
712
|
+
await this.fetchData();
|
|
713
|
+
},
|
|
714
|
+
async forceDeleteResource() {
|
|
715
|
+
// Permanent delete - uses confirmForceDelete which has built-in confirmation
|
|
716
|
+
await this.resourceApi.confirmForceDelete(this.model.id);
|
|
717
|
+
this.$router.push({ name: this.listRouteName });
|
|
718
|
+
},
|
|
719
|
+
navigateToVersion(versionId, options = {}) {
|
|
720
|
+
const query = {};
|
|
721
|
+
|
|
722
|
+
// Handle versions drawer state
|
|
723
|
+
if (options.versions !== undefined) {
|
|
724
|
+
// Explicitly set versions state if provided
|
|
725
|
+
query.versions = options.versions.toString();
|
|
726
|
+
} else if (this.isVersionDrawerActive && !options.fullscreen) {
|
|
727
|
+
// Otherwise, preserve versions=true unless it's a fullscreen view action
|
|
728
|
+
query.versions = 'true';
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// Add any additional query params from options
|
|
732
|
+
if (options.fullscreen !== undefined) {
|
|
733
|
+
query.fullscreen = options.fullscreen.toString();
|
|
734
|
+
}
|
|
735
|
+
if (options.fullPage !== undefined) {
|
|
736
|
+
query.fullPage = options.fullPage.toString();
|
|
737
|
+
}
|
|
738
|
+
if (options.mode) {
|
|
739
|
+
query.mode = options.mode;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
this.$router.push({
|
|
743
|
+
name: this.routePrefix + '.edit',
|
|
744
|
+
params: { id: versionId },
|
|
745
|
+
query
|
|
746
|
+
});
|
|
747
|
+
},
|
|
748
|
+
navigateToHeadVersion() {
|
|
749
|
+
if (this.model.headVersion) {
|
|
750
|
+
this.$router.push({ name: this.routePrefix + '.edit', params: { id: this.model.headVersion } });
|
|
751
|
+
}
|
|
752
|
+
},
|
|
753
|
+
async onVersionPublish(versionId) {
|
|
754
|
+
if (!this.hasPublish) {
|
|
755
|
+
console.warn('onVersionPublish called but hasPublish is false');
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
await this.resourceApi.publish(versionId);
|
|
759
|
+
// Always refresh the current page since publishing one version might unpublish another
|
|
760
|
+
await this.fetchData();
|
|
761
|
+
},
|
|
762
|
+
async onMakeHeadVersion(versionId) {
|
|
763
|
+
await this.resourceApi.makeHeadVersion(versionId);
|
|
764
|
+
await this.fetchData();
|
|
765
|
+
},
|
|
766
|
+
formatDate(dateString) {
|
|
767
|
+
return new Date(dateString).toLocaleDateString();
|
|
768
|
+
},
|
|
769
|
+
updateQueryParam(key, value) {
|
|
770
|
+
const query = { ...this.$route.query };
|
|
771
|
+
// Handle different value types
|
|
772
|
+
if (typeof value === 'boolean') {
|
|
773
|
+
query[key] = value.toString();
|
|
774
|
+
} else if (typeof value === 'string') {
|
|
775
|
+
query[key] = value;
|
|
776
|
+
} else if (value === null || value === undefined) {
|
|
777
|
+
delete query[key];
|
|
778
|
+
} else {
|
|
779
|
+
query[key] = String(value);
|
|
780
|
+
}
|
|
781
|
+
// Only update if query actually changed
|
|
782
|
+
if (JSON.stringify(query) !== JSON.stringify(this.$route.query)) {
|
|
783
|
+
this.$router.replace({ query }).catch(() => {});
|
|
784
|
+
}
|
|
785
|
+
},
|
|
786
|
+
switchEditorMode(newMode) {
|
|
787
|
+
// Check if we need confirmation before switching to an editable mode
|
|
788
|
+
if (this.needsEditConfirmation && (newMode === 'code' || newMode === 'split')) {
|
|
789
|
+
this.$buefy.dialog.confirm({
|
|
790
|
+
title: 'Edit Historical Version',
|
|
791
|
+
message: `You are about to edit a historical version of this ${this.displayName.toLowerCase()}. This is not the current version. Are you sure you want to continue?`,
|
|
792
|
+
confirmText: 'Edit Anyway',
|
|
793
|
+
type: 'is-warning',
|
|
794
|
+
onConfirm: () => {
|
|
795
|
+
this.editOverrideConfirmed = true;
|
|
796
|
+
this.updateQueryParam('mode', newMode);
|
|
797
|
+
}
|
|
798
|
+
});
|
|
799
|
+
} else {
|
|
800
|
+
this.updateQueryParam('mode', newMode);
|
|
801
|
+
}
|
|
802
|
+
},
|
|
803
|
+
openSettings() {
|
|
804
|
+
// Check if we need confirmation before opening settings
|
|
805
|
+
if (this.needsEditConfirmation) {
|
|
806
|
+
this.$buefy.dialog.confirm({
|
|
807
|
+
title: 'Edit Historical Version',
|
|
808
|
+
message: `You are about to edit a historical version of this ${this.displayName.toLowerCase()}. This is not the current version. Are you sure you want to continue?`,
|
|
809
|
+
confirmText: 'Edit Anyway',
|
|
810
|
+
type: 'is-warning',
|
|
811
|
+
onConfirm: () => {
|
|
812
|
+
this.editOverrideConfirmed = true;
|
|
813
|
+
this.updateQueryParam('settings', true);
|
|
814
|
+
}
|
|
815
|
+
});
|
|
816
|
+
} else {
|
|
817
|
+
this.updateQueryParam('settings', true);
|
|
818
|
+
}
|
|
819
|
+
},
|
|
820
|
+
goBack() {
|
|
821
|
+
if (this.isTrashed) {
|
|
822
|
+
this.$router.push({ name: this.trashRouteName });
|
|
823
|
+
} else {
|
|
824
|
+
this.$router.push({ name: this.listRouteName });
|
|
825
|
+
}
|
|
826
|
+
},
|
|
827
|
+
async restoreResource() {
|
|
828
|
+
try {
|
|
829
|
+
await this.resourceApi.restoreAndNotify(this.model.id);
|
|
830
|
+
await this.fetchData();
|
|
831
|
+
} catch (error) {
|
|
832
|
+
console.error('Restore failed:', error);
|
|
833
|
+
}
|
|
834
|
+
},
|
|
835
|
+
startEditingTitle() {
|
|
836
|
+
this.editingTitleValue = this.model.title;
|
|
837
|
+
this.editingTitle = true;
|
|
838
|
+
this.$nextTick(() => {
|
|
839
|
+
if (this.$refs.titleInput) {
|
|
840
|
+
this.$refs.titleInput.focus();
|
|
841
|
+
}
|
|
842
|
+
});
|
|
843
|
+
},
|
|
844
|
+
finishEditingTitle() {
|
|
845
|
+
this.model.title = this.editingTitleValue;
|
|
846
|
+
this.editingTitle = false;
|
|
847
|
+
},
|
|
848
|
+
cancelEditingTitle() {
|
|
849
|
+
this.editingTitle = false;
|
|
850
|
+
this.editingTitleValue = '';
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
</script>
|
|
855
|
+
|
|
856
|
+
<style scoped>
|
|
857
|
+
.edit-container {
|
|
858
|
+
background: white;
|
|
859
|
+
height: 100%;
|
|
860
|
+
overflow-y: scroll;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
.edit-container.is-fullscreen {
|
|
864
|
+
position: fixed;
|
|
865
|
+
top: 0;
|
|
866
|
+
left: 0;
|
|
867
|
+
right: 0;
|
|
868
|
+
bottom: 0;
|
|
869
|
+
z-index: 9999;
|
|
870
|
+
background: white;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
.title-display {
|
|
874
|
+
display: flex;
|
|
875
|
+
align-items: center;
|
|
876
|
+
gap: 0.5rem;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
.title-display .title {
|
|
880
|
+
margin: 0;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
.title-editing {
|
|
884
|
+
display: flex;
|
|
885
|
+
gap: 0.5rem;
|
|
886
|
+
align-items: center;
|
|
887
|
+
max-width: 40rem;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
.editor-area {
|
|
891
|
+
min-height: 40rem;
|
|
892
|
+
overflow: hidden;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
.preview-iframe {
|
|
896
|
+
width: 100%;
|
|
897
|
+
height: 100%;
|
|
898
|
+
border: none;
|
|
899
|
+
background: white;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
.split-mode {
|
|
903
|
+
gap: 0;
|
|
904
|
+
height: 100%;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/* Fade transition for mode switching */
|
|
908
|
+
.fade-enter-active, .fade-leave-active {
|
|
909
|
+
transition: opacity 0.2s ease;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
.fade-enter, .fade-leave-to {
|
|
913
|
+
opacity: 0;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/* Settings drawer styles */
|
|
917
|
+
.settings-drawer {
|
|
918
|
+
position: fixed;
|
|
919
|
+
top: 0;
|
|
920
|
+
left: 0;
|
|
921
|
+
right: 0;
|
|
922
|
+
bottom: 0;
|
|
923
|
+
z-index: 40;
|
|
924
|
+
pointer-events: none;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
.settings-drawer .drawer-overlay {
|
|
928
|
+
position: absolute;
|
|
929
|
+
top: 0;
|
|
930
|
+
left: 0;
|
|
931
|
+
right: 0;
|
|
932
|
+
bottom: 0;
|
|
933
|
+
background: rgba(0, 0, 0, 0.5);
|
|
934
|
+
pointer-events: all;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
.settings-drawer .drawer-content {
|
|
938
|
+
position: absolute;
|
|
939
|
+
top: 0;
|
|
940
|
+
right: 0;
|
|
941
|
+
bottom: 0;
|
|
942
|
+
width: 500px;
|
|
943
|
+
max-width: 90vw;
|
|
944
|
+
background: white;
|
|
945
|
+
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1);
|
|
946
|
+
display: flex;
|
|
947
|
+
flex-direction: column;
|
|
948
|
+
pointer-events: all;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
.settings-drawer .drawer-header {
|
|
952
|
+
flex-shrink: 0;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
.settings-drawer .drawer-body {
|
|
956
|
+
flex: 1;
|
|
957
|
+
overflow-y: auto;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
/* Slide transition from right */
|
|
961
|
+
.slide-right-enter-active,
|
|
962
|
+
.slide-right-leave-active {
|
|
963
|
+
transition: transform 0.3s ease;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
.slide-right-enter-active .drawer-overlay,
|
|
967
|
+
.slide-right-leave-active .drawer-overlay {
|
|
968
|
+
transition: opacity 0.3s ease;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
.slide-right-enter .drawer-content,
|
|
972
|
+
.slide-right-leave-to .drawer-content {
|
|
973
|
+
transform: translateX(100%);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
.slide-right-enter .drawer-overlay,
|
|
977
|
+
.slide-right-leave-to .drawer-overlay {
|
|
978
|
+
opacity: 0;
|
|
979
|
+
}
|
|
980
|
+
</style>
|