aboutsummaryrefslogtreecommitdiff
path: root/scripts/theme-studio
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/theme-studio')
-rw-r--r--scripts/theme-studio/app-core.js9
-rw-r--r--scripts/theme-studio/app.js7
-rw-r--r--scripts/theme-studio/browser-gates.js18
-rw-r--r--scripts/theme-studio/styles.css3
-rw-r--r--scripts/theme-studio/test-app-core.mjs19
-rw-r--r--scripts/theme-studio/theme-studio.html37
-rw-r--r--scripts/theme-studio/theme-studio.template.html2
7 files changed, 91 insertions, 4 deletions
diff --git a/scripts/theme-studio/app-core.js b/scripts/theme-studio/app-core.js
index df99a0d37..74b441b96 100644
--- a/scripts/theme-studio/app-core.js
+++ b/scripts/theme-studio/app-core.js
@@ -417,6 +417,13 @@ function appViewKeysSorted(apps){
String((apps[b]&&apps[b].label)||b), undefined, {sensitivity:'base'}));
}
+// The prev/next arrows step the view-dropdown selection by DIR (-1/+1), clamped
+// to [0, LEN-1] with no wrap. An empty list (LEN<=0) keeps CUR.
+function stepViewIndex(cur,len,dir){
+ if(!(len>0)) return cur;
+ return Math.max(0, Math.min(len-1, cur+dir));
+}
+
// Which of the six per-face setting boxes (fg, bg, style, inherit, height, box)
// differ from the face's seed default, so the table can mark a non-default box.
// A non-default height looks identical to the default in the number input, so the
@@ -436,4 +443,4 @@ function faceBoxNonDefaults(cur,def){
};
}
-export { nameToHex, normalizePkgFace, buildPkgmap, packagesForExport, mergePackagesInto, effResolve, resolveSyntaxFg, resolveUiAttr, dropdownRowTextColor, paletteOptionList, galleryModel, appViewKeysSorted, faceBoxNonDefaults, spanNeighborHex, slugify, fgSetFor, floor, lMax, COVERED_FACES, columnsFromPalette, usedPaletteHexes, paletteUsages, regenColumn, rankByLightness, stepRepointPlan, sortColumns, sortColumnMembers, groundRoleOfEntry, groundColumnMembersFromPalette, clearPalettePlan, deletePaletteColumnPlan, areAllLocked, lockToggleLabel, toggleLockSet };
+export { nameToHex, normalizePkgFace, buildPkgmap, packagesForExport, mergePackagesInto, effResolve, resolveSyntaxFg, resolveUiAttr, dropdownRowTextColor, paletteOptionList, galleryModel, appViewKeysSorted, faceBoxNonDefaults, stepViewIndex, spanNeighborHex, slugify, fgSetFor, floor, lMax, COVERED_FACES, columnsFromPalette, usedPaletteHexes, paletteUsages, regenColumn, rankByLightness, stepRepointPlan, sortColumns, sortColumnMembers, groundRoleOfEntry, groundColumnMembersFromPalette, clearPalettePlan, deletePaletteColumnPlan, areAllLocked, lockToggleLabel, toggleLockSet };
diff --git a/scripts/theme-studio/app.js b/scripts/theme-studio/app.js
index 07ca06fe1..f2c322aaf 100644
--- a/scripts/theme-studio/app.js
+++ b/scripts/theme-studio/app.js
@@ -528,6 +528,13 @@ function buildViewSel(){const s=document.getElementById('viewsel');if(!s)return;
const og=document.createElement('optgroup');og.label='package faces';
for(const app of appViewKeysSorted(APPS))og.appendChild(mk(app,APPS[app].label));
s.appendChild(og);}
+// The ‹ › buttons flanking the dropdown step the selection by DIR and re-render
+// the view (faces table + preview), so you can walk the list without reopening it.
+function stepView(dir){
+ const s=document.getElementById('viewsel');if(!s)return;
+ const i=stepViewIndex(s.selectedIndex,s.options.length,dir);
+ if(i!==s.selectedIndex){s.selectedIndex=i;onViewChange();}
+}
function onViewChange(){const s=document.getElementById('viewsel');const v=(s&&s.value)||'@code';
const show=(id,on)=>{const e=document.getElementById(id);if(e)e.style.display=on?'':'none';};
show('view-code',v==='@code');show('view-ui',v==='@ui');show('view-pkg',v[0]!=='@');
diff --git a/scripts/theme-studio/browser-gates.js b/scripts/theme-studio/browser-gates.js
index 7c8b05d3f..b03ec6a47 100644
--- a/scripts/theme-studio/browser-gates.js
+++ b/scripts/theme-studio/browser-gates.js
@@ -730,6 +730,24 @@ if(location.hash==='#crtest'){let ok=true;const notes=[];const A=(c,n)=>{if(!c){
A(span&&span.title&&/(passes|fails) WCAG/i.test(span.title),'contrast cell carries a WCAG hover: '+(span&&span.title));
document.title='CRTEST '+(ok?'PASS':'FAIL');
const d=document.createElement('div');d.id='crtest';d.textContent='CRTEST '+(ok?'PASS':'FAIL')+(notes.length?' fails='+notes.join(','):'');document.body.appendChild(d);}
+// View-nav gate (open with #navtest): the prev/next arrows flanking the view
+// dropdown step the selection (clamped, no wrap) and re-render the view.
+if(location.hash==='#navtest'){let ok=true;const notes=[];const A=(c,n)=>{if(!c){ok=false;notes.push(n);}};
+ const sel=document.getElementById('viewsel'),prev=document.getElementById('viewprev'),next=document.getElementById('viewnext');
+ A(!!prev&&!!next,'nav arrows exist');
+ if(sel&&prev&&next){
+ const vis=id=>{const e=document.getElementById(id);return !!e&&e.style.display!=='none';};
+ sel.selectedIndex=0;onViewChange();
+ next.click();A(sel.selectedIndex===1,'next advances the selection');
+ prev.click();A(sel.selectedIndex===0,'prev steps back');
+ prev.click();A(sel.selectedIndex===0,'prev clamps at the first option');
+ sel.selectedIndex=sel.options.length-1;onViewChange();
+ next.click();A(sel.selectedIndex===sel.options.length-1,'next clamps at the last option');
+ sel.selectedIndex=2;onViewChange();
+ A(sel.options[2]&&sel.options[2].value[0]!=='@'&&vis('view-pkg'),'stepping to a package shows the pkg view');
+ }
+ document.title='NAVTEST '+(ok?'PASS':'FAIL');
+ const d=document.createElement('div');d.id='navtest';d.textContent='NAVTEST '+(ok?'PASS':'FAIL')+(notes.length?' fails='+notes.join(','):'');document.body.appendChild(d);}
// Box-cluster gate (open with #boxtest): the box control is a 2x2 cluster of
// four radio buttons (none / line / pressed / raised); the color swatch shows
// only while a box style is active.
diff --git a/scripts/theme-studio/styles.css b/scripts/theme-studio/styles.css
index 9c8b5aac9..a90c649ab 100644
--- a/scripts/theme-studio/styles.css
+++ b/scripts/theme-studio/styles.css
@@ -23,6 +23,9 @@
.stylecluster .sbtn{margin:0}
table.leg th:hover{color:#e8bd30}
select.chip{appearance:none;border:1px solid #00000060;border-radius:5px;padding:5px 10px;font:bold 14px monospace;width:160px;cursor:pointer}
+ /* Prev/next arrows flanking the view dropdown: step the selection without reopening it. */
+ .viewnav{appearance:none;border:1px solid #00000060;border-radius:5px;background:#1f1c19;color:#e8bd30;font:bold 16px monospace;width:24px;height:30px;padding:0;margin:0 4px;cursor:pointer;vertical-align:middle}
+ .viewnav:hover{border-color:#e8bd30}
/* Non-default marker: a small gold corner flag on a per-face setting cell whose
value differs from the face's default. The size box looks identical default
or not, so the flag is the only at-a-glance cue that a value was changed. */
diff --git a/scripts/theme-studio/test-app-core.mjs b/scripts/theme-studio/test-app-core.mjs
index 20f3d5734..8f62ae55a 100644
--- a/scripts/theme-studio/test-app-core.mjs
+++ b/scripts/theme-studio/test-app-core.mjs
@@ -9,7 +9,7 @@ import { fileURLToPath } from 'node:url';
import {
nameToHex, normalizePkgFace, buildPkgmap, packagesForExport, mergePackagesInto, effResolve, resolveSyntaxFg, resolveUiAttr, dropdownRowTextColor, paletteOptionList, spanNeighborHex, slugify,
clearPalettePlan, deletePaletteColumnPlan, groundColumnMembersFromPalette, areAllLocked, lockToggleLabel, toggleLockSet,
- galleryModel, appViewKeysSorted, faceBoxNonDefaults,
+ galleryModel, appViewKeysSorted, faceBoxNonDefaults, stepViewIndex,
} from './app-core.js';
import { planPaletteGenerator, entriesForGeneratedColumn } from './palette-generator-core.js';
import { oklch2hex, deltaE } from './colormath.js';
@@ -878,3 +878,20 @@ test('faceBoxNonDefaults: nullish inputs flag nothing', () => {
assert.deepEqual(faceBoxNonDefaults(null, null),
{ fg: false, bg: false, style: false, inherit: false, height: false, box: false });
});
+
+// stepViewIndex: the prev/next arrows step the view-dropdown selection, clamped
+// to the option range (no wrap).
+test('stepViewIndex: steps forward and back within range', () => {
+ assert.equal(stepViewIndex(2, 5, 1), 3);
+ assert.equal(stepViewIndex(2, 5, -1), 1);
+});
+test('stepViewIndex: clamps at both ends, no wrap', () => {
+ assert.equal(stepViewIndex(0, 5, -1), 0);
+ assert.equal(stepViewIndex(4, 5, 1), 4);
+});
+test('stepViewIndex: a single option or empty list stays put', () => {
+ assert.equal(stepViewIndex(0, 1, 1), 0);
+ assert.equal(stepViewIndex(0, 1, -1), 0);
+ assert.equal(stepViewIndex(3, 0, -1), 3);
+ assert.equal(stepViewIndex(0, 0, 1), 0);
+});
diff --git a/scripts/theme-studio/theme-studio.html b/scripts/theme-studio/theme-studio.html
index c4dd7149d..84c9ea59e 100644
--- a/scripts/theme-studio/theme-studio.html
+++ b/scripts/theme-studio/theme-studio.html
@@ -25,6 +25,9 @@
.stylecluster .sbtn{margin:0}
table.leg th:hover{color:#e8bd30}
select.chip{appearance:none;border:1px solid #00000060;border-radius:5px;padding:5px 10px;font:bold 14px monospace;width:160px;cursor:pointer}
+ /* Prev/next arrows flanking the view dropdown: step the selection without reopening it. */
+ .viewnav{appearance:none;border:1px solid #00000060;border-radius:5px;background:#1f1c19;color:#e8bd30;font:bold 16px monospace;width:24px;height:30px;padding:0;margin:0 4px;cursor:pointer;vertical-align:middle}
+ .viewnav:hover{border-color:#e8bd30}
/* Non-default marker: a small gold corner flag on a per-face setting cell whose
value differs from the face's default. The size box looks identical default
or not, so the flag is the only at-a-glance cue that a value was changed. */
@@ -223,7 +226,7 @@
<div class="pals" id="pals"></div>
</section>
<h1>assignment</h1>
-<div class="pkgbar"><label style="color:#b4b1a2">view</label><select id="viewsel" class="chip" style="width:auto;font:bold 10pt monospace" onchange="onViewChange()"></select></div>
+<div class="pkgbar"><label style="color:#b4b1a2">view</label><button id="viewprev" class="viewnav" title="previous in the list" onclick="stepView(-1)">&lsaquo;</button><select id="viewsel" class="chip" style="width:auto;font:bold 10pt monospace" onchange="onViewChange()"></select><button id="viewnext" class="viewnav" title="next in the list" onclick="stepView(1)">&rsaquo;</button></div>
<div id="view-code" class="viewblock">
<div class="cols">
<section class="pane">
@@ -927,6 +930,13 @@ function appViewKeysSorted(apps){
String((apps[b]&&apps[b].label)||b), undefined, {sensitivity:'base'}));
}
+// The prev/next arrows step the view-dropdown selection by DIR (-1/+1), clamped
+// to [0, LEN-1] with no wrap. An empty list (LEN<=0) keeps CUR.
+function stepViewIndex(cur,len,dir){
+ if(!(len>0)) return cur;
+ return Math.max(0, Math.min(len-1, cur+dir));
+}
+
// Which of the six per-face setting boxes (fg, bg, style, inherit, height, box)
// differ from the face's seed default, so the table can mark a non-default box.
// A non-default height looks identical to the default in the number input, so the
@@ -2136,6 +2146,13 @@ function buildViewSel(){const s=document.getElementById('viewsel');if(!s)return;
const og=document.createElement('optgroup');og.label='package faces';
for(const app of appViewKeysSorted(APPS))og.appendChild(mk(app,APPS[app].label));
s.appendChild(og);}
+// The ‹ › buttons flanking the dropdown step the selection by DIR and re-render
+// the view (faces table + preview), so you can walk the list without reopening it.
+function stepView(dir){
+ const s=document.getElementById('viewsel');if(!s)return;
+ const i=stepViewIndex(s.selectedIndex,s.options.length,dir);
+ if(i!==s.selectedIndex){s.selectedIndex=i;onViewChange();}
+}
function onViewChange(){const s=document.getElementById('viewsel');const v=(s&&s.value)||'@code';
const show=(id,on)=>{const e=document.getElementById(id);if(e)e.style.display=on?'':'none';};
show('view-code',v==='@code');show('view-ui',v==='@ui');show('view-pkg',v[0]!=='@');
@@ -3390,6 +3407,24 @@ if(location.hash==='#crtest'){let ok=true;const notes=[];const A=(c,n)=>{if(!c){
A(span&&span.title&&/(passes|fails) WCAG/i.test(span.title),'contrast cell carries a WCAG hover: '+(span&&span.title));
document.title='CRTEST '+(ok?'PASS':'FAIL');
const d=document.createElement('div');d.id='crtest';d.textContent='CRTEST '+(ok?'PASS':'FAIL')+(notes.length?' fails='+notes.join(','):'');document.body.appendChild(d);}
+// View-nav gate (open with #navtest): the prev/next arrows flanking the view
+// dropdown step the selection (clamped, no wrap) and re-render the view.
+if(location.hash==='#navtest'){let ok=true;const notes=[];const A=(c,n)=>{if(!c){ok=false;notes.push(n);}};
+ const sel=document.getElementById('viewsel'),prev=document.getElementById('viewprev'),next=document.getElementById('viewnext');
+ A(!!prev&&!!next,'nav arrows exist');
+ if(sel&&prev&&next){
+ const vis=id=>{const e=document.getElementById(id);return !!e&&e.style.display!=='none';};
+ sel.selectedIndex=0;onViewChange();
+ next.click();A(sel.selectedIndex===1,'next advances the selection');
+ prev.click();A(sel.selectedIndex===0,'prev steps back');
+ prev.click();A(sel.selectedIndex===0,'prev clamps at the first option');
+ sel.selectedIndex=sel.options.length-1;onViewChange();
+ next.click();A(sel.selectedIndex===sel.options.length-1,'next clamps at the last option');
+ sel.selectedIndex=2;onViewChange();
+ A(sel.options[2]&&sel.options[2].value[0]!=='@'&&vis('view-pkg'),'stepping to a package shows the pkg view');
+ }
+ document.title='NAVTEST '+(ok?'PASS':'FAIL');
+ const d=document.createElement('div');d.id='navtest';d.textContent='NAVTEST '+(ok?'PASS':'FAIL')+(notes.length?' fails='+notes.join(','):'');document.body.appendChild(d);}
// Box-cluster gate (open with #boxtest): the box control is a 2x2 cluster of
// four radio buttons (none / line / pressed / raised); the color swatch shows
// only while a box style is active.
diff --git a/scripts/theme-studio/theme-studio.template.html b/scripts/theme-studio/theme-studio.template.html
index 06c3e2bc5..5f41eb66d 100644
--- a/scripts/theme-studio/theme-studio.template.html
+++ b/scripts/theme-studio/theme-studio.template.html
@@ -58,7 +58,7 @@ STYLES_CSS</style>
<div class="pals" id="pals"></div>
</section>
<h1>assignment</h1>
-<div class="pkgbar"><label style="color:#b4b1a2">view</label><select id="viewsel" class="chip" style="width:auto;font:bold 10pt monospace" onchange="onViewChange()"></select></div>
+<div class="pkgbar"><label style="color:#b4b1a2">view</label><button id="viewprev" class="viewnav" title="previous in the list" onclick="stepView(-1)">&lsaquo;</button><select id="viewsel" class="chip" style="width:auto;font:bold 10pt monospace" onchange="onViewChange()"></select><button id="viewnext" class="viewnav" title="next in the list" onclick="stepView(1)">&rsaquo;</button></div>
<div id="view-code" class="viewblock">
<div class="cols">
<section class="pane">