Zustand DevTools
+1
-1
| { | ||
| "manifest_version": 2, | ||
| "name": "Zustand DevTools", | ||
| "version": "1.8.0", | ||
| "version": "1.10.0", | ||
| "description": "Inspect and debug Zustand state stores in real-time. View state snapshots, track actions, and time-travel through state changes.", | ||
@@ -6,0 +6,0 @@ "author": "Your Name", |
+141
-38
@@ -471,21 +471,73 @@ /** | ||
| // SVG icon strings — single source of truth for copy button states | ||
| const ICON_COPY = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>`; | ||
| const ICON_CHECK = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>`; | ||
| // ── SVG DOM builders (no innerHTML — AMO safe) ─────────────────────────────── | ||
| const _SVG = 'http://www.w3.org/2000/svg'; | ||
| function _mkSvg(w, h, fill, sw, children) { | ||
| const s = document.createElementNS(_SVG, 'svg'); | ||
| s.setAttribute('width', String(w)); s.setAttribute('height', String(h)); | ||
| s.setAttribute('viewBox', '0 0 24 24'); s.setAttribute('fill', fill); | ||
| if (sw) { | ||
| s.setAttribute('stroke', 'currentColor'); s.setAttribute('stroke-width', String(sw)); | ||
| s.setAttribute('stroke-linecap', 'round'); s.setAttribute('stroke-linejoin', 'round'); | ||
| } | ||
| children.forEach(([tag, attrs]) => { | ||
| const el = document.createElementNS(_SVG, tag); | ||
| Object.entries(attrs).forEach(([k, v]) => el.setAttribute(k, v)); | ||
| s.appendChild(el); | ||
| }); | ||
| return s; | ||
| } | ||
| function iconCopy() { | ||
| return _mkSvg(14, 14, 'none', 2, [ | ||
| ['rect', { x:'9', y:'9', width:'13', height:'13', rx:'2' }], | ||
| ['path', { d:'M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1' }], | ||
| ]); | ||
| } | ||
| function iconCheck() { | ||
| return _mkSvg(14, 14, 'none', 2.5, [ | ||
| ['polyline', { points:'20 6 9 17 4 12' }], | ||
| ]); | ||
| } | ||
| function iconPlay() { | ||
| return _mkSvg(12, 12, 'currentColor', null, [ | ||
| ['polygon', { points:'5 3 19 12 5 21 5 3' }], | ||
| ]); | ||
| } | ||
| function iconPause() { | ||
| return _mkSvg(12, 12, 'currentColor', null, [ | ||
| ['rect', { x:'6', y:'5', width:'4', height:'14' }], | ||
| ['rect', { x:'14', y:'5', width:'4', height:'14' }], | ||
| ]); | ||
| } | ||
| function iconReset() { | ||
| return _mkSvg(12, 12, 'none', 2, [ | ||
| ['polyline', { points:'1 4 1 10 7 10' }], | ||
| ['path', { d:'M3.51 15a9 9 0 1 0 .49-3.5' }], | ||
| ]); | ||
| } | ||
| function iconLap() { | ||
| return _mkSvg(12, 12, 'none', 2, [ | ||
| ['path', { d:'M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z' }], | ||
| ['line', { x1:'4', y1:'22', x2:'4', y2:'15' }], | ||
| ]); | ||
| } | ||
| let _copyToastTimer = null; | ||
| function _setBtnIcon(btn, svgNode) { | ||
| btn.textContent = ''; // clears children safely | ||
| btn.appendChild(svgNode); | ||
| } | ||
| function showCopyToast() { | ||
| const toast = document.getElementById('copy-toast'); | ||
| const btn = document.getElementById('btn-copy'); | ||
| // Swap to checkmark icon | ||
| btn.innerHTML = ICON_CHECK; | ||
| _setBtnIcon(btn, iconCheck()); | ||
| btn.classList.add('btn-copy-success'); | ||
| // Toast | ||
| toast.classList.add('visible'); | ||
| // Reset after 2 s | ||
| clearTimeout(_copyToastTimer); | ||
| _copyToastTimer = setTimeout(() => { | ||
| toast.classList.remove('visible'); | ||
| btn.innerHTML = ICON_COPY; | ||
| _setBtnIcon(btn, iconCopy()); | ||
| btn.classList.remove('btn-copy-success'); | ||
@@ -809,9 +861,3 @@ }, 2000); | ||
| // SVG icons used by the timer buttons | ||
| const T_ICON = { | ||
| play: `<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>`, | ||
| pause: `<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="5" width="4" height="14"/><rect x="14" y="5" width="4" height="14"/></svg>`, | ||
| reset: `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 .49-3.5"/></svg>`, | ||
| lap: `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"/><line x1="4" y1="22" x2="4" y2="15"/></svg>`, | ||
| }; | ||
| // Timer SVG icons are provided by iconPlay/iconPause/iconReset/iconLap (defined above) | ||
@@ -841,9 +887,15 @@ function timerFormatMs(ms) { | ||
| const ssLabel = document.createElement('span'); | ||
| if (timerState.isRunning) { | ||
| btnSS.innerHTML = T_ICON.pause + '<span>Stop</span>'; | ||
| ssLabel.textContent = 'Stop'; | ||
| btnSS.textContent = ''; | ||
| btnSS.appendChild(iconPause()); | ||
| btnSS.appendChild(ssLabel); | ||
| btnSS.className = 'timer-btn timer-btn-stop'; | ||
| btnLap.disabled = false; | ||
| } else { | ||
| const hasTime = timerState.elapsed > 0; | ||
| btnSS.innerHTML = T_ICON.play + `<span>${hasTime ? 'Resume' : 'Start'}</span>`; | ||
| ssLabel.textContent = timerState.elapsed > 0 ? 'Resume' : 'Start'; | ||
| btnSS.textContent = ''; | ||
| btnSS.appendChild(iconPlay()); | ||
| btnSS.appendChild(ssLabel); | ||
| btnSS.className = 'timer-btn timer-btn-start'; | ||
@@ -907,6 +959,14 @@ btnLap.disabled = true; | ||
| row.className = 'timer-lap-row' + (isLatest ? ' timer-lap-latest' : ''); | ||
| row.innerHTML = | ||
| `<span class="timer-lap-num">Lap ${lap.num}</span>` + | ||
| `<span class="timer-lap-split">+${timerFormatMs(lap.split)}</span>` + | ||
| `<span class="timer-lap-total">${timerFormatMs(lap.total)}</span>`; | ||
| const numEl = document.createElement('span'); | ||
| numEl.className = 'timer-lap-num'; | ||
| numEl.textContent = `Lap ${lap.num}`; | ||
| const splitEl = document.createElement('span'); | ||
| splitEl.className = 'timer-lap-split'; | ||
| splitEl.textContent = `+${timerFormatMs(lap.split)}`; | ||
| const totalEl = document.createElement('span'); | ||
| totalEl.className = 'timer-lap-total'; | ||
| totalEl.textContent = timerFormatMs(lap.total); | ||
| row.appendChild(numEl); | ||
| row.appendChild(splitEl); | ||
| row.appendChild(totalEl); | ||
| list.appendChild(row); | ||
@@ -928,21 +988,64 @@ }); | ||
| $c.innerHTML = ` | ||
| <div class="timer-wrap"> | ||
| <div class="timer-display-wrap"> | ||
| <div class="timer-label">ELAPSED</div> | ||
| <div id="timer-display" class="timer-display">00:00.00</div> | ||
| </div> | ||
| <div class="timer-controls"> | ||
| <button id="timer-btn-ss" class="timer-btn timer-btn-start">${T_ICON.play}<span>Start</span></button> | ||
| <button id="timer-btn-reset" class="timer-btn timer-btn-reset">${T_ICON.reset}<span>Reset</span></button> | ||
| <button id="timer-btn-lap" class="timer-btn timer-btn-lap" disabled>${T_ICON.lap}<span>Lap</span></button> | ||
| </div> | ||
| <div id="timer-laps" class="timer-laps"></div> | ||
| </div>`; | ||
| // Build entire timer DOM with createElement — no innerHTML (AMO safe) | ||
| const wrap = document.createElement('div'); | ||
| wrap.className = 'timer-wrap'; | ||
| document.getElementById('timer-btn-ss').addEventListener('click', () => { | ||
| // Display | ||
| const dispWrap = document.createElement('div'); | ||
| dispWrap.className = 'timer-display-wrap'; | ||
| const labelEl = document.createElement('div'); | ||
| labelEl.className = 'timer-label'; | ||
| labelEl.textContent = 'ELAPSED'; | ||
| const dispEl = document.createElement('div'); | ||
| dispEl.id = 'timer-display'; | ||
| dispEl.className = 'timer-display'; | ||
| dispEl.textContent = '00:00.00'; | ||
| dispWrap.appendChild(labelEl); | ||
| dispWrap.appendChild(dispEl); | ||
| // Controls | ||
| const controls = document.createElement('div'); | ||
| controls.className = 'timer-controls'; | ||
| const btnSS = document.createElement('button'); | ||
| btnSS.id = 'timer-btn-ss'; | ||
| btnSS.className = 'timer-btn timer-btn-start'; | ||
| btnSS.appendChild(iconPlay()); | ||
| const ssSpan = document.createElement('span'); ssSpan.textContent = 'Start'; | ||
| btnSS.appendChild(ssSpan); | ||
| const btnReset = document.createElement('button'); | ||
| btnReset.id = 'timer-btn-reset'; | ||
| btnReset.className = 'timer-btn timer-btn-reset'; | ||
| btnReset.appendChild(iconReset()); | ||
| const resetSpan = document.createElement('span'); resetSpan.textContent = 'Reset'; | ||
| btnReset.appendChild(resetSpan); | ||
| const btnLap = document.createElement('button'); | ||
| btnLap.id = 'timer-btn-lap'; | ||
| btnLap.className = 'timer-btn timer-btn-lap'; | ||
| btnLap.disabled = true; | ||
| btnLap.appendChild(iconLap()); | ||
| const lapSpan = document.createElement('span'); lapSpan.textContent = 'Lap'; | ||
| btnLap.appendChild(lapSpan); | ||
| controls.appendChild(btnSS); | ||
| controls.appendChild(btnReset); | ||
| controls.appendChild(btnLap); | ||
| // Laps list | ||
| const lapsEl = document.createElement('div'); | ||
| lapsEl.id = 'timer-laps'; | ||
| lapsEl.className = 'timer-laps'; | ||
| wrap.appendChild(dispWrap); | ||
| wrap.appendChild(controls); | ||
| wrap.appendChild(lapsEl); | ||
| $c.appendChild(wrap); | ||
| btnSS.addEventListener('click', () => { | ||
| timerState.isRunning ? timerStop() : timerStart(); | ||
| }); | ||
| document.getElementById('timer-btn-reset').addEventListener('click', timerReset); | ||
| document.getElementById('timer-btn-lap').addEventListener('click', timerLap); | ||
| btnReset.addEventListener('click', timerReset); | ||
| btnLap.addEventListener('click', timerLap); | ||
@@ -949,0 +1052,0 @@ timerTick(); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet