Improve server picker scrolling and subscription input layout
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
export const HARBOR_VERSIONS = Object.freeze({
|
export const HARBOR_VERSIONS = Object.freeze({
|
||||||
macClient: '0.23.6',
|
macClient: '0.23.7',
|
||||||
gatewayClient: '0.24.6',
|
gatewayClient: '0.24.7',
|
||||||
gatewayBackend: '0.24.0',
|
gatewayBackend: '0.24.0',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ type PickerServer = HarborServer & {
|
|||||||
const FAVORITES_KEY = 'harbor-server-favorites';
|
const FAVORITES_KEY = 'harbor-server-favorites';
|
||||||
const RECENT_KEY = 'harbor-server-recent';
|
const RECENT_KEY = 'harbor-server-recent';
|
||||||
const AUTO_KEY = 'harbor-server-auto';
|
const AUTO_KEY = 'harbor-server-auto';
|
||||||
const SIMPLE_SERVER_LIMIT = 5;
|
const INLINE_SERVER_ROWS = 8;
|
||||||
|
|
||||||
interface PingResult {
|
interface PingResult {
|
||||||
id?: string;
|
id?: string;
|
||||||
@@ -314,9 +314,9 @@ export function ServerPicker({
|
|||||||
const simpleServers = [
|
const simpleServers = [
|
||||||
...(selected && selected.id !== anchorServerId ? [selected] : []),
|
...(selected && selected.id !== anchorServerId ? [selected] : []),
|
||||||
...servers.filter(({ id }) => id !== selectedServerId && id !== anchorServerId),
|
...servers.filter(({ id }) => id !== selectedServerId && id !== anchorServerId),
|
||||||
].slice(0, SIMPLE_SERVER_LIMIT);
|
];
|
||||||
|
|
||||||
return <section className={`client-servers is-scalable${servers.length <= SIMPLE_SERVER_LIMIT ? ' is-short' : ''}`} aria-label="Выберите сервер">
|
return <section className={`client-servers is-scalable${simpleServers.length <= INLINE_SERVER_ROWS ? ' is-short' : ''}`} aria-label="Выберите сервер">
|
||||||
{prompt && <span className="client-server-prompt">Выберите сервер</span>}
|
{prompt && <span className="client-server-prompt">Выберите сервер</span>}
|
||||||
<div className="client-server-toolbar">
|
<div className="client-server-toolbar">
|
||||||
<span className="client-server-toolbar-title">Список серверов</span>
|
<span className="client-server-toolbar-title">Список серверов</span>
|
||||||
@@ -352,9 +352,6 @@ export function ServerPicker({
|
|||||||
onSelect={select}
|
onSelect={select}
|
||||||
/>)}
|
/>)}
|
||||||
</div>
|
</div>
|
||||||
{servers.length > simpleServers.length && <p className="client-server-overflow-note">
|
|
||||||
Ещё {servers.length - simpleServers.length} — доступны через поиск
|
|
||||||
</p>}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -338,18 +338,16 @@ function AddProfileForm({ feature }: { feature: SubscriptionFeatureController })
|
|||||||
feature.addProfile();
|
feature.addProfile();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<label>
|
<input
|
||||||
<span>Ссылка подписки</span>
|
ref={feature.labelRef}
|
||||||
<input
|
type="url"
|
||||||
ref={feature.labelRef}
|
inputMode="url"
|
||||||
type="url"
|
value={feature.url}
|
||||||
inputMode="url"
|
placeholder="https://…"
|
||||||
value={feature.url}
|
aria-label="Ссылка подписки"
|
||||||
placeholder="https://…"
|
aria-invalid={feature.validationStatus === 'invalid'}
|
||||||
aria-invalid={feature.validationStatus === 'invalid'}
|
onChange={(event) => feature.setUrl(event.target.value)}
|
||||||
onChange={(event) => feature.setUrl(event.target.value)}
|
/>
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
{message && <div className="client-profile-form-status" role="status">{message}</div>}
|
{message && <div className="client-profile-form-status" role="status">{message}</div>}
|
||||||
<div className="client-profile-form-actions">
|
<div className="client-profile-form-actions">
|
||||||
{feature.profiles.length > 0 && <button type="button" onClick={feature.cancelAdd}>Отмена</button>}
|
{feature.profiles.length > 0 && <button type="button" onClick={feature.cancelAdd}>Отмена</button>}
|
||||||
|
|||||||
@@ -348,13 +348,6 @@
|
|||||||
transition-delay: 210ms;
|
transition-delay: 210ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-server-overflow-note {
|
|
||||||
margin: 10px 0 0;
|
|
||||||
color: var(--client-muted);
|
|
||||||
font-size: 9px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-server-tools input {
|
.client-server-tools input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
@@ -450,9 +443,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.client-server-mode-panel.is-simple .client-server-scroll {
|
.client-server-mode-panel.is-simple .client-server-scroll {
|
||||||
max-height: none;
|
max-height: 352px;
|
||||||
overflow: visible;
|
overflow-y: auto;
|
||||||
padding-right: 0;
|
padding-right: 4px;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-server-mode-panel.is-simple .client-server-scroll::-webkit-scrollbar {
|
||||||
|
display: block;
|
||||||
|
width: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-server-scroll.is-leaving {
|
.client-server-scroll.is-leaving {
|
||||||
|
|||||||
@@ -328,7 +328,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.client-subscription-sheet > .client-profile-add {
|
.client-subscription-sheet > .client-profile-add {
|
||||||
margin-top: 8px;
|
margin-top: 10px;
|
||||||
animation: client-profile-add-open 420ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
animation: client-profile-add-open 420ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -389,23 +389,13 @@
|
|||||||
|
|
||||||
.client-profile-add {
|
.client-profile-add {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 10px;
|
gap: 8px;
|
||||||
padding: 10px 0 4px;
|
padding: 0 0 4px;
|
||||||
}
|
|
||||||
|
|
||||||
.client-profile-add label {
|
|
||||||
display: grid;
|
|
||||||
gap: 6px;
|
|
||||||
color: var(--client-muted);
|
|
||||||
font-size: 8px;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-profile-add input {
|
.client-profile-add input {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
height: 40px;
|
height: 36px;
|
||||||
padding: 0 2px;
|
padding: 0 2px;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-bottom: 1px solid color-mix(in oklch, var(--client-border) 72%, transparent);
|
border-bottom: 1px solid color-mix(in oklch, var(--client-border) 72%, transparent);
|
||||||
|
|||||||
@@ -75,8 +75,9 @@ test('server rows scroll inside the subscription drawer without moving the main
|
|||||||
assert.match(scroll, /overflow-y:\s*auto/);
|
assert.match(scroll, /overflow-y:\s*auto/);
|
||||||
assert.match(scroll, /scrollbar-width:\s*none/);
|
assert.match(scroll, /scrollbar-width:\s*none/);
|
||||||
assert.match(styles, /\.client-server-scroll::-webkit-scrollbar\s*\{[\s\S]*display:\s*none/);
|
assert.match(styles, /\.client-server-scroll::-webkit-scrollbar\s*\{[\s\S]*display:\s*none/);
|
||||||
assert.match(simpleScroll, /max-height:\s*none/);
|
assert.match(simpleScroll, /max-height:\s*352px/);
|
||||||
assert.match(simpleScroll, /overflow:\s*visible/);
|
assert.match(simpleScroll, /overflow-y:\s*auto/);
|
||||||
|
assert.match(simpleScroll, /scrollbar-width:\s*thin/);
|
||||||
assert.match(grid, /width:\s*min\(100%, 220px\)/);
|
assert.match(grid, /width:\s*min\(100%, 220px\)/);
|
||||||
assert.match(rule('.client-drawer'), /overflow-y:\s*auto/);
|
assert.match(rule('.client-drawer'), /overflow-y:\s*auto/);
|
||||||
assert.match(subscription, /client-profile-list[\s\S]*feature\.profiles\.map\(\(profile\) => <ProfileGroup/);
|
assert.match(subscription, /client-profile-list[\s\S]*feature\.profiles\.map\(\(profile\) => <ProfileGroup/);
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ test('manual ping uses plain language and keeps results beside server names', ()
|
|||||||
assert.doesNotMatch(picker, /TCP|Не проверен/);
|
assert.doesNotMatch(picker, /TCP|Не проверен/);
|
||||||
assert.match(picker, /<strong>\{server\.label\}<\/strong>[\s\S]*?<\/button>[\s\S]*?client-server-meta/);
|
assert.match(picker, /<strong>\{server\.label\}<\/strong>[\s\S]*?<\/button>[\s\S]*?client-server-meta/);
|
||||||
assert.match(picker, /\[server\.city, server\.country\]\.filter\(Boolean\)\.join\(' · '\)/);
|
assert.match(picker, /\[server\.city, server\.country\]\.filter\(Boolean\)\.join\(' · '\)/);
|
||||||
assert.match(picker, /is-scalable\$\{servers\.length <= SIMPLE_SERVER_LIMIT \? ' is-short' : ''\}/);
|
assert.match(picker, /is-scalable\$\{simpleServers\.length <= INLINE_SERVER_ROWS \? ' is-short' : ''\}/);
|
||||||
assert.match(styles, /\.client-server-row \{[\s\S]*?grid-template-columns: minmax\(0, 1fr\) 120px minmax\(0, 1fr\);/);
|
assert.match(styles, /\.client-server-row \{[\s\S]*?grid-template-columns: minmax\(0, 1fr\) 120px minmax\(0, 1fr\);/);
|
||||||
assert.match(styles, /\.client-server-row \.client-server \{[\s\S]*?width: 120px;/);
|
assert.match(styles, /\.client-server-row \.client-server \{[\s\S]*?width: 120px;/);
|
||||||
assert.match(styles, /\.client-server-health \{[\s\S]*?font-size: 9px;[\s\S]*?font-variant-numeric: tabular-nums;/);
|
assert.match(styles, /\.client-server-health \{[\s\S]*?font-size: 9px;[\s\S]*?font-variant-numeric: tabular-nums;/);
|
||||||
@@ -129,8 +129,9 @@ test('server picker starts simple and reveals advanced controls on demand', () =
|
|||||||
assert.match(picker, /client-server-mode-panel is-advanced/);
|
assert.match(picker, /client-server-mode-panel is-advanced/);
|
||||||
assert.match(picker, /inert={advanced \? true : undefined}/);
|
assert.match(picker, /inert={advanced \? true : undefined}/);
|
||||||
assert.match(picker, /inert={!advanced \? true : undefined}/);
|
assert.match(picker, /inert={!advanced \? true : undefined}/);
|
||||||
assert.match(picker, /const SIMPLE_SERVER_LIMIT = 5/);
|
assert.match(picker, /const INLINE_SERVER_ROWS = 8/);
|
||||||
assert.match(picker, /\.slice\(0, SIMPLE_SERVER_LIMIT\)/);
|
assert.doesNotMatch(picker, /simpleServers[\s\S]*\.slice\(0,/);
|
||||||
|
assert.doesNotMatch(picker, /client-server-overflow-note|доступны через поиск/);
|
||||||
assert.match(picker, /selected && selected\.id !== anchorServerId && <ServerRow/);
|
assert.match(picker, /selected && selected\.id !== anchorServerId && <ServerRow/);
|
||||||
assert.match(picker, /id !== selectedServerId && id !== anchorServerId/);
|
assert.match(picker, /id !== selectedServerId && id !== anchorServerId/);
|
||||||
assert.match(overview, /anchorServerId=\{pickerState\.anchorServerId\}/);
|
assert.match(overview, /anchorServerId=\{pickerState\.anchorServerId\}/);
|
||||||
|
|||||||
@@ -36,26 +36,26 @@ const expectedImports = [
|
|||||||
const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex');
|
const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex');
|
||||||
const acceptedLedger = {
|
const acceptedLedger = {
|
||||||
counts: {
|
counts: {
|
||||||
cascadeEdges: 820,
|
cascadeEdges: 819,
|
||||||
customProperties: 31,
|
customProperties: 31,
|
||||||
declarations: 2983,
|
declarations: 2975,
|
||||||
important: 0,
|
important: 0,
|
||||||
keyframes: 56,
|
keyframes: 56,
|
||||||
media: 13,
|
media: 13,
|
||||||
rules: 920,
|
rules: 919,
|
||||||
variableReferences: 330,
|
variableReferences: 328,
|
||||||
},
|
},
|
||||||
hashes: {
|
hashes: {
|
||||||
cascadeEdges: '1590610791a755c9ec3c64da5b56da82764c9906344819fae414bf871bab1bad',
|
cascadeEdges: '1a3882545e488b80ac5454cc8fa236fd242d15eb8a27ac3371876af866b6c6e8',
|
||||||
customProperties: 'c7dd331e4bad898c450568999d8c9c6837e275a79c365c7680e143026fde4545',
|
customProperties: 'c7dd331e4bad898c450568999d8c9c6837e275a79c365c7680e143026fde4545',
|
||||||
declarations: '0bde16ab93623a2546daf42e83f037dc00d65dbf08da5851bf433d22697338b8',
|
declarations: '3972e7fd7da24d872a16cfa9dad9e9355861f489708fd9643a8610422e712825',
|
||||||
duplicateKeyframes: '4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945',
|
duplicateKeyframes: '4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945',
|
||||||
duplicateSelectors: 'c0ec5c96e48cd2ceed32aa5050383bcf29c3c0b8f1b440ed6883435e3e9b964d',
|
duplicateSelectors: 'c0ec5c96e48cd2ceed32aa5050383bcf29c3c0b8f1b440ed6883435e3e9b964d',
|
||||||
keyframes: '39b6fc9c9ab451d63bb77afd90caf63d2d74a30cb351a8f86503de380d76a936',
|
keyframes: '39b6fc9c9ab451d63bb77afd90caf63d2d74a30cb351a8f86503de380d76a936',
|
||||||
ruleDeclarationSequences: '883a3996d9d4ccd4ab6124237b6e0c00f17505dc8ff7314a8c0069fa828a2475',
|
ruleDeclarationSequences: '99469fb4eeb3af72a12cc4365f135e988ab95ebcf194572779d62e5185778d20',
|
||||||
selectors: 'ec10ef7ce8f812c52c7ff6b6952ae66f5bbeebbc61e582ceddcb54c7112bbf2b',
|
selectors: 'b2564f9e6c078233b388e84680181cbd5f21d55e963907990b237b14c5bc250f',
|
||||||
variableReferences: '8cb18a63664f73e6186e1730d3cd97c6e9f11bbe6b0f5cf04900948494a71f78',
|
variableReferences: 'c8348faf26b2174f02b9760aaa5812e22e02a72da7902fb5897f0c23e7f4e939',
|
||||||
witnesses: '204a3ad7cb5b3681ea78a2e4ef1265515c33ca97ebfe560c082d51303cd3bedb',
|
witnesses: '3c44c91b2cc2a03049b328b0f7a974b1c189c66d2a778dbb386601eed82994e8',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -110,7 +110,7 @@ test('tokens, shared primitives, and feature styles have one explicit owner', ()
|
|||||||
|
|
||||||
test('accepted stylesheet has pinned declaration, selector, keyframe, variable, and cascade ledgers', () => {
|
test('accepted stylesheet has pinned declaration, selector, keyframe, variable, and cascade ledgers', () => {
|
||||||
const witnesses = readStyleWitnesses(root);
|
const witnesses = readStyleWitnesses(root);
|
||||||
assert.equal(witnesses.length, 735);
|
assert.equal(witnesses.length, 730);
|
||||||
assert.equal(witnesses.filter((witness) => witness.unknown || witness.ancestorUnknown).length, 0);
|
assert.equal(witnesses.filter((witness) => witness.unknown || witness.ancestorUnknown).length, 0);
|
||||||
const ledger = createStyleLedger(readStyleSource(root), { witnesses });
|
const ledger = createStyleLedger(readStyleSource(root), { witnesses });
|
||||||
assert.deepEqual(ledger.counts, acceptedLedger.counts);
|
assert.deepEqual(ledger.counts, acceptedLedger.counts);
|
||||||
@@ -306,8 +306,8 @@ test('main owns one public stylesheet and the regrouped production CSS is determ
|
|||||||
assert.equal((main.match(/import ['"][^'"]+\.css['"]/g) || []).length, 1);
|
assert.equal((main.match(/import ['"][^'"]+\.css['"]/g) || []).length, 1);
|
||||||
|
|
||||||
const assets = fs.readdirSync(path.join(root, 'dist/assets')).filter((file) => file.endsWith('.css'));
|
const assets = fs.readdirSync(path.join(root, 'dist/assets')).filter((file) => file.endsWith('.css'));
|
||||||
assert.deepEqual(assets, ['index-DgGVjCSB.css']);
|
assert.deepEqual(assets, ['index-kNiIGd5E.css']);
|
||||||
const built = fs.readFileSync(path.join(root, 'dist/assets', assets[0]));
|
const built = fs.readFileSync(path.join(root, 'dist/assets', assets[0]));
|
||||||
assert.equal(built.byteLength, 112427);
|
assert.equal(built.byteLength, 112296);
|
||||||
assert.equal(sha256(built), '70e5b945a85ac965b31f43c25cf5766a3860453090678c44fbcdffa9951d624e');
|
assert.equal(sha256(built), 'cbda70aff44215a69dc7c87ceaa4166ae573131bc53aaea7ddd935950b2c983f');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ test('local validation, scoped refresh and drawer focus remain feature-owned', (
|
|||||||
assert.match(feature, /feature\.adding \|\| feature\.addClosing/);
|
assert.match(feature, /feature\.adding \|\| feature\.addClosing/);
|
||||||
assert.doesNotMatch(feature, /addInvokerRef/);
|
assert.doesNotMatch(feature, /addInvokerRef/);
|
||||||
assert.match(feature, /className="client-profile-delete"[\s\S]*client-profile-delete-lid/);
|
assert.match(feature, /className="client-profile-delete"[\s\S]*client-profile-delete-lid/);
|
||||||
|
assert.match(feature, /placeholder="https:\/\/…"[\s\S]*aria-label="Ссылка подписки"/);
|
||||||
|
assert.doesNotMatch(feature, /<span>Ссылка подписки<\/span>/);
|
||||||
assert.doesNotMatch(feature, /cancelRename|client-profile-menu|onRename/);
|
assert.doesNotMatch(feature, /cancelRename|client-profile-menu|onRename/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user