diff --git a/frontend/help-ru.html b/frontend/help-ru.html
index 35837ed..133eac2 100644
--- a/frontend/help-ru.html
+++ b/frontend/help-ru.html
@@ -497,7 +497,7 @@
';
diff --git a/frontend/style.css b/frontend/style.css
index 37880ca..1d31825 100644
--- a/frontend/style.css
+++ b/frontend/style.css
@@ -612,6 +612,10 @@ video {
}
}
+.pointer-events-none {
+ pointer-events: none;
+}
+
.visible {
visibility: visible;
}
@@ -1036,6 +1040,10 @@ video {
list-style-type: none;
}
+.grid-cols-1 {
+ grid-template-columns: repeat(1, minmax(0, 1fr));
+}
+
.grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
@@ -1159,6 +1167,12 @@ video {
margin-bottom: calc(0.75rem * var(--tw-space-y-reverse));
}
+.space-y-4 > :not([hidden]) ~ :not([hidden]) {
+ --tw-space-y-reverse: 0;
+ margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse)));
+ margin-bottom: calc(1rem * var(--tw-space-y-reverse));
+}
+
.overflow-auto {
overflow: auto;
}
@@ -1185,6 +1199,10 @@ video {
white-space: pre-wrap;
}
+.break-words {
+ overflow-wrap: break-word;
+}
+
.break-all {
word-break: break-all;
}
@@ -1850,6 +1868,11 @@ video {
color: rgb(251 191 36 / var(--tw-text-opacity, 1));
}
+.dark\:text-amber-500:is(.dark *) {
+ --tw-text-opacity: 1;
+ color: rgb(245 158 11 / var(--tw-text-opacity, 1));
+}
+
.dark\:text-blue-400:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(96 165 250 / var(--tw-text-opacity, 1));
@@ -1905,6 +1928,11 @@ video {
color: rgb(100 116 139 / var(--tw-text-opacity, 1));
}
+.dark\:text-slate-600:is(.dark *) {
+ --tw-text-opacity: 1;
+ color: rgb(71 85 105 / var(--tw-text-opacity, 1));
+}
+
.dark\:text-white:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity, 1));
@@ -1947,6 +1975,24 @@ video {
.dark\:hover\:text-slate-300:hover:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(203 213 225 / var(--tw-text-opacity, 1));
+}
+
+@media (min-width: 1024px) {
+ .lg\:col-span-1 {
+ grid-column: span 1 / span 1;
+ }
+
+ .lg\:col-span-2 {
+ grid-column: span 2 / span 2;
+ }
+
+ .lg\:col-span-3 {
+ grid-column: span 3 / span 3;
+ }
+
+ .lg\:grid-cols-3 {
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ }
}/* ===== Theme contrast adjustments ===== */
/* Light theme: darken softer (muted) text one step for higher contrast */
diff --git a/frontend/sw.js b/frontend/sw.js
index 0ca8a2b..a434e5a 100644
--- a/frontend/sw.js
+++ b/frontend/sw.js
@@ -1,4 +1,4 @@
-const CACHE = 'simple-v231';
+const CACHE = 'simple-v232';
const URLS = [
'index.html',
'help.html',
diff --git a/frontend/tests/esim.test.js b/frontend/tests/esim.test.js
index 4ecf8f9..c131064 100644
--- a/frontend/tests/esim.test.js
+++ b/frontend/tests/esim.test.js
@@ -24,7 +24,7 @@ function extractFunc(src, name) {
let code = '';
for (const fn of ['esimLabel', 'esimGroupEid', 'esimFieldRows', 'esimResultText',
'esimStateLabel', 'esimOperationsText', 'esimProfileRows', 'esimIconDataUrl',
- 'esimSwitchStatus']) {
+ 'esimChipSectionRows', 'esimChipBox', 'esimSwitchStatus']) {
code += extractFunc(html, fn) + '\n';
}
for (const c of ['ESIM_CHIP_LABELS', 'ESIM_RESULT_KEYS']) {
@@ -32,6 +32,7 @@ for (const c of ['ESIM_CHIP_LABELS', 'ESIM_RESULT_KEYS']) {
.replace('const ', 'var ') + '\n';
}
code += 'function t(s){return s;}\n';
+code += 'function esc(s){return String(s);}\n';
eval(code);
test('esimGroupEid groups the hex digits in fours', () => {
@@ -153,6 +154,39 @@ test('the profile card renders the icon image next to the rows', () => {
assert.match(fn, /
;)
{
+ assert.deepStrictEqual(esimChipSectionRows({ svn: '2.2.2' }), [['SVN', '2.2.2']]);
+ assert.deepStrictEqual(esimChipSectionRows(null), []);
+ assert.deepStrictEqual(esimChipSectionRows([{ ppr_ids: ['ppr1'] }]),
+ [['Rule 1 / PPR IDs', 'ppr1']]);
+});
+
+test('esimChipBox renders a titled box and skips empty sections', () => {
+ assert.strictEqual(esimChipBox('EUICCInfo1', []), '');
+ assert.strictEqual(esimChipBox('EUICCInfo1', null), '');
+ const box = esimChipBox('EUICCInfo1', [['SVN', '2.2.2'], ['', 'raw-value']]);
+ assert.match(box, /EUICCInfo1/);
+ assert.match(box, /SVN/);
+ assert.match(box, /2\.2\.2/);
+ assert.match(box, /raw-value/);
+});
+
+test('the chip boxes prefer wrapping at spaces over breaking words', () => {
+ const box = esimChipBox('EUICCInfo1', [['SVN', '2.2.2']]);
+ assert.match(box, /break-words/);
+ assert.doesNotMatch(box, /break-all/);
+ const render = extractFunc(html, 'esimRenderProfiles');
+ assert.doesNotMatch(render, /break-all/);
+});
+
+test('the chip view groups the sections into a grid', () => {
+ const fn = extractFunc(html, 'esimRenderChip');
+ assert.match(fn, /lg:grid-cols-3/);
+ assert.match(fn, /lg:col-span-2/);
+ assert.match(fn, /esimChipBox\('EUICCInfo2'/);
+ assert.match(fn, /esimChipSectionRows/);
+});
+
test('the eSIM pill is wired into the Phone simulator tab', () => {
assert.ok(html.includes('data-phone-sub="esim"'));
assert.ok(html.includes('id="phone-sub-esim"'));