mirror of
https://github.com/usetrmnl/trmnl-picker.git
synced 2026-04-29 13:44:02 -07:00
Initial working demo
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
node_modules/
|
||||
dist/
|
||||
*.log
|
||||
.DS_Store
|
||||
.vscode/
|
||||
.idea/
|
||||
@@ -0,0 +1,8 @@
|
||||
example/
|
||||
src/
|
||||
node_modules/
|
||||
.gitignore
|
||||
*.log
|
||||
.DS_Store
|
||||
.vscode/
|
||||
.idea/
|
||||
@@ -0,0 +1,439 @@
|
||||
# @trmnl/picker
|
||||
|
||||
A lightweight, framework-agnostic JavaScript library for managing TRMNL device model and palette selection.
|
||||
|
||||
## Features
|
||||
|
||||
- Zero dependencies
|
||||
- Vanilla JavaScript (ES6+)
|
||||
- Event-driven architecture
|
||||
- Support for both NPM and browser usage
|
||||
- Minimal API surface
|
||||
- TypeScript-friendly
|
||||
|
||||
## Installation
|
||||
|
||||
### NPM
|
||||
```bash
|
||||
npm install @trmnl/picker
|
||||
```
|
||||
|
||||
### Browser (CDN)
|
||||
```html
|
||||
<script src="https://unpkg.com/@trmnl/picker@latest/dist/trmnl-picker.min.js"></script>
|
||||
```
|
||||
|
||||
### Browser (Local)
|
||||
```html
|
||||
<script src="dist/trmnl-picker.min.js"></script>
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Create HTML Structure
|
||||
|
||||
The library expects a form with specific element IDs:
|
||||
|
||||
```html
|
||||
<form id="picker-form">
|
||||
<!-- Required: Model selector -->
|
||||
<select id="model-select"></select>
|
||||
|
||||
<!-- Required: Palette selector -->
|
||||
<select id="palette-select"></select>
|
||||
|
||||
<!-- Optional: Orientation toggle -->
|
||||
<button type="button" id="orientation-toggle">
|
||||
<span data-orientation-text>Landscape</span>
|
||||
</button>
|
||||
|
||||
<!-- Optional: Dark mode toggle -->
|
||||
<button type="button" id="dark-mode-toggle">
|
||||
<span data-dark-mode-text>Light Mode</span>
|
||||
</button>
|
||||
|
||||
<!-- Optional: Reset button -->
|
||||
<button type="button" id="reset-button">Reset</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
### 2. Initialize Picker
|
||||
|
||||
#### Browser Usage
|
||||
```html
|
||||
<script src="dist/trmnl-picker.min.js"></script>
|
||||
<script>
|
||||
// Get data from your API or define locally
|
||||
const models = [
|
||||
{
|
||||
name: 'trmnl_original',
|
||||
label: 'TRMNL Original',
|
||||
size: '2.9',
|
||||
width: 800,
|
||||
height: 480,
|
||||
palette_ids: ['bw', '4c', '7c']
|
||||
}
|
||||
]
|
||||
|
||||
const palettes = [
|
||||
{ id: 'bw', name: 'Black & White', framework_class: 'palette-bw' },
|
||||
{ id: '4c', name: '4-Color', framework_class: 'palette-4c' },
|
||||
{ id: '7c', name: '7-Color', framework_class: 'palette-7c' }
|
||||
]
|
||||
|
||||
// Initialize (TRMNLPicker is available globally)
|
||||
const picker = new TRMNLPicker('picker-form', models, palettes)
|
||||
</script>
|
||||
```
|
||||
|
||||
#### NPM Module Usage
|
||||
```javascript
|
||||
import TRMNLPicker from '@trmnl/picker'
|
||||
|
||||
// Fetch data from API
|
||||
const modelsResponse = await fetch('/api/models')
|
||||
const modelsData = await modelsResponse.json()
|
||||
const models = modelsData.data // Adjust based on your API response structure
|
||||
|
||||
const palettes = [
|
||||
{ id: 'bw', name: 'Black & White', framework_class: 'palette-bw' },
|
||||
{ id: '4c', name: '4-Color', framework_class: 'palette-4c' }
|
||||
]
|
||||
|
||||
// Initialize
|
||||
const picker = new TRMNLPicker('picker-form', models, palettes)
|
||||
```
|
||||
|
||||
### 3. Listen for Changes
|
||||
|
||||
```javascript
|
||||
document.getElementById('picker-form').addEventListener('changed', (event) => {
|
||||
const { screenClasses, state } = event.detail
|
||||
|
||||
console.log('Screen classes:', screenClasses)
|
||||
// ['palette-bw', 'screen--trmnl_original', 'screen--2.9', 'screen--landscape', 'screen--1x']
|
||||
|
||||
console.log('State:', state)
|
||||
// { model: {...}, palette: {...}, isPortrait: false, isDarkMode: false }
|
||||
|
||||
// Apply classes to your screen elements
|
||||
document.querySelectorAll('.screen').forEach(screen => {
|
||||
const filtered = screen.className.split(' ')
|
||||
.filter(c => !c.startsWith('screen--') || c.startsWith('screen--scale-'))
|
||||
screen.className = [...filtered, ...screenClasses].join(' ')
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Constructor
|
||||
|
||||
```javascript
|
||||
new TRMNLPicker(formId, models, palettes)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `formId` (string, required): ID of the form element containing picker controls
|
||||
- `models` (array, required): Array of model objects from the `/api/models` endpoint
|
||||
- `palettes` (array, required): Array of palette objects
|
||||
|
||||
**Model Object Structure:**
|
||||
```javascript
|
||||
{
|
||||
name: 'trmnl_original', // Unique identifier
|
||||
label: 'TRMNL Original', // Display name
|
||||
size: '2.9', // Screen size
|
||||
width: 800, // Width in pixels
|
||||
height: 480, // Height in pixels
|
||||
palette_ids: ['bw', '4c'] // Available palettes for this model
|
||||
}
|
||||
```
|
||||
|
||||
**Palette Object Structure:**
|
||||
```javascript
|
||||
{
|
||||
id: 'bw', // Palette identifier
|
||||
name: 'Black & White', // Display name
|
||||
framework_class: 'palette-bw' // CSS class for styling
|
||||
}
|
||||
```
|
||||
|
||||
### Methods
|
||||
|
||||
#### `update(config)`
|
||||
|
||||
Programmatically update picker state.
|
||||
|
||||
```javascript
|
||||
picker.update({
|
||||
modelName: 'trmnl_original', // Model name to select
|
||||
paletteId: 'bw', // Palette ID to select
|
||||
isPortrait: true, // Set portrait orientation
|
||||
isDarkMode: false // Set dark mode
|
||||
})
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `config.modelName` (string, optional): Model name to select
|
||||
- `config.paletteId` (string, optional): Palette ID to select
|
||||
- `config.isPortrait` (boolean, optional): Set portrait orientation
|
||||
- `config.isDarkMode` (boolean, optional): Enable/disable dark mode
|
||||
|
||||
#### `getState()`
|
||||
|
||||
Get current picker state.
|
||||
|
||||
```javascript
|
||||
const state = picker.getState()
|
||||
```
|
||||
|
||||
**Returns:**
|
||||
```javascript
|
||||
{
|
||||
model: {
|
||||
name: 'trmnl_original',
|
||||
label: 'TRMNL Original',
|
||||
size: '2.9',
|
||||
width: 800,
|
||||
height: 480
|
||||
},
|
||||
palette: {
|
||||
id: 'bw',
|
||||
name: 'Black & White',
|
||||
framework_class: 'palette-bw'
|
||||
},
|
||||
isPortrait: false,
|
||||
isDarkMode: false,
|
||||
screenClasses: [
|
||||
'palette-bw',
|
||||
'screen--trmnl_original',
|
||||
'screen--2.9',
|
||||
'screen--landscape',
|
||||
'screen--1x'
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### `destroy()`
|
||||
|
||||
Clean up event listeners and references. Call this when removing the picker.
|
||||
|
||||
```javascript
|
||||
picker.destroy()
|
||||
```
|
||||
|
||||
### Events
|
||||
|
||||
#### `changed` Event
|
||||
|
||||
Emitted when user changes any selection (model, palette, orientation, or dark mode).
|
||||
|
||||
```javascript
|
||||
document.getElementById('picker-form').addEventListener('changed', (event) => {
|
||||
const { screenClasses, state } = event.detail
|
||||
// Handle the change
|
||||
})
|
||||
```
|
||||
|
||||
**Event Detail Structure:**
|
||||
```javascript
|
||||
{
|
||||
screenClasses: [
|
||||
'palette-bw',
|
||||
'screen--trmnl_original',
|
||||
'screen--2.9',
|
||||
'screen--landscape',
|
||||
'screen--1x'
|
||||
],
|
||||
state: {
|
||||
model: { name, label, size, width, height },
|
||||
palette: { id, name, framework_class },
|
||||
isPortrait: false,
|
||||
isDarkMode: false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Screen Class Generation
|
||||
|
||||
The library generates CSS classes in the following order:
|
||||
|
||||
1. **Palette class**: From `palette.framework_class` (e.g., `palette-bw`)
|
||||
2. **Model name**: `screen--{model.name}` (e.g., `screen--trmnl_original`)
|
||||
3. **Model size**: `screen--{model.size}` (e.g., `screen--2.9`)
|
||||
4. **Orientation**: `screen--portrait` or `screen--landscape`
|
||||
5. **Scale**: Always `screen--1x`
|
||||
6. **Dark mode** (conditional): `screen--dark-mode` (when enabled)
|
||||
|
||||
## Form Elements
|
||||
|
||||
The library expects the following elements within the form:
|
||||
|
||||
### Required Elements
|
||||
- `#model-select` - Model dropdown
|
||||
- `#palette-select` - Palette dropdown
|
||||
|
||||
### Optional Elements
|
||||
- `#orientation-toggle` - Button to toggle portrait/landscape
|
||||
- `#dark-mode-toggle` - Button to toggle dark mode
|
||||
- `#reset-button` - Button to reset palette to model's default
|
||||
- `[data-orientation-text]` - Text element showing current orientation
|
||||
- `[data-dark-mode-text]` - Text element showing current mode
|
||||
|
||||
## Examples
|
||||
|
||||
### Complete Working Example
|
||||
|
||||
See `example/index.html` for a complete working example with styling.
|
||||
|
||||
### Applying Classes to Screen Elements
|
||||
|
||||
```javascript
|
||||
document.getElementById('picker-form').addEventListener('changed', (event) => {
|
||||
const { screenClasses } = event.detail
|
||||
|
||||
// Apply to all elements with class 'screen'
|
||||
document.querySelectorAll('.screen').forEach(screen => {
|
||||
// Remove old screen-- classes (except scale and no-bleed)
|
||||
const currentClasses = screen.className.split(' ')
|
||||
const filteredClasses = currentClasses.filter(c => {
|
||||
if (!c.startsWith('screen--')) return true
|
||||
if (c.startsWith('screen--scale-')) return true
|
||||
if (c === 'screen--no-bleed') return true
|
||||
return false
|
||||
})
|
||||
|
||||
// Add new screen classes
|
||||
filteredClasses.push(...screenClasses)
|
||||
screen.className = filteredClasses.join(' ')
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Using with React
|
||||
|
||||
```jsx
|
||||
import { useEffect, useRef } from 'react'
|
||||
import TRMNLPicker from '@trmnl/picker'
|
||||
|
||||
function ScreenPicker({ models, palettes, onChange }) {
|
||||
const pickerRef = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
const picker = new TRMNLPicker('picker-form', models, palettes)
|
||||
|
||||
const handleChange = (event) => {
|
||||
onChange(event.detail)
|
||||
}
|
||||
|
||||
document.getElementById('picker-form').addEventListener('changed', handleChange)
|
||||
|
||||
pickerRef.current = picker
|
||||
|
||||
return () => {
|
||||
picker.destroy()
|
||||
document.getElementById('picker-form').removeEventListener('changed', handleChange)
|
||||
}
|
||||
}, [models, palettes, onChange])
|
||||
|
||||
return (
|
||||
<form id="picker-form">
|
||||
<select id="model-select"></select>
|
||||
<select id="palette-select"></select>
|
||||
<button type="button" id="orientation-toggle">
|
||||
<span data-orientation-text>Landscape</span>
|
||||
</button>
|
||||
<button type="button" id="dark-mode-toggle">
|
||||
<span data-dark-mode-text>Light Mode</span>
|
||||
</button>
|
||||
<button type="button" id="reset-button">Reset</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Migration from Stimulus Version
|
||||
|
||||
If you're migrating from the Stimulus-based controller:
|
||||
|
||||
### Key Differences
|
||||
|
||||
**Removed Features:**
|
||||
- No localStorage persistence (implement in your app if needed)
|
||||
- No BroadcastChannel synchronization
|
||||
- No automatic DOM manipulation of `.screen` elements
|
||||
- No automatic page reload
|
||||
|
||||
**Data Model Changes:**
|
||||
- Use `model.name` instead of `model.keyname`
|
||||
- API response field `name` maps directly (e.g., `"trmnl_original"`)
|
||||
|
||||
### Migration Steps
|
||||
|
||||
1. Remove Stimulus controller and data attributes
|
||||
2. Add standard HTML form with required element IDs
|
||||
3. Include `@trmnl/picker` library
|
||||
4. Initialize picker with `new TRMNLPicker()`
|
||||
5. Listen to `changed` event and apply classes manually
|
||||
6. Implement localStorage persistence if needed (application concern)
|
||||
|
||||
**Before (Stimulus):**
|
||||
```erb
|
||||
<div data-controller="screen-picker"
|
||||
data-screen-picker-models-value="<%= models.to_json %>"
|
||||
data-screen-picker-palettes-value="<%= palettes.to_json %>">
|
||||
<select data-screen-picker-target="modelSelect"></select>
|
||||
</div>
|
||||
```
|
||||
|
||||
**After (Vanilla):**
|
||||
```html
|
||||
<form id="picker-form">
|
||||
<select id="model-select"></select>
|
||||
</form>
|
||||
|
||||
<script type="module">
|
||||
import TRMNLPicker from '@trmnl/picker'
|
||||
const picker = new TRMNLPicker('picker-form', models, palettes)
|
||||
</script>
|
||||
```
|
||||
|
||||
## Browser Support
|
||||
|
||||
- Modern browsers with ES6+ support
|
||||
- Chrome 51+
|
||||
- Firefox 54+
|
||||
- Safari 10+
|
||||
- Edge 15+
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Build all formats
|
||||
npm run build
|
||||
|
||||
# Build and watch for changes
|
||||
npm run watch
|
||||
|
||||
# Build specific formats
|
||||
npm run build:esm # ES module
|
||||
npm run build:browser # IIFE browser bundle
|
||||
npm run build:browser:min # Minified browser bundle
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please open an issue or submit a pull request.
|
||||
|
||||
## Support
|
||||
|
||||
For issues and questions, please use the [GitHub issue tracker](https://github.com/trmnl/trmnl-picker/issues).
|
||||
@@ -0,0 +1,337 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TRMNL Picker - Browser Example</title>
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 2rem;
|
||||
background: #f5f5f5;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #333;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #666;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.picker-wrapper {
|
||||
background: white;
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.picker-form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.picker-select,
|
||||
.picker-button {
|
||||
padding: 0.5rem 1rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.picker-select:hover,
|
||||
.picker-button:hover:not(:disabled) {
|
||||
background: #f9f9f9;
|
||||
border-color: #ccc;
|
||||
}
|
||||
|
||||
.picker-select:focus,
|
||||
.picker-button:focus {
|
||||
outline: none;
|
||||
border-color: #4a90e2;
|
||||
box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.1);
|
||||
}
|
||||
|
||||
.picker-button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.divider {
|
||||
width: 1px;
|
||||
height: 2rem;
|
||||
background: #e0e0e0;
|
||||
margin: 0 0.25rem;
|
||||
}
|
||||
|
||||
.output-section {
|
||||
background: white;
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.output-section h2 {
|
||||
margin: 0 0 1rem 0;
|
||||
color: #333;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.output-content {
|
||||
background: #f8f9fa;
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
overflow-x: auto;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.screen-preview {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.screen-preview h2 {
|
||||
margin: 0 0 1.5rem 0;
|
||||
color: #333;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.screen {
|
||||
display: inline-block;
|
||||
border: 2px solid #333;
|
||||
background: white;
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.screen--trmnl_original {
|
||||
width: 400px;
|
||||
height: 240px;
|
||||
}
|
||||
|
||||
.screen--og_plus {
|
||||
width: 296px;
|
||||
height: 128px;
|
||||
}
|
||||
|
||||
.screen--pro {
|
||||
width: 800px;
|
||||
height: 480px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.screen--portrait {
|
||||
transform: rotate(90deg);
|
||||
transform-origin: center;
|
||||
margin: 100px 0;
|
||||
}
|
||||
|
||||
.screen--dark-mode {
|
||||
background: #1a1a1a;
|
||||
border-color: #666;
|
||||
}
|
||||
|
||||
.screen-content {
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.screen--dark-mode .screen-content {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.palette-bw { background: white; color: black; }
|
||||
.palette-4c { background: linear-gradient(to right, #fff, #ff0, #f00, #000); }
|
||||
.palette-7c { background: linear-gradient(to right, #fff, #ff0, #f80, #f00, #00f, #0f0, #000); }
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: #4a90e2;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>@trmnl/picker - Browser Example</h1>
|
||||
<p>Vanilla JavaScript library for TRMNL device model and palette selection</p>
|
||||
|
||||
<div class="picker-wrapper">
|
||||
<form id="picker-form" class="picker-form">
|
||||
<select id="model-select" class="picker-select"></select>
|
||||
|
||||
<select id="palette-select" class="picker-select"></select>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<button type="button" id="orientation-toggle" class="picker-button">
|
||||
<span data-orientation-text>Landscape</span>
|
||||
</button>
|
||||
|
||||
<button type="button" id="dark-mode-toggle" class="picker-button">
|
||||
<span data-dark-mode-text>Light Mode</span>
|
||||
</button>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<button type="button" id="reset-button" class="picker-button">
|
||||
Reset
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="output-section">
|
||||
<h2>Event Output <span class="badge" id="event-count">0 events</span></h2>
|
||||
<div class="output-content">
|
||||
<pre id="event-output">Waiting for changes...</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="screen-preview">
|
||||
<h2>Screen Preview</h2>
|
||||
<div class="screen">
|
||||
<div class="screen-content">
|
||||
<strong>TRMNL Screen Preview</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="../dist/trmnl-picker.min.js"></script>
|
||||
<script>
|
||||
// Sample data (simulating API responses)
|
||||
const models = [
|
||||
{
|
||||
name: 'trmnl_original',
|
||||
label: 'TRMNL Original',
|
||||
description: 'Original TRMNL model',
|
||||
width: 800,
|
||||
height: 480,
|
||||
colors: 2,
|
||||
bit_depth: 1,
|
||||
scale_factor: 1.0,
|
||||
rotation: 90,
|
||||
mime_type: 'image/png',
|
||||
offset_x: 10,
|
||||
offset_y: 20,
|
||||
published_at: '2023-10-01T12:00:00Z',
|
||||
kind: 'trmnl',
|
||||
size: '2.9',
|
||||
palette_ids: ['bw', '4c', '7c']
|
||||
},
|
||||
{
|
||||
name: 'og_plus',
|
||||
label: 'TRMNL OG+',
|
||||
description: 'OG Plus model',
|
||||
width: 296,
|
||||
height: 128,
|
||||
size: '2.9',
|
||||
palette_ids: ['bw', '4c']
|
||||
},
|
||||
{
|
||||
name: 'pro',
|
||||
label: 'TRMNL Pro',
|
||||
description: 'Pro model with full color',
|
||||
width: 800,
|
||||
height: 480,
|
||||
size: '7.3',
|
||||
palette_ids: ['7c', 'bw']
|
||||
}
|
||||
]
|
||||
|
||||
const palettes = [
|
||||
{ id: 'bw', name: 'Black & White', framework_class: 'palette-bw' },
|
||||
{ id: '4c', name: '4-Color', framework_class: 'palette-4c' },
|
||||
{ id: '7c', name: '7-Color', framework_class: 'palette-7c' }
|
||||
]
|
||||
|
||||
// Initialize picker
|
||||
const picker = new TRMNLPicker('picker-form', models, palettes)
|
||||
|
||||
// Track events
|
||||
let eventCount = 0
|
||||
|
||||
// Listen for changes
|
||||
document.getElementById('picker-form').addEventListener('changed', (event) => {
|
||||
eventCount++
|
||||
document.getElementById('event-count').textContent = `${eventCount} event${eventCount !== 1 ? 's' : ''}`
|
||||
|
||||
const { screenClasses, state } = event.detail
|
||||
|
||||
// Display event data
|
||||
document.getElementById('event-output').textContent = JSON.stringify({
|
||||
screenClasses,
|
||||
state
|
||||
}, null, 2)
|
||||
|
||||
// Apply classes to screen preview
|
||||
const screen = document.querySelector('.screen')
|
||||
const currentClasses = screen.className.split(' ')
|
||||
|
||||
// Remove old screen classes
|
||||
const filteredClasses = currentClasses.filter(c => {
|
||||
if (!c.startsWith('screen--')) return true
|
||||
if (c.startsWith('screen--scale-')) return true
|
||||
if (c === 'screen--no-bleed') return true
|
||||
return false
|
||||
})
|
||||
|
||||
// Add new screen classes
|
||||
filteredClasses.push(...screenClasses)
|
||||
screen.className = filteredClasses.join(' ')
|
||||
|
||||
console.log('Picker changed:', event.detail)
|
||||
})
|
||||
|
||||
// Example: Programmatic update after 3 seconds
|
||||
setTimeout(() => {
|
||||
console.log('Programmatically updating picker...')
|
||||
picker.update({
|
||||
modelName: 'pro',
|
||||
paletteId: '7c',
|
||||
isPortrait: false,
|
||||
isDarkMode: false
|
||||
})
|
||||
}, 3000)
|
||||
|
||||
// Display current state in console
|
||||
console.log('Initial state:', picker.getState())
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+446
@@ -0,0 +1,446 @@
|
||||
{
|
||||
"name": "@trmnl/picker",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@trmnl/picker",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
|
||||
"integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz",
|
||||
"integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz",
|
||||
"integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz",
|
||||
"integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz",
|
||||
"integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz",
|
||||
"integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz",
|
||||
"integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz",
|
||||
"integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz",
|
||||
"integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz",
|
||||
"integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
|
||||
"integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.20.2",
|
||||
"@esbuild/android-arm": "0.20.2",
|
||||
"@esbuild/android-arm64": "0.20.2",
|
||||
"@esbuild/android-x64": "0.20.2",
|
||||
"@esbuild/darwin-arm64": "0.20.2",
|
||||
"@esbuild/darwin-x64": "0.20.2",
|
||||
"@esbuild/freebsd-arm64": "0.20.2",
|
||||
"@esbuild/freebsd-x64": "0.20.2",
|
||||
"@esbuild/linux-arm": "0.20.2",
|
||||
"@esbuild/linux-arm64": "0.20.2",
|
||||
"@esbuild/linux-ia32": "0.20.2",
|
||||
"@esbuild/linux-loong64": "0.20.2",
|
||||
"@esbuild/linux-mips64el": "0.20.2",
|
||||
"@esbuild/linux-ppc64": "0.20.2",
|
||||
"@esbuild/linux-riscv64": "0.20.2",
|
||||
"@esbuild/linux-s390x": "0.20.2",
|
||||
"@esbuild/linux-x64": "0.20.2",
|
||||
"@esbuild/netbsd-x64": "0.20.2",
|
||||
"@esbuild/openbsd-x64": "0.20.2",
|
||||
"@esbuild/sunos-x64": "0.20.2",
|
||||
"@esbuild/win32-arm64": "0.20.2",
|
||||
"@esbuild/win32-ia32": "0.20.2",
|
||||
"@esbuild/win32-x64": "0.20.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "@trmnl/picker",
|
||||
"version": "1.0.0",
|
||||
"description": "Vanilla JavaScript library for TRMNL device model and palette selection",
|
||||
"main": "dist/trmnl-picker.js",
|
||||
"module": "dist/trmnl-picker.esm.js",
|
||||
"browser": "dist/trmnl-picker.min.js",
|
||||
"files": [
|
||||
"dist",
|
||||
"src",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "npm run build:esm && npm run build:browser && npm run build:browser:min",
|
||||
"build:esm": "esbuild src/index.js --bundle --format=esm --outfile=dist/trmnl-picker.esm.js --sourcemap",
|
||||
"build:browser": "esbuild src/index.js --bundle --format=iife --global-name=TRMNLPicker --outfile=dist/trmnl-picker.js --sourcemap --footer:js=\"TRMNLPicker=TRMNLPicker.default;\"",
|
||||
"build:browser:min": "esbuild src/index.js --bundle --format=iife --global-name=TRMNLPicker --minify --outfile=dist/trmnl-picker.min.js --sourcemap --footer:js=\"TRMNLPicker=TRMNLPicker.default;\"",
|
||||
"watch": "npm run build:browser -- --watch",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"keywords": [
|
||||
"trmnl",
|
||||
"picker",
|
||||
"screen",
|
||||
"device",
|
||||
"palette",
|
||||
"vanilla-js"
|
||||
],
|
||||
"author": "TRMNL",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.20.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/trmnl/trmnl-picker.git"
|
||||
}
|
||||
}
|
||||
+465
@@ -0,0 +1,465 @@
|
||||
/**
|
||||
* TRMNLPicker - Vanilla JS library for TRMNL device and palette selection
|
||||
*
|
||||
* @class TRMNLPicker
|
||||
* @param {string} formId - ID of the form element containing picker controls
|
||||
* @param {Array<Object>} models - Array of model objects from API
|
||||
* @param {Array<Object>} palettes - Array of palette objects
|
||||
*/
|
||||
class TRMNLPicker {
|
||||
constructor(formId, models, palettes) {
|
||||
// Validate inputs
|
||||
if (!formId || typeof formId !== 'string') {
|
||||
throw new Error('TRMNLPicker: formId must be a non-empty string')
|
||||
}
|
||||
|
||||
if (!Array.isArray(models) || models.length === 0) {
|
||||
throw new Error('TRMNLPicker: models must be a non-empty array')
|
||||
}
|
||||
|
||||
if (!Array.isArray(palettes) || palettes.length === 0) {
|
||||
throw new Error('TRMNLPicker: palettes must be a non-empty array')
|
||||
}
|
||||
|
||||
// Store references
|
||||
this.formElement = document.getElementById(formId)
|
||||
if (!this.formElement) {
|
||||
throw new Error(`TRMNLPicker: Form element with id "${formId}" not found`)
|
||||
}
|
||||
|
||||
this.models = models
|
||||
this.palettes = palettes
|
||||
|
||||
// Initialize state
|
||||
this.state = {
|
||||
selectedModel: null,
|
||||
selectedPalette: null,
|
||||
isPortrait: false,
|
||||
isDarkMode: false
|
||||
}
|
||||
|
||||
// Initialize DOM elements and bind events
|
||||
this._initializeElements()
|
||||
this._bindEvents()
|
||||
|
||||
// Set initial state
|
||||
this._setInitialState()
|
||||
}
|
||||
|
||||
/**
|
||||
* Find and store references to form elements
|
||||
* @private
|
||||
*/
|
||||
_initializeElements() {
|
||||
this.elements = {
|
||||
modelSelect: this.formElement.querySelector('#model-select'),
|
||||
paletteSelect: this.formElement.querySelector('#palette-select'),
|
||||
orientationToggle: this.formElement.querySelector('#orientation-toggle'),
|
||||
darkModeToggle: this.formElement.querySelector('#dark-mode-toggle'),
|
||||
resetButton: this.formElement.querySelector('#reset-button'),
|
||||
|
||||
// Optional: UI indicator elements
|
||||
orientationText: this.formElement.querySelector('[data-orientation-text]'),
|
||||
darkModeText: this.formElement.querySelector('[data-dark-mode-text]')
|
||||
}
|
||||
|
||||
// Validate required elements
|
||||
const required = ['modelSelect', 'paletteSelect']
|
||||
for (const key of required) {
|
||||
if (!this.elements[key]) {
|
||||
throw new Error(`TRMNLPicker: Required element "${key}" not found in form`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind event listeners to form elements
|
||||
* @private
|
||||
*/
|
||||
_bindEvents() {
|
||||
// Store bound handlers for cleanup
|
||||
this.handlers = {
|
||||
modelChange: this._handleModelChange.bind(this),
|
||||
paletteChange: this._handlePaletteChange.bind(this),
|
||||
orientationToggle: this._toggleOrientation.bind(this),
|
||||
darkModeToggle: this._toggleDarkMode.bind(this),
|
||||
reset: this._resetToDefaults.bind(this)
|
||||
}
|
||||
|
||||
// Attach event listeners
|
||||
this.elements.modelSelect.addEventListener('change', this.handlers.modelChange)
|
||||
this.elements.paletteSelect.addEventListener('change', this.handlers.paletteChange)
|
||||
|
||||
if (this.elements.orientationToggle) {
|
||||
this.elements.orientationToggle.addEventListener('click', this.handlers.orientationToggle)
|
||||
}
|
||||
|
||||
if (this.elements.darkModeToggle) {
|
||||
this.elements.darkModeToggle.addEventListener('click', this.handlers.darkModeToggle)
|
||||
}
|
||||
|
||||
if (this.elements.resetButton) {
|
||||
this.elements.resetButton.addEventListener('click', this.handlers.reset)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set initial state and populate form
|
||||
* @private
|
||||
*/
|
||||
_setInitialState() {
|
||||
// Populate model select with all models
|
||||
this.elements.modelSelect.innerHTML = ''
|
||||
this.models.forEach(model => {
|
||||
const option = document.createElement('option')
|
||||
option.value = model.name
|
||||
option.textContent = model.label || model.name
|
||||
this.elements.modelSelect.appendChild(option)
|
||||
})
|
||||
|
||||
// Set default model (first model or specific default)
|
||||
const defaultModel = this.models[0]
|
||||
this.elements.modelSelect.value = defaultModel.name
|
||||
this.state.selectedModel = defaultModel
|
||||
|
||||
// Populate palettes based on selected model
|
||||
this._populatePalettes()
|
||||
|
||||
// Set first palette as default
|
||||
const firstPaletteId = defaultModel.palette_ids[0]
|
||||
this.elements.paletteSelect.value = firstPaletteId
|
||||
this.state.selectedPalette = this.palettes.find(p => p.id === firstPaletteId)
|
||||
|
||||
// Update UI
|
||||
this._updateResetButton()
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate palette dropdown based on selected model
|
||||
* @private
|
||||
*/
|
||||
_populatePalettes() {
|
||||
const modelName = this.elements.modelSelect.value
|
||||
const model = this.models.find(m => m.name === modelName)
|
||||
|
||||
if (!model) return
|
||||
|
||||
// Clear existing options
|
||||
this.elements.paletteSelect.innerHTML = ''
|
||||
|
||||
// Add options for each palette_id in the model
|
||||
model.palette_ids.forEach(paletteId => {
|
||||
const palette = this.palettes.find(p => p.id === paletteId)
|
||||
if (palette) {
|
||||
const option = document.createElement('option')
|
||||
option.value = palette.id
|
||||
option.textContent = palette.name
|
||||
this.elements.paletteSelect.appendChild(option)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate screen classes based on current state
|
||||
* @private
|
||||
* @returns {Array<string>} Array of CSS class names
|
||||
*/
|
||||
_calculateScreenClasses() {
|
||||
const model = this.state.selectedModel
|
||||
const palette = this.state.selectedPalette
|
||||
|
||||
if (!model) {
|
||||
throw new Error('No model selected')
|
||||
}
|
||||
|
||||
const classes = []
|
||||
|
||||
// 1. Palette framework class
|
||||
if (palette && palette.framework_class) {
|
||||
classes.push(palette.framework_class)
|
||||
}
|
||||
|
||||
// 2. Model name
|
||||
classes.push(`screen--${model.name}`)
|
||||
|
||||
// 3. Model size
|
||||
if (model.size) {
|
||||
classes.push(`screen--${model.size}`)
|
||||
}
|
||||
|
||||
// 4. Orientation
|
||||
classes.push(`screen--${this.state.isPortrait ? 'portrait' : 'landscape'}`)
|
||||
|
||||
// 5. Scale (always 1x)
|
||||
classes.push('screen--1x')
|
||||
|
||||
// 6. Dark mode (conditional)
|
||||
if (this.state.isDarkMode) {
|
||||
classes.push('screen--dark-mode')
|
||||
}
|
||||
|
||||
return classes
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit 'changed' event with current state and screen classes
|
||||
* @private
|
||||
*/
|
||||
_emitChangeEvent() {
|
||||
const model = this.state.selectedModel
|
||||
const palette = this.state.selectedPalette
|
||||
|
||||
const event = new CustomEvent('changed', {
|
||||
detail: {
|
||||
screenClasses: this._calculateScreenClasses(),
|
||||
state: {
|
||||
model: model ? {
|
||||
name: model.name,
|
||||
label: model.label,
|
||||
size: model.size,
|
||||
width: model.width,
|
||||
height: model.height
|
||||
} : null,
|
||||
palette: palette ? {
|
||||
id: palette.id,
|
||||
name: palette.name,
|
||||
framework_class: palette.framework_class
|
||||
} : null,
|
||||
isPortrait: this.state.isPortrait,
|
||||
isDarkMode: this.state.isDarkMode
|
||||
}
|
||||
},
|
||||
bubbles: true
|
||||
})
|
||||
|
||||
this.formElement.dispatchEvent(event)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle model selection change
|
||||
* @private
|
||||
*/
|
||||
_handleModelChange(event) {
|
||||
const modelName = event.target.value
|
||||
const model = this.models.find(m => m.name === modelName)
|
||||
|
||||
if (!model) return
|
||||
|
||||
// Update state
|
||||
this.state.selectedModel = model
|
||||
|
||||
// Repopulate palettes for new model
|
||||
this._populatePalettes()
|
||||
|
||||
// Select first palette of new model
|
||||
const firstPaletteId = model.palette_ids[0]
|
||||
this.elements.paletteSelect.value = firstPaletteId
|
||||
this.state.selectedPalette = this.palettes.find(p => p.id === firstPaletteId)
|
||||
|
||||
// Update UI
|
||||
this._updateResetButton()
|
||||
|
||||
// Emit change event
|
||||
this._emitChangeEvent()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle palette selection change
|
||||
* @private
|
||||
*/
|
||||
_handlePaletteChange(event) {
|
||||
const paletteId = event.target.value
|
||||
const palette = this.palettes.find(p => p.id === paletteId)
|
||||
|
||||
this.state.selectedPalette = palette
|
||||
|
||||
// Update UI
|
||||
this._updateResetButton()
|
||||
|
||||
// Emit change event
|
||||
this._emitChangeEvent()
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle orientation between portrait and landscape
|
||||
* @private
|
||||
*/
|
||||
_toggleOrientation() {
|
||||
this.state.isPortrait = !this.state.isPortrait
|
||||
|
||||
// Update optional UI elements
|
||||
if (this.elements.orientationText) {
|
||||
this.elements.orientationText.textContent = this.state.isPortrait ? 'Portrait' : 'Landscape'
|
||||
}
|
||||
|
||||
// Emit change event
|
||||
this._emitChangeEvent()
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle dark mode on/off
|
||||
* @private
|
||||
*/
|
||||
_toggleDarkMode() {
|
||||
this.state.isDarkMode = !this.state.isDarkMode
|
||||
|
||||
// Update optional UI elements
|
||||
if (this.elements.darkModeText) {
|
||||
this.elements.darkModeText.textContent = this.state.isDarkMode ? 'Dark Mode' : 'Light Mode'
|
||||
}
|
||||
|
||||
// Emit change event
|
||||
this._emitChangeEvent()
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset palette to model's default (first palette)
|
||||
* @private
|
||||
*/
|
||||
_resetToDefaults() {
|
||||
const model = this.state.selectedModel
|
||||
if (!model) return
|
||||
|
||||
// Reset to first palette of current model
|
||||
const firstPaletteId = model.palette_ids[0]
|
||||
this.elements.paletteSelect.value = firstPaletteId
|
||||
this.state.selectedPalette = this.palettes.find(p => p.id === firstPaletteId)
|
||||
|
||||
// Update UI
|
||||
this._updateResetButton()
|
||||
|
||||
// Emit change event
|
||||
this._emitChangeEvent()
|
||||
}
|
||||
|
||||
/**
|
||||
* Update reset button enabled/disabled state
|
||||
* @private
|
||||
*/
|
||||
_updateResetButton() {
|
||||
if (!this.elements.resetButton) return
|
||||
|
||||
const model = this.state.selectedModel
|
||||
if (!model) return
|
||||
|
||||
const isAtDefaults = this.elements.paletteSelect.value === String(model.palette_ids[0])
|
||||
|
||||
this.elements.resetButton.disabled = isAtDefaults
|
||||
|
||||
if (isAtDefaults) {
|
||||
this.elements.resetButton.classList.add('opacity-50', 'cursor-default')
|
||||
this.elements.resetButton.setAttribute('aria-disabled', 'true')
|
||||
} else {
|
||||
this.elements.resetButton.classList.remove('opacity-50', 'cursor-default')
|
||||
this.elements.resetButton.removeAttribute('aria-disabled')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update picker with new configuration
|
||||
* @public
|
||||
* @param {Object} config - Configuration object
|
||||
* @param {string} config.modelName - Model name to select
|
||||
* @param {string} config.paletteId - Palette ID to select
|
||||
* @param {boolean} config.isPortrait - Portrait orientation
|
||||
* @param {boolean} config.isDarkMode - Dark mode enabled
|
||||
*/
|
||||
update(config) {
|
||||
if (!config || typeof config !== 'object') {
|
||||
throw new Error('TRMNLPicker.update: config must be an object')
|
||||
}
|
||||
|
||||
let changed = false
|
||||
|
||||
// Update model if provided
|
||||
if (config.modelName) {
|
||||
const model = this.models.find(m => m.name === config.modelName)
|
||||
if (model) {
|
||||
this.elements.modelSelect.value = model.name
|
||||
this.state.selectedModel = model
|
||||
this._populatePalettes()
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
|
||||
// Update palette if provided
|
||||
if (config.paletteId) {
|
||||
const palette = this.palettes.find(p => p.id === config.paletteId)
|
||||
if (palette) {
|
||||
this.elements.paletteSelect.value = palette.id
|
||||
this.state.selectedPalette = palette
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
|
||||
// Update orientation if provided
|
||||
if (typeof config.isPortrait === 'boolean') {
|
||||
this.state.isPortrait = config.isPortrait
|
||||
if (this.elements.orientationText) {
|
||||
this.elements.orientationText.textContent = this.state.isPortrait ? 'Portrait' : 'Landscape'
|
||||
}
|
||||
changed = true
|
||||
}
|
||||
|
||||
// Update dark mode if provided
|
||||
if (typeof config.isDarkMode === 'boolean') {
|
||||
this.state.isDarkMode = config.isDarkMode
|
||||
if (this.elements.darkModeText) {
|
||||
this.elements.darkModeText.textContent = this.state.isDarkMode ? 'Dark Mode' : 'Light Mode'
|
||||
}
|
||||
changed = true
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
this._updateResetButton()
|
||||
this._emitChangeEvent()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current picker state
|
||||
* @public
|
||||
* @returns {Object} Current state including model, palette, flags, and screen classes
|
||||
*/
|
||||
getState() {
|
||||
return {
|
||||
model: this.state.selectedModel,
|
||||
palette: this.state.selectedPalette,
|
||||
isPortrait: this.state.isPortrait,
|
||||
isDarkMode: this.state.isDarkMode,
|
||||
screenClasses: this._calculateScreenClasses()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up event listeners and references
|
||||
* @public
|
||||
*/
|
||||
destroy() {
|
||||
// Remove event listeners
|
||||
this.elements.modelSelect.removeEventListener('change', this.handlers.modelChange)
|
||||
this.elements.paletteSelect.removeEventListener('change', this.handlers.paletteChange)
|
||||
|
||||
if (this.elements.orientationToggle) {
|
||||
this.elements.orientationToggle.removeEventListener('click', this.handlers.orientationToggle)
|
||||
}
|
||||
|
||||
if (this.elements.darkModeToggle) {
|
||||
this.elements.darkModeToggle.removeEventListener('click', this.handlers.darkModeToggle)
|
||||
}
|
||||
|
||||
if (this.elements.resetButton) {
|
||||
this.elements.resetButton.removeEventListener('click', this.handlers.reset)
|
||||
}
|
||||
|
||||
// Clear references
|
||||
this.formElement = null
|
||||
this.elements = null
|
||||
this.handlers = null
|
||||
this.models = null
|
||||
this.palettes = null
|
||||
this.state = null
|
||||
}
|
||||
}
|
||||
|
||||
export default TRMNLPicker
|
||||
Reference in New Issue
Block a user