diff --git a/_.html b/_.html new file mode 100644 index 0000000..309cc0f --- /dev/null +++ b/_.html @@ -0,0 +1,21 @@ +
+ + +
+ \ No newline at end of file diff --git a/css/felexdev-themes.css b/css/felexdev-themes.css new file mode 100644 index 0000000..4038c96 --- /dev/null +++ b/css/felexdev-themes.css @@ -0,0 +1,112 @@ +/* Settings for dark system theme, same as color-sheme-light */ +:root { + --bg-color: #ddd; + --text-color: #222; + --accent-color: #007bff; + --border-color: #aaa; + --shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + --font-family: 'Arial', sans-serif; + --text-shadow: 0 2px 4px rgba(150, 150, 150, 0.3); + --width: 80vw; + --font-size: 1rem; +} + +@media (max-width: 756px) { + :root { + --font-size: 1.1rem; + --width: 92vw; + } +} +@media (min-width: 757px) and (max-width: 1024px) { + :root { + --font-size: 1rem; + --width: 90vw; + } +} + +/* Settings for dark system theme, same as color-sheme-dark */ +@media (prefers-color-scheme: dark) { + :root { + --bg-color: #222; + --text-color: #ddd; + --accent-color: #40a9ff; + --border-color: #444; + --text-shadow: 0 2px 4px rgba(150, 150, 150, 0.3); + --font-family: 'Arial', sans-serif; + } +} + +.flxd-tm-color-scheme-dark { + --bg-color: #222; + --text-color: #ddd; + --accent-color: #40a9ff; + --border-color: #444; + --text-shadow: 0 2px 4px rgba(150, 150, 150, 0.3); + --font-family: 'Arial', sans-serif; +} +.flxd-tm-color-scheme-light { + --bg-color: #ddd; + --text-color: #222; + --accent-color: #007bff; + --border-color: #aaa; + --text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + --font-family: 'Arial', sans-serif; +} +.flxd-tm-color-scheme-old-terminal { + --bg-color: #222; + --text-color: #9d9; + --accent-color: #40a9ff; + --border-color: #444; + --text-shadow: 0px 0px 10px rgba(255, 255, 0, 0.9); + --font-family: Monospace; +} + +body { + margin: auto; + background-color: var(--bg-color); + color: var(--text-color); + text-shadow: var(--text-shadow); + font-family: var(--font-family); + transition: background-color 0.3s ease, color 0.3s ease, font-size 0.3s ease; + width: var(--width); + font-size: var(--font-size); +} +a { + color: var(--text-color); + transition: background-color 0.3s ease, color 0.3s ease; + text-shadow: var(--text-shadow); + font-family: var(--font-family); + font-weight: bold; + transition:transform 0.3s ease, color 0.3s ease; + text-decoration:underline; + display:inline-block; +} +a:hover, a:focus { + transform: scale(1.05); + color:var(--accent-color); + text-shadow: var(--accent-color); + text-decoration: none; +} +.button { + background-color: var(--accent-color); + border: 1px solid var(--border-color); + padding: 0.5rem 1rem; + color: var(--text-color); + border-radius: 4px; + font-family: var(--font-family); + transition: background-color 0.3s ease, color 0.3s ease; +} + +#flxd-tm-btn-toggle { + position: fixed; + width: 30px; + height: 30px; + right: 15px; + top: 15px; + border-radius: 18px; + padding: 2px; + filter: drop-shadow(var(--text-shadow)); + cursor: pointer; + background-color: rgba(150, 150, 150, 0.3); + opacity: 0.6; +} diff --git a/html/head.html b/html/head.html index 333a3bc..466339e 100644 --- a/html/head.html +++ b/html/head.html @@ -1,70 +1,2 @@ - - - - - - - -Подопытный - - - -
- -
- - - -
-

Не бойся что-нибудь сломать, бойся это не исправить! 😂

- - - - -
-
- -

Laborum voluptate pariatur ex culpa magna nostrud est incididunt fugiat -pariatur do dolor ipsum enim. Consequat tempor do dolor eu. Non id id anim anim -excepteur excepteur pariatur nostrud qui irure ullamco.

- - -

Лучше не жмакай

-

Occaecat aliqua consequat laborum ut ex aute aliqua culpa quis irure esse magna dolore quis. Proident fugiat labore eu laboris officia Lorem enim. Ipsum occaecat cillum ut tempor id sint aliqua incididunt nisi incididunt reprehenderit. Voluptate ad minim sint est aute aliquip esse occaecat tempor officia qui sunt. Aute ex ipsum id ut in est velit est laborum incididunt. Aliqua qui id do esse sunt eiusmod id deserunt eu nostrud aute sit ipsum. Deserunt esse cillum Lorem non magna adipisicing mollit amet consequat.

- -

Сюда тоже

-

Anim eiusmod irure incididunt sint cupidatat. Incididunt irure irure irure nisi ipsum do ut quis fugiat consectetur proident cupidatat incididunt cillum. Dolore voluptate occaecat qui mollit laborum ullamco et. Ipsum laboris officia anim laboris culpa eiusmod ex magna ex cupidatat anim ipsum aute. Mollit aliquip occaecat qui sunt velit ut cupidatat reprehenderit enim sunt laborum. Velit veniam in officia nulla adipisicing ut duis officia.

-

Exercitation voluptate irure in irure tempor mollit Lorem nostrud ad officia. Velit id fugiat occaecat do tempor. Sit officia Lorem aliquip eu deserunt consectetur. Aute proident deserunt in nulla aliquip dolore ipsum Lorem ut cupidatat consectetur sit sint laborum. Esse cupidatat sit sint sunt tempor exercitation deserunt. Labore dolor duis laborum est do nisi ut veniam dolor et nostrud nostrud.

- -

Про эту вообще забудь!

-

Tempor proident minim aliquip reprehenderit dolor et ad anim Lorem duis sint eiusmod. Labore ut ea duis dolor. Incididunt consectetur proident qui occaecat incididunt do nisi Lorem. Tempor do laborum elit laboris excepteur eiusmod do. Eiusmod nisi excepteur ut amet pariatur adipisicing Lorem.

-

Occaecat nulla excepteur dolore excepteur duis eiusmod ullamco officia anim in voluptate ea occaecat officia. Cillum sint esse velit ea officia minim fugiat. Elit ea esse id aliquip pariatur cupidatat id duis minim incididunt ea ea. Anim ut duis sunt nisi. Culpa cillum sit voluptate voluptate eiusmod dolor. Enim nisi Lorem ipsum irure est excepteur voluptate eu in enim nisi. Nostrud ipsum Lorem anim sint labore consequat do.

- - -
- - - \ No newline at end of file + diff --git a/js/felexdev-theme-manager.js b/js/felexdev-theme-manager.js new file mode 100644 index 0000000..e2869f4 --- /dev/null +++ b/js/felexdev-theme-manager.js @@ -0,0 +1,418 @@ +/** + * @file ThemeManager.js + * @version 1.0.0 dev-in-progress (2026-06-09) + * @author felex67 (admin@felexdev.ru) + * @license Apache2.0 + * + * @brief ThemeManager - модуль-переключатель тем + * @details Архитектура singleton-модуля на базе самоинициализирующегося статического класса + */ +class ThemeManager { + /** + * Состояние модуля: + * При инициализации данного объекта создаётся обработчик события изменения системной темы + * автоматически выставляющий поле `.isDark` в соответствующее значение. + * @typedef ManagerState + * + * @property {DOMTokenList} clist: список классов ::root + * @property {HTMLElement} element: ::root, тег html + * @property {number} idx: индекс текущей темы + * @property {boolean} isDark: флаг тёмной системной темы + * @property {MediaQueryList} _schemeQuery: Медиа слушатель системной темы + * @property {boolean} _schemeListener: Обработчик изменений системной темы + */ + /** + * @typedef Theme + * @property {string} name: Внутреннее название темы `'auto'` + * @property {string} class: Селектор CSS + * @property {number} idx: Индекс темы в массиве `#THEMES` + * @property {HTMLDivElement} row: Строка меню + * @property {HTMLDivElement} icon: Иконка темы + * @property {HTMLDivElement} desc: Описательное имя темы + * @property {ThemeMenuSizes} sizes: Фактические размеры меню при применении темы в пикселях + */ + /** + * @typedef ThemeMenuSizes - Фактические размеры меню при использовании темы + * @property {string} w: ширина меню в пикселях + * @property {string} h: высота меню в пикселях + */ + /** + * @typedef MenuState + * Состояние меню + * + * @property {DOMTokenList} clist: список классов управляемого элемента classList + * @property {number} idx: индекс текущей темы меню + * @property {number} tmh: таймер сокрытия меню + * @property {number} tms: таймер показа меню + */ + /** + * Меню выбора тем + * @typedef ThemeMenu + * @property {ThemeManager} _tm - Ссылка на класс + * @property {MenuState} _stt - Состояние меню + * @property {[Theme]} _t - Ссылка на массив тем + * @property {()=>void} _tv - Переключает видимость + * @property {HTMLDivElement} div - Управляемый div элемент + * @property {CSSStyleDeclaration} style - Ссылка на стиль управляемого элемента + * @property {()=>void} appendChild - div.appendChild + * @property {(idx: number)=>void} apply - Превью темы + * @property {()=>void} hide - Скрыть меню + * @property {()=>void} show - Показать меню + */ + + + /** + * Глобальное состояние модуля + * + * @static + * @type {ManagerState} + */ + static #$ = (() => { + let e = { + clist: document.documentElement.classList, + element: document.documentElement, + idx: 0, + isDark: q.matches, + _schemeQuery: window.matchMedia('(prefers-color-scheme: dark)'), + _schemeListener: null + }; + e._schemeQuery.addEventListener('change', e._schemeListener = (function(ev) { this.isDark = ev.matches; }).bind(e)); + return e; + })(); + /** + * Конфигурация/настройки модуля + * @property {string} buttonToggleId: Идентификатор кнопки-переключателя тем. 'button-toggle' + * @property {string} colorSchemePrefix: Префикс селектора CSS. 'color-scheme' + * @property {string} defautlIcon: Иконка темы по умолчанию + * @property {string} globalPrefix: Глобальный префикс. 'theme-' + * @property {number} menuAppearTimeout: таймаут до появления меню в милисекундах (2000), + * @property {number} menuDisappearTimeout: таймауйт исчезновения меню в милисекудах (300), + * @property {string} menuId: Идентификатор меню. 'theme-menu' + * @property {string} menuRowClass: Класс элементов-строк меню 'theme-menu-row', + * @property {string} menuImageClass: Класс иконки темы 'theme-menu-image', + * @property {string} menuDescriptionClass: Класс описателя темы 'theme-menu-desc', + * @property {string} storageKey: Ключ в хранилище 'theme-id', + */ + static #CONFIG = (function() { + const GLOBAL_PREFIX = 'flxd-tm'; + const VISUALY_HIDDEN = 'visualy-hidden'; + + let c = { + /* Основные настройки модуля */ + globalPrefix: 'flxd-tm', // глобальный префикс + storageKey: 'id', // базовое имя ключа в хранилище + toggle: { + id: 'button-toggle-color-scheme', // идентификатор кнопки переключения тем + liveId: 'button-tiggle-live', // идентификатор aria-live блока + liveClass: 'visualy-hidden', + }, + theme: { + prefix: 'color-scheme', // корень названия классов темы + }, + menu: { + attr: { + 'id': 'menu', + 'role': 'menu', + }, + row: { + attr: { + class: 'entry', + role: 'menuitem', + }, + icon: { + attr: { + class: 'icon', + 'aria-hidden': 'true' + }, + + }, + }, + tmtAppear: 2000, + tmtDisappear: 300, + rowClass: 'row', + iconClass: 'icon', + descClass: 'desc', + ariaLabel: 'theme menu', + }, + + menuId: 'menu', // базовый идентификатор меню + menuRowClass: 'row', // базовый класс строки меню + menuImageClass: 'image', // базовый класс картинки темы в меню + menuDescriptionClass: 'desc', // базовый класс описательного имени темы в меню + menuAreaLabel: 'theme menu', // описание элемента меню + menuRowAreaLabel: 'row', // описание элемента строки меню + menuIconAreaLabel: 'icon', // описание элемента иконки темы + menuDescAreaLabel: 'description', // описание элемента описания темы + defautlIcon:'', + }; + /* Собираем строки */ + c.globalPrefix += '-'; + /* С этого момента globalPrefix = '${c.globalPrefix}-' */ + c.storageKey = c.globalPrefix + c.storageKey; + /* Логика инициализации */ + /* Меню выбора тем оформления */ + /* инициализация классов меню */ + c.menuId = c.globalPrefix + c.menuId; + c.menuRowClass = `${c.menuId}-${c.menuRowClass}`; + c.menuImageClass = `${c.menuId}-${c.menuImageClass}`; + c.menuDescriptionClass = `${c.menuId}-${c.menuDescriptionClass}`; + /* Настраиваем ARIA */ + c.menuRowAreaLabel = `${c.menuAreaLabel} ${c.menuRowAreaLabel}`; + c.menuIconAreaLabel = `${c.menuRowAreaLabel} ${c.menuIconAreaLabel}`; + c.menuDescAreaLabel = `${c.menuRowAreaLabel} ${c.menuDescAreaLabel}`; + /* Кнопка смены темы оформления */ + /* инициализация имени кнопки переключения тем */ + c.buttonToggleId = c.globalPrefix + c.buttonToggleId; + /* инициализация префикса классов в селекторе CSS */ + /* Тема оформления */ + c.colorSchemePrefix = c.globalPrefix + c.colorSchemePrefix + '-'; + /* Возвращаем полностью инициализированный объект с динамически скомпонованными строками */ + return c; + })(); + + /** + * Массив тем + * + * @static + * @type {[Theme]} + */ + static #THEMES = []; + + + /** + * Сохраняет индекс текущей темы в хранилище + * + * @static + * @type {(idx: number) => void} + */ + static #save = (() => { + let save = (idx) => { + console.error(this.#CONFIG.globalPrefix + '#save(): It is unavailable to save a theme, the storages are unreachable.'); + } + try { + /* Пробуем localStorage */ + this.#$.current = parseInt(localStorage.getItem(this.#CONFIG.storageKey)) || 0; + save = (idx) => { localStorage.setItem(this.#CONFIG.storageKey, idx); }; + } + catch(e) { + console.log(this.#CONFIG.globalPrefix + '#save-IIFE(): localStorage is unreachable, trying to use cookie.') + try { + let cookie = document.cookie.match(`${this.#CONFIG.storageKey}\\s*=\\s*(\\d+)`); + this.#$.current = cookie ? parseInt(cookie[1]) || 0 : 0; + save = (idx) => { document.cookie = `${this.#CONFIG.storageKey}=${idx}; Max-Age=${31536000}; Path=/`; }; + } + catch (e) { + console.log(`${this.#CONFIG.globalPrefix}#save-IIFE: All storages are unreachable('localStorage' & 'document.cookie')`); + } + } + /* Мы не дублируем, а просто навешиваем класс на `::root` ели тема не 'auto' + * Это самый быстрый способ применить тему. + * Не знаю как Вам, а мне - всё понятно =D */ + if (this.#$.current < this.#THEMES.length) + this.#$.current > 0 + ? this.#$.element.classList.add(this.#THEMES[this.#$.current].class) + : this.#$.current + && (console.error(`${this.#CONFIG.globalPrefix}#save-IIFE: Negative theme index!!!: '${this.#$.current}'. Fallback to 'auto'`) + || (this.#$.current = 0)); + else + console.log(`${this.#CONFIG.globalPrefix}#save-IIFE: Theme index out of range: '${this.#$.current}'. Fallback to 'auto'`); + return save; + })(); + + /** + * Добавляет/заменяет/удаляет класс старой темы на новый + * @param {number} idx - Индекс новой темы + * @param {{ idx: number, clist: DOMTokenList }?} e - JS-объект со следующими обязательными полями: { idx: number, clist: DOMTokenList, } + * @param {[Theme]?} t - Ссылка на массив тем + */ + static #set(idx, e = this.#$, t = this.#THEMES) { + if (idx == e.idx) + return; + if (idx && e.idx) + e.clist.replace(t[e.idx].class, t[idx].class); + else if (idx) + e.clist.add(t[idx].class); + else + e.clist.remove(t[e.idx].class); + this.#save(e.idx = idx); + } + + /** + * Меню выбора тем + * + * @static + * @type {ThemeMenu} + */ + static #MENU = (() => { + let menu = { + /* private: */ + _сonfig: { + timeout: { + apear: 2000, // таймаут появления меню + disapear: 300, // таймаут исчезновения меню + }, + select: { + id: 'menu', // базовый идентификатор меню + row: 'row', // базовый класс строки меню + image: 'image', // базовый класс картинки темы в меню + desc: 'desc', // базовый класс описательного имени темы в меню + }, + aria: { + menu: 'меню выбора тем',// описание элемента меню + row: 'тема', // описание элемента строки меню + }, + }, + /* Свойства */ + _tm: this, // ссылка на основной класс1 + /* СОстояние */ + _stt: { + clist: null, // classList of element + idx: 0, // текущая тема меню + tmh: null, // таймер сокрытия меню + tms: null, // таймер показа меню + }, + _t: this.#THEMES, // ссылка на массив тем + /* Методы */ + _tv: null, // переключить видимость + /* public: */ + /* Методы */ + appendChild: null, // `div.appendChild` + apply: null, + div: document.createElement('div'), // ссылка на контролируемый DOM-элемент + hide: null, // спрятать меню + show: null, // показать меню + style: null, // `div.style` + }; + /* Инициализируем div-элемент */ + menu.div.id = this.#CONFIG.menuId; // устанавливаем id + menu._stt.clist = menu.div.classList; // привязываем список классов + /* Пробрасываем часто нужные методы и поля div-элемента */ + menu.appendChild = menu.div.appendChild.bind(menu._state.element); + menu.style = menu._state.element.style; + /* Внутренняя логика меню */ + + /* Спрятать меню */ + menu.hide = (function() { + this.style.width = this.style.height = '0px'; + }).bind(menu); + /* Показать меню */ + menu.show = (function() { + let s = this._themes[this._manager.#current].sizes; + this.style.visibility = 'visible'; + this.style.width = s.w; + this.style.height = s.h; + }).bind(menu); + + /* Применяем тему ко всему документу */ + menu.apply = (function(idx) { + this._manager.#set(idx, this._state); + this.hide(); + /* Это необходимо для того чтобы меню подхватило стиль всего документа */ + this._remove(this.themes[this._current].name); + this._current = 0; + }).bind(menu); + + return menu; + })(); + + /** + * Создаёт и инициализирует новый объект темы + * @param {string} name - внутреннее название темы. + * @param {string?} desc - описательное название темы. `{ desc: desc || name, }` + * @param {string?} svg - векторное изображение темы. `{ svg: svg || this.#CONFIG.defaultImg, }` + * + * @returns {Theme} - инициализированный объект темы + * + * @throws {Error} при `!name.length || typeof(name) != 'string'` + * @throws {Error} если тема с таким 'name' уже существует + */ + static #newTheme(name, desc = null, svg = null) { + if ((typeof name) != 'string' || !name) { + throw new Error(`${this.#CONFIG.globalPrefix}#newTheme: Пустое или не строковое значение 'name': ${typeof name}, val: '${name}'`) + } + else if (-1 != this.#THEMES.indexOf(name)) { + throw new Error(`${this.#CONFIG.globalPrefix}#newTheme: theme '${name}' allready exists`); + } + let theme = { + menu: this.#MENU, + name: name, // Имя темы + class: this.#CONFIG.colorSchemePrefix + name, // Класс в CSS-селекторе + idx: 0, // Индекс темы в массиве тем + row: document.createElement('div'), // Элемент строки + icon: document.createElement('div'), // Элемент иконки + desc: document.createElement('div'), // Элемент описания + sizes: { w: '0px', h: '0px' }, // Размеры меню при установке темы(для анимации) + }; + /* настраиваем классы элементов */ + theme.row.classList.add(this.#CONFIG.menuRowClass); + theme.icon.classList.add(this.#CONFIG.menuImageClass); + theme.desc.classList.add(this.#CONFIG.menuDescriptionClass); + /* заполняем иконку и описание */ + theme.icon.innerHTML = svg || this.#CONFIG.defautlIcon; + theme.desc.innerText = desc || name; + /* собираем строку меню */ + theme.row.appendChild(theme.icon); + theme.row.appendChild(theme.desc); + /* настраиваем обработчики */ + theme.row.addEventListener('mouseenter', (function () { this.menu.preview(this.idx); }).bind(theme)); + theme.row.addEventListener('focusin', (function () { this.menu.preview(this.idx); }).bind(theme)); + this.#MENU.appendChild(theme.row); + theme.idx = this.#THEMES.push(theme) - 1; + return theme; + } + static #findTheme(name) { + let id = -1; + for (let i = 0; i < this.#THEMES.length; i++) { + if (name === this.#THEMES[i].name) { + id = i; + } + } + return id; + } + static #initDeafultThemes = (() => { + this.#newTheme( + 'auto', + 'Системная', + '', + ); + this.#newTheme( + 'light', + 'Светлая', + '', + ); + this.#newTheme( + 'dark', + 'Тёмная', + '', + ); + this.#newTheme( + 'old-terminal', + 'Старый терминал', + '', + ); + }); + static #_ = (() => { + this.#initDeafultThemes(); + console.log(this.#CONFIG); + console.log(this.#MENU); + })(); +}; + +const menu = document.createElement('div'); +menu.setAttribute('aria-label', 'меню выбора тем'); +menu.setAttribute('role', 'menu'); +menu.tabIndex = 0; +for (let n = 1; n < 6; n++) { + let r = document.createElement('div'); + let i = document.createElement('div'); + let d = document.createElement('div'); + + r.setAttribute('role', `menuitem`); + r.setAttribute('aria-label', `цветовая схема ` + n); + i.setAttribute('aria-hidden', 'true'); + d.setAttribute('aria-hidden', 'true'); + + r.appendChild(i); + r.appendChild(d); + menu.appendChild(r); +} diff --git a/test.html b/test.html new file mode 100644 index 0000000..67b0247 --- /dev/null +++ b/test.html @@ -0,0 +1,54 @@ + + + + + + + + Подопытный + + + + +
+

Не бойся что-нибудь сломать, бойся это не исправить! 😂

+ + + + +
+
+ +

Laborum voluptate pariatur ex culpa magna nostrud est incididunt fugiat +pariatur do dolor ipsum enim. Consequat tempor do dolor eu. Non id id anim anim +excepteur excepteur pariatur nostrud qui irure ullamco.

+ + +

Лучше не жмакай

+

Occaecat aliqua consequat laborum ut ex aute aliqua culpa quis irure esse magna dolore quis. Proident fugiat labore eu laboris officia Lorem enim. Ipsum occaecat cillum ut tempor id sint aliqua incididunt nisi incididunt reprehenderit. Voluptate ad minim sint est aute aliquip esse occaecat tempor officia qui sunt. Aute ex ipsum id ut in est velit est laborum incididunt. Aliqua qui id do esse sunt eiusmod id deserunt eu nostrud aute sit ipsum. Deserunt esse cillum Lorem non magna adipisicing mollit amet consequat.

+ +

Сюда тоже

+

Anim eiusmod irure incididunt sint cupidatat. Incididunt irure irure irure nisi ipsum do ut quis fugiat consectetur proident cupidatat incididunt cillum. Dolore voluptate occaecat qui mollit laborum ullamco et. Ipsum laboris officia anim laboris culpa eiusmod ex magna ex cupidatat anim ipsum aute. Mollit aliquip occaecat qui sunt velit ut cupidatat reprehenderit enim sunt laborum. Velit veniam in officia nulla adipisicing ut duis officia.

+

Exercitation voluptate irure in irure tempor mollit Lorem nostrud ad officia. Velit id fugiat occaecat do tempor. Sit officia Lorem aliquip eu deserunt consectetur. Aute proident deserunt in nulla aliquip dolore ipsum Lorem ut cupidatat consectetur sit sint laborum. Esse cupidatat sit sint sunt tempor exercitation deserunt. Labore dolor duis laborum est do nisi ut veniam dolor et nostrud nostrud.

+ +

Про эту вообще забудь!

+

Tempor proident minim aliquip reprehenderit dolor et ad anim Lorem duis sint eiusmod. Labore ut ea duis dolor. Incididunt consectetur proident qui occaecat incididunt do nisi Lorem. Tempor do laborum elit laboris excepteur eiusmod do. Eiusmod nisi excepteur ut amet pariatur adipisicing Lorem.

+

Occaecat nulla excepteur dolore excepteur duis eiusmod ullamco officia anim in voluptate ea occaecat officia. Cillum sint esse velit ea officia minim fugiat. Elit ea esse id aliquip pariatur cupidatat id duis minim incididunt ea ea. Anim ut duis sunt nisi. Culpa cillum sit voluptate voluptate eiusmod dolor. Enim nisi Lorem ipsum irure est excepteur voluptate eu in enim nisi. Nostrud ipsum Lorem anim sint labore consequat do.

+ + +
+ + + \ No newline at end of file