Skip to content

Commit 6aacd5f

Browse files
committed
0.07
1 parent f454065 commit 6aacd5f

3 files changed

Lines changed: 385 additions & 4 deletions

File tree

Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
(() => {
2+
const STORAGE_KEY = "qrcraft_palette_v1";
3+
4+
const presets = [
5+
{
6+
id: "md3",
7+
name: "MD3 默认",
8+
primary: "#6750A4",
9+
accent: "#EFB8C8",
10+
},
11+
{
12+
id: "teal_purple",
13+
name: "Teal + Purple",
14+
primary: "#009688",
15+
accent: "#9C27B0",
16+
},
17+
{
18+
id: "blue_amber",
19+
name: "Blue + Amber",
20+
primary: "#1E88E5",
21+
accent: "#FFB300",
22+
},
23+
];
24+
25+
function safeParse(json) {
26+
try {
27+
return JSON.parse(json);
28+
} catch {
29+
return null;
30+
}
31+
}
32+
33+
function loadConfig() {
34+
const raw = localStorage.getItem(STORAGE_KEY);
35+
const data = raw ? safeParse(raw) : null;
36+
if (!data || typeof data !== "object") return null;
37+
return data;
38+
}
39+
40+
function saveConfig(cfg) {
41+
localStorage.setItem(STORAGE_KEY, JSON.stringify(cfg));
42+
}
43+
44+
function isHexColor(s) {
45+
return typeof s === "string" && /^#[0-9a-fA-F]{6}$/.test(s);
46+
}
47+
48+
function applyColors(primary, accent) {
49+
const root = document.documentElement;
50+
if (isHexColor(primary)) {
51+
root.style.setProperty("--md-primary-fg-color", primary);
52+
root.style.setProperty("--md-primary-fg-color--light", primary);
53+
root.style.setProperty("--md-primary-fg-color--dark", primary);
54+
}
55+
if (isHexColor(accent)) {
56+
root.style.setProperty("--md-accent-fg-color", accent);
57+
root.style.setProperty("--md-accent-fg-color--transparent", `${accent}22`);
58+
}
59+
}
60+
61+
function applyFromStorage() {
62+
const cfg = loadConfig();
63+
if (!cfg) return;
64+
applyColors(cfg.primary, cfg.accent);
65+
}
66+
67+
function getSourceForCurrentPage() {
68+
const content = document.querySelector(".md-content");
69+
const code = content?.querySelector("pre > code")?.textContent?.trim();
70+
return code || "";
71+
}
72+
73+
function createHeaderButton() {
74+
const btn = document.createElement("button");
75+
btn.type = "button";
76+
btn.className = "md-button theme-palette-header-button";
77+
btn.textContent = "配色";
78+
btn.title = "配色设置";
79+
btn.setAttribute("aria-label", btn.title);
80+
return btn;
81+
}
82+
83+
function ensureHeaderButton(onClick) {
84+
const header = document.querySelector("header.md-header") || document.querySelector(".md-header");
85+
if (!(header instanceof HTMLElement)) return;
86+
87+
const search = header.querySelector(".md-search");
88+
const target =
89+
(search && search.parentElement instanceof HTMLElement ? search.parentElement : null) ||
90+
header.querySelector(".md-header__inner") ||
91+
header;
92+
93+
if (!(target instanceof HTMLElement)) return;
94+
if (target.querySelector(".theme-palette-header-button")) return;
95+
96+
const btn = createHeaderButton();
97+
btn.addEventListener("click", (e) => {
98+
e.preventDefault();
99+
e.stopPropagation();
100+
onClick();
101+
});
102+
103+
if (search && search instanceof HTMLElement) {
104+
const before = target.querySelector(".md-search") || search;
105+
target.insertBefore(btn, before);
106+
} else {
107+
target.appendChild(btn);
108+
}
109+
}
110+
111+
function createDialog() {
112+
const overlay = document.createElement("div");
113+
overlay.className = "theme-palette-overlay";
114+
overlay.hidden = true;
115+
116+
const panel = document.createElement("div");
117+
panel.className = "theme-palette-panel";
118+
panel.setAttribute("role", "dialog");
119+
panel.setAttribute("aria-modal", "true");
120+
panel.setAttribute("aria-label", "配色设置");
121+
122+
const header = document.createElement("div");
123+
header.className = "theme-palette-panel__header";
124+
125+
const title = document.createElement("div");
126+
title.className = "theme-palette-panel__title";
127+
title.textContent = "配色设置";
128+
129+
const close = document.createElement("button");
130+
close.type = "button";
131+
close.className = "md-button theme-palette-close";
132+
close.textContent = "关闭";
133+
134+
header.appendChild(title);
135+
header.appendChild(close);
136+
137+
const body = document.createElement("div");
138+
body.className = "theme-palette-panel__body";
139+
140+
const presetLabel = document.createElement("div");
141+
presetLabel.className = "theme-palette-section-title";
142+
presetLabel.textContent = "预设";
143+
144+
const presetList = document.createElement("div");
145+
presetList.className = "theme-palette-presets";
146+
147+
for (const p of presets) {
148+
const item = document.createElement("button");
149+
item.type = "button";
150+
item.className = "md-button theme-palette-preset";
151+
item.textContent = p.name;
152+
item.dataset.primary = p.primary;
153+
item.dataset.accent = p.accent;
154+
presetList.appendChild(item);
155+
}
156+
157+
const customLabel = document.createElement("div");
158+
customLabel.className = "theme-palette-section-title";
159+
customLabel.textContent = "自定义";
160+
161+
const customRow = document.createElement("div");
162+
customRow.className = "theme-palette-custom";
163+
164+
const primaryWrap = document.createElement("label");
165+
primaryWrap.className = "theme-palette-field";
166+
const primaryText = document.createElement("span");
167+
primaryText.textContent = "主色";
168+
const primaryInput = document.createElement("input");
169+
primaryInput.type = "color";
170+
primaryInput.value = "#6750A4";
171+
primaryWrap.appendChild(primaryText);
172+
primaryWrap.appendChild(primaryInput);
173+
174+
const accentWrap = document.createElement("label");
175+
accentWrap.className = "theme-palette-field";
176+
const accentText = document.createElement("span");
177+
accentText.textContent = "强调色";
178+
const accentInput = document.createElement("input");
179+
accentInput.type = "color";
180+
accentInput.value = "#EFB8C8";
181+
accentWrap.appendChild(accentText);
182+
accentWrap.appendChild(accentInput);
183+
184+
const applyBtn = document.createElement("button");
185+
applyBtn.type = "button";
186+
applyBtn.className = "md-button md-button--primary theme-palette-apply";
187+
applyBtn.textContent = "应用";
188+
189+
const resetBtn = document.createElement("button");
190+
resetBtn.type = "button";
191+
resetBtn.className = "md-button theme-palette-reset";
192+
resetBtn.textContent = "恢复默认";
193+
194+
customRow.appendChild(primaryWrap);
195+
customRow.appendChild(accentWrap);
196+
customRow.appendChild(applyBtn);
197+
customRow.appendChild(resetBtn);
198+
199+
body.appendChild(presetLabel);
200+
body.appendChild(presetList);
201+
body.appendChild(customLabel);
202+
body.appendChild(customRow);
203+
204+
panel.appendChild(header);
205+
panel.appendChild(body);
206+
207+
overlay.appendChild(panel);
208+
209+
function open() {
210+
const cfg = loadConfig();
211+
if (cfg?.primary && isHexColor(cfg.primary)) primaryInput.value = cfg.primary;
212+
if (cfg?.accent && isHexColor(cfg.accent)) accentInput.value = cfg.accent;
213+
overlay.hidden = false;
214+
document.documentElement.classList.add("theme-palette-open");
215+
}
216+
217+
function closePanel() {
218+
overlay.hidden = true;
219+
document.documentElement.classList.remove("theme-palette-open");
220+
}
221+
222+
function applyAndSave(primary, accent) {
223+
applyColors(primary, accent);
224+
saveConfig({ primary, accent });
225+
}
226+
227+
overlay.addEventListener("click", (e) => {
228+
if (e.target === overlay) closePanel();
229+
});
230+
231+
close.addEventListener("click", () => closePanel());
232+
document.addEventListener("keydown", (e) => {
233+
if (!overlay.hidden && e.key === "Escape") closePanel();
234+
});
235+
236+
presetList.addEventListener("click", (e) => {
237+
const t = e.target;
238+
if (!(t instanceof HTMLElement)) return;
239+
if (!(t instanceof HTMLButtonElement)) return;
240+
const primary = t.dataset.primary;
241+
const accent = t.dataset.accent;
242+
if (!isHexColor(primary) || !isHexColor(accent)) return;
243+
applyAndSave(primary, accent);
244+
});
245+
246+
applyBtn.addEventListener("click", () => {
247+
applyAndSave(primaryInput.value, accentInput.value);
248+
});
249+
250+
resetBtn.addEventListener("click", () => {
251+
localStorage.removeItem(STORAGE_KEY);
252+
document.documentElement.style.removeProperty("--md-primary-fg-color");
253+
document.documentElement.style.removeProperty("--md-primary-fg-color--light");
254+
document.documentElement.style.removeProperty("--md-primary-fg-color--dark");
255+
document.documentElement.style.removeProperty("--md-accent-fg-color");
256+
document.documentElement.style.removeProperty("--md-accent-fg-color--transparent");
257+
primaryInput.value = "#6750A4";
258+
accentInput.value = "#EFB8C8";
259+
});
260+
261+
return { overlay, open, close: closePanel };
262+
}
263+
264+
function mount() {
265+
applyFromStorage();
266+
267+
if (document.querySelector(".theme-palette-overlay")) return;
268+
const { overlay, open } = createDialog();
269+
document.body.appendChild(overlay);
270+
271+
ensureHeaderButton(() => open());
272+
}
273+
274+
function onReady(fn) {
275+
if (document.readyState === "loading") {
276+
document.addEventListener("DOMContentLoaded", fn, { once: true });
277+
} else {
278+
fn();
279+
}
280+
}
281+
282+
onReady(() => {
283+
const doc$ = window.document$;
284+
if (doc$ && typeof doc$.subscribe === "function") {
285+
doc$.subscribe(() => mount());
286+
} else {
287+
mount();
288+
}
289+
});
290+
})();
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
.theme-palette-header-button {
2+
margin-right: 0.5rem;
3+
padding: 0.25rem 0.6rem;
4+
font-size: 0.7rem;
5+
line-height: 1.2;
6+
white-space: nowrap;
7+
}
8+
9+
.theme-palette-overlay {
10+
position: fixed;
11+
inset: 0;
12+
z-index: 50;
13+
background: rgba(0, 0, 0, 0.35);
14+
display: flex;
15+
align-items: flex-start;
16+
justify-content: center;
17+
padding: 4rem 1rem 1rem;
18+
}
19+
20+
.theme-palette-panel {
21+
width: min(720px, 100%);
22+
background: var(--md-default-bg-color);
23+
color: var(--md-default-fg-color);
24+
border-radius: 0.6rem;
25+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
26+
overflow: hidden;
27+
}
28+
29+
.theme-palette-panel__header {
30+
display: flex;
31+
align-items: center;
32+
justify-content: space-between;
33+
padding: 0.75rem 0.9rem;
34+
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
35+
}
36+
37+
.theme-palette-panel__title {
38+
font-weight: 600;
39+
}
40+
41+
.theme-palette-panel__body {
42+
padding: 0.9rem;
43+
display: grid;
44+
gap: 0.9rem;
45+
}
46+
47+
.theme-palette-section-title {
48+
font-weight: 600;
49+
opacity: 0.9;
50+
}
51+
52+
.theme-palette-presets {
53+
display: flex;
54+
flex-wrap: wrap;
55+
gap: 0.5rem;
56+
}
57+
58+
.theme-palette-custom {
59+
display: grid;
60+
grid-template-columns: 1fr 1fr auto auto;
61+
gap: 0.6rem;
62+
align-items: center;
63+
}
64+
65+
@media (max-width: 600px) {
66+
.theme-palette-custom {
67+
grid-template-columns: 1fr 1fr;
68+
}
69+
}
70+
71+
.theme-palette-field {
72+
display: grid;
73+
grid-template-columns: auto 1fr;
74+
gap: 0.5rem;
75+
align-items: center;
76+
}
77+
78+
.theme-palette-field input[type="color"] {
79+
width: 100%;
80+
min-width: 3.2rem;
81+
height: 2rem;
82+
padding: 0;
83+
border: 0;
84+
background: transparent;
85+
}
86+
87+
.theme-palette-open {
88+
overflow: hidden;
89+
}

0 commit comments

Comments
 (0)