From 6b96bd01854ecfb7b0ee816831ff4634af8af856 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 2 Oct 2018 16:04:31 +0900
Subject: [PATCH] =?UTF-8?q?=E3=83=86=E3=83=BC=E3=83=9E=E3=81=AB=E9=96=A2?=
 =?UTF-8?q?=E3=81=97=E3=81=A6=E5=BC=B7=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 package.json                                  |   2 +
 src/client/app/app.vue                        |   3 -
 src/client/app/boot.js                        |   1 -
 .../app/common/views/components/theme.vue     | 103 ++++++---
 src/client/app/init.ts                        |  11 +-
 src/client/app/theme.ts                       |  62 +++---
 src/client/theme/dark.json                    | 204 -----------------
 src/client/theme/dark.json5                   | 207 ++++++++++++++++++
 src/client/theme/halloween.json               |  17 --
 src/client/theme/halloween.json5              |  21 ++
 src/client/theme/light.json                   | 204 -----------------
 src/client/theme/light.json5                  | 207 ++++++++++++++++++
 src/client/theme/pink.json                    |  17 --
 src/client/theme/pink.json5                   |  20 ++
 webpack.config.ts                             |   3 +
 15 files changed, 569 insertions(+), 513 deletions(-)
 delete mode 100644 src/client/theme/dark.json
 create mode 100644 src/client/theme/dark.json5
 delete mode 100644 src/client/theme/halloween.json
 create mode 100644 src/client/theme/halloween.json5
 delete mode 100644 src/client/theme/light.json
 create mode 100644 src/client/theme/light.json5
 delete mode 100644 src/client/theme/pink.json
 create mode 100644 src/client/theme/pink.json5

diff --git a/package.json b/package.json
index 17603790f0..c12b8861c6 100644
--- a/package.json
+++ b/package.json
@@ -134,6 +134,8 @@
 		"is-url": "1.2.4",
 		"js-yaml": "3.12.0",
 		"jsdom": "11.12.0",
+		"json5": "2.1.0",
+		"json5-loader": "1.0.1",
 		"koa": "2.5.1",
 		"koa-bodyparser": "4.2.1",
 		"koa-compress": "3.0.0",
diff --git a/src/client/app/app.vue b/src/client/app/app.vue
index 778e9f29cf..e639c9f9ac 100644
--- a/src/client/app/app.vue
+++ b/src/client/app/app.vue
@@ -5,9 +5,6 @@
 <script lang="ts">
 import Vue from 'vue';
 import { url, lang } from './config';
-import applyTheme from './common/scripts/theme';
-const darkTheme = require('../theme/dark');
-const halloweenTheme = require('../theme/halloween');
 
 export default Vue.extend({
 	computed: {
diff --git a/src/client/app/boot.js b/src/client/app/boot.js
index e122e0423a..6e06a88aa3 100644
--- a/src/client/app/boot.js
+++ b/src/client/app/boot.js
@@ -24,7 +24,6 @@
 	const theme = localStorage.getItem('theme');
 	if (theme) {
 		Object.entries(JSON.parse(theme)).forEach(([k, v]) => {
-			if (k == 'meta') return;
 			document.documentElement.style.setProperty(`--${k}`, v.toString());
 		});
 	}
diff --git a/src/client/app/common/views/components/theme.vue b/src/client/app/common/views/components/theme.vue
index 56b07da498..293238e542 100644
--- a/src/client/app/common/views/components/theme.vue
+++ b/src/client/app/common/views/components/theme.vue
@@ -3,14 +3,14 @@
 	<label>
 		<span>%i18n:@light-theme%</span>
 		<ui-select v-model="light" placeholder="%i18n:@light-theme%">
-			<option v-for="x in themes" :value="x.meta.id" :key="x.meta.id">{{ x.meta.name }}</option>
+			<option v-for="x in themes" :value="x.id" :key="x.id">{{ x.name }}</option>
 		</ui-select>
 	</label>
 
 	<label>
 		<span>%i18n:@dark-theme%</span>
 		<ui-select v-model="dark" placeholder="%i18n:@dark-theme%">
-			<option v-for="x in themes" :value="x.meta.id" :key="x.meta.id">{{ x.meta.name }}</option>
+			<option v-for="x in themes" :value="x.id" :key="x.id">{{ x.name }}</option>
 		</ui-select>
 	</label>
 
@@ -53,7 +53,7 @@
 	<details>
 		<summary>%i18n:@installed-themes%</summary>
 		<ui-select v-model="selectedInstalledTheme" placeholder="%i18n:@select-theme%">
-			<option v-for="x in installedThemes" :value="x.meta.id" :key="x.meta.id">{{ x.meta.name }}</option>
+			<option v-for="x in installedThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
 		</ui-select>
 		<ui-textarea readonly :value="selectedInstalledThemeCode">
 			<span>%i18n:@theme-code%</span>
@@ -65,10 +65,25 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import { lightTheme, darkTheme, builtinThemes, applyTheme } from '../../../theme';
+import { lightTheme, darkTheme, builtinThemes, applyTheme, Theme } from '../../../theme';
 import { Chrome } from 'vue-color';
 import * as uuid from 'uuid';
 import * as tinycolor from 'tinycolor2';
+import * as JSON5 from 'json5';
+
+// 後方互換性のため
+function convertOldThemedefinition(t) {
+	const t2 = {
+		id: t.meta.id,
+		name: t.meta.name,
+		author: t.meta.author,
+		base: t.meta.base,
+		vars: t.meta.vars,
+		props: t
+	};
+	delete t2.props.meta;
+	return t2;
+}
 
 export default Vue.extend({
 	components: {
@@ -81,18 +96,18 @@ export default Vue.extend({
 			selectedInstalledTheme: null,
 			myThemeBase: 'light',
 			myThemeName: '',
-			myThemePrimary: lightTheme.meta.vars.primary,
-			myThemeSecondary: lightTheme.meta.vars.secondary,
-			myThemeText: lightTheme.meta.vars.text
+			myThemePrimary: lightTheme.vars.primary,
+			myThemeSecondary: lightTheme.vars.secondary,
+			myThemeText: lightTheme.vars.text
 		};
 	},
 
 	computed: {
-		themes(): any {
+		themes(): Theme[] {
 			return this.$store.state.device.themes.concat(builtinThemes);
 		},
 
-		installedThemes(): any {
+		installedThemes(): Theme[] {
 			return this.$store.state.device.themes;
 		},
 
@@ -108,20 +123,18 @@ export default Vue.extend({
 
 		selectedInstalledThemeCode() {
 			if (this.selectedInstalledTheme == null) return null;
-			return JSON.stringify(this.installedThemes.find(x => x.meta.id == this.selectedInstalledTheme));
+			return JSON5.stringify(this.installedThemes.find(x => x.id == this.selectedInstalledTheme), null, '\t');
 		},
 
 		myTheme(): any {
 			return {
-				meta: {
-					name: this.myThemeName,
-					author: this.$store.state.i.name,
-					base: this.myThemeBase,
-					vars: {
-						primary: tinycolor(typeof this.myThemePrimary == 'string' ? this.myThemePrimary : this.myThemePrimary.rgba).toRgbString(),
-						secondary: tinycolor(typeof this.myThemeSecondary == 'string' ? this.myThemeSecondary : this.myThemeSecondary.rgba).toRgbString(),
-						text: tinycolor(typeof this.myThemeText == 'string' ? this.myThemeText : this.myThemeText.rgba).toRgbString()
-					}
+				name: this.myThemeName,
+				author: this.$store.state.i.name,
+				base: this.myThemeBase,
+				vars: {
+					primary: tinycolor(typeof this.myThemePrimary == 'string' ? this.myThemePrimary : this.myThemePrimary.rgba).toRgbString(),
+					secondary: tinycolor(typeof this.myThemeSecondary == 'string' ? this.myThemeSecondary : this.myThemeSecondary.rgba).toRgbString(),
+					text: tinycolor(typeof this.myThemeText == 'string' ? this.myThemeText : this.myThemeText.rgba).toRgbString()
 				}
 			};
 		}
@@ -130,37 +143,67 @@ export default Vue.extend({
 	watch: {
 		myThemeBase(v) {
 			const theme = v == 'light' ? lightTheme : darkTheme;
-			this.myThemePrimary = theme.meta.vars.primary;
-			this.myThemeSecondary = theme.meta.vars.secondary;
-			this.myThemeText = theme.meta.vars.text;
+			this.myThemePrimary = theme.vars.primary;
+			this.myThemeSecondary = theme.vars.secondary;
+			this.myThemeText = theme.vars.text;
 		}
 	},
 
+	beforeCreate() {
+		// migrate old theme definitions
+		// 後方互換性のため
+		this.$store.commit('device/set', {
+			key: 'themes', value: this.$store.state.device.themes.map(t => {
+				if (t.id == null) {
+					return convertOldThemedefinition(t);
+				} else {
+					return t;
+				}
+			})
+		});
+	},
+
 	methods: {
 		install() {
-			const theme = JSON.parse(this.installThemeCode);
-			if (theme.meta == null || theme.meta.id == null) {
+			let theme;
+
+			try {
+				theme = JSON5.parse(this.installThemeCode);
+			} catch (e) {
 				alert('%i18n:@invalid-theme%');
 				return;
 			}
-			if (this.$store.state.device.themes.some(t => t.meta.id == theme.meta.id)) {
+
+			// 後方互換性のため
+			if (theme.id == null && theme.meta != null) {
+				theme = convertOldThemedefinition(theme);
+			}
+
+			if (theme.id == null) {
+				alert('%i18n:@invalid-theme%');
+				return;
+			}
+
+			if (this.$store.state.device.themes.some(t => t.id == theme.id)) {
 				alert('%i18n:@already-installed%');
 				return;
 			}
+
 			const themes = this.$store.state.device.themes.concat(theme);
 			this.$store.commit('device/set', {
 				key: 'themes', value: themes
 			});
-			alert('%i18n:@installed%'.replace('{}', theme.meta.name));
+
+			alert('%i18n:@installed%'.replace('{}', theme.name));
 		},
 
 		uninstall() {
-			const theme = this.installedThemes.find(x => x.meta.id == this.selectedInstalledTheme);
-			const themes = this.$store.state.device.themes.filter(t => t.meta.id != theme.meta.id);
+			const theme = this.installedThemes.find(x => x.id == this.selectedInstalledTheme);
+			const themes = this.$store.state.device.themes.filter(t => t.id != theme.id);
 			this.$store.commit('device/set', {
 				key: 'themes', value: themes
 			});
-			alert('%i18n:@uninstalled%'.replace('{}', theme.meta.name));
+			alert('%i18n:@uninstalled%'.replace('{}', theme.name));
 		},
 
 		preview() {
@@ -169,7 +212,7 @@ export default Vue.extend({
 
 		gen() {
 			const theme = this.myTheme;
-			theme.meta.id = uuid();
+			theme.id = uuid();
 			const themes = this.$store.state.device.themes.concat(theme);
 			this.$store.commit('device/set', {
 				key: 'themes', value: themes
diff --git a/src/client/app/init.ts b/src/client/app/init.ts
index 802f7b42eb..c2381067da 100644
--- a/src/client/app/init.ts
+++ b/src/client/app/init.ts
@@ -14,8 +14,7 @@ import App from './app.vue';
 import checkForUpdate from './common/scripts/check-for-update';
 import MiOS, { API } from './mios';
 import { version, codename, lang } from './config';
-import { builtinThemes, applyTheme } from './theme';
-const lightTheme = require('../theme/light.json');
+import { builtinThemes, lightTheme, applyTheme } from './theme';
 
 if (localStorage.getItem('theme') == null) {
 	applyTheme(lightTheme);
@@ -97,15 +96,15 @@ export default (callback: (launch: (router: VueRouter, api?: (os: MiOS) => API)
 				return s.device.darkmode;
 			}, v => {
 				const themes = os.store.state.device.themes.concat(builtinThemes);
-				const dark = themes.find(t => t.meta.id == os.store.state.device.darkTheme);
-				const light = themes.find(t => t.meta.id == os.store.state.device.lightTheme);
+				const dark = themes.find(t => t.id == os.store.state.device.darkTheme);
+				const light = themes.find(t => t.id == os.store.state.device.lightTheme);
 				applyTheme(v ? dark : light);
 			});
 			os.store.watch(s => {
 				return s.device.lightTheme;
 			}, v => {
 				const themes = os.store.state.device.themes.concat(builtinThemes);
-				const theme = themes.find(t => t.meta.id == v);
+				const theme = themes.find(t => t.id == v);
 				if (!os.store.state.device.darkmode) {
 					applyTheme(theme);
 				}
@@ -114,7 +113,7 @@ export default (callback: (launch: (router: VueRouter, api?: (os: MiOS) => API)
 				return s.device.darkTheme;
 			}, v => {
 				const themes = os.store.state.device.themes.concat(builtinThemes);
-				const theme = themes.find(t => t.meta.id == v);
+				const theme = themes.find(t => t.id == v);
 				if (os.store.state.device.darkmode) {
 					applyTheme(theme);
 				}
diff --git a/src/client/app/theme.ts b/src/client/app/theme.ts
index 555f8411f2..828ce33862 100644
--- a/src/client/app/theme.ts
+++ b/src/client/app/theme.ts
@@ -1,27 +1,40 @@
 import * as tinycolor from 'tinycolor2';
 
-type Theme = {
-	meta: {
-		id: string;
-		name: string;
-		author: string;
-		base?: string;
-		vars: any;
-	};
-} & {
-	[key: string]: string;
+export type Theme = {
+	id: string;
+	name: string;
+	author: string;
+	desc?: string;
+	base?: 'dark' | 'light';
+	vars: { [key: string]: string };
+	props: { [key: string]: string };
 };
 
+export const lightTheme: Theme = require('../theme/light.json5');
+export const darkTheme: Theme = require('../theme/dark.json5');
+export const pinkTheme: Theme = require('../theme/pink.json5');
+export const halloweenTheme: Theme = require('../theme/halloween.json5');
+
+export const builtinThemes = [
+	lightTheme,
+	darkTheme,
+	pinkTheme,
+	halloweenTheme
+];
+
 export function applyTheme(theme: Theme, persisted = true) {
-	if (theme.meta.base) {
-		const base = [lightTheme, darkTheme].find(x => x.meta.id == theme.meta.base);
-		theme = Object.assign({}, base, theme);
+	// Deep copy
+	const _theme = JSON.parse(JSON.stringify(theme));
+
+	if (_theme.base) {
+		const base = [lightTheme, darkTheme].find(x => x.id == _theme.base);
+		_theme.vars = Object.assign({}, base.vars, _theme.vars);
+		_theme.props = Object.assign({}, base.props, _theme.props);
 	}
 
-	const props = compile(theme);
+	const props = compile(_theme);
 
 	Object.entries(props).forEach(([k, v]) => {
-		if (k == 'meta') return;
 		document.documentElement.style.setProperty(`--${k}`, v.toString());
 	});
 
@@ -34,10 +47,10 @@ function compile(theme: Theme): { [key: string]: string } {
 	function getColor(code: string): tinycolor.Instance {
 		// ref
 		if (code[0] == '@') {
-			return getColor(theme[code.substr(1)]);
+			return getColor(theme.props[code.substr(1)]);
 		}
 		if (code[0] == '$') {
-			return getColor(theme.meta.vars[code.substr(1)]);
+			return getColor(theme.vars[code.substr(1)]);
 		}
 
 		// func
@@ -59,8 +72,7 @@ function compile(theme: Theme): { [key: string]: string } {
 
 	const props = {};
 
-	Object.entries(theme).forEach(([k, v]) => {
-		if (k == 'meta') return;
+	Object.entries(theme.props).forEach(([k, v]) => {
 		const c = getColor(v);
 		props[k] = genValue(c);
 	});
@@ -88,15 +100,3 @@ function compile(theme: Theme): { [key: string]: string } {
 function genValue(c: tinycolor.Instance): string {
 	return c.toRgbString();
 }
-
-export const lightTheme = require('../theme/light.json');
-export const darkTheme = require('../theme/dark.json');
-export const pinkTheme = require('../theme/pink.json');
-export const halloweenTheme = require('../theme/halloween.json');
-
-export const builtinThemes = [
-	lightTheme,
-	darkTheme,
-	pinkTheme,
-	halloweenTheme
-];
diff --git a/src/client/theme/dark.json b/src/client/theme/dark.json
deleted file mode 100644
index 74447b8f2f..0000000000
--- a/src/client/theme/dark.json
+++ /dev/null
@@ -1,204 +0,0 @@
-{
-	"meta": {
-		"id": "dark",
-		"name": "Dark",
-		"author": "syuilo",
-		"vars": {
-			"primary": "#fb4e4e",
-			"secondary": "#282C37",
-			"text": "#d6dae0"
-		}
-	},
-
-	"primary": "$primary",
-	"primaryForeground": "#fff",
-	"secondary": "$secondary",
-	"bg": ":darken<8<$secondary",
-	"text": "$text",
-
-	"scrollbarTrack": ":darken<5<$secondary",
-	"scrollbarHandle": ":lighten<5<$secondary",
-	"scrollbarHandleHover": ":lighten<10<$secondary",
-
-	"face": "$secondary",
-	"faceText": "#fff",
-	"faceHeader": ":lighten<5<$secondary",
-	"faceHeaderText": "#e3e5e8",
-	"faceDivider": "rgba(0, 0, 0, 0.3)",
-	"faceTextButton": "#9baec8",
-	"faceTextButtonHover": "#b2c1d5",
-	"faceTextButtonActive": "#b2c1d5",
-	"faceClearButtonHover": "rgba(0, 0, 0, 0.1)",
-	"faceClearButtonActive": "rgba(0, 0, 0, 0.2)",
-	"popupBg": ":lighten<5<$secondary",
-	"popupFg": "#d6dce2",
-
-	"subNoteBg": "rgba(0, 0, 0, 0.18)",
-	"subNoteText": ":alpha<0.7<$text",
-	"renoteGradient": "#314027",
-	"renoteText": "#9dbb00",
-	"quoteBorder": "#4e945e",
-	"noteText": "#fff",
-	"noteHeaderName": "#fff",
-	"noteHeaderBadgeFg": "#758188",
-	"noteHeaderBadgeBg": "rgba(0, 0, 0, 0.25)",
-	"noteHeaderAdminFg": "#f15f71",
-	"noteHeaderAdminBg": "#5d282e",
-	"noteHeaderAcct": "#606984",
-	"noteHeaderInfo": "#606984",
-
-	"noteActions": "#606984",
-	"noteActionsHover": "#a1a8bf",
-	"noteActionsReplyHover": "#0af",
-	"noteActionsRenoteHover": "#8d0",
-	"noteActionsReactionHover": "#fa0",
-	"noteActionsHighlighted": "#707b97",
-
-	"noteAttachedFile": "rgba(255, 255, 255, 0.1)",
-
-	"modalBackdrop": "rgba(0, 0, 0, 0.5)",
-
-	"dateDividerBg": ":darken<2<$secondary",
-	"dateDividerFg": ":alpha<0.7<$text",
-
-	"switchTrack": "rgba(255, 255, 255, 0.15)",
-	"radioBorder": "rgba(255, 255, 255, 0.6)",
-	"inputBorder": "rgba(255, 255, 255, 0.7)",
-	"inputLabel": "rgba(255, 255, 255, 0.7)",
-	"inputText": "#fff",
-
-	"buttonBg": "rgba(255, 255, 255, 0.05)",
-	"buttonHoverBg": "rgba(255, 255, 255, 0.1)",
-	"buttonActiveBg": "rgba(255, 255, 255, 0.15)",
-
-	"autocompleteItemHoverBg": "rgba(255, 255, 255, 0.1)",
-	"autocompleteItemText": "rgba(255, 255, 255, 0.8)",
-	"autocompleteItemTextSub": "rgba(255, 255, 255, 0.3)",
-
-	"cwButtonBg": "#687390",
-	"cwButtonFg": "#393f4f",
-	"cwButtonHoverBg": "#707b97",
-
-	"reactionPickerButtonHoverBg": "rgba(255, 255, 255, 0.18)",
-
-	"reactionViewerBorder": "rgba(255, 255, 255, 0.1)",
-
-	"pollEditorInputBg": "rgba(0, 0, 0, 0.25)",
-
-	"pollChoiceText": "#fff",
-	"pollChoiceBorder": "rgba(255, 255, 255, 0.1)",
-
-	"urlPreviewBorder": "rgba(0, 0, 0, 0.4)",
-	"urlPreviewBorderHover": "rgba(255, 255, 255, 0.2)",
-	"urlPreviewTitle": "$text",
-	"urlPreviewText": ":alpha<0.7<$text",
-	"urlPreviewInfo": ":alpha<0.8<$text",
-
-	"calendarWeek": "#43d5dc",
-	"calendarSaturdayOrSunday": "#ff6679",
-	"calendarDay": "#c5ced6",
-
-	"materBg": "rgba(0, 0, 0, 0.3)",
-
-	"chartCaption": ":alpha<0.6<$text",
-
-	"announcementsBg": "#253a50",
-	"announcementsTitle": "#539eff",
-	"announcementsText": "#fff",
-
-	"donationBg": "#5d5242",
-	"donationFg": "#e4dbce",
-
-	"googleSearchBg": "rgba(0, 0, 0, 0.2)",
-	"googleSearchFg": "#dee4e8",
-	"googleSearchBorder": "rgba(255, 255, 255, 0.2)",
-	"googleSearchHoverBorder": "rgba(255, 255, 255, 0.3)",
-	"googleSearchHoverButton": "rgba(255, 255, 255, 0.1)",
-
-	"mfmTitleBg": "rgba(0, 0, 0, 0.2)",
-	"mfmQuote": ":alpha<0.7<$text",
-	"mfmQuoteLine": ":alpha<0.6<$text",
-
-	"suspendedInfoBg": "#611d1d",
-	"suspendedInfoFg": "#ffb4b4",
-	"remoteInfoBg": "#42321c",
-	"remoteInfoFg": "#ffbd3e",
-
-	"messagingRoomBg": "@bg",
-	"messagingRoomInfo": "#fff",
-	"messagingRoomDateDividerLine": "rgba(255, 255, 255, 0.1)",
-	"messagingRoomDateDividerText": "rgba(255, 255, 255, 0.3)",
-	"messagingRoomMessageInfo": "rgba(255, 255, 255, 0.4)",
-	"messagingRoomMessageBg": "$secondary",
-	"messagingRoomMessageFg": "#fff",
-
-	"formButtonBorder": "rgba(255, 255, 255, 0.1)",
-	"formButtonHoverBg": ":alpha<0.2<$primary",
-	"formButtonHoverBorder": ":alpha<0.5<$primary",
-	"formButtonActiveBg": ":alpha<0.12<$primary",
-
-	"desktopHeaderBg": ":lighten<5<$secondary",
-	"desktopHeaderFg": "$text",
-	"desktopHeaderHoverFg": "#fff",
-	"desktopHeaderSearchBg": "rgba(0, 0, 0, 0.1)",
-	"desktopHeaderSearchHoverBg": "rgba(255, 255, 255, 0.04)",
-	"desktopHeaderSearchFg": "#fff",
-	"desktopNotificationBg": ":alpha<0.9<$secondary",
-	"desktopNotificationFg": ":alpha<0.7<$text",
-	"desktopNotificationShadow": "rgba(0, 0, 0, 0.4)",
-	"desktopPostFormBg": "@face",
-	"desktopPostFormTextareaBg": "rgba(0, 0, 0, 0.25)",
-	"desktopPostFormTextareaFg": "#fff",
-	"desktopPostFormTransparentButtonFg": "$primary",
-	"desktopPostFormTransparentButtonActiveGradientStart": ":darken<8<$secondary",
-	"desktopPostFormTransparentButtonActiveGradientEnd": ":darken<3<$secondary",
-	"desktopRenoteFormFooter": ":lighten<5<$secondary",
-	"desktopTimelineHeaderShadow": "rgba(0, 0, 0, 0.15)",
-	"desktopTimelineSrc": "@faceTextButton",
-	"desktopTimelineSrcHover": "@faceTextButtonHover",
-	"desktopWindowTitle": "@faceHeaderText",
-	"desktopWindowShadow": "rgba(0, 0, 0, 0.5)",
-	"desktopDriveBg": "@bg",
-	"desktopDriveFolderBg": ":alpha<0.2<$primary",
-	"desktopDriveFolderHoverBg": ":alpha<0.3<$primary",
-	"desktopDriveFolderActiveBg": ":alpha<0.3<:darken<10<$primary",
-	"desktopDriveFolderFg": "#fff",
-	"desktopSettingsNavItem": ":alpha<0.8<$text",
-	"desktopSettingsNavItemHover": ":lighten<10<$text",
-
-	"deckAcrylicColumnBg": "rgba(0, 0, 0, 0.25)",
-
-	"mobileHeaderBg": ":lighten<5<$secondary",
-	"mobileHeaderFg": "$text",
-	"mobileNavBackdrop": "rgba(0, 0, 0, 0.7)",
-	"mobilePostFormDivider": "rgba(0, 0, 0, 0.2)",
-	"mobilePostFormTextareaBg": "rgba(0, 0, 0, 0.3)",
-	"mobileDriveNavBg": ":alpha<0.75<$secondary",
-	"mobileHomeTlItemHover": "rgba(255, 255, 255, 0.1)",
-	"mobileUserPageName": "#fff",
-	"mobileUserPageAcct": "$text",
-	"mobileUserPageDescription": "$text",
-	"mobileUserPageFollowedBg": "rgba(0, 0, 0, 0.3)",
-	"mobileUserPageFollowedFg": "$text",
-	"mobileUserPageStatusHighlight": "#fff",
-	"mobileUserPageHeaderShadow": "rgba(0, 0, 0, 0.3)",
-	"mobileAnnouncement": "rgba(30, 129, 216, 0.2)",
-	"mobileAnnouncementFg": "#fff",
-	"mobileSignedInAsBg": "#273c34",
-	"mobileSignedInAsFg": "#49ab63",
-	"mobileSignoutBg": "#652222",
-	"mobileSignoutFg": "#ff5f56",
-
-	"reversiBannerGradientStart": "#45730e",
-	"reversiBannerGradientEnd": "#464300",
-	"reversiDescBg": "rgba(255, 255, 255, 0.1)",
-	"reversiListItemShadow": "rgba(0, 0, 0, 0.7)",
-	"reversiMapSelectBorder": "rgba(255, 255, 255, 0.1)",
-	"reversiMapSelectHoverBorder": "rgba(255, 255, 255, 0.2)",
-	"reversiRoomFormShadow": "rgba(0, 0, 0, 0.7)",
-	"reversiRoomFooterBg": ":alpha<0.9<$secondary",
-	"reversiGameHeaderLine": ":alpha<0.5<$secondary",
-	"reversiGameEmptyCell": ":lighten<2<$secondary",
-	"reversiGameEmptyCellMyTurn": ":lighten<5<$secondary",
-	"reversiGameEmptyCellCanPut": ":lighten<4<$secondary"
-}
diff --git a/src/client/theme/dark.json5 b/src/client/theme/dark.json5
new file mode 100644
index 0000000000..2042bd7931
--- /dev/null
+++ b/src/client/theme/dark.json5
@@ -0,0 +1,207 @@
+{
+	id: 'dark',
+
+	name: 'Dark',
+	author: 'syuilo',
+	desc: 'Default dark theme',
+
+	vars: {
+		primary: '#fb4e4e',
+		secondary: '#282C37',
+		text: '#d6dae0',
+	},
+
+	props: {
+		primary: '$primary',
+		primaryForeground: '#fff',
+		secondary: '$secondary',
+		bg: ':darken<8<$secondary',
+		text: '$text',
+
+		scrollbarTrack: ':darken<5<$secondary',
+		scrollbarHandle: ':lighten<5<$secondary',
+		scrollbarHandleHover: ':lighten<10<$secondary',
+
+		face: '$secondary',
+		faceText: '#fff',
+		faceHeader: ':lighten<5<$secondary',
+		faceHeaderText: '#e3e5e8',
+		faceDivider: 'rgba(0, 0, 0, 0.3)',
+		faceTextButton: '$text',
+		faceTextButtonHover: ':lighten<10<$text',
+		faceTextButtonActive: ':darken<10<$text',
+		faceClearButtonHover: 'rgba(0, 0, 0, 0.1)',
+		faceClearButtonActive: 'rgba(0, 0, 0, 0.2)',
+		popupBg: ':lighten<5<$secondary',
+		popupFg: '#d6dce2',
+
+		subNoteBg: 'rgba(0, 0, 0, 0.18)',
+		subNoteText: ':alpha<0.7<$text',
+		renoteGradient: '#314027',
+		renoteText: '#9dbb00',
+		quoteBorder: '#4e945e',
+		noteText: '#fff',
+		noteHeaderName: '#fff',
+		noteHeaderBadgeFg: '#758188',
+		noteHeaderBadgeBg: 'rgba(0, 0, 0, 0.25)',
+		noteHeaderAdminFg: '#f15f71',
+		noteHeaderAdminBg: '#5d282e',
+		noteHeaderAcct: '#606984',
+		noteHeaderInfo: '#606984',
+
+		noteActions: '#606984',
+		noteActionsHover: '#a1a8bf',
+		noteActionsReplyHover: '#0af',
+		noteActionsRenoteHover: '#8d0',
+		noteActionsReactionHover: '#fa0',
+		noteActionsHighlighted: '#707b97',
+
+		noteAttachedFile: 'rgba(255, 255, 255, 0.1)',
+
+		modalBackdrop: 'rgba(0, 0, 0, 0.5)',
+
+		dateDividerBg: ':darken<2<$secondary',
+		dateDividerFg: ':alpha<0.7<$text',
+
+		switchTrack: 'rgba(255, 255, 255, 0.15)',
+		radioBorder: 'rgba(255, 255, 255, 0.6)',
+		inputBorder: 'rgba(255, 255, 255, 0.7)',
+		inputLabel: 'rgba(255, 255, 255, 0.7)',
+		inputText: '#fff',
+
+		buttonBg: 'rgba(255, 255, 255, 0.05)',
+		buttonHoverBg: 'rgba(255, 255, 255, 0.1)',
+		buttonActiveBg: 'rgba(255, 255, 255, 0.15)',
+
+		autocompleteItemHoverBg: 'rgba(255, 255, 255, 0.1)',
+		autocompleteItemText: 'rgba(255, 255, 255, 0.8)',
+		autocompleteItemTextSub: 'rgba(255, 255, 255, 0.3)',
+
+		cwButtonBg: '#687390',
+		cwButtonFg: '#393f4f',
+		cwButtonHoverBg: '#707b97',
+
+		reactionPickerButtonHoverBg: 'rgba(255, 255, 255, 0.18)',
+
+		reactionViewerBorder: 'rgba(255, 255, 255, 0.1)',
+
+		pollEditorInputBg: 'rgba(0, 0, 0, 0.25)',
+
+		pollChoiceText: '#fff',
+		pollChoiceBorder: 'rgba(255, 255, 255, 0.1)',
+
+		urlPreviewBorder: 'rgba(0, 0, 0, 0.4)',
+		urlPreviewBorderHover: 'rgba(255, 255, 255, 0.2)',
+		urlPreviewTitle: '$text',
+		urlPreviewText: ':alpha<0.7<$text',
+		urlPreviewInfo: ':alpha<0.8<$text',
+
+		calendarWeek: '#43d5dc',
+		calendarSaturdayOrSunday: '#ff6679',
+		calendarDay: '#c5ced6',
+
+		materBg: 'rgba(0, 0, 0, 0.3)',
+
+		chartCaption: ':alpha<0.6<$text',
+
+		announcementsBg: '#253a50',
+		announcementsTitle: '#539eff',
+		announcementsText: '#fff',
+
+		donationBg: '#5d5242',
+		donationFg: '#e4dbce',
+
+		googleSearchBg: 'rgba(0, 0, 0, 0.2)',
+		googleSearchFg: '#dee4e8',
+		googleSearchBorder: 'rgba(255, 255, 255, 0.2)',
+		googleSearchHoverBorder: 'rgba(255, 255, 255, 0.3)',
+		googleSearchHoverButton: 'rgba(255, 255, 255, 0.1)',
+
+		mfmTitleBg: 'rgba(0, 0, 0, 0.2)',
+		mfmQuote: ':alpha<0.7<$text',
+		mfmQuoteLine: ':alpha<0.6<$text',
+
+		suspendedInfoBg: '#611d1d',
+		suspendedInfoFg: '#ffb4b4',
+		remoteInfoBg: '#42321c',
+		remoteInfoFg: '#ffbd3e',
+
+		messagingRoomBg: '@bg',
+		messagingRoomInfo: '#fff',
+		messagingRoomDateDividerLine: 'rgba(255, 255, 255, 0.1)',
+		messagingRoomDateDividerText: 'rgba(255, 255, 255, 0.3)',
+		messagingRoomMessageInfo: 'rgba(255, 255, 255, 0.4)',
+		messagingRoomMessageBg: '$secondary',
+		messagingRoomMessageFg: '#fff',
+
+		formButtonBorder: 'rgba(255, 255, 255, 0.1)',
+		formButtonHoverBg: ':alpha<0.2<$primary',
+		formButtonHoverBorder: ':alpha<0.5<$primary',
+		formButtonActiveBg: ':alpha<0.12<$primary',
+
+		desktopHeaderBg: ':lighten<5<$secondary',
+		desktopHeaderFg: '$text',
+		desktopHeaderHoverFg: '#fff',
+		desktopHeaderSearchBg: 'rgba(0, 0, 0, 0.1)',
+		desktopHeaderSearchHoverBg: 'rgba(255, 255, 255, 0.04)',
+		desktopHeaderSearchFg: '#fff',
+		desktopNotificationBg: ':alpha<0.9<$secondary',
+		desktopNotificationFg: ':alpha<0.7<$text',
+		desktopNotificationShadow: 'rgba(0, 0, 0, 0.4)',
+		desktopPostFormBg: '@face',
+		desktopPostFormTextareaBg: 'rgba(0, 0, 0, 0.25)',
+		desktopPostFormTextareaFg: '#fff',
+		desktopPostFormTransparentButtonFg: '$primary',
+		desktopPostFormTransparentButtonActiveGradientStart: ':darken<8<$secondary',
+		desktopPostFormTransparentButtonActiveGradientEnd: ':darken<3<$secondary',
+		desktopRenoteFormFooter: ':lighten<5<$secondary',
+		desktopTimelineHeaderShadow: 'rgba(0, 0, 0, 0.15)',
+		desktopTimelineSrc: '@faceTextButton',
+		desktopTimelineSrcHover: '@faceTextButtonHover',
+		desktopWindowTitle: '@faceHeaderText',
+		desktopWindowShadow: 'rgba(0, 0, 0, 0.5)',
+		desktopDriveBg: '@bg',
+		desktopDriveFolderBg: ':alpha<0.2<$primary',
+		desktopDriveFolderHoverBg: ':alpha<0.3<$primary',
+		desktopDriveFolderActiveBg: ':alpha<0.3<:darken<10<$primary',
+		desktopDriveFolderFg: '#fff',
+		desktopSettingsNavItem: ':alpha<0.8<$text',
+		desktopSettingsNavItemHover: ':lighten<10<$text',
+
+		deckAcrylicColumnBg: 'rgba(0, 0, 0, 0.25)',
+
+		mobileHeaderBg: ':lighten<5<$secondary',
+		mobileHeaderFg: '$text',
+		mobileNavBackdrop: 'rgba(0, 0, 0, 0.7)',
+		mobilePostFormDivider: 'rgba(0, 0, 0, 0.2)',
+		mobilePostFormTextareaBg: 'rgba(0, 0, 0, 0.3)',
+		mobileDriveNavBg: ':alpha<0.75<$secondary',
+		mobileHomeTlItemHover: 'rgba(255, 255, 255, 0.1)',
+		mobileUserPageName: '#fff',
+		mobileUserPageAcct: '$text',
+		mobileUserPageDescription: '$text',
+		mobileUserPageFollowedBg: 'rgba(0, 0, 0, 0.3)',
+		mobileUserPageFollowedFg: '$text',
+		mobileUserPageStatusHighlight: '#fff',
+		mobileUserPageHeaderShadow: 'rgba(0, 0, 0, 0.3)',
+		mobileAnnouncement: 'rgba(30, 129, 216, 0.2)',
+		mobileAnnouncementFg: '#fff',
+		mobileSignedInAsBg: '#273c34',
+		mobileSignedInAsFg: '#49ab63',
+		mobileSignoutBg: '#652222',
+		mobileSignoutFg: '#ff5f56',
+
+		reversiBannerGradientStart: '#45730e',
+		reversiBannerGradientEnd: '#464300',
+		reversiDescBg: 'rgba(255, 255, 255, 0.1)',
+		reversiListItemShadow: 'rgba(0, 0, 0, 0.7)',
+		reversiMapSelectBorder: 'rgba(255, 255, 255, 0.1)',
+		reversiMapSelectHoverBorder: 'rgba(255, 255, 255, 0.2)',
+		reversiRoomFormShadow: 'rgba(0, 0, 0, 0.7)',
+		reversiRoomFooterBg: ':alpha<0.9<$secondary',
+		reversiGameHeaderLine: ':alpha<0.5<$secondary',
+		reversiGameEmptyCell: ':lighten<2<$secondary',
+		reversiGameEmptyCellMyTurn: ':lighten<5<$secondary',
+		reversiGameEmptyCellCanPut: ':lighten<4<$secondary',
+	},
+}
diff --git a/src/client/theme/halloween.json b/src/client/theme/halloween.json
deleted file mode 100644
index fb34db57a8..0000000000
--- a/src/client/theme/halloween.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
-	"meta": {
-		"id": "42e4f09b-67d5-498c-af7d-29faa54745b0",
-		"name": "Halloween",
-		"author": "syuilo",
-		"base": "dark",
-		"vars": {
-			"primary": "#d67036",
-			"secondary": "#1f1d30",
-			"text": "#b1bee3"
-		}
-	},
-
-	"renoteGradient": "#5d2d1a",
-	"renoteText": "#ff6c00",
-	"quoteBorder": "#c3631c"
-}
diff --git a/src/client/theme/halloween.json5 b/src/client/theme/halloween.json5
new file mode 100644
index 0000000000..608105903a
--- /dev/null
+++ b/src/client/theme/halloween.json5
@@ -0,0 +1,21 @@
+{
+	id: '42e4f09b-67d5-498c-af7d-29faa54745b0',
+
+	name: 'Halloween',
+	author: 'syuilo',
+	desc: 'Hello, Happy Halloween!',
+
+	base: 'dark',
+
+	vars: {
+		primary: '#d67036',
+		secondary: '#1f1d30',
+		text: '#b1bee3',
+	},
+
+	props: {
+		renoteGradient: '#5d2d1a',
+		renoteText: '#ff6c00',
+		quoteBorder: '#c3631c',
+	},
+}
diff --git a/src/client/theme/light.json b/src/client/theme/light.json
deleted file mode 100644
index 0d50dc5caa..0000000000
--- a/src/client/theme/light.json
+++ /dev/null
@@ -1,204 +0,0 @@
-{
-	"meta": {
-		"id": "light",
-		"name": "Light",
-		"author": "syuilo",
-		"vars": {
-			"primary": "#fb4e4e",
-			"secondary": "#fff",
-			"text": "#666"
-		}
-	},
-
-	"primary": "$primary",
-	"primaryForeground": "#fff",
-	"secondary": "$secondary",
-	"bg": ":darken<8<$secondary",
-	"text": "$text",
-
-	"scrollbarTrack": "#fff",
-	"scrollbarHandle": "#00000033",
-	"scrollbarHandleHover": "#00000066",
-
-	"face": "$secondary",
-	"faceText": "#444",
-	"faceHeader": ":lighten<5<$secondary",
-	"faceHeaderText": "#888",
-	"faceDivider": "rgba(0, 0, 0, 0.082)",
-	"faceTextButton": "#ccc",
-	"faceTextButtonHover": "#aaa",
-	"faceTextButtonActive": "#999",
-	"faceClearButtonHover": "rgba(0, 0, 0, 0.025)",
-	"faceClearButtonActive": "rgba(0, 0, 0, 0.05)",
-	"popupBg": ":lighten<5<$secondary",
-	"popupFg": "#586069",
-
-	"subNoteBg": "rgba(0, 0, 0, 0.01)",
-	"subNoteText": ":alpha<0.7<$text",
-	"renoteGradient": "#edfde2",
-	"renoteText": "#9dbb00",
-	"quoteBorder": "#c0dac6",
-	"noteText": "#717171",
-	"noteHeaderName": ":darken<2<$text",
-	"noteHeaderBadgeFg": "#aaa",
-	"noteHeaderBadgeBg": "rgba(0, 0, 0, 0.05)",
-	"noteHeaderAdminFg": "#f15f71",
-	"noteHeaderAdminBg": "#ffdfdf",
-	"noteHeaderAcct": ":alpha<0.7<@noteHeaderName",
-	"noteHeaderInfo": ":alpha<0.7<@noteHeaderName",
-
-	"noteActions": ":alpha<0.3<$text",
-	"noteActionsHover": ":alpha<0.9<$text",
-	"noteActionsReplyHover": "#0af",
-	"noteActionsRenoteHover": "#8d0",
-	"noteActionsReactionHover": "#fa0",
-	"noteActionsHighlighted": "#888",
-
-	"noteAttachedFile": "rgba(0, 0, 0, 0.05)",
-
-	"modalBackdrop": "rgba(0, 0, 0, 0.1)",
-
-	"dateDividerBg": ":darken<2<$secondary",
-	"dateDividerFg": ":alpha<0.7<$text",
-
-	"switchTrack": "rgba(0, 0, 0, 0.25)",
-	"radioBorder": "rgba(0, 0, 0, 0.4)",
-	"inputBorder": "rgba(0, 0, 0, 0.42)",
-	"inputLabel": "rgba(0, 0, 0, 0.54)",
-	"inputText": "#000",
-
-	"buttonBg": "rgba(0, 0, 0, 0.05)",
-	"buttonHoverBg": "rgba(0, 0, 0, 0.1)",
-	"buttonActiveBg": "rgba(0, 0, 0, 0.15)",
-
-	"autocompleteItemHoverBg": "rgba(0, 0, 0, 0.1)",
-	"autocompleteItemText": "rgba(0, 0, 0, 0.8)",
-	"autocompleteItemTextSub": "rgba(0, 0, 0, 0.3)",
-
-	"cwButtonBg": "#b1b9c1",
-	"cwButtonFg": "#fff",
-	"cwButtonHoverBg": "#bbc4ce",
-
-	"reactionPickerButtonHoverBg": "#eee",
-
-	"reactionViewerBorder": "rgba(0, 0, 0, 0.1)",
-
-	"pollEditorInputBg": "#fff",
-
-	"pollChoiceText": "#000",
-	"pollChoiceBorder": "rgba(0, 0, 0, 0.1)",
-
-	"urlPreviewBorder": "rgba(0, 0, 0, 0.1)",
-	"urlPreviewBorderHover": "rgba(0, 0, 0, 0.2)",
-	"urlPreviewTitle": "$text",
-	"urlPreviewText": ":alpha<0.7<$text",
-	"urlPreviewInfo": ":alpha<0.8<$text",
-
-	"calendarWeek": "#19a2a9",
-	"calendarSaturdayOrSunday": "#ef95a0",
-	"calendarDay": "#777",
-
-	"materBg": "rgba(0, 0, 0, 0.1)",
-
-	"chartCaption": ":alpha<0.6<$text",
-
-	"announcementsBg": "#f3f9ff",
-	"announcementsTitle": "#4078c0",
-	"announcementsText": "#57616f",
-
-	"donationBg": "#fbead4",
-	"donationFg": "#777d71",
-
-	"googleSearchBg": "#fff",
-	"googleSearchFg": "#55595c",
-	"googleSearchBorder": "rgba(0, 0, 0, 0.2)",
-	"googleSearchHoverBorder": "rgba(0, 0, 0, 0.3)",
-	"googleSearchHoverButton": "rgba(0, 0, 0, 0.05)",
-
-	"mfmTitleBg": "rgba(0, 0, 0, 0.07)",
-	"mfmQuote": ":alpha<0.6<$text",
-	"mfmQuoteLine": ":alpha<0.5<$text",
-
-	"suspendedInfoBg": "#ffdbdb",
-	"suspendedInfoFg": "#570808",
-	"remoteInfoBg": "#fff0db",
-	"remoteInfoFg": "#573c08",
-
-	"messagingRoomBg": "#fff",
-	"messagingRoomInfo": "#000",
-	"messagingRoomDateDividerLine": "rgba(0, 0, 0, 0.1)",
-	"messagingRoomDateDividerText": "rgba(0, 0, 0, 0.3)",
-	"messagingRoomMessageInfo": "rgba(0, 0, 0, 0.4)",
-	"messagingRoomMessageBg": "#eee",
-	"messagingRoomMessageFg": "#333",
-
-	"formButtonBorder": "rgba(0, 0, 0, 0.1)",
-	"formButtonHoverBg": ":alpha<0.12<$primary",
-	"formButtonHoverBorder": ":alpha<0.3<$primary",
-	"formButtonActiveBg": ":alpha<0.12<$primary",
-
-	"desktopHeaderBg": ":lighten<5<$secondary",
-	"desktopHeaderFg": "$text",
-	"desktopHeaderHoverFg": "#7b8c88",
-	"desktopHeaderSearchBg": "rgba(0, 0, 0, 0.05)",
-	"desktopHeaderSearchHoverBg": "rgba(0, 0, 0, 0.08)",
-	"desktopHeaderSearchFg": "#000",
-	"desktopNotificationBg": ":alpha<0.9<$secondary",
-	"desktopNotificationFg": ":alpha<0.7<$text",
-	"desktopNotificationShadow": "rgba(0, 0, 0, 0.2)",
-	"desktopPostFormBg": ":lighten<33<$primary",
-	"desktopPostFormTextareaBg": "#fff",
-	"desktopPostFormTextareaFg": "#333",
-	"desktopPostFormTransparentButtonFg": ":alpha<0.5<$primary",
-	"desktopPostFormTransparentButtonActiveGradientStart": ":lighten<30<$primary",
-	"desktopPostFormTransparentButtonActiveGradientEnd": ":lighten<33<$primary",
-	"desktopRenoteFormFooter": ":lighten<33<$primary",
-	"desktopTimelineHeaderShadow": "rgba(0, 0, 0, 0.08)",
-	"desktopTimelineSrc": "#6f7477",
-	"desktopTimelineSrcHover": "#525a5f",
-	"desktopWindowTitle": "#666",
-	"desktopWindowShadow": "rgba(0, 0, 0, 0.2)",
-	"desktopDriveBg": "#fff",
-	"desktopDriveFolderBg": ":lighten<31<$primary",
-	"desktopDriveFolderHoverBg": ":lighten<27<$primary",
-	"desktopDriveFolderActiveBg": ":lighten<25<$primary",
-	"desktopDriveFolderFg": ":darken<10<$primary",
-	"desktopSettingsNavItem": ":alpha<0.8<$text",
-	"desktopSettingsNavItemHover": ":darken<10<$text",
-
-	"deckAcrylicColumnBg": "rgba(0, 0, 0, 0.1)",
-
-	"mobileHeaderBg": ":lighten<5<$secondary",
-	"mobileHeaderFg": "$text",
-	"mobileNavBackdrop": "rgba(0, 0, 0, 0.2)",
-	"mobilePostFormDivider": "rgba(0, 0, 0, 0.1)",
-	"mobilePostFormTextareaBg": "#fff",
-	"mobileDriveNavBg": ":alpha<0.75<$secondary",
-	"mobileHomeTlItemHover": "rgba(0, 0, 0, 0.05)",
-	"mobileUserPageName": "#757c82",
-	"mobileUserPageAcct": "#969ea5",
-	"mobileUserPageDescription": "#757c82",
-	"mobileUserPageFollowedBg": "#a7bec7",
-	"mobileUserPageFollowedFg": "#fff",
-	"mobileUserPageStatusHighlight": "#787e86",
-	"mobileUserPageHeaderShadow": "rgba(0, 0, 0, 0.07)",
-	"mobileAnnouncement": "rgba(155, 196, 232, 0.2)",
-	"mobileAnnouncementFg": "#3f4967",
-	"mobileSignedInAsBg": "#fcfff5",
-	"mobileSignedInAsFg": "#2c662d",
-	"mobileSignoutBg": "#fff6f5",
-	"mobileSignoutFg": "#cc2727",
-
-	"reversiBannerGradientStart": "#8bca3e",
-	"reversiBannerGradientEnd": "#d6cf31",
-	"reversiDescBg": "rgba(0, 0, 0, 0.1)",
-	"reversiListItemShadow": "rgba(0, 0, 0, 0.15)",
-	"reversiMapSelectBorder": "rgba(0, 0, 0, 0.1)",
-	"reversiMapSelectHoverBorder": "rgba(0, 0, 0, 0.2)",
-	"reversiRoomFormShadow": "rgba(0, 0, 0, 0.1)",
-	"reversiRoomFooterBg": ":alpha<0.9<$secondary",
-	"reversiGameHeaderLine": "#c4cdd4",
-	"reversiGameEmptyCell": "rgba(0, 0, 0, 0.06)",
-	"reversiGameEmptyCellMyTurn": "rgba(0, 0, 0, 0.12)",
-	"reversiGameEmptyCellCanPut": "rgba(0, 0, 0, 0.9)"
-}
diff --git a/src/client/theme/light.json5 b/src/client/theme/light.json5
new file mode 100644
index 0000000000..1e795ee8c5
--- /dev/null
+++ b/src/client/theme/light.json5
@@ -0,0 +1,207 @@
+{
+	id: 'light',
+
+	name: 'Light',
+	author: 'syuilo',
+	desc: 'Default light theme',
+
+	vars: {
+		primary: '#fb4e4e',
+		secondary: '#fff',
+		text: '#666',
+	},
+
+	props: {
+		primary: '$primary',
+		primaryForeground: '#fff',
+		secondary: '$secondary',
+		bg: ':darken<8<$secondary',
+		text: '$text',
+
+		scrollbarTrack: '#fff',
+		scrollbarHandle: '#00000033',
+		scrollbarHandleHover: '#00000066',
+
+		face: '$secondary',
+		faceText: '#444',
+		faceHeader: ':lighten<5<$secondary',
+		faceHeaderText: '#888',
+		faceDivider: 'rgba(0, 0, 0, 0.082)',
+		faceTextButton: '#ccc',
+		faceTextButtonHover: '#aaa',
+		faceTextButtonActive: '#999',
+		faceClearButtonHover: 'rgba(0, 0, 0, 0.025)',
+		faceClearButtonActive: 'rgba(0, 0, 0, 0.05)',
+		popupBg: ':lighten<5<$secondary',
+		popupFg: '#586069',
+
+		subNoteBg: 'rgba(0, 0, 0, 0.01)',
+		subNoteText: ':alpha<0.7<$text',
+		renoteGradient: '#edfde2',
+		renoteText: '#9dbb00',
+		quoteBorder: '#c0dac6',
+		noteText: '#717171',
+		noteHeaderName: ':darken<2<$text',
+		noteHeaderBadgeFg: '#aaa',
+		noteHeaderBadgeBg: 'rgba(0, 0, 0, 0.05)',
+		noteHeaderAdminFg: '#f15f71',
+		noteHeaderAdminBg: '#ffdfdf',
+		noteHeaderAcct: ':alpha<0.7<@noteHeaderName',
+		noteHeaderInfo: ':alpha<0.7<@noteHeaderName',
+
+		noteActions: ':alpha<0.3<$text',
+		noteActionsHover: ':alpha<0.9<$text',
+		noteActionsReplyHover: '#0af',
+		noteActionsRenoteHover: '#8d0',
+		noteActionsReactionHover: '#fa0',
+		noteActionsHighlighted: '#888',
+
+		noteAttachedFile: 'rgba(0, 0, 0, 0.05)',
+
+		modalBackdrop: 'rgba(0, 0, 0, 0.1)',
+
+		dateDividerBg: ':darken<2<$secondary',
+		dateDividerFg: ':alpha<0.7<$text',
+
+		switchTrack: 'rgba(0, 0, 0, 0.25)',
+		radioBorder: 'rgba(0, 0, 0, 0.4)',
+		inputBorder: 'rgba(0, 0, 0, 0.42)',
+		inputLabel: 'rgba(0, 0, 0, 0.54)',
+		inputText: '#000',
+
+		buttonBg: 'rgba(0, 0, 0, 0.05)',
+		buttonHoverBg: 'rgba(0, 0, 0, 0.1)',
+		buttonActiveBg: 'rgba(0, 0, 0, 0.15)',
+
+		autocompleteItemHoverBg: 'rgba(0, 0, 0, 0.1)',
+		autocompleteItemText: 'rgba(0, 0, 0, 0.8)',
+		autocompleteItemTextSub: 'rgba(0, 0, 0, 0.3)',
+
+		cwButtonBg: '#b1b9c1',
+		cwButtonFg: '#fff',
+		cwButtonHoverBg: '#bbc4ce',
+
+		reactionPickerButtonHoverBg: '#eee',
+
+		reactionViewerBorder: 'rgba(0, 0, 0, 0.1)',
+
+		pollEditorInputBg: '#fff',
+
+		pollChoiceText: '#000',
+		pollChoiceBorder: 'rgba(0, 0, 0, 0.1)',
+
+		urlPreviewBorder: 'rgba(0, 0, 0, 0.1)',
+		urlPreviewBorderHover: 'rgba(0, 0, 0, 0.2)',
+		urlPreviewTitle: '$text',
+		urlPreviewText: ':alpha<0.7<$text',
+		urlPreviewInfo: ':alpha<0.8<$text',
+
+		calendarWeek: '#19a2a9',
+		calendarSaturdayOrSunday: '#ef95a0',
+		calendarDay: '#777',
+
+		materBg: 'rgba(0, 0, 0, 0.1)',
+
+		chartCaption: ':alpha<0.6<$text',
+
+		announcementsBg: '#f3f9ff',
+		announcementsTitle: '#4078c0',
+		announcementsText: '#57616f',
+
+		donationBg: '#fbead4',
+		donationFg: '#777d71',
+
+		googleSearchBg: '#fff',
+		googleSearchFg: '#55595c',
+		googleSearchBorder: 'rgba(0, 0, 0, 0.2)',
+		googleSearchHoverBorder: 'rgba(0, 0, 0, 0.3)',
+		googleSearchHoverButton: 'rgba(0, 0, 0, 0.05)',
+
+		mfmTitleBg: 'rgba(0, 0, 0, 0.07)',
+		mfmQuote: ':alpha<0.6<$text',
+		mfmQuoteLine: ':alpha<0.5<$text',
+
+		suspendedInfoBg: '#ffdbdb',
+		suspendedInfoFg: '#570808',
+		remoteInfoBg: '#fff0db',
+		remoteInfoFg: '#573c08',
+
+		messagingRoomBg: '#fff',
+		messagingRoomInfo: '#000',
+		messagingRoomDateDividerLine: 'rgba(0, 0, 0, 0.1)',
+		messagingRoomDateDividerText: 'rgba(0, 0, 0, 0.3)',
+		messagingRoomMessageInfo: 'rgba(0, 0, 0, 0.4)',
+		messagingRoomMessageBg: '#eee',
+		messagingRoomMessageFg: '#333',
+
+		formButtonBorder: 'rgba(0, 0, 0, 0.1)',
+		formButtonHoverBg: ':alpha<0.12<$primary',
+		formButtonHoverBorder: ':alpha<0.3<$primary',
+		formButtonActiveBg: ':alpha<0.12<$primary',
+
+		desktopHeaderBg: ':lighten<5<$secondary',
+		desktopHeaderFg: '$text',
+		desktopHeaderHoverFg: '#7b8c88',
+		desktopHeaderSearchBg: 'rgba(0, 0, 0, 0.05)',
+		desktopHeaderSearchHoverBg: 'rgba(0, 0, 0, 0.08)',
+		desktopHeaderSearchFg: '#000',
+		desktopNotificationBg: ':alpha<0.9<$secondary',
+		desktopNotificationFg: ':alpha<0.7<$text',
+		desktopNotificationShadow: 'rgba(0, 0, 0, 0.2)',
+		desktopPostFormBg: ':lighten<33<$primary',
+		desktopPostFormTextareaBg: '#fff',
+		desktopPostFormTextareaFg: '#333',
+		desktopPostFormTransparentButtonFg: ':alpha<0.5<$primary',
+		desktopPostFormTransparentButtonActiveGradientStart: ':lighten<30<$primary',
+		desktopPostFormTransparentButtonActiveGradientEnd: ':lighten<33<$primary',
+		desktopRenoteFormFooter: ':lighten<33<$primary',
+		desktopTimelineHeaderShadow: 'rgba(0, 0, 0, 0.08)',
+		desktopTimelineSrc: '#6f7477',
+		desktopTimelineSrcHover: '#525a5f',
+		desktopWindowTitle: '#666',
+		desktopWindowShadow: 'rgba(0, 0, 0, 0.2)',
+		desktopDriveBg: '#fff',
+		desktopDriveFolderBg: ':lighten<31<$primary',
+		desktopDriveFolderHoverBg: ':lighten<27<$primary',
+		desktopDriveFolderActiveBg: ':lighten<25<$primary',
+		desktopDriveFolderFg: ':darken<10<$primary',
+		desktopSettingsNavItem: ':alpha<0.8<$text',
+		desktopSettingsNavItemHover: ':darken<10<$text',
+
+		deckAcrylicColumnBg: 'rgba(0, 0, 0, 0.1)',
+
+		mobileHeaderBg: ':lighten<5<$secondary',
+		mobileHeaderFg: '$text',
+		mobileNavBackdrop: 'rgba(0, 0, 0, 0.2)',
+		mobilePostFormDivider: 'rgba(0, 0, 0, 0.1)',
+		mobilePostFormTextareaBg: '#fff',
+		mobileDriveNavBg: ':alpha<0.75<$secondary',
+		mobileHomeTlItemHover: 'rgba(0, 0, 0, 0.05)',
+		mobileUserPageName: '#757c82',
+		mobileUserPageAcct: '#969ea5',
+		mobileUserPageDescription: '#757c82',
+		mobileUserPageFollowedBg: '#a7bec7',
+		mobileUserPageFollowedFg: '#fff',
+		mobileUserPageStatusHighlight: '#787e86',
+		mobileUserPageHeaderShadow: 'rgba(0, 0, 0, 0.07)',
+		mobileAnnouncement: 'rgba(155, 196, 232, 0.2)',
+		mobileAnnouncementFg: '#3f4967',
+		mobileSignedInAsBg: '#fcfff5',
+		mobileSignedInAsFg: '#2c662d',
+		mobileSignoutBg: '#fff6f5',
+		mobileSignoutFg: '#cc2727',
+
+		reversiBannerGradientStart: '#8bca3e',
+		reversiBannerGradientEnd: '#d6cf31',
+		reversiDescBg: 'rgba(0, 0, 0, 0.1)',
+		reversiListItemShadow: 'rgba(0, 0, 0, 0.15)',
+		reversiMapSelectBorder: 'rgba(0, 0, 0, 0.1)',
+		reversiMapSelectHoverBorder: 'rgba(0, 0, 0, 0.2)',
+		reversiRoomFormShadow: 'rgba(0, 0, 0, 0.1)',
+		reversiRoomFooterBg: ':alpha<0.9<$secondary',
+		reversiGameHeaderLine: '#c4cdd4',
+		reversiGameEmptyCell: 'rgba(0, 0, 0, 0.06)',
+		reversiGameEmptyCellMyTurn: 'rgba(0, 0, 0, 0.12)',
+		reversiGameEmptyCellCanPut: 'rgba(0, 0, 0, 0.9)',
+	},
+}
diff --git a/src/client/theme/pink.json b/src/client/theme/pink.json
deleted file mode 100644
index ddb56b46e1..0000000000
--- a/src/client/theme/pink.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
-	"meta": {
-		"id": "e9c8c01d-9c15-48d0-9b5c-3d00843b5b36",
-		"name": "Pink",
-		"author": "syuilo",
-		"base": "light",
-		"vars": {
-			"primary": "rgb(251, 78, 112)",
-			"secondary": "rgb(255, 218, 240)",
-			"text": "rgb(113, 91, 102)"
-		}
-	},
-
-	"renoteGradient": "#ffb1c9",
-	"renoteText": "#ff588d",
-	"quoteBorder": "#ff6c9b"
-}
diff --git a/src/client/theme/pink.json5 b/src/client/theme/pink.json5
new file mode 100644
index 0000000000..2e136fba5d
--- /dev/null
+++ b/src/client/theme/pink.json5
@@ -0,0 +1,20 @@
+{
+	id: 'e9c8c01d-9c15-48d0-9b5c-3d00843b5b36',
+
+	name: 'Pink',
+	author: 'syuilo',
+
+	base: 'light',
+
+	vars: {
+		primary: 'rgb(251, 78, 112)',
+		secondary: 'rgb(255, 218, 240)',
+		text: 'rgb(113, 91, 102)',
+	},
+
+	props: {
+		renoteGradient: '#ffb1c9',
+		renoteText: '#ff588d',
+		quoteBorder: '#ff6c9b',
+	},
+}
diff --git a/webpack.config.ts b/webpack.config.ts
index 3b14ee4a8a..e1163133c0 100644
--- a/webpack.config.ts
+++ b/webpack.config.ts
@@ -196,6 +196,9 @@ module.exports = {
 		}, {
 			test: /\.(eot|woff|woff2|svg|ttf)([\?]?.*)$/,
 			loader: 'url-loader'
+		}, {
+			test: /\.json5$/,
+			loader: 'json5-loader'
 		}, {
 			test: /\.ts$/,
 			exclude: /node_modules/,
-- 
GitLab