trmnl_preview 0.5.10 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/lib/trmnlp/app.rb +10 -1
- data/lib/trmnlp/context.rb +13 -5
- data/lib/trmnlp/version.rb +1 -1
- data/trmnl_preview.gemspec +1 -0
- data/web/public/index.css +60 -24
- data/web/public/index.js +19 -31
- data/web/public/trmnl-picker.js +656 -0
- data/web/views/index.erb +33 -17
- data/web/views/render_html.erb +3 -0
- metadata +17 -3
- data/web/public/trmnl-component.js +0 -782
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
var TRMNLPicker = (() => {
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.js
|
|
21
|
+
var src_exports = {};
|
|
22
|
+
__export(src_exports, {
|
|
23
|
+
default: () => src_default
|
|
24
|
+
});
|
|
25
|
+
var _DEFAULT_MODEL_NAME = "og_plus";
|
|
26
|
+
var _API_CACHE_KEY = "trmnl-picker-api-cache";
|
|
27
|
+
var _CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
28
|
+
var TRMNLPicker = class _TRMNLPicker {
|
|
29
|
+
static API_BASE_URL = "https://usetrmnl.com";
|
|
30
|
+
/**
|
|
31
|
+
* Get cached API response from localStorage
|
|
32
|
+
* @private
|
|
33
|
+
* @static
|
|
34
|
+
* @returns {{models: Array, palettes: Array} | null} Cached data or null if expired/missing
|
|
35
|
+
*/
|
|
36
|
+
static _getCachedApiData() {
|
|
37
|
+
try {
|
|
38
|
+
const cached = localStorage.getItem(_API_CACHE_KEY);
|
|
39
|
+
if (!cached)
|
|
40
|
+
return null;
|
|
41
|
+
const { timestamp, models, palettes } = JSON.parse(cached);
|
|
42
|
+
const now = Date.now();
|
|
43
|
+
if (now - timestamp > _CACHE_TTL_MS) {
|
|
44
|
+
localStorage.removeItem(_API_CACHE_KEY);
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return { models, palettes };
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.warn("TRMNLPicker: Failed to read API cache:", error);
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Save API response to localStorage cache
|
|
55
|
+
* @private
|
|
56
|
+
* @static
|
|
57
|
+
* @param {Array} models - Models array
|
|
58
|
+
* @param {Array} palettes - Palettes array
|
|
59
|
+
*/
|
|
60
|
+
static _setCachedApiData(models, palettes) {
|
|
61
|
+
try {
|
|
62
|
+
const cacheData = {
|
|
63
|
+
timestamp: Date.now(),
|
|
64
|
+
models,
|
|
65
|
+
palettes
|
|
66
|
+
};
|
|
67
|
+
localStorage.setItem(_API_CACHE_KEY, JSON.stringify(cacheData));
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.warn("TRMNLPicker: Failed to save API cache:", error);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Create a TRMNLPicker instance, fetching data from TRMNL API if not provided
|
|
74
|
+
*
|
|
75
|
+
* Automatically caches API responses in localStorage for 24 hours to reduce network requests.
|
|
76
|
+
*
|
|
77
|
+
* @static
|
|
78
|
+
* @param {string|Element} formIdOrElement - Form element ID or DOM element
|
|
79
|
+
* @param {Object} options - Configuration options
|
|
80
|
+
* @param {Array<Object>} [options.models] - Optional models array (fetched from API if not provided)
|
|
81
|
+
* @param {Array<Object>} [options.palettes] - Optional palettes array (fetched from API if not provided)
|
|
82
|
+
* @param {string} [options.localStorageKey] - Optional key for state persistence
|
|
83
|
+
* @returns {Promise<TRMNLPicker>} Promise resolving to picker instance
|
|
84
|
+
* @throws {Error} If API fetch fails when models or palettes are not provided
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* // Fetch models and palettes from API (or use cached data if available)
|
|
88
|
+
* const picker = await TRMNLPicker.create('screen-picker')
|
|
89
|
+
*
|
|
90
|
+
* // Provide your own data
|
|
91
|
+
* const picker = await TRMNLPicker.create('screen-picker', { models, palettes })
|
|
92
|
+
*/
|
|
93
|
+
static async create(formId, options = {}) {
|
|
94
|
+
let { models, palettes, localStorageKey } = options;
|
|
95
|
+
if (!models && !palettes) {
|
|
96
|
+
const cached = _TRMNLPicker._getCachedApiData();
|
|
97
|
+
if (cached) {
|
|
98
|
+
models = cached.models;
|
|
99
|
+
palettes = cached.palettes;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (!models) {
|
|
103
|
+
try {
|
|
104
|
+
const response = await fetch(`${_TRMNLPicker.API_BASE_URL}/api/models`);
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
throw new Error(`Failed to fetch models: ${response.status} ${response.statusText}`);
|
|
107
|
+
}
|
|
108
|
+
const data = await response.json();
|
|
109
|
+
models = data.data || data;
|
|
110
|
+
} catch (error) {
|
|
111
|
+
throw new Error(`TRMNLPicker: Failed to fetch models from API: ${error.message}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (!palettes) {
|
|
115
|
+
try {
|
|
116
|
+
const response = await fetch(`${_TRMNLPicker.API_BASE_URL}/api/palettes`);
|
|
117
|
+
if (!response.ok) {
|
|
118
|
+
throw new Error(`Failed to fetch palettes: ${response.status} ${response.statusText}`);
|
|
119
|
+
}
|
|
120
|
+
const data = await response.json();
|
|
121
|
+
palettes = data.data || data;
|
|
122
|
+
} catch (error) {
|
|
123
|
+
throw new Error(`TRMNLPicker: Failed to fetch palettes from API: ${error.message}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (!options.models && !options.palettes) {
|
|
127
|
+
_TRMNLPicker._setCachedApiData(models, palettes);
|
|
128
|
+
}
|
|
129
|
+
return new _TRMNLPicker(formId, { models, palettes, localStorageKey });
|
|
130
|
+
}
|
|
131
|
+
constructor(formIdOrElement, options = {}) {
|
|
132
|
+
if (!formIdOrElement) {
|
|
133
|
+
throw new Error("TRMNLPicker: formIdOrElement is required");
|
|
134
|
+
}
|
|
135
|
+
if (typeof formIdOrElement === "string") {
|
|
136
|
+
this.formElement = document.getElementById(formIdOrElement);
|
|
137
|
+
if (!this.formElement) {
|
|
138
|
+
throw new Error(`TRMNLPicker: Form element with id "${formIdOrElement}" not found`);
|
|
139
|
+
}
|
|
140
|
+
} else if (formIdOrElement instanceof Element) {
|
|
141
|
+
this.formElement = formIdOrElement;
|
|
142
|
+
} else {
|
|
143
|
+
throw new Error("TRMNLPicker: formIdOrElement must be a string ID or DOM element");
|
|
144
|
+
}
|
|
145
|
+
const { models, palettes, localStorageKey } = options;
|
|
146
|
+
this.models = models;
|
|
147
|
+
this.palettes = palettes;
|
|
148
|
+
this.localStorageKey = localStorageKey;
|
|
149
|
+
if (this.models && this.palettes) {
|
|
150
|
+
if (!Array.isArray(this.models) || this.models.length === 0) {
|
|
151
|
+
throw new Error("TRMNLPicker: models must be a non-empty array");
|
|
152
|
+
}
|
|
153
|
+
if (!Array.isArray(this.palettes) || this.palettes.length === 0) {
|
|
154
|
+
throw new Error("TRMNLPicker: palettes must be a non-empty array");
|
|
155
|
+
}
|
|
156
|
+
this.models = this._filterValidModels();
|
|
157
|
+
if (this.models.length === 0) {
|
|
158
|
+
throw new Error("TRMNLPicker: no valid models found (all models have palettes with empty framework_class)");
|
|
159
|
+
}
|
|
160
|
+
this._initializeElements();
|
|
161
|
+
this._bindEvents();
|
|
162
|
+
this._setInitialState();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Filter out models where all their palettes have empty framework_class
|
|
167
|
+
* @private
|
|
168
|
+
* @returns {Array<Object>} Filtered models array
|
|
169
|
+
*/
|
|
170
|
+
_filterValidModels() {
|
|
171
|
+
return this.models.filter((model) => {
|
|
172
|
+
return model.palette_ids.some((paletteId) => {
|
|
173
|
+
const palette = this.palettes.find((p) => p.id === paletteId);
|
|
174
|
+
return palette && palette.framework_class && palette.framework_class.trim() !== "";
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Get the first valid palette ID for a model (one with non-empty framework_class)
|
|
180
|
+
* @private
|
|
181
|
+
* @param {Object} model - Model object
|
|
182
|
+
* @returns {string|null} First valid palette ID or null
|
|
183
|
+
*/
|
|
184
|
+
_getFirstValidPaletteId(model) {
|
|
185
|
+
if (!model)
|
|
186
|
+
return null;
|
|
187
|
+
for (const paletteId of model.palette_ids) {
|
|
188
|
+
const palette = this.palettes.find((p) => p.id === paletteId);
|
|
189
|
+
if (palette && palette.framework_class && palette.framework_class.trim() !== "") {
|
|
190
|
+
return paletteId;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Find and store references to form elements using data-* attributes
|
|
197
|
+
* @private
|
|
198
|
+
*/
|
|
199
|
+
_initializeElements() {
|
|
200
|
+
this.elements = {
|
|
201
|
+
modelSelect: this.formElement.querySelector("[data-model-select]"),
|
|
202
|
+
paletteSelect: this.formElement.querySelector("[data-palette-select]"),
|
|
203
|
+
orientationToggle: this.formElement.querySelector("[data-orientation-toggle]"),
|
|
204
|
+
darkModeToggle: this.formElement.querySelector("[data-dark-mode-toggle]"),
|
|
205
|
+
resetButton: this.formElement.querySelector("[data-reset-button]"),
|
|
206
|
+
// Optional: UI indicator elements
|
|
207
|
+
orientationText: this.formElement.querySelector("[data-orientation-text]"),
|
|
208
|
+
darkModeText: this.formElement.querySelector("[data-dark-mode-text]")
|
|
209
|
+
};
|
|
210
|
+
const required = ["modelSelect", "paletteSelect"];
|
|
211
|
+
for (const key of required) {
|
|
212
|
+
if (!this.elements[key]) {
|
|
213
|
+
throw new Error(`TRMNLPicker: Required element "${key}" not found in form`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Bind event listeners to form elements
|
|
219
|
+
* @private
|
|
220
|
+
*/
|
|
221
|
+
_bindEvents() {
|
|
222
|
+
this.handlers = {
|
|
223
|
+
modelChange: this._handleModelChange.bind(this),
|
|
224
|
+
paletteChange: this._handlePaletteChange.bind(this),
|
|
225
|
+
orientationToggle: this._toggleOrientation.bind(this),
|
|
226
|
+
darkModeToggle: this._toggleDarkMode.bind(this),
|
|
227
|
+
reset: this._resetToModelDefaults.bind(this)
|
|
228
|
+
};
|
|
229
|
+
this.elements.modelSelect.addEventListener("change", this.handlers.modelChange);
|
|
230
|
+
this.elements.paletteSelect.addEventListener("change", this.handlers.paletteChange);
|
|
231
|
+
if (this.elements.orientationToggle) {
|
|
232
|
+
this.elements.orientationToggle.addEventListener("click", this.handlers.orientationToggle);
|
|
233
|
+
}
|
|
234
|
+
if (this.elements.darkModeToggle) {
|
|
235
|
+
this.elements.darkModeToggle.addEventListener("click", this.handlers.darkModeToggle);
|
|
236
|
+
}
|
|
237
|
+
if (this.elements.resetButton) {
|
|
238
|
+
this.elements.resetButton.addEventListener("click", this.handlers.reset);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Set initial state and populate form
|
|
243
|
+
* @private
|
|
244
|
+
*/
|
|
245
|
+
_setInitialState() {
|
|
246
|
+
const trmnlModels = this.models.filter((m) => m.kind === "trmnl");
|
|
247
|
+
const byodModels = this.models.filter((m) => m.kind !== "trmnl");
|
|
248
|
+
const sortTRMNL = [...trmnlModels].sort((a, b) => {
|
|
249
|
+
const labelA = (a.label || a.name).toLowerCase();
|
|
250
|
+
const labelB = (b.label || b.name).toLowerCase();
|
|
251
|
+
return labelA.localeCompare(labelB);
|
|
252
|
+
});
|
|
253
|
+
const sortBYOD = [...byodModels].sort((a, b) => {
|
|
254
|
+
const labelA = (a.label || a.name).toLowerCase();
|
|
255
|
+
const labelB = (b.label || b.name).toLowerCase();
|
|
256
|
+
return labelA.localeCompare(labelB);
|
|
257
|
+
});
|
|
258
|
+
this.elements.modelSelect.innerHTML = "";
|
|
259
|
+
if (sortTRMNL.length > 0) {
|
|
260
|
+
const trmnlGroup = document.createElement("optgroup");
|
|
261
|
+
trmnlGroup.label = "TRMNL";
|
|
262
|
+
sortTRMNL.forEach((model) => {
|
|
263
|
+
const option = document.createElement("option");
|
|
264
|
+
option.value = model.name;
|
|
265
|
+
option.textContent = model.label || model.name;
|
|
266
|
+
trmnlGroup.appendChild(option);
|
|
267
|
+
});
|
|
268
|
+
this.elements.modelSelect.appendChild(trmnlGroup);
|
|
269
|
+
}
|
|
270
|
+
if (sortBYOD.length > 0) {
|
|
271
|
+
const byodGroup = document.createElement("optgroup");
|
|
272
|
+
byodGroup.label = "BYOD";
|
|
273
|
+
sortBYOD.forEach((model) => {
|
|
274
|
+
const option = document.createElement("option");
|
|
275
|
+
option.value = model.name;
|
|
276
|
+
option.textContent = model.label || model.name;
|
|
277
|
+
byodGroup.appendChild(option);
|
|
278
|
+
});
|
|
279
|
+
this.elements.modelSelect.appendChild(byodGroup);
|
|
280
|
+
}
|
|
281
|
+
const sortedModels = [...sortTRMNL, ...sortBYOD];
|
|
282
|
+
this._state = {};
|
|
283
|
+
const savedParams = this._loadFromLocalStorage();
|
|
284
|
+
if (savedParams) {
|
|
285
|
+
this._setParams("constructor", savedParams);
|
|
286
|
+
} else {
|
|
287
|
+
const defaultModel = sortedModels.find((m) => m.name === _DEFAULT_MODEL_NAME) || sortedModels[0];
|
|
288
|
+
const defaultPaletteId = this._getFirstValidPaletteId(defaultModel);
|
|
289
|
+
this._setParams("constructor", {
|
|
290
|
+
modelName: defaultModel.name,
|
|
291
|
+
paletteId: defaultPaletteId,
|
|
292
|
+
isPortrait: false,
|
|
293
|
+
isDarkMode: false
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Populate palette dropdown based on selected model
|
|
299
|
+
* @private
|
|
300
|
+
*/
|
|
301
|
+
_populateModelPalettes() {
|
|
302
|
+
const modelName = this.elements.modelSelect.value;
|
|
303
|
+
const model = this.models.find((m) => m.name === modelName);
|
|
304
|
+
if (!model)
|
|
305
|
+
return;
|
|
306
|
+
this.elements.paletteSelect.innerHTML = "";
|
|
307
|
+
model.palette_ids.forEach((paletteId) => {
|
|
308
|
+
const palette = this.palettes.find((p) => p.id === paletteId);
|
|
309
|
+
if (palette && palette.framework_class && palette.framework_class.trim() !== "") {
|
|
310
|
+
const option = document.createElement("option");
|
|
311
|
+
option.value = palette.id;
|
|
312
|
+
option.textContent = palette.name;
|
|
313
|
+
this.elements.paletteSelect.appendChild(option);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Emit 'trmnl:change' event with current state and screen classes
|
|
319
|
+
* @private
|
|
320
|
+
* @param {string} origin - Source of the change ('constructor', 'form', 'setParams')
|
|
321
|
+
* @fires TRMNLPicker#trmnl:change
|
|
322
|
+
*/
|
|
323
|
+
_emitChangeEvent(origin) {
|
|
324
|
+
this._saveToLocalStorage();
|
|
325
|
+
const event = new CustomEvent("trmnl:change", {
|
|
326
|
+
detail: {
|
|
327
|
+
origin,
|
|
328
|
+
...this.state
|
|
329
|
+
},
|
|
330
|
+
bubbles: true
|
|
331
|
+
});
|
|
332
|
+
this.formElement.dispatchEvent(event);
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Load state from localStorage
|
|
336
|
+
* @private
|
|
337
|
+
* @returns {Object|null} Saved state or null if not available
|
|
338
|
+
*/
|
|
339
|
+
_loadFromLocalStorage() {
|
|
340
|
+
if (!this.localStorageKey)
|
|
341
|
+
return null;
|
|
342
|
+
try {
|
|
343
|
+
const saved = localStorage.getItem(this.localStorageKey);
|
|
344
|
+
if (saved) {
|
|
345
|
+
return JSON.parse(saved);
|
|
346
|
+
}
|
|
347
|
+
} catch (error) {
|
|
348
|
+
console.warn("TRMNLPicker: Failed to load from localStorage:", error);
|
|
349
|
+
}
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Save current state to localStorage
|
|
354
|
+
* @private
|
|
355
|
+
*/
|
|
356
|
+
_saveToLocalStorage() {
|
|
357
|
+
if (!this.localStorageKey)
|
|
358
|
+
return;
|
|
359
|
+
try {
|
|
360
|
+
localStorage.setItem(this.localStorageKey, JSON.stringify(this.params));
|
|
361
|
+
} catch (error) {
|
|
362
|
+
console.warn("TRMNLPicker: Failed to save to localStorage:", error);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Handle model selection change
|
|
367
|
+
* @private
|
|
368
|
+
*/
|
|
369
|
+
_handleModelChange(event) {
|
|
370
|
+
this._setParams("form", { modelName: event.target.value });
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Handle palette selection change
|
|
374
|
+
* @private
|
|
375
|
+
*/
|
|
376
|
+
_handlePaletteChange(event) {
|
|
377
|
+
this._setParams("form", { paletteId: event.target.value });
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Toggle orientation between portrait and landscape
|
|
381
|
+
* @private
|
|
382
|
+
*/
|
|
383
|
+
_toggleOrientation() {
|
|
384
|
+
this._setParams("form", { isPortrait: !this._state.isPortrait });
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Toggle dark mode on/off
|
|
388
|
+
* @private
|
|
389
|
+
*/
|
|
390
|
+
_toggleDarkMode() {
|
|
391
|
+
this._setParams("form", { isDarkMode: !this._state.isDarkMode });
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Reset to defaults: first valid palette, landscape orientation, light mode
|
|
395
|
+
* @private
|
|
396
|
+
*/
|
|
397
|
+
_resetToModelDefaults() {
|
|
398
|
+
const model = this._state.model;
|
|
399
|
+
if (!model)
|
|
400
|
+
return;
|
|
401
|
+
const firstPaletteId = this._getFirstValidPaletteId(model);
|
|
402
|
+
this._setParams("form", {
|
|
403
|
+
paletteId: firstPaletteId,
|
|
404
|
+
isPortrait: false,
|
|
405
|
+
isDarkMode: false
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Update reset button enabled/disabled state
|
|
410
|
+
* Button is disabled only when palette, orientation, and dark mode are all at defaults
|
|
411
|
+
* @private
|
|
412
|
+
*/
|
|
413
|
+
_updateResetButton() {
|
|
414
|
+
if (!this.elements.resetButton)
|
|
415
|
+
return;
|
|
416
|
+
const model = this._state.model;
|
|
417
|
+
if (!model)
|
|
418
|
+
return;
|
|
419
|
+
const firstValidPaletteId = this._getFirstValidPaletteId(model);
|
|
420
|
+
const isPaletteDefault = this.elements.paletteSelect.value === String(firstValidPaletteId);
|
|
421
|
+
const isOrientationDefault = this._state.isPortrait === false;
|
|
422
|
+
const isDarkModeDefault = this._state.isDarkMode === false;
|
|
423
|
+
const isAtDefaults = isPaletteDefault && isOrientationDefault && isDarkModeDefault;
|
|
424
|
+
this.elements.resetButton.disabled = isAtDefaults;
|
|
425
|
+
if (isAtDefaults) {
|
|
426
|
+
this.elements.resetButton.classList.add("opacity-50", "cursor-default");
|
|
427
|
+
this.elements.resetButton.setAttribute("aria-disabled", "true");
|
|
428
|
+
} else {
|
|
429
|
+
this.elements.resetButton.classList.remove("opacity-50", "cursor-default");
|
|
430
|
+
this.elements.resetButton.removeAttribute("aria-disabled");
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Get CSS classes for the current picker configuration
|
|
435
|
+
* @private
|
|
436
|
+
* @returns {Array<string>} Array of CSS class names for Framework CSS rendering
|
|
437
|
+
*
|
|
438
|
+
* Generated classes (in order):
|
|
439
|
+
* 1. 'screen' - Base class (always present)
|
|
440
|
+
* 2. palette.framework_class - From selected palette (e.g., 'screen--1bit')
|
|
441
|
+
* 3. model.css.classes.device - From model API (e.g., 'screen--v2')
|
|
442
|
+
* 4. model.css.classes.size - From model API (e.g., 'screen--md')
|
|
443
|
+
* 5. 'screen--portrait' - Only when portrait orientation is enabled
|
|
444
|
+
* 6. 'screen--1x' - Scale indicator (always 1x)
|
|
445
|
+
* 7. 'screen--dark-mode' - Only when dark mode is enabled
|
|
446
|
+
*
|
|
447
|
+
* @example
|
|
448
|
+
* const classes = picker.screenClasses
|
|
449
|
+
* // ['screen', 'screen--1bit', 'screen--v2', 'screen--md', 'screen--1x']
|
|
450
|
+
*/
|
|
451
|
+
get _screenClasses() {
|
|
452
|
+
const model = this._state.model;
|
|
453
|
+
const palette = this._state.palette;
|
|
454
|
+
if (!model) {
|
|
455
|
+
throw new Error("No model selected");
|
|
456
|
+
}
|
|
457
|
+
const classes = [];
|
|
458
|
+
classes.push("screen");
|
|
459
|
+
if (palette && palette.framework_class) {
|
|
460
|
+
classes.push(palette.framework_class);
|
|
461
|
+
}
|
|
462
|
+
if (model.css && model.css.classes && model.css.classes.device) {
|
|
463
|
+
classes.push(model.css.classes.device);
|
|
464
|
+
}
|
|
465
|
+
if (model.css && model.css.classes && model.css.classes.size) {
|
|
466
|
+
classes.push(model.css.classes.size);
|
|
467
|
+
}
|
|
468
|
+
if (this._state.isPortrait) {
|
|
469
|
+
classes.push("screen--portrait");
|
|
470
|
+
}
|
|
471
|
+
classes.push("screen--1x");
|
|
472
|
+
if (this._state.isDarkMode) {
|
|
473
|
+
classes.push("screen--dark-mode");
|
|
474
|
+
}
|
|
475
|
+
return classes;
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Get current picker parameters (serializable state)
|
|
479
|
+
* @public
|
|
480
|
+
* @returns {Object} Current parameters for persistence or API calls
|
|
481
|
+
* @returns {string} return.modelName - Selected model name
|
|
482
|
+
* @returns {string} return.paletteId - Selected palette ID
|
|
483
|
+
* @returns {boolean} return.isPortrait - Portrait orientation flag
|
|
484
|
+
* @returns {boolean} return.isDarkMode - Dark mode flag
|
|
485
|
+
*
|
|
486
|
+
* @example
|
|
487
|
+
* const params = picker.params
|
|
488
|
+
* // { modelName: 'og_plus', paletteId: '123', isPortrait: false, isDarkMode: false }
|
|
489
|
+
*
|
|
490
|
+
* // Can be used to restore state later
|
|
491
|
+
* localStorage.setItem('picker-state', JSON.stringify(picker.params))
|
|
492
|
+
*/
|
|
493
|
+
get params() {
|
|
494
|
+
return {
|
|
495
|
+
modelName: this._state.model?.name,
|
|
496
|
+
paletteId: this._state.palette?.id,
|
|
497
|
+
isPortrait: this._state.isPortrait,
|
|
498
|
+
isDarkMode: this._state.isDarkMode
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Update picker configuration programmatically
|
|
503
|
+
* @public
|
|
504
|
+
* @param {Object} params - Configuration object (all fields optional)
|
|
505
|
+
* @param {string} [params.modelName] - Model name to select
|
|
506
|
+
* @param {string} [params.paletteId] - Palette ID to select
|
|
507
|
+
* @param {boolean} [params.isPortrait] - Portrait orientation
|
|
508
|
+
* @param {boolean} [params.isDarkMode] - Dark mode enabled
|
|
509
|
+
* @fires TRMNLPicker#trmnl:change
|
|
510
|
+
* @throws {Error} If params is not an object
|
|
511
|
+
*
|
|
512
|
+
* @example
|
|
513
|
+
* // Update single parameter
|
|
514
|
+
* picker.setParams({ isDarkMode: true })
|
|
515
|
+
*
|
|
516
|
+
* // Update multiple parameters
|
|
517
|
+
* picker.setParams({
|
|
518
|
+
* modelName: 'og_plus',
|
|
519
|
+
* paletteId: '123',
|
|
520
|
+
* isPortrait: true
|
|
521
|
+
* })
|
|
522
|
+
*
|
|
523
|
+
* // Note: Changing model resets palette to first valid palette of that model
|
|
524
|
+
*/
|
|
525
|
+
setParams(params) {
|
|
526
|
+
this._setParams("setParams", params);
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Internal method to update picker state with origin tracking
|
|
530
|
+
* @private
|
|
531
|
+
* @param {string} origin - Source of change ('constructor', 'form', 'setParams')
|
|
532
|
+
* @param {Object} params - Parameters to update
|
|
533
|
+
* @returns {boolean} True if any changes were made
|
|
534
|
+
*/
|
|
535
|
+
_setParams(origin, params) {
|
|
536
|
+
if (!params || typeof params !== "object") {
|
|
537
|
+
throw new Error("params must be an object");
|
|
538
|
+
}
|
|
539
|
+
let changed = false;
|
|
540
|
+
if (params.modelName) {
|
|
541
|
+
const model = this.models.find((m) => m.name === params.modelName);
|
|
542
|
+
if (model) {
|
|
543
|
+
this.elements.modelSelect.value = model.name;
|
|
544
|
+
this._state.model = model;
|
|
545
|
+
this._populateModelPalettes();
|
|
546
|
+
const firstPaletteId = this._getFirstValidPaletteId(model);
|
|
547
|
+
this.elements.paletteSelect.value = firstPaletteId;
|
|
548
|
+
this._state.palette = this.palettes.find((p) => p.id === firstPaletteId);
|
|
549
|
+
changed = true;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
if (params.paletteId) {
|
|
553
|
+
const palette = this.palettes.find((p) => p.id === params.paletteId);
|
|
554
|
+
if (palette) {
|
|
555
|
+
this.elements.paletteSelect.value = palette.id;
|
|
556
|
+
this._state.palette = palette;
|
|
557
|
+
changed = true;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
if (typeof params.isPortrait === "boolean") {
|
|
561
|
+
this._state.isPortrait = params.isPortrait;
|
|
562
|
+
if (this.elements.orientationText) {
|
|
563
|
+
this.elements.orientationText.textContent = this._state.isPortrait ? "Portrait" : "Landscape";
|
|
564
|
+
}
|
|
565
|
+
changed = true;
|
|
566
|
+
}
|
|
567
|
+
if (typeof params.isDarkMode === "boolean") {
|
|
568
|
+
this._state.isDarkMode = params.isDarkMode;
|
|
569
|
+
if (this.elements.darkModeText) {
|
|
570
|
+
this.elements.darkModeText.textContent = this._state.isDarkMode ? "Dark Mode" : "Light Mode";
|
|
571
|
+
}
|
|
572
|
+
changed = true;
|
|
573
|
+
}
|
|
574
|
+
if (changed) {
|
|
575
|
+
this._updateResetButton();
|
|
576
|
+
}
|
|
577
|
+
if (changed && origin) {
|
|
578
|
+
this._emitChangeEvent(origin);
|
|
579
|
+
}
|
|
580
|
+
return changed;
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Get complete picker state including full model and palette objects
|
|
584
|
+
* @public
|
|
585
|
+
* @returns {{
|
|
586
|
+
* model: Object,
|
|
587
|
+
* palette: Object,
|
|
588
|
+
* isPortrait: boolean,
|
|
589
|
+
* isDarkMode: boolean,
|
|
590
|
+
* screenClasses: Array<string>,
|
|
591
|
+
* width: number,
|
|
592
|
+
* height: number
|
|
593
|
+
* }} State object containing model (full model object from API), palette (full palette object from API), isPortrait flag, and isDarkMode flag
|
|
594
|
+
*
|
|
595
|
+
* @example
|
|
596
|
+
* const state = picker.state
|
|
597
|
+
* // {
|
|
598
|
+
* // model: { name: 'og_plus', label: 'OG+', width: 800, height: 480, ... },
|
|
599
|
+
* // palette: { id: '123', name: 'Black', framework_class: 'screen--1bit', ... },
|
|
600
|
+
* // isPortrait: false,
|
|
601
|
+
* // isDarkMode: false,
|
|
602
|
+
* // screenClasses: ['screen', 'screen--1bit', 'screen--v2', 'screen--md', 'screen--1x'],
|
|
603
|
+
* // width: 800,
|
|
604
|
+
* // height: 480
|
|
605
|
+
* // }
|
|
606
|
+
*/
|
|
607
|
+
get state() {
|
|
608
|
+
return {
|
|
609
|
+
screenClasses: this._screenClasses,
|
|
610
|
+
...this._dimensions,
|
|
611
|
+
...this._state
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Get current dimensions (width and height) of the screen in pixels
|
|
616
|
+
* @private
|
|
617
|
+
* @returns {{ width: number, height: number }} Object with width and height properties
|
|
618
|
+
*/
|
|
619
|
+
get _dimensions() {
|
|
620
|
+
const model = this._state.model;
|
|
621
|
+
let width = model.width / model.scale_factor;
|
|
622
|
+
let height = model.height / model.scale_factor;
|
|
623
|
+
if (this._state.isPortrait) {
|
|
624
|
+
[width, height] = [height, width];
|
|
625
|
+
}
|
|
626
|
+
return { width, height };
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Clean up event listeners and references
|
|
630
|
+
* @public
|
|
631
|
+
*/
|
|
632
|
+
destroy() {
|
|
633
|
+
this.elements.modelSelect.removeEventListener("change", this.handlers.modelChange);
|
|
634
|
+
this.elements.paletteSelect.removeEventListener("change", this.handlers.paletteChange);
|
|
635
|
+
if (this.elements.orientationToggle) {
|
|
636
|
+
this.elements.orientationToggle.removeEventListener("click", this.handlers.orientationToggle);
|
|
637
|
+
}
|
|
638
|
+
if (this.elements.darkModeToggle) {
|
|
639
|
+
this.elements.darkModeToggle.removeEventListener("click", this.handlers.darkModeToggle);
|
|
640
|
+
}
|
|
641
|
+
if (this.elements.resetButton) {
|
|
642
|
+
this.elements.resetButton.removeEventListener("click", this.handlers.reset);
|
|
643
|
+
}
|
|
644
|
+
this.formElement = null;
|
|
645
|
+
this.elements = null;
|
|
646
|
+
this.handlers = null;
|
|
647
|
+
this.models = null;
|
|
648
|
+
this.palettes = null;
|
|
649
|
+
this._state = null;
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
var src_default = TRMNLPicker;
|
|
653
|
+
return __toCommonJS(src_exports);
|
|
654
|
+
})();
|
|
655
|
+
TRMNLPicker=TRMNLPicker.default;
|
|
656
|
+
//# sourceMappingURL=trmnl-picker.js.map
|