diff --git a/locales/index.d.ts b/locales/index.d.ts index 440f24ac84..e80e40bcea 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -9276,6 +9276,14 @@ export interface Locale extends ILocale { * 特殊 */ "specialBlocks": string; + /** + * タイトルを入力 + */ + "inputTitleHere": string; + /** + * ここに移動 + */ + "moveToHere": string; "blocks": { /** * テキスト diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 5d8e1a5e72..3a14d73c0e 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -2446,6 +2446,8 @@ _pages: contentBlocks: "コンテンツ" inputBlocks: "入力" specialBlocks: "特殊" + inputTitleHere: "タイトルを入力" + moveToHere: "ここに移動" blocks: text: "テキスト" textarea: "テキストエリア" diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index e0473dce5e..8da6279d6b 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -246,9 +246,11 @@ import { isEnabledUrlPreview } from '@/instance.js'; import { getAppearNote } from '@/scripts/get-appear-note.js'; import { type Keymap } from '@/scripts/hotkey.js'; +type Tab = 'replies' | 'renotes' | 'reactions'; + const props = withDefaults(defineProps<{ note: Misskey.entities.Note; - initialTab: string; + initialTab?: Tab; }>(), { initialTab: 'replies', }); @@ -332,7 +334,7 @@ provide('react', (reaction: string) => { }); }); -const tab = ref(props.initialTab); +const tab = ref<Tab>(props.initialTab); const reactionTabType = ref<string | null>(null); const renotesPagination = computed<Paging>(() => ({ diff --git a/packages/frontend/src/components/global/MkStickyContainer.vue b/packages/frontend/src/components/global/MkStickyContainer.vue index 1aebf487bb..24f9333f74 100644 --- a/packages/frontend/src/components/global/MkStickyContainer.vue +++ b/packages/frontend/src/components/global/MkStickyContainer.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div ref="rootEl"> +<div ref="rootEl" :class="$style.root"> <div ref="headerEl" :class="$style.header"> <slot name="header"></slot> </div> @@ -84,8 +84,16 @@ defineExpose({ </script> <style lang='scss' module> +.root { + position: relative; + display: flex; + flex-direction: column; + height: 100%; +} + .body { position: relative; + flex-grow: 1; z-index: 0; --MI-stickyTop: v-bind("childStickyTop + 'px'"); --MI-stickyBottom: v-bind("childStickyBottom + 'px'"); @@ -93,12 +101,14 @@ defineExpose({ .header { position: sticky; + flex-shrink: 0; top: var(--MI-stickyTop, 0); z-index: 1; } .footer { position: sticky; + flex-shrink: 0; bottom: var(--MI-stickyBottom, 0); z-index: 1; } diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue index c3ad6657b0..a17a532edd 100644 --- a/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue +++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <!-- eslint-disable vue/no-mutating-props --> -<XContainer :draggable="true" @remove="() => emit('remove')"> +<XContainer :draggable="true" :blockId="modelValue.id" @remove="() => emit('remove')"> <template #header><i class="ti ti-photo"></i> {{ i18n.ts._pages.blocks.image }}</template> <template #func> <button @click="choose()"> diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue index 36e03b4790..18057c01b5 100644 --- a/packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue +++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <!-- eslint-disable vue/no-mutating-props --> -<XContainer :draggable="true" @remove="() => emit('remove')"> +<XContainer :draggable="true" :blockId="modelValue.id" @remove="() => emit('remove')"> <template #header><i class="ti ti-note"></i> {{ i18n.ts._pages.blocks.note }}</template> <section style="padding: 16px;" class="_gaps_s"> diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue index 3fed07f7e8..6d189ba134 100644 --- a/packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue +++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <!-- eslint-disable vue/no-mutating-props --> -<XContainer :draggable="true" @remove="() => emit('remove')"> +<XContainer :draggable="true" :blockId="modelValue.id" @remove="() => emit('remove')"> <template #header><i class="ti ti-note"></i> {{ props.modelValue.title }}</template> <template #func> <button class="_button" @click="rename()"> @@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> - + import { defineAsyncComponent, inject, onMounted, watch, ref } from 'vue'; import * as Misskey from 'misskey-js'; import { v4 as uuid } from 'uuid'; diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue index 5795b46c00..238e41bc83 100644 --- a/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue +++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <!-- eslint-disable vue/no-mutating-props --> -<XContainer :draggable="true" @remove="() => emit('remove')"> +<XContainer :draggable="true" :blockId="modelValue.id" @remove="() => emit('remove')"> <template #header><i class="ti ti-align-left"></i> {{ i18n.ts._pages.blocks.text }}</template> <section> @@ -15,7 +15,6 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> - import { watch, ref, shallowRef, onMounted, onUnmounted } from 'vue'; import * as Misskey from 'misskey-js'; import XContainer from '../page-editor.container.vue'; diff --git a/packages/frontend/src/pages/page-editor/page-editor.blocks.vue b/packages/frontend/src/pages/page-editor/page-editor.blocks.vue index f191320180..65999c3cc0 100644 --- a/packages/frontend/src/pages/page-editor/page-editor.blocks.vue +++ b/packages/frontend/src/pages/page-editor/page-editor.blocks.vue @@ -2,21 +2,53 @@ SPDX-FileCopyrightText: syuilo and misskey-project SPDX-License-Identifier: AGPL-3.0-only --> - <template> -<Sortable :modelValue="modelValue" tag="div" itemKey="id" handle=".drag-handle" :group="{ name: 'blocks' }" :animation="150" :swapThreshold="0.5" @update:modelValue="v => emit('update:modelValue', v)"> - <template #item="{element}"> - <div :class="$style.item"> - <!-- divが無いとエラーになる https://github.com/SortableJS/vue.draggable.next/issues/189 --> - <component :is="getComponent(element.type)" :modelValue="element" @update:modelValue="updateItem" @remove="() => removeItem(element)"/> +<div + @dragstart.capture="dragStart" + @dragend.capture="dragEnd" + @drop.capture="dragEnd" +> + <div + data-after-id="__FIRST__" + :class="[$style.insertBetweenRoot, { + [$style.insertBetweenDraggingOver]: draggingOverAfterId === '__FIRST__' && draggingBlockId !== modelValue[0]?.id, + }]" + @dragover="insertBetweenDragOver($event, '__FIRST__')" + @dragleave="insertBetweenDragLeave" + @drop="insertBetweenDrop($event, '__FIRST__')" + > + <div :class="$style.insertBetweenBorder"></div> + <span :class="$style.insertBetweenText">{{ i18n.ts._pages.moveToHere }}</span> + </div> + + <div v-for="block, index in modelValue" :key="block.id" :class="$style.item"> + <!-- divが無いとエラーになる https://github.com/SortableJS/vue.draggable.next/issues/189 --> + <component + :is="getComponent(block.type)" + :modelValue="block" + @update:modelValue="updateItem" + @remove="() => removeItem(block)" + /> + <div + :data-after-id="block.id" + :class="[$style.insertBetweenRoot, { + [$style.insertBetweenDraggingOver]: draggingOverAfterId === block.id && draggingBlockId !== block.id && draggingBlockId !== modelValue[index + 1]?.id, + }]" + @dragover="insertBetweenDragOver($event, block.id, modelValue[index + 1]?.id)" + @dragleave="insertBetweenDragLeave" + @drop="insertBetweenDrop($event, block.id, modelValue[index + 1]?.id)" + > + <div :class="$style.insertBetweenBorder"></div> + <span :class="$style.insertBetweenText">{{ i18n.ts._pages.moveToHere }}</span> </div> - </template> -</Sortable> + </div> +</div> </template> <script lang="ts" setup> -import { defineAsyncComponent } from 'vue'; +import { ref } from 'vue'; import * as Misskey from 'misskey-js'; +import { i18n } from '@/i18n.js'; import XSection from './els/page-editor.el.section.vue'; import XText from './els/page-editor.el.text.vue'; import XImage from './els/page-editor.el.image.vue'; @@ -32,8 +64,6 @@ function getComponent(type: string) { } } -const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default)); - const props = defineProps<{ modelValue: Misskey.entities.Page['content']; }>(); @@ -42,7 +72,75 @@ const emit = defineEmits<{ (ev: 'update:modelValue', value: Misskey.entities.Page['content']): void; }>(); -function updateItem(v) { +const isDragging = ref(false); +const draggingOverAfterId = ref<string | null>(null); +const draggingBlockId = ref<string | null>(null); + +function dragStart(ev: DragEvent) { + if (ev.target instanceof HTMLElement) { + const blockId = ev.target.dataset.blockId; + if (blockId != null) { + console.log('dragStart', blockId); + ev.dataTransfer!.setData('text/plain', blockId); + isDragging.value = true; + draggingBlockId.value = blockId; + } + } +} + +function dragEnd() { + isDragging.value = false; + draggingBlockId.value = null; +} + +function insertBetweenDragOver(ev: DragEvent, id: string, nextId?: string) { + if (draggingBlockId.value === id || draggingBlockId.value === nextId) return; + + ev.preventDefault(); + if (ev.target instanceof HTMLElement) { + const afterId = ev.target.dataset.afterId; + if (afterId != null) { + draggingOverAfterId.value = afterId; + } + } +} + +function insertBetweenDragLeave() { + draggingOverAfterId.value = null; +} + +function insertBetweenDrop(ev: DragEvent, id: string, nextId?: string) { + if (draggingBlockId.value === id || draggingBlockId.value === nextId) return; + + ev.preventDefault(); + if (ev.target instanceof HTMLElement) { + const afterId = ev.target.dataset.afterId; // insert after this + const moveId = ev.dataTransfer?.getData('text/plain'); + if (afterId != null && moveId != null) { + const oldValue = props.modelValue.filter((x) => x.id !== moveId); + const afterIdAt = afterId === '__FIRST__' ? 0 : oldValue.findIndex((x) => x.id === afterId); + const movingBlock = props.modelValue.find((x) => x.id === moveId); + if (afterId === '__FIRST__' && movingBlock != null) { + const newValue = [ + movingBlock, + ...oldValue, + ]; + emit('update:modelValue', newValue); + } else if (afterIdAt >= 0 && movingBlock != null) { + const newValue = [ + ...oldValue.slice(0, afterIdAt + 1), + movingBlock, + ...oldValue.slice(afterIdAt + 1), + ]; + emit('update:modelValue', newValue); + } + } + } + isDragging.value = false; + draggingOverAfterId.value = null; +} + +function updateItem(v: Misskey.entities.PageBlock) { const i = props.modelValue.findIndex(x => x.id === v.id); const newValue = [ ...props.modelValue.slice(0, i), @@ -52,8 +150,8 @@ function updateItem(v) { emit('update:modelValue', newValue); } -function removeItem(el) { - const i = props.modelValue.findIndex(x => x.id === el.id); +function removeItem(v: Misskey.entities.PageBlock) { + const i = props.modelValue.findIndex(x => x.id === v.id); const newValue = [ ...props.modelValue.slice(0, i), ...props.modelValue.slice(i + 1), @@ -63,9 +161,51 @@ function removeItem(el) { </script> <style lang="scss" module> -.item { - & + .item { - margin-top: 16px; +.insertBetweenRoot { + height: calc(var(--MI-margin) * 2); + width: 100%; + border-radius: 2px; + position: relative; +} + +.insertBetweenBorder { + position: absolute; + top: 50%; + left: 0; + transform: translateY(-50%); + height: 4px; + width: 100%; + border-radius: 2px; + background-color: var(--MI_THEME-accent); + display: none; +} + +.insertBetweenText { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: var(--MI_THEME-fgOnAccent); + padding: 0 14px; + line-height: 24px; + border-radius: 999px; + display: none; + background-color: var(--MI_THEME-accent); +} + +.insertBetweenBorder, +.insertBetweenText { + pointer-events: none; +} + +.insertBetweenDraggingOver { + padding: 10px 0; + + .insertBetweenBorder { + display: block; + } + .insertBetweenText { + display: inline-block; } } </style> diff --git a/packages/frontend/src/pages/page-editor/page-editor.container.vue b/packages/frontend/src/pages/page-editor/page-editor.container.vue index a96c2c2a77..1bc0494ce9 100644 --- a/packages/frontend/src/pages/page-editor/page-editor.container.vue +++ b/packages/frontend/src/pages/page-editor/page-editor.container.vue @@ -4,24 +4,35 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div class="cpjygsrt"> - <header> - <div class="title"><slot name="header"></slot></div> - <div class="buttons"> - <slot name="func"></slot> - <button v-if="removable" class="_button" @click="remove()"> +<div + :class="[$style.blockContainerRoot, { + [$style.dragging]: isDragging, + [$style.draggingOver]: isDraggingOver, + }]" + @dragover="dragOver" + @dragleave="dragLeave" + @drop="drop" +> + <header :class="$style.blockContainerHeader"> + <div :class="$style.title"><slot name="header"></slot></div> + <div :class="$style.buttons"> + <button v-if="removable" :class="$style.blockContainerActionButton" class="_button" @click="remove()"> <i class="ti ti-trash"></i> </button> - <button v-if="draggable" class="drag-handle _button"> + <button + v-if="draggable" + draggable="true" + :class="$style.blockContainerActionButton" + class="_button" + :data-block-id="blockId" + @dragstart="dragStart" + @dragend="dragEnd" + > <i class="ti ti-menu-2"></i> - </button> - <button class="_button" @click="toggleContent(!showBody)"> - <template v-if="showBody"><i class="ti ti-chevron-up"></i></template> - <template v-else><i class="ti ti-chevron-down"></i></template> - </button> + </button> </div> </header> - <div v-show="showBody" class="body"> + <div :class="$style.blockContainerBody" tabindex="0"> <slot></slot> </div> </div> @@ -31,6 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref } from 'vue'; const props = withDefaults(defineProps<{ + blockId: string; expanded?: boolean; removable?: boolean; draggable?: boolean; @@ -40,24 +52,80 @@ const props = withDefaults(defineProps<{ }); const emit = defineEmits<{ - (ev: 'toggle', show: boolean): void; (ev: 'remove'): void; }>(); -const showBody = ref(props.expanded); - -function toggleContent(show: boolean) { - showBody.value = show; - emit('toggle', show); -} - function remove() { emit('remove'); } + +const isDragging = ref(false); +function dragStart(ev: DragEvent) { + ev.dataTransfer?.setData('text/plain', props.blockId); + isDragging.value = true; +} +function dragEnd() { + isDragging.value = false; +} + +const isDraggingOver = ref(false); +function dragOver(ev: DragEvent) { + if (isDragging.value) { + // ブロックの中にドロップできるのは自分自身だけ + ev.preventDefault(); + isDraggingOver.value = true; + } +} +function dragLeave() { + isDraggingOver.value = false; +} +function drop() { + // 自分自身しかドロップできないので何もしない + isDraggingOver.value = false; +} </script> -<style lang="scss" scoped> -.cpjygsrt { +<style lang="scss" module> +.blockContainerRoot { + position: relative; +} + +.blockContainerHeader { + position: absolute; + box-sizing: border-box; + top: 0; + right: 0; + transform: translateY(-100%); + z-index: 1; + display: none; + gap: var(--MI-margin); + + height: 42px; + padding: 6px 14px; + background-color: var(--MI_THEME-panel); + border: 2px solid var(--MI_THEME-accent); + border-bottom: none; + border-radius: 8px 8px 0 0; + + > .title { + line-height: 26px; + } + + > .buttons { + display: flex; + gap: 8px; + } +} + +.blockContainerActionButton { + display: block; + width: 26px; + height: 26px; + line-height: 26px; + text-align: center; +} + +.blockContainerBody { position: relative; overflow: hidden; background: var(--MI_THEME-panel); @@ -67,62 +135,45 @@ function remove() { &:hover { border: solid 2px var(--MI_THEME-X13); } +} - &.warn { - border: solid 2px #dec44c; +.blockContainerRoot.dragging { + &::after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: var(--MI_THEME-bg); + z-index: 1; + border-radius: 8px 0 8px 8px; } - &.error { - border: solid 2px #f00; + &.draggingOver::after { + outline: dashed 2px var(--MI_THEME-accent); + outline-offset: -2px; } - > header { - > .title { - z-index: 1; - margin: 0; - padding: 0 16px; - line-height: 42px; - font-size: 0.9em; - font-weight: bold; - box-shadow: 0 1px rgba(#000, 0.07); + .blockContainerHeader { + display: flex; + } - > i { - margin-right: 6px; - } + .blockContainerBody { + border: solid 2px var(--MI_THEME-accent); + border-top-right-radius: 0; + } +} - &:empty { - display: none; - } +@container (min-width: 700px) { + .blockContainerRoot:focus-within { + .blockContainerHeader { + display: flex; } - > .buttons { - position: absolute; - z-index: 2; - top: 0; - right: 0; - - > button { - padding: 0; - width: 42px; - font-size: 0.9em; - line-height: 42px; - } - - .drag-handle { - cursor: move; - } - } - } - - > .body { - ::v-deep(.juejbjww), ::v-deep(.eiipwacr) { - &:not(.inline):first-child { - margin-top: 28px; - } - - &:not(.inline):last-child { - margin-bottom: 20px; - } + .blockContainerBody { + border: solid 2px var(--MI_THEME-accent); + border-top-right-radius: 0; } } } diff --git a/packages/frontend/src/pages/page-editor/page-editor.vue b/packages/frontend/src/pages/page-editor/page-editor.vue index e8059dbecd..435221a2a3 100644 --- a/packages/frontend/src/pages/page-editor/page-editor.vue +++ b/packages/frontend/src/pages/page-editor/page-editor.vue @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-if="fetchStatus === 'loading'"> <MkLoading/> </div> - <div v-else-if="fetchStatus === 'done' && page != null" :class="$style.pageMain"> + <div v-else-if="fetchStatus === 'done' && page != null" class="_gaps" :class="$style.pageMain"> <div :class="$style.pageBanner"> <div v-if="page?.eyeCatchingImageId" :class="$style.pageBannerImage"> <MkMediaImage @@ -22,22 +22,32 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </div> <div :class="$style.pageBannerTitle" class="_gaps_s"> - <h1></h1> + <input v-model="title" :class="$style.titleForm" :placeholder="i18n.ts._pages.inputTitleHere"/> <div :class="$style.pageBannerTitleSub"> <div v-if="page?.user" :class="$style.pageBannerTitleUser"> - <MkAvatar :user="page.user" :class="$style.avatar" indicator link preview/> <MkA :to="`/@${username}`"><MkUserName :user="page.user" :nowrap="false"/></MkA> + <MkAvatar :user="page.user" :class="$style.avatar" indicator/> <MkUserName :user="page.user" :nowrap="false"/> </div> <div :class="$style.pageBannerTitleSubActions"> - <MkA v-if="page?.userId === $i?.id" v-tooltip="i18n.ts._pages.editThisPage" :to="`/pages/edit/${page.id}`" class="_button" :class="$style.generalActionButton"><i class="ti ti-pencil ti-fw"></i></MkA> - <button v-tooltip="i18n.ts.share" class="_button" :class="$style.generalActionButton" @click="share"><i class="ti ti-share ti-fw"></i></button> </div> </div> </div> + <div :class="$style.pageContent"> + <XBlocks v-model="content"/> + </div> </div> <div v-else-if="fetchStatus === 'notMe'" class="_fullInfo"> This page is not yours </div> </MkSpacer> + <template #footer> + <div :class="$style.footer"> + <div class="_buttons" :class="$style.footerInner"> + <MkButton primary @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> + <MkButton @click="show"><i class="ti ti-eye"></i> {{ i18n.ts.show }}</MkButton> + <MkButton v-if="initPageId != null" danger @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> + </div> + </div> + </template> </MkStickyContainer> </template> @@ -69,6 +79,18 @@ const $i = signinRequired(); const fetchStatus = ref<'loading' | 'done' | 'notMe'>('loading'); const page = ref<Partial<Misskey.entities.Page> | null>(null); +const title = computed({ + get: () => page.value?.title ?? '', + set: (value) => { + if (page.value) { + page.value.title = value; + } else { + page.value = { + title: value, + }; + } + }, +}); const content = computed<Misskey.entities.Page['content']>({ get: () => page.value?.content ?? [], set: (value) => { @@ -82,6 +104,22 @@ const content = computed<Misskey.entities.Page['content']>({ }, }); +function onTitleUpdated(ev: Event) { + title.value = (ev.target as HTMLDivElement).innerText; +} + +async function save() { + +} + +async function show() { + +} + +async function del() { + +} + async function init() { if (props.initPageId) { const _page = await misskeyApi('pages/show', { @@ -91,6 +129,7 @@ async function init() { fetchStatus.value = 'notMe'; return; } + page.value = _page; } if (page.value === null) { @@ -127,7 +166,7 @@ definePageMetadata(() => ({ .pageBanner { width: calc(100% + 4rem); - margin: -2rem -2rem 1.5rem; + margin: -2rem -2rem 0.5rem; border-radius: var(--MI-radius) var(--MI-radius) 0 0; overflow: hidden; position: relative; @@ -149,11 +188,36 @@ definePageMetadata(() => ({ .pageBannerTitle { position: relative; - h1 { + .titleForm { + appearance: none; + -webkit-appearance: none; + box-sizing: border-box; + display: block; + padding: 6px 12px; + font: inherit; font-size: 2rem; font-weight: 700; color: var(--MI_THEME-fg); margin: 0; + border: none; + border-bottom: 2px solid var(--MI_THEME-divider); + transition: border-color 0.1s ease-out; + background-color: var(--MI_THEME-bg); + border-radius: var(--MI-radius) var(--MI-radius) 0 0; + + &:hover { + border-color: var(--MI_THEME-inputBorderHover); + } + + &:focus { + outline: none; + border-color: var(--MI_THEME-accent); + } + + &:focus-visible { + outline: 2px solid var(--MI_THEME-focus); + outline-offset: -2px; + } } .pageBannerTitleSub { @@ -181,4 +245,16 @@ definePageMetadata(() => ({ margin-left: auto; } } + +.footer { + backdrop-filter: var(--MI-blur, blur(15px)); + background: var(--MI_THEME-acrylicBg); + border-top: solid .5px var(--MI_THEME-divider); +} + +.footerInner { + padding: 16px; + margin: 0 auto; + max-width: 800px; +} </style>