1
0
mirror of https://github.com/misskey-dev/misskey synced 2024-11-27 22:38:39 +09:00

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 5c7ef366ec.

* 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>
This commit is contained in:
Acid Chicken (硫酸鶏) 2023-06-01 17:19:46 +09:00 committed by GitHub
parent 146e89edd0
commit 337dd97b49
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
53 changed files with 2295 additions and 368 deletions

View File

@ -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));
});

View File

@ -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) };
},
};
}

View File

@ -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",

View File

@ -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>

View File

@ -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>

View File

@ -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';

View File

@ -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;
}

View File

@ -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>

View File

@ -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;

View File

@ -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>

View File

@ -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;

View File

@ -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?: {

View File

@ -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;
}

View File

@ -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>

View File

@ -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>

View File

@ -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;
}

View File

@ -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"/>

View File

@ -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"/>

View File

@ -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>

View File

@ -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%;

View File

@ -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>

View File

@ -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;">

View File

@ -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);

View File

@ -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"

View File

@ -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}&amp;hideCard=false&amp;hideThread=false&amp;lang=en&amp;theme=${defaultStore.state.darkMode ? 'dark' : 'light'}&amp;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;

View File

@ -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>

View File

@ -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);

View File

@ -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;

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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,

View File

@ -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>

View File

@ -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"/>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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',

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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>

View File

@ -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 {

View File

@ -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>

View File

@ -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',
}]"

View File

@ -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>

View File

@ -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"

View File

@ -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%;
}

View File

@ -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"

View File

@ -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>

View File

@ -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'
? [

File diff suppressed because it is too large Load Diff