diff --git a/README.md b/README.md index 73fc878..748c851 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A lightweight JavaScript library for managing TRMNL device model and palette selection. -It's used by the core TRMNL web app, [trmnlp](https://github.com/usetrmnl/trmnlp), [BYOS (Bring Your Own Server)](https://docs.usetrmnl.com/go/diy/byos) applications, and anybody else who needs it. +It's used by the core TRMNL web app, [trmnlp](https://github.com/usetrmnl/trmnlp), [BYOS (Bring Your Own Server)](https://docs.trmnl.com/go/diy/byos) applications, and anybody else who needs it. ![Screenshot](example/screenshot.png) diff --git a/dist/trmnl-picker.esm.js b/dist/trmnl-picker.esm.js index e2dd43a..e84b6d2 100644 --- a/dist/trmnl-picker.esm.js +++ b/dist/trmnl-picker.esm.js @@ -3,7 +3,7 @@ var _DEFAULT_MODEL_NAME = "og_plus"; var _API_CACHE_KEY = "trmnl-picker-api-cache"; var _CACHE_TTL_MS = 24 * 60 * 60 * 1e3; var TRMNLPicker = class _TRMNLPicker { - static API_BASE_URL = "https://usetrmnl.com"; + static API_BASE_URL = "https://trmnl.com"; /** * Get cached API response from localStorage * @private diff --git a/dist/trmnl-picker.esm.js.map b/dist/trmnl-picker.esm.js.map index d99c935..2fe2eef 100644 --- a/dist/trmnl-picker.esm.js.map +++ b/dist/trmnl-picker.esm.js.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["../src/index.js"], - "sourcesContent": ["/**\n * Default model to select when no localStorage state is found\n * @private\n * @constant {string}\n */\nconst _DEFAULT_MODEL_NAME = 'og_plus'\n\n/**\n * localStorage key for caching API responses (models and palettes)\n * @private\n * @constant {string}\n */\nconst _API_CACHE_KEY = 'trmnl-picker-api-cache'\n\n/**\n * Cache TTL in milliseconds (1 day)\n * @private\n * @constant {number}\n */\nconst _CACHE_TTL_MS = 24 * 60 * 60 * 1000\n\n/**\n * Event fired when picker state changes\n * @event TRMNLPicker#trmnl:change\n * @type {CustomEvent}\n * @property {Object} detail - Event details\n * @property {string} detail.origin - What triggered the change: 'constructor', 'form', or 'setParams'\n * @property {Array} detail.screenClasses - Array of CSS classes for Framework CSS rendering\n * @property {Object} detail.model - Current model object with name, label, width, height, kind, css properties\n * @property {Object} detail.palette - Current palette object with id, name, framework_class properties\n * @property {boolean} detail.isPortrait - Portrait orientation flag\n * @property {boolean} detail.isDarkMode - Dark mode flag\n * @property {number} detail.width - Current width of the screen in pixels\n * @property {number} detail.height - Current height of the screen in pixels\n *\n * @example\n * picker.formElement.addEventListener('trmnl:change', (event) => {\n * const { origin, screenClasses, model, palette } = event.detail\n * console.log(`Changed via ${origin}`)\n * console.log('Classes:', screenClasses)\n * })\n */\n\n/**\n * TRMNLPicker - Vanilla JS library for TRMNL device and palette selection\n *\n * Provides a reactive picker component that manages device models, color palettes,\n * orientation, and display mode. Emits 'trmnl:change' events with current state\n * and CSS classes for rendering.\n * \n * **[View a live demo \u2192](https://usetrmnl.github.io/trmnl-picker/example/)**\n *\n * **Note:** Using the constructor directly is not recommended. Use the static\n * {@link TRMNLPicker.create} method instead, which automatically fetches\n * models and palettes from the TRMNL API if not provided.\n *\n * @class TRMNLPicker\n * @param {string|Element} formIdOrElement - Form element ID or DOM element reference\n * @param {Object} options - Configuration options\n * @param {Array} options.models - Array of model objects from TRMNL API\n * @param {Array} options.palettes - Array of palette objects from TRMNL API\n * @param {string} [options.localStorageKey] - Optional key for persisting state to localStorage\n *\n * @fires TRMNLPicker#trmnl:change\n *\n * @example\n * // HTML Structure - Required form with data-* attributes\n * //
\n * // \n * // \n * //\n * // \n * // \n * //\n * // \n * // \n * //\n * // \n * // \n * //\n * // \n * // \n * //
\n *\n * // Create with element ID\n * const picker = new TRMNLPicker('picker-form', { models, palettes })\n *\n * // Create with DOM element\n * const element = document.getElementById('picker-form')\n * const picker = new TRMNLPicker(element, { models, palettes })\n *\n * // Listen for changes\n * picker.formElement.addEventListener('trmnl:change', (event) => {\n * console.log(event.detail.screenClasses)\n * })\n */\nclass TRMNLPicker {\n static API_BASE_URL = 'https://usetrmnl.com'\n\n /**\n * Get cached API response from localStorage\n * @private\n * @static\n * @returns {{models: Array, palettes: Array} | null} Cached data or null if expired/missing\n */\n static _getCachedApiData() {\n try {\n const cached = localStorage.getItem(_API_CACHE_KEY)\n if (!cached) return null\n\n const { timestamp, models, palettes } = JSON.parse(cached)\n const now = Date.now()\n\n // Check if cache is still valid\n if (now - timestamp > _CACHE_TTL_MS) {\n localStorage.removeItem(_API_CACHE_KEY)\n return null\n }\n\n return { models, palettes }\n } catch (error) {\n console.warn('TRMNLPicker: Failed to read API cache:', error)\n return null\n }\n }\n\n /**\n * Save API response to localStorage cache\n * @private\n * @static\n * @param {Array} models - Models array\n * @param {Array} palettes - Palettes array\n */\n static _setCachedApiData(models, palettes) {\n try {\n const cacheData = {\n timestamp: Date.now(),\n models,\n palettes\n }\n localStorage.setItem(_API_CACHE_KEY, JSON.stringify(cacheData))\n } catch (error) {\n console.warn('TRMNLPicker: Failed to save API cache:', error)\n }\n }\n\n /**\n * Create a TRMNLPicker instance, fetching data from TRMNL API if not provided\n *\n * Automatically caches API responses in localStorage for 24 hours to reduce network requests.\n *\n * @static\n * @param {string|Element} formIdOrElement - Form element ID or DOM element\n * @param {Object} options - Configuration options\n * @param {Array} [options.models] - Optional models array (fetched from API if not provided)\n * @param {Array} [options.palettes] - Optional palettes array (fetched from API if not provided)\n * @param {string} [options.localStorageKey] - Optional key for state persistence\n * @returns {Promise} Promise resolving to picker instance\n * @throws {Error} If API fetch fails when models or palettes are not provided\n *\n * @example\n * // Fetch models and palettes from API (or use cached data if available)\n * const picker = await TRMNLPicker.create('screen-picker')\n *\n * // Provide your own data\n * const picker = await TRMNLPicker.create('screen-picker', { models, palettes })\n */\n static async create(formId, options = {}) {\n let { models, palettes, localStorageKey } = options\n\n // Try to use cached data if neither models nor palettes are provided\n if (!models && !palettes) {\n const cached = TRMNLPicker._getCachedApiData()\n if (cached) {\n models = cached.models\n palettes = cached.palettes\n }\n }\n\n // Fetch models if not provided and not in cache\n if (!models) {\n try {\n const response = await fetch(`${TRMNLPicker.API_BASE_URL}/api/models`)\n if (!response.ok) {\n throw new Error(`Failed to fetch models: ${response.status} ${response.statusText}`)\n }\n const data = await response.json()\n models = data.data || data\n } catch (error) {\n throw new Error(`TRMNLPicker: Failed to fetch models from API: ${error.message}`)\n }\n }\n\n // Fetch palettes if not provided and not in cache\n if (!palettes) {\n try {\n const response = await fetch(`${TRMNLPicker.API_BASE_URL}/api/palettes`)\n if (!response.ok) {\n throw new Error(`Failed to fetch palettes: ${response.status} ${response.statusText}`)\n }\n const data = await response.json()\n palettes = data.data || data\n } catch (error) {\n throw new Error(`TRMNLPicker: Failed to fetch palettes from API: ${error.message}`)\n }\n }\n\n // Cache the API data for future use (if both were fetched)\n if (!options.models && !options.palettes) {\n TRMNLPicker._setCachedApiData(models, palettes)\n }\n\n return new TRMNLPicker(formId, { models, palettes, localStorageKey })\n }\n\n constructor(formIdOrElement, options = {}) {\n // Validate inputs\n if (!formIdOrElement) {\n throw new Error('TRMNLPicker: formIdOrElement is required')\n }\n\n // Store references - accept either string ID or DOM element\n if (typeof formIdOrElement === 'string') {\n this.formElement = document.getElementById(formIdOrElement)\n if (!this.formElement) {\n throw new Error(`TRMNLPicker: Form element with id \"${formIdOrElement}\" not found`)\n }\n } else if (formIdOrElement instanceof Element) {\n this.formElement = formIdOrElement\n } else {\n throw new Error('TRMNLPicker: formIdOrElement must be a string ID or DOM element')\n }\n\n const { models, palettes, localStorageKey } = options\n\n this.models = models\n this.palettes = palettes\n this.localStorageKey = localStorageKey\n\n // Only initialize if we have data\n if (this.models && this.palettes) {\n if (!Array.isArray(this.models) || this.models.length === 0) {\n throw new Error('TRMNLPicker: models must be a non-empty array')\n }\n\n if (!Array.isArray(this.palettes) || this.palettes.length === 0) {\n throw new Error('TRMNLPicker: palettes must be a non-empty array')\n }\n\n // Filter out models where all palettes have empty framework_class\n this.models = this._filterValidModels()\n\n if (this.models.length === 0) {\n throw new Error('TRMNLPicker: no valid models found (all models have palettes with empty framework_class)')\n }\n\n // Initialize DOM elements and bind events\n this._initializeElements()\n this._bindEvents()\n\n // Set initial state (will load from localStorage if available)\n this._setInitialState()\n }\n }\n\n /**\n * Filter out models where all their palettes have empty framework_class\n * @private\n * @returns {Array} Filtered models array\n */\n _filterValidModels() {\n return this.models.filter(model => {\n // Check if at least one palette for this model has a non-empty framework_class\n return model.palette_ids.some(paletteId => {\n const palette = this.palettes.find(p => p.id === paletteId)\n return palette && palette.framework_class && palette.framework_class.trim() !== ''\n })\n })\n }\n\n /**\n * Get the first valid palette ID for a model (one with non-empty framework_class)\n * @private\n * @param {Object} model - Model object\n * @returns {string|null} First valid palette ID or null\n */\n _getFirstValidPaletteId(model) {\n if (!model) return null\n\n for (const paletteId of model.palette_ids) {\n const palette = this.palettes.find(p => p.id === paletteId)\n if (palette && palette.framework_class && palette.framework_class.trim() !== '') {\n return paletteId\n }\n }\n\n return null\n }\n\n /**\n * Find and store references to form elements using data-* attributes\n * @private\n */\n _initializeElements() {\n this.elements = {\n modelSelect: this.formElement.querySelector('[data-model-select]'),\n paletteSelect: this.formElement.querySelector('[data-palette-select]'),\n orientationToggle: this.formElement.querySelector('[data-orientation-toggle]'),\n darkModeToggle: this.formElement.querySelector('[data-dark-mode-toggle]'),\n resetButton: this.formElement.querySelector('[data-reset-button]'),\n\n // Optional: UI indicator elements\n orientationText: this.formElement.querySelector('[data-orientation-text]'),\n darkModeText: this.formElement.querySelector('[data-dark-mode-text]')\n }\n\n // Validate required elements\n const required = ['modelSelect', 'paletteSelect']\n for (const key of required) {\n if (!this.elements[key]) {\n throw new Error(`TRMNLPicker: Required element \"${key}\" not found in form`)\n }\n }\n }\n\n /**\n * Bind event listeners to form elements\n * @private\n */\n _bindEvents() {\n // Store bound handlers for cleanup\n this.handlers = {\n modelChange: this._handleModelChange.bind(this),\n paletteChange: this._handlePaletteChange.bind(this),\n orientationToggle: this._toggleOrientation.bind(this),\n darkModeToggle: this._toggleDarkMode.bind(this),\n reset: this._resetToModelDefaults.bind(this)\n }\n\n // Attach event listeners\n this.elements.modelSelect.addEventListener('change', this.handlers.modelChange)\n this.elements.paletteSelect.addEventListener('change', this.handlers.paletteChange)\n\n if (this.elements.orientationToggle) {\n this.elements.orientationToggle.addEventListener('click', this.handlers.orientationToggle)\n }\n\n if (this.elements.darkModeToggle) {\n this.elements.darkModeToggle.addEventListener('click', this.handlers.darkModeToggle)\n }\n\n if (this.elements.resetButton) {\n this.elements.resetButton.addEventListener('click', this.handlers.reset)\n }\n }\n\n /**\n * Set initial state and populate form\n * @private\n */\n _setInitialState() {\n // Separate models into TRMNL and BYOD groups\n const trmnlModels = this.models.filter(m => m.kind === 'trmnl')\n const byodModels = this.models.filter(m => m.kind !== 'trmnl')\n\n // Sort each group alphabetically by label\n const sortTRMNL = [...trmnlModels].sort((a, b) => {\n const labelA = (a.label || a.name).toLowerCase()\n const labelB = (b.label || b.name).toLowerCase()\n return labelA.localeCompare(labelB)\n })\n\n const sortBYOD = [...byodModels].sort((a, b) => {\n const labelA = (a.label || a.name).toLowerCase()\n const labelB = (b.label || b.name).toLowerCase()\n return labelA.localeCompare(labelB)\n })\n\n // Populate model select with grouped models\n this.elements.modelSelect.innerHTML = ''\n\n // Add TRMNL group\n if (sortTRMNL.length > 0) {\n const trmnlGroup = document.createElement('optgroup')\n trmnlGroup.label = 'TRMNL'\n sortTRMNL.forEach(model => {\n const option = document.createElement('option')\n option.value = model.name\n option.textContent = model.label || model.name\n trmnlGroup.appendChild(option)\n })\n this.elements.modelSelect.appendChild(trmnlGroup)\n }\n\n // Add BYOD group\n if (sortBYOD.length > 0) {\n const byodGroup = document.createElement('optgroup')\n byodGroup.label = 'BYOD'\n sortBYOD.forEach(model => {\n const option = document.createElement('option')\n option.value = model.name\n option.textContent = model.label || model.name\n byodGroup.appendChild(option)\n })\n this.elements.modelSelect.appendChild(byodGroup)\n }\n\n // Keep combined sorted list for default selection logic\n const sortedModels = [...sortTRMNL, ...sortBYOD]\n\n // Initialize state\n this._state = {}\n\n const defaultModel = sortedModels.find(m => m.name === _DEFAULT_MODEL_NAME) || sortedModels[0]\n const defaultPaletteId = this._getFirstValidPaletteId(defaultModel)\n const defaultParams = {\n modelName: defaultModel.name,\n paletteId: defaultPaletteId,\n isPortrait: false,\n isDarkMode: false\n }\n\n const savedParams = this._loadFromLocalStorage() || {}\n\n this._setParams('constructor', { ...defaultParams, ...savedParams })\n }\n\n /**\n * Populate palette dropdown based on selected model\n * @private\n */\n _populateModelPalettes() {\n const modelName = this.elements.modelSelect.value\n const model = this.models.find(m => m.name === modelName)\n\n if (!model) return\n\n // Clear existing options\n this.elements.paletteSelect.innerHTML = ''\n\n // Add options for each palette_id in the model (only if framework_class is not empty)\n model.palette_ids.forEach(paletteId => {\n const palette = this.palettes.find(p => p.id === paletteId)\n if (palette && palette.framework_class && palette.framework_class.trim() !== '') {\n const option = document.createElement('option')\n option.value = palette.id\n option.textContent = palette.name\n this.elements.paletteSelect.appendChild(option)\n }\n })\n }\n\n /**\n * Emit 'trmnl:change' event with current state and screen classes\n * @private\n * @param {string} origin - Source of the change ('constructor', 'form', 'setParams')\n * @fires TRMNLPicker#trmnl:change\n */\n _emitChangeEvent(origin) {\n // Save to localStorage if key is configured\n this._saveToLocalStorage()\n\n const event = new CustomEvent('trmnl:change', {\n detail: {\n origin,\n ...this.state,\n },\n bubbles: true\n })\n\n this.formElement.dispatchEvent(event)\n }\n\n /**\n * Load state from localStorage\n * @private\n * @returns {Object|null} Saved state or null if not available\n */\n _loadFromLocalStorage() {\n if (!this.localStorageKey) return null\n\n try {\n const saved = localStorage.getItem(this.localStorageKey)\n if (saved) {\n return JSON.parse(saved)\n }\n } catch (error) {\n console.warn('TRMNLPicker: Failed to load from localStorage:', error)\n }\n\n return null\n }\n\n /**\n * Save current state to localStorage\n * @private\n */\n _saveToLocalStorage() {\n if (!this.localStorageKey) return\n\n try {\n localStorage.setItem(this.localStorageKey, JSON.stringify(this.params))\n } catch (error) {\n console.warn('TRMNLPicker: Failed to save to localStorage:', error)\n }\n }\n\n /**\n * Handle model selection change\n * @private\n */\n _handleModelChange(event) {\n this._setParams('form', { modelName: event.target.value })\n }\n\n /**\n * Handle palette selection change\n * @private\n */\n _handlePaletteChange(event) {\n this._setParams('form', { paletteId: event.target.value })\n }\n\n /**\n * Toggle orientation between portrait and landscape\n * @private\n */\n _toggleOrientation() {\n this._setParams('form', { isPortrait: !this._state.isPortrait })\n }\n\n /**\n * Toggle dark mode on/off\n * @private\n */\n _toggleDarkMode() {\n this._setParams('form', { isDarkMode: !this._state.isDarkMode })\n }\n\n /**\n * Reset to defaults: first valid palette, landscape orientation, light mode\n * @private\n */\n _resetToModelDefaults() {\n const model = this._state.model\n if (!model) return\n\n // Reset to first valid palette of current model\n const firstPaletteId = this._getFirstValidPaletteId(model)\n\n this._setParams('form', {\n paletteId: firstPaletteId,\n isPortrait: false,\n isDarkMode: false\n })\n }\n\n /**\n * Update reset button enabled/disabled state\n * Button is disabled only when palette, orientation, and dark mode are all at defaults\n * @private\n */\n _updateResetButton() {\n if (!this.elements.resetButton) return\n\n const model = this._state.model\n if (!model) return\n\n const firstValidPaletteId = this._getFirstValidPaletteId(model)\n const isPaletteDefault = this.elements.paletteSelect.value === String(firstValidPaletteId)\n const isOrientationDefault = this._state.isPortrait === false\n const isDarkModeDefault = this._state.isDarkMode === false\n\n const isAtDefaults = isPaletteDefault && isOrientationDefault && isDarkModeDefault\n\n this.elements.resetButton.disabled = isAtDefaults\n\n if (isAtDefaults) {\n this.elements.resetButton.classList.add('opacity-50', 'cursor-default')\n this.elements.resetButton.setAttribute('aria-disabled', 'true')\n } else {\n this.elements.resetButton.classList.remove('opacity-50', 'cursor-default')\n this.elements.resetButton.removeAttribute('aria-disabled')\n }\n }\n\n /**\n * Get CSS classes for the current picker configuration\n * @private\n * @returns {Array} Array of CSS class names for Framework CSS rendering\n *\n * Generated classes (in order):\n * 1. 'screen' - Base class (always present)\n * 2. palette.framework_class - From selected palette (e.g., 'screen--1bit')\n * 3. model.css.classes.device - From model API (e.g., 'screen--v2')\n * 4. model.css.classes.size - From model API (e.g., 'screen--md')\n * 5. 'screen--portrait' - Only when portrait orientation is enabled\n * 6. 'screen--1x' - Scale indicator (always 1x)\n * 7. 'screen--dark-mode' - Only when dark mode is enabled\n *\n * @example\n * const classes = picker.screenClasses\n * // ['screen', 'screen--1bit', 'screen--v2', 'screen--md', 'screen--1x']\n */\n get _screenClasses() {\n const model = this._state.model\n const palette = this._state.palette\n\n if (!model) {\n throw new Error('No model selected')\n }\n\n const classes = []\n\n // 0. Base screen class (always present)\n classes.push('screen')\n\n // 1. Palette framework class\n if (palette && palette.framework_class) {\n classes.push(palette.framework_class)\n }\n\n // 2. Model device class (from API)\n if (model.css && model.css.classes && model.css.classes.device) {\n classes.push(model.css.classes.device)\n }\n\n // 3. Model size class (from API)\n if (model.css && model.css.classes && model.css.classes.size) {\n classes.push(model.css.classes.size)\n }\n\n // 4. Orientation (UI state, only portrait - landscape is default)\n if (this._state.isPortrait) {\n classes.push('screen--portrait')\n }\n\n // 5. Scale (UI state, always 1x)\n classes.push('screen--1x')\n\n // 6. Dark mode (UI state, conditional)\n if (this._state.isDarkMode) {\n classes.push('screen--dark-mode')\n }\n\n return classes\n }\n\n /**\n * Get current picker parameters (serializable state)\n * @public\n * @returns {Object} Current parameters for persistence or API calls\n * @returns {string} return.modelName - Selected model name\n * @returns {string} return.paletteId - Selected palette ID\n * @returns {boolean} return.isPortrait - Portrait orientation flag\n * @returns {boolean} return.isDarkMode - Dark mode flag\n *\n * @example\n * const params = picker.params\n * // { modelName: 'og_plus', paletteId: '123', isPortrait: false, isDarkMode: false }\n *\n * // Can be used to restore state later\n * localStorage.setItem('picker-state', JSON.stringify(picker.params))\n */\n get params() {\n return {\n modelName: this._state.model?.name,\n paletteId: this._state.palette?.id,\n isPortrait: this._state.isPortrait,\n isDarkMode: this._state.isDarkMode\n }\n }\n\n /**\n * Update picker configuration programmatically\n * @public\n * @param {Object} params - Configuration object (all fields optional)\n * @param {string} [params.modelName] - Model name to select\n * @param {string} [params.paletteId] - Palette ID to select\n * @param {boolean} [params.isPortrait] - Portrait orientation\n * @param {boolean} [params.isDarkMode] - Dark mode enabled\n * @fires TRMNLPicker#trmnl:change\n * @throws {Error} If params is not an object\n *\n * @example\n * // Update single parameter\n * picker.setParams({ isDarkMode: true })\n *\n * // Update multiple parameters\n * picker.setParams({\n * modelName: 'og_plus',\n * paletteId: '123',\n * isPortrait: true\n * })\n *\n * // Note: Changing model resets palette to first valid palette of that model\n */\n setParams(params) {\n this._setParams('setParams', params)\n }\n\n /**\n * Internal method to update picker state with origin tracking\n * @private\n * @param {string} origin - Source of change ('constructor', 'form', 'setParams')\n * @param {Object} params - Parameters to update\n * @returns {boolean} True if any changes were made\n */\n _setParams(origin, params) {\n if (!params || typeof params !== 'object') {\n throw new Error('params must be an object')\n }\n\n let changed = false\n\n // Update model if provided\n if (params.modelName) {\n const model = this.models.find(m => m.name === params.modelName)\n if (model) {\n this.elements.modelSelect.value = model.name\n this._state.model = model\n this._populateModelPalettes()\n\n // Select first valid palette of new model\n const firstPaletteId = this._getFirstValidPaletteId(model)\n this.elements.paletteSelect.value = firstPaletteId\n this._state.palette = this.palettes.find(p => p.id === firstPaletteId)\n changed = true\n }\n }\n\n // Update palette if provided\n if (params.paletteId) {\n const palette = this.palettes.find(p => p.id === params.paletteId)\n if (palette) {\n this.elements.paletteSelect.value = palette.id\n this._state.palette = palette\n changed = true\n }\n }\n\n // Update orientation if provided\n if (typeof params.isPortrait === 'boolean') {\n this._state.isPortrait = params.isPortrait\n if (this.elements.orientationText) {\n this.elements.orientationText.textContent = this._state.isPortrait ? 'Portrait' : 'Landscape'\n }\n changed = true\n }\n\n // Update dark mode if provided\n if (typeof params.isDarkMode === 'boolean') {\n this._state.isDarkMode = params.isDarkMode\n if (this.elements.darkModeText) {\n this.elements.darkModeText.textContent = this._state.isDarkMode ? 'Dark Mode' : 'Light Mode'\n }\n changed = true\n }\n\n if (changed) {\n this._updateResetButton()\n }\n\n if (changed && origin) {\n this._emitChangeEvent(origin)\n }\n\n return changed\n }\n\n /**\n * Get complete picker state including full model and palette objects\n * @public\n * @returns {{\n * model: Object,\n * palette: Object,\n * isPortrait: boolean,\n * isDarkMode: boolean,\n * screenClasses: Array,\n * width: number,\n * height: number\n * }} State object containing model (full model object from API), palette (full palette object from API), isPortrait flag, and isDarkMode flag\n *\n * @example\n * const state = picker.state\n * // {\n * // model: { name: 'og_plus', label: 'OG+', width: 800, height: 480, ... },\n * // palette: { id: '123', name: 'Black', framework_class: 'screen--1bit', ... },\n * // isPortrait: false,\n * // isDarkMode: false,\n * // screenClasses: ['screen', 'screen--1bit', 'screen--v2', 'screen--md', 'screen--1x'],\n * // width: 800,\n * // height: 480\n * // }\n */\n get state() {\n return {\n screenClasses: this._screenClasses,\n ...this._dimensions,\n ...this._state,\n };\n }\n\n /**\n * Get current dimensions (width and height) of the screen in pixels\n * @private\n * @returns {{ width: number, height: number }} Object with width and height properties\n */\n get _dimensions() {\n const model = this._state.model\n\n let width = model.width / model.scale_factor\n let height = model.height / model.scale_factor\n\n if (this._state.isPortrait) {\n [width, height] = [height, width]\n }\n\n return { width, height }\n }\n\n /**\n * Clean up event listeners and references\n * @public\n */\n destroy() {\n // Remove event listeners\n this.elements.modelSelect.removeEventListener('change', this.handlers.modelChange)\n this.elements.paletteSelect.removeEventListener('change', this.handlers.paletteChange)\n\n if (this.elements.orientationToggle) {\n this.elements.orientationToggle.removeEventListener('click', this.handlers.orientationToggle)\n }\n\n if (this.elements.darkModeToggle) {\n this.elements.darkModeToggle.removeEventListener('click', this.handlers.darkModeToggle)\n }\n\n if (this.elements.resetButton) {\n this.elements.resetButton.removeEventListener('click', this.handlers.reset)\n }\n\n // Clear references\n this.formElement = null\n this.elements = null\n this.handlers = null\n this.models = null\n this.palettes = null\n this._state = null\n }\n}\n\nexport default TRMNLPicker\n"], + "sourcesContent": ["/**\n * Default model to select when no localStorage state is found\n * @private\n * @constant {string}\n */\nconst _DEFAULT_MODEL_NAME = 'og_plus'\n\n/**\n * localStorage key for caching API responses (models and palettes)\n * @private\n * @constant {string}\n */\nconst _API_CACHE_KEY = 'trmnl-picker-api-cache'\n\n/**\n * Cache TTL in milliseconds (1 day)\n * @private\n * @constant {number}\n */\nconst _CACHE_TTL_MS = 24 * 60 * 60 * 1000\n\n/**\n * Event fired when picker state changes\n * @event TRMNLPicker#trmnl:change\n * @type {CustomEvent}\n * @property {Object} detail - Event details\n * @property {string} detail.origin - What triggered the change: 'constructor', 'form', or 'setParams'\n * @property {Array} detail.screenClasses - Array of CSS classes for Framework CSS rendering\n * @property {Object} detail.model - Current model object with name, label, width, height, kind, css properties\n * @property {Object} detail.palette - Current palette object with id, name, framework_class properties\n * @property {boolean} detail.isPortrait - Portrait orientation flag\n * @property {boolean} detail.isDarkMode - Dark mode flag\n * @property {number} detail.width - Current width of the screen in pixels\n * @property {number} detail.height - Current height of the screen in pixels\n *\n * @example\n * picker.formElement.addEventListener('trmnl:change', (event) => {\n * const { origin, screenClasses, model, palette } = event.detail\n * console.log(`Changed via ${origin}`)\n * console.log('Classes:', screenClasses)\n * })\n */\n\n/**\n * TRMNLPicker - Vanilla JS library for TRMNL device and palette selection\n *\n * Provides a reactive picker component that manages device models, color palettes,\n * orientation, and display mode. Emits 'trmnl:change' events with current state\n * and CSS classes for rendering.\n * \n * **[View a live demo \u2192](https://usetrmnl.github.io/trmnl-picker/example/)**\n *\n * **Note:** Using the constructor directly is not recommended. Use the static\n * {@link TRMNLPicker.create} method instead, which automatically fetches\n * models and palettes from the TRMNL API if not provided.\n *\n * @class TRMNLPicker\n * @param {string|Element} formIdOrElement - Form element ID or DOM element reference\n * @param {Object} options - Configuration options\n * @param {Array} options.models - Array of model objects from TRMNL API\n * @param {Array} options.palettes - Array of palette objects from TRMNL API\n * @param {string} [options.localStorageKey] - Optional key for persisting state to localStorage\n *\n * @fires TRMNLPicker#trmnl:change\n *\n * @example\n * // HTML Structure - Required form with data-* attributes\n * //
\n * // \n * // \n * //\n * // \n * // \n * //\n * // \n * // \n * //\n * // \n * // \n * //\n * // \n * // \n * //
\n *\n * // Create with element ID\n * const picker = new TRMNLPicker('picker-form', { models, palettes })\n *\n * // Create with DOM element\n * const element = document.getElementById('picker-form')\n * const picker = new TRMNLPicker(element, { models, palettes })\n *\n * // Listen for changes\n * picker.formElement.addEventListener('trmnl:change', (event) => {\n * console.log(event.detail.screenClasses)\n * })\n */\nclass TRMNLPicker {\n static API_BASE_URL = 'https://trmnl.com'\n\n /**\n * Get cached API response from localStorage\n * @private\n * @static\n * @returns {{models: Array, palettes: Array} | null} Cached data or null if expired/missing\n */\n static _getCachedApiData() {\n try {\n const cached = localStorage.getItem(_API_CACHE_KEY)\n if (!cached) return null\n\n const { timestamp, models, palettes } = JSON.parse(cached)\n const now = Date.now()\n\n // Check if cache is still valid\n if (now - timestamp > _CACHE_TTL_MS) {\n localStorage.removeItem(_API_CACHE_KEY)\n return null\n }\n\n return { models, palettes }\n } catch (error) {\n console.warn('TRMNLPicker: Failed to read API cache:', error)\n return null\n }\n }\n\n /**\n * Save API response to localStorage cache\n * @private\n * @static\n * @param {Array} models - Models array\n * @param {Array} palettes - Palettes array\n */\n static _setCachedApiData(models, palettes) {\n try {\n const cacheData = {\n timestamp: Date.now(),\n models,\n palettes\n }\n localStorage.setItem(_API_CACHE_KEY, JSON.stringify(cacheData))\n } catch (error) {\n console.warn('TRMNLPicker: Failed to save API cache:', error)\n }\n }\n\n /**\n * Create a TRMNLPicker instance, fetching data from TRMNL API if not provided\n *\n * Automatically caches API responses in localStorage for 24 hours to reduce network requests.\n *\n * @static\n * @param {string|Element} formIdOrElement - Form element ID or DOM element\n * @param {Object} options - Configuration options\n * @param {Array} [options.models] - Optional models array (fetched from API if not provided)\n * @param {Array} [options.palettes] - Optional palettes array (fetched from API if not provided)\n * @param {string} [options.localStorageKey] - Optional key for state persistence\n * @returns {Promise} Promise resolving to picker instance\n * @throws {Error} If API fetch fails when models or palettes are not provided\n *\n * @example\n * // Fetch models and palettes from API (or use cached data if available)\n * const picker = await TRMNLPicker.create('screen-picker')\n *\n * // Provide your own data\n * const picker = await TRMNLPicker.create('screen-picker', { models, palettes })\n */\n static async create(formId, options = {}) {\n let { models, palettes, localStorageKey } = options\n\n // Try to use cached data if neither models nor palettes are provided\n if (!models && !palettes) {\n const cached = TRMNLPicker._getCachedApiData()\n if (cached) {\n models = cached.models\n palettes = cached.palettes\n }\n }\n\n // Fetch models if not provided and not in cache\n if (!models) {\n try {\n const response = await fetch(`${TRMNLPicker.API_BASE_URL}/api/models`)\n if (!response.ok) {\n throw new Error(`Failed to fetch models: ${response.status} ${response.statusText}`)\n }\n const data = await response.json()\n models = data.data || data\n } catch (error) {\n throw new Error(`TRMNLPicker: Failed to fetch models from API: ${error.message}`)\n }\n }\n\n // Fetch palettes if not provided and not in cache\n if (!palettes) {\n try {\n const response = await fetch(`${TRMNLPicker.API_BASE_URL}/api/palettes`)\n if (!response.ok) {\n throw new Error(`Failed to fetch palettes: ${response.status} ${response.statusText}`)\n }\n const data = await response.json()\n palettes = data.data || data\n } catch (error) {\n throw new Error(`TRMNLPicker: Failed to fetch palettes from API: ${error.message}`)\n }\n }\n\n // Cache the API data for future use (if both were fetched)\n if (!options.models && !options.palettes) {\n TRMNLPicker._setCachedApiData(models, palettes)\n }\n\n return new TRMNLPicker(formId, { models, palettes, localStorageKey })\n }\n\n constructor(formIdOrElement, options = {}) {\n // Validate inputs\n if (!formIdOrElement) {\n throw new Error('TRMNLPicker: formIdOrElement is required')\n }\n\n // Store references - accept either string ID or DOM element\n if (typeof formIdOrElement === 'string') {\n this.formElement = document.getElementById(formIdOrElement)\n if (!this.formElement) {\n throw new Error(`TRMNLPicker: Form element with id \"${formIdOrElement}\" not found`)\n }\n } else if (formIdOrElement instanceof Element) {\n this.formElement = formIdOrElement\n } else {\n throw new Error('TRMNLPicker: formIdOrElement must be a string ID or DOM element')\n }\n\n const { models, palettes, localStorageKey } = options\n\n this.models = models\n this.palettes = palettes\n this.localStorageKey = localStorageKey\n\n // Only initialize if we have data\n if (this.models && this.palettes) {\n if (!Array.isArray(this.models) || this.models.length === 0) {\n throw new Error('TRMNLPicker: models must be a non-empty array')\n }\n\n if (!Array.isArray(this.palettes) || this.palettes.length === 0) {\n throw new Error('TRMNLPicker: palettes must be a non-empty array')\n }\n\n // Filter out models where all palettes have empty framework_class\n this.models = this._filterValidModels()\n\n if (this.models.length === 0) {\n throw new Error('TRMNLPicker: no valid models found (all models have palettes with empty framework_class)')\n }\n\n // Initialize DOM elements and bind events\n this._initializeElements()\n this._bindEvents()\n\n // Set initial state (will load from localStorage if available)\n this._setInitialState()\n }\n }\n\n /**\n * Filter out models where all their palettes have empty framework_class\n * @private\n * @returns {Array} Filtered models array\n */\n _filterValidModels() {\n return this.models.filter(model => {\n // Check if at least one palette for this model has a non-empty framework_class\n return model.palette_ids.some(paletteId => {\n const palette = this.palettes.find(p => p.id === paletteId)\n return palette && palette.framework_class && palette.framework_class.trim() !== ''\n })\n })\n }\n\n /**\n * Get the first valid palette ID for a model (one with non-empty framework_class)\n * @private\n * @param {Object} model - Model object\n * @returns {string|null} First valid palette ID or null\n */\n _getFirstValidPaletteId(model) {\n if (!model) return null\n\n for (const paletteId of model.palette_ids) {\n const palette = this.palettes.find(p => p.id === paletteId)\n if (palette && palette.framework_class && palette.framework_class.trim() !== '') {\n return paletteId\n }\n }\n\n return null\n }\n\n /**\n * Find and store references to form elements using data-* attributes\n * @private\n */\n _initializeElements() {\n this.elements = {\n modelSelect: this.formElement.querySelector('[data-model-select]'),\n paletteSelect: this.formElement.querySelector('[data-palette-select]'),\n orientationToggle: this.formElement.querySelector('[data-orientation-toggle]'),\n darkModeToggle: this.formElement.querySelector('[data-dark-mode-toggle]'),\n resetButton: this.formElement.querySelector('[data-reset-button]'),\n\n // Optional: UI indicator elements\n orientationText: this.formElement.querySelector('[data-orientation-text]'),\n darkModeText: this.formElement.querySelector('[data-dark-mode-text]')\n }\n\n // Validate required elements\n const required = ['modelSelect', 'paletteSelect']\n for (const key of required) {\n if (!this.elements[key]) {\n throw new Error(`TRMNLPicker: Required element \"${key}\" not found in form`)\n }\n }\n }\n\n /**\n * Bind event listeners to form elements\n * @private\n */\n _bindEvents() {\n // Store bound handlers for cleanup\n this.handlers = {\n modelChange: this._handleModelChange.bind(this),\n paletteChange: this._handlePaletteChange.bind(this),\n orientationToggle: this._toggleOrientation.bind(this),\n darkModeToggle: this._toggleDarkMode.bind(this),\n reset: this._resetToModelDefaults.bind(this)\n }\n\n // Attach event listeners\n this.elements.modelSelect.addEventListener('change', this.handlers.modelChange)\n this.elements.paletteSelect.addEventListener('change', this.handlers.paletteChange)\n\n if (this.elements.orientationToggle) {\n this.elements.orientationToggle.addEventListener('click', this.handlers.orientationToggle)\n }\n\n if (this.elements.darkModeToggle) {\n this.elements.darkModeToggle.addEventListener('click', this.handlers.darkModeToggle)\n }\n\n if (this.elements.resetButton) {\n this.elements.resetButton.addEventListener('click', this.handlers.reset)\n }\n }\n\n /**\n * Set initial state and populate form\n * @private\n */\n _setInitialState() {\n // Separate models into TRMNL and BYOD groups\n const trmnlModels = this.models.filter(m => m.kind === 'trmnl')\n const byodModels = this.models.filter(m => m.kind !== 'trmnl')\n\n // Sort each group alphabetically by label\n const sortTRMNL = [...trmnlModels].sort((a, b) => {\n const labelA = (a.label || a.name).toLowerCase()\n const labelB = (b.label || b.name).toLowerCase()\n return labelA.localeCompare(labelB)\n })\n\n const sortBYOD = [...byodModels].sort((a, b) => {\n const labelA = (a.label || a.name).toLowerCase()\n const labelB = (b.label || b.name).toLowerCase()\n return labelA.localeCompare(labelB)\n })\n\n // Populate model select with grouped models\n this.elements.modelSelect.innerHTML = ''\n\n // Add TRMNL group\n if (sortTRMNL.length > 0) {\n const trmnlGroup = document.createElement('optgroup')\n trmnlGroup.label = 'TRMNL'\n sortTRMNL.forEach(model => {\n const option = document.createElement('option')\n option.value = model.name\n option.textContent = model.label || model.name\n trmnlGroup.appendChild(option)\n })\n this.elements.modelSelect.appendChild(trmnlGroup)\n }\n\n // Add BYOD group\n if (sortBYOD.length > 0) {\n const byodGroup = document.createElement('optgroup')\n byodGroup.label = 'BYOD'\n sortBYOD.forEach(model => {\n const option = document.createElement('option')\n option.value = model.name\n option.textContent = model.label || model.name\n byodGroup.appendChild(option)\n })\n this.elements.modelSelect.appendChild(byodGroup)\n }\n\n // Keep combined sorted list for default selection logic\n const sortedModels = [...sortTRMNL, ...sortBYOD]\n\n // Initialize state\n this._state = {}\n\n const defaultModel = sortedModels.find(m => m.name === _DEFAULT_MODEL_NAME) || sortedModels[0]\n const defaultPaletteId = this._getFirstValidPaletteId(defaultModel)\n const defaultParams = {\n modelName: defaultModel.name,\n paletteId: defaultPaletteId,\n isPortrait: false,\n isDarkMode: false\n }\n\n const savedParams = this._loadFromLocalStorage() || {}\n\n this._setParams('constructor', { ...defaultParams, ...savedParams })\n }\n\n /**\n * Populate palette dropdown based on selected model\n * @private\n */\n _populateModelPalettes() {\n const modelName = this.elements.modelSelect.value\n const model = this.models.find(m => m.name === modelName)\n\n if (!model) return\n\n // Clear existing options\n this.elements.paletteSelect.innerHTML = ''\n\n // Add options for each palette_id in the model (only if framework_class is not empty)\n model.palette_ids.forEach(paletteId => {\n const palette = this.palettes.find(p => p.id === paletteId)\n if (palette && palette.framework_class && palette.framework_class.trim() !== '') {\n const option = document.createElement('option')\n option.value = palette.id\n option.textContent = palette.name\n this.elements.paletteSelect.appendChild(option)\n }\n })\n }\n\n /**\n * Emit 'trmnl:change' event with current state and screen classes\n * @private\n * @param {string} origin - Source of the change ('constructor', 'form', 'setParams')\n * @fires TRMNLPicker#trmnl:change\n */\n _emitChangeEvent(origin) {\n // Save to localStorage if key is configured\n this._saveToLocalStorage()\n\n const event = new CustomEvent('trmnl:change', {\n detail: {\n origin,\n ...this.state,\n },\n bubbles: true\n })\n\n this.formElement.dispatchEvent(event)\n }\n\n /**\n * Load state from localStorage\n * @private\n * @returns {Object|null} Saved state or null if not available\n */\n _loadFromLocalStorage() {\n if (!this.localStorageKey) return null\n\n try {\n const saved = localStorage.getItem(this.localStorageKey)\n if (saved) {\n return JSON.parse(saved)\n }\n } catch (error) {\n console.warn('TRMNLPicker: Failed to load from localStorage:', error)\n }\n\n return null\n }\n\n /**\n * Save current state to localStorage\n * @private\n */\n _saveToLocalStorage() {\n if (!this.localStorageKey) return\n\n try {\n localStorage.setItem(this.localStorageKey, JSON.stringify(this.params))\n } catch (error) {\n console.warn('TRMNLPicker: Failed to save to localStorage:', error)\n }\n }\n\n /**\n * Handle model selection change\n * @private\n */\n _handleModelChange(event) {\n this._setParams('form', { modelName: event.target.value })\n }\n\n /**\n * Handle palette selection change\n * @private\n */\n _handlePaletteChange(event) {\n this._setParams('form', { paletteId: event.target.value })\n }\n\n /**\n * Toggle orientation between portrait and landscape\n * @private\n */\n _toggleOrientation() {\n this._setParams('form', { isPortrait: !this._state.isPortrait })\n }\n\n /**\n * Toggle dark mode on/off\n * @private\n */\n _toggleDarkMode() {\n this._setParams('form', { isDarkMode: !this._state.isDarkMode })\n }\n\n /**\n * Reset to defaults: first valid palette, landscape orientation, light mode\n * @private\n */\n _resetToModelDefaults() {\n const model = this._state.model\n if (!model) return\n\n // Reset to first valid palette of current model\n const firstPaletteId = this._getFirstValidPaletteId(model)\n\n this._setParams('form', {\n paletteId: firstPaletteId,\n isPortrait: false,\n isDarkMode: false\n })\n }\n\n /**\n * Update reset button enabled/disabled state\n * Button is disabled only when palette, orientation, and dark mode are all at defaults\n * @private\n */\n _updateResetButton() {\n if (!this.elements.resetButton) return\n\n const model = this._state.model\n if (!model) return\n\n const firstValidPaletteId = this._getFirstValidPaletteId(model)\n const isPaletteDefault = this.elements.paletteSelect.value === String(firstValidPaletteId)\n const isOrientationDefault = this._state.isPortrait === false\n const isDarkModeDefault = this._state.isDarkMode === false\n\n const isAtDefaults = isPaletteDefault && isOrientationDefault && isDarkModeDefault\n\n this.elements.resetButton.disabled = isAtDefaults\n\n if (isAtDefaults) {\n this.elements.resetButton.classList.add('opacity-50', 'cursor-default')\n this.elements.resetButton.setAttribute('aria-disabled', 'true')\n } else {\n this.elements.resetButton.classList.remove('opacity-50', 'cursor-default')\n this.elements.resetButton.removeAttribute('aria-disabled')\n }\n }\n\n /**\n * Get CSS classes for the current picker configuration\n * @private\n * @returns {Array} Array of CSS class names for Framework CSS rendering\n *\n * Generated classes (in order):\n * 1. 'screen' - Base class (always present)\n * 2. palette.framework_class - From selected palette (e.g., 'screen--1bit')\n * 3. model.css.classes.device - From model API (e.g., 'screen--v2')\n * 4. model.css.classes.size - From model API (e.g., 'screen--md')\n * 5. 'screen--portrait' - Only when portrait orientation is enabled\n * 6. 'screen--1x' - Scale indicator (always 1x)\n * 7. 'screen--dark-mode' - Only when dark mode is enabled\n *\n * @example\n * const classes = picker.screenClasses\n * // ['screen', 'screen--1bit', 'screen--v2', 'screen--md', 'screen--1x']\n */\n get _screenClasses() {\n const model = this._state.model\n const palette = this._state.palette\n\n if (!model) {\n throw new Error('No model selected')\n }\n\n const classes = []\n\n // 0. Base screen class (always present)\n classes.push('screen')\n\n // 1. Palette framework class\n if (palette && palette.framework_class) {\n classes.push(palette.framework_class)\n }\n\n // 2. Model device class (from API)\n if (model.css && model.css.classes && model.css.classes.device) {\n classes.push(model.css.classes.device)\n }\n\n // 3. Model size class (from API)\n if (model.css && model.css.classes && model.css.classes.size) {\n classes.push(model.css.classes.size)\n }\n\n // 4. Orientation (UI state, only portrait - landscape is default)\n if (this._state.isPortrait) {\n classes.push('screen--portrait')\n }\n\n // 5. Scale (UI state, always 1x)\n classes.push('screen--1x')\n\n // 6. Dark mode (UI state, conditional)\n if (this._state.isDarkMode) {\n classes.push('screen--dark-mode')\n }\n\n return classes\n }\n\n /**\n * Get current picker parameters (serializable state)\n * @public\n * @returns {Object} Current parameters for persistence or API calls\n * @returns {string} return.modelName - Selected model name\n * @returns {string} return.paletteId - Selected palette ID\n * @returns {boolean} return.isPortrait - Portrait orientation flag\n * @returns {boolean} return.isDarkMode - Dark mode flag\n *\n * @example\n * const params = picker.params\n * // { modelName: 'og_plus', paletteId: '123', isPortrait: false, isDarkMode: false }\n *\n * // Can be used to restore state later\n * localStorage.setItem('picker-state', JSON.stringify(picker.params))\n */\n get params() {\n return {\n modelName: this._state.model?.name,\n paletteId: this._state.palette?.id,\n isPortrait: this._state.isPortrait,\n isDarkMode: this._state.isDarkMode\n }\n }\n\n /**\n * Update picker configuration programmatically\n * @public\n * @param {Object} params - Configuration object (all fields optional)\n * @param {string} [params.modelName] - Model name to select\n * @param {string} [params.paletteId] - Palette ID to select\n * @param {boolean} [params.isPortrait] - Portrait orientation\n * @param {boolean} [params.isDarkMode] - Dark mode enabled\n * @fires TRMNLPicker#trmnl:change\n * @throws {Error} If params is not an object\n *\n * @example\n * // Update single parameter\n * picker.setParams({ isDarkMode: true })\n *\n * // Update multiple parameters\n * picker.setParams({\n * modelName: 'og_plus',\n * paletteId: '123',\n * isPortrait: true\n * })\n *\n * // Note: Changing model resets palette to first valid palette of that model\n */\n setParams(params) {\n this._setParams('setParams', params)\n }\n\n /**\n * Internal method to update picker state with origin tracking\n * @private\n * @param {string} origin - Source of change ('constructor', 'form', 'setParams')\n * @param {Object} params - Parameters to update\n * @returns {boolean} True if any changes were made\n */\n _setParams(origin, params) {\n if (!params || typeof params !== 'object') {\n throw new Error('params must be an object')\n }\n\n let changed = false\n\n // Update model if provided\n if (params.modelName) {\n const model = this.models.find(m => m.name === params.modelName)\n if (model) {\n this.elements.modelSelect.value = model.name\n this._state.model = model\n this._populateModelPalettes()\n\n // Select first valid palette of new model\n const firstPaletteId = this._getFirstValidPaletteId(model)\n this.elements.paletteSelect.value = firstPaletteId\n this._state.palette = this.palettes.find(p => p.id === firstPaletteId)\n changed = true\n }\n }\n\n // Update palette if provided\n if (params.paletteId) {\n const palette = this.palettes.find(p => p.id === params.paletteId)\n if (palette) {\n this.elements.paletteSelect.value = palette.id\n this._state.palette = palette\n changed = true\n }\n }\n\n // Update orientation if provided\n if (typeof params.isPortrait === 'boolean') {\n this._state.isPortrait = params.isPortrait\n if (this.elements.orientationText) {\n this.elements.orientationText.textContent = this._state.isPortrait ? 'Portrait' : 'Landscape'\n }\n changed = true\n }\n\n // Update dark mode if provided\n if (typeof params.isDarkMode === 'boolean') {\n this._state.isDarkMode = params.isDarkMode\n if (this.elements.darkModeText) {\n this.elements.darkModeText.textContent = this._state.isDarkMode ? 'Dark Mode' : 'Light Mode'\n }\n changed = true\n }\n\n if (changed) {\n this._updateResetButton()\n }\n\n if (changed && origin) {\n this._emitChangeEvent(origin)\n }\n\n return changed\n }\n\n /**\n * Get complete picker state including full model and palette objects\n * @public\n * @returns {{\n * model: Object,\n * palette: Object,\n * isPortrait: boolean,\n * isDarkMode: boolean,\n * screenClasses: Array,\n * width: number,\n * height: number\n * }} State object containing model (full model object from API), palette (full palette object from API), isPortrait flag, and isDarkMode flag\n *\n * @example\n * const state = picker.state\n * // {\n * // model: { name: 'og_plus', label: 'OG+', width: 800, height: 480, ... },\n * // palette: { id: '123', name: 'Black', framework_class: 'screen--1bit', ... },\n * // isPortrait: false,\n * // isDarkMode: false,\n * // screenClasses: ['screen', 'screen--1bit', 'screen--v2', 'screen--md', 'screen--1x'],\n * // width: 800,\n * // height: 480\n * // }\n */\n get state() {\n return {\n screenClasses: this._screenClasses,\n ...this._dimensions,\n ...this._state,\n };\n }\n\n /**\n * Get current dimensions (width and height) of the screen in pixels\n * @private\n * @returns {{ width: number, height: number }} Object with width and height properties\n */\n get _dimensions() {\n const model = this._state.model\n\n let width = model.width / model.scale_factor\n let height = model.height / model.scale_factor\n\n if (this._state.isPortrait) {\n [width, height] = [height, width]\n }\n\n return { width, height }\n }\n\n /**\n * Clean up event listeners and references\n * @public\n */\n destroy() {\n // Remove event listeners\n this.elements.modelSelect.removeEventListener('change', this.handlers.modelChange)\n this.elements.paletteSelect.removeEventListener('change', this.handlers.paletteChange)\n\n if (this.elements.orientationToggle) {\n this.elements.orientationToggle.removeEventListener('click', this.handlers.orientationToggle)\n }\n\n if (this.elements.darkModeToggle) {\n this.elements.darkModeToggle.removeEventListener('click', this.handlers.darkModeToggle)\n }\n\n if (this.elements.resetButton) {\n this.elements.resetButton.removeEventListener('click', this.handlers.reset)\n }\n\n // Clear references\n this.formElement = null\n this.elements = null\n this.handlers = null\n this.models = null\n this.palettes = null\n this._state = null\n }\n}\n\nexport default TRMNLPicker\n"], "mappings": "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", "names": [] } diff --git a/dist/trmnl-picker.js b/dist/trmnl-picker.js index 25d90f1..f094843 100644 --- a/dist/trmnl-picker.js +++ b/dist/trmnl-picker.js @@ -26,7 +26,7 @@ var TRMNLPicker = (() => { var _API_CACHE_KEY = "trmnl-picker-api-cache"; var _CACHE_TTL_MS = 24 * 60 * 60 * 1e3; var TRMNLPicker = class _TRMNLPicker { - static API_BASE_URL = "https://usetrmnl.com"; + static API_BASE_URL = "https://trmnl.com"; /** * Get cached API response from localStorage * @private diff --git a/dist/trmnl-picker.js.map b/dist/trmnl-picker.js.map index 0db3118..b278d02 100644 --- a/dist/trmnl-picker.js.map +++ b/dist/trmnl-picker.js.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["../src/index.js"], - "sourcesContent": ["/**\n * Default model to select when no localStorage state is found\n * @private\n * @constant {string}\n */\nconst _DEFAULT_MODEL_NAME = 'og_plus'\n\n/**\n * localStorage key for caching API responses (models and palettes)\n * @private\n * @constant {string}\n */\nconst _API_CACHE_KEY = 'trmnl-picker-api-cache'\n\n/**\n * Cache TTL in milliseconds (1 day)\n * @private\n * @constant {number}\n */\nconst _CACHE_TTL_MS = 24 * 60 * 60 * 1000\n\n/**\n * Event fired when picker state changes\n * @event TRMNLPicker#trmnl:change\n * @type {CustomEvent}\n * @property {Object} detail - Event details\n * @property {string} detail.origin - What triggered the change: 'constructor', 'form', or 'setParams'\n * @property {Array} detail.screenClasses - Array of CSS classes for Framework CSS rendering\n * @property {Object} detail.model - Current model object with name, label, width, height, kind, css properties\n * @property {Object} detail.palette - Current palette object with id, name, framework_class properties\n * @property {boolean} detail.isPortrait - Portrait orientation flag\n * @property {boolean} detail.isDarkMode - Dark mode flag\n * @property {number} detail.width - Current width of the screen in pixels\n * @property {number} detail.height - Current height of the screen in pixels\n *\n * @example\n * picker.formElement.addEventListener('trmnl:change', (event) => {\n * const { origin, screenClasses, model, palette } = event.detail\n * console.log(`Changed via ${origin}`)\n * console.log('Classes:', screenClasses)\n * })\n */\n\n/**\n * TRMNLPicker - Vanilla JS library for TRMNL device and palette selection\n *\n * Provides a reactive picker component that manages device models, color palettes,\n * orientation, and display mode. Emits 'trmnl:change' events with current state\n * and CSS classes for rendering.\n * \n * **[View a live demo \u2192](https://usetrmnl.github.io/trmnl-picker/example/)**\n *\n * **Note:** Using the constructor directly is not recommended. Use the static\n * {@link TRMNLPicker.create} method instead, which automatically fetches\n * models and palettes from the TRMNL API if not provided.\n *\n * @class TRMNLPicker\n * @param {string|Element} formIdOrElement - Form element ID or DOM element reference\n * @param {Object} options - Configuration options\n * @param {Array} options.models - Array of model objects from TRMNL API\n * @param {Array} options.palettes - Array of palette objects from TRMNL API\n * @param {string} [options.localStorageKey] - Optional key for persisting state to localStorage\n *\n * @fires TRMNLPicker#trmnl:change\n *\n * @example\n * // HTML Structure - Required form with data-* attributes\n * //
\n * // \n * // \n * //\n * // \n * // \n * //\n * // \n * // \n * //\n * // \n * // \n * //\n * // \n * // \n * //
\n *\n * // Create with element ID\n * const picker = new TRMNLPicker('picker-form', { models, palettes })\n *\n * // Create with DOM element\n * const element = document.getElementById('picker-form')\n * const picker = new TRMNLPicker(element, { models, palettes })\n *\n * // Listen for changes\n * picker.formElement.addEventListener('trmnl:change', (event) => {\n * console.log(event.detail.screenClasses)\n * })\n */\nclass TRMNLPicker {\n static API_BASE_URL = 'https://usetrmnl.com'\n\n /**\n * Get cached API response from localStorage\n * @private\n * @static\n * @returns {{models: Array, palettes: Array} | null} Cached data or null if expired/missing\n */\n static _getCachedApiData() {\n try {\n const cached = localStorage.getItem(_API_CACHE_KEY)\n if (!cached) return null\n\n const { timestamp, models, palettes } = JSON.parse(cached)\n const now = Date.now()\n\n // Check if cache is still valid\n if (now - timestamp > _CACHE_TTL_MS) {\n localStorage.removeItem(_API_CACHE_KEY)\n return null\n }\n\n return { models, palettes }\n } catch (error) {\n console.warn('TRMNLPicker: Failed to read API cache:', error)\n return null\n }\n }\n\n /**\n * Save API response to localStorage cache\n * @private\n * @static\n * @param {Array} models - Models array\n * @param {Array} palettes - Palettes array\n */\n static _setCachedApiData(models, palettes) {\n try {\n const cacheData = {\n timestamp: Date.now(),\n models,\n palettes\n }\n localStorage.setItem(_API_CACHE_KEY, JSON.stringify(cacheData))\n } catch (error) {\n console.warn('TRMNLPicker: Failed to save API cache:', error)\n }\n }\n\n /**\n * Create a TRMNLPicker instance, fetching data from TRMNL API if not provided\n *\n * Automatically caches API responses in localStorage for 24 hours to reduce network requests.\n *\n * @static\n * @param {string|Element} formIdOrElement - Form element ID or DOM element\n * @param {Object} options - Configuration options\n * @param {Array} [options.models] - Optional models array (fetched from API if not provided)\n * @param {Array} [options.palettes] - Optional palettes array (fetched from API if not provided)\n * @param {string} [options.localStorageKey] - Optional key for state persistence\n * @returns {Promise} Promise resolving to picker instance\n * @throws {Error} If API fetch fails when models or palettes are not provided\n *\n * @example\n * // Fetch models and palettes from API (or use cached data if available)\n * const picker = await TRMNLPicker.create('screen-picker')\n *\n * // Provide your own data\n * const picker = await TRMNLPicker.create('screen-picker', { models, palettes })\n */\n static async create(formId, options = {}) {\n let { models, palettes, localStorageKey } = options\n\n // Try to use cached data if neither models nor palettes are provided\n if (!models && !palettes) {\n const cached = TRMNLPicker._getCachedApiData()\n if (cached) {\n models = cached.models\n palettes = cached.palettes\n }\n }\n\n // Fetch models if not provided and not in cache\n if (!models) {\n try {\n const response = await fetch(`${TRMNLPicker.API_BASE_URL}/api/models`)\n if (!response.ok) {\n throw new Error(`Failed to fetch models: ${response.status} ${response.statusText}`)\n }\n const data = await response.json()\n models = data.data || data\n } catch (error) {\n throw new Error(`TRMNLPicker: Failed to fetch models from API: ${error.message}`)\n }\n }\n\n // Fetch palettes if not provided and not in cache\n if (!palettes) {\n try {\n const response = await fetch(`${TRMNLPicker.API_BASE_URL}/api/palettes`)\n if (!response.ok) {\n throw new Error(`Failed to fetch palettes: ${response.status} ${response.statusText}`)\n }\n const data = await response.json()\n palettes = data.data || data\n } catch (error) {\n throw new Error(`TRMNLPicker: Failed to fetch palettes from API: ${error.message}`)\n }\n }\n\n // Cache the API data for future use (if both were fetched)\n if (!options.models && !options.palettes) {\n TRMNLPicker._setCachedApiData(models, palettes)\n }\n\n return new TRMNLPicker(formId, { models, palettes, localStorageKey })\n }\n\n constructor(formIdOrElement, options = {}) {\n // Validate inputs\n if (!formIdOrElement) {\n throw new Error('TRMNLPicker: formIdOrElement is required')\n }\n\n // Store references - accept either string ID or DOM element\n if (typeof formIdOrElement === 'string') {\n this.formElement = document.getElementById(formIdOrElement)\n if (!this.formElement) {\n throw new Error(`TRMNLPicker: Form element with id \"${formIdOrElement}\" not found`)\n }\n } else if (formIdOrElement instanceof Element) {\n this.formElement = formIdOrElement\n } else {\n throw new Error('TRMNLPicker: formIdOrElement must be a string ID or DOM element')\n }\n\n const { models, palettes, localStorageKey } = options\n\n this.models = models\n this.palettes = palettes\n this.localStorageKey = localStorageKey\n\n // Only initialize if we have data\n if (this.models && this.palettes) {\n if (!Array.isArray(this.models) || this.models.length === 0) {\n throw new Error('TRMNLPicker: models must be a non-empty array')\n }\n\n if (!Array.isArray(this.palettes) || this.palettes.length === 0) {\n throw new Error('TRMNLPicker: palettes must be a non-empty array')\n }\n\n // Filter out models where all palettes have empty framework_class\n this.models = this._filterValidModels()\n\n if (this.models.length === 0) {\n throw new Error('TRMNLPicker: no valid models found (all models have palettes with empty framework_class)')\n }\n\n // Initialize DOM elements and bind events\n this._initializeElements()\n this._bindEvents()\n\n // Set initial state (will load from localStorage if available)\n this._setInitialState()\n }\n }\n\n /**\n * Filter out models where all their palettes have empty framework_class\n * @private\n * @returns {Array} Filtered models array\n */\n _filterValidModels() {\n return this.models.filter(model => {\n // Check if at least one palette for this model has a non-empty framework_class\n return model.palette_ids.some(paletteId => {\n const palette = this.palettes.find(p => p.id === paletteId)\n return palette && palette.framework_class && palette.framework_class.trim() !== ''\n })\n })\n }\n\n /**\n * Get the first valid palette ID for a model (one with non-empty framework_class)\n * @private\n * @param {Object} model - Model object\n * @returns {string|null} First valid palette ID or null\n */\n _getFirstValidPaletteId(model) {\n if (!model) return null\n\n for (const paletteId of model.palette_ids) {\n const palette = this.palettes.find(p => p.id === paletteId)\n if (palette && palette.framework_class && palette.framework_class.trim() !== '') {\n return paletteId\n }\n }\n\n return null\n }\n\n /**\n * Find and store references to form elements using data-* attributes\n * @private\n */\n _initializeElements() {\n this.elements = {\n modelSelect: this.formElement.querySelector('[data-model-select]'),\n paletteSelect: this.formElement.querySelector('[data-palette-select]'),\n orientationToggle: this.formElement.querySelector('[data-orientation-toggle]'),\n darkModeToggle: this.formElement.querySelector('[data-dark-mode-toggle]'),\n resetButton: this.formElement.querySelector('[data-reset-button]'),\n\n // Optional: UI indicator elements\n orientationText: this.formElement.querySelector('[data-orientation-text]'),\n darkModeText: this.formElement.querySelector('[data-dark-mode-text]')\n }\n\n // Validate required elements\n const required = ['modelSelect', 'paletteSelect']\n for (const key of required) {\n if (!this.elements[key]) {\n throw new Error(`TRMNLPicker: Required element \"${key}\" not found in form`)\n }\n }\n }\n\n /**\n * Bind event listeners to form elements\n * @private\n */\n _bindEvents() {\n // Store bound handlers for cleanup\n this.handlers = {\n modelChange: this._handleModelChange.bind(this),\n paletteChange: this._handlePaletteChange.bind(this),\n orientationToggle: this._toggleOrientation.bind(this),\n darkModeToggle: this._toggleDarkMode.bind(this),\n reset: this._resetToModelDefaults.bind(this)\n }\n\n // Attach event listeners\n this.elements.modelSelect.addEventListener('change', this.handlers.modelChange)\n this.elements.paletteSelect.addEventListener('change', this.handlers.paletteChange)\n\n if (this.elements.orientationToggle) {\n this.elements.orientationToggle.addEventListener('click', this.handlers.orientationToggle)\n }\n\n if (this.elements.darkModeToggle) {\n this.elements.darkModeToggle.addEventListener('click', this.handlers.darkModeToggle)\n }\n\n if (this.elements.resetButton) {\n this.elements.resetButton.addEventListener('click', this.handlers.reset)\n }\n }\n\n /**\n * Set initial state and populate form\n * @private\n */\n _setInitialState() {\n // Separate models into TRMNL and BYOD groups\n const trmnlModels = this.models.filter(m => m.kind === 'trmnl')\n const byodModels = this.models.filter(m => m.kind !== 'trmnl')\n\n // Sort each group alphabetically by label\n const sortTRMNL = [...trmnlModels].sort((a, b) => {\n const labelA = (a.label || a.name).toLowerCase()\n const labelB = (b.label || b.name).toLowerCase()\n return labelA.localeCompare(labelB)\n })\n\n const sortBYOD = [...byodModels].sort((a, b) => {\n const labelA = (a.label || a.name).toLowerCase()\n const labelB = (b.label || b.name).toLowerCase()\n return labelA.localeCompare(labelB)\n })\n\n // Populate model select with grouped models\n this.elements.modelSelect.innerHTML = ''\n\n // Add TRMNL group\n if (sortTRMNL.length > 0) {\n const trmnlGroup = document.createElement('optgroup')\n trmnlGroup.label = 'TRMNL'\n sortTRMNL.forEach(model => {\n const option = document.createElement('option')\n option.value = model.name\n option.textContent = model.label || model.name\n trmnlGroup.appendChild(option)\n })\n this.elements.modelSelect.appendChild(trmnlGroup)\n }\n\n // Add BYOD group\n if (sortBYOD.length > 0) {\n const byodGroup = document.createElement('optgroup')\n byodGroup.label = 'BYOD'\n sortBYOD.forEach(model => {\n const option = document.createElement('option')\n option.value = model.name\n option.textContent = model.label || model.name\n byodGroup.appendChild(option)\n })\n this.elements.modelSelect.appendChild(byodGroup)\n }\n\n // Keep combined sorted list for default selection logic\n const sortedModels = [...sortTRMNL, ...sortBYOD]\n\n // Initialize state\n this._state = {}\n\n const defaultModel = sortedModels.find(m => m.name === _DEFAULT_MODEL_NAME) || sortedModels[0]\n const defaultPaletteId = this._getFirstValidPaletteId(defaultModel)\n const defaultParams = {\n modelName: defaultModel.name,\n paletteId: defaultPaletteId,\n isPortrait: false,\n isDarkMode: false\n }\n\n const savedParams = this._loadFromLocalStorage() || {}\n\n this._setParams('constructor', { ...defaultParams, ...savedParams })\n }\n\n /**\n * Populate palette dropdown based on selected model\n * @private\n */\n _populateModelPalettes() {\n const modelName = this.elements.modelSelect.value\n const model = this.models.find(m => m.name === modelName)\n\n if (!model) return\n\n // Clear existing options\n this.elements.paletteSelect.innerHTML = ''\n\n // Add options for each palette_id in the model (only if framework_class is not empty)\n model.palette_ids.forEach(paletteId => {\n const palette = this.palettes.find(p => p.id === paletteId)\n if (palette && palette.framework_class && palette.framework_class.trim() !== '') {\n const option = document.createElement('option')\n option.value = palette.id\n option.textContent = palette.name\n this.elements.paletteSelect.appendChild(option)\n }\n })\n }\n\n /**\n * Emit 'trmnl:change' event with current state and screen classes\n * @private\n * @param {string} origin - Source of the change ('constructor', 'form', 'setParams')\n * @fires TRMNLPicker#trmnl:change\n */\n _emitChangeEvent(origin) {\n // Save to localStorage if key is configured\n this._saveToLocalStorage()\n\n const event = new CustomEvent('trmnl:change', {\n detail: {\n origin,\n ...this.state,\n },\n bubbles: true\n })\n\n this.formElement.dispatchEvent(event)\n }\n\n /**\n * Load state from localStorage\n * @private\n * @returns {Object|null} Saved state or null if not available\n */\n _loadFromLocalStorage() {\n if (!this.localStorageKey) return null\n\n try {\n const saved = localStorage.getItem(this.localStorageKey)\n if (saved) {\n return JSON.parse(saved)\n }\n } catch (error) {\n console.warn('TRMNLPicker: Failed to load from localStorage:', error)\n }\n\n return null\n }\n\n /**\n * Save current state to localStorage\n * @private\n */\n _saveToLocalStorage() {\n if (!this.localStorageKey) return\n\n try {\n localStorage.setItem(this.localStorageKey, JSON.stringify(this.params))\n } catch (error) {\n console.warn('TRMNLPicker: Failed to save to localStorage:', error)\n }\n }\n\n /**\n * Handle model selection change\n * @private\n */\n _handleModelChange(event) {\n this._setParams('form', { modelName: event.target.value })\n }\n\n /**\n * Handle palette selection change\n * @private\n */\n _handlePaletteChange(event) {\n this._setParams('form', { paletteId: event.target.value })\n }\n\n /**\n * Toggle orientation between portrait and landscape\n * @private\n */\n _toggleOrientation() {\n this._setParams('form', { isPortrait: !this._state.isPortrait })\n }\n\n /**\n * Toggle dark mode on/off\n * @private\n */\n _toggleDarkMode() {\n this._setParams('form', { isDarkMode: !this._state.isDarkMode })\n }\n\n /**\n * Reset to defaults: first valid palette, landscape orientation, light mode\n * @private\n */\n _resetToModelDefaults() {\n const model = this._state.model\n if (!model) return\n\n // Reset to first valid palette of current model\n const firstPaletteId = this._getFirstValidPaletteId(model)\n\n this._setParams('form', {\n paletteId: firstPaletteId,\n isPortrait: false,\n isDarkMode: false\n })\n }\n\n /**\n * Update reset button enabled/disabled state\n * Button is disabled only when palette, orientation, and dark mode are all at defaults\n * @private\n */\n _updateResetButton() {\n if (!this.elements.resetButton) return\n\n const model = this._state.model\n if (!model) return\n\n const firstValidPaletteId = this._getFirstValidPaletteId(model)\n const isPaletteDefault = this.elements.paletteSelect.value === String(firstValidPaletteId)\n const isOrientationDefault = this._state.isPortrait === false\n const isDarkModeDefault = this._state.isDarkMode === false\n\n const isAtDefaults = isPaletteDefault && isOrientationDefault && isDarkModeDefault\n\n this.elements.resetButton.disabled = isAtDefaults\n\n if (isAtDefaults) {\n this.elements.resetButton.classList.add('opacity-50', 'cursor-default')\n this.elements.resetButton.setAttribute('aria-disabled', 'true')\n } else {\n this.elements.resetButton.classList.remove('opacity-50', 'cursor-default')\n this.elements.resetButton.removeAttribute('aria-disabled')\n }\n }\n\n /**\n * Get CSS classes for the current picker configuration\n * @private\n * @returns {Array} Array of CSS class names for Framework CSS rendering\n *\n * Generated classes (in order):\n * 1. 'screen' - Base class (always present)\n * 2. palette.framework_class - From selected palette (e.g., 'screen--1bit')\n * 3. model.css.classes.device - From model API (e.g., 'screen--v2')\n * 4. model.css.classes.size - From model API (e.g., 'screen--md')\n * 5. 'screen--portrait' - Only when portrait orientation is enabled\n * 6. 'screen--1x' - Scale indicator (always 1x)\n * 7. 'screen--dark-mode' - Only when dark mode is enabled\n *\n * @example\n * const classes = picker.screenClasses\n * // ['screen', 'screen--1bit', 'screen--v2', 'screen--md', 'screen--1x']\n */\n get _screenClasses() {\n const model = this._state.model\n const palette = this._state.palette\n\n if (!model) {\n throw new Error('No model selected')\n }\n\n const classes = []\n\n // 0. Base screen class (always present)\n classes.push('screen')\n\n // 1. Palette framework class\n if (palette && palette.framework_class) {\n classes.push(palette.framework_class)\n }\n\n // 2. Model device class (from API)\n if (model.css && model.css.classes && model.css.classes.device) {\n classes.push(model.css.classes.device)\n }\n\n // 3. Model size class (from API)\n if (model.css && model.css.classes && model.css.classes.size) {\n classes.push(model.css.classes.size)\n }\n\n // 4. Orientation (UI state, only portrait - landscape is default)\n if (this._state.isPortrait) {\n classes.push('screen--portrait')\n }\n\n // 5. Scale (UI state, always 1x)\n classes.push('screen--1x')\n\n // 6. Dark mode (UI state, conditional)\n if (this._state.isDarkMode) {\n classes.push('screen--dark-mode')\n }\n\n return classes\n }\n\n /**\n * Get current picker parameters (serializable state)\n * @public\n * @returns {Object} Current parameters for persistence or API calls\n * @returns {string} return.modelName - Selected model name\n * @returns {string} return.paletteId - Selected palette ID\n * @returns {boolean} return.isPortrait - Portrait orientation flag\n * @returns {boolean} return.isDarkMode - Dark mode flag\n *\n * @example\n * const params = picker.params\n * // { modelName: 'og_plus', paletteId: '123', isPortrait: false, isDarkMode: false }\n *\n * // Can be used to restore state later\n * localStorage.setItem('picker-state', JSON.stringify(picker.params))\n */\n get params() {\n return {\n modelName: this._state.model?.name,\n paletteId: this._state.palette?.id,\n isPortrait: this._state.isPortrait,\n isDarkMode: this._state.isDarkMode\n }\n }\n\n /**\n * Update picker configuration programmatically\n * @public\n * @param {Object} params - Configuration object (all fields optional)\n * @param {string} [params.modelName] - Model name to select\n * @param {string} [params.paletteId] - Palette ID to select\n * @param {boolean} [params.isPortrait] - Portrait orientation\n * @param {boolean} [params.isDarkMode] - Dark mode enabled\n * @fires TRMNLPicker#trmnl:change\n * @throws {Error} If params is not an object\n *\n * @example\n * // Update single parameter\n * picker.setParams({ isDarkMode: true })\n *\n * // Update multiple parameters\n * picker.setParams({\n * modelName: 'og_plus',\n * paletteId: '123',\n * isPortrait: true\n * })\n *\n * // Note: Changing model resets palette to first valid palette of that model\n */\n setParams(params) {\n this._setParams('setParams', params)\n }\n\n /**\n * Internal method to update picker state with origin tracking\n * @private\n * @param {string} origin - Source of change ('constructor', 'form', 'setParams')\n * @param {Object} params - Parameters to update\n * @returns {boolean} True if any changes were made\n */\n _setParams(origin, params) {\n if (!params || typeof params !== 'object') {\n throw new Error('params must be an object')\n }\n\n let changed = false\n\n // Update model if provided\n if (params.modelName) {\n const model = this.models.find(m => m.name === params.modelName)\n if (model) {\n this.elements.modelSelect.value = model.name\n this._state.model = model\n this._populateModelPalettes()\n\n // Select first valid palette of new model\n const firstPaletteId = this._getFirstValidPaletteId(model)\n this.elements.paletteSelect.value = firstPaletteId\n this._state.palette = this.palettes.find(p => p.id === firstPaletteId)\n changed = true\n }\n }\n\n // Update palette if provided\n if (params.paletteId) {\n const palette = this.palettes.find(p => p.id === params.paletteId)\n if (palette) {\n this.elements.paletteSelect.value = palette.id\n this._state.palette = palette\n changed = true\n }\n }\n\n // Update orientation if provided\n if (typeof params.isPortrait === 'boolean') {\n this._state.isPortrait = params.isPortrait\n if (this.elements.orientationText) {\n this.elements.orientationText.textContent = this._state.isPortrait ? 'Portrait' : 'Landscape'\n }\n changed = true\n }\n\n // Update dark mode if provided\n if (typeof params.isDarkMode === 'boolean') {\n this._state.isDarkMode = params.isDarkMode\n if (this.elements.darkModeText) {\n this.elements.darkModeText.textContent = this._state.isDarkMode ? 'Dark Mode' : 'Light Mode'\n }\n changed = true\n }\n\n if (changed) {\n this._updateResetButton()\n }\n\n if (changed && origin) {\n this._emitChangeEvent(origin)\n }\n\n return changed\n }\n\n /**\n * Get complete picker state including full model and palette objects\n * @public\n * @returns {{\n * model: Object,\n * palette: Object,\n * isPortrait: boolean,\n * isDarkMode: boolean,\n * screenClasses: Array,\n * width: number,\n * height: number\n * }} State object containing model (full model object from API), palette (full palette object from API), isPortrait flag, and isDarkMode flag\n *\n * @example\n * const state = picker.state\n * // {\n * // model: { name: 'og_plus', label: 'OG+', width: 800, height: 480, ... },\n * // palette: { id: '123', name: 'Black', framework_class: 'screen--1bit', ... },\n * // isPortrait: false,\n * // isDarkMode: false,\n * // screenClasses: ['screen', 'screen--1bit', 'screen--v2', 'screen--md', 'screen--1x'],\n * // width: 800,\n * // height: 480\n * // }\n */\n get state() {\n return {\n screenClasses: this._screenClasses,\n ...this._dimensions,\n ...this._state,\n };\n }\n\n /**\n * Get current dimensions (width and height) of the screen in pixels\n * @private\n * @returns {{ width: number, height: number }} Object with width and height properties\n */\n get _dimensions() {\n const model = this._state.model\n\n let width = model.width / model.scale_factor\n let height = model.height / model.scale_factor\n\n if (this._state.isPortrait) {\n [width, height] = [height, width]\n }\n\n return { width, height }\n }\n\n /**\n * Clean up event listeners and references\n * @public\n */\n destroy() {\n // Remove event listeners\n this.elements.modelSelect.removeEventListener('change', this.handlers.modelChange)\n this.elements.paletteSelect.removeEventListener('change', this.handlers.paletteChange)\n\n if (this.elements.orientationToggle) {\n this.elements.orientationToggle.removeEventListener('click', this.handlers.orientationToggle)\n }\n\n if (this.elements.darkModeToggle) {\n this.elements.darkModeToggle.removeEventListener('click', this.handlers.darkModeToggle)\n }\n\n if (this.elements.resetButton) {\n this.elements.resetButton.removeEventListener('click', this.handlers.reset)\n }\n\n // Clear references\n this.formElement = null\n this.elements = null\n this.handlers = null\n this.models = null\n this.palettes = null\n this._state = null\n }\n}\n\nexport default TRMNLPicker\n"], + "sourcesContent": ["/**\n * Default model to select when no localStorage state is found\n * @private\n * @constant {string}\n */\nconst _DEFAULT_MODEL_NAME = 'og_plus'\n\n/**\n * localStorage key for caching API responses (models and palettes)\n * @private\n * @constant {string}\n */\nconst _API_CACHE_KEY = 'trmnl-picker-api-cache'\n\n/**\n * Cache TTL in milliseconds (1 day)\n * @private\n * @constant {number}\n */\nconst _CACHE_TTL_MS = 24 * 60 * 60 * 1000\n\n/**\n * Event fired when picker state changes\n * @event TRMNLPicker#trmnl:change\n * @type {CustomEvent}\n * @property {Object} detail - Event details\n * @property {string} detail.origin - What triggered the change: 'constructor', 'form', or 'setParams'\n * @property {Array} detail.screenClasses - Array of CSS classes for Framework CSS rendering\n * @property {Object} detail.model - Current model object with name, label, width, height, kind, css properties\n * @property {Object} detail.palette - Current palette object with id, name, framework_class properties\n * @property {boolean} detail.isPortrait - Portrait orientation flag\n * @property {boolean} detail.isDarkMode - Dark mode flag\n * @property {number} detail.width - Current width of the screen in pixels\n * @property {number} detail.height - Current height of the screen in pixels\n *\n * @example\n * picker.formElement.addEventListener('trmnl:change', (event) => {\n * const { origin, screenClasses, model, palette } = event.detail\n * console.log(`Changed via ${origin}`)\n * console.log('Classes:', screenClasses)\n * })\n */\n\n/**\n * TRMNLPicker - Vanilla JS library for TRMNL device and palette selection\n *\n * Provides a reactive picker component that manages device models, color palettes,\n * orientation, and display mode. Emits 'trmnl:change' events with current state\n * and CSS classes for rendering.\n * \n * **[View a live demo \u2192](https://usetrmnl.github.io/trmnl-picker/example/)**\n *\n * **Note:** Using the constructor directly is not recommended. Use the static\n * {@link TRMNLPicker.create} method instead, which automatically fetches\n * models and palettes from the TRMNL API if not provided.\n *\n * @class TRMNLPicker\n * @param {string|Element} formIdOrElement - Form element ID or DOM element reference\n * @param {Object} options - Configuration options\n * @param {Array} options.models - Array of model objects from TRMNL API\n * @param {Array} options.palettes - Array of palette objects from TRMNL API\n * @param {string} [options.localStorageKey] - Optional key for persisting state to localStorage\n *\n * @fires TRMNLPicker#trmnl:change\n *\n * @example\n * // HTML Structure - Required form with data-* attributes\n * //
\n * // \n * // \n * //\n * // \n * // \n * //\n * // \n * // \n * //\n * // \n * // \n * //\n * // \n * // \n * //
\n *\n * // Create with element ID\n * const picker = new TRMNLPicker('picker-form', { models, palettes })\n *\n * // Create with DOM element\n * const element = document.getElementById('picker-form')\n * const picker = new TRMNLPicker(element, { models, palettes })\n *\n * // Listen for changes\n * picker.formElement.addEventListener('trmnl:change', (event) => {\n * console.log(event.detail.screenClasses)\n * })\n */\nclass TRMNLPicker {\n static API_BASE_URL = 'https://trmnl.com'\n\n /**\n * Get cached API response from localStorage\n * @private\n * @static\n * @returns {{models: Array, palettes: Array} | null} Cached data or null if expired/missing\n */\n static _getCachedApiData() {\n try {\n const cached = localStorage.getItem(_API_CACHE_KEY)\n if (!cached) return null\n\n const { timestamp, models, palettes } = JSON.parse(cached)\n const now = Date.now()\n\n // Check if cache is still valid\n if (now - timestamp > _CACHE_TTL_MS) {\n localStorage.removeItem(_API_CACHE_KEY)\n return null\n }\n\n return { models, palettes }\n } catch (error) {\n console.warn('TRMNLPicker: Failed to read API cache:', error)\n return null\n }\n }\n\n /**\n * Save API response to localStorage cache\n * @private\n * @static\n * @param {Array} models - Models array\n * @param {Array} palettes - Palettes array\n */\n static _setCachedApiData(models, palettes) {\n try {\n const cacheData = {\n timestamp: Date.now(),\n models,\n palettes\n }\n localStorage.setItem(_API_CACHE_KEY, JSON.stringify(cacheData))\n } catch (error) {\n console.warn('TRMNLPicker: Failed to save API cache:', error)\n }\n }\n\n /**\n * Create a TRMNLPicker instance, fetching data from TRMNL API if not provided\n *\n * Automatically caches API responses in localStorage for 24 hours to reduce network requests.\n *\n * @static\n * @param {string|Element} formIdOrElement - Form element ID or DOM element\n * @param {Object} options - Configuration options\n * @param {Array} [options.models] - Optional models array (fetched from API if not provided)\n * @param {Array} [options.palettes] - Optional palettes array (fetched from API if not provided)\n * @param {string} [options.localStorageKey] - Optional key for state persistence\n * @returns {Promise} Promise resolving to picker instance\n * @throws {Error} If API fetch fails when models or palettes are not provided\n *\n * @example\n * // Fetch models and palettes from API (or use cached data if available)\n * const picker = await TRMNLPicker.create('screen-picker')\n *\n * // Provide your own data\n * const picker = await TRMNLPicker.create('screen-picker', { models, palettes })\n */\n static async create(formId, options = {}) {\n let { models, palettes, localStorageKey } = options\n\n // Try to use cached data if neither models nor palettes are provided\n if (!models && !palettes) {\n const cached = TRMNLPicker._getCachedApiData()\n if (cached) {\n models = cached.models\n palettes = cached.palettes\n }\n }\n\n // Fetch models if not provided and not in cache\n if (!models) {\n try {\n const response = await fetch(`${TRMNLPicker.API_BASE_URL}/api/models`)\n if (!response.ok) {\n throw new Error(`Failed to fetch models: ${response.status} ${response.statusText}`)\n }\n const data = await response.json()\n models = data.data || data\n } catch (error) {\n throw new Error(`TRMNLPicker: Failed to fetch models from API: ${error.message}`)\n }\n }\n\n // Fetch palettes if not provided and not in cache\n if (!palettes) {\n try {\n const response = await fetch(`${TRMNLPicker.API_BASE_URL}/api/palettes`)\n if (!response.ok) {\n throw new Error(`Failed to fetch palettes: ${response.status} ${response.statusText}`)\n }\n const data = await response.json()\n palettes = data.data || data\n } catch (error) {\n throw new Error(`TRMNLPicker: Failed to fetch palettes from API: ${error.message}`)\n }\n }\n\n // Cache the API data for future use (if both were fetched)\n if (!options.models && !options.palettes) {\n TRMNLPicker._setCachedApiData(models, palettes)\n }\n\n return new TRMNLPicker(formId, { models, palettes, localStorageKey })\n }\n\n constructor(formIdOrElement, options = {}) {\n // Validate inputs\n if (!formIdOrElement) {\n throw new Error('TRMNLPicker: formIdOrElement is required')\n }\n\n // Store references - accept either string ID or DOM element\n if (typeof formIdOrElement === 'string') {\n this.formElement = document.getElementById(formIdOrElement)\n if (!this.formElement) {\n throw new Error(`TRMNLPicker: Form element with id \"${formIdOrElement}\" not found`)\n }\n } else if (formIdOrElement instanceof Element) {\n this.formElement = formIdOrElement\n } else {\n throw new Error('TRMNLPicker: formIdOrElement must be a string ID or DOM element')\n }\n\n const { models, palettes, localStorageKey } = options\n\n this.models = models\n this.palettes = palettes\n this.localStorageKey = localStorageKey\n\n // Only initialize if we have data\n if (this.models && this.palettes) {\n if (!Array.isArray(this.models) || this.models.length === 0) {\n throw new Error('TRMNLPicker: models must be a non-empty array')\n }\n\n if (!Array.isArray(this.palettes) || this.palettes.length === 0) {\n throw new Error('TRMNLPicker: palettes must be a non-empty array')\n }\n\n // Filter out models where all palettes have empty framework_class\n this.models = this._filterValidModels()\n\n if (this.models.length === 0) {\n throw new Error('TRMNLPicker: no valid models found (all models have palettes with empty framework_class)')\n }\n\n // Initialize DOM elements and bind events\n this._initializeElements()\n this._bindEvents()\n\n // Set initial state (will load from localStorage if available)\n this._setInitialState()\n }\n }\n\n /**\n * Filter out models where all their palettes have empty framework_class\n * @private\n * @returns {Array} Filtered models array\n */\n _filterValidModels() {\n return this.models.filter(model => {\n // Check if at least one palette for this model has a non-empty framework_class\n return model.palette_ids.some(paletteId => {\n const palette = this.palettes.find(p => p.id === paletteId)\n return palette && palette.framework_class && palette.framework_class.trim() !== ''\n })\n })\n }\n\n /**\n * Get the first valid palette ID for a model (one with non-empty framework_class)\n * @private\n * @param {Object} model - Model object\n * @returns {string|null} First valid palette ID or null\n */\n _getFirstValidPaletteId(model) {\n if (!model) return null\n\n for (const paletteId of model.palette_ids) {\n const palette = this.palettes.find(p => p.id === paletteId)\n if (palette && palette.framework_class && palette.framework_class.trim() !== '') {\n return paletteId\n }\n }\n\n return null\n }\n\n /**\n * Find and store references to form elements using data-* attributes\n * @private\n */\n _initializeElements() {\n this.elements = {\n modelSelect: this.formElement.querySelector('[data-model-select]'),\n paletteSelect: this.formElement.querySelector('[data-palette-select]'),\n orientationToggle: this.formElement.querySelector('[data-orientation-toggle]'),\n darkModeToggle: this.formElement.querySelector('[data-dark-mode-toggle]'),\n resetButton: this.formElement.querySelector('[data-reset-button]'),\n\n // Optional: UI indicator elements\n orientationText: this.formElement.querySelector('[data-orientation-text]'),\n darkModeText: this.formElement.querySelector('[data-dark-mode-text]')\n }\n\n // Validate required elements\n const required = ['modelSelect', 'paletteSelect']\n for (const key of required) {\n if (!this.elements[key]) {\n throw new Error(`TRMNLPicker: Required element \"${key}\" not found in form`)\n }\n }\n }\n\n /**\n * Bind event listeners to form elements\n * @private\n */\n _bindEvents() {\n // Store bound handlers for cleanup\n this.handlers = {\n modelChange: this._handleModelChange.bind(this),\n paletteChange: this._handlePaletteChange.bind(this),\n orientationToggle: this._toggleOrientation.bind(this),\n darkModeToggle: this._toggleDarkMode.bind(this),\n reset: this._resetToModelDefaults.bind(this)\n }\n\n // Attach event listeners\n this.elements.modelSelect.addEventListener('change', this.handlers.modelChange)\n this.elements.paletteSelect.addEventListener('change', this.handlers.paletteChange)\n\n if (this.elements.orientationToggle) {\n this.elements.orientationToggle.addEventListener('click', this.handlers.orientationToggle)\n }\n\n if (this.elements.darkModeToggle) {\n this.elements.darkModeToggle.addEventListener('click', this.handlers.darkModeToggle)\n }\n\n if (this.elements.resetButton) {\n this.elements.resetButton.addEventListener('click', this.handlers.reset)\n }\n }\n\n /**\n * Set initial state and populate form\n * @private\n */\n _setInitialState() {\n // Separate models into TRMNL and BYOD groups\n const trmnlModels = this.models.filter(m => m.kind === 'trmnl')\n const byodModels = this.models.filter(m => m.kind !== 'trmnl')\n\n // Sort each group alphabetically by label\n const sortTRMNL = [...trmnlModels].sort((a, b) => {\n const labelA = (a.label || a.name).toLowerCase()\n const labelB = (b.label || b.name).toLowerCase()\n return labelA.localeCompare(labelB)\n })\n\n const sortBYOD = [...byodModels].sort((a, b) => {\n const labelA = (a.label || a.name).toLowerCase()\n const labelB = (b.label || b.name).toLowerCase()\n return labelA.localeCompare(labelB)\n })\n\n // Populate model select with grouped models\n this.elements.modelSelect.innerHTML = ''\n\n // Add TRMNL group\n if (sortTRMNL.length > 0) {\n const trmnlGroup = document.createElement('optgroup')\n trmnlGroup.label = 'TRMNL'\n sortTRMNL.forEach(model => {\n const option = document.createElement('option')\n option.value = model.name\n option.textContent = model.label || model.name\n trmnlGroup.appendChild(option)\n })\n this.elements.modelSelect.appendChild(trmnlGroup)\n }\n\n // Add BYOD group\n if (sortBYOD.length > 0) {\n const byodGroup = document.createElement('optgroup')\n byodGroup.label = 'BYOD'\n sortBYOD.forEach(model => {\n const option = document.createElement('option')\n option.value = model.name\n option.textContent = model.label || model.name\n byodGroup.appendChild(option)\n })\n this.elements.modelSelect.appendChild(byodGroup)\n }\n\n // Keep combined sorted list for default selection logic\n const sortedModels = [...sortTRMNL, ...sortBYOD]\n\n // Initialize state\n this._state = {}\n\n const defaultModel = sortedModels.find(m => m.name === _DEFAULT_MODEL_NAME) || sortedModels[0]\n const defaultPaletteId = this._getFirstValidPaletteId(defaultModel)\n const defaultParams = {\n modelName: defaultModel.name,\n paletteId: defaultPaletteId,\n isPortrait: false,\n isDarkMode: false\n }\n\n const savedParams = this._loadFromLocalStorage() || {}\n\n this._setParams('constructor', { ...defaultParams, ...savedParams })\n }\n\n /**\n * Populate palette dropdown based on selected model\n * @private\n */\n _populateModelPalettes() {\n const modelName = this.elements.modelSelect.value\n const model = this.models.find(m => m.name === modelName)\n\n if (!model) return\n\n // Clear existing options\n this.elements.paletteSelect.innerHTML = ''\n\n // Add options for each palette_id in the model (only if framework_class is not empty)\n model.palette_ids.forEach(paletteId => {\n const palette = this.palettes.find(p => p.id === paletteId)\n if (palette && palette.framework_class && palette.framework_class.trim() !== '') {\n const option = document.createElement('option')\n option.value = palette.id\n option.textContent = palette.name\n this.elements.paletteSelect.appendChild(option)\n }\n })\n }\n\n /**\n * Emit 'trmnl:change' event with current state and screen classes\n * @private\n * @param {string} origin - Source of the change ('constructor', 'form', 'setParams')\n * @fires TRMNLPicker#trmnl:change\n */\n _emitChangeEvent(origin) {\n // Save to localStorage if key is configured\n this._saveToLocalStorage()\n\n const event = new CustomEvent('trmnl:change', {\n detail: {\n origin,\n ...this.state,\n },\n bubbles: true\n })\n\n this.formElement.dispatchEvent(event)\n }\n\n /**\n * Load state from localStorage\n * @private\n * @returns {Object|null} Saved state or null if not available\n */\n _loadFromLocalStorage() {\n if (!this.localStorageKey) return null\n\n try {\n const saved = localStorage.getItem(this.localStorageKey)\n if (saved) {\n return JSON.parse(saved)\n }\n } catch (error) {\n console.warn('TRMNLPicker: Failed to load from localStorage:', error)\n }\n\n return null\n }\n\n /**\n * Save current state to localStorage\n * @private\n */\n _saveToLocalStorage() {\n if (!this.localStorageKey) return\n\n try {\n localStorage.setItem(this.localStorageKey, JSON.stringify(this.params))\n } catch (error) {\n console.warn('TRMNLPicker: Failed to save to localStorage:', error)\n }\n }\n\n /**\n * Handle model selection change\n * @private\n */\n _handleModelChange(event) {\n this._setParams('form', { modelName: event.target.value })\n }\n\n /**\n * Handle palette selection change\n * @private\n */\n _handlePaletteChange(event) {\n this._setParams('form', { paletteId: event.target.value })\n }\n\n /**\n * Toggle orientation between portrait and landscape\n * @private\n */\n _toggleOrientation() {\n this._setParams('form', { isPortrait: !this._state.isPortrait })\n }\n\n /**\n * Toggle dark mode on/off\n * @private\n */\n _toggleDarkMode() {\n this._setParams('form', { isDarkMode: !this._state.isDarkMode })\n }\n\n /**\n * Reset to defaults: first valid palette, landscape orientation, light mode\n * @private\n */\n _resetToModelDefaults() {\n const model = this._state.model\n if (!model) return\n\n // Reset to first valid palette of current model\n const firstPaletteId = this._getFirstValidPaletteId(model)\n\n this._setParams('form', {\n paletteId: firstPaletteId,\n isPortrait: false,\n isDarkMode: false\n })\n }\n\n /**\n * Update reset button enabled/disabled state\n * Button is disabled only when palette, orientation, and dark mode are all at defaults\n * @private\n */\n _updateResetButton() {\n if (!this.elements.resetButton) return\n\n const model = this._state.model\n if (!model) return\n\n const firstValidPaletteId = this._getFirstValidPaletteId(model)\n const isPaletteDefault = this.elements.paletteSelect.value === String(firstValidPaletteId)\n const isOrientationDefault = this._state.isPortrait === false\n const isDarkModeDefault = this._state.isDarkMode === false\n\n const isAtDefaults = isPaletteDefault && isOrientationDefault && isDarkModeDefault\n\n this.elements.resetButton.disabled = isAtDefaults\n\n if (isAtDefaults) {\n this.elements.resetButton.classList.add('opacity-50', 'cursor-default')\n this.elements.resetButton.setAttribute('aria-disabled', 'true')\n } else {\n this.elements.resetButton.classList.remove('opacity-50', 'cursor-default')\n this.elements.resetButton.removeAttribute('aria-disabled')\n }\n }\n\n /**\n * Get CSS classes for the current picker configuration\n * @private\n * @returns {Array} Array of CSS class names for Framework CSS rendering\n *\n * Generated classes (in order):\n * 1. 'screen' - Base class (always present)\n * 2. palette.framework_class - From selected palette (e.g., 'screen--1bit')\n * 3. model.css.classes.device - From model API (e.g., 'screen--v2')\n * 4. model.css.classes.size - From model API (e.g., 'screen--md')\n * 5. 'screen--portrait' - Only when portrait orientation is enabled\n * 6. 'screen--1x' - Scale indicator (always 1x)\n * 7. 'screen--dark-mode' - Only when dark mode is enabled\n *\n * @example\n * const classes = picker.screenClasses\n * // ['screen', 'screen--1bit', 'screen--v2', 'screen--md', 'screen--1x']\n */\n get _screenClasses() {\n const model = this._state.model\n const palette = this._state.palette\n\n if (!model) {\n throw new Error('No model selected')\n }\n\n const classes = []\n\n // 0. Base screen class (always present)\n classes.push('screen')\n\n // 1. Palette framework class\n if (palette && palette.framework_class) {\n classes.push(palette.framework_class)\n }\n\n // 2. Model device class (from API)\n if (model.css && model.css.classes && model.css.classes.device) {\n classes.push(model.css.classes.device)\n }\n\n // 3. Model size class (from API)\n if (model.css && model.css.classes && model.css.classes.size) {\n classes.push(model.css.classes.size)\n }\n\n // 4. Orientation (UI state, only portrait - landscape is default)\n if (this._state.isPortrait) {\n classes.push('screen--portrait')\n }\n\n // 5. Scale (UI state, always 1x)\n classes.push('screen--1x')\n\n // 6. Dark mode (UI state, conditional)\n if (this._state.isDarkMode) {\n classes.push('screen--dark-mode')\n }\n\n return classes\n }\n\n /**\n * Get current picker parameters (serializable state)\n * @public\n * @returns {Object} Current parameters for persistence or API calls\n * @returns {string} return.modelName - Selected model name\n * @returns {string} return.paletteId - Selected palette ID\n * @returns {boolean} return.isPortrait - Portrait orientation flag\n * @returns {boolean} return.isDarkMode - Dark mode flag\n *\n * @example\n * const params = picker.params\n * // { modelName: 'og_plus', paletteId: '123', isPortrait: false, isDarkMode: false }\n *\n * // Can be used to restore state later\n * localStorage.setItem('picker-state', JSON.stringify(picker.params))\n */\n get params() {\n return {\n modelName: this._state.model?.name,\n paletteId: this._state.palette?.id,\n isPortrait: this._state.isPortrait,\n isDarkMode: this._state.isDarkMode\n }\n }\n\n /**\n * Update picker configuration programmatically\n * @public\n * @param {Object} params - Configuration object (all fields optional)\n * @param {string} [params.modelName] - Model name to select\n * @param {string} [params.paletteId] - Palette ID to select\n * @param {boolean} [params.isPortrait] - Portrait orientation\n * @param {boolean} [params.isDarkMode] - Dark mode enabled\n * @fires TRMNLPicker#trmnl:change\n * @throws {Error} If params is not an object\n *\n * @example\n * // Update single parameter\n * picker.setParams({ isDarkMode: true })\n *\n * // Update multiple parameters\n * picker.setParams({\n * modelName: 'og_plus',\n * paletteId: '123',\n * isPortrait: true\n * })\n *\n * // Note: Changing model resets palette to first valid palette of that model\n */\n setParams(params) {\n this._setParams('setParams', params)\n }\n\n /**\n * Internal method to update picker state with origin tracking\n * @private\n * @param {string} origin - Source of change ('constructor', 'form', 'setParams')\n * @param {Object} params - Parameters to update\n * @returns {boolean} True if any changes were made\n */\n _setParams(origin, params) {\n if (!params || typeof params !== 'object') {\n throw new Error('params must be an object')\n }\n\n let changed = false\n\n // Update model if provided\n if (params.modelName) {\n const model = this.models.find(m => m.name === params.modelName)\n if (model) {\n this.elements.modelSelect.value = model.name\n this._state.model = model\n this._populateModelPalettes()\n\n // Select first valid palette of new model\n const firstPaletteId = this._getFirstValidPaletteId(model)\n this.elements.paletteSelect.value = firstPaletteId\n this._state.palette = this.palettes.find(p => p.id === firstPaletteId)\n changed = true\n }\n }\n\n // Update palette if provided\n if (params.paletteId) {\n const palette = this.palettes.find(p => p.id === params.paletteId)\n if (palette) {\n this.elements.paletteSelect.value = palette.id\n this._state.palette = palette\n changed = true\n }\n }\n\n // Update orientation if provided\n if (typeof params.isPortrait === 'boolean') {\n this._state.isPortrait = params.isPortrait\n if (this.elements.orientationText) {\n this.elements.orientationText.textContent = this._state.isPortrait ? 'Portrait' : 'Landscape'\n }\n changed = true\n }\n\n // Update dark mode if provided\n if (typeof params.isDarkMode === 'boolean') {\n this._state.isDarkMode = params.isDarkMode\n if (this.elements.darkModeText) {\n this.elements.darkModeText.textContent = this._state.isDarkMode ? 'Dark Mode' : 'Light Mode'\n }\n changed = true\n }\n\n if (changed) {\n this._updateResetButton()\n }\n\n if (changed && origin) {\n this._emitChangeEvent(origin)\n }\n\n return changed\n }\n\n /**\n * Get complete picker state including full model and palette objects\n * @public\n * @returns {{\n * model: Object,\n * palette: Object,\n * isPortrait: boolean,\n * isDarkMode: boolean,\n * screenClasses: Array,\n * width: number,\n * height: number\n * }} State object containing model (full model object from API), palette (full palette object from API), isPortrait flag, and isDarkMode flag\n *\n * @example\n * const state = picker.state\n * // {\n * // model: { name: 'og_plus', label: 'OG+', width: 800, height: 480, ... },\n * // palette: { id: '123', name: 'Black', framework_class: 'screen--1bit', ... },\n * // isPortrait: false,\n * // isDarkMode: false,\n * // screenClasses: ['screen', 'screen--1bit', 'screen--v2', 'screen--md', 'screen--1x'],\n * // width: 800,\n * // height: 480\n * // }\n */\n get state() {\n return {\n screenClasses: this._screenClasses,\n ...this._dimensions,\n ...this._state,\n };\n }\n\n /**\n * Get current dimensions (width and height) of the screen in pixels\n * @private\n * @returns {{ width: number, height: number }} Object with width and height properties\n */\n get _dimensions() {\n const model = this._state.model\n\n let width = model.width / model.scale_factor\n let height = model.height / model.scale_factor\n\n if (this._state.isPortrait) {\n [width, height] = [height, width]\n }\n\n return { width, height }\n }\n\n /**\n * Clean up event listeners and references\n * @public\n */\n destroy() {\n // Remove event listeners\n this.elements.modelSelect.removeEventListener('change', this.handlers.modelChange)\n this.elements.paletteSelect.removeEventListener('change', this.handlers.paletteChange)\n\n if (this.elements.orientationToggle) {\n this.elements.orientationToggle.removeEventListener('click', this.handlers.orientationToggle)\n }\n\n if (this.elements.darkModeToggle) {\n this.elements.darkModeToggle.removeEventListener('click', this.handlers.darkModeToggle)\n }\n\n if (this.elements.resetButton) {\n this.elements.resetButton.removeEventListener('click', this.handlers.reset)\n }\n\n // Clear references\n this.formElement = null\n this.elements = null\n this.handlers = null\n this.models = null\n this.palettes = null\n this._state = null\n }\n}\n\nexport default TRMNLPicker\n"], "mappings": "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", "names": [] } diff --git a/dist/trmnl-picker.min.js b/dist/trmnl-picker.min.js index 403cfef..a373029 100644 --- a/dist/trmnl-picker.min.js +++ b/dist/trmnl-picker.min.js @@ -1,3 +1,3 @@ -var TRMNLPicker=(()=>{var m=Object.defineProperty;var _=Object.getOwnPropertyDescriptor;var k=Object.getOwnPropertyNames;var M=Object.prototype.hasOwnProperty;var E=(r,e)=>{for(var s in e)m(r,s,{get:e[s],enumerable:!0})},P=(r,e,s,t)=>{if(e&&typeof e=="object"||typeof e=="function")for(let a of k(e))!M.call(r,a)&&a!==s&&m(r,a,{get:()=>e[a],enumerable:!(t=_(e,a))||t.enumerable});return r};var w=r=>P(m({},"__esModule",{value:!0}),r);var v={};E(v,{default:()=>T});var S="og_plus",f="trmnl-picker-api-cache";var u=class r{static API_BASE_URL="https://usetrmnl.com";static _getCachedApiData(){try{let e=localStorage.getItem(f);if(!e)return null;let{timestamp:s,models:t,palettes:a}=JSON.parse(e);return Date.now()-s>864e5?(localStorage.removeItem(f),null):{models:t,palettes:a}}catch(e){return console.warn("TRMNLPicker: Failed to read API cache:",e),null}}static _setCachedApiData(e,s){try{let t={timestamp:Date.now(),models:e,palettes:s};localStorage.setItem(f,JSON.stringify(t))}catch(t){console.warn("TRMNLPicker: Failed to save API cache:",t)}}static async create(e,s={}){let{models:t,palettes:a,localStorageKey:o}=s;if(!t&&!a){let l=r._getCachedApiData();l&&(t=l.models,a=l.palettes)}if(!t)try{let l=await fetch(`${r.API_BASE_URL}/api/models`);if(!l.ok)throw new Error(`Failed to fetch models: ${l.status} ${l.statusText}`);let h=await l.json();t=h.data||h}catch(l){throw new Error(`TRMNLPicker: Failed to fetch models from API: ${l.message}`)}if(!a)try{let l=await fetch(`${r.API_BASE_URL}/api/palettes`);if(!l.ok)throw new Error(`Failed to fetch palettes: ${l.status} ${l.statusText}`);let h=await l.json();a=h.data||h}catch(l){throw new Error(`TRMNLPicker: Failed to fetch palettes from API: ${l.message}`)}return!s.models&&!s.palettes&&r._setCachedApiData(t,a),new r(e,{models:t,palettes:a,localStorageKey:o})}constructor(e,s={}){if(!e)throw new Error("TRMNLPicker: formIdOrElement is required");if(typeof e=="string"){if(this.formElement=document.getElementById(e),!this.formElement)throw new Error(`TRMNLPicker: Form element with id "${e}" not found`)}else if(e instanceof Element)this.formElement=e;else throw new Error("TRMNLPicker: formIdOrElement must be a string ID or DOM element");let{models:t,palettes:a,localStorageKey:o}=s;if(this.models=t,this.palettes=a,this.localStorageKey=o,this.models&&this.palettes){if(!Array.isArray(this.models)||this.models.length===0)throw new Error("TRMNLPicker: models must be a non-empty array");if(!Array.isArray(this.palettes)||this.palettes.length===0)throw new Error("TRMNLPicker: palettes must be a non-empty array");if(this.models=this._filterValidModels(),this.models.length===0)throw new Error("TRMNLPicker: no valid models found (all models have palettes with empty framework_class)");this._initializeElements(),this._bindEvents(),this._setInitialState()}}_filterValidModels(){return this.models.filter(e=>e.palette_ids.some(s=>{let t=this.palettes.find(a=>a.id===s);return t&&t.framework_class&&t.framework_class.trim()!==""}))}_getFirstValidPaletteId(e){if(!e)return null;for(let s of e.palette_ids){let t=this.palettes.find(a=>a.id===s);if(t&&t.framework_class&&t.framework_class.trim()!=="")return s}return null}_initializeElements(){this.elements={modelSelect:this.formElement.querySelector("[data-model-select]"),paletteSelect:this.formElement.querySelector("[data-palette-select]"),orientationToggle:this.formElement.querySelector("[data-orientation-toggle]"),darkModeToggle:this.formElement.querySelector("[data-dark-mode-toggle]"),resetButton:this.formElement.querySelector("[data-reset-button]"),orientationText:this.formElement.querySelector("[data-orientation-text]"),darkModeText:this.formElement.querySelector("[data-dark-mode-text]")};let e=["modelSelect","paletteSelect"];for(let s of e)if(!this.elements[s])throw new Error(`TRMNLPicker: Required element "${s}" not found in form`)}_bindEvents(){this.handlers={modelChange:this._handleModelChange.bind(this),paletteChange:this._handlePaletteChange.bind(this),orientationToggle:this._toggleOrientation.bind(this),darkModeToggle:this._toggleDarkMode.bind(this),reset:this._resetToModelDefaults.bind(this)},this.elements.modelSelect.addEventListener("change",this.handlers.modelChange),this.elements.paletteSelect.addEventListener("change",this.handlers.paletteChange),this.elements.orientationToggle&&this.elements.orientationToggle.addEventListener("click",this.handlers.orientationToggle),this.elements.darkModeToggle&&this.elements.darkModeToggle.addEventListener("click",this.handlers.darkModeToggle),this.elements.resetButton&&this.elements.resetButton.addEventListener("click",this.handlers.reset)}_setInitialState(){let e=this.models.filter(i=>i.kind==="trmnl"),s=this.models.filter(i=>i.kind!=="trmnl"),t=[...e].sort((i,n)=>{let d=(i.label||i.name).toLowerCase(),c=(n.label||n.name).toLowerCase();return d.localeCompare(c)}),a=[...s].sort((i,n)=>{let d=(i.label||i.name).toLowerCase(),c=(n.label||n.name).toLowerCase();return d.localeCompare(c)});if(this.elements.modelSelect.innerHTML="",t.length>0){let i=document.createElement("optgroup");i.label="TRMNL",t.forEach(n=>{let d=document.createElement("option");d.value=n.name,d.textContent=n.label||n.name,i.appendChild(d)}),this.elements.modelSelect.appendChild(i)}if(a.length>0){let i=document.createElement("optgroup");i.label="BYOD",a.forEach(n=>{let d=document.createElement("option");d.value=n.name,d.textContent=n.label||n.name,i.appendChild(d)}),this.elements.modelSelect.appendChild(i)}let o=[...t,...a];this._state={};let l=o.find(i=>i.name===S)||o[0],h=this._getFirstValidPaletteId(l),g={modelName:l.name,paletteId:h,isPortrait:!1,isDarkMode:!1},p=this._loadFromLocalStorage()||{};this._setParams("constructor",{...g,...p})}_populateModelPalettes(){let e=this.elements.modelSelect.value,s=this.models.find(t=>t.name===e);s&&(this.elements.paletteSelect.innerHTML="",s.palette_ids.forEach(t=>{let a=this.palettes.find(o=>o.id===t);if(a&&a.framework_class&&a.framework_class.trim()!==""){let o=document.createElement("option");o.value=a.id,o.textContent=a.name,this.elements.paletteSelect.appendChild(o)}}))}_emitChangeEvent(e){this._saveToLocalStorage();let s=new CustomEvent("trmnl:change",{detail:{origin:e,...this.state},bubbles:!0});this.formElement.dispatchEvent(s)}_loadFromLocalStorage(){if(!this.localStorageKey)return null;try{let e=localStorage.getItem(this.localStorageKey);if(e)return JSON.parse(e)}catch(e){console.warn("TRMNLPicker: Failed to load from localStorage:",e)}return null}_saveToLocalStorage(){if(this.localStorageKey)try{localStorage.setItem(this.localStorageKey,JSON.stringify(this.params))}catch(e){console.warn("TRMNLPicker: Failed to save to localStorage:",e)}}_handleModelChange(e){this._setParams("form",{modelName:e.target.value})}_handlePaletteChange(e){this._setParams("form",{paletteId:e.target.value})}_toggleOrientation(){this._setParams("form",{isPortrait:!this._state.isPortrait})}_toggleDarkMode(){this._setParams("form",{isDarkMode:!this._state.isDarkMode})}_resetToModelDefaults(){let e=this._state.model;if(!e)return;let s=this._getFirstValidPaletteId(e);this._setParams("form",{paletteId:s,isPortrait:!1,isDarkMode:!1})}_updateResetButton(){if(!this.elements.resetButton)return;let e=this._state.model;if(!e)return;let s=this._getFirstValidPaletteId(e),t=this.elements.paletteSelect.value===String(s),a=this._state.isPortrait===!1,o=this._state.isDarkMode===!1,l=t&&a&&o;this.elements.resetButton.disabled=l,l?(this.elements.resetButton.classList.add("opacity-50","cursor-default"),this.elements.resetButton.setAttribute("aria-disabled","true")):(this.elements.resetButton.classList.remove("opacity-50","cursor-default"),this.elements.resetButton.removeAttribute("aria-disabled"))}get _screenClasses(){let e=this._state.model,s=this._state.palette;if(!e)throw new Error("No model selected");let t=[];return t.push("screen"),s&&s.framework_class&&t.push(s.framework_class),e.css&&e.css.classes&&e.css.classes.device&&t.push(e.css.classes.device),e.css&&e.css.classes&&e.css.classes.size&&t.push(e.css.classes.size),this._state.isPortrait&&t.push("screen--portrait"),t.push("screen--1x"),this._state.isDarkMode&&t.push("screen--dark-mode"),t}get params(){return{modelName:this._state.model?.name,paletteId:this._state.palette?.id,isPortrait:this._state.isPortrait,isDarkMode:this._state.isDarkMode}}setParams(e){this._setParams("setParams",e)}_setParams(e,s){if(!s||typeof s!="object")throw new Error("params must be an object");let t=!1;if(s.modelName){let a=this.models.find(o=>o.name===s.modelName);if(a){this.elements.modelSelect.value=a.name,this._state.model=a,this._populateModelPalettes();let o=this._getFirstValidPaletteId(a);this.elements.paletteSelect.value=o,this._state.palette=this.palettes.find(l=>l.id===o),t=!0}}if(s.paletteId){let a=this.palettes.find(o=>o.id===s.paletteId);a&&(this.elements.paletteSelect.value=a.id,this._state.palette=a,t=!0)}return typeof s.isPortrait=="boolean"&&(this._state.isPortrait=s.isPortrait,this.elements.orientationText&&(this.elements.orientationText.textContent=this._state.isPortrait?"Portrait":"Landscape"),t=!0),typeof s.isDarkMode=="boolean"&&(this._state.isDarkMode=s.isDarkMode,this.elements.darkModeText&&(this.elements.darkModeText.textContent=this._state.isDarkMode?"Dark Mode":"Light Mode"),t=!0),t&&this._updateResetButton(),t&&e&&this._emitChangeEvent(e),t}get state(){return{screenClasses:this._screenClasses,...this._dimensions,...this._state}}get _dimensions(){let e=this._state.model,s=e.width/e.scale_factor,t=e.height/e.scale_factor;return this._state.isPortrait&&([s,t]=[t,s]),{width:s,height:t}}destroy(){this.elements.modelSelect.removeEventListener("change",this.handlers.modelChange),this.elements.paletteSelect.removeEventListener("change",this.handlers.paletteChange),this.elements.orientationToggle&&this.elements.orientationToggle.removeEventListener("click",this.handlers.orientationToggle),this.elements.darkModeToggle&&this.elements.darkModeToggle.removeEventListener("click",this.handlers.darkModeToggle),this.elements.resetButton&&this.elements.resetButton.removeEventListener("click",this.handlers.reset),this.formElement=null,this.elements=null,this.handlers=null,this.models=null,this.palettes=null,this._state=null}},T=u;return w(v);})(); +var TRMNLPicker=(()=>{var m=Object.defineProperty;var _=Object.getOwnPropertyDescriptor;var k=Object.getOwnPropertyNames;var M=Object.prototype.hasOwnProperty;var E=(r,e)=>{for(var s in e)m(r,s,{get:e[s],enumerable:!0})},P=(r,e,s,t)=>{if(e&&typeof e=="object"||typeof e=="function")for(let a of k(e))!M.call(r,a)&&a!==s&&m(r,a,{get:()=>e[a],enumerable:!(t=_(e,a))||t.enumerable});return r};var w=r=>P(m({},"__esModule",{value:!0}),r);var v={};E(v,{default:()=>T});var S="og_plus",f="trmnl-picker-api-cache";var u=class r{static API_BASE_URL="https://trmnl.com";static _getCachedApiData(){try{let e=localStorage.getItem(f);if(!e)return null;let{timestamp:s,models:t,palettes:a}=JSON.parse(e);return Date.now()-s>864e5?(localStorage.removeItem(f),null):{models:t,palettes:a}}catch(e){return console.warn("TRMNLPicker: Failed to read API cache:",e),null}}static _setCachedApiData(e,s){try{let t={timestamp:Date.now(),models:e,palettes:s};localStorage.setItem(f,JSON.stringify(t))}catch(t){console.warn("TRMNLPicker: Failed to save API cache:",t)}}static async create(e,s={}){let{models:t,palettes:a,localStorageKey:o}=s;if(!t&&!a){let l=r._getCachedApiData();l&&(t=l.models,a=l.palettes)}if(!t)try{let l=await fetch(`${r.API_BASE_URL}/api/models`);if(!l.ok)throw new Error(`Failed to fetch models: ${l.status} ${l.statusText}`);let h=await l.json();t=h.data||h}catch(l){throw new Error(`TRMNLPicker: Failed to fetch models from API: ${l.message}`)}if(!a)try{let l=await fetch(`${r.API_BASE_URL}/api/palettes`);if(!l.ok)throw new Error(`Failed to fetch palettes: ${l.status} ${l.statusText}`);let h=await l.json();a=h.data||h}catch(l){throw new Error(`TRMNLPicker: Failed to fetch palettes from API: ${l.message}`)}return!s.models&&!s.palettes&&r._setCachedApiData(t,a),new r(e,{models:t,palettes:a,localStorageKey:o})}constructor(e,s={}){if(!e)throw new Error("TRMNLPicker: formIdOrElement is required");if(typeof e=="string"){if(this.formElement=document.getElementById(e),!this.formElement)throw new Error(`TRMNLPicker: Form element with id "${e}" not found`)}else if(e instanceof Element)this.formElement=e;else throw new Error("TRMNLPicker: formIdOrElement must be a string ID or DOM element");let{models:t,palettes:a,localStorageKey:o}=s;if(this.models=t,this.palettes=a,this.localStorageKey=o,this.models&&this.palettes){if(!Array.isArray(this.models)||this.models.length===0)throw new Error("TRMNLPicker: models must be a non-empty array");if(!Array.isArray(this.palettes)||this.palettes.length===0)throw new Error("TRMNLPicker: palettes must be a non-empty array");if(this.models=this._filterValidModels(),this.models.length===0)throw new Error("TRMNLPicker: no valid models found (all models have palettes with empty framework_class)");this._initializeElements(),this._bindEvents(),this._setInitialState()}}_filterValidModels(){return this.models.filter(e=>e.palette_ids.some(s=>{let t=this.palettes.find(a=>a.id===s);return t&&t.framework_class&&t.framework_class.trim()!==""}))}_getFirstValidPaletteId(e){if(!e)return null;for(let s of e.palette_ids){let t=this.palettes.find(a=>a.id===s);if(t&&t.framework_class&&t.framework_class.trim()!=="")return s}return null}_initializeElements(){this.elements={modelSelect:this.formElement.querySelector("[data-model-select]"),paletteSelect:this.formElement.querySelector("[data-palette-select]"),orientationToggle:this.formElement.querySelector("[data-orientation-toggle]"),darkModeToggle:this.formElement.querySelector("[data-dark-mode-toggle]"),resetButton:this.formElement.querySelector("[data-reset-button]"),orientationText:this.formElement.querySelector("[data-orientation-text]"),darkModeText:this.formElement.querySelector("[data-dark-mode-text]")};let e=["modelSelect","paletteSelect"];for(let s of e)if(!this.elements[s])throw new Error(`TRMNLPicker: Required element "${s}" not found in form`)}_bindEvents(){this.handlers={modelChange:this._handleModelChange.bind(this),paletteChange:this._handlePaletteChange.bind(this),orientationToggle:this._toggleOrientation.bind(this),darkModeToggle:this._toggleDarkMode.bind(this),reset:this._resetToModelDefaults.bind(this)},this.elements.modelSelect.addEventListener("change",this.handlers.modelChange),this.elements.paletteSelect.addEventListener("change",this.handlers.paletteChange),this.elements.orientationToggle&&this.elements.orientationToggle.addEventListener("click",this.handlers.orientationToggle),this.elements.darkModeToggle&&this.elements.darkModeToggle.addEventListener("click",this.handlers.darkModeToggle),this.elements.resetButton&&this.elements.resetButton.addEventListener("click",this.handlers.reset)}_setInitialState(){let e=this.models.filter(i=>i.kind==="trmnl"),s=this.models.filter(i=>i.kind!=="trmnl"),t=[...e].sort((i,n)=>{let d=(i.label||i.name).toLowerCase(),c=(n.label||n.name).toLowerCase();return d.localeCompare(c)}),a=[...s].sort((i,n)=>{let d=(i.label||i.name).toLowerCase(),c=(n.label||n.name).toLowerCase();return d.localeCompare(c)});if(this.elements.modelSelect.innerHTML="",t.length>0){let i=document.createElement("optgroup");i.label="TRMNL",t.forEach(n=>{let d=document.createElement("option");d.value=n.name,d.textContent=n.label||n.name,i.appendChild(d)}),this.elements.modelSelect.appendChild(i)}if(a.length>0){let i=document.createElement("optgroup");i.label="BYOD",a.forEach(n=>{let d=document.createElement("option");d.value=n.name,d.textContent=n.label||n.name,i.appendChild(d)}),this.elements.modelSelect.appendChild(i)}let o=[...t,...a];this._state={};let l=o.find(i=>i.name===S)||o[0],h=this._getFirstValidPaletteId(l),g={modelName:l.name,paletteId:h,isPortrait:!1,isDarkMode:!1},p=this._loadFromLocalStorage()||{};this._setParams("constructor",{...g,...p})}_populateModelPalettes(){let e=this.elements.modelSelect.value,s=this.models.find(t=>t.name===e);s&&(this.elements.paletteSelect.innerHTML="",s.palette_ids.forEach(t=>{let a=this.palettes.find(o=>o.id===t);if(a&&a.framework_class&&a.framework_class.trim()!==""){let o=document.createElement("option");o.value=a.id,o.textContent=a.name,this.elements.paletteSelect.appendChild(o)}}))}_emitChangeEvent(e){this._saveToLocalStorage();let s=new CustomEvent("trmnl:change",{detail:{origin:e,...this.state},bubbles:!0});this.formElement.dispatchEvent(s)}_loadFromLocalStorage(){if(!this.localStorageKey)return null;try{let e=localStorage.getItem(this.localStorageKey);if(e)return JSON.parse(e)}catch(e){console.warn("TRMNLPicker: Failed to load from localStorage:",e)}return null}_saveToLocalStorage(){if(this.localStorageKey)try{localStorage.setItem(this.localStorageKey,JSON.stringify(this.params))}catch(e){console.warn("TRMNLPicker: Failed to save to localStorage:",e)}}_handleModelChange(e){this._setParams("form",{modelName:e.target.value})}_handlePaletteChange(e){this._setParams("form",{paletteId:e.target.value})}_toggleOrientation(){this._setParams("form",{isPortrait:!this._state.isPortrait})}_toggleDarkMode(){this._setParams("form",{isDarkMode:!this._state.isDarkMode})}_resetToModelDefaults(){let e=this._state.model;if(!e)return;let s=this._getFirstValidPaletteId(e);this._setParams("form",{paletteId:s,isPortrait:!1,isDarkMode:!1})}_updateResetButton(){if(!this.elements.resetButton)return;let e=this._state.model;if(!e)return;let s=this._getFirstValidPaletteId(e),t=this.elements.paletteSelect.value===String(s),a=this._state.isPortrait===!1,o=this._state.isDarkMode===!1,l=t&&a&&o;this.elements.resetButton.disabled=l,l?(this.elements.resetButton.classList.add("opacity-50","cursor-default"),this.elements.resetButton.setAttribute("aria-disabled","true")):(this.elements.resetButton.classList.remove("opacity-50","cursor-default"),this.elements.resetButton.removeAttribute("aria-disabled"))}get _screenClasses(){let e=this._state.model,s=this._state.palette;if(!e)throw new Error("No model selected");let t=[];return t.push("screen"),s&&s.framework_class&&t.push(s.framework_class),e.css&&e.css.classes&&e.css.classes.device&&t.push(e.css.classes.device),e.css&&e.css.classes&&e.css.classes.size&&t.push(e.css.classes.size),this._state.isPortrait&&t.push("screen--portrait"),t.push("screen--1x"),this._state.isDarkMode&&t.push("screen--dark-mode"),t}get params(){return{modelName:this._state.model?.name,paletteId:this._state.palette?.id,isPortrait:this._state.isPortrait,isDarkMode:this._state.isDarkMode}}setParams(e){this._setParams("setParams",e)}_setParams(e,s){if(!s||typeof s!="object")throw new Error("params must be an object");let t=!1;if(s.modelName){let a=this.models.find(o=>o.name===s.modelName);if(a){this.elements.modelSelect.value=a.name,this._state.model=a,this._populateModelPalettes();let o=this._getFirstValidPaletteId(a);this.elements.paletteSelect.value=o,this._state.palette=this.palettes.find(l=>l.id===o),t=!0}}if(s.paletteId){let a=this.palettes.find(o=>o.id===s.paletteId);a&&(this.elements.paletteSelect.value=a.id,this._state.palette=a,t=!0)}return typeof s.isPortrait=="boolean"&&(this._state.isPortrait=s.isPortrait,this.elements.orientationText&&(this.elements.orientationText.textContent=this._state.isPortrait?"Portrait":"Landscape"),t=!0),typeof s.isDarkMode=="boolean"&&(this._state.isDarkMode=s.isDarkMode,this.elements.darkModeText&&(this.elements.darkModeText.textContent=this._state.isDarkMode?"Dark Mode":"Light Mode"),t=!0),t&&this._updateResetButton(),t&&e&&this._emitChangeEvent(e),t}get state(){return{screenClasses:this._screenClasses,...this._dimensions,...this._state}}get _dimensions(){let e=this._state.model,s=e.width/e.scale_factor,t=e.height/e.scale_factor;return this._state.isPortrait&&([s,t]=[t,s]),{width:s,height:t}}destroy(){this.elements.modelSelect.removeEventListener("change",this.handlers.modelChange),this.elements.paletteSelect.removeEventListener("change",this.handlers.paletteChange),this.elements.orientationToggle&&this.elements.orientationToggle.removeEventListener("click",this.handlers.orientationToggle),this.elements.darkModeToggle&&this.elements.darkModeToggle.removeEventListener("click",this.handlers.darkModeToggle),this.elements.resetButton&&this.elements.resetButton.removeEventListener("click",this.handlers.reset),this.formElement=null,this.elements=null,this.handlers=null,this.models=null,this.palettes=null,this._state=null}},T=u;return w(v);})(); TRMNLPicker=TRMNLPicker.default; //# sourceMappingURL=trmnl-picker.min.js.map diff --git a/dist/trmnl-picker.min.js.map b/dist/trmnl-picker.min.js.map index 5706a83..2a5af09 100644 --- a/dist/trmnl-picker.min.js.map +++ b/dist/trmnl-picker.min.js.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["../src/index.js"], - "sourcesContent": ["/**\n * Default model to select when no localStorage state is found\n * @private\n * @constant {string}\n */\nconst _DEFAULT_MODEL_NAME = 'og_plus'\n\n/**\n * localStorage key for caching API responses (models and palettes)\n * @private\n * @constant {string}\n */\nconst _API_CACHE_KEY = 'trmnl-picker-api-cache'\n\n/**\n * Cache TTL in milliseconds (1 day)\n * @private\n * @constant {number}\n */\nconst _CACHE_TTL_MS = 24 * 60 * 60 * 1000\n\n/**\n * Event fired when picker state changes\n * @event TRMNLPicker#trmnl:change\n * @type {CustomEvent}\n * @property {Object} detail - Event details\n * @property {string} detail.origin - What triggered the change: 'constructor', 'form', or 'setParams'\n * @property {Array} detail.screenClasses - Array of CSS classes for Framework CSS rendering\n * @property {Object} detail.model - Current model object with name, label, width, height, kind, css properties\n * @property {Object} detail.palette - Current palette object with id, name, framework_class properties\n * @property {boolean} detail.isPortrait - Portrait orientation flag\n * @property {boolean} detail.isDarkMode - Dark mode flag\n * @property {number} detail.width - Current width of the screen in pixels\n * @property {number} detail.height - Current height of the screen in pixels\n *\n * @example\n * picker.formElement.addEventListener('trmnl:change', (event) => {\n * const { origin, screenClasses, model, palette } = event.detail\n * console.log(`Changed via ${origin}`)\n * console.log('Classes:', screenClasses)\n * })\n */\n\n/**\n * TRMNLPicker - Vanilla JS library for TRMNL device and palette selection\n *\n * Provides a reactive picker component that manages device models, color palettes,\n * orientation, and display mode. Emits 'trmnl:change' events with current state\n * and CSS classes for rendering.\n * \n * **[View a live demo \u2192](https://usetrmnl.github.io/trmnl-picker/example/)**\n *\n * **Note:** Using the constructor directly is not recommended. Use the static\n * {@link TRMNLPicker.create} method instead, which automatically fetches\n * models and palettes from the TRMNL API if not provided.\n *\n * @class TRMNLPicker\n * @param {string|Element} formIdOrElement - Form element ID or DOM element reference\n * @param {Object} options - Configuration options\n * @param {Array} options.models - Array of model objects from TRMNL API\n * @param {Array} options.palettes - Array of palette objects from TRMNL API\n * @param {string} [options.localStorageKey] - Optional key for persisting state to localStorage\n *\n * @fires TRMNLPicker#trmnl:change\n *\n * @example\n * // HTML Structure - Required form with data-* attributes\n * //
\n * // \n * // \n * //\n * // \n * // \n * //\n * // \n * // \n * //\n * // \n * // \n * //\n * // \n * // \n * //
\n *\n * // Create with element ID\n * const picker = new TRMNLPicker('picker-form', { models, palettes })\n *\n * // Create with DOM element\n * const element = document.getElementById('picker-form')\n * const picker = new TRMNLPicker(element, { models, palettes })\n *\n * // Listen for changes\n * picker.formElement.addEventListener('trmnl:change', (event) => {\n * console.log(event.detail.screenClasses)\n * })\n */\nclass TRMNLPicker {\n static API_BASE_URL = 'https://usetrmnl.com'\n\n /**\n * Get cached API response from localStorage\n * @private\n * @static\n * @returns {{models: Array, palettes: Array} | null} Cached data or null if expired/missing\n */\n static _getCachedApiData() {\n try {\n const cached = localStorage.getItem(_API_CACHE_KEY)\n if (!cached) return null\n\n const { timestamp, models, palettes } = JSON.parse(cached)\n const now = Date.now()\n\n // Check if cache is still valid\n if (now - timestamp > _CACHE_TTL_MS) {\n localStorage.removeItem(_API_CACHE_KEY)\n return null\n }\n\n return { models, palettes }\n } catch (error) {\n console.warn('TRMNLPicker: Failed to read API cache:', error)\n return null\n }\n }\n\n /**\n * Save API response to localStorage cache\n * @private\n * @static\n * @param {Array} models - Models array\n * @param {Array} palettes - Palettes array\n */\n static _setCachedApiData(models, palettes) {\n try {\n const cacheData = {\n timestamp: Date.now(),\n models,\n palettes\n }\n localStorage.setItem(_API_CACHE_KEY, JSON.stringify(cacheData))\n } catch (error) {\n console.warn('TRMNLPicker: Failed to save API cache:', error)\n }\n }\n\n /**\n * Create a TRMNLPicker instance, fetching data from TRMNL API if not provided\n *\n * Automatically caches API responses in localStorage for 24 hours to reduce network requests.\n *\n * @static\n * @param {string|Element} formIdOrElement - Form element ID or DOM element\n * @param {Object} options - Configuration options\n * @param {Array} [options.models] - Optional models array (fetched from API if not provided)\n * @param {Array} [options.palettes] - Optional palettes array (fetched from API if not provided)\n * @param {string} [options.localStorageKey] - Optional key for state persistence\n * @returns {Promise} Promise resolving to picker instance\n * @throws {Error} If API fetch fails when models or palettes are not provided\n *\n * @example\n * // Fetch models and palettes from API (or use cached data if available)\n * const picker = await TRMNLPicker.create('screen-picker')\n *\n * // Provide your own data\n * const picker = await TRMNLPicker.create('screen-picker', { models, palettes })\n */\n static async create(formId, options = {}) {\n let { models, palettes, localStorageKey } = options\n\n // Try to use cached data if neither models nor palettes are provided\n if (!models && !palettes) {\n const cached = TRMNLPicker._getCachedApiData()\n if (cached) {\n models = cached.models\n palettes = cached.palettes\n }\n }\n\n // Fetch models if not provided and not in cache\n if (!models) {\n try {\n const response = await fetch(`${TRMNLPicker.API_BASE_URL}/api/models`)\n if (!response.ok) {\n throw new Error(`Failed to fetch models: ${response.status} ${response.statusText}`)\n }\n const data = await response.json()\n models = data.data || data\n } catch (error) {\n throw new Error(`TRMNLPicker: Failed to fetch models from API: ${error.message}`)\n }\n }\n\n // Fetch palettes if not provided and not in cache\n if (!palettes) {\n try {\n const response = await fetch(`${TRMNLPicker.API_BASE_URL}/api/palettes`)\n if (!response.ok) {\n throw new Error(`Failed to fetch palettes: ${response.status} ${response.statusText}`)\n }\n const data = await response.json()\n palettes = data.data || data\n } catch (error) {\n throw new Error(`TRMNLPicker: Failed to fetch palettes from API: ${error.message}`)\n }\n }\n\n // Cache the API data for future use (if both were fetched)\n if (!options.models && !options.palettes) {\n TRMNLPicker._setCachedApiData(models, palettes)\n }\n\n return new TRMNLPicker(formId, { models, palettes, localStorageKey })\n }\n\n constructor(formIdOrElement, options = {}) {\n // Validate inputs\n if (!formIdOrElement) {\n throw new Error('TRMNLPicker: formIdOrElement is required')\n }\n\n // Store references - accept either string ID or DOM element\n if (typeof formIdOrElement === 'string') {\n this.formElement = document.getElementById(formIdOrElement)\n if (!this.formElement) {\n throw new Error(`TRMNLPicker: Form element with id \"${formIdOrElement}\" not found`)\n }\n } else if (formIdOrElement instanceof Element) {\n this.formElement = formIdOrElement\n } else {\n throw new Error('TRMNLPicker: formIdOrElement must be a string ID or DOM element')\n }\n\n const { models, palettes, localStorageKey } = options\n\n this.models = models\n this.palettes = palettes\n this.localStorageKey = localStorageKey\n\n // Only initialize if we have data\n if (this.models && this.palettes) {\n if (!Array.isArray(this.models) || this.models.length === 0) {\n throw new Error('TRMNLPicker: models must be a non-empty array')\n }\n\n if (!Array.isArray(this.palettes) || this.palettes.length === 0) {\n throw new Error('TRMNLPicker: palettes must be a non-empty array')\n }\n\n // Filter out models where all palettes have empty framework_class\n this.models = this._filterValidModels()\n\n if (this.models.length === 0) {\n throw new Error('TRMNLPicker: no valid models found (all models have palettes with empty framework_class)')\n }\n\n // Initialize DOM elements and bind events\n this._initializeElements()\n this._bindEvents()\n\n // Set initial state (will load from localStorage if available)\n this._setInitialState()\n }\n }\n\n /**\n * Filter out models where all their palettes have empty framework_class\n * @private\n * @returns {Array} Filtered models array\n */\n _filterValidModels() {\n return this.models.filter(model => {\n // Check if at least one palette for this model has a non-empty framework_class\n return model.palette_ids.some(paletteId => {\n const palette = this.palettes.find(p => p.id === paletteId)\n return palette && palette.framework_class && palette.framework_class.trim() !== ''\n })\n })\n }\n\n /**\n * Get the first valid palette ID for a model (one with non-empty framework_class)\n * @private\n * @param {Object} model - Model object\n * @returns {string|null} First valid palette ID or null\n */\n _getFirstValidPaletteId(model) {\n if (!model) return null\n\n for (const paletteId of model.palette_ids) {\n const palette = this.palettes.find(p => p.id === paletteId)\n if (palette && palette.framework_class && palette.framework_class.trim() !== '') {\n return paletteId\n }\n }\n\n return null\n }\n\n /**\n * Find and store references to form elements using data-* attributes\n * @private\n */\n _initializeElements() {\n this.elements = {\n modelSelect: this.formElement.querySelector('[data-model-select]'),\n paletteSelect: this.formElement.querySelector('[data-palette-select]'),\n orientationToggle: this.formElement.querySelector('[data-orientation-toggle]'),\n darkModeToggle: this.formElement.querySelector('[data-dark-mode-toggle]'),\n resetButton: this.formElement.querySelector('[data-reset-button]'),\n\n // Optional: UI indicator elements\n orientationText: this.formElement.querySelector('[data-orientation-text]'),\n darkModeText: this.formElement.querySelector('[data-dark-mode-text]')\n }\n\n // Validate required elements\n const required = ['modelSelect', 'paletteSelect']\n for (const key of required) {\n if (!this.elements[key]) {\n throw new Error(`TRMNLPicker: Required element \"${key}\" not found in form`)\n }\n }\n }\n\n /**\n * Bind event listeners to form elements\n * @private\n */\n _bindEvents() {\n // Store bound handlers for cleanup\n this.handlers = {\n modelChange: this._handleModelChange.bind(this),\n paletteChange: this._handlePaletteChange.bind(this),\n orientationToggle: this._toggleOrientation.bind(this),\n darkModeToggle: this._toggleDarkMode.bind(this),\n reset: this._resetToModelDefaults.bind(this)\n }\n\n // Attach event listeners\n this.elements.modelSelect.addEventListener('change', this.handlers.modelChange)\n this.elements.paletteSelect.addEventListener('change', this.handlers.paletteChange)\n\n if (this.elements.orientationToggle) {\n this.elements.orientationToggle.addEventListener('click', this.handlers.orientationToggle)\n }\n\n if (this.elements.darkModeToggle) {\n this.elements.darkModeToggle.addEventListener('click', this.handlers.darkModeToggle)\n }\n\n if (this.elements.resetButton) {\n this.elements.resetButton.addEventListener('click', this.handlers.reset)\n }\n }\n\n /**\n * Set initial state and populate form\n * @private\n */\n _setInitialState() {\n // Separate models into TRMNL and BYOD groups\n const trmnlModels = this.models.filter(m => m.kind === 'trmnl')\n const byodModels = this.models.filter(m => m.kind !== 'trmnl')\n\n // Sort each group alphabetically by label\n const sortTRMNL = [...trmnlModels].sort((a, b) => {\n const labelA = (a.label || a.name).toLowerCase()\n const labelB = (b.label || b.name).toLowerCase()\n return labelA.localeCompare(labelB)\n })\n\n const sortBYOD = [...byodModels].sort((a, b) => {\n const labelA = (a.label || a.name).toLowerCase()\n const labelB = (b.label || b.name).toLowerCase()\n return labelA.localeCompare(labelB)\n })\n\n // Populate model select with grouped models\n this.elements.modelSelect.innerHTML = ''\n\n // Add TRMNL group\n if (sortTRMNL.length > 0) {\n const trmnlGroup = document.createElement('optgroup')\n trmnlGroup.label = 'TRMNL'\n sortTRMNL.forEach(model => {\n const option = document.createElement('option')\n option.value = model.name\n option.textContent = model.label || model.name\n trmnlGroup.appendChild(option)\n })\n this.elements.modelSelect.appendChild(trmnlGroup)\n }\n\n // Add BYOD group\n if (sortBYOD.length > 0) {\n const byodGroup = document.createElement('optgroup')\n byodGroup.label = 'BYOD'\n sortBYOD.forEach(model => {\n const option = document.createElement('option')\n option.value = model.name\n option.textContent = model.label || model.name\n byodGroup.appendChild(option)\n })\n this.elements.modelSelect.appendChild(byodGroup)\n }\n\n // Keep combined sorted list for default selection logic\n const sortedModels = [...sortTRMNL, ...sortBYOD]\n\n // Initialize state\n this._state = {}\n\n const defaultModel = sortedModels.find(m => m.name === _DEFAULT_MODEL_NAME) || sortedModels[0]\n const defaultPaletteId = this._getFirstValidPaletteId(defaultModel)\n const defaultParams = {\n modelName: defaultModel.name,\n paletteId: defaultPaletteId,\n isPortrait: false,\n isDarkMode: false\n }\n\n const savedParams = this._loadFromLocalStorage() || {}\n\n this._setParams('constructor', { ...defaultParams, ...savedParams })\n }\n\n /**\n * Populate palette dropdown based on selected model\n * @private\n */\n _populateModelPalettes() {\n const modelName = this.elements.modelSelect.value\n const model = this.models.find(m => m.name === modelName)\n\n if (!model) return\n\n // Clear existing options\n this.elements.paletteSelect.innerHTML = ''\n\n // Add options for each palette_id in the model (only if framework_class is not empty)\n model.palette_ids.forEach(paletteId => {\n const palette = this.palettes.find(p => p.id === paletteId)\n if (palette && palette.framework_class && palette.framework_class.trim() !== '') {\n const option = document.createElement('option')\n option.value = palette.id\n option.textContent = palette.name\n this.elements.paletteSelect.appendChild(option)\n }\n })\n }\n\n /**\n * Emit 'trmnl:change' event with current state and screen classes\n * @private\n * @param {string} origin - Source of the change ('constructor', 'form', 'setParams')\n * @fires TRMNLPicker#trmnl:change\n */\n _emitChangeEvent(origin) {\n // Save to localStorage if key is configured\n this._saveToLocalStorage()\n\n const event = new CustomEvent('trmnl:change', {\n detail: {\n origin,\n ...this.state,\n },\n bubbles: true\n })\n\n this.formElement.dispatchEvent(event)\n }\n\n /**\n * Load state from localStorage\n * @private\n * @returns {Object|null} Saved state or null if not available\n */\n _loadFromLocalStorage() {\n if (!this.localStorageKey) return null\n\n try {\n const saved = localStorage.getItem(this.localStorageKey)\n if (saved) {\n return JSON.parse(saved)\n }\n } catch (error) {\n console.warn('TRMNLPicker: Failed to load from localStorage:', error)\n }\n\n return null\n }\n\n /**\n * Save current state to localStorage\n * @private\n */\n _saveToLocalStorage() {\n if (!this.localStorageKey) return\n\n try {\n localStorage.setItem(this.localStorageKey, JSON.stringify(this.params))\n } catch (error) {\n console.warn('TRMNLPicker: Failed to save to localStorage:', error)\n }\n }\n\n /**\n * Handle model selection change\n * @private\n */\n _handleModelChange(event) {\n this._setParams('form', { modelName: event.target.value })\n }\n\n /**\n * Handle palette selection change\n * @private\n */\n _handlePaletteChange(event) {\n this._setParams('form', { paletteId: event.target.value })\n }\n\n /**\n * Toggle orientation between portrait and landscape\n * @private\n */\n _toggleOrientation() {\n this._setParams('form', { isPortrait: !this._state.isPortrait })\n }\n\n /**\n * Toggle dark mode on/off\n * @private\n */\n _toggleDarkMode() {\n this._setParams('form', { isDarkMode: !this._state.isDarkMode })\n }\n\n /**\n * Reset to defaults: first valid palette, landscape orientation, light mode\n * @private\n */\n _resetToModelDefaults() {\n const model = this._state.model\n if (!model) return\n\n // Reset to first valid palette of current model\n const firstPaletteId = this._getFirstValidPaletteId(model)\n\n this._setParams('form', {\n paletteId: firstPaletteId,\n isPortrait: false,\n isDarkMode: false\n })\n }\n\n /**\n * Update reset button enabled/disabled state\n * Button is disabled only when palette, orientation, and dark mode are all at defaults\n * @private\n */\n _updateResetButton() {\n if (!this.elements.resetButton) return\n\n const model = this._state.model\n if (!model) return\n\n const firstValidPaletteId = this._getFirstValidPaletteId(model)\n const isPaletteDefault = this.elements.paletteSelect.value === String(firstValidPaletteId)\n const isOrientationDefault = this._state.isPortrait === false\n const isDarkModeDefault = this._state.isDarkMode === false\n\n const isAtDefaults = isPaletteDefault && isOrientationDefault && isDarkModeDefault\n\n this.elements.resetButton.disabled = isAtDefaults\n\n if (isAtDefaults) {\n this.elements.resetButton.classList.add('opacity-50', 'cursor-default')\n this.elements.resetButton.setAttribute('aria-disabled', 'true')\n } else {\n this.elements.resetButton.classList.remove('opacity-50', 'cursor-default')\n this.elements.resetButton.removeAttribute('aria-disabled')\n }\n }\n\n /**\n * Get CSS classes for the current picker configuration\n * @private\n * @returns {Array} Array of CSS class names for Framework CSS rendering\n *\n * Generated classes (in order):\n * 1. 'screen' - Base class (always present)\n * 2. palette.framework_class - From selected palette (e.g., 'screen--1bit')\n * 3. model.css.classes.device - From model API (e.g., 'screen--v2')\n * 4. model.css.classes.size - From model API (e.g., 'screen--md')\n * 5. 'screen--portrait' - Only when portrait orientation is enabled\n * 6. 'screen--1x' - Scale indicator (always 1x)\n * 7. 'screen--dark-mode' - Only when dark mode is enabled\n *\n * @example\n * const classes = picker.screenClasses\n * // ['screen', 'screen--1bit', 'screen--v2', 'screen--md', 'screen--1x']\n */\n get _screenClasses() {\n const model = this._state.model\n const palette = this._state.palette\n\n if (!model) {\n throw new Error('No model selected')\n }\n\n const classes = []\n\n // 0. Base screen class (always present)\n classes.push('screen')\n\n // 1. Palette framework class\n if (palette && palette.framework_class) {\n classes.push(palette.framework_class)\n }\n\n // 2. Model device class (from API)\n if (model.css && model.css.classes && model.css.classes.device) {\n classes.push(model.css.classes.device)\n }\n\n // 3. Model size class (from API)\n if (model.css && model.css.classes && model.css.classes.size) {\n classes.push(model.css.classes.size)\n }\n\n // 4. Orientation (UI state, only portrait - landscape is default)\n if (this._state.isPortrait) {\n classes.push('screen--portrait')\n }\n\n // 5. Scale (UI state, always 1x)\n classes.push('screen--1x')\n\n // 6. Dark mode (UI state, conditional)\n if (this._state.isDarkMode) {\n classes.push('screen--dark-mode')\n }\n\n return classes\n }\n\n /**\n * Get current picker parameters (serializable state)\n * @public\n * @returns {Object} Current parameters for persistence or API calls\n * @returns {string} return.modelName - Selected model name\n * @returns {string} return.paletteId - Selected palette ID\n * @returns {boolean} return.isPortrait - Portrait orientation flag\n * @returns {boolean} return.isDarkMode - Dark mode flag\n *\n * @example\n * const params = picker.params\n * // { modelName: 'og_plus', paletteId: '123', isPortrait: false, isDarkMode: false }\n *\n * // Can be used to restore state later\n * localStorage.setItem('picker-state', JSON.stringify(picker.params))\n */\n get params() {\n return {\n modelName: this._state.model?.name,\n paletteId: this._state.palette?.id,\n isPortrait: this._state.isPortrait,\n isDarkMode: this._state.isDarkMode\n }\n }\n\n /**\n * Update picker configuration programmatically\n * @public\n * @param {Object} params - Configuration object (all fields optional)\n * @param {string} [params.modelName] - Model name to select\n * @param {string} [params.paletteId] - Palette ID to select\n * @param {boolean} [params.isPortrait] - Portrait orientation\n * @param {boolean} [params.isDarkMode] - Dark mode enabled\n * @fires TRMNLPicker#trmnl:change\n * @throws {Error} If params is not an object\n *\n * @example\n * // Update single parameter\n * picker.setParams({ isDarkMode: true })\n *\n * // Update multiple parameters\n * picker.setParams({\n * modelName: 'og_plus',\n * paletteId: '123',\n * isPortrait: true\n * })\n *\n * // Note: Changing model resets palette to first valid palette of that model\n */\n setParams(params) {\n this._setParams('setParams', params)\n }\n\n /**\n * Internal method to update picker state with origin tracking\n * @private\n * @param {string} origin - Source of change ('constructor', 'form', 'setParams')\n * @param {Object} params - Parameters to update\n * @returns {boolean} True if any changes were made\n */\n _setParams(origin, params) {\n if (!params || typeof params !== 'object') {\n throw new Error('params must be an object')\n }\n\n let changed = false\n\n // Update model if provided\n if (params.modelName) {\n const model = this.models.find(m => m.name === params.modelName)\n if (model) {\n this.elements.modelSelect.value = model.name\n this._state.model = model\n this._populateModelPalettes()\n\n // Select first valid palette of new model\n const firstPaletteId = this._getFirstValidPaletteId(model)\n this.elements.paletteSelect.value = firstPaletteId\n this._state.palette = this.palettes.find(p => p.id === firstPaletteId)\n changed = true\n }\n }\n\n // Update palette if provided\n if (params.paletteId) {\n const palette = this.palettes.find(p => p.id === params.paletteId)\n if (palette) {\n this.elements.paletteSelect.value = palette.id\n this._state.palette = palette\n changed = true\n }\n }\n\n // Update orientation if provided\n if (typeof params.isPortrait === 'boolean') {\n this._state.isPortrait = params.isPortrait\n if (this.elements.orientationText) {\n this.elements.orientationText.textContent = this._state.isPortrait ? 'Portrait' : 'Landscape'\n }\n changed = true\n }\n\n // Update dark mode if provided\n if (typeof params.isDarkMode === 'boolean') {\n this._state.isDarkMode = params.isDarkMode\n if (this.elements.darkModeText) {\n this.elements.darkModeText.textContent = this._state.isDarkMode ? 'Dark Mode' : 'Light Mode'\n }\n changed = true\n }\n\n if (changed) {\n this._updateResetButton()\n }\n\n if (changed && origin) {\n this._emitChangeEvent(origin)\n }\n\n return changed\n }\n\n /**\n * Get complete picker state including full model and palette objects\n * @public\n * @returns {{\n * model: Object,\n * palette: Object,\n * isPortrait: boolean,\n * isDarkMode: boolean,\n * screenClasses: Array,\n * width: number,\n * height: number\n * }} State object containing model (full model object from API), palette (full palette object from API), isPortrait flag, and isDarkMode flag\n *\n * @example\n * const state = picker.state\n * // {\n * // model: { name: 'og_plus', label: 'OG+', width: 800, height: 480, ... },\n * // palette: { id: '123', name: 'Black', framework_class: 'screen--1bit', ... },\n * // isPortrait: false,\n * // isDarkMode: false,\n * // screenClasses: ['screen', 'screen--1bit', 'screen--v2', 'screen--md', 'screen--1x'],\n * // width: 800,\n * // height: 480\n * // }\n */\n get state() {\n return {\n screenClasses: this._screenClasses,\n ...this._dimensions,\n ...this._state,\n };\n }\n\n /**\n * Get current dimensions (width and height) of the screen in pixels\n * @private\n * @returns {{ width: number, height: number }} Object with width and height properties\n */\n get _dimensions() {\n const model = this._state.model\n\n let width = model.width / model.scale_factor\n let height = model.height / model.scale_factor\n\n if (this._state.isPortrait) {\n [width, height] = [height, width]\n }\n\n return { width, height }\n }\n\n /**\n * Clean up event listeners and references\n * @public\n */\n destroy() {\n // Remove event listeners\n this.elements.modelSelect.removeEventListener('change', this.handlers.modelChange)\n this.elements.paletteSelect.removeEventListener('change', this.handlers.paletteChange)\n\n if (this.elements.orientationToggle) {\n this.elements.orientationToggle.removeEventListener('click', this.handlers.orientationToggle)\n }\n\n if (this.elements.darkModeToggle) {\n this.elements.darkModeToggle.removeEventListener('click', this.handlers.darkModeToggle)\n }\n\n if (this.elements.resetButton) {\n this.elements.resetButton.removeEventListener('click', this.handlers.reset)\n }\n\n // Clear references\n this.formElement = null\n this.elements = null\n this.handlers = null\n this.models = null\n this.palettes = null\n this._state = null\n }\n}\n\nexport default TRMNLPicker\n"], - "mappings": "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", + "sourcesContent": ["/**\n * Default model to select when no localStorage state is found\n * @private\n * @constant {string}\n */\nconst _DEFAULT_MODEL_NAME = 'og_plus'\n\n/**\n * localStorage key for caching API responses (models and palettes)\n * @private\n * @constant {string}\n */\nconst _API_CACHE_KEY = 'trmnl-picker-api-cache'\n\n/**\n * Cache TTL in milliseconds (1 day)\n * @private\n * @constant {number}\n */\nconst _CACHE_TTL_MS = 24 * 60 * 60 * 1000\n\n/**\n * Event fired when picker state changes\n * @event TRMNLPicker#trmnl:change\n * @type {CustomEvent}\n * @property {Object} detail - Event details\n * @property {string} detail.origin - What triggered the change: 'constructor', 'form', or 'setParams'\n * @property {Array} detail.screenClasses - Array of CSS classes for Framework CSS rendering\n * @property {Object} detail.model - Current model object with name, label, width, height, kind, css properties\n * @property {Object} detail.palette - Current palette object with id, name, framework_class properties\n * @property {boolean} detail.isPortrait - Portrait orientation flag\n * @property {boolean} detail.isDarkMode - Dark mode flag\n * @property {number} detail.width - Current width of the screen in pixels\n * @property {number} detail.height - Current height of the screen in pixels\n *\n * @example\n * picker.formElement.addEventListener('trmnl:change', (event) => {\n * const { origin, screenClasses, model, palette } = event.detail\n * console.log(`Changed via ${origin}`)\n * console.log('Classes:', screenClasses)\n * })\n */\n\n/**\n * TRMNLPicker - Vanilla JS library for TRMNL device and palette selection\n *\n * Provides a reactive picker component that manages device models, color palettes,\n * orientation, and display mode. Emits 'trmnl:change' events with current state\n * and CSS classes for rendering.\n * \n * **[View a live demo \u2192](https://usetrmnl.github.io/trmnl-picker/example/)**\n *\n * **Note:** Using the constructor directly is not recommended. Use the static\n * {@link TRMNLPicker.create} method instead, which automatically fetches\n * models and palettes from the TRMNL API if not provided.\n *\n * @class TRMNLPicker\n * @param {string|Element} formIdOrElement - Form element ID or DOM element reference\n * @param {Object} options - Configuration options\n * @param {Array} options.models - Array of model objects from TRMNL API\n * @param {Array} options.palettes - Array of palette objects from TRMNL API\n * @param {string} [options.localStorageKey] - Optional key for persisting state to localStorage\n *\n * @fires TRMNLPicker#trmnl:change\n *\n * @example\n * // HTML Structure - Required form with data-* attributes\n * //
\n * // \n * // \n * //\n * // \n * // \n * //\n * // \n * // \n * //\n * // \n * // \n * //\n * // \n * // \n * //
\n *\n * // Create with element ID\n * const picker = new TRMNLPicker('picker-form', { models, palettes })\n *\n * // Create with DOM element\n * const element = document.getElementById('picker-form')\n * const picker = new TRMNLPicker(element, { models, palettes })\n *\n * // Listen for changes\n * picker.formElement.addEventListener('trmnl:change', (event) => {\n * console.log(event.detail.screenClasses)\n * })\n */\nclass TRMNLPicker {\n static API_BASE_URL = 'https://trmnl.com'\n\n /**\n * Get cached API response from localStorage\n * @private\n * @static\n * @returns {{models: Array, palettes: Array} | null} Cached data or null if expired/missing\n */\n static _getCachedApiData() {\n try {\n const cached = localStorage.getItem(_API_CACHE_KEY)\n if (!cached) return null\n\n const { timestamp, models, palettes } = JSON.parse(cached)\n const now = Date.now()\n\n // Check if cache is still valid\n if (now - timestamp > _CACHE_TTL_MS) {\n localStorage.removeItem(_API_CACHE_KEY)\n return null\n }\n\n return { models, palettes }\n } catch (error) {\n console.warn('TRMNLPicker: Failed to read API cache:', error)\n return null\n }\n }\n\n /**\n * Save API response to localStorage cache\n * @private\n * @static\n * @param {Array} models - Models array\n * @param {Array} palettes - Palettes array\n */\n static _setCachedApiData(models, palettes) {\n try {\n const cacheData = {\n timestamp: Date.now(),\n models,\n palettes\n }\n localStorage.setItem(_API_CACHE_KEY, JSON.stringify(cacheData))\n } catch (error) {\n console.warn('TRMNLPicker: Failed to save API cache:', error)\n }\n }\n\n /**\n * Create a TRMNLPicker instance, fetching data from TRMNL API if not provided\n *\n * Automatically caches API responses in localStorage for 24 hours to reduce network requests.\n *\n * @static\n * @param {string|Element} formIdOrElement - Form element ID or DOM element\n * @param {Object} options - Configuration options\n * @param {Array} [options.models] - Optional models array (fetched from API if not provided)\n * @param {Array} [options.palettes] - Optional palettes array (fetched from API if not provided)\n * @param {string} [options.localStorageKey] - Optional key for state persistence\n * @returns {Promise} Promise resolving to picker instance\n * @throws {Error} If API fetch fails when models or palettes are not provided\n *\n * @example\n * // Fetch models and palettes from API (or use cached data if available)\n * const picker = await TRMNLPicker.create('screen-picker')\n *\n * // Provide your own data\n * const picker = await TRMNLPicker.create('screen-picker', { models, palettes })\n */\n static async create(formId, options = {}) {\n let { models, palettes, localStorageKey } = options\n\n // Try to use cached data if neither models nor palettes are provided\n if (!models && !palettes) {\n const cached = TRMNLPicker._getCachedApiData()\n if (cached) {\n models = cached.models\n palettes = cached.palettes\n }\n }\n\n // Fetch models if not provided and not in cache\n if (!models) {\n try {\n const response = await fetch(`${TRMNLPicker.API_BASE_URL}/api/models`)\n if (!response.ok) {\n throw new Error(`Failed to fetch models: ${response.status} ${response.statusText}`)\n }\n const data = await response.json()\n models = data.data || data\n } catch (error) {\n throw new Error(`TRMNLPicker: Failed to fetch models from API: ${error.message}`)\n }\n }\n\n // Fetch palettes if not provided and not in cache\n if (!palettes) {\n try {\n const response = await fetch(`${TRMNLPicker.API_BASE_URL}/api/palettes`)\n if (!response.ok) {\n throw new Error(`Failed to fetch palettes: ${response.status} ${response.statusText}`)\n }\n const data = await response.json()\n palettes = data.data || data\n } catch (error) {\n throw new Error(`TRMNLPicker: Failed to fetch palettes from API: ${error.message}`)\n }\n }\n\n // Cache the API data for future use (if both were fetched)\n if (!options.models && !options.palettes) {\n TRMNLPicker._setCachedApiData(models, palettes)\n }\n\n return new TRMNLPicker(formId, { models, palettes, localStorageKey })\n }\n\n constructor(formIdOrElement, options = {}) {\n // Validate inputs\n if (!formIdOrElement) {\n throw new Error('TRMNLPicker: formIdOrElement is required')\n }\n\n // Store references - accept either string ID or DOM element\n if (typeof formIdOrElement === 'string') {\n this.formElement = document.getElementById(formIdOrElement)\n if (!this.formElement) {\n throw new Error(`TRMNLPicker: Form element with id \"${formIdOrElement}\" not found`)\n }\n } else if (formIdOrElement instanceof Element) {\n this.formElement = formIdOrElement\n } else {\n throw new Error('TRMNLPicker: formIdOrElement must be a string ID or DOM element')\n }\n\n const { models, palettes, localStorageKey } = options\n\n this.models = models\n this.palettes = palettes\n this.localStorageKey = localStorageKey\n\n // Only initialize if we have data\n if (this.models && this.palettes) {\n if (!Array.isArray(this.models) || this.models.length === 0) {\n throw new Error('TRMNLPicker: models must be a non-empty array')\n }\n\n if (!Array.isArray(this.palettes) || this.palettes.length === 0) {\n throw new Error('TRMNLPicker: palettes must be a non-empty array')\n }\n\n // Filter out models where all palettes have empty framework_class\n this.models = this._filterValidModels()\n\n if (this.models.length === 0) {\n throw new Error('TRMNLPicker: no valid models found (all models have palettes with empty framework_class)')\n }\n\n // Initialize DOM elements and bind events\n this._initializeElements()\n this._bindEvents()\n\n // Set initial state (will load from localStorage if available)\n this._setInitialState()\n }\n }\n\n /**\n * Filter out models where all their palettes have empty framework_class\n * @private\n * @returns {Array} Filtered models array\n */\n _filterValidModels() {\n return this.models.filter(model => {\n // Check if at least one palette for this model has a non-empty framework_class\n return model.palette_ids.some(paletteId => {\n const palette = this.palettes.find(p => p.id === paletteId)\n return palette && palette.framework_class && palette.framework_class.trim() !== ''\n })\n })\n }\n\n /**\n * Get the first valid palette ID for a model (one with non-empty framework_class)\n * @private\n * @param {Object} model - Model object\n * @returns {string|null} First valid palette ID or null\n */\n _getFirstValidPaletteId(model) {\n if (!model) return null\n\n for (const paletteId of model.palette_ids) {\n const palette = this.palettes.find(p => p.id === paletteId)\n if (palette && palette.framework_class && palette.framework_class.trim() !== '') {\n return paletteId\n }\n }\n\n return null\n }\n\n /**\n * Find and store references to form elements using data-* attributes\n * @private\n */\n _initializeElements() {\n this.elements = {\n modelSelect: this.formElement.querySelector('[data-model-select]'),\n paletteSelect: this.formElement.querySelector('[data-palette-select]'),\n orientationToggle: this.formElement.querySelector('[data-orientation-toggle]'),\n darkModeToggle: this.formElement.querySelector('[data-dark-mode-toggle]'),\n resetButton: this.formElement.querySelector('[data-reset-button]'),\n\n // Optional: UI indicator elements\n orientationText: this.formElement.querySelector('[data-orientation-text]'),\n darkModeText: this.formElement.querySelector('[data-dark-mode-text]')\n }\n\n // Validate required elements\n const required = ['modelSelect', 'paletteSelect']\n for (const key of required) {\n if (!this.elements[key]) {\n throw new Error(`TRMNLPicker: Required element \"${key}\" not found in form`)\n }\n }\n }\n\n /**\n * Bind event listeners to form elements\n * @private\n */\n _bindEvents() {\n // Store bound handlers for cleanup\n this.handlers = {\n modelChange: this._handleModelChange.bind(this),\n paletteChange: this._handlePaletteChange.bind(this),\n orientationToggle: this._toggleOrientation.bind(this),\n darkModeToggle: this._toggleDarkMode.bind(this),\n reset: this._resetToModelDefaults.bind(this)\n }\n\n // Attach event listeners\n this.elements.modelSelect.addEventListener('change', this.handlers.modelChange)\n this.elements.paletteSelect.addEventListener('change', this.handlers.paletteChange)\n\n if (this.elements.orientationToggle) {\n this.elements.orientationToggle.addEventListener('click', this.handlers.orientationToggle)\n }\n\n if (this.elements.darkModeToggle) {\n this.elements.darkModeToggle.addEventListener('click', this.handlers.darkModeToggle)\n }\n\n if (this.elements.resetButton) {\n this.elements.resetButton.addEventListener('click', this.handlers.reset)\n }\n }\n\n /**\n * Set initial state and populate form\n * @private\n */\n _setInitialState() {\n // Separate models into TRMNL and BYOD groups\n const trmnlModels = this.models.filter(m => m.kind === 'trmnl')\n const byodModels = this.models.filter(m => m.kind !== 'trmnl')\n\n // Sort each group alphabetically by label\n const sortTRMNL = [...trmnlModels].sort((a, b) => {\n const labelA = (a.label || a.name).toLowerCase()\n const labelB = (b.label || b.name).toLowerCase()\n return labelA.localeCompare(labelB)\n })\n\n const sortBYOD = [...byodModels].sort((a, b) => {\n const labelA = (a.label || a.name).toLowerCase()\n const labelB = (b.label || b.name).toLowerCase()\n return labelA.localeCompare(labelB)\n })\n\n // Populate model select with grouped models\n this.elements.modelSelect.innerHTML = ''\n\n // Add TRMNL group\n if (sortTRMNL.length > 0) {\n const trmnlGroup = document.createElement('optgroup')\n trmnlGroup.label = 'TRMNL'\n sortTRMNL.forEach(model => {\n const option = document.createElement('option')\n option.value = model.name\n option.textContent = model.label || model.name\n trmnlGroup.appendChild(option)\n })\n this.elements.modelSelect.appendChild(trmnlGroup)\n }\n\n // Add BYOD group\n if (sortBYOD.length > 0) {\n const byodGroup = document.createElement('optgroup')\n byodGroup.label = 'BYOD'\n sortBYOD.forEach(model => {\n const option = document.createElement('option')\n option.value = model.name\n option.textContent = model.label || model.name\n byodGroup.appendChild(option)\n })\n this.elements.modelSelect.appendChild(byodGroup)\n }\n\n // Keep combined sorted list for default selection logic\n const sortedModels = [...sortTRMNL, ...sortBYOD]\n\n // Initialize state\n this._state = {}\n\n const defaultModel = sortedModels.find(m => m.name === _DEFAULT_MODEL_NAME) || sortedModels[0]\n const defaultPaletteId = this._getFirstValidPaletteId(defaultModel)\n const defaultParams = {\n modelName: defaultModel.name,\n paletteId: defaultPaletteId,\n isPortrait: false,\n isDarkMode: false\n }\n\n const savedParams = this._loadFromLocalStorage() || {}\n\n this._setParams('constructor', { ...defaultParams, ...savedParams })\n }\n\n /**\n * Populate palette dropdown based on selected model\n * @private\n */\n _populateModelPalettes() {\n const modelName = this.elements.modelSelect.value\n const model = this.models.find(m => m.name === modelName)\n\n if (!model) return\n\n // Clear existing options\n this.elements.paletteSelect.innerHTML = ''\n\n // Add options for each palette_id in the model (only if framework_class is not empty)\n model.palette_ids.forEach(paletteId => {\n const palette = this.palettes.find(p => p.id === paletteId)\n if (palette && palette.framework_class && palette.framework_class.trim() !== '') {\n const option = document.createElement('option')\n option.value = palette.id\n option.textContent = palette.name\n this.elements.paletteSelect.appendChild(option)\n }\n })\n }\n\n /**\n * Emit 'trmnl:change' event with current state and screen classes\n * @private\n * @param {string} origin - Source of the change ('constructor', 'form', 'setParams')\n * @fires TRMNLPicker#trmnl:change\n */\n _emitChangeEvent(origin) {\n // Save to localStorage if key is configured\n this._saveToLocalStorage()\n\n const event = new CustomEvent('trmnl:change', {\n detail: {\n origin,\n ...this.state,\n },\n bubbles: true\n })\n\n this.formElement.dispatchEvent(event)\n }\n\n /**\n * Load state from localStorage\n * @private\n * @returns {Object|null} Saved state or null if not available\n */\n _loadFromLocalStorage() {\n if (!this.localStorageKey) return null\n\n try {\n const saved = localStorage.getItem(this.localStorageKey)\n if (saved) {\n return JSON.parse(saved)\n }\n } catch (error) {\n console.warn('TRMNLPicker: Failed to load from localStorage:', error)\n }\n\n return null\n }\n\n /**\n * Save current state to localStorage\n * @private\n */\n _saveToLocalStorage() {\n if (!this.localStorageKey) return\n\n try {\n localStorage.setItem(this.localStorageKey, JSON.stringify(this.params))\n } catch (error) {\n console.warn('TRMNLPicker: Failed to save to localStorage:', error)\n }\n }\n\n /**\n * Handle model selection change\n * @private\n */\n _handleModelChange(event) {\n this._setParams('form', { modelName: event.target.value })\n }\n\n /**\n * Handle palette selection change\n * @private\n */\n _handlePaletteChange(event) {\n this._setParams('form', { paletteId: event.target.value })\n }\n\n /**\n * Toggle orientation between portrait and landscape\n * @private\n */\n _toggleOrientation() {\n this._setParams('form', { isPortrait: !this._state.isPortrait })\n }\n\n /**\n * Toggle dark mode on/off\n * @private\n */\n _toggleDarkMode() {\n this._setParams('form', { isDarkMode: !this._state.isDarkMode })\n }\n\n /**\n * Reset to defaults: first valid palette, landscape orientation, light mode\n * @private\n */\n _resetToModelDefaults() {\n const model = this._state.model\n if (!model) return\n\n // Reset to first valid palette of current model\n const firstPaletteId = this._getFirstValidPaletteId(model)\n\n this._setParams('form', {\n paletteId: firstPaletteId,\n isPortrait: false,\n isDarkMode: false\n })\n }\n\n /**\n * Update reset button enabled/disabled state\n * Button is disabled only when palette, orientation, and dark mode are all at defaults\n * @private\n */\n _updateResetButton() {\n if (!this.elements.resetButton) return\n\n const model = this._state.model\n if (!model) return\n\n const firstValidPaletteId = this._getFirstValidPaletteId(model)\n const isPaletteDefault = this.elements.paletteSelect.value === String(firstValidPaletteId)\n const isOrientationDefault = this._state.isPortrait === false\n const isDarkModeDefault = this._state.isDarkMode === false\n\n const isAtDefaults = isPaletteDefault && isOrientationDefault && isDarkModeDefault\n\n this.elements.resetButton.disabled = isAtDefaults\n\n if (isAtDefaults) {\n this.elements.resetButton.classList.add('opacity-50', 'cursor-default')\n this.elements.resetButton.setAttribute('aria-disabled', 'true')\n } else {\n this.elements.resetButton.classList.remove('opacity-50', 'cursor-default')\n this.elements.resetButton.removeAttribute('aria-disabled')\n }\n }\n\n /**\n * Get CSS classes for the current picker configuration\n * @private\n * @returns {Array} Array of CSS class names for Framework CSS rendering\n *\n * Generated classes (in order):\n * 1. 'screen' - Base class (always present)\n * 2. palette.framework_class - From selected palette (e.g., 'screen--1bit')\n * 3. model.css.classes.device - From model API (e.g., 'screen--v2')\n * 4. model.css.classes.size - From model API (e.g., 'screen--md')\n * 5. 'screen--portrait' - Only when portrait orientation is enabled\n * 6. 'screen--1x' - Scale indicator (always 1x)\n * 7. 'screen--dark-mode' - Only when dark mode is enabled\n *\n * @example\n * const classes = picker.screenClasses\n * // ['screen', 'screen--1bit', 'screen--v2', 'screen--md', 'screen--1x']\n */\n get _screenClasses() {\n const model = this._state.model\n const palette = this._state.palette\n\n if (!model) {\n throw new Error('No model selected')\n }\n\n const classes = []\n\n // 0. Base screen class (always present)\n classes.push('screen')\n\n // 1. Palette framework class\n if (palette && palette.framework_class) {\n classes.push(palette.framework_class)\n }\n\n // 2. Model device class (from API)\n if (model.css && model.css.classes && model.css.classes.device) {\n classes.push(model.css.classes.device)\n }\n\n // 3. Model size class (from API)\n if (model.css && model.css.classes && model.css.classes.size) {\n classes.push(model.css.classes.size)\n }\n\n // 4. Orientation (UI state, only portrait - landscape is default)\n if (this._state.isPortrait) {\n classes.push('screen--portrait')\n }\n\n // 5. Scale (UI state, always 1x)\n classes.push('screen--1x')\n\n // 6. Dark mode (UI state, conditional)\n if (this._state.isDarkMode) {\n classes.push('screen--dark-mode')\n }\n\n return classes\n }\n\n /**\n * Get current picker parameters (serializable state)\n * @public\n * @returns {Object} Current parameters for persistence or API calls\n * @returns {string} return.modelName - Selected model name\n * @returns {string} return.paletteId - Selected palette ID\n * @returns {boolean} return.isPortrait - Portrait orientation flag\n * @returns {boolean} return.isDarkMode - Dark mode flag\n *\n * @example\n * const params = picker.params\n * // { modelName: 'og_plus', paletteId: '123', isPortrait: false, isDarkMode: false }\n *\n * // Can be used to restore state later\n * localStorage.setItem('picker-state', JSON.stringify(picker.params))\n */\n get params() {\n return {\n modelName: this._state.model?.name,\n paletteId: this._state.palette?.id,\n isPortrait: this._state.isPortrait,\n isDarkMode: this._state.isDarkMode\n }\n }\n\n /**\n * Update picker configuration programmatically\n * @public\n * @param {Object} params - Configuration object (all fields optional)\n * @param {string} [params.modelName] - Model name to select\n * @param {string} [params.paletteId] - Palette ID to select\n * @param {boolean} [params.isPortrait] - Portrait orientation\n * @param {boolean} [params.isDarkMode] - Dark mode enabled\n * @fires TRMNLPicker#trmnl:change\n * @throws {Error} If params is not an object\n *\n * @example\n * // Update single parameter\n * picker.setParams({ isDarkMode: true })\n *\n * // Update multiple parameters\n * picker.setParams({\n * modelName: 'og_plus',\n * paletteId: '123',\n * isPortrait: true\n * })\n *\n * // Note: Changing model resets palette to first valid palette of that model\n */\n setParams(params) {\n this._setParams('setParams', params)\n }\n\n /**\n * Internal method to update picker state with origin tracking\n * @private\n * @param {string} origin - Source of change ('constructor', 'form', 'setParams')\n * @param {Object} params - Parameters to update\n * @returns {boolean} True if any changes were made\n */\n _setParams(origin, params) {\n if (!params || typeof params !== 'object') {\n throw new Error('params must be an object')\n }\n\n let changed = false\n\n // Update model if provided\n if (params.modelName) {\n const model = this.models.find(m => m.name === params.modelName)\n if (model) {\n this.elements.modelSelect.value = model.name\n this._state.model = model\n this._populateModelPalettes()\n\n // Select first valid palette of new model\n const firstPaletteId = this._getFirstValidPaletteId(model)\n this.elements.paletteSelect.value = firstPaletteId\n this._state.palette = this.palettes.find(p => p.id === firstPaletteId)\n changed = true\n }\n }\n\n // Update palette if provided\n if (params.paletteId) {\n const palette = this.palettes.find(p => p.id === params.paletteId)\n if (palette) {\n this.elements.paletteSelect.value = palette.id\n this._state.palette = palette\n changed = true\n }\n }\n\n // Update orientation if provided\n if (typeof params.isPortrait === 'boolean') {\n this._state.isPortrait = params.isPortrait\n if (this.elements.orientationText) {\n this.elements.orientationText.textContent = this._state.isPortrait ? 'Portrait' : 'Landscape'\n }\n changed = true\n }\n\n // Update dark mode if provided\n if (typeof params.isDarkMode === 'boolean') {\n this._state.isDarkMode = params.isDarkMode\n if (this.elements.darkModeText) {\n this.elements.darkModeText.textContent = this._state.isDarkMode ? 'Dark Mode' : 'Light Mode'\n }\n changed = true\n }\n\n if (changed) {\n this._updateResetButton()\n }\n\n if (changed && origin) {\n this._emitChangeEvent(origin)\n }\n\n return changed\n }\n\n /**\n * Get complete picker state including full model and palette objects\n * @public\n * @returns {{\n * model: Object,\n * palette: Object,\n * isPortrait: boolean,\n * isDarkMode: boolean,\n * screenClasses: Array,\n * width: number,\n * height: number\n * }} State object containing model (full model object from API), palette (full palette object from API), isPortrait flag, and isDarkMode flag\n *\n * @example\n * const state = picker.state\n * // {\n * // model: { name: 'og_plus', label: 'OG+', width: 800, height: 480, ... },\n * // palette: { id: '123', name: 'Black', framework_class: 'screen--1bit', ... },\n * // isPortrait: false,\n * // isDarkMode: false,\n * // screenClasses: ['screen', 'screen--1bit', 'screen--v2', 'screen--md', 'screen--1x'],\n * // width: 800,\n * // height: 480\n * // }\n */\n get state() {\n return {\n screenClasses: this._screenClasses,\n ...this._dimensions,\n ...this._state,\n };\n }\n\n /**\n * Get current dimensions (width and height) of the screen in pixels\n * @private\n * @returns {{ width: number, height: number }} Object with width and height properties\n */\n get _dimensions() {\n const model = this._state.model\n\n let width = model.width / model.scale_factor\n let height = model.height / model.scale_factor\n\n if (this._state.isPortrait) {\n [width, height] = [height, width]\n }\n\n return { width, height }\n }\n\n /**\n * Clean up event listeners and references\n * @public\n */\n destroy() {\n // Remove event listeners\n this.elements.modelSelect.removeEventListener('change', this.handlers.modelChange)\n this.elements.paletteSelect.removeEventListener('change', this.handlers.paletteChange)\n\n if (this.elements.orientationToggle) {\n this.elements.orientationToggle.removeEventListener('click', this.handlers.orientationToggle)\n }\n\n if (this.elements.darkModeToggle) {\n this.elements.darkModeToggle.removeEventListener('click', this.handlers.darkModeToggle)\n }\n\n if (this.elements.resetButton) {\n this.elements.resetButton.removeEventListener('click', this.handlers.reset)\n }\n\n // Clear references\n this.formElement = null\n this.elements = null\n this.handlers = null\n this.models = null\n this.palettes = null\n this._state = null\n }\n}\n\nexport default TRMNLPicker\n"], + "mappings": "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", "names": ["src_exports", "__export", "src_default", "_DEFAULT_MODEL_NAME", "_API_CACHE_KEY", "TRMNLPicker", "_TRMNLPicker", "cached", "_API_CACHE_KEY", "timestamp", "models", "palettes", "error", "cacheData", "formId", "options", "localStorageKey", "response", "data", "formIdOrElement", "model", "paletteId", "palette", "p", "required", "key", "trmnlModels", "m", "byodModels", "sortTRMNL", "a", "b", "labelA", "labelB", "sortBYOD", "trmnlGroup", "option", "byodGroup", "sortedModels", "defaultModel", "_DEFAULT_MODEL_NAME", "defaultPaletteId", "defaultParams", "savedParams", "modelName", "origin", "event", "saved", "firstPaletteId", "firstValidPaletteId", "isPaletteDefault", "isOrientationDefault", "isDarkModeDefault", "isAtDefaults", "classes", "params", "changed", "width", "height", "src_default"] } diff --git a/example/index.html b/example/index.html index 844d461..c04ec26 100644 --- a/example/index.html +++ b/example/index.html @@ -5,8 +5,8 @@ TRMNL Picker - Demo - - + + @@ -22,7 +22,7 @@

API Reference :: Source :: - About TRMNL + About TRMNL

@@ -477,12 +477,12 @@
- + GitHub @ryanckulp
- + diff --git a/src/index.js b/src/index.js index 96d7268..7cadb1a 100644 --- a/src/index.js +++ b/src/index.js @@ -99,7 +99,7 @@ const _CACHE_TTL_MS = 24 * 60 * 60 * 1000 * }) */ class TRMNLPicker { - static API_BASE_URL = 'https://usetrmnl.com' + static API_BASE_URL = 'https://trmnl.com' /** * Get cached API response from localStorage