Polish subscription and server picker animations
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
export const HARBOR_VERSIONS = Object.freeze({
|
export const HARBOR_VERSIONS = Object.freeze({
|
||||||
macClient: '0.23.4',
|
macClient: '0.23.5',
|
||||||
gatewayClient: '0.24.4',
|
gatewayClient: '0.24.5',
|
||||||
gatewayBackend: '0.24.0',
|
gatewayBackend: '0.24.0',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -127,14 +127,19 @@ function ServerRow({
|
|||||||
}) {
|
}) {
|
||||||
const health = ping?.checking ? 'Проверяем пинг' : serverHealthText(ping);
|
const health = ping?.checking ? 'Проверяем пинг' : serverHealthText(ping);
|
||||||
|
|
||||||
return <div className={`client-server-row${selected ? ' is-selected' : ''}${onFavorite ? ' has-favorite' : ''}`}>
|
return <div
|
||||||
|
className={`client-server-row${selected ? ' is-selected' : ''}${onFavorite ? ' has-favorite' : ''}`}
|
||||||
|
style={{
|
||||||
|
'--server-index': Math.min(index, 7),
|
||||||
|
'--server-exit-delay': `${90 + Math.max(0, 4 - Math.min(index, 4)) * 45}ms`,
|
||||||
|
} as CSSProperties}
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
className={`client-server${selected ? ' is-selected' : ''}`}
|
className={`client-server${selected ? ' is-selected' : ''}`}
|
||||||
type="button"
|
type="button"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
aria-pressed={selected}
|
aria-pressed={selected}
|
||||||
aria-label={`${server.label}, ${server.host}:${server.port}${health ? `, ${health}` : ''}`}
|
aria-label={`${server.label}, ${server.host}:${server.port}${health ? `, ${health}` : ''}`}
|
||||||
style={{ '--server-index': Math.min(index, 7) } as CSSProperties}
|
|
||||||
onClick={() => onSelect(server.id)}
|
onClick={() => onSelect(server.id)}
|
||||||
>
|
>
|
||||||
<strong>{server.label}</strong>
|
<strong>{server.label}</strong>
|
||||||
|
|||||||
@@ -66,8 +66,10 @@ export function useSubscriptionFeature({
|
|||||||
}: SubscriptionFeatureOptions) {
|
}: SubscriptionFeatureOptions) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [expanded, setExpanded] = useState<string[]>([]);
|
const [expanded, setExpanded] = useState<string[]>([]);
|
||||||
|
const [closing, setClosing] = useState<string[]>([]);
|
||||||
const [visited, setVisited] = useState<string[]>([]);
|
const [visited, setVisited] = useState<string[]>([]);
|
||||||
const [adding, setAdding] = useState(profiles.length === 0);
|
const [adding, setAdding] = useState(profiles.length === 0);
|
||||||
|
const [addClosing, setAddClosing] = useState(false);
|
||||||
const [url, setUrl] = useState('');
|
const [url, setUrl] = useState('');
|
||||||
const [deleteId, setDeleteId] = useState('');
|
const [deleteId, setDeleteId] = useState('');
|
||||||
const [refreshingIds, setRefreshingIds] = useState<string[]>([]);
|
const [refreshingIds, setRefreshingIds] = useState<string[]>([]);
|
||||||
@@ -76,6 +78,9 @@ export function useSubscriptionFeature({
|
|||||||
const toggleRef = useRef<HTMLButtonElement>(null);
|
const toggleRef = useRef<HTMLButtonElement>(null);
|
||||||
const closeRef = useRef<HTMLButtonElement>(null);
|
const closeRef = useRef<HTMLButtonElement>(null);
|
||||||
const labelRef = useRef<HTMLInputElement>(null);
|
const labelRef = useRef<HTMLInputElement>(null);
|
||||||
|
const addFormRef = useRef<HTMLFormElement>(null);
|
||||||
|
const collapseTimersRef = useRef(new Map<string, ReturnType<typeof setTimeout>>());
|
||||||
|
const addCloseTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const deleteIdRef = useRef(deleteId);
|
const deleteIdRef = useRef(deleteId);
|
||||||
const previousProfileCountRef = useRef(profiles.length);
|
const previousProfileCountRef = useRef(profiles.length);
|
||||||
deleteIdRef.current = deleteId;
|
deleteIdRef.current = deleteId;
|
||||||
@@ -90,9 +95,11 @@ export function useSubscriptionFeature({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const ids = new Set(profiles.map((profile) => profile.id));
|
const ids = new Set(profiles.map((profile) => profile.id));
|
||||||
setExpanded((current) => current.filter((id) => ids.has(id)));
|
setExpanded((current) => current.filter((id) => ids.has(id)));
|
||||||
|
setClosing((current) => current.filter((id) => ids.has(id)));
|
||||||
setVisited((current) => current.filter((id) => ids.has(id)));
|
setVisited((current) => current.filter((id) => ids.has(id)));
|
||||||
if (profiles.length === 0) {
|
if (profiles.length === 0) {
|
||||||
setAdding(true);
|
setAdding(true);
|
||||||
|
setAddClosing(false);
|
||||||
if (previousProfileCountRef.current > 0) setOpen(false);
|
if (previousProfileCountRef.current > 0) setOpen(false);
|
||||||
}
|
}
|
||||||
if (previousProfileCountRef.current === 0 && profiles.length > 0) {
|
if (previousProfileCountRef.current === 0 && profiles.length > 0) {
|
||||||
@@ -135,12 +142,56 @@ export function useSubscriptionFeature({
|
|||||||
};
|
};
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
function resetAdd() {
|
useEffect(() => {
|
||||||
setUrl('');
|
if (!adding || profiles.length === 0) return undefined;
|
||||||
if (profiles.length) setAdding(false);
|
const closeAdd = (event: PointerEvent | KeyboardEvent) => {
|
||||||
if (profiles.length) requestAnimationFrame(() => (
|
if (event.type === 'keydown' && (event as KeyboardEvent).key !== 'Escape') return;
|
||||||
document.getElementById('client-profile-add-trigger')?.focus()
|
const target = event.target as Node | null;
|
||||||
));
|
if (event.type !== 'keydown' && addFormRef.current?.contains(target)) return;
|
||||||
|
resetAdd(Boolean(target && panelRef.current?.contains(target)));
|
||||||
|
};
|
||||||
|
document.addEventListener('pointerdown', closeAdd);
|
||||||
|
document.addEventListener('keydown', closeAdd);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('pointerdown', closeAdd);
|
||||||
|
document.removeEventListener('keydown', closeAdd);
|
||||||
|
};
|
||||||
|
}, [adding, profiles.length]);
|
||||||
|
|
||||||
|
useEffect(() => () => {
|
||||||
|
for (const timer of collapseTimersRef.current.values()) clearTimeout(timer);
|
||||||
|
if (addCloseTimerRef.current) clearTimeout(addCloseTimerRef.current);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
function resetAdd(restoreFocus = true) {
|
||||||
|
if (!profiles.length) {
|
||||||
|
setUrl('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setAdding(false);
|
||||||
|
const finish = () => {
|
||||||
|
setAddClosing(false);
|
||||||
|
setUrl('');
|
||||||
|
addCloseTimerRef.current = null;
|
||||||
|
if (restoreFocus) requestAnimationFrame(() => (
|
||||||
|
document.getElementById('client-profile-add-trigger')?.focus()
|
||||||
|
));
|
||||||
|
};
|
||||||
|
if (matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setAddClosing(true);
|
||||||
|
if (addCloseTimerRef.current) clearTimeout(addCloseTimerRef.current);
|
||||||
|
addCloseTimerRef.current = setTimeout(finish, 320);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showAdd() {
|
||||||
|
if (addCloseTimerRef.current) clearTimeout(addCloseTimerRef.current);
|
||||||
|
addCloseTimerRef.current = null;
|
||||||
|
setAddClosing(false);
|
||||||
|
setAdding(true);
|
||||||
|
onDismissError();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addProfile() {
|
async function addProfile() {
|
||||||
@@ -180,10 +231,22 @@ export function useSubscriptionFeature({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toggleProfile(profileId: string) {
|
function toggleProfile(profileId: string) {
|
||||||
|
const activeTimer = collapseTimersRef.current.get(profileId);
|
||||||
|
if (activeTimer) clearTimeout(activeTimer);
|
||||||
|
collapseTimersRef.current.delete(profileId);
|
||||||
setVisited((current) => current.includes(profileId) ? current : [...current, profileId]);
|
setVisited((current) => current.includes(profileId) ? current : [...current, profileId]);
|
||||||
setExpanded((current) => current.includes(profileId)
|
if (!expanded.includes(profileId)) {
|
||||||
? current.filter((id) => id !== profileId)
|
setClosing((current) => current.filter((id) => id !== profileId));
|
||||||
: [...current, profileId]);
|
setExpanded((current) => current.includes(profileId) ? current : [...current, profileId]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setExpanded((current) => current.filter((id) => id !== profileId));
|
||||||
|
if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
|
||||||
|
setClosing((current) => current.includes(profileId) ? current : [...current, profileId]);
|
||||||
|
collapseTimersRef.current.set(profileId, setTimeout(() => {
|
||||||
|
setClosing((current) => current.filter((id) => id !== profileId));
|
||||||
|
collapseTimersRef.current.delete(profileId);
|
||||||
|
}, 950));
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -196,8 +259,10 @@ export function useSubscriptionFeature({
|
|||||||
error,
|
error,
|
||||||
open,
|
open,
|
||||||
expanded,
|
expanded,
|
||||||
|
closing,
|
||||||
visited,
|
visited,
|
||||||
adding,
|
adding,
|
||||||
|
addClosing,
|
||||||
url,
|
url,
|
||||||
validationStatus,
|
validationStatus,
|
||||||
addError,
|
addError,
|
||||||
@@ -209,17 +274,15 @@ export function useSubscriptionFeature({
|
|||||||
toggleRef,
|
toggleRef,
|
||||||
closeRef,
|
closeRef,
|
||||||
labelRef,
|
labelRef,
|
||||||
|
addFormRef,
|
||||||
addBlocked: operationBlocked(operations, 'profileAdd'),
|
addBlocked: operationBlocked(operations, 'profileAdd'),
|
||||||
refreshBlocked: operationBlocked(operations, 'profileRefresh'),
|
refreshBlocked: operationBlocked(operations, 'profileRefresh'),
|
||||||
deleteBlocked: operationBlocked(operations, 'profileDelete'),
|
deleteBlocked: operationBlocked(operations, 'profileDelete'),
|
||||||
activateBlocked: operationBlocked(operations, 'profileActivate'),
|
activateBlocked: operationBlocked(operations, 'profileActivate'),
|
||||||
toggle: () => setOpen((current) => !current),
|
toggle: () => setOpen((current) => !current),
|
||||||
close: () => setOpen(false),
|
close: () => setOpen(false),
|
||||||
showAdd: () => {
|
showAdd,
|
||||||
setAdding(true);
|
cancelAdd: () => resetAdd(),
|
||||||
onDismissError();
|
|
||||||
},
|
|
||||||
cancelAdd: resetAdd,
|
|
||||||
setUrl: (value: string) => {
|
setUrl: (value: string) => {
|
||||||
setUrl(value);
|
setUrl(value);
|
||||||
onDismissError();
|
onDismissError();
|
||||||
@@ -264,8 +327,11 @@ function AddProfileForm({ feature }: { feature: SubscriptionFeatureController })
|
|||||||
? ERROR_DEFINITIONS.SUBSCRIPTION_INVALID.message
|
? ERROR_DEFINITIONS.SUBSCRIPTION_INVALID.message
|
||||||
: feature.addError?.message || '';
|
: feature.addError?.message || '';
|
||||||
return <form
|
return <form
|
||||||
className="client-profile-add"
|
ref={feature.addFormRef}
|
||||||
|
className={`client-profile-add${feature.addClosing ? ' is-closing' : ''}`}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
|
aria-hidden={feature.addClosing}
|
||||||
|
inert={feature.addClosing ? true : undefined}
|
||||||
onSubmit={(event) => {
|
onSubmit={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
feature.addProfile();
|
feature.addProfile();
|
||||||
@@ -316,6 +382,7 @@ function ProfileGroup({
|
|||||||
renderServerPicker: (profile: ProfileSnapshot, state: ServerPickerRenderState) => ReactNode;
|
renderServerPicker: (profile: ProfileSnapshot, state: ServerPickerRenderState) => ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const expanded = feature.expanded.includes(profile.id);
|
const expanded = feature.expanded.includes(profile.id);
|
||||||
|
const closing = feature.closing.includes(profile.id);
|
||||||
const visited = feature.visited.includes(profile.id);
|
const visited = feature.visited.includes(profile.id);
|
||||||
const applied = feature.connected
|
const applied = feature.connected
|
||||||
&& !feature.gatewayDirect
|
&& !feature.gatewayDirect
|
||||||
@@ -397,7 +464,7 @@ function ProfileGroup({
|
|||||||
|
|
||||||
{localStatus && <div className="client-profile-local-status" role="status">{localStatus}</div>}
|
{localStatus && <div className="client-profile-local-status" role="status">{localStatus}</div>}
|
||||||
|
|
||||||
{!expanded && visibleServer && <button
|
{!expanded && !closing && visibleServer && <button
|
||||||
className="client-profile-selected-server"
|
className="client-profile-selected-server"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => feature.toggleProfile(profile.id)}
|
onClick={() => feature.toggleProfile(profile.id)}
|
||||||
@@ -409,16 +476,18 @@ function ProfileGroup({
|
|||||||
|
|
||||||
{visited && <div
|
{visited && <div
|
||||||
id={`client-profile-${profile.id}`}
|
id={`client-profile-${profile.id}`}
|
||||||
className="client-profile-body"
|
className={`client-profile-body${expanded ? ' is-open' : ''}${closing ? ' is-closing' : ''}`}
|
||||||
aria-hidden={!expanded}
|
aria-hidden={!expanded}
|
||||||
inert={!expanded ? true : undefined}
|
inert={!expanded ? true : undefined}
|
||||||
>
|
>
|
||||||
{!profile.desiredServerId && <p className="client-profile-server-hint">Выберите сервер этой подписки</p>}
|
<div className="client-profile-body-inner">
|
||||||
{renderServerPicker(profile, {
|
{!profile.desiredServerId && <p className="client-profile-server-hint">Выберите сервер этой подписки</p>}
|
||||||
disabled: controlsBlocked,
|
{renderServerPicker(profile, {
|
||||||
leaving: false,
|
disabled: controlsBlocked,
|
||||||
revealVersion: feature.revealVersions[profile.id] || 0,
|
leaving: false,
|
||||||
})}
|
revealVersion: feature.revealVersions[profile.id] || 0,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</div>}
|
</div>}
|
||||||
</section>;
|
</section>;
|
||||||
}
|
}
|
||||||
@@ -471,7 +540,7 @@ export function SubscriptionPanel({
|
|||||||
renderServerPicker={renderServerPicker}
|
renderServerPicker={renderServerPicker}
|
||||||
/>)}
|
/>)}
|
||||||
</div>
|
</div>
|
||||||
{feature.adding && feature.profiles.length > 0
|
{(feature.adding || feature.addClosing) && feature.profiles.length > 0
|
||||||
? <AddProfileForm feature={feature} />
|
? <AddProfileForm feature={feature} />
|
||||||
: <button
|
: <button
|
||||||
id="client-profile-add-trigger"
|
id="client-profile-add-trigger"
|
||||||
|
|||||||
@@ -638,6 +638,54 @@
|
|||||||
animation: none;
|
animation: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.client-profile-body.is-open .client-server-row {
|
||||||
|
animation: client-profile-server-in 420ms calc(var(--server-index) * 70ms) cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-profile-body.is-open .client-server-toolbar {
|
||||||
|
animation: client-profile-ping-in 300ms 620ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-profile-body.is-closing .client-server-toolbar {
|
||||||
|
animation: client-profile-ping-out 160ms cubic-bezier(0.4, 0, 1, 1) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-profile-body.is-closing .client-server-row {
|
||||||
|
animation: client-profile-server-out 300ms var(--server-exit-delay) cubic-bezier(0.4, 0, 1, 1) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes client-profile-server-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
filter: blur(4px);
|
||||||
|
transform: translateY(-8px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes client-profile-server-out {
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
filter: blur(4px);
|
||||||
|
transform: translateY(-8px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes client-profile-ping-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
filter: blur(4px);
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes client-profile-ping-out {
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
filter: blur(4px);
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.client-profile-body .client-server-row .client-server::before {
|
.client-profile-body .client-server-row .client-server::before {
|
||||||
content: '';
|
content: '';
|
||||||
width: 10px;
|
width: 10px;
|
||||||
|
|||||||
@@ -223,11 +223,40 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.client-profile-body {
|
.client-profile-body {
|
||||||
padding: 0 0 4px;
|
display: grid;
|
||||||
|
grid-template-rows: 0fr;
|
||||||
|
visibility: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
filter: blur(5px);
|
||||||
|
transition:
|
||||||
|
grid-template-rows 360ms cubic-bezier(0.16, 1, 0.3, 1),
|
||||||
|
opacity 260ms ease,
|
||||||
|
filter 420ms ease,
|
||||||
|
visibility 0s 930ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-profile-body[aria-hidden='true'] {
|
.client-profile-body.is-open {
|
||||||
display: none;
|
grid-template-rows: 1fr;
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
filter: blur(0);
|
||||||
|
transition:
|
||||||
|
grid-template-rows 620ms cubic-bezier(0.16, 1, 0.3, 1),
|
||||||
|
opacity 260ms ease,
|
||||||
|
filter 420ms ease,
|
||||||
|
visibility 0s;
|
||||||
|
transition-delay: 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-profile-body.is-closing {
|
||||||
|
visibility: visible;
|
||||||
|
transition-delay: 570ms, 570ms, 570ms, 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-profile-body-inner {
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
padding-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-profile-body .client-servers {
|
.client-profile-body .client-servers {
|
||||||
@@ -249,6 +278,15 @@
|
|||||||
font: inherit;
|
font: inherit;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
animation: client-profile-selected-in 260ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes client-profile-selected-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
filter: blur(3px);
|
||||||
|
transform: translateY(-4px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-profile-selected-server > span {
|
.client-profile-selected-server > span {
|
||||||
@@ -299,6 +337,27 @@
|
|||||||
|
|
||||||
.client-subscription-sheet > .client-profile-add {
|
.client-subscription-sheet > .client-profile-add {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
|
animation: client-profile-add-open 420ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-subscription-sheet > .client-profile-add.is-closing {
|
||||||
|
animation: client-profile-add-close 320ms cubic-bezier(0.4, 0, 1, 1) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes client-profile-add-open {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
filter: blur(5px);
|
||||||
|
transform: translateY(-7px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes client-profile-add-close {
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
filter: blur(5px);
|
||||||
|
transform: translateY(-7px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-profile-activate {
|
.client-profile-activate {
|
||||||
@@ -314,6 +373,11 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
animation: client-profile-add-trigger-in 240ms ease both;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes client-profile-add-trigger-in {
|
||||||
|
from { opacity: 0; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-profile-server-hint,
|
.client-profile-server-hint,
|
||||||
@@ -433,6 +497,11 @@
|
|||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.client-profile-chevron,
|
.client-profile-chevron,
|
||||||
|
.client-profile-body,
|
||||||
|
.client-profile-body *,
|
||||||
|
.client-profile-selected-server,
|
||||||
|
.client-profile-add,
|
||||||
|
.client-profile-add-trigger,
|
||||||
.client-profile-refresh,
|
.client-profile-refresh,
|
||||||
.client-profile-refresh svg,
|
.client-profile-refresh svg,
|
||||||
.client-profile-delete-lid,
|
.client-profile-delete-lid,
|
||||||
|
|||||||
@@ -135,5 +135,9 @@ test('server picker starts simple and reveals advanced controls on demand', () =
|
|||||||
assert.match(styles, /\.client-profile-body \.client-server-row \{[\s\S]*?grid-template-columns: minmax\(0, 1fr\) 64px;/);
|
assert.match(styles, /\.client-profile-body \.client-server-row \{[\s\S]*?grid-template-columns: minmax\(0, 1fr\) 64px;/);
|
||||||
assert.match(styles, /\.client-profile-body \.client-server-row\.is-selected \{[\s\S]*?background: color-mix\(in oklch, var\(--client-accent\) 2%, transparent\);/);
|
assert.match(styles, /\.client-profile-body \.client-server-row\.is-selected \{[\s\S]*?background: color-mix\(in oklch, var\(--client-accent\) 2%, transparent\);/);
|
||||||
assert.match(styles, /\.client-profile-body \.client-server-check-label \{[\s\S]*?display: inline;/);
|
assert.match(styles, /\.client-profile-body \.client-server-check-label \{[\s\S]*?display: inline;/);
|
||||||
|
assert.match(picker, /'--server-index': Math\.min\(index, 7\)[\s\S]*'--server-exit-delay'/);
|
||||||
|
assert.match(styles, /\.client-profile-body\.is-open \.client-server-row \{[\s\S]*client-profile-server-in[\s\S]*var\(--server-index\)/);
|
||||||
|
assert.match(styles, /\.client-profile-body\.is-open \.client-server-toolbar \{[\s\S]*client-profile-ping-in 300ms 620ms/);
|
||||||
|
assert.match(styles, /\.client-profile-body\.is-closing \.client-server-toolbar \{[\s\S]*client-profile-ping-out[\s\S]*\.client-profile-body\.is-closing \.client-server-row \{[\s\S]*var\(--server-exit-delay\)/);
|
||||||
assert.doesNotMatch(styles, /\.client-profile-body \.client-server-check::after/);
|
assert.doesNotMatch(styles, /\.client-profile-body \.client-server-check::after/);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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: 790,
|
cascadeEdges: 821,
|
||||||
customProperties: 31,
|
customProperties: 31,
|
||||||
declarations: 2941,
|
declarations: 2986,
|
||||||
important: 0,
|
important: 0,
|
||||||
keyframes: 49,
|
keyframes: 57,
|
||||||
media: 13,
|
media: 13,
|
||||||
rules: 906,
|
rules: 921,
|
||||||
variableReferences: 328,
|
variableReferences: 330,
|
||||||
},
|
},
|
||||||
hashes: {
|
hashes: {
|
||||||
cascadeEdges: '1f4fd4cc495ac8d430f0d3891a7d63db34ee44313b4f865b58de7c587378b261',
|
cascadeEdges: 'ac82341d26b6598c9e54a5cc2f20fd791baf0ab6a99c8bd09e838608a16567f7',
|
||||||
customProperties: 'c7dd331e4bad898c450568999d8c9c6837e275a79c365c7680e143026fde4545',
|
customProperties: 'c7dd331e4bad898c450568999d8c9c6837e275a79c365c7680e143026fde4545',
|
||||||
declarations: 'c17ab95f3bccd3fca26b2707f07e5298deda0f6343e80d77dbc5df8e97ef0ecb',
|
declarations: 'a7b2ecccc36118b0f77b100a48b2fdf8d8653587090a73b527b9565c7f5a88b8',
|
||||||
duplicateKeyframes: '4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945',
|
duplicateKeyframes: '4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945',
|
||||||
duplicateSelectors: 'c0ec5c96e48cd2ceed32aa5050383bcf29c3c0b8f1b440ed6883435e3e9b964d',
|
duplicateSelectors: 'c0ec5c96e48cd2ceed32aa5050383bcf29c3c0b8f1b440ed6883435e3e9b964d',
|
||||||
keyframes: 'bde9c628dc86cb8a4299d5397eb7a81c5fbc9481136a073130991d3182d0799d',
|
keyframes: '077e69647e0bd0761ca918e8d4109a9e8b07a1d7dbd9e0702bdca0e48011aed1',
|
||||||
ruleDeclarationSequences: 'a0e34d9639e87e77a2a095503196f4472a95ac875e398908f184b64b5bf5feba',
|
ruleDeclarationSequences: '36ea3201b45a89f3607e9336d30e2d0e5b88400f921847fa28263c2a2b8b75ac',
|
||||||
selectors: '7051c489e7dac8bd04f3cdeb44bda8c069793cff4232ea730d40a19545ea0475',
|
selectors: 'ec10ef7ce8f812c52c7ff6b6952ae66f5bbeebbc61e582ceddcb54c7112bbf2b',
|
||||||
variableReferences: '37d0cec8ffda35b4b3823322c0e21802ad0d3252258deade0e2124bdc6eb6ca0',
|
variableReferences: '8cb18a63664f73e6186e1730d3cd97c6e9f11bbe6b0f5cf04900948494a71f78',
|
||||||
witnesses: '1b0e3d6fb35dde7acd7b3597f290c2e8b2f25ca569e74b047457121967affc6f',
|
witnesses: '204a3ad7cb5b3681ea78a2e4ef1265515c33ca97ebfe560c082d51303cd3bedb',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -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, 734);
|
assert.equal(witnesses.length, 735);
|
||||||
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-hHmAphe7.css']);
|
assert.deepEqual(assets, ['index-CgoReV4i.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, 110356);
|
assert.equal(built.byteLength, 112580);
|
||||||
assert.equal(sha256(built), '67a5df63d5ee0624c0e58f7fc9751c279a0c009c37abc8ef77567892c93da931');
|
assert.equal(sha256(built), 'cdfeb513550fab679856b62c6ecb3d9bab8cbc464a0ffaa6c707701f7c2078ae');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ test('local validation, scoped refresh and drawer focus remain feature-owned', (
|
|||||||
assert.match(feature, /if \(!adding \|\| \(profiles\.length > 0 && !open\)\) return undefined/);
|
assert.match(feature, /if \(!adding \|\| \(profiles\.length > 0 && !open\)\) return undefined/);
|
||||||
assert.match(feature, /if \(deleteIdRef\.current\) return;[\s\S]*toggleRef\.current\?\.focus\(\)/);
|
assert.match(feature, /if \(deleteIdRef\.current\) return;[\s\S]*toggleRef\.current\?\.focus\(\)/);
|
||||||
assert.match(feature, /setAdding\(false\)[\s\S]*client-profile-add-trigger'\)\?\.focus\(\)/);
|
assert.match(feature, /setAdding\(false\)[\s\S]*client-profile-add-trigger'\)\?\.focus\(\)/);
|
||||||
|
assert.match(feature, /addFormRef\.current\?\.contains\(target\)[\s\S]*resetAdd\(Boolean\(target && panelRef\.current\?\.contains\(target\)\)\)/);
|
||||||
|
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.doesNotMatch(feature, /cancelRename|client-profile-menu|onRename/);
|
assert.doesNotMatch(feature, /cancelRename|client-profile-menu|onRename/);
|
||||||
@@ -104,7 +106,7 @@ test('profiles render as flat accordion groups with scoped controls', () => {
|
|||||||
assert.match(feature, /aria-expanded=\{expanded\}/);
|
assert.match(feature, /aria-expanded=\{expanded\}/);
|
||||||
assert.match(feature, /profile\.subscription\.status === 'stale'[\s\S]*profile\.subscription\.fetchedAt/);
|
assert.match(feature, /profile\.subscription\.status === 'stale'[\s\S]*profile\.subscription\.fetchedAt/);
|
||||||
assert.match(feature, /feature\.operations\.profileRefresh\?\.target === profile\.id/);
|
assert.match(feature, /feature\.operations\.profileRefresh\?\.target === profile\.id/);
|
||||||
assert.match(feature, /!expanded && visibleServer && <button[\s\S]*client-profile-selected-server/);
|
assert.match(feature, /!expanded && !closing && visibleServer && <button[\s\S]*client-profile-selected-server/);
|
||||||
assert.match(feature, /const visibleServerId = applied \? feature\.selection\.appliedServerId : profile\.desiredServerId/);
|
assert.match(feature, /const visibleServerId = applied \? feature\.selection\.appliedServerId : profile\.desiredServerId/);
|
||||||
assert.match(feature, /const desired = !feature\.connected[\s\S]*&& !feature\.gatewayDirect/);
|
assert.match(feature, /const desired = !feature\.connected[\s\S]*&& !feature\.gatewayDirect/);
|
||||||
assert.match(feature, /const profileDomain = subscriptionDomain\(profile\.subscription\.host\)/);
|
assert.match(feature, /const profileDomain = subscriptionDomain\(profile\.subscription\.host\)/);
|
||||||
@@ -112,6 +114,8 @@ test('profiles render as flat accordion groups with scoped controls', () => {
|
|||||||
assert.doesNotMatch(feature, /<strong>\{profile\.label\}<\/strong>|Переименовать|client-profile-menu/);
|
assert.doesNotMatch(feature, /<strong>\{profile\.label\}<\/strong>|Переименовать|client-profile-menu/);
|
||||||
assert.match(styles, /\.client-profile-group/);
|
assert.match(styles, /\.client-profile-group/);
|
||||||
assert.match(styles, /\.client-profile-body/);
|
assert.match(styles, /\.client-profile-body/);
|
||||||
|
assert.match(styles, /\.client-profile-body \{[\s\S]*grid-template-rows: 0fr[\s\S]*\.client-profile-body\.is-open \{[\s\S]*grid-template-rows: 1fr/);
|
||||||
|
assert.match(styles, /@media \(prefers-reduced-motion: reduce\)[\s\S]*\.client-profile-body \*/);
|
||||||
assert.match(styles, /\.client-profile-refresh\.is-refreshing/);
|
assert.match(styles, /\.client-profile-refresh\.is-refreshing/);
|
||||||
assert.doesNotMatch(feature, /client-subscription-card|role="tab"/);
|
assert.doesNotMatch(feature, /client-subscription-card|role="tab"/);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user