diff options
| author | Acid Chicken (硫酸鶏) <root@acid-chicken.com> | 2023-06-01 17:19:46 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-06-01 17:19:46 +0900 |
| commit | 337dd97b490fb6bcfc351566a4fd80c35a9cda14 (patch) | |
| tree | 5dc95fc8617a22f713dcf8511398f4100d96c5a9 /packages/frontend | |
| parent | Update index.d.ts (diff) | |
| download | misskey-337dd97b490fb6bcfc351566a4fd80c35a9cda14.tar.gz misskey-337dd97b490fb6bcfc351566a4fd80c35a9cda14.tar.bz2 misskey-337dd97b490fb6bcfc351566a4fd80c35a9cda14.zip | |
perf(#10923): CSS Modules のクラス名をインライン化する (#10930)
* perf(#10923): unwind css module class name
* perf(#10923): support multiple components
* refactor: clean up
* refactor(#10923): avoid `useCssModule()`
* fix(#10923): allow direct literal class name
* fix(#10923): avoid computed class name
* fix(#10923): allow literal keys
* fix(#10923): typo
* fix(#10923): invalid class names
* chore: test
* revert: test
This reverts commit 5c7ef366eceebe8ba260efa4d5d675f6c1775c45.
* fix(#10923): hidden tale
* perf(#10923): also unwind scoped css contained components
* perf(#10923): `normalizeClass` AOT compilation
---------
Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
Diffstat (limited to 'packages/frontend')
52 files changed, 1077 insertions, 183 deletions
diff --git a/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts b/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts new file mode 100644 index 0000000000..3929bf0608 --- /dev/null +++ b/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts @@ -0,0 +1,597 @@ +import { parse } from 'acorn'; +import { generate } from 'astring'; +import { describe, expect, it } from 'vitest'; +import { normalizeClass, unwindCssModuleClassName } from './rollup-plugin-unwind-css-module-class-name'; +import type * as estree from 'estree'; + +function parseExpression(code: string): estree.Expression { + const program = parse(code, { ecmaVersion: 'latest', sourceType: 'module' }) as unknown as estree.Program; + const statement = program.body[0] as estree.ExpressionStatement; + return statement.expression; +} + +describe(normalizeClass.name, () => { + it('should normalize string', () => { + expect(normalizeClass(parseExpression('"a b c"'))).toBe('a b c'); + }); + it('should trim redundant spaces', () => { + expect(normalizeClass(parseExpression('" a b c "'))).toBe('a b c'); + }); + it('should ignore undefined', () => { + expect(normalizeClass(parseExpression('undefined'))).toBe(''); + }); + it('should ignore non string literals', () => { + expect(normalizeClass(parseExpression('0'))).toBe(''); + expect(normalizeClass(parseExpression('true'))).toBe(''); + expect(normalizeClass(parseExpression('null'))).toBe(''); + expect(normalizeClass(parseExpression('/I.D/'))).toBe(''); + }); + it('should not normalize identifiers', () => { + expect(normalizeClass(parseExpression('EScape'))).toBeNull(); + }); + it('should normalize recursively array', () => { + expect(normalizeClass(parseExpression('["from", ...["Utopia"]]'))).toBe('from Utopia'); + expect(normalizeClass(parseExpression('["from", ...[Utopia]]'))).toBeNull(); + }); + it('should normalize recursively template literal', () => { + expect(normalizeClass(parseExpression('`name ${"shiho"} code ${33}`'))).toBe('name shiho code'); + expect(normalizeClass(parseExpression('`name ${shiho.name} code ${33}`'))).toBeNull(); + }); + it('should normalize recursively binary expression', () => { + expect(normalizeClass(parseExpression('"mirage" + "mirror"'))).toBe('miragemirror'); + expect(normalizeClass(parseExpression('"mirage" + mirror'))).toBeNull(); + }); + it('should normalize recursively object expression', () => { + expect(normalizeClass(parseExpression('({ a: true, b: "c" })'))).toBe('a b'); + expect(normalizeClass(parseExpression('({ a: false, b: "c" })'))).toBe('b'); + expect(normalizeClass(parseExpression('({ a: true, b: c })'))).toBeNull(); + expect(normalizeClass(parseExpression('({ a: true, b: "c", ...({ d: true }) })'))).toBe('a b d'); + expect(normalizeClass(parseExpression('({ a: true, [b]: "c" })'))).toBeNull(); + expect(normalizeClass(parseExpression('({ a: true, b: false, c: !false, d: !!0 })'))).toBe('a c'); + }); +}); + +it('Composition API (standard)', () => { + const ast = parse(` +import { c as api, d as defaultStore, i as i18n, aD as notePage, bN as ImgWithBlurhash, bY as getStaticImageUrl, _ as _export_sfc } from './app-!~{001}~.js'; +import { M as MkContainer } from './MkContainer-!~{03M}~.js'; +import { b as defineComponent, a as ref, e as onMounted, z as resolveComponent, g as openBlock, h as createBlock, i as withCtx, K as createTextVNode, E as toDisplayString, u as unref, l as createBaseVNode, q as normalizeClass, B as createCommentVNode, k as createElementBlock, F as Fragment, C as renderList, A as createVNode } from './vue-!~{002}~.js'; +import './photoswipe-!~{003}~.js'; + +const _hoisted_1 = /* @__PURE__ */ createBaseVNode("i", { class: "ti ti-photo" }, null, -1); +const _sfc_main = /* @__PURE__ */ defineComponent({ + __name: "index.photos", + props: { + user: {} + }, + setup(__props) { + const props = __props; + let fetching = ref(true); + let images = ref([]); + function thumbnail(image) { + return defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(image.url) : image.thumbnailUrl; + } + onMounted(() => { + const image = [ + "image/jpeg", + "image/webp", + "image/avif", + "image/png", + "image/gif", + "image/apng", + "image/vnd.mozilla.apng" + ]; + api("users/notes", { + userId: props.user.id, + fileType: image, + excludeNsfw: defaultStore.state.nsfw !== "ignore", + limit: 10 + }).then((notes) => { + for (const note of notes) { + for (const file of note.files) { + images.value.push({ + note, + file + }); + } + } + fetching.value = false; + }); + }); + return (_ctx, _cache) => { + const _component_MkLoading = resolveComponent("MkLoading"); + const _component_MkA = resolveComponent("MkA"); + return openBlock(), createBlock(MkContainer, { + "max-height": 300, + foldable: true + }, { + icon: withCtx(() => [ + _hoisted_1 + ]), + header: withCtx(() => [ + createTextVNode(toDisplayString(unref(i18n).ts.images), 1) + ]), + default: withCtx(() => [ + createBaseVNode("div", { + class: normalizeClass(_ctx.$style.root) + }, [ + unref(fetching) ? (openBlock(), createBlock(_component_MkLoading, { key: 0 })) : createCommentVNode("", true), + !unref(fetching) && unref(images).length > 0 ? (openBlock(), createElementBlock("div", { + key: 1, + class: normalizeClass(_ctx.$style.stream) + }, [ + (openBlock(true), createElementBlock(Fragment, null, renderList(unref(images), (image) => { + return openBlock(), createBlock(_component_MkA, { + key: image.note.id + image.file.id, + class: normalizeClass(_ctx.$style.img), + to: unref(notePage)(image.note) + }, { + default: withCtx(() => [ + createVNode(ImgWithBlurhash, { + hash: image.file.blurhash, + src: thumbnail(image.file), + title: image.file.name + }, null, 8, ["hash", "src", "title"]) + ]), + _: 2 + }, 1032, ["class", "to"]); + }), 128)) + ], 2)) : createCommentVNode("", true), + !unref(fetching) && unref(images).length == 0 ? (openBlock(), createElementBlock("p", { + key: 2, + class: normalizeClass(_ctx.$style.empty) + }, toDisplayString(unref(i18n).ts.nothing), 3)) : createCommentVNode("", true) + ], 2) + ]), + _: 1 + }); + }; + } +}); + +const root = "xenMW"; +const stream = "xaZzf"; +const img = "xtA8t"; +const empty = "xhYKj"; +const style0 = { + root: root, + stream: stream, + img: img, + empty: empty +}; + +const cssModules = { + "$style": style0 +}; +const index_photos = /* @__PURE__ */ _export_sfc(_sfc_main, [["__cssModules", cssModules]]); + +export { index_photos as default }; +`.slice(1), { ecmaVersion: 'latest', sourceType: 'module' }); + unwindCssModuleClassName(ast); + expect(generate(ast)).toBe(` +import {c as api, d as defaultStore, i as i18n, aD as notePage, bN as ImgWithBlurhash, bY as getStaticImageUrl, _ as _export_sfc} from './app-!~{001}~.js'; +import {M as MkContainer} from './MkContainer-!~{03M}~.js'; +import {b as defineComponent, a as ref, e as onMounted, z as resolveComponent, g as openBlock, h as createBlock, i as withCtx, K as createTextVNode, E as toDisplayString, u as unref, l as createBaseVNode, q as normalizeClass, B as createCommentVNode, k as createElementBlock, F as Fragment, C as renderList, A as createVNode} from './vue-!~{002}~.js'; +import './photoswipe-!~{003}~.js'; +const _hoisted_1 = createBaseVNode("i", { + class: "ti ti-photo" +}, null, -1); +const _sfc_main = defineComponent({ + __name: "index.photos", + props: { + user: {} + }, + setup(__props) { + const props = __props; + let fetching = ref(true); + let images = ref([]); + function thumbnail(image) { + return defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(image.url) : image.thumbnailUrl; + } + onMounted(() => { + const image = ["image/jpeg", "image/webp", "image/avif", "image/png", "image/gif", "image/apng", "image/vnd.mozilla.apng"]; + api("users/notes", { + userId: props.user.id, + fileType: image, + excludeNsfw: defaultStore.state.nsfw !== "ignore", + limit: 10 + }).then(notes => { + for (const note of notes) { + for (const file of note.files) { + images.value.push({ + note, + file + }); + } + } + fetching.value = false; + }); + }); + return (_ctx, _cache) => { + const _component_MkLoading = resolveComponent("MkLoading"); + const _component_MkA = resolveComponent("MkA"); + return (openBlock(), createBlock(MkContainer, { + "max-height": 300, + foldable: true + }, { + icon: withCtx(() => [_hoisted_1]), + header: withCtx(() => [createTextVNode(toDisplayString(unref(i18n).ts.images), 1)]), + default: withCtx(() => [createBaseVNode("div", { + class: "xenMW" + }, [unref(fetching) ? (openBlock(), createBlock(_component_MkLoading, { + key: 0 + })) : createCommentVNode("", true), !unref(fetching) && unref(images).length > 0 ? (openBlock(), createElementBlock("div", { + key: 1, + class: "xaZzf" + }, [(openBlock(true), createElementBlock(Fragment, null, renderList(unref(images), image => { + return (openBlock(), createBlock(_component_MkA, { + key: image.note.id + image.file.id, + class: "xtA8t", + to: unref(notePage)(image.note) + }, { + default: withCtx(() => [createVNode(ImgWithBlurhash, { + hash: image.file.blurhash, + src: thumbnail(image.file), + title: image.file.name + }, null, 8, ["hash", "src", "title"])]), + _: 2 + }, 1032, ["class", "to"])); + }), 128))], 2)) : createCommentVNode("", true), !unref(fetching) && unref(images).length == 0 ? (openBlock(), createElementBlock("p", { + key: 2, + class: "xhYKj" + }, toDisplayString(unref(i18n).ts.nothing), 3)) : createCommentVNode("", true)], 2)]), + _: 1 + })); + }; + } +}); +const root = "xenMW"; +const stream = "xaZzf"; +const img = "xtA8t"; +const empty = "xhYKj"; +const style0 = { + root: root, + stream: stream, + img: img, + empty: empty +}; +const cssModules = { + "$style": style0 +}; +const index_photos = _sfc_main; +export {index_photos as default}; +`.slice(1)); +}); + +it('Composition API (with `useCssModule()`)', () => { + const ast = parse(` +import { a7 as getCurrentInstance, b as defineComponent, G as useCssModule, a1 as h, H as TransitionGroup } from './!~{002}~.js'; +import { d as defaultStore, aK as toast, b5 as MkAd, i as i18n, _ as _export_sfc } from './app-!~{001}~.js'; + +function isDebuggerEnabled(id) { + try { + return localStorage.getItem(\`DEBUG_\${id}\`) !== null; + } catch { + return false; + } +} +function stackTraceInstances() { + let instance = getCurrentInstance(); + const stack = []; + while (instance) { + stack.push(instance); + instance = instance.parent; + } + return stack; +} + +const _sfc_main = defineComponent({ + props: { + items: { + type: Array, + required: true + }, + direction: { + type: String, + required: false, + default: "down" + }, + reversed: { + type: Boolean, + required: false, + default: false + }, + noGap: { + type: Boolean, + required: false, + default: false + }, + ad: { + type: Boolean, + required: false, + default: false + } + }, + setup(props, { slots, expose }) { + const $style = useCssModule(); + function getDateText(time) { + const date = new Date(time).getDate(); + const month = new Date(time).getMonth() + 1; + return i18n.t("monthAndDay", { + month: month.toString(), + day: date.toString() + }); + } + if (props.items.length === 0) + return; + const renderChildrenImpl = () => props.items.map((item, i) => { + if (!slots || !slots.default) + return; + const el = slots.default({ + item + })[0]; + if (el.key == null && item.id) + el.key = item.id; + if (i !== props.items.length - 1 && new Date(item.createdAt).getDate() !== new Date(props.items[i + 1].createdAt).getDate()) { + const separator = h("div", { + class: $style["separator"], + key: item.id + ":separator" + }, h("p", { + class: $style["date"] + }, [ + h("span", { + class: $style["date-1"] + }, [ + h("i", { + class: \`ti ti-chevron-up \${$style["date-1-icon"]}\` + }), + getDateText(item.createdAt) + ]), + h("span", { + class: $style["date-2"] + }, [ + getDateText(props.items[i + 1].createdAt), + h("i", { + class: \`ti ti-chevron-down \${$style["date-2-icon"]}\` + }) + ]) + ])); + return [el, separator]; + } else { + if (props.ad && item._shouldInsertAd_) { + return [h(MkAd, { + key: item.id + ":ad", + prefer: ["horizontal", "horizontal-big"] + }), el]; + } else { + return el; + } + } + }); + const renderChildren = () => { + const children = renderChildrenImpl(); + if (isDebuggerEnabled(6864)) { + const nodes = children.flatMap((node) => node ?? []); + const keys = new Set(nodes.map((node) => node.key)); + if (keys.size !== nodes.length) { + const id = crypto.randomUUID(); + const instances = stackTraceInstances(); + toast(instances.reduce((a, c) => \`\${a} at \${c.type.name}\`, \`[DEBUG_6864 (\${id})]: \${nodes.length - keys.size} duplicated keys found\`)); + console.warn({ id, debugId: 6864, stack: instances }); + } + } + return children; + }; + function onBeforeLeave(el) { + el.style.top = \`\${el.offsetTop}px\`; + el.style.left = \`\${el.offsetLeft}px\`; + } + function onLeaveCanceled(el) { + el.style.top = ""; + el.style.left = ""; + } + return () => h( + defaultStore.state.animation ? TransitionGroup : "div", + { + class: { + [$style["date-separated-list"]]: true, + [$style["date-separated-list-nogap"]]: props.noGap, + [$style["reversed"]]: props.reversed, + [$style["direction-down"]]: props.direction === "down", + [$style["direction-up"]]: props.direction === "up" + }, + ...defaultStore.state.animation ? { + name: "list", + tag: "div", + onBeforeLeave, + onLeaveCanceled + } : {} + }, + { default: renderChildren } + ); + } +}); + +const reversed = "xxiZh"; +const separator = "xxeDx"; +const date = "xxawD"; +const style0 = { + "date-separated-list": "xfKPa", + "date-separated-list-nogap": "xf9zr", + "direction-up": "x7AeO", + "direction-down": "xBIqc", + reversed: reversed, + separator: separator, + date: date, + "date-1": "xwtmh", + "date-1-icon": "xsNPa", + "date-2": "x1xvw", + "date-2-icon": "x9ZiG" +}; + +const cssModules = { + "$style": style0 +}; +const MkDateSeparatedList = /* @__PURE__ */ _export_sfc(_sfc_main, [["__cssModules", cssModules]]); + +export { MkDateSeparatedList as M }; +`.slice(1), { ecmaVersion: 'latest', sourceType: 'module' }); + unwindCssModuleClassName(ast); + expect(generate(ast)).toBe(` +import {a7 as getCurrentInstance, b as defineComponent, G as useCssModule, a1 as h, H as TransitionGroup} from './!~{002}~.js'; +import {d as defaultStore, aK as toast, b5 as MkAd, i as i18n, _ as _export_sfc} from './app-!~{001}~.js'; +function isDebuggerEnabled(id) { + try { + return localStorage.getItem(\`DEBUG_\${id}\`) !== null; + } catch { + return false; + } +} +function stackTraceInstances() { + let instance = getCurrentInstance(); + const stack = []; + while (instance) { + stack.push(instance); + instance = instance.parent; + } + return stack; +} +const _sfc_main = defineComponent({ + props: { + items: { + type: Array, + required: true + }, + direction: { + type: String, + required: false, + default: "down" + }, + reversed: { + type: Boolean, + required: false, + default: false + }, + noGap: { + type: Boolean, + required: false, + default: false + }, + ad: { + type: Boolean, + required: false, + default: false + } + }, + setup(props, {slots, expose}) { + const $style = useCssModule(); + function getDateText(time) { + const date = new Date(time).getDate(); + const month = new Date(time).getMonth() + 1; + return i18n.t("monthAndDay", { + month: month.toString(), + day: date.toString() + }); + } + if (props.items.length === 0) return; + const renderChildrenImpl = () => props.items.map((item, i) => { + if (!slots || !slots.default) return; + const el = slots.default({ + item + })[0]; + if (el.key == null && item.id) el.key = item.id; + if (i !== props.items.length - 1 && new Date(item.createdAt).getDate() !== new Date(props.items[i + 1].createdAt).getDate()) { + const separator = h("div", { + class: $style["separator"], + key: item.id + ":separator" + }, h("p", { + class: $style["date"] + }, [h("span", { + class: $style["date-1"] + }, [h("i", { + class: \`ti ti-chevron-up \${$style["date-1-icon"]}\` + }), getDateText(item.createdAt)]), h("span", { + class: $style["date-2"] + }, [getDateText(props.items[i + 1].createdAt), h("i", { + class: \`ti ti-chevron-down \${$style["date-2-icon"]}\` + })])])); + return [el, separator]; + } else { + if (props.ad && item._shouldInsertAd_) { + return [h(MkAd, { + key: item.id + ":ad", + prefer: ["horizontal", "horizontal-big"] + }), el]; + } else { + return el; + } + } + }); + const renderChildren = () => { + const children = renderChildrenImpl(); + if (isDebuggerEnabled(6864)) { + const nodes = children.flatMap(node => node ?? []); + const keys = new Set(nodes.map(node => node.key)); + if (keys.size !== nodes.length) { + const id = crypto.randomUUID(); + const instances = stackTraceInstances(); + toast(instances.reduce((a, c) => \`\${a} at \${c.type.name}\`, \`[DEBUG_6864 (\${id})]: \${nodes.length - keys.size} duplicated keys found\`)); + console.warn({ + id, + debugId: 6864, + stack: instances + }); + } + } + return children; + }; + function onBeforeLeave(el) { + el.style.top = \`\${el.offsetTop}px\`; + el.style.left = \`\${el.offsetLeft}px\`; + } + function onLeaveCanceled(el) { + el.style.top = ""; + el.style.left = ""; + } + return () => h(defaultStore.state.animation ? TransitionGroup : "div", { + class: { + [$style["date-separated-list"]]: true, + [$style["date-separated-list-nogap"]]: props.noGap, + [$style["reversed"]]: props.reversed, + [$style["direction-down"]]: props.direction === "down", + [$style["direction-up"]]: props.direction === "up" + }, + ...defaultStore.state.animation ? { + name: "list", + tag: "div", + onBeforeLeave, + onLeaveCanceled + } : {} + }, { + default: renderChildren + }); + } +}); +const reversed = "xxiZh"; +const separator = "xxeDx"; +const date = "xxawD"; +const style0 = { + "date-separated-list": "xfKPa", + "date-separated-list-nogap": "xf9zr", + "direction-up": "x7AeO", + "direction-down": "xBIqc", + reversed: reversed, + separator: separator, + date: date, + "date-1": "xwtmh", + "date-1-icon": "xsNPa", + "date-2": "x1xvw", + "date-2-icon": "x9ZiG" +}; +const cssModules = { + "$style": style0 +}; +const MkDateSeparatedList = _export_sfc(_sfc_main, [["__cssModules", cssModules]]); +export {MkDateSeparatedList as M}; +`.slice(1)); +}); diff --git a/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.ts b/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.ts new file mode 100644 index 0000000000..a18f0d9049 --- /dev/null +++ b/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.ts @@ -0,0 +1,275 @@ +import { generate } from 'astring'; +import * as estree from 'estree'; +import { walk } from '../node_modules/estree-walker/src/index.js'; +import type * as estreeWalker from 'estree-walker'; +import type { Plugin } from 'vite'; + +function isFalsyIdentifier(identifier: estree.Identifier): boolean { + return identifier.name === 'undefined' || identifier.name === 'NaN'; +} + +function normalizeClassWalker(tree: estree.Node): string | null { + if (tree.type === 'Identifier') return isFalsyIdentifier(tree) ? '' : null; + if (tree.type === 'Literal') return typeof tree.value === 'string' ? tree.value : ''; + if (tree.type === 'BinaryExpression') { + if (tree.operator !== '+') return null; + const left = normalizeClassWalker(tree.left); + const right = normalizeClassWalker(tree.right); + if (left === null || right === null) return null; + return `${left}${right}`; + } + if (tree.type === 'TemplateLiteral') { + if (tree.expressions.some((x) => x.type !== 'Literal' && (x.type !== 'Identifier' || !isFalsyIdentifier(x)))) return null; + return tree.quasis.reduce((a, c, i) => { + const v = i === tree.quasis.length - 1 ? '' : (tree.expressions[i] as Partial<estree.Literal>).value; + return a + c.value.raw + (typeof v === 'string' ? v : ''); + }, ''); + } + if (tree.type === 'ArrayExpression') { + const values = tree.elements.map((treeNode) => { + if (treeNode === null) return ''; + if (treeNode.type === 'SpreadElement') return normalizeClassWalker(treeNode.argument); + return normalizeClassWalker(treeNode); + }); + if (values.some((x) => x === null)) return null; + return values.join(' '); + } + if (tree.type === 'ObjectExpression') { + const values = tree.properties.map((treeNode) => { + if (treeNode.type === 'SpreadElement') return normalizeClassWalker(treeNode.argument); + let x = treeNode.value; + let inveted = false; + while (x.type === 'UnaryExpression' && x.operator === '!') { + x = x.argument; + inveted = !inveted; + } + if (x.type === 'Literal') { + if (inveted === !x.value) { + return treeNode.key.type === 'Identifier' ? treeNode.computed ? null : treeNode.key.name : treeNode.key.type === 'Literal' ? treeNode.key.value : ''; + } else { + return ''; + } + } + if (x.type === 'Identifier') { + if (inveted !== isFalsyIdentifier(x)) { + return ''; + } else { + return null; + } + } + return null; + }); + if (values.some((x) => x === null)) return null; + return values.join(' '); + } + console.error(`Unexpected node type: ${tree.type}`); + return null; +} + +export function normalizeClass(tree: estree.Node): string | null { + const walked = normalizeClassWalker(tree); + return walked && walked.replace(/^\s+|\s+(?=\s)|\s+$/g, ''); +} + +export function unwindCssModuleClassName(ast: estree.Node): void { + (walk as typeof estreeWalker.walk)(ast, { + enter(node, parent): void { + if (parent?.type !== 'Program') return; + if (node.type !== 'VariableDeclaration') return; + if (node.declarations.length !== 1) return; + if (node.declarations[0].id.type !== 'Identifier') return; + const name = node.declarations[0].id.name; + if (node.declarations[0].init?.type !== 'CallExpression') return; + if (node.declarations[0].init.callee.type !== 'Identifier') return; + if (node.declarations[0].init.callee.name !== '_export_sfc') return; + if (node.declarations[0].init.arguments.length !== 2) return; + if (node.declarations[0].init.arguments[0].type !== 'Identifier') return; + const ident = node.declarations[0].init.arguments[0].name; + if (!ident.startsWith('_sfc_main')) return; + if (node.declarations[0].init.arguments[1].type !== 'ArrayExpression') return; + if (node.declarations[0].init.arguments[1].elements.length === 0) return; + const __cssModulesIndex = node.declarations[0].init.arguments[1].elements.findIndex((x) => { + if (x?.type !== 'ArrayExpression') return false; + if (x.elements.length !== 2) return false; + if (x.elements[0]?.type !== 'Literal') return false; + if (x.elements[0].value !== '__cssModules') return false; + if (x.elements[1]?.type !== 'Identifier') return false; + return true; + }); + if (!~__cssModulesIndex) return; + const cssModuleForestName = ((node.declarations[0].init.arguments[1].elements[__cssModulesIndex] as estree.ArrayExpression).elements[1] as estree.Identifier).name; + const cssModuleForestNode = parent.body.find((x) => { + if (x.type !== 'VariableDeclaration') return false; + if (x.declarations.length !== 1) return false; + if (x.declarations[0].id.type !== 'Identifier') return false; + if (x.declarations[0].id.name !== cssModuleForestName) return false; + if (x.declarations[0].init?.type !== 'ObjectExpression') return false; + return true; + }) as unknown as estree.VariableDeclaration; + const moduleForest = new Map((cssModuleForestNode.declarations[0].init as estree.ObjectExpression).properties.flatMap((property) => { + if (property.type !== 'Property') return []; + if (property.key.type !== 'Literal') return []; + if (property.value.type !== 'Identifier') return []; + return [[property.key.value as string, property.value.name as string]]; + })); + const sfcMain = parent.body.find((x) => { + if (x.type !== 'VariableDeclaration') return false; + if (x.declarations.length !== 1) return false; + if (x.declarations[0].id.type !== 'Identifier') return false; + if (x.declarations[0].id.name !== ident) return false; + return true; + }) as unknown as estree.VariableDeclaration; + if (sfcMain.declarations[0].init?.type !== 'CallExpression') return; + if (sfcMain.declarations[0].init.callee.type !== 'Identifier') return; + if (sfcMain.declarations[0].init.callee.name !== 'defineComponent') return; + if (sfcMain.declarations[0].init.arguments.length !== 1) return; + if (sfcMain.declarations[0].init.arguments[0].type !== 'ObjectExpression') return; + const setup = sfcMain.declarations[0].init.arguments[0].properties.find((x) => { + if (x.type !== 'Property') return false; + if (x.key.type !== 'Identifier') return false; + if (x.key.name !== 'setup') return false; + return true; + }) as unknown as estree.Property; + if (setup.value.type !== 'FunctionExpression') return; + const render = setup.value.body.body.find((x) => { + if (x.type !== 'ReturnStatement') return false; + return true; + }) as unknown as estree.ReturnStatement; + if (render.argument?.type !== 'ArrowFunctionExpression') return; + if (render.argument.params.length !== 2) return; + const ctx = render.argument.params[0]; + if (ctx.type !== 'Identifier') return; + if (ctx.name !== '_ctx') return; + if (render.argument.body.type !== 'BlockStatement') return; + for (const [key, value] of moduleForest) { + const cssModuleTreeNode = parent.body.find((x) => { + if (x.type !== 'VariableDeclaration') return false; + if (x.declarations.length !== 1) return false; + if (x.declarations[0].id.type !== 'Identifier') return false; + if (x.declarations[0].id.name !== value) return false; + return true; + }) as unknown as estree.VariableDeclaration; + if (cssModuleTreeNode.declarations[0].init?.type !== 'ObjectExpression') return; + const moduleTree = new Map(cssModuleTreeNode.declarations[0].init.properties.flatMap((property) => { + if (property.type !== 'Property') return []; + const actualKey = property.key.type === 'Identifier' ? property.key.name : property.key.type === 'Literal' ? property.key.value : null; + if (typeof actualKey !== 'string') return []; + if (property.value.type === 'Literal') return [[actualKey, property.value.value as string]]; + if (property.value.type !== 'Identifier') return []; + const labelledValue = property.value.name; + const actualValue = parent.body.find((x) => { + if (x.type !== 'VariableDeclaration') return false; + if (x.declarations.length !== 1) return false; + if (x.declarations[0].id.type !== 'Identifier') return false; + if (x.declarations[0].id.name !== labelledValue) return false; + return true; + }) as unknown as estree.VariableDeclaration; + if (actualValue.declarations[0].init?.type !== 'Literal') return []; + return [[actualKey, actualValue.declarations[0].init.value as string]]; + })); + (walk as typeof estreeWalker.walk)(render.argument.body, { + enter(childNode) { + if (childNode.type !== 'MemberExpression') return; + if (childNode.object.type !== 'MemberExpression') return; + if (childNode.object.object.type !== 'Identifier') return; + if (childNode.object.object.name !== ctx.name) return; + if (childNode.object.property.type !== 'Identifier') return; + if (childNode.object.property.name !== key) return; + if (childNode.property.type !== 'Identifier') return; + const actualValue = moduleTree.get(childNode.property.name); + if (actualValue === undefined) return; + this.replace({ + type: 'Literal', + value: actualValue, + }); + }, + }); + (walk as typeof estreeWalker.walk)(render.argument.body, { + enter(childNode) { + if (childNode.type !== 'MemberExpression') return; + if (childNode.object.type !== 'MemberExpression') return; + if (childNode.object.object.type !== 'Identifier') return; + if (childNode.object.object.name !== ctx.name) return; + if (childNode.object.property.type !== 'Identifier') return; + if (childNode.object.property.name !== key) return; + if (childNode.property.type !== 'Identifier') return; + console.error(`Undefined style detected: ${key}.${childNode.property.name} (in ${name})`); + this.replace({ + type: 'Identifier', + name: 'undefined', + }); + }, + }); + (walk as typeof estreeWalker.walk)(render.argument.body, { + enter(childNode) { + if (childNode.type !== 'CallExpression') return; + if (childNode.callee.type !== 'Identifier') return; + if (childNode.callee.name !== 'normalizeClass') return; + if (childNode.arguments.length !== 1) return; + const normalized = normalizeClass(childNode.arguments[0]); + if (normalized === null) return; + this.replace({ + type: 'Literal', + value: normalized, + }); + }, + }); + } + if (node.declarations[0].init.arguments[1].elements.length === 1) { + this.replace({ + type: 'VariableDeclaration', + declarations: [{ + type: 'VariableDeclarator', + id: { + type: 'Identifier', + name: node.declarations[0].id.name, + }, + init: { + type: 'Identifier', + name: ident, + }, + }], + kind: 'const', + }); + } else { + this.replace({ + type: 'VariableDeclaration', + declarations: [{ + type: 'VariableDeclarator', + id: { + type: 'Identifier', + name: node.declarations[0].id.name, + }, + init: { + type: 'CallExpression', + callee: { + type: 'Identifier', + name: '_export_sfc', + }, + arguments: [{ + type: 'Identifier', + name: ident, + }, { + type: 'ArrayExpression', + elements: node.declarations[0].init.arguments[1].elements.slice(0, __cssModulesIndex).concat(node.declarations[0].init.arguments[1].elements.slice(__cssModulesIndex + 1)), + }], + }, + }], + kind: 'const', + }); + } + }, + }); +} + +// eslint-disable-next-line import/no-default-export +export default function pluginUnwindCssModuleClassName(): Plugin { + return { + name: 'UnwindCssModuleClassName', + renderChunk(code): { code: string } { + const ast = this.parse(code) as unknown as estree.Node; + unwindCssModuleClassName(ast); + return { code: generate(ast) }; + }, + }; +} diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 84f3d9ce63..6720a5939b 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -24,6 +24,7 @@ "@vitejs/plugin-vue": "4.2.3", "@vue-macros/reactivity-transform": "0.3.8", "@vue/compiler-sfc": "3.3.4", + "astring": "1.8.5", "autosize": "6.0.1", "broadcast-channel": "4.20.2", "browser-image-resizer": "github:misskey-dev/browser-image-resizer#v2.2.1-misskey.3", @@ -39,6 +40,7 @@ "cropperjs": "2.0.0-beta.2", "date-fns": "2.30.0", "escape-regexp": "0.0.1", + "estree-walker": "^3.0.3", "eventemitter3": "5.0.1", "gsap": "3.11.5", "idb-keyval": "6.2.1", @@ -116,7 +118,7 @@ "@typescript-eslint/parser": "5.59.5", "@vitest/coverage-c8": "0.31.1", "@vue/runtime-core": "3.3.4", - "astring": "1.8.5", + "acorn": "^8.8.2", "chokidar-cli": "3.0.0", "cross-env": "7.0.3", "cypress": "12.13.0", diff --git a/packages/frontend/src/components/MkAchievements.vue b/packages/frontend/src/components/MkAchievements.vue index d30037dcf9..3fdb261dac 100644 --- a/packages/frontend/src/components/MkAchievements.vue +++ b/packages/frontend/src/components/MkAchievements.vue @@ -3,7 +3,14 @@ <div v-if="achievements" :class="$style.root"> <div v-for="achievement in achievements" :key="achievement" :class="$style.achievement" class="_panel"> <div :class="$style.icon"> - <div :class="[$style.iconFrame, $style['iconFrame_' + ACHIEVEMENT_BADGES[achievement.name].frame]]"> + <div + :class="[$style.iconFrame, { + [$style.iconFrame_bronze]: ACHIEVEMENT_BADGES[achievement.name].frame === 'bronze', + [$style.iconFrame_silver]: ACHIEVEMENT_BADGES[achievement.name].frame === 'silver', + [$style.iconFrame_gold]: ACHIEVEMENT_BADGES[achievement.name].frame === 'gold', + [$style.iconFrame_platinum]: ACHIEVEMENT_BADGES[achievement.name].frame === 'platinum', + }]" + > <div :class="[$style.iconInner]" :style="{ background: ACHIEVEMENT_BADGES[achievement.name].bg }"> <img :class="$style.iconImg" :src="ACHIEVEMENT_BADGES[achievement.name].img"> </div> diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue index 1af998dedd..fd892d8174 100644 --- a/packages/frontend/src/components/MkAutocomplete.vue +++ b/packages/frontend/src/components/MkAutocomplete.vue @@ -10,7 +10,7 @@ </li> <li tabindex="-1" :class="$style.item" @click="chooseUser()" @keydown="onKeydown">{{ i18n.ts.selectUser }}</li> </ol> - <ol v-else-if="hashtags.length > 0" ref="suggests" :class="[$style.list, $style.hashtags]"> + <ol v-else-if="hashtags.length > 0" ref="suggests" :class="$style.list"> <li v-for="hashtag in hashtags" tabindex="-1" :class="$style.item" @click="complete(type, hashtag)" @keydown="onKeydown"> <span class="name">{{ hashtag }}</span> </li> diff --git a/packages/frontend/src/components/MkButton.vue b/packages/frontend/src/components/MkButton.vue index 6de6a4cc70..16e44ec618 100644 --- a/packages/frontend/src/components/MkButton.vue +++ b/packages/frontend/src/components/MkButton.vue @@ -7,7 +7,7 @@ @click="emit('click', $event)" @mousedown="onMousedown" > - <div ref="ripples" :class="$style.ripples"></div> + <div ref="ripples" :class="$style.ripples" :data-children-class="$style.ripple"></div> <div :class="$style.content"> <slot></slot> </div> @@ -18,7 +18,7 @@ :to="to" @mousedown="onMousedown" > - <div ref="ripples" :class="$style.ripples"></div> + <div ref="ripples" :class="$style.ripples" :data-children-class="$style.ripple"></div> <div :class="$style.content"> <slot></slot> </div> @@ -26,9 +26,7 @@ </template> <script lang="ts" setup> -import { nextTick, onMounted, useCssModule } from 'vue'; - -const $style = useCssModule(); +import { nextTick, onMounted } from 'vue'; const props = defineProps<{ type?: 'button' | 'submit' | 'reset'; @@ -81,7 +79,7 @@ function onMousedown(evt: MouseEvent): void { const rect = target.getBoundingClientRect(); const ripple = document.createElement('div'); - ripple.classList.add($style.ripple); + ripple.classList.add(ripples!.dataset.childrenClass!); ripple.style.top = (evt.clientY - rect.top - 1).toString() + 'px'; ripple.style.left = (evt.clientX - rect.left - 1).toString() + 'px'; diff --git a/packages/frontend/src/components/MkClickerGame.vue b/packages/frontend/src/components/MkClickerGame.vue index da6439fd2c..a6ab5aded4 100644 --- a/packages/frontend/src/components/MkClickerGame.vue +++ b/packages/frontend/src/components/MkClickerGame.vue @@ -3,7 +3,7 @@ <div v-if="game.ready" :class="$style.game"> <div :class="$style.cps" class="">{{ number(cps) }}cps</div> <div :class="$style.count" class=""><i class="ti ti-cookie" style="font-size: 70%;"></i> {{ number(cookies) }}</div> - <button v-click-anime class="_button" :class="$style.button" @click="onClick"> + <button v-click-anime class="_button" @click="onClick"> <img src="/client-assets/cookie.png" :class="$style.img"> </button> </div> @@ -84,10 +84,6 @@ onUnmounted(() => { margin-bottom: 6px; } -.button { - -} - .img { max-width: 90px; } diff --git a/packages/frontend/src/components/MkContainer.vue b/packages/frontend/src/components/MkContainer.vue index f7cc697d67..af1c57b349 100644 --- a/packages/frontend/src/components/MkContainer.vue +++ b/packages/frontend/src/components/MkContainer.vue @@ -1,5 +1,5 @@ <template> -<div ref="rootEl" class="_panel" :class="[$style.root, { [$style.naked]: naked, [$style.thin]: thin, [$style.hideHeader]: !showHeader, [$style.scrollable]: scrollable, [$style.closed]: !showBody }]"> +<div ref="rootEl" class="_panel" :class="[$style.root, { [$style.naked]: naked, [$style.thin]: thin, [$style.scrollable]: scrollable }]"> <header v-if="showHeader" ref="headerEl" :class="$style.header"> <div :class="$style.title"> <span :class="$style.titleIcon"><slot name="icon"></slot></span> diff --git a/packages/frontend/src/components/MkDateSeparatedList.vue b/packages/frontend/src/components/MkDateSeparatedList.vue index d6303f9675..6942a0e6c3 100644 --- a/packages/frontend/src/components/MkDateSeparatedList.vue +++ b/packages/frontend/src/components/MkDateSeparatedList.vue @@ -36,7 +36,7 @@ export default defineComponent({ }, setup(props, { slots, expose }) { - const $style = useCssModule(); + const $style = useCssModule(); // カスタムレンダラなので使っても大丈夫 function getDateText(time: string) { const date = new Date(time).getDate(); const month = new Date(time).getMonth() + 1; diff --git a/packages/frontend/src/components/MkDialog.vue b/packages/frontend/src/components/MkDialog.vue index bd4da6c545..4d5df0bba4 100644 --- a/packages/frontend/src/components/MkDialog.vue +++ b/packages/frontend/src/components/MkDialog.vue @@ -4,7 +4,15 @@ <div v-if="icon" :class="$style.icon"> <i :class="icon"></i> </div> - <div v-else-if="!input && !select" :class="[$style.icon, $style['type_' + type]]"> + <div + v-else-if="!input && !select" + :class="[$style.icon, { + [$style.type_success]: type === 'success', + [$style.type_error]: type === 'error', + [$style.type_warning]: type === 'warning', + [$style.type_info]: type === 'info', + }]" + > <i v-if="type === 'success'" :class="$style.iconInner" class="ti ti-check"></i> <i v-else-if="type === 'error'" :class="$style.iconInner" class="ti ti-circle-x"></i> <i v-else-if="type === 'warning'" :class="$style.iconInner" class="ti ti-alert-triangle"></i> diff --git a/packages/frontend/src/components/MkFolder.vue b/packages/frontend/src/components/MkFolder.vue index 526f5e94ea..70f0cc5cda 100644 --- a/packages/frontend/src/components/MkFolder.vue +++ b/packages/frontend/src/components/MkFolder.vue @@ -5,7 +5,7 @@ <div :class="[$style.header, { [$style.opened]: opened }]" class="_button" role="button" data-cy-folder-header @click="toggle"> <div :class="$style.headerIcon"><slot name="icon"></slot></div> <div :class="$style.headerText"> - <div :class="$style.headerTextMain"> + <div> <MkCondensedLine :minScale="2 / 3"><slot name="label"></slot></MkCondensedLine> </div> <div :class="$style.headerTextSub"> @@ -185,10 +185,6 @@ onMounted(() => { padding-right: 12px; } -.headerTextMain { - -} - .headerTextSub { color: var(--fgTransparentWeak); font-size: .85em; diff --git a/packages/frontend/src/components/MkImgWithBlurhash.vue b/packages/frontend/src/components/MkImgWithBlurhash.vue index 38406cc0be..3750dacddc 100644 --- a/packages/frontend/src/components/MkImgWithBlurhash.vue +++ b/packages/frontend/src/components/MkImgWithBlurhash.vue @@ -3,7 +3,7 @@ <TransitionGroup :duration="defaultStore.state.animation && props.transition?.duration || undefined" :enterActiveClass="defaultStore.state.animation && props.transition?.enterActiveClass || undefined" - :leaveActiveClass="defaultStore.state.animation && (props.transition?.leaveActiveClass ?? $style['transition_leaveActive']) || undefined" + :leaveActiveClass="defaultStore.state.animation && (props.transition?.leaveActiveClass ?? $style.transition_leaveActive) || undefined" :enterFromClass="defaultStore.state.animation && props.transition?.enterFromClass || undefined" :leaveToClass="defaultStore.state.animation && props.transition?.leaveToClass || undefined" :enterToClass="defaultStore.state.animation && props.transition?.enterToClass || undefined" @@ -42,11 +42,10 @@ const workerPromise = new Promise<WorkerMultiDispatch | null>(resolve => { </script> <script lang="ts" setup> -import { computed, nextTick, onMounted, onUnmounted, shallowRef, useCssModule, watch } from 'vue'; +import { computed, nextTick, onMounted, onUnmounted, shallowRef, watch } from 'vue'; import { v4 as uuid } from 'uuid'; import { render } from 'buraha'; import { defaultStore } from '@/store'; -const $style = useCssModule(); const props = withDefaults(defineProps<{ transition?: { diff --git a/packages/frontend/src/components/MkMediaList.vue b/packages/frontend/src/components/MkMediaList.vue index 0f41ef248f..a0a2450054 100644 --- a/packages/frontend/src/components/MkMediaList.vue +++ b/packages/frontend/src/components/MkMediaList.vue @@ -6,8 +6,11 @@ ref="gallery" :class="[ $style.medias, - count <= 4 ? $style['n' + count] : $style.nMany, - $style[`n1${defaultStore.reactiveState.mediaListWithOneImageAppearance.value}`] + count === 1 ? [$style.n1, { + [$style.n116_9]: defaultStore.reactiveState.mediaListWithOneImageAppearance.value === '16_9', + [$style.n11_1]: defaultStore.reactiveState.mediaListWithOneImageAppearance.value === '1_1', + [$style.n12_3]: defaultStore.reactiveState.mediaListWithOneImageAppearance.value === '2_3', + }] : count === 2 ? $style.n2 : count === 3 ? $style.n3 : count === 4 ? $style.n4 : $style.nMany, ]" > <template v-for="media in mediaList.filter(media => previewable(media))"> @@ -20,7 +23,7 @@ </template> <script lang="ts" setup> -import { onMounted, ref, useCssModule, watch, shallowRef } from 'vue'; +import { onMounted, watch, shallowRef } from 'vue'; import * as misskey from 'misskey-js'; import PhotoSwipeLightbox from 'photoswipe/lightbox'; import PhotoSwipe from 'photoswipe'; @@ -37,8 +40,6 @@ const props = defineProps<{ raw?: boolean; }>(); -const $style = useCssModule(); - const gallery = shallowRef<HTMLDivElement>(); const pswpZIndex = os.claimZIndex('middle'); document.documentElement.style.setProperty('--mk-pswp-root-z-index', pswpZIndex.toString()); @@ -96,7 +97,7 @@ onMounted(() => { return item; }), gallery: gallery.value, - mainClass: $style.pswp, + mainClass: 'pswp', children: '.image', thumbSelector: '.image', loop: false, @@ -268,7 +269,7 @@ const previewable = (file: misskey.entities.DriveFile): boolean => { border-radius: 8px; } -.pswp { +:global(.pswp) { --pswp-root-z-index: var(--mk-pswp-root-z-index, 2000700) !important; --pswp-bg: var(--modalBg) !important; } diff --git a/packages/frontend/src/components/MkMention.vue b/packages/frontend/src/components/MkMention.vue index 481c3710ca..bb256c394b 100644 --- a/packages/frontend/src/components/MkMention.vue +++ b/packages/frontend/src/components/MkMention.vue @@ -2,7 +2,7 @@ <MkA v-user-preview="canonical" :class="[$style.root, { [$style.isMe]: isMe }]" :to="url" :style="{ background: bgCss }"> <img :class="$style.icon" :src="`/avatar/@${username}@${host}`" alt=""> <span> - <span :class="$style.username">@{{ username }}</span> + <span>@{{ username }}</span> <span v-if="(host != localHost) || defaultStore.state.showFullAcct" :class="$style.host">@{{ toUnicode(host) }}</span> </span> </MkA> diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue index 2697e3e21a..7dd6a8c88f 100644 --- a/packages/frontend/src/components/MkMenu.vue +++ b/packages/frontend/src/components/MkMenu.vue @@ -49,7 +49,7 @@ <span>{{ i18n.ts.none }}</span> </span> </div> - <div v-if="childMenu" :class="$style.child"> + <div v-if="childMenu"> <XChild ref="child" :items="childMenu" :targetElement="childTarget" :rootElement="itemsEl" showing @actioned="childActioned"/> </div> </div> diff --git a/packages/frontend/src/components/MkModal.vue b/packages/frontend/src/components/MkModal.vue index b9ce6de697..bb5c6c7aab 100644 --- a/packages/frontend/src/components/MkModal.vue +++ b/packages/frontend/src/components/MkModal.vue @@ -1,10 +1,30 @@ <template> <Transition :name="transitionName" - :enterActiveClass="$style['transition_' + transitionName + '_enterActive']" - :leaveActiveClass="$style['transition_' + transitionName + '_leaveActive']" - :enterFromClass="$style['transition_' + transitionName + '_enterFrom']" - :leaveToClass="$style['transition_' + transitionName + '_leaveTo']" + :enterActiveClass="normalizeClass({ + [$style.transition_modalDrawer_enterActive]: transitionName === 'modal-drawer', + [$style.transition_modalPopup_enterActive]: transitionName === 'modal-popup', + [$style.transition_modal_enterActive]: transitionName === 'modal', + [$style.transition_send_enterActive]: transitionName === 'send', + })" + :leaveActiveClass="normalizeClass({ + [$style.transition_modalDrawer_leaveActive]: transitionName === 'modal-drawer', + [$style.transition_modalPopup_leaveActive]: transitionName === 'modal-popup', + [$style.transition_modal_leaveActive]: transitionName === 'modal', + [$style.transition_send_leaveActive]: transitionName === 'send', + })" + :enterFromClass="normalizeClass({ + [$style.transition_modalDrawer_enterFrom]: transitionName === 'modal-drawer', + [$style.transition_modalPopup_enterFrom]: transitionName === 'modal-popup', + [$style.transition_modal_enterFrom]: transitionName === 'modal', + [$style.transition_send_enterFrom]: transitionName === 'send', + })" + :leaveToClass="normalizeClass({ + [$style.transition_modalDrawer_leaveTo]: transitionName === 'modal-drawer', + [$style.transition_modalPopup_leaveTo]: transitionName === 'modal-popup', + [$style.transition_modal_leaveTo]: transitionName === 'modal', + [$style.transition_send_leaveTo]: transitionName === 'send', + })" :duration="transitionDuration" appear @afterLeave="emit('closed')" @enter="emit('opening')" @afterEnter="onOpened" > <div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" :class="[$style.root, { [$style.drawer]: type === 'drawer', [$style.dialog]: type === 'dialog', [$style.popup]: type === 'popup' }]" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }"> @@ -17,7 +37,7 @@ </template> <script lang="ts" setup> -import { nextTick, onMounted, watch, provide, onUnmounted } from 'vue'; +import { nextTick, normalizeClass, onMounted, onUnmounted, provide, watch } from 'vue'; import * as os from '@/os'; import { isTouchUsing } from '@/scripts/touch'; import { defaultStore } from '@/store'; @@ -345,8 +365,8 @@ defineExpose({ } } -.transition_modal-popup_enterActive, -.transition_modal-popup_leaveActive { +.transition_modalPopup_enterActive, +.transition_modalPopup_leaveActive { > .bg { transition: opacity 0.1s !important; } @@ -356,8 +376,8 @@ defineExpose({ transition: opacity 0.1s cubic-bezier(0, 0, 0.2, 1), transform 0.1s cubic-bezier(0, 0, 0.2, 1) !important; } } -.transition_modal-popup_enterFrom, -.transition_modal-popup_leaveTo { +.transition_modalPopup_enterFrom, +.transition_modalPopup_leaveTo { > .bg { opacity: 0; } @@ -370,7 +390,7 @@ defineExpose({ } } -.transition_modal-drawer_enterActive { +.transition_modalDrawer_enterActive { > .bg { transition: opacity 0.2s !important; } @@ -379,7 +399,7 @@ defineExpose({ transition: transform 0.2s cubic-bezier(0,.5,0,1) !important; } } -.transition_modal-drawer_leaveActive { +.transition_modalDrawer_leaveActive { > .bg { transition: opacity 0.2s !important; } @@ -388,8 +408,8 @@ defineExpose({ transition: transform 0.2s cubic-bezier(0,.5,0,1) !important; } } -.transition_modal-drawer_enterFrom, -.transition_modal-drawer_leaveTo { +.transition_modalDrawer_enterFrom, +.transition_modalDrawer_leaveTo { > .bg { opacity: 0; } diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 30ec9123bb..2483204d4c 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -44,8 +44,8 @@ <div v-if="appearNote.channel" :class="$style.colorBar" :style="{ background: appearNote.channel.color }"></div> <MkAvatar :class="$style.avatar" :user="appearNote.user" link preview/> <div :class="$style.main"> - <MkNoteHeader :class="$style.header" :note="appearNote" :mini="true"/> - <MkInstanceTicker v-if="showTicker" :class="$style.ticker" :instance="appearNote.user.instance"/> + <MkNoteHeader :note="appearNote" :mini="true"/> + <MkInstanceTicker v-if="showTicker" :instance="appearNote.user.instance"/> <div style="container-type: inline-size;"> <p v-if="appearNote.cw != null" :class="$style.cw"> <Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :i="$i"/> @@ -58,13 +58,13 @@ <Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :emojiUrls="appearNote.emojis"/> <div v-if="translating || translation" :class="$style.translation"> <MkLoading v-if="translating" mini/> - <div v-else :class="$style.translated"> + <div v-else> <b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b> <Mfm :text="translation.text" :author="appearNote.user" :i="$i" :emojiUrls="appearNote.emojis"/> </div> </div> </div> - <div v-if="appearNote.files.length > 0" :class="$style.files"> + <div v-if="appearNote.files.length > 0"> <MkMediaList :mediaList="appearNote.files"/> </div> <MkPoll v-if="appearNote.poll" :note="appearNote" :class="$style.poll"/> diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index 19aa73127b..a65039277b 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -52,7 +52,7 @@ </div> </div> <div :class="$style.noteHeaderUsername"><MkAcct :user="appearNote.user"/></div> - <MkInstanceTicker v-if="showTicker" :class="$style.ticker" :instance="appearNote.user.instance"/> + <MkInstanceTicker v-if="showTicker" :instance="appearNote.user.instance"/> </div> </header> <div :class="$style.noteContent"> @@ -72,7 +72,7 @@ <Mfm :text="translation.text" :author="appearNote.user" :i="$i" :emojiUrls="appearNote.emojis"/> </div> </div> - <div v-if="appearNote.files.length > 0" :class="$style.files"> + <div v-if="appearNote.files.length > 0"> <MkMediaList :mediaList="appearNote.files"/> </div> <MkPoll v-if="appearNote.poll" ref="pollViewer" :note="appearNote" :class="$style.poll"/> diff --git a/packages/frontend/src/components/MkNotePreview.vue b/packages/frontend/src/components/MkNotePreview.vue index 6b55c27869..6786f8b256 100644 --- a/packages/frontend/src/components/MkNotePreview.vue +++ b/packages/frontend/src/components/MkNotePreview.vue @@ -6,7 +6,7 @@ <MkUserName :user="$i" :nowrap="true"/> </div> <div> - <div :class="$style.content"> + <div> <Mfm :text="text.trim()" :author="$i" :i="$i"/> </div> </div> diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue index b4c424e79e..d25332b10f 100644 --- a/packages/frontend/src/components/MkNotification.vue +++ b/packages/frontend/src/components/MkNotification.vue @@ -5,7 +5,19 @@ <MkAvatar v-else-if="notification.type === 'achievementEarned'" :class="$style.icon" :user="$i" link preview/> <MkAvatar v-else-if="notification.user" :class="$style.icon" :user="notification.user" link preview/> <img v-else-if="notification.icon" :class="$style.icon" :src="notification.icon" alt=""/> - <div :class="[$style.subIcon, $style['t_' + notification.type]]"> + <div + :class="[$style.subIcon, { + [$style.t_follow]: notification.type === 'follow', + [$style.t_followRequestAccepted]: notification.type === 'followRequestAccepted', + [$style.t_receiveFollowRequest]: notification.type === 'receiveFollowRequest', + [$style.t_renote]: notification.type === 'renote', + [$style.t_reply]: notification.type === 'reply', + [$style.t_mention]: notification.type === 'mention', + [$style.t_quote]: notification.type === 'quote', + [$style.t_pollEnded]: notification.type === 'pollEnded', + [$style.t_achievementEarned]: notification.type === 'achievementEarned', + }]" + > <i v-if="notification.type === 'follow'" class="ti ti-plus"></i> <i v-else-if="notification.type === 'receiveFollowRequest'" class="ti ti-clock"></i> <i v-else-if="notification.type === 'followRequestAccepted'" class="ti ti-check"></i> @@ -34,7 +46,7 @@ <span v-else>{{ notification.header }}</span> <MkTime v-if="withTime" :time="notification.createdAt" :class="$style.headerTime"/> </header> - <div :class="$style.content"> + <div> <MkA v-if="notification.type === 'reaction'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> <i class="ti ti-quote" :class="$style.quote"></i> <Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :author="notification.note.user"/> @@ -243,9 +255,6 @@ useTooltip(reactionRef, (showing) => { font-size: 0.9em; } -.content { -} - .text { display: flex; width: 100%; diff --git a/packages/frontend/src/components/MkPoll.vue b/packages/frontend/src/components/MkPoll.vue index e2ca5603e3..464e340116 100644 --- a/packages/frontend/src/components/MkPoll.vue +++ b/packages/frontend/src/components/MkPoll.vue @@ -1,7 +1,7 @@ <template> <div :class="{ [$style.done]: closed || isVoted }"> <ul :class="$style.choices"> - <li v-for="(choice, i) in note.poll.choices" :key="i" :class="[$style.choice, { [$style.voted]: choice.voted }]" @click="vote(i)"> + <li v-for="(choice, i) in note.poll.choices" :key="i" :class="$style.choice" @click="vote(i)"> <div :class="$style.bg" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div> <span :class="$style.fg"> <template v-if="choice.isVoted"><i class="ti ti-check" style="margin-right: 4px; color: var(--accent);"></i></template> diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index 731d171178..5c65569683 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -27,16 +27,16 @@ <span :class="$style.headerRightButtonText">{{ channel.name }}</span> </button> </template> - <button v-click-anime v-tooltip="i18n.ts._visibility.disableFederation" class="_button" :class="[$style.headerRightItem, $style.localOnly, { [$style.danger]: localOnly }]" :disabled="channel != null || visibility === 'specified'" @click="toggleLocalOnly"> + <button v-click-anime v-tooltip="i18n.ts._visibility.disableFederation" class="_button" :class="[$style.headerRightItem, { [$style.danger]: localOnly }]" :disabled="channel != null || visibility === 'specified'" @click="toggleLocalOnly"> <span v-if="!localOnly"><i class="ti ti-rocket"></i></span> <span v-else><i class="ti ti-rocket-off"></i></span> </button> - <button v-click-anime v-tooltip="i18n.ts.reactionAcceptance" class="_button" :class="[$style.headerRightItem, $style.reactionAcceptance, { [$style.danger]: reactionAcceptance === 'likeOnly' }]" @click="toggleReactionAcceptance"> + <button v-click-anime v-tooltip="i18n.ts.reactionAcceptance" class="_button" :class="[$style.headerRightItem, { [$style.danger]: reactionAcceptance === 'likeOnly' }]" @click="toggleReactionAcceptance"> <span v-if="reactionAcceptance === 'likeOnly'"><i class="ti ti-heart"></i></span> <span v-else-if="reactionAcceptance === 'likeOnlyForRemote'"><i class="ti ti-heart-plus"></i></span> <span v-else><i class="ti ti-icons"></i></span> </button> - <button v-click-anime class="_button" :class="[$style.submit, { [$style.submitPosting]: posting }]" :disabled="!canPost" data-cy-open-post-form-submit @click="post"> + <button v-click-anime class="_button" :class="$style.submit" :disabled="!canPost" data-cy-open-post-form-submit @click="post"> <div :class="$style.submitInner"> <template v-if="posted"></template> <template v-else-if="posting"><MkEllipsis/></template> @@ -66,7 +66,7 @@ <div v-if="maxTextLength - textLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</div> </div> <input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags"> - <XPostFormAttaches v-model="files" :class="$style.attaches" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName"/> + <XPostFormAttaches v-model="files" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName"/> <MkPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/> <MkNotePreview v-if="showPreview" :class="$style.preview" :text="text"/> <div v-if="showingOptions" style="padding: 8px 16px;"> diff --git a/packages/frontend/src/components/MkSubNoteContent.vue b/packages/frontend/src/components/MkSubNoteContent.vue index 5f32391c7e..3a050889c8 100644 --- a/packages/frontend/src/components/MkSubNoteContent.vue +++ b/packages/frontend/src/components/MkSubNoteContent.vue @@ -1,6 +1,6 @@ <template> <div :class="[$style.root, { [$style.collapsed]: collapsed }]"> - <div :class="$style.body"> + <div> <span v-if="note.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span> <span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deleted }})</span> <MkA v-if="note.replyId" :class="$style.reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA> @@ -76,10 +76,6 @@ const collapsed = $ref( } } -.body { - -} - .reply { margin-right: 6px; color: var(--accent); diff --git a/packages/frontend/src/components/MkTextarea.vue b/packages/frontend/src/components/MkTextarea.vue index f5e6c71b75..83b2ed2444 100644 --- a/packages/frontend/src/components/MkTextarea.vue +++ b/packages/frontend/src/components/MkTextarea.vue @@ -6,7 +6,7 @@ ref="inputEl" v-model="v" v-adaptive-border - :class="[$style.textarea, { [$style.code]: code, _monospace: code }]" + :class="[$style.textarea, { _monospace: code }]" :disabled="disabled" :required="required" :readonly="readonly" diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue index 23be814322..fcad5b8064 100644 --- a/packages/frontend/src/components/MkUrlPreview.vue +++ b/packages/frontend/src/components/MkUrlPreview.vue @@ -22,7 +22,7 @@ </div> </template> <template v-else-if="tweetId && tweetExpanded"> - <div ref="twitter" :class="$style.twitter"> + <div ref="twitter"> <iframe ref="tweet" scrolling="no" frameborder="no" :style="{ position: 'relative', width: '100%', height: `${tweetHeight}px` }" :src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&hideCard=false&hideThread=false&lang=en&theme=${defaultStore.state.darkMode ? 'dark' : 'light'}&id=${tweetId}`"></iframe> </div> <div :class="$style.action"> @@ -31,7 +31,7 @@ </MkButton> </div> </template> -<div v-else :class="$style.urlPreview"> +<div v-else> <component :is="self ? 'MkA' : 'a'" :class="[$style.link, { [$style.compact]: compact }]" :[attr]="self ? url.substr(local.length) : url" rel="nofollow noopener" :target="target" :title="url"> <div v-if="thumbnail" :class="$style.thumbnail" :style="`background-image: url('${thumbnail}')`"> </div> @@ -210,13 +210,6 @@ onUnmounted(() => { width: 100%; } -.twitter { - -} - -.urlPreview { -} - .link { position: relative; display: block; diff --git a/packages/frontend/src/components/MkUserOnlineIndicator.vue b/packages/frontend/src/components/MkUserOnlineIndicator.vue index 251ab5d79a..a2c2b53b08 100644 --- a/packages/frontend/src/components/MkUserOnlineIndicator.vue +++ b/packages/frontend/src/components/MkUserOnlineIndicator.vue @@ -1,5 +1,13 @@ <template> -<div v-tooltip="text" :class="[$style.root, $style['status_' + user.onlineStatus]]"></div> +<div + v-tooltip="text" + :class="[$style.root, { + [$style.status_online]: user.onlineStatus === 'online', + [$style.status_active]: user.onlineStatus === 'active', + [$style.status_offline]: user.onlineStatus === 'offline', + [$style.status_unknown]: user.onlineStatus === 'unknown', + }]" +></div> </template> <script lang="ts" setup> diff --git a/packages/frontend/src/components/MkUserSelectDialog.vue b/packages/frontend/src/components/MkUserSelectDialog.vue index 9ba5d9fc53..792ff7afd7 100644 --- a/packages/frontend/src/components/MkUserSelectDialog.vue +++ b/packages/frontend/src/components/MkUserSelectDialog.vue @@ -9,7 +9,7 @@ @closed="$emit('closed')" > <template #header>{{ i18n.ts.selectUser }}</template> - <div :class="$style.root"> + <div> <div :class="$style.form"> <FormSplit :minWidth="170"> <MkInput v-model="username" :autofocus="true" @update:modelValue="search"> @@ -126,8 +126,6 @@ onMounted(() => { </script> <style lang="scss" module> -.root { -} .form { padding: 0 var(--root-margin); diff --git a/packages/frontend/src/components/MkUsersTooltip.vue b/packages/frontend/src/components/MkUsersTooltip.vue index bed725cd1d..0b80c2edc7 100644 --- a/packages/frontend/src/components/MkUsersTooltip.vue +++ b/packages/frontend/src/components/MkUsersTooltip.vue @@ -3,9 +3,9 @@ <div :class="$style.root"> <div v-for="u in users" :key="u.id" :class="$style.user"> <MkAvatar :class="$style.avatar" :user="u"/> - <MkUserName :class="$style.name" :user="u" :nowrap="true"/> + <MkUserName :user="u" :nowrap="true"/> </div> - <div v-if="users.length < count" :class="$style.omitted">+{{ count - users.length }}</div> + <div v-if="users.length < count">+{{ count - users.length }}</div> </div> </MkTooltip> </template> @@ -43,14 +43,6 @@ const emit = defineEmits<{ } } -.name { - -} - -.omitted { - -} - .avatar { width: 24px; height: 24px; diff --git a/packages/frontend/src/components/MkVisitorDashboard.vue b/packages/frontend/src/components/MkVisitorDashboard.vue index 6226768127..9566cc651f 100644 --- a/packages/frontend/src/components/MkVisitorDashboard.vue +++ b/packages/frontend/src/components/MkVisitorDashboard.vue @@ -39,7 +39,7 @@ <MkTimeline src="local"/> </div> </div> - <div :class="[$style.activeUsersChart, $style.panel]"> + <div :class="$style.panel"> <XActiveUsersChart/> </div> </div> @@ -220,8 +220,4 @@ function exploreOtherServers() { height: 350px; overflow: auto; } - -.activeUsersChart { - -} </style> diff --git a/packages/frontend/src/components/MkWidgets.vue b/packages/frontend/src/components/MkWidgets.vue index 9fd1d61632..30547c7444 100644 --- a/packages/frontend/src/components/MkWidgets.vue +++ b/packages/frontend/src/components/MkWidgets.vue @@ -1,7 +1,7 @@ <template> <div :class="$style.root"> <template v-if="edit"> - <header :class="$style['edit-header']"> + <header :class="$style.editHeader"> <MkSelect v-model="widgetAdderSelected" style="margin-bottom: var(--margin)" data-cy-widget-select> <template #label>{{ i18n.ts.selectWidget }}</template> <option v-for="widget in widgetDefs" :key="widget" :value="widget">{{ i18n.t(`_widgets.${widget}`) }}</option> @@ -15,15 +15,15 @@ handle=".handle" :animation="150" :group="{ name: 'SortableMkWidgets' }" - :class="$style['edit-editing']" + :class="$style.editEditing" @update:modelValue="v => emit('updateWidgets', v)" > <template #item="{element}"> - <div :class="[$style.widget, $style['customize-container']]" data-cy-customize-container> - <button :class="$style['customize-container-config']" class="_button" @click.prevent.stop="configWidget(element.id)"><i class="ti ti-settings"></i></button> - <button :class="$style['customize-container-remove']" data-cy-customize-container-remove class="_button" @click.prevent.stop="removeWidget(element)"><i class="ti ti-x"></i></button> + <div :class="[$style.widget, $style.customizeContainer]" data-cy-customize-container> + <button :class="$style.customizeContainerConfig" class="_button" @click.prevent.stop="configWidget(element.id)"><i class="ti ti-settings"></i></button> + <button :class="$style.customizeContainerRemove" data-cy-customize-container-remove class="_button" @click.prevent.stop="removeWidget(element)"><i class="ti ti-x"></i></button> <div class="handle"> - <component :is="`widget-${element.name}`" :ref="el => widgetRefs[element.id] = el" class="widget" :class="$style['customize-container-handle-widget']" :widget="element" @updateProps="updateWidget(element.id, $event)"/> + <component :is="`widget-${element.name}`" :ref="el => widgetRefs[element.id] = el" class="widget" :class="$style.customizeContainerHandleWidget" :widget="element" @updateProps="updateWidget(element.id, $event)"/> </div> </div> </template> @@ -130,7 +130,7 @@ function onContextmenu(widget: Widget, ev: MouseEvent) { } .edit { - &-header { + &Header { margin: 16px 0; > * { @@ -139,17 +139,17 @@ function onContextmenu(widget: Widget, ev: MouseEvent) { } } - &-editing { + &Editing { min-height: 100px; } } -.customize-container { +.customizeContainer { position: relative; cursor: move; - &-config, - &-remove { + &Config, + &Remove { position: absolute; z-index: 10000; top: 8px; @@ -160,17 +160,17 @@ function onContextmenu(widget: Widget, ev: MouseEvent) { border-radius: 4px; } - &-config { + &Config { right: 8px + 8px + 32px; } - &-remove { + &Remove { right: 8px; } - &-handle { + &Handle { - &-widget { + &Widget { pointer-events: none; } } diff --git a/packages/frontend/src/components/form/link.vue b/packages/frontend/src/components/form/link.vue index e6da039ac5..22b5edc3c9 100644 --- a/packages/frontend/src/components/form/link.vue +++ b/packages/frontend/src/components/form/link.vue @@ -5,7 +5,7 @@ <span :class="$style.text"><slot></slot></span> <span :class="$style.suffix"> <span :class="$style.suffixText"><slot name="suffix"></slot></span> - <i class="ti ti-external-link" :class="$style.suffixIcon"></i> + <i class="ti ti-external-link"></i> </span> </a> <MkA v-else :class="[$style.main, { [$style.active]: active }]" class="_button" :to="to" :behavior="behavior"> @@ -13,7 +13,7 @@ <span :class="$style.text"><slot></slot></span> <span :class="$style.suffix"> <span :class="$style.suffixText"><slot name="suffix"></slot></span> - <i class="ti ti-chevron-right" :class="$style.suffixIcon"></i> + <i class="ti ti-chevron-right"></i> </span> </MkA> </div> diff --git a/packages/frontend/src/components/form/slot.vue b/packages/frontend/src/components/form/slot.vue index 792f664ea0..809d80620f 100644 --- a/packages/frontend/src/components/form/slot.vue +++ b/packages/frontend/src/components/form/slot.vue @@ -1,7 +1,7 @@ <template> <div> <div :class="$style.label" @click="focus"><slot name="label"></slot></div> - <div :class="$style.content"> + <div> <slot></slot> </div> <div :class="$style.caption"><slot name="caption"></slot></div> diff --git a/packages/frontend/src/components/global/MkAd.vue b/packages/frontend/src/components/global/MkAd.vue index aa975600f0..8b25ab1b6a 100644 --- a/packages/frontend/src/components/global/MkAd.vue +++ b/packages/frontend/src/components/global/MkAd.vue @@ -1,6 +1,14 @@ <template> <div v-if="chosen && !shouldHide" :class="$style.root"> - <div v-if="!showMenu" :class="[$style.main, $style['form_' + chosen.place]]"> + <div + v-if="!showMenu" + :class="[$style.main, { + [$style.form_square]: chosen.place === 'square', + [$style.form_horizontal]: chosen.place === 'horizontal', + [$style.form_horizontalBig]: chosen.place === 'horizontal-big', + [$style.form_vertical]: chosen.place === 'vertical', + }]" + > <a :href="chosen.url" target="_blank" :class="$style.link"> <img :src="chosen.imageUrl" :class="$style.img"> <button class="_button" :class="$style.i" @click.prevent.stop="toggleMenu"><i :class="$style.iIcon" class="ti ti-info-circle"></i></button> @@ -122,7 +130,7 @@ function reduceFrequency(): void { } } - &.form_horizontal-big { + &.form_horizontalBig { padding: 8px; > .link, diff --git a/packages/frontend/src/components/global/MkUrl.vue b/packages/frontend/src/components/global/MkUrl.vue index 2a92780306..c1efd9a06b 100644 --- a/packages/frontend/src/components/global/MkUrl.vue +++ b/packages/frontend/src/components/global/MkUrl.vue @@ -6,7 +6,7 @@ <template v-if="!self"> <span :class="$style.schema">{{ schema }}//</span> <span :class="$style.hostname">{{ hostname }}</span> - <span v-if="port != ''" :class="$style.port">:{{ port }}</span> + <span v-if="port != ''">:{{ port }}</span> </template> <template v-if="pathname === '/' && self"> <span :class="$style.self">{{ hostname }}</span> diff --git a/packages/frontend/src/components/page/page.section.vue b/packages/frontend/src/components/page/page.section.vue index 9f79ecc833..83a16ae0a5 100644 --- a/packages/frontend/src/components/page/page.section.vue +++ b/packages/frontend/src/components/page/page.section.vue @@ -1,6 +1,15 @@ <template> <section> - <component :is="'h' + h" :class="h < 5 ? $style['h' + h] : null">{{ block.title }}</component> + <component + :is="'h' + h" + :class="{ + 'h2': h === 2, + 'h3': h === 3, + 'h4': h === 4, + }" + > + {{ block.title }} + </component> <div class="_gaps"> <XBlock v-for="child in block.children" :key="child.id" :page="page" :block="child" :h="h + 1"/> diff --git a/packages/frontend/src/pages/admin/RolesEditorFormula.vue b/packages/frontend/src/pages/admin/RolesEditorFormula.vue index 36e3c32391..9530b27bad 100644 --- a/packages/frontend/src/pages/admin/RolesEditorFormula.vue +++ b/packages/frontend/src/pages/admin/RolesEditorFormula.vue @@ -1,5 +1,5 @@ <template> -<div :class="$style.root" class="_gaps"> +<div class="_gaps"> <div :class="$style.header"> <MkSelect v-model="type" :class="$style.typeSelect"> <option value="isLocal">{{ i18n.ts._role._condition.isLocal }}</option> @@ -24,7 +24,7 @@ </button> </div> - <div v-if="type === 'and' || type === 'or'" :class="$style.values" class="_gaps"> + <div v-if="type === 'and' || type === 'or'" class="_gaps"> <Sortable v-model="v.values" tag="div" class="_gaps" itemKey="id" handle=".drag-handle" :group="{ name: 'roleFormula' }" :animation="150" :swapThreshold="0.5"> <template #item="{element}"> <div :class="$style.item"> @@ -118,10 +118,6 @@ function removeSelf() { </script> <style lang="scss" module> -.root { - -} - .header { display: flex; } @@ -148,8 +144,4 @@ function removeSelf() { border-color: var(--accent); } } - -.values { - -} </style> diff --git a/packages/frontend/src/pages/admin/queue.chart.vue b/packages/frontend/src/pages/admin/queue.chart.vue index 3ad8324d63..8e6856fddd 100644 --- a/packages/frontend/src/pages/admin/queue.chart.vue +++ b/packages/frontend/src/pages/admin/queue.chart.vue @@ -28,8 +28,8 @@ <template #icon><i class="ti ti-alert-triangle"></i></template> <template #label>Errored instances</template> <template #suffix>({{ number(jobs.reduce((a, b) => a + b[1], 0)) }} jobs)</template> - - <div :class="$style.jobs"> + + <div> <div v-if="jobs.length > 0"> <div v-for="job in jobs" :key="job[0]"> <MkA :to="`/instance-info/${job[0]}`" behavior="window">{{ job[0] }}</MkA> @@ -150,7 +150,4 @@ onUnmounted(() => { font-size: 80%; opacity: 0.6; } - -.jobs { -} </style> diff --git a/packages/frontend/src/pages/admin/server-rules.vue b/packages/frontend/src/pages/admin/server-rules.vue index 2e4aba5fe4..fdba4f464e 100644 --- a/packages/frontend/src/pages/admin/server-rules.vue +++ b/packages/frontend/src/pages/admin/server-rules.vue @@ -27,7 +27,7 @@ </Sortable> <div :class="$style.commands"> <MkButton rounded @click="serverRules.push('')"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton> - <MkButton primary rounded :class="$style.buttonSave" @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> + <MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> </div> </div> </MkSpacer> diff --git a/packages/frontend/src/pages/settings/preferences-backups.vue b/packages/frontend/src/pages/settings/preferences-backups.vue index 86f633c445..e34901cd11 100644 --- a/packages/frontend/src/pages/settings/preferences-backups.vue +++ b/packages/frontend/src/pages/settings/preferences-backups.vue @@ -32,7 +32,7 @@ </template> <script lang="ts" setup> -import { computed, onMounted, onUnmounted, useCssModule } from 'vue'; +import { computed, onMounted, onUnmounted } from 'vue'; import { v4 as uuid } from 'uuid'; import FormSection from '@/components/form/section.vue'; import MkButton from '@/components/MkButton.vue'; @@ -48,8 +48,6 @@ import { definePageMetadata } from '@/scripts/page-metadata'; import { miLocalStorage } from '@/local-storage'; const { t, ts } = i18n; -useCssModule(); - const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [ 'menu', 'visibility', diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue index 11e891b784..58217d0475 100644 --- a/packages/frontend/src/pages/settings/profile.vue +++ b/packages/frontend/src/pages/settings/profile.vue @@ -3,7 +3,7 @@ <div :class="$style.avatarAndBanner" :style="{ backgroundImage: $i.bannerUrl ? `url(${ $i.bannerUrl })` : null }"> <div :class="$style.avatarContainer"> <MkAvatar :class="$style.avatar" :user="$i" @click="changeAvatar"/> - <MkButton primary rounded :class="$style.avatarEdit" @click="changeAvatar">{{ i18n.ts._profile.changeAvatar }}</MkButton> + <MkButton primary rounded @click="changeAvatar">{{ i18n.ts._profile.changeAvatar }}</MkButton> </div> <MkButton primary rounded :class="$style.bannerEdit" @click="changeBanner">{{ i18n.ts._profile.changeBanner }}</MkButton> </div> @@ -271,10 +271,6 @@ definePageMetadata({ margin: 0 auto 16px auto; } -.avatarEdit { - -} - .bannerEdit { position: absolute; top: 16px; diff --git a/packages/frontend/src/pages/signup-complete.vue b/packages/frontend/src/pages/signup-complete.vue index 2700601c44..61d7eb24fd 100644 --- a/packages/frontend/src/pages/signup-complete.vue +++ b/packages/frontend/src/pages/signup-complete.vue @@ -1,5 +1,5 @@ <template> -<div :class="$style.root"> +<div> <MkAnimBg style="position: fixed; top: 0;"/> <div :class="$style.formContainer"> <form :class="$style.form" class="_panel" @submit.prevent="submit()"> @@ -53,9 +53,6 @@ function submit() { </script> <style lang="scss" module> -.root { -} - .formContainer { min-height: 100svh; padding: 32px 32px 64px 32px; diff --git a/packages/frontend/src/pages/welcome.setup.vue b/packages/frontend/src/pages/welcome.setup.vue index 5a68292b1b..2081cb9c93 100644 --- a/packages/frontend/src/pages/welcome.setup.vue +++ b/packages/frontend/src/pages/welcome.setup.vue @@ -1,5 +1,5 @@ <template> -<div :class="$style.root"> +<div> <MkAnimBg style="position: fixed; top: 0;"/> <div :class="$style.formContainer"> <form :class="$style.form" class="_panel" @submit.prevent="submit()"> @@ -64,9 +64,6 @@ function submit() { </script> <style lang="scss" module> -.root { -} - .formContainer { min-height: 100svh; padding: 32px 32px 64px 32px; diff --git a/packages/frontend/src/pages/welcome.timeline.vue b/packages/frontend/src/pages/welcome.timeline.vue index 46a0775579..a93d103d4f 100644 --- a/packages/frontend/src/pages/welcome.timeline.vue +++ b/packages/frontend/src/pages/welcome.timeline.vue @@ -3,7 +3,7 @@ <div ref="scrollEl" :class="[$style.scrollbox, { [$style.scroll]: isScrolling }]"> <div v-for="note in notes" :key="note.id" :class="$style.note"> <div class="_panel" :class="$style.content"> - <div :class="$style.body"> + <div> <MkA v-if="note.replyId" class="reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA> <Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i"/> <MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA> diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue index 4600d2af8e..3b970eefbe 100644 --- a/packages/frontend/src/ui/_common_/common.vue +++ b/packages/frontend/src/ui/_common_/common.vue @@ -10,7 +10,15 @@ <XUpload v-if="uploads.length > 0"/> <TransitionGroup - tag="div" :class="[$style.notifications, $style[`notificationsPosition-${defaultStore.state.notificationPosition}`], $style[`notificationsStackAxis-${defaultStore.state.notificationStackAxis}`]]" + tag="div" + :class="[$style.notifications, { + [$style.notificationsPosition_leftTop]: defaultStore.state.notificationPosition === 'leftTop', + [$style.notificationsPosition_leftBottom]: defaultStore.state.notificationPosition === 'leftBottom', + [$style.notificationsPosition_rightTop]: defaultStore.state.notificationPosition === 'rightTop', + [$style.notificationsPosition_rightBottom]: defaultStore.state.notificationPosition === 'rightBottom', + [$style.notificationsStackAxis_vertical]: defaultStore.state.notificationStackAxis === 'vertical', + [$style.notificationsStackAxis_horizontal]: defaultStore.state.notificationStackAxis === 'horizontal', + }]" :moveClass="defaultStore.state.animation ? $style.transition_notification_move : ''" :enterActiveClass="defaultStore.state.animation ? $style.transition_notification_enterActive : ''" :leaveActiveClass="defaultStore.state.animation ? $style.transition_notification_leaveActive : ''" @@ -103,31 +111,31 @@ if ($i) { pointer-events: none; display: flex; - &.notificationsPosition-leftTop { + &.notificationsPosition_leftTop { top: var(--margin); left: 0; } - &.notificationsPosition-rightTop { + &.notificationsPosition_rightTop { top: var(--margin); right: 0; } - &.notificationsPosition-leftBottom { + &.notificationsPosition_leftBottom { bottom: calc(var(--minBottomSpacing) + var(--margin)); left: 0; } - &.notificationsPosition-rightBottom { + &.notificationsPosition_rightBottom { bottom: calc(var(--minBottomSpacing) + var(--margin)); right: 0; } - &.notificationsStackAxis-vertical { + &.notificationsStackAxis_vertical { width: 250px; - &.notificationsPosition-leftTop, - &.notificationsPosition-rightTop { + &.notificationsPosition_leftTop, + &.notificationsPosition_rightTop { flex-direction: column; .notification { @@ -137,8 +145,8 @@ if ($i) { } } - &.notificationsPosition-leftBottom, - &.notificationsPosition-rightBottom { + &.notificationsPosition_leftBottom, + &.notificationsPosition_rightBottom { flex-direction: column-reverse; .notification { @@ -149,11 +157,11 @@ if ($i) { } } - &.notificationsStackAxis-horizontal { + &.notificationsStackAxis_horizontal { width: 100%; - &.notificationsPosition-leftTop, - &.notificationsPosition-leftBottom { + &.notificationsPosition_leftTop, + &.notificationsPosition_leftBottom { flex-direction: row; .notification { @@ -163,8 +171,8 @@ if ($i) { } } - &.notificationsPosition-rightTop, - &.notificationsPosition-rightBottom { + &.notificationsPosition_rightTop, + &.notificationsPosition_rightBottom { flex-direction: row-reverse; .notification { diff --git a/packages/frontend/src/ui/_common_/statusbar-federation.vue b/packages/frontend/src/ui/_common_/statusbar-federation.vue index 4ea84c82c2..6f2e4bc9a7 100644 --- a/packages/frontend/src/ui/_common_/statusbar-federation.vue +++ b/packages/frontend/src/ui/_common_/statusbar-federation.vue @@ -14,7 +14,7 @@ <MkA :to="`/instance-info/${instance.host}`" :class="$style.host" class="_monospace"> {{ instance.host }} </MkA> - <span :class="$style.divider"></span> + <span></span> </span> </MarqueeText> </Transition> diff --git a/packages/frontend/src/ui/_common_/statusbars.vue b/packages/frontend/src/ui/_common_/statusbars.vue index bb245c7dd8..3533972cdf 100644 --- a/packages/frontend/src/ui/_common_/statusbars.vue +++ b/packages/frontend/src/ui/_common_/statusbars.vue @@ -4,7 +4,6 @@ v-for="x in defaultStore.reactiveState.statusbars.value" :key="x.id" :class="[$style.item, { [$style.black]: x.black, [$style.verySmall]: x.size === 'verySmall', [$style.small]: x.size === 'small', - [$style.medium]: x.size === 'medium', [$style.large]: x.size === 'large', [$style.veryLarge]: x.size === 'veryLarge', }]" diff --git a/packages/frontend/src/ui/_common_/stream-indicator.vue b/packages/frontend/src/ui/_common_/stream-indicator.vue index 3e97df7ee5..74c475fc7d 100644 --- a/packages/frontend/src/ui/_common_/stream-indicator.vue +++ b/packages/frontend/src/ui/_common_/stream-indicator.vue @@ -2,8 +2,8 @@ <div v-if="hasDisconnected && defaultStore.state.serverDisconnectedBehavior === 'quiet'" :class="$style.root" class="_panel _shadow" @click="resetDisconnected"> <div><i class="ti ti-alert-triangle"></i> {{ i18n.ts.disconnectedFromServer }}</div> <div :class="$style.command" class="_buttons"> - <MkButton :class="$style.commandButton" small primary @click="reload">{{ i18n.ts.reload }}</MkButton> - <MkButton :class="$style.commandButton" small>{{ i18n.ts.doNothing }}</MkButton> + <MkButton small primary @click="reload">{{ i18n.ts.reload }}</MkButton> + <MkButton small>{{ i18n.ts.doNothing }}</MkButton> </div> </div> </template> @@ -54,7 +54,4 @@ onUnmounted(() => { .command { margin-top: 8px; } - -.commandButton { -} </style> diff --git a/packages/frontend/src/ui/deck/column.vue b/packages/frontend/src/ui/deck/column.vue index d95f826f20..8296c46691 100644 --- a/packages/frontend/src/ui/deck/column.vue +++ b/packages/frontend/src/ui/deck/column.vue @@ -1,6 +1,6 @@ <template> <div - :class="[$style.root, { [$style.paged]: isMainColumn, [$style.naked]: naked, [$style.active]: active, [$style.isStacked]: isStacked, [$style.draghover]: draghover, [$style.dragging]: dragging, [$style.dropready]: dropready }]" + :class="[$style.root, { [$style.paged]: isMainColumn, [$style.naked]: naked, [$style.active]: active, [$style.draghover]: draghover, [$style.dragging]: dragging, [$style.dropready]: dropready }]" @dragover.prevent.stop="onDragover" @dragleave="onDragleave" @drop.prevent.stop="onDrop" diff --git a/packages/frontend/src/ui/universal.widgets.vue b/packages/frontend/src/ui/universal.widgets.vue index 1ee95bf06e..ec5e8bb03f 100644 --- a/packages/frontend/src/ui/universal.widgets.vue +++ b/packages/frontend/src/ui/universal.widgets.vue @@ -1,6 +1,6 @@ <template> -<div :class="$style.root"> - <XWidgets :class="$style.widgets" :edit="editMode" :widgets="widgets" @addWidget="addWidget" @removeWidget="removeWidget" @updateWidget="updateWidget" @updateWidgets="updateWidgets" @exit="editMode = false"/> +<div> + <XWidgets :edit="editMode" :widgets="widgets" @addWidget="addWidget" @removeWidget="removeWidget" @updateWidget="updateWidget" @updateWidgets="updateWidgets" @exit="editMode = false"/> <button v-if="editMode" class="_textButton" style="font-size: 0.9em;" @click="editMode = false"><i class="ti ti-check"></i> {{ i18n.ts.editWidgetsExit }}</button> <button v-else class="_textButton" data-cy-widget-edit :class="$style.edit" style="font-size: 0.9em;" @click="editMode = true"><i class="ti ti-pencil"></i> {{ i18n.ts.editWidgets }}</button> @@ -70,9 +70,6 @@ function updateWidgets(thisWidgets) { </script> <style lang="scss" module> -.root { -} - .edit { width: 100%; } diff --git a/packages/frontend/src/widgets/WidgetClock.vue b/packages/frontend/src/widgets/WidgetClock.vue index 707f403603..aee5026db4 100644 --- a/packages/frontend/src/widgets/WidgetClock.vue +++ b/packages/frontend/src/widgets/WidgetClock.vue @@ -1,6 +1,12 @@ <template> <MkContainer :naked="widgetProps.transparent" :showHeader="false" data-cy-mkw-clock> - <div :class="[$style.root, $style[widgetProps.size]]"> + <div + :class="[$style.root, { + [$style.small]: widgetProps.size === 'small', + [$style.medium]: widgetProps.size === 'medium', + [$style.large]: widgetProps.size === 'large', + }]" + > <div v-if="widgetProps.label === 'tz' || widgetProps.label === 'timeAndTz'" class="_monospace" :class="[$style.label, $style.a]">{{ tzAbbrev }}</div> <MkAnalogClock :class="$style.clock" diff --git a/packages/frontend/src/widgets/WidgetRssTicker.vue b/packages/frontend/src/widgets/WidgetRssTicker.vue index 135ca37026..6b346c0598 100644 --- a/packages/frontend/src/widgets/WidgetRssTicker.vue +++ b/packages/frontend/src/widgets/WidgetRssTicker.vue @@ -12,7 +12,7 @@ <Transition :name="$style.change" mode="default" appear> <MarqueeText :key="key" :duration="widgetProps.duration" :reverse="widgetProps.reverse"> <span v-for="item in items" :key="item.link" :class="$style.item"> - <a :class="$style.link" :href="item.link" rel="nofollow noopener" target="_blank" :title="item.title">{{ item.title }}</a><span :class="$style.divider"></span> + <a :href="item.link" rel="nofollow noopener" target="_blank" :title="item.title">{{ item.title }}</a><span :class="$style.divider"></span> </span> </MarqueeText> </Transition> diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts index 482c98fe88..531dd0b488 100644 --- a/packages/frontend/vite.config.ts +++ b/packages/frontend/vite.config.ts @@ -8,6 +8,7 @@ import ReactivityTransform from '@vue-macros/reactivity-transform/vite'; import locales from '../../locales'; import generateDTS from '../../locales/generateDTS'; import meta from '../../package.json'; +import pluginUnwindCssModuleClassName from './lib/rollup-plugin-unwind-css-module-class-name'; import pluginJson5 from './vite.json5'; const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.json', '.json5', '.svg', '.sass', '.scss', '.css', '.vue']; @@ -54,6 +55,7 @@ export function getConfig(): UserConfig { reactivityTransform: true, }), ReactivityTransform(), + pluginUnwindCssModuleClassName(), pluginJson5(), ...process.env.NODE_ENV === 'production' ? [ |