| import { applyEffect, getAvailableEffects } from './effects.js'; |
| import { InvertPostProcess } from './postprocess/invert.js'; |
| import { HuePostProcess } from './postprocess/hue.js'; |
| import { BasePostProcess } from './postprocess/base.js'; |
|
|
| const tagDisplayNames = { |
| japanese: "日本語", |
| english: "英語", |
| kanji: "漢字対応", |
| business: "ビジネス", |
| fancy: "装飾的", |
| playful: "遊び心", |
| display: "ディスプレイ", |
| handwritten: "手書き", |
| retro: "レトロ", |
| calm: "落ち着いた", |
| cute: "かわいい", |
| script: "筆記体", |
| bold: "太字", |
| horror: "ホラー", |
| comic: "コミック" |
| }; |
|
|
| const fontTags = [ |
| |
| { name: "Aoboshi One", tags: ["japanese"] }, |
| { name: "BIZ UDGothic", tags: ["japanese", "kanji", "business"] }, |
| { name: "BIZ UDMincho", tags: ["japanese", "kanji", "business"] }, |
| { name: "BIZ UDPGothic", tags: ["japanese", "kanji", "business"] }, |
| { name: "BIZ UDPMincho", tags: ["japanese", "kanji", "business"] }, |
| { name: "Cherry Bomb One", tags: ["japanese", "cute"] }, |
| { name: "Chokokutai", tags: ["japanese", "fancy"] }, |
| { name: "Darumadrop One", tags: ["japanese", "playful"] }, |
| { name: "Dela Gothic One", tags: ["japanese", "kanji", "display"] }, |
| { name: "DotGothic16", tags: ["japanese", "kanji", "retro"] }, |
| { name: "Hachi Maru Pop", tags: ["japanese", "kanji", "cute"] }, |
| { name: "Hina Mincho", tags: ["japanese", "kanji", "fancy"] }, |
| { name: "IBM Plex Sans JP", tags: ["japanese", "kanji", "business"] }, |
| { name: "Kaisei Decol", tags: ["japanese", "kanji", "fancy"] }, |
| { name: "Kaisei HarunoUmi", tags: ["japanese", "kanji", "fancy"] }, |
| { name: "Kaisei Opti", tags: ["japanese", "kanji", "business"] }, |
| { name: "Kaisei Tokumin", tags: ["japanese", "kanji", "business"] }, |
| { name: "Kiwi Maru", tags: ["japanese", "kanji", "cute"] }, |
| { name: "Klee One", tags: ["japanese", "kanji", "handwritten"] }, |
| { name: "Kosugi", tags: ["japanese", "kanji", "business"] }, |
| { name: "Kosugi Maru", tags: ["japanese", "kanji", "calm"] }, |
| { name: "M PLUS 1", tags: ["japanese", "kanji", "business"] }, |
| { name: "M PLUS 1 Code", tags: ["japanese", "kanji", "display"] }, |
| { name: "M PLUS 1p", tags: ["japanese", "kanji", "business"] }, |
| { name: "M PLUS 2", tags: ["japanese", "kanji", "business"] }, |
| { name: "M PLUS Rounded 1c", tags: ["japanese", "kanji", "calm"] }, |
| { name: "Mochiy Pop One", tags: ["japanese", "kanji", "playful"] }, |
| { name: "Mochiy Pop P One", tags: ["japanese", "kanji", "playful"] }, |
| { name: "Monomaniac One", tags: ["japanese", "display"] }, |
| { name: "Murecho", tags: ["japanese", "business"] }, |
| { name: "New Tegomin", tags: ["japanese", "kanji", "fancy"] }, |
| { name: "Noto Sans JP", tags: ["japanese", "kanji", "business"] }, |
| { name: "Noto Serif JP", tags: ["japanese", "kanji", "business"] }, |
| { name: "Palette Mosaic", tags: ["japanese", "display"] }, |
| { name: "Potta One", tags: ["japanese", "kanji", "playful"] }, |
| { name: "Rampart One", tags: ["japanese", "kanji", "display"] }, |
| { name: "Reggae One", tags: ["japanese", "kanji", "display"] }, |
| { name: "Rock 3D", tags: ["japanese", "display"] }, |
| { name: "RocknRoll One", tags: ["japanese", "kanji", "playful"] }, |
| { name: "Sawarabi Gothic", tags: ["japanese", "kanji", "business"] }, |
| { name: "Sawarabi Mincho", tags: ["japanese", "kanji", "business"] }, |
| { name: "Shippori Antique", tags: ["japanese", "kanji", "retro"] }, |
| { name: "Shippori Antique B1", tags: ["japanese", "kanji", "retro"] }, |
| { name: "Shippori Mincho", tags: ["japanese", "kanji", "business"] }, |
| { name: "Shippori Mincho B1", tags: ["japanese", "kanji", "business"] }, |
| { name: "Shizuru", tags: ["japanese", "display"] }, |
| { name: "Slackside One", tags: ["japanese", "handwritten"] }, |
| { name: "Stick", tags: ["japanese", "kanji", "display"] }, |
| { name: "Train One", tags: ["japanese", "kanji", "display"] }, |
| { name: "Tsukimi Rounded", tags: ["japanese", "calm"] }, |
| { name: "Yomogi", tags: ["japanese", "kanji", "handwritten"] }, |
| { name: "Yuji Boku", tags: ["japanese", "kanji", "fancy"] }, |
| { name: "Yuji Hentaigana Akari", tags: ["japanese", "fancy"] }, |
| { name: "Yuji Hentaigana Akebono", tags: ["japanese", "fancy"] }, |
| { name: "Yuji Mai", tags: ["japanese", "kanji", "fancy"] }, |
| { name: "Yuji Syuku", tags: ["japanese", "kanji", "fancy"] }, |
| { name: "Yusei Magic", tags: ["japanese", "kanji", "playful"] }, |
| { name: "Zen Antique", tags: ["japanese", "kanji", "retro"] }, |
| { name: "Zen Antique Soft", tags: ["japanese", "kanji", "retro"] }, |
| { name: "Zen Kaku Gothic Antique", tags: ["japanese", "kanji", "business"] }, |
| { name: "Zen Kaku Gothic New", tags: ["japanese", "kanji", "business"] }, |
| { name: "Zen Kurenaido", tags: ["japanese", "calm"] }, |
| { name: "Zen Maru Gothic", tags: ["japanese", "calm"] }, |
| { name: "Zen Old Mincho", tags: ["japanese", "kanji", "retro"] }, |
|
|
| |
| { name: "Montserrat", tags: ["english", "business"] }, |
| { name: "Playfair Display", tags: ["english", "business", "fancy"] }, |
| { name: "Roboto", tags: ["english", "business"] }, |
| { name: "Lato", tags: ["english", "business"] }, |
| { name: "Poppins", tags: ["english", "business", "calm"] }, |
| { name: "Quicksand", tags: ["english", "calm"] }, |
| { name: "Raleway", tags: ["english", "calm"] }, |
|
|
| |
| { name: "Pacifico", tags: ["english", "fancy", "script"] }, |
| { name: "Great Vibes", tags: ["english", "fancy", "script"] }, |
| { name: "Lobster", tags: ["english", "fancy"] }, |
| { name: "Dancing Script", tags: ["english", "fancy", "script"] }, |
| { name: "Satisfy", tags: ["english", "fancy", "script"] }, |
| { name: "Courgette", tags: ["english", "fancy", "script"] }, |
| { name: "Kaushan Script", tags: ["english", "fancy", "script"] }, |
| { name: "Sacramento", tags: ["english", "fancy", "script", "handwritten"] }, |
|
|
| |
| { name: "Bubblegum Sans", tags: ["english", "display", "cute", "playful"] }, |
| { name: "Comic Neue", tags: ["english", "comic", "cute", "handwritten"] }, |
| { name: "Sniglet", tags: ["english", "display", "cute", "playful"] }, |
| { name: "Patrick Hand", tags: ["english", "handwritten", "playful"] }, |
| { name: "Indie Flower", tags: ["english", "handwritten", "playful"] }, |
|
|
| |
| { name: "Caveat", tags: ["english", "handwritten", "script"] }, |
| { name: "Shadows Into Light", tags: ["english", "handwritten"] }, |
| { name: "Architects Daughter", tags: ["english", "handwritten"] }, |
| { name: "Covered By Your Grace", tags: ["english", "handwritten"] }, |
| { name: "Just Another Hand", tags: ["english", "handwritten"] }, |
|
|
| |
| { name: "Righteous", tags: ["english", "display"] }, |
| { name: "Permanent Marker", tags: ["english", "display", "handwritten"] }, |
| { name: "Press Start 2P", tags: ["english", "display", "retro"] }, |
| { name: "Fredoka One", tags: ["english", "display", "playful"] }, |
| { name: "Creepster", tags: ["english", "display", "horror"] }, |
| { name: "Bangers", tags: ["english", "display", "comic"] }, |
| { name: "Rubik Mono One", tags: ["english", "display", "bold"] }, |
| { name: "Bungee", tags: ["english", "display", "bold"] }, |
| { name: "Bungee Shade", tags: ["english", "display", "fancy"] }, |
| { name: "Monoton", tags: ["english", "display", "retro"] }, |
| { name: "Anton", tags: ["english", "display", "bold"] }, |
| { name: "Bebas Neue", tags: ["english", "display", "bold"] }, |
| { name: "Black Ops One", tags: ["english", "display", "bold"] }, |
| { name: "Bowlby One SC", tags: ["english", "display", "bold"] } |
| ]; |
|
|
|
|
| |
| async function loadGoogleFont(fontFamily) { |
| |
| const formattedFamily = fontFamily.replace(/ /g, '+'); |
|
|
| |
| const url = `https://fonts.googleapis.com/css2?family=${formattedFamily}&display=swap`; |
|
|
| |
| const existingLink = document.querySelector(`link[href*="${formattedFamily}"]`); |
| if (existingLink) { |
| existingLink.remove(); |
| } |
|
|
| |
| const link = document.createElement('link'); |
| link.href = url; |
| link.rel = 'stylesheet'; |
| document.head.appendChild(link); |
|
|
| |
| await new Promise((resolve, reject) => { |
| link.onload = async () => { |
| try { |
| |
| await document.fonts.load(`16px "${fontFamily}"`); |
| |
| setTimeout(resolve, 100); |
| } catch (error) { |
| reject(error); |
| } |
| }; |
| link.onerror = reject; |
| }); |
| } |
|
|
| |
| async function textToImage(text, fontFamily, fontSize = '48px', effectType = 'simple') { |
| console.debug(`テキスト描画開始: ${effectType}`, { text, fontFamily, fontSize }); |
| try { |
| await document.fonts.load(`${fontSize} "${fontFamily}"`); |
| const fontSizeNum = parseInt(fontSize); |
| const verticalText = document.getElementById('verticalText').checked; |
| const verticalSpacing = document.getElementById('verticalSpacing').value; |
|
|
| |
| const canvas = await applyEffect(effectType, text, { |
| font: fontFamily, |
| fontSize: fontSizeNum, |
| vertical: verticalText, |
| verticalSpacing: verticalSpacing |
| }); |
|
|
| |
| const processedCanvas = await applyPostProcessors(canvas); |
|
|
| |
| return BasePostProcess.toPng(processedCanvas); |
| } catch (error) { |
| console.error('フォント描画エラー:', error); |
| throw error; |
| } |
| } |
|
|
| |
| let renderTimeout = null; |
| let isRendering = false; |
|
|
| function debounceRender(callback, delay = 200) { |
| if (renderTimeout) { |
| clearTimeout(renderTimeout); |
| } |
|
|
| if (isRendering) { |
| return; |
| } |
|
|
| renderTimeout = setTimeout(async () => { |
| isRendering = true; |
| try { |
| await callback(); |
| } finally { |
| isRendering = false; |
| } |
| }, delay); |
| } |
|
|
| |
| const postProcessors = { |
| invert: new InvertPostProcess(), |
| hue: new HuePostProcess() |
| }; |
|
|
| |
| |
| |
| function createPostProcessCard(processor) { |
| const div = document.createElement('div'); |
| div.className = 'col-md-6 col-lg-4'; |
| div.innerHTML = ` |
| <div class="card h-100"> |
| <div class="card-header d-flex align-items-center"> |
| <div class="form-check mb-0"> |
| <input type="checkbox" class="form-check-input" name="postProcess" id="postProcess${processor.name}" value="${processor.name}"> |
| <label class="form-check-label" for="postProcess${processor.name}"> |
| ${processor.label} |
| </label> |
| </div> |
| </div> |
| <div class="card-body"> |
| ${processor.ui.template} |
| </div> |
| </div> |
| `; |
| return div; |
| } |
|
|
| |
| |
| |
| function initializePostProcessUI() { |
| const container = document.getElementById('postProcessContainer'); |
| container.innerHTML = ''; |
|
|
| |
| Object.values(postProcessors).forEach(processor => { |
| const card = createPostProcessCard(processor); |
| container.appendChild(card); |
| }); |
|
|
| |
| const hueRotate = document.getElementById('hueRotate'); |
| if (hueRotate) { |
| hueRotate.addEventListener('input', (e) => { |
| document.getElementById('hueRotateValue').textContent = e.target.value; |
| debounceRender(renderAllPresets); |
| }); |
| } |
| } |
|
|
| |
| |
| |
| function getSelectedPostProcessors() { |
| const container = document.getElementById('postProcessContainer'); |
| const checkboxes = container.querySelectorAll('input[type="checkbox"]:checked'); |
| return Array.from(checkboxes).map(cb => postProcessors[cb.value]).filter(Boolean); |
| } |
|
|
| |
| |
| |
| async function applyPostProcessors(canvas) { |
| let currentCanvas = canvas; |
| const processors = getSelectedPostProcessors(); |
| |
| for (const processor of processors) { |
| currentCanvas = await processor.apply(currentCanvas); |
| } |
| |
| return currentCanvas; |
| } |
|
|
| |
| |
| |
| async function updatePreview(effectType) { |
| const text = document.getElementById('textInput').value; |
| const font = document.getElementById('googleFontInput').value; |
| const fontSize = parseInt(document.getElementById('fontSize').value); |
| const vertical = document.getElementById('verticalText').checked; |
| const verticalSpacing = document.getElementById('verticalSpacing').value; |
|
|
| try { |
| |
| if (!effectType) return; |
|
|
| |
| const canvas = await applyEffect(effectType, text, { |
| font, |
| fontSize, |
| vertical, |
| verticalSpacing |
| }); |
|
|
| |
| const processedCanvas = await applyPostProcessors(canvas); |
|
|
| |
| const dataUrl = BasePostProcess.toPng(processedCanvas); |
| const previewImage = document.querySelector(`.effect-item[data-effect="${effectType}"] img`); |
| if (previewImage) { |
| previewImage.src = dataUrl; |
| } |
| } catch (error) { |
| console.error('プレビューの更新に失敗しました:', error); |
| } |
| } |
|
|
| |
| async function renderAllPresets() { |
| const effectGrid = document.querySelector('.effect-grid'); |
| const textInput = document.getElementById('textInput'); |
| const fontSelect = document.getElementById('googleFontInput'); |
| const fontSizeInput = document.getElementById('fontSize'); |
|
|
| effectGrid.innerHTML = ''; |
| const text = textInput.value || 'プレビュー'; |
| const fontFamily = fontSelect.value; |
| const fontSize = fontSizeInput.value + 'px'; |
|
|
| const effects = getAvailableEffects(); |
| for (const effect of effects) { |
| try { |
| const imageUrl = await textToImage(text, fontFamily, fontSize, effect.name); |
|
|
| const presetCard = document.createElement('div'); |
| presetCard.className = 'effect-item'; |
| presetCard.dataset.effect = effect.name; |
| presetCard.innerHTML = ` |
| <div class="effect-name">${effect.name}</div> |
| <div class="preview-container"> |
| <img src="${imageUrl}" alt="${effect.name}"> |
| </div> |
| `; |
|
|
| effectGrid.appendChild(presetCard); |
| } catch (error) { |
| console.error(`プリセット ${effect.name} の描画エラー:`, error); |
|
|
| const errorCard = document.createElement('div'); |
| errorCard.className = 'effect-item error'; |
| errorCard.dataset.effect = effect.name; |
| errorCard.innerHTML = ` |
| <div class="effect-name text-danger">${effect.name}</div> |
| <div class="preview-container"> |
| <div class="text-danger"> |
| <small>エラー: ${error.message}</small> |
| </div> |
| </div> |
| `; |
| effectGrid.appendChild(errorCard); |
| } |
| } |
| } |
|
|
| |
| document.getElementById('postProcessContainer').addEventListener('change', () => { |
| renderAllPresets(); |
| }); |
|
|
| |
| document.addEventListener('DOMContentLoaded', async () => { |
| const fontSelect = document.getElementById('googleFontInput'); |
| const fontTagFilter = document.getElementById('fontTagFilter'); |
| const textInput = document.getElementById('textInput'); |
| const fontSizeInput = document.getElementById('fontSize'); |
| const verticalTextInput = document.getElementById('verticalText'); |
| const effectGrid = document.querySelector('.effect-grid'); |
| const noFontsMessage = document.getElementById('noFontsMessage'); |
|
|
| |
| function getTagsWithCount() { |
| const tagCount = new Map(); |
| fontTags.forEach(font => { |
| font.tags.forEach(tag => { |
| tagCount.set(tag, (tagCount.get(tag) || 0) + 1); |
| }); |
| }); |
| return tagCount; |
| } |
|
|
| |
| function isLanguageTag(tag) { |
| return ['japanese', 'english', 'kanji'].includes(tag); |
| } |
|
|
| |
| function createFilterButtons() { |
| const tagCount = getTagsWithCount(); |
| fontTagFilter.innerHTML = ''; |
|
|
| |
| const languageTags = [...tagCount.entries()] |
| .filter(([tag]) => isLanguageTag(tag)) |
| .sort((a, b) => b[1] - a[1]); |
|
|
| const otherTags = [...tagCount.entries()] |
| .filter(([tag]) => !isLanguageTag(tag)) |
| .sort((a, b) => b[1] - a[1]); |
|
|
| |
| if (languageTags.length > 0) { |
| const langGroup = document.createElement('div'); |
| langGroup.className = 'filter-group mb-2'; |
| langGroup.innerHTML = '<div class="filter-group-label mb-1">言語</div>'; |
| |
| const langButtonGroup = document.createElement('div'); |
| langButtonGroup.className = 'btn-group-wrapper'; |
| |
| languageTags.forEach(([tag, count]) => { |
| const displayName = tagDisplayNames[tag] || tag; |
| const button = document.createElement('div'); |
| button.className = 'btn-check-wrapper'; |
| button.innerHTML = ` |
| <input type="checkbox" class="btn-check" name="fontFilter" id="filter${tag}" value="${tag}"> |
| <label class="btn btn-outline-primary" for="filter${tag}"> |
| ${displayName} <span class="tag-count" data-tag="${tag}">(${count})</span> |
| </label> |
| `; |
| langButtonGroup.appendChild(button); |
| }); |
| |
| langGroup.appendChild(langButtonGroup); |
| fontTagFilter.appendChild(langGroup); |
| } |
|
|
| |
| if (otherTags.length > 0) { |
| const otherGroup = document.createElement('div'); |
| otherGroup.className = 'filter-group'; |
| otherGroup.innerHTML = '<div class="filter-group-label mb-1">スタイル</div>'; |
| |
| const otherButtonGroup = document.createElement('div'); |
| otherButtonGroup.className = 'btn-group-wrapper'; |
| |
| otherTags.forEach(([tag, count]) => { |
| const displayName = tagDisplayNames[tag] || tag; |
| const button = document.createElement('div'); |
| button.className = 'btn-check-wrapper'; |
| button.innerHTML = ` |
| <input type="checkbox" class="btn-check" name="fontFilter" id="filter${tag}" value="${tag}"> |
| <label class="btn btn-outline-primary" for="filter${tag}"> |
| ${displayName} <span class="tag-count" data-tag="${tag}">(${count})</span> |
| </label> |
| `; |
| otherButtonGroup.appendChild(button); |
| }); |
| |
| otherGroup.appendChild(otherButtonGroup); |
| fontTagFilter.appendChild(otherGroup); |
| } |
| } |
|
|
| |
| function updateTagCounts() { |
| const selectedFilters = Array.from(fontTagFilter.querySelectorAll('input[type="checkbox"]:checked')) |
| .map(checkbox => checkbox.value); |
|
|
| |
| if (selectedFilters.length === 0) { |
| const totalCounts = getTagsWithCount(); |
| totalCounts.forEach((count, tag) => { |
| const wrapper = document.querySelector(`#filter${tag}`).closest('.btn-check-wrapper'); |
| wrapper.style.display = 'inline-block'; |
| const countElement = document.querySelector(`.tag-count[data-tag="${tag}"]`); |
| if (countElement) { |
| countElement.textContent = `(${count})`; |
| } |
| }); |
| return; |
| } |
|
|
| |
| const allTags = [...new Set(fontTags.flatMap(font => font.tags))]; |
| allTags.forEach(tag => { |
| const countElement = document.querySelector(`.tag-count[data-tag="${tag}"]`); |
| const wrapper = document.querySelector(`#filter${tag}`).closest('.btn-check-wrapper'); |
| |
| if (countElement && wrapper) { |
| |
| const filtersToCheck = selectedFilters.includes(tag) |
| ? selectedFilters |
| : [...selectedFilters, tag]; |
| |
| const count = fontTags.filter(font => |
| filtersToCheck.every(filter => font.tags.includes(filter)) |
| ).length; |
|
|
| countElement.textContent = `(${count})`; |
|
|
| |
| if (count === 0 && !selectedFilters.includes(tag)) { |
| wrapper.style.display = 'none'; |
| } else { |
| wrapper.style.display = 'inline-block'; |
| } |
| } |
| }); |
| } |
|
|
| |
| function initializeFontOptions() { |
| |
| const currentFont = fontSelect.value; |
| |
| |
| const selectedFilters = Array.from(fontTagFilter.querySelectorAll('input[type="checkbox"]:checked')) |
| .map(checkbox => checkbox.value); |
| |
| |
| fontSelect.innerHTML = ''; |
| |
| |
| const filteredFonts = selectedFilters.length === 0 |
| ? fontTags |
| : fontTags.filter(font => |
| selectedFilters.every(filter => font.tags.includes(filter)) |
| ); |
|
|
| |
| if (filteredFonts.length === 0) { |
| noFontsMessage.style.display = 'block'; |
| fontSelect.disabled = true; |
| return Promise.resolve(); |
| } else { |
| noFontsMessage.style.display = 'none'; |
| fontSelect.disabled = false; |
| } |
|
|
| |
| filteredFonts.forEach((font, index) => { |
| const option = document.createElement('option'); |
| option.value = font.name; |
| option.textContent = font.name; |
| |
| if (font.name === currentFont || (index === 0 && !currentFont)) { |
| option.selected = true; |
| } |
| fontSelect.appendChild(option); |
| }); |
|
|
| |
| updateTagCounts(); |
|
|
| |
| return loadGoogleFont(fontSelect.value); |
| } |
|
|
| |
| fontTagFilter.addEventListener('change', async (e) => { |
| if (e.target.type === 'checkbox') { |
| await initializeFontOptions(); |
| if (!fontSelect.disabled) { |
| await renderAllPresets(); |
| } |
| } |
| }); |
|
|
| |
| createFilterButtons(); |
| await initializeFontOptions(); |
| await loadGoogleFont(fontSelect.value); |
|
|
| |
| verticalTextInput.addEventListener('change', (e) => { |
| effectGrid.dataset.vertical = e.target.checked; |
| renderAllPresets(); |
| }); |
|
|
| |
| fontSelect.addEventListener('change', async (e) => { |
| try { |
| const fontFamily = e.target.value; |
| await loadGoogleFont(fontFamily); |
| await renderAllPresets(); |
| } catch (error) { |
| console.error('フォント読み込みエラー:', error); |
| } |
| }); |
|
|
| |
| [textInput, fontSizeInput, verticalTextInput, verticalSpacing].forEach(element => { |
| element.addEventListener('input', () => { |
| debounceRender(renderAllPresets); |
| }); |
| }); |
|
|
| |
| await renderAllPresets(); |
|
|
| |
| initializePostProcessUI(); |
| }); |
|
|
| |
| document.getElementById('verticalText').addEventListener('change', function (e) { |
| const spacingContainer = document.getElementById('verticalSpacingContainer'); |
| spacingContainer.style.display = e.target.checked ? 'block' : 'none'; |
| |
| }); |
|
|
| |
| document.getElementById('verticalSpacing').addEventListener('input', function (e) { |
| document.getElementById('verticalSpacingValue').textContent = e.target.value; |
| |
| }); |
|
|
| |
| document.addEventListener('DOMContentLoaded', () => { |
| const themeSwitch = document.getElementById('themeSwitch'); |
| |
| |
| const savedTheme = localStorage.getItem('theme'); |
| if (savedTheme === 'dark') { |
| document.documentElement.setAttribute('data-bs-theme', 'dark'); |
| themeSwitch.checked = true; |
| } |
|
|
| |
| themeSwitch.addEventListener('change', () => { |
| if (themeSwitch.checked) { |
| document.documentElement.setAttribute('data-bs-theme', 'dark'); |
| localStorage.setItem('theme', 'dark'); |
| } else { |
| document.documentElement.setAttribute('data-bs-theme', 'light'); |
| localStorage.setItem('theme', 'light'); |
| } |
| }); |
| }); |
|
|