UI : éléments répliqués choisis dans la fenêtre de dry-run, menu de la barre retiré
Le choix Répliquer / Ne pas répliquer de la fenêtre de dry-run devient la liste enregistrée des éléments répliqués (administrateurs) : tous les éléments y figurent, avec leurs dépendances, et un changement relance le dry-run. Le menu « Éléments répliqués » de la barre du haut et l'exclusion par exécution (skip) côté interface sont retirés. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
993fe2d8a7
commit
4980cd4c7f
3 files changed
+40
-71
No files matched your search
+36
-61
@@ -13,10 +13,7 @@
|
|||||||
refreshTitle: 'Relire le primaire et les cibles', settings: 'Paramètres', theme: 'Thème clair / sombre',
|
refreshTitle: 'Relire le primaire et les cibles', settings: 'Paramètres', theme: 'Thème clair / sombre',
|
||||||
logout: 'Se déconnecter', viewAria: 'Affichage', viewDiff: 'Écarts', viewClusters: 'Clusters', viewRuns: 'Journal', viewAudit: 'Audit',
|
logout: 'Se déconnecter', viewAria: 'Affichage', viewDiff: 'Écarts', viewClusters: 'Clusters', viewRuns: 'Journal', viewAudit: 'Audit',
|
||||||
kindAria: "Type d'objet", statusAria: 'Statut', modeAria: 'Mode', dryAll: 'Dry-run', syncAll: 'Répliquer',
|
kindAria: "Type d'objet", statusAria: 'Statut', modeAria: 'Mode', dryAll: 'Dry-run', syncAll: 'Répliquer',
|
||||||
kindMenu: 'Éléments répliqués', targetMenu: 'Cibles', allTargets: 'Toutes les cibles', noTarget: 'Aucune cible sélectionnée.',
|
targetMenu: 'Cibles', allTargets: 'Toutes les cibles', noTarget: 'Aucune cible sélectionnée.',
|
||||||
kindMenuRO: 'Seuls les administrateurs changent les éléments répliqués.',
|
|
||||||
kindMenuHelp: 'Les comptes du realm pve sont répliqués avec leurs mots de passe et ont besoin de leurs groupes : ils sont cochés ensemble.',
|
|
||||||
kindGroups: { dc: 'Datacenter', access: 'Comptes et droits (realm pve)', sdn: 'SDN' },
|
|
||||||
setupClusters: 'Base vide : ajoutez vos clusters (URL et mot de passe root), puis choisissez le primaire.', setupClustersBtn: 'Ajouter des clusters',
|
setupClusters: 'Base vide : ajoutez vos clusters (URL et mot de passe root), puis choisissez le primaire.', setupClustersBtn: 'Ajouter des clusters',
|
||||||
setupPrimary: 'Aucun cluster primaire : choisissez la source de la réplication dans la vue Clusters.', setupPrimaryBtn: 'Choisir le primaire',
|
setupPrimary: 'Aucun cluster primaire : choisissez la source de la réplication dans la vue Clusters.', setupPrimaryBtn: 'Choisir le primaire',
|
||||||
stripAria: 'Clusters', colKind: 'Type', colObject: 'Objet', colStatus: 'Statut', colFields: 'Champs',
|
stripAria: 'Clusters', colKind: 'Type', colObject: 'Objet', colStatus: 'Statut', colFields: 'Champs',
|
||||||
@@ -100,8 +97,9 @@
|
|||||||
language: 'Langue', themeTitle: 'Thème', themeSystem: 'Système', themeLight: 'Clair', themeDark: 'Sombre',
|
language: 'Langue', themeTitle: 'Thème', themeSystem: 'Système', themeLight: 'Clair', themeDark: 'Sombre',
|
||||||
density: 'Densité', densityNormal: 'Normale', densityCompact: 'Compacte',
|
density: 'Densité', densityNormal: 'Normale', densityCompact: 'Compacte',
|
||||||
noRights: 'Votre rôle ne permet pas de répliquer.', errorPrefix: 'Erreur : ',
|
noRights: 'Votre rôle ne permet pas de répliquer.', errorPrefix: 'Erreur : ',
|
||||||
planKinds: 'Éléments de cette exécution', kindOn: 'Répliquer', kindOff: 'Ne pas répliquer',
|
planKinds: 'Éléments répliqués', kindOn: 'Répliquer', kindOff: 'Ne pas répliquer',
|
||||||
planKindsHelp: "Un élément exclu n'est pas écrit, sauf ce dont les autres ont besoin (un groupe à créer pour un compte). Changer un choix relance le dry-run.",
|
planKindsHelp: 'Choix enregistré pour toutes les réplications. Les comptes du realm pve vont avec leurs mots de passe et ont besoin de leurs groupes. Changer un choix relance le dry-run.',
|
||||||
|
planKindsRO: 'Seuls les administrateurs changent les éléments répliqués.',
|
||||||
redacted: 'masqué',
|
redacted: 'masqué',
|
||||||
polDef: {
|
polDef: {
|
||||||
'datacenter-options': 'tag-style, registered-tags, user-tag-access, next-id : sync ; le reste : ignore ; webauthn, u2f, http_proxy : jamais',
|
'datacenter-options': 'tag-style, registered-tags, user-tag-access, next-id : sync ; le reste : ignore ; webauthn, u2f, http_proxy : jamais',
|
||||||
@@ -117,10 +115,7 @@
|
|||||||
refreshTitle: 'Read the primary and the targets again', settings: 'Settings', theme: 'Light / dark theme',
|
refreshTitle: 'Read the primary and the targets again', settings: 'Settings', theme: 'Light / dark theme',
|
||||||
logout: 'Sign out', viewAria: 'View', viewDiff: 'Differences', viewClusters: 'Clusters', viewRuns: 'Journal', viewAudit: 'Audit',
|
logout: 'Sign out', viewAria: 'View', viewDiff: 'Differences', viewClusters: 'Clusters', viewRuns: 'Journal', viewAudit: 'Audit',
|
||||||
kindAria: 'Object kind', statusAria: 'Status', modeAria: 'Mode', dryAll: 'Dry-run', syncAll: 'Replicate',
|
kindAria: 'Object kind', statusAria: 'Status', modeAria: 'Mode', dryAll: 'Dry-run', syncAll: 'Replicate',
|
||||||
kindMenu: 'Replicated items', targetMenu: 'Targets', allTargets: 'All targets', noTarget: 'No target selected.',
|
targetMenu: 'Targets', allTargets: 'All targets', noTarget: 'No target selected.',
|
||||||
kindMenuRO: 'Only administrators change the replicated items.',
|
|
||||||
kindMenuHelp: 'The accounts of the pve realm are replicated with their passwords and need their groups: they are checked together.',
|
|
||||||
kindGroups: { dc: 'Datacenter', access: 'Accounts and permissions (pve realm)', sdn: 'SDN' },
|
|
||||||
setupClusters: 'Empty database: add your clusters (URL and root password), then choose the primary.', setupClustersBtn: 'Add clusters',
|
setupClusters: 'Empty database: add your clusters (URL and root password), then choose the primary.', setupClustersBtn: 'Add clusters',
|
||||||
setupPrimary: 'No primary cluster: choose the source of the replication in the Clusters view.', setupPrimaryBtn: 'Choose the primary',
|
setupPrimary: 'No primary cluster: choose the source of the replication in the Clusters view.', setupPrimaryBtn: 'Choose the primary',
|
||||||
stripAria: 'Clusters', colKind: 'Kind', colObject: 'Object', colStatus: 'Status', colFields: 'Fields',
|
stripAria: 'Clusters', colKind: 'Kind', colObject: 'Object', colStatus: 'Status', colFields: 'Fields',
|
||||||
@@ -204,8 +199,9 @@
|
|||||||
language: 'Language', themeTitle: 'Theme', themeSystem: 'System', themeLight: 'Light', themeDark: 'Dark',
|
language: 'Language', themeTitle: 'Theme', themeSystem: 'System', themeLight: 'Light', themeDark: 'Dark',
|
||||||
density: 'Density', densityNormal: 'Normal', densityCompact: 'Compact',
|
density: 'Density', densityNormal: 'Normal', densityCompact: 'Compact',
|
||||||
noRights: 'Your role cannot replicate.', errorPrefix: 'Error: ',
|
noRights: 'Your role cannot replicate.', errorPrefix: 'Error: ',
|
||||||
planKinds: 'Items of this run', kindOn: 'Replicate', kindOff: 'Do not replicate',
|
planKinds: 'Replicated items', kindOn: 'Replicate', kindOff: 'Do not replicate',
|
||||||
planKindsHelp: 'An excluded item is not written, except what the others need (a group to create for an account). Changing a choice runs the dry-run again.',
|
planKindsHelp: 'Choice saved for every replication. The accounts of the pve realm go with their passwords and need their groups. Changing a choice runs the dry-run again.',
|
||||||
|
planKindsRO: 'Only administrators change the replicated items.',
|
||||||
redacted: 'hidden',
|
redacted: 'hidden',
|
||||||
polDef: {
|
polDef: {
|
||||||
'datacenter-options': 'tag-style, registered-tags, user-tag-access, next-id: sync; the rest: ignore; webauthn, u2f, http_proxy: never',
|
'datacenter-options': 'tag-style, registered-tags, user-tag-access, next-id: sync; the rest: ignore; webauthn, u2f, http_proxy: never',
|
||||||
@@ -218,7 +214,6 @@
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
const KINDS = ['datacenter-options', 'pve-group', 'pve-role', 'pve-user', 'pve-password', 'sdn-zone', 'sdn-vnet', 'sdn-subnet'];
|
const KINDS = ['datacenter-options', 'pve-group', 'pve-role', 'pve-user', 'pve-password', 'sdn-zone', 'sdn-vnet', 'sdn-subnet'];
|
||||||
const KIND_GROUPS = [['dc', ['datacenter-options']], ['access', ['pve-group', 'pve-role', 'pve-user']], ['sdn', ['sdn-zone', 'sdn-vnet', 'sdn-subnet']]];
|
|
||||||
// kinds chosen as one item: the accounts of the pve realm with their passwords
|
// kinds chosen as one item: the accounts of the pve realm with their passwords
|
||||||
const BUNDLES = { 'pve-user': ['pve-password'] };
|
const BUNDLES = { 'pve-user': ['pve-password'] };
|
||||||
const ITEMS = KINDS.filter((k) => !Object.values(BUNDLES).some((b) => b.includes(k)));
|
const ITEMS = KINDS.filter((k) => !Object.values(BUNDLES).some((b) => b.includes(k)));
|
||||||
@@ -548,7 +543,7 @@
|
|||||||
$('#setupBtn').textContent = t(S.setup === 'clusters' ? 'setupClustersBtn' : 'setupPrimaryBtn');
|
$('#setupBtn').textContent = t(S.setup === 'clusters' ? 'setupClustersBtn' : 'setupPrimaryBtn');
|
||||||
}
|
}
|
||||||
$$('[data-admin-only]').forEach((e) => { e.hidden = !can.admin(); });
|
$$('[data-admin-only]').forEach((e) => { e.hidden = !can.admin(); });
|
||||||
if (S.view === 'diff') { renderDiff(); renderKindMenu(); renderTargetMenu(); }
|
if (S.view === 'diff') { renderDiff(); renderTargetMenu(); }
|
||||||
if (S.view === 'clusters') renderClusters();
|
if (S.view === 'clusters') renderClusters();
|
||||||
if (S.view === 'runs') renderRuns();
|
if (S.view === 'runs') renderRuns();
|
||||||
if (S.view === 'audit') renderAudit();
|
if (S.view === 'audit') renderAudit();
|
||||||
@@ -802,22 +797,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// renderPlanKinds shows, above the plan, one "replicate / do not replicate"
|
// renderPlanKinds shows, above the plan, one "replicate / do not replicate"
|
||||||
// choice per replicated item: a change runs the dry-run again without the
|
// choice per item: it is the saved list of replicated items (administrators),
|
||||||
// items left out, so the reviewed plan is the one applied.
|
// and a change runs the dry-run again, so the reviewed plan is the one applied.
|
||||||
function renderPlanKinds(sel, plans, intentSync) {
|
function renderPlanKinds(sel, plans, intentSync) {
|
||||||
const box = $('#planKinds');
|
const box = $('#planKinds');
|
||||||
const kinds = (S.settings && S.settings.kinds) || KINDS;
|
box.hidden = sel.scope === 'object' || !S.settings;
|
||||||
const items = ITEMS.filter((k) => kinds.includes(k));
|
|
||||||
box.hidden = sel.scope === 'object' || items.length < 2;
|
|
||||||
if (box.hidden) return;
|
if (box.hidden) return;
|
||||||
const skip = new Set(sel.skip || []);
|
const kinds = S.settings.kinds || KINDS;
|
||||||
|
const admin = can.admin();
|
||||||
const count = (k) => plans.reduce((n, p) => n + ((p.plan && p.plan.changes) || []).filter((c) => bundled(k).includes(c.kind)).length, 0);
|
const count = (k) => plans.reduce((n, p) => n + ((p.plan && p.plan.changes) || []).filter((c) => bundled(k).includes(c.kind)).length, 0);
|
||||||
const rows = items.map((k) => {
|
const rows = ITEMS.map((k) => {
|
||||||
const off = skip.has(k);
|
const off = !kinds.includes(k);
|
||||||
const set = (v) => {
|
const set = async (v) => {
|
||||||
const next = new Set(skip);
|
if (await saveKinds(withDeps(kinds, k, v))) openPlan(sel, intentSync);
|
||||||
bundled(k).forEach((x) => (v ? next.delete(x) : next.add(x)));
|
|
||||||
openPlan(Object.assign({}, sel, { skip: KINDS.filter((x) => next.has(x)) }), intentSync);
|
|
||||||
};
|
};
|
||||||
const n = off ? '—' : String(count(k));
|
const n = off ? '—' : String(count(k));
|
||||||
return h('div', { class: 'pk-row' + (off ? ' off' : '') },
|
return h('div', { class: 'pk-row' + (off ? ' off' : '') },
|
||||||
@@ -825,10 +817,23 @@
|
|||||||
h('span', { class: 'pk-name', text: t('kinds.' + k) }),
|
h('span', { class: 'pk-name', text: t('kinds.' + k) }),
|
||||||
h('span', { class: 'pk-n', text: n, title: off ? '' : t('changesN', { n }) }),
|
h('span', { class: 'pk-n', text: n, title: off ? '' : t('changesN', { n }) }),
|
||||||
h('div', { class: 'seg', role: 'radiogroup', 'aria-label': t('kinds.' + k) },
|
h('div', { class: 'seg', role: 'radiogroup', 'aria-label': t('kinds.' + k) },
|
||||||
h('button', { type: 'button', role: 'radio', class: off ? '' : 'on', 'aria-checked': String(!off), onclick: () => off && set(true) }, t('kindOn')),
|
h('button', { type: 'button', role: 'radio', class: off ? '' : 'on', 'aria-checked': String(!off), disabled: !admin, onclick: () => off && set(true) }, t('kindOn')),
|
||||||
h('button', { type: 'button', role: 'radio', class: off ? 'on' : '', 'aria-checked': String(off), onclick: () => !off && set(false) }, t('kindOff'))));
|
h('button', { type: 'button', role: 'radio', class: off ? 'on' : '', 'aria-checked': String(off), disabled: !admin, onclick: () => !off && set(false) }, t('kindOff'))));
|
||||||
});
|
});
|
||||||
box.replaceChildren(h('div', { class: 'pk-head' }, h('strong', { text: t('planKinds') }), h('span', { class: 'muted small', text: t('planKindsHelp') })), ...rows);
|
box.replaceChildren(h('div', { class: 'pk-head' }, h('strong', { text: t('planKinds') }), h('span', { class: 'muted small', text: t(admin ? 'planKindsHelp' : 'planKindsRO') })), ...rows);
|
||||||
|
}
|
||||||
|
// saveKinds saves the list of replicated items (at least one).
|
||||||
|
async function saveKinds(kinds) {
|
||||||
|
if (!kinds.length) return false;
|
||||||
|
const body = Object.assign({}, S.settings, { kinds });
|
||||||
|
try {
|
||||||
|
await api('PUT', 'api/settings', body);
|
||||||
|
S.settings = body;
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
if (e.message !== 'auth') toast(e.message, true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeList(changes) {
|
function changeList(changes) {
|
||||||
@@ -1506,7 +1511,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- menus of the filter bar: replicated items, targets ----------
|
// ---------- replicated items, targets of the global actions ----------
|
||||||
// withDeps checks or unchecks a kind with the kinds it needs (or that need it).
|
// withDeps checks or unchecks a kind with the kinds it needs (or that need it).
|
||||||
function withDeps(kinds, k, on) {
|
function withDeps(kinds, k, on) {
|
||||||
const out = new Set(kinds);
|
const out = new Set(kinds);
|
||||||
@@ -1515,36 +1520,6 @@
|
|||||||
if (on) need(k); else bundled(k).forEach(drop);
|
if (on) need(k); else bundled(k).forEach(drop);
|
||||||
return KINDS.filter((x) => out.has(x));
|
return KINDS.filter((x) => out.has(x));
|
||||||
}
|
}
|
||||||
function renderKindMenu() {
|
|
||||||
const pop = $('#kindMenuPop');
|
|
||||||
const kinds = (S.settings && S.settings.kinds) || KINDS;
|
|
||||||
$('#kindMenuN').textContent = ITEMS.filter((k) => kinds.includes(k)).length + '/' + ITEMS.length;
|
|
||||||
const admin = can.admin();
|
|
||||||
pop.replaceChildren();
|
|
||||||
for (const [g, ks] of KIND_GROUPS) {
|
|
||||||
pop.appendChild(h('div', { class: 'grp', text: t('kindGroups.' + g) }));
|
|
||||||
for (const k of ks) {
|
|
||||||
pop.appendChild(h('label', { class: 'opt' },
|
|
||||||
h('input', { type: 'checkbox', checked: kinds.includes(k), disabled: !admin || !S.settings,
|
|
||||||
onchange: (ev) => saveKinds(withDeps(kinds, k, ev.target.checked)) }),
|
|
||||||
h('span', { class: 'kind ' + k, text: t('kindShort.' + k) }), h('span', { text: t('kinds.' + k) })));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pop.appendChild(h('div', { class: 'foot' }, h('span', { class: 'note-s', text: admin ? t('kindMenuHelp') : t('kindMenuRO') })));
|
|
||||||
}
|
|
||||||
async function saveKinds(kinds) {
|
|
||||||
if (!kinds.length) { renderKindMenu(); return; }
|
|
||||||
const body = Object.assign({}, S.settings, { kinds });
|
|
||||||
try {
|
|
||||||
await api('PUT', 'api/settings', body);
|
|
||||||
S.settings = body;
|
|
||||||
renderKindMenu();
|
|
||||||
loadState(true);
|
|
||||||
} catch (e) {
|
|
||||||
if (e.message !== 'auth') toast(e.message, true);
|
|
||||||
renderKindMenu();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// targets of the global dry-run / replication (all by default; the ones
|
// targets of the global dry-run / replication (all by default; the ones
|
||||||
// unchecked are remembered in this browser)
|
// unchecked are remembered in this browser)
|
||||||
function selectedTargets() {
|
function selectedTargets() {
|
||||||
@@ -1727,7 +1702,7 @@
|
|||||||
const d = await api('GET', 'api/settings');
|
const d = await api('GET', 'api/settings');
|
||||||
S.settings = d.settings;
|
S.settings = d.settings;
|
||||||
S.deps = d.deps || {};
|
S.deps = d.deps || {};
|
||||||
} catch (e) { /* the menu stays read only */ }
|
} catch (e) { /* the replicated items stay read only */ }
|
||||||
if (S.view === 'clusters') loadClusters();
|
if (S.view === 'clusters') loadClusters();
|
||||||
await loadState(false);
|
await loadState(false);
|
||||||
if (S.view === 'runs') loadRuns(true);
|
if (S.view === 'runs') loadRuns(true);
|
||||||
|
|||||||
@@ -67,14 +67,6 @@
|
|||||||
<span data-i18n="viewAudit">Audit</span>
|
<span data-i18n="viewAudit">Audit</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<details id="kindMenu" class="menu" data-diff>
|
|
||||||
<summary>
|
|
||||||
<!-- Lucide "list-checks" (ISC license) -->
|
|
||||||
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m3 17 2 2 4-4M3 7l2 2 4-4M13 6h8M13 12h8M13 18h8"/></svg>
|
|
||||||
<span data-i18n="kindMenu">Éléments répliqués</span><span class="n" id="kindMenuN"></span>
|
|
||||||
</summary>
|
|
||||||
<div class="pop" id="kindMenuPop"></div>
|
|
||||||
</details>
|
|
||||||
<select id="kindSel" data-diff data-i18n-aria="kindAria"></select>
|
<select id="kindSel" data-diff data-i18n-aria="kindAria"></select>
|
||||||
<select id="statusSel" data-diff data-i18n-aria="statusAria"></select>
|
<select id="statusSel" data-diff data-i18n-aria="statusAria"></select>
|
||||||
<select id="runModeSel" data-runs data-i18n-aria="modeAria" hidden></select>
|
<select id="runModeSel" data-runs data-i18n-aria="modeAria" hidden></select>
|
||||||
|
|||||||
+4
-2
@@ -697,7 +697,7 @@ body.login-page { min-height: 100vh; padding: 16px; display: grid; place-items:
|
|||||||
.erow .acts { display: flex; gap: 2px; }
|
.erow .acts { display: flex; gap: 2px; }
|
||||||
.import-res { margin: 10px 0 0; }
|
.import-res { margin: 10px 0 0; }
|
||||||
|
|
||||||
/* ---------- drop-down menus of the filter bar (replicated kinds, targets) ---------- */
|
/* ---------- drop-down menu of the filter bar (targets) ---------- */
|
||||||
.menu { position: relative; }
|
.menu { position: relative; }
|
||||||
.menu > summary {
|
.menu > summary {
|
||||||
list-style: none; display: inline-flex; align-items: center; gap: 6px; height: 32px; padding: 0 10px;
|
list-style: none; display: inline-flex; align-items: center; gap: 6px; height: 32px; padding: 0 10px;
|
||||||
@@ -746,11 +746,13 @@ body.login-page { min-height: 100vh; padding: 16px; display: grid; place-items:
|
|||||||
.setup-form h2 { margin: 0 0 4px; font-size: 15px; text-align: center; }
|
.setup-form h2 { margin: 0 0 4px; font-size: 15px; text-align: center; }
|
||||||
.setup-form p { margin: 0 0 6px; font-size: 12.5px; color: var(--muted); }
|
.setup-form p { margin: 0 0 6px; font-size: 12.5px; color: var(--muted); }
|
||||||
|
|
||||||
/* dry-run: replicate / do not replicate, per item */
|
/* dry-run: replicated items (replicate / do not replicate) */
|
||||||
.plan-kinds { display: grid; gap: 6px; padding: 12px 20px; border-bottom: 1px solid var(--border); background: var(--panel-2); }
|
.plan-kinds { display: grid; gap: 6px; padding: 12px 20px; border-bottom: 1px solid var(--border); background: var(--panel-2); }
|
||||||
.plan-kinds .pk-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px; margin-bottom: 2px; }
|
.plan-kinds .pk-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px; margin-bottom: 2px; }
|
||||||
.plan-kinds .pk-row { display: grid; grid-template-columns: 6.5rem minmax(0, 1fr) 2.5rem auto; align-items: center; gap: 10px; }
|
.plan-kinds .pk-row { display: grid; grid-template-columns: 6.5rem minmax(0, 1fr) 2.5rem auto; align-items: center; gap: 10px; }
|
||||||
.plan-kinds .pk-row.off .pk-name, .plan-kinds .pk-row.off .kind { opacity: .55; }
|
.plan-kinds .pk-row.off .pk-name, .plan-kinds .pk-row.off .kind { opacity: .55; }
|
||||||
.plan-kinds .pk-n { text-align: right; font-variant-numeric: tabular-nums; color: var(--muted); }
|
.plan-kinds .pk-n { text-align: right; font-variant-numeric: tabular-nums; color: var(--muted); }
|
||||||
.plan-kinds .seg button { padding: 3px 10px; font-size: 12px; }
|
.plan-kinds .seg button { padding: 3px 10px; font-size: 12px; }
|
||||||
|
.plan-kinds .seg button:disabled { cursor: default; }
|
||||||
|
.plan-kinds .seg button:disabled:not([aria-checked="true"]) { opacity: .55; }
|
||||||
@media (max-width: 560px) { .plan-kinds .pk-row { grid-template-columns: minmax(0, 1fr) auto; } .plan-kinds .pk-row .kind, .plan-kinds .pk-n { display: none; } }
|
@media (max-width: 560px) { .plan-kinds .pk-row { grid-template-columns: minmax(0, 1fr) auto; } .plan-kinds .pk-row .kind, .plan-kinds .pk-n { display: none; } }
|
||||||
Reference in new issue
Block a user