Compare commits
3 Commits
b6f3f5c0aa
...
6cb1e14632
| Author | SHA1 | Date | |
|---|---|---|---|
| 6cb1e14632 | |||
| 7d866404b3 | |||
| c264de53b5 |
@@ -1,7 +1,9 @@
|
|||||||
import React, { ReactNode, useCallback, useState } from 'react';
|
import React, { ReactNode, useCallback, useState } from 'react';
|
||||||
import FocusTrap from 'focus-trap-react';
|
import FocusTrap from 'focus-trap-react';
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
import { Modal, Overlay, OverlayBackdrop, OverlayCenter, PopOutContainerProvider } from 'folds';
|
import { Modal, Overlay, OverlayBackdrop, OverlayCenter, PopOutContainerProvider } from 'folds';
|
||||||
import { stopPropagation } from '../utils/keyboard';
|
import { stopPropagation } from '../utils/keyboard';
|
||||||
|
import { releaseNotesDialogAtom } from '../state/releaseNotes';
|
||||||
|
|
||||||
type Modal500Props = {
|
type Modal500Props = {
|
||||||
requestClose: () => void;
|
requestClose: () => void;
|
||||||
@@ -10,11 +12,13 @@ type Modal500Props = {
|
|||||||
export function Modal500({ requestClose, children }: Modal500Props) {
|
export function Modal500({ requestClose, children }: Modal500Props) {
|
||||||
const [modalEl, setModalEl] = useState<HTMLDivElement | null>(null);
|
const [modalEl, setModalEl] = useState<HTMLDivElement | null>(null);
|
||||||
const modalRef = useCallback((el: HTMLDivElement | null) => setModalEl(el), []);
|
const modalRef = useCallback((el: HTMLDivElement | null) => setModalEl(el), []);
|
||||||
|
const releaseNotesOpen = useAtomValue(releaseNotesDialogAtom).open;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Overlay open backdrop={<OverlayBackdrop />}>
|
<Overlay open backdrop={<OverlayBackdrop />}>
|
||||||
<OverlayCenter>
|
<OverlayCenter>
|
||||||
<FocusTrap
|
<FocusTrap
|
||||||
|
active={!releaseNotesOpen}
|
||||||
focusTrapOptions={{
|
focusTrapOptions={{
|
||||||
initialFocus: false,
|
initialFocus: false,
|
||||||
clickOutsideDeactivates: true,
|
clickOutsideDeactivates: true,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ type PageRootProps = {
|
|||||||
export function PageRoot({ nav, children }: PageRootProps) {
|
export function PageRoot({ nav, children }: PageRootProps) {
|
||||||
const screenSize = useScreenSizeContext();
|
const screenSize = useScreenSizeContext();
|
||||||
const showCompactMaster = useShowCompactMasterView();
|
const showCompactMaster = useShowCompactMasterView();
|
||||||
|
const showDetail = !showCompactMaster || nav == null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box grow="Yes" className={ContainerColor({ variant: 'Background' })}>
|
<Box grow="Yes" className={ContainerColor({ variant: 'Background' })}>
|
||||||
@@ -23,7 +24,7 @@ export function PageRoot({ nav, children }: PageRootProps) {
|
|||||||
{screenSize !== ScreenSize.Mobile && (
|
{screenSize !== ScreenSize.Mobile && (
|
||||||
<Line variant="Background" size="300" direction="Vertical" />
|
<Line variant="Background" size="300" direction="Vertical" />
|
||||||
)}
|
)}
|
||||||
{!showCompactMaster && children}
|
{showDetail && children}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,37 +3,64 @@ import { color, config, toRem } from 'folds';
|
|||||||
|
|
||||||
const MOBILE_BREAKPOINT = '480px';
|
const MOBILE_BREAKPOINT = '480px';
|
||||||
|
|
||||||
export const PortalLayer = style({
|
/** Above folds overlays (9999) and Settings modals. */
|
||||||
position: 'fixed',
|
const UPDATES_DIALOG_Z = 10001;
|
||||||
inset: 0,
|
|
||||||
zIndex: config.zIndex.Max,
|
|
||||||
});
|
|
||||||
|
|
||||||
/** Dialog + overlay padding must never exceed the viewport height. */
|
|
||||||
const DIALOG_MAX_HEIGHT =
|
const DIALOG_MAX_HEIGHT =
|
||||||
'calc(85vh - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px))';
|
'calc(85vh - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px))';
|
||||||
|
|
||||||
export const OverlayFrame = style({
|
/** [data-updates-dialog] — full-screen host portaled to document.body */
|
||||||
|
export const Root = style({
|
||||||
|
position: 'fixed',
|
||||||
|
inset: 0,
|
||||||
|
zIndex: UPDATES_DIALOG_Z,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
pointerEvents: 'auto',
|
||||||
|
});
|
||||||
|
|
||||||
|
/** [data-updates-dialog-backdrop] */
|
||||||
|
export const Backdrop = style({
|
||||||
|
position: 'absolute',
|
||||||
|
inset: 0,
|
||||||
|
zIndex: 0,
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.55)',
|
||||||
|
});
|
||||||
|
|
||||||
|
/** [data-updates-dialog-frame] */
|
||||||
|
export const Frame = style({
|
||||||
|
position: 'relative',
|
||||||
|
zIndex: 1,
|
||||||
boxSizing: 'border-box',
|
boxSizing: 'border-box',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
width: '100%',
|
||||||
maxHeight: '100vh',
|
maxHeight: '100vh',
|
||||||
maxWidth: '100vw',
|
maxWidth: '100vw',
|
||||||
padding:
|
padding:
|
||||||
'env(safe-area-inset-top, 0px) env(safe-area-inset-right, 0px) env(safe-area-inset-bottom, 0px) env(safe-area-inset-left, 0px)',
|
'env(safe-area-inset-top, 0px) env(safe-area-inset-right, 0px) env(safe-area-inset-bottom, 0px) env(safe-area-inset-left, 0px)',
|
||||||
|
pointerEvents: 'none',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const DialogShell = style({
|
/** [data-updates-dialog-panel] */
|
||||||
|
export const Panel = style({
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
maxWidth: `min(${toRem(560)}, calc(100vw - ${config.space.S800}))`,
|
maxWidth: toRem(560),
|
||||||
maxHeight: DIALOG_MAX_HEIGHT,
|
maxHeight: DIALOG_MAX_HEIGHT,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
|
pointerEvents: 'auto',
|
||||||
|
borderRadius: config.radii.R400,
|
||||||
backgroundColor: color.Surface.Container,
|
backgroundColor: color.Surface.Container,
|
||||||
color: color.Surface.OnContainer,
|
color: color.Surface.OnContainer,
|
||||||
|
boxShadow: config.shadow.E400,
|
||||||
'@media': {
|
'@media': {
|
||||||
[`(max-width: ${MOBILE_BREAKPOINT})`]: {
|
[`(max-width: ${MOBILE_BREAKPOINT})`]: {
|
||||||
maxWidth: `calc(100vw - ${config.space.S400})`,
|
maxWidth: `calc(100vw - 2 * ${config.space.S400})`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -102,7 +129,7 @@ export const BodyContent = style({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const BodyLoading = style({
|
export const BodyLoading = style({
|
||||||
padding: config.space.S800,
|
padding: config.space.S600,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const Markdown = style({
|
export const Markdown = style({
|
||||||
@@ -165,7 +192,7 @@ globalStyle(`${Markdown} code`, {
|
|||||||
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
|
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
|
||||||
fontSize: toRem(13),
|
fontSize: toRem(13),
|
||||||
padding: `0 ${config.space.S100}`,
|
padding: `0 ${config.space.S100}`,
|
||||||
borderRadius: config.radii.R200,
|
borderRadius: config.radii.R300,
|
||||||
backgroundColor: color.SurfaceVariant.Container,
|
backgroundColor: color.SurfaceVariant.Container,
|
||||||
color: color.SurfaceVariant.OnContainer,
|
color: color.SurfaceVariant.OnContainer,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,19 +1,7 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import FocusTrap from 'focus-trap-react';
|
import FocusTrap from 'focus-trap-react';
|
||||||
import {
|
import { Box, Button, IconButton, Scroll, Spinner, Text } from 'folds';
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
config,
|
|
||||||
Dialog,
|
|
||||||
IconButton,
|
|
||||||
Overlay,
|
|
||||||
OverlayBackdrop,
|
|
||||||
OverlayCenter,
|
|
||||||
Scroll,
|
|
||||||
Spinner,
|
|
||||||
Text,
|
|
||||||
} from 'folds';
|
|
||||||
import { Icon, Icons } from '../icons';
|
import { Icon, Icons } from '../icons';
|
||||||
import { stopPropagation } from '../../utils/keyboard';
|
import { stopPropagation } from '../../utils/keyboard';
|
||||||
import type { ParsedUpdateDoc, UpdateManifest } from '../../data/updateNotes';
|
import type { ParsedUpdateDoc, UpdateManifest } from '../../data/updateNotes';
|
||||||
@@ -49,24 +37,34 @@ export function UpdatesDialog({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<div className={css.PortalLayer}>
|
<div
|
||||||
<Overlay open backdrop={<OverlayBackdrop />}>
|
data-updates-dialog=""
|
||||||
<OverlayCenter className={css.OverlayFrame}>
|
className={css.Root}
|
||||||
<FocusTrap
|
data-disable-swipe-back="true"
|
||||||
focusTrapOptions={{
|
data-disable-swipe-reply="true"
|
||||||
initialFocus: false,
|
>
|
||||||
onDeactivate: onClose,
|
<div data-updates-dialog-backdrop="" className={css.Backdrop} aria-hidden="true" />
|
||||||
clickOutsideDeactivates: true,
|
<div data-updates-dialog-frame="" className={css.Frame}>
|
||||||
escapeDeactivates: stopPropagation,
|
<FocusTrap
|
||||||
}}
|
focusTrapOptions={{
|
||||||
|
initialFocus: false,
|
||||||
|
clickOutsideDeactivates: false,
|
||||||
|
escapeDeactivates: stopPropagation,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="updates-dialog-title"
|
||||||
|
data-updates-dialog-panel=""
|
||||||
|
className={css.Panel}
|
||||||
>
|
>
|
||||||
<Dialog variant="Surface" className={css.DialogShell}>
|
|
||||||
<Box className={css.Hero}>
|
<Box className={css.Hero}>
|
||||||
<Box className={css.HeroRow}>
|
<Box className={css.HeroRow}>
|
||||||
<img className={css.HeroLogo} src={PaarrotSVG} alt="" draggable={false} />
|
<img className={css.HeroLogo} src={PaarrotSVG} alt="" draggable={false} />
|
||||||
<Box className={css.HeroTitleWrap}>
|
<Box className={css.HeroTitleWrap}>
|
||||||
<Text size="L400" priority="300">What's new</Text>
|
<Text size="L400" priority="300">What's new</Text>
|
||||||
<Text size="H4" truncate>
|
<Text id="updates-dialog-title" size="H4" truncate>
|
||||||
{displayVersion ? `Paarrot ${displayVersion}` : 'Paarrot updates'}
|
{displayVersion ? `Paarrot ${displayVersion}` : 'Paarrot updates'}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -176,10 +174,9 @@ export function UpdatesDialog({
|
|||||||
<Icon src={Icons.Cross} />
|
<Icon src={Icons.Cross} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
</Dialog>
|
</div>
|
||||||
</FocusTrap>
|
</FocusTrap>
|
||||||
</OverlayCenter>
|
</div>
|
||||||
</Overlay>
|
|
||||||
</div>,
|
</div>,
|
||||||
document.body
|
document.body
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -26,7 +26,10 @@ type UpdaterInfo = {
|
|||||||
export function useOpenReleaseNotesDialog(): () => void {
|
export function useOpenReleaseNotesDialog(): () => void {
|
||||||
const setDialogState = useSetAtom(releaseNotesDialogAtom);
|
const setDialogState = useSetAtom(releaseNotesDialogAtom);
|
||||||
return useCallback(() => {
|
return useCallback(() => {
|
||||||
setDialogState({ open: true, manual: true });
|
// Defer so the opening tap does not trip FocusTrap click-outside on Android.
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
setDialogState({ open: true, manual: true });
|
||||||
|
});
|
||||||
}, [setDialogState]);
|
}, [setDialogState]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
|
import bundledManifest from '../../../public/update/manifest.json';
|
||||||
import { trimTrailingSlash } from '../utils/common';
|
import { trimTrailingSlash } from '../utils/common';
|
||||||
|
|
||||||
|
const bundledMarkdownByFile = import.meta.glob('../../../public/update/*.md', {
|
||||||
|
query: '?raw',
|
||||||
|
import: 'default',
|
||||||
|
eager: true,
|
||||||
|
}) as Record<string, string>;
|
||||||
|
|
||||||
|
const bundledImageByFile = import.meta.glob('../../../public/update/images/*', {
|
||||||
|
import: 'default',
|
||||||
|
eager: true,
|
||||||
|
}) as Record<string, string>;
|
||||||
|
|
||||||
function getUpdateBaseUrl(): string {
|
function getUpdateBaseUrl(): string {
|
||||||
const basePath = trimTrailingSlash(import.meta.env.BASE_URL || './');
|
const basePath = trimTrailingSlash(import.meta.env.BASE_URL || './');
|
||||||
const relative =
|
const relative =
|
||||||
@@ -64,27 +76,66 @@ function parseUpdateMarkdown(file: string, source: string): ParsedUpdateDoc {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadBundledManifest(): UpdateManifest {
|
||||||
|
const manifest = bundledManifest as UpdateManifest;
|
||||||
|
return {
|
||||||
|
current: manifest.current,
|
||||||
|
older: (manifest.older ?? []).filter((entry) => entry?.file),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadBundledDocument(file: string): ParsedUpdateDoc | null {
|
||||||
|
const safeFile = file.replace(/^\/+/, '');
|
||||||
|
const entry = Object.entries(bundledMarkdownByFile).find(([path]) =>
|
||||||
|
path.endsWith(`/${safeFile}`)
|
||||||
|
);
|
||||||
|
if (!entry) return null;
|
||||||
|
return parseUpdateMarkdown(safeFile, entry[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveBundledImageUrl(normalizedPath: string): string | undefined {
|
||||||
|
const needle = normalizedPath.replace(/^\/+/, '');
|
||||||
|
if (!needle) return undefined;
|
||||||
|
|
||||||
|
const entry = Object.entries(bundledImageByFile).find(([path]) => {
|
||||||
|
const normalized = path.replace(/\\/g, '/');
|
||||||
|
return normalized.endsWith(`/${needle}`) || normalized.endsWith(`/${needle.split('/').pop() ?? ''}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
return entry?.[1];
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveUpdateAssetUrl(src: string | undefined): string | undefined {
|
export function resolveUpdateAssetUrl(src: string | undefined): string | undefined {
|
||||||
if (!src) return undefined;
|
if (!src) return undefined;
|
||||||
if (/^(https?:|data:|blob:)/i.test(src)) return src;
|
if (/^(https?:|data:|blob:|capacitor:)/i.test(src)) return src;
|
||||||
|
|
||||||
|
const normalized = src.replace(/^\.\//, '').replace(/^\/+/, '');
|
||||||
|
const bundled = resolveBundledImageUrl(normalized);
|
||||||
|
if (bundled) return bundled;
|
||||||
|
|
||||||
if (src.startsWith('/')) return src;
|
if (src.startsWith('/')) return src;
|
||||||
|
|
||||||
const normalized = src.replace(/^\.\//, '');
|
|
||||||
return new URL(normalized, `${getUpdateBaseUrl()}/`).href;
|
return new URL(normalized, `${getUpdateBaseUrl()}/`).href;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadUpdateManifest(): Promise<UpdateManifest | null> {
|
export async function loadUpdateManifest(): Promise<UpdateManifest | null> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(getUpdateFileUrl('manifest.json'), { cache: 'no-cache' });
|
const response = await fetch(getUpdateFileUrl('manifest.json'), { cache: 'no-cache' });
|
||||||
if (!response.ok) return null;
|
if (response.ok) {
|
||||||
|
const data = (await response.json()) as UpdateManifest;
|
||||||
|
if (data?.current && Array.isArray(data.older)) {
|
||||||
|
return {
|
||||||
|
current: data.current,
|
||||||
|
older: data.older.filter((entry) => entry?.file),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// fall through to bundled copy (Capacitor / offline)
|
||||||
|
}
|
||||||
|
|
||||||
const data = (await response.json()) as UpdateManifest;
|
try {
|
||||||
if (!data?.current || !Array.isArray(data.older)) return null;
|
return loadBundledManifest();
|
||||||
|
|
||||||
return {
|
|
||||||
current: data.current,
|
|
||||||
older: data.older.filter((entry) => entry?.file),
|
|
||||||
};
|
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -96,13 +147,15 @@ export async function loadUpdateDocument(file: string): Promise<ParsedUpdateDoc
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(getUpdateFileUrl(safeFile), { cache: 'no-cache' });
|
const response = await fetch(getUpdateFileUrl(safeFile), { cache: 'no-cache' });
|
||||||
if (!response.ok) return null;
|
if (response.ok) {
|
||||||
|
const source = await response.text();
|
||||||
const source = await response.text();
|
return parseUpdateMarkdown(safeFile, source);
|
||||||
return parseUpdateMarkdown(safeFile, source);
|
}
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
// fall through to bundled copy
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return loadBundledDocument(safeFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadCurrentUpdateDocument(): Promise<ParsedUpdateDoc | null> {
|
export async function loadCurrentUpdateDocument(): Promise<ParsedUpdateDoc | null> {
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ export function About({ requestClose }: AboutProps) {
|
|||||||
const [version, setVersion] = useState<string>('');
|
const [version, setVersion] = useState<string>('');
|
||||||
const [protocolStatus, setProtocolStatus] = useState<string>('Checking desktop protocol integration...');
|
const [protocolStatus, setProtocolStatus] = useState<string>('Checking desktop protocol integration...');
|
||||||
const [protocolBusy, setProtocolBusy] = useState<boolean>(false);
|
const [protocolBusy, setProtocolBusy] = useState<boolean>(false);
|
||||||
|
const [updatePreview, setUpdatePreview] = useState<{ title: string; description: string } | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
const formatProtocolStatus = useCallback((data: {
|
const formatProtocolStatus = useCallback((data: {
|
||||||
scheme: string;
|
scheme: string;
|
||||||
@@ -101,8 +104,6 @@ export function About({ requestClose }: AboutProps) {
|
|||||||
getCurrentUpdatePreview().then(setUpdatePreview).catch(() => setUpdatePreview(null));
|
getCurrentUpdatePreview().then(setUpdatePreview).catch(() => setUpdatePreview(null));
|
||||||
}, [refreshProtocolStatus]);
|
}, [refreshProtocolStatus]);
|
||||||
|
|
||||||
const [updatePreview, setUpdatePreview] = useState<{ title: string; description: string } | null>(null);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<PageHeader outlined={false}>
|
<PageHeader outlined={false}>
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ export async function loadColorPreference(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function extractMemberColorPreference(room: Room | undefined, userId: string): ColorPreference | undefined {
|
export function extractMemberColorPreference(room: Room | undefined, userId: string): ColorPreference | undefined {
|
||||||
if (!room) return undefined;
|
if (!room || typeof (room as Room).getMember !== 'function' || !userId) return undefined;
|
||||||
const member = room.getMember(userId);
|
const member = room.getMember(userId);
|
||||||
const content = member?.events.member?.getContent();
|
const content = member?.events.member?.getContent();
|
||||||
if (!content) return undefined;
|
if (!content) return undefined;
|
||||||
|
|||||||
@@ -147,6 +147,27 @@ body.stationery-dark-theme {
|
|||||||
background-color: #262626;
|
background-color: #262626;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Folds Overlay / PopOut portals mount here. The container must not steal taps;
|
||||||
|
* only its children should. Empty portal shells (e.g. closed overlays) must not
|
||||||
|
* block dialogs portaled elsewhere.
|
||||||
|
*/
|
||||||
|
#portalContainer {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 9998;
|
||||||
|
pointer-events: none;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#portalContainer > * {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#portalContainer > *:empty {
|
||||||
|
pointer-events: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
.twilight-theme #root {
|
.twilight-theme #root {
|
||||||
background: linear-gradient(180deg, rgba(28, 26, 46, 0.5) 0%, rgba(36, 34, 61, 0.3) 100%);
|
background: linear-gradient(180deg, rgba(28, 26, 46, 0.5) 0%, rgba(36, 34, 61, 0.3) 100%);
|
||||||
}
|
}
|
||||||
|
|||||||
101
vanillaExtractIdentifiers.js
Normal file
101
vanillaExtractIdentifiers.js
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import path from 'path';
|
||||||
|
import { transformAsync } from '@babel/core';
|
||||||
|
import vanillaBabelPlugin from '@vanilla-extract/babel-plugin-debug-ids';
|
||||||
|
import typescriptSyntax from '@babel/plugin-syntax-typescript';
|
||||||
|
|
||||||
|
const CSS_TS_FILTER = /\.css\.(js|cjs|mjs|jsx|ts|tsx)(\?.*)?$/;
|
||||||
|
|
||||||
|
/** Slug for vanilla-extract class names (letters, digits, _, -). */
|
||||||
|
function sanitizeIdentifierPart(value) {
|
||||||
|
return String(value)
|
||||||
|
.replace(/\s/g, '_')
|
||||||
|
.replace(/[^a-zA-Z0-9_-]/g, '_')
|
||||||
|
.replace(/_+/g, '_')
|
||||||
|
.replace(/^_+|_+$/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function fileScopeSlug(filePath, projectRoot, packageName) {
|
||||||
|
const absolute = path.isAbsolute(filePath) ? filePath : path.join(projectRoot, filePath);
|
||||||
|
const normalized = absolute.replace(/\\/g, '/');
|
||||||
|
const srcRoot = path.join(projectRoot, 'src').replace(/\\/g, '/');
|
||||||
|
|
||||||
|
let rel;
|
||||||
|
if (normalized.startsWith(srcRoot)) {
|
||||||
|
rel = path.relative(path.join(projectRoot, 'src'), absolute);
|
||||||
|
} else if (normalized.includes('/src/')) {
|
||||||
|
rel = normalized.split('/src/').pop();
|
||||||
|
} else if (packageName) {
|
||||||
|
rel = path.join(packageName, path.basename(absolute));
|
||||||
|
} else {
|
||||||
|
rel = path.relative(projectRoot, absolute);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sanitizeIdentifierPart(
|
||||||
|
String(rel)
|
||||||
|
.replace(/\\/g, '/')
|
||||||
|
.replace(/\.css\.(ts|tsx|js|cjs|mjs|jsx)$/i, '')
|
||||||
|
.replace(/\//g, '_')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function finalizeIdentifier(parts) {
|
||||||
|
let name = parts.filter(Boolean).join('_');
|
||||||
|
if (!name) {
|
||||||
|
name = 've_style';
|
||||||
|
}
|
||||||
|
if (/^[0-9]/.test(name)) {
|
||||||
|
name = `_${name}`;
|
||||||
|
}
|
||||||
|
if (!/^[A-Z_][0-9A-Z_-]+$/i.test(name)) {
|
||||||
|
name = `ve_${name}`;
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Human-readable vanilla-extract class names without hash suffixes.
|
||||||
|
* Example: app_components_updates_dialog_UpdatesDialog_Root
|
||||||
|
*/
|
||||||
|
export function createReadableVanillaExtractIdentifiers(projectRoot) {
|
||||||
|
return function readableVanillaExtractIdentifier({ debugId, filePath, packageName, hash }) {
|
||||||
|
const scope = fileScopeSlug(filePath, projectRoot, packageName);
|
||||||
|
const exportName = debugId ? sanitizeIdentifierPart(debugId) : '';
|
||||||
|
|
||||||
|
if (exportName) {
|
||||||
|
return finalizeIdentifier([scope, exportName]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unnamed styles (rare): keep a short disambiguator from the scoped hash.
|
||||||
|
const suffix = sanitizeIdentifierPart(String(hash).replace(/^_/, ''));
|
||||||
|
return finalizeIdentifier([scope, suffix]);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injects export names into style() calls so readable identifiers can use them.
|
||||||
|
* Required when identifiers is a function — vanilla-extract only runs this for identOption === 'debug'.
|
||||||
|
*/
|
||||||
|
export function vanillaExtractDebugIdsPlugin() {
|
||||||
|
return {
|
||||||
|
name: 'vanilla-extract-debug-ids',
|
||||||
|
enforce: 'pre',
|
||||||
|
async transform(code, id) {
|
||||||
|
if (!CSS_TS_FILTER.test(id)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await transformAsync(code, {
|
||||||
|
filename: id,
|
||||||
|
plugins: [vanillaBabelPlugin, typescriptSyntax],
|
||||||
|
configFile: false,
|
||||||
|
babelrc: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result?.code) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { code: result.code, map: result.map };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -11,6 +11,10 @@ import fs from 'fs';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import buildConfig from './build.config';
|
import buildConfig from './build.config';
|
||||||
import { liveTsxPlugin, readDefaultLiveSource } from './playground-liveTsxPlugin';
|
import { liveTsxPlugin, readDefaultLiveSource } from './playground-liveTsxPlugin';
|
||||||
|
import {
|
||||||
|
createReadableVanillaExtractIdentifiers,
|
||||||
|
vanillaExtractDebugIdsPlugin,
|
||||||
|
} from './vanillaExtractIdentifiers.js';
|
||||||
|
|
||||||
const projectRoot = path.resolve();
|
const projectRoot = path.resolve();
|
||||||
|
|
||||||
@@ -56,7 +60,8 @@ const copyFiles = {
|
|||||||
{
|
{
|
||||||
src: 'public/update/**/*',
|
src: 'public/update/**/*',
|
||||||
dest: 'update',
|
dest: 'update',
|
||||||
rename: (_name, _ext, fullPath) => fullPath.replace(/^public[/\\]update[/\\]/, ''),
|
// stripBase removes public/update from the matched path; rename alone only changes the filename.
|
||||||
|
rename: { stripBase: 2 },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -251,7 +256,7 @@ function corsProxyMiddleware() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig(() => ({
|
||||||
appType: 'spa',
|
appType: 'spa',
|
||||||
publicDir: false,
|
publicDir: false,
|
||||||
base: buildConfig.base,
|
base: buildConfig.base,
|
||||||
@@ -303,7 +308,15 @@ export default defineConfig({
|
|||||||
promiseImportName: (i) => `__tla_${i}`,
|
promiseImportName: (i) => `__tla_${i}`,
|
||||||
}),
|
}),
|
||||||
viteStaticCopy(copyFiles),
|
viteStaticCopy(copyFiles),
|
||||||
vanillaExtractPlugin(),
|
...(process.env.VITE_VE_IDENTIFIERS === 'short' ? [] : [vanillaExtractDebugIdsPlugin()]),
|
||||||
|
vanillaExtractPlugin({
|
||||||
|
unstable_pluginFilter: ({ name }) =>
|
||||||
|
name === 'vite-tsconfig-paths' || name === 'vanilla-extract-debug-ids',
|
||||||
|
identifiers:
|
||||||
|
process.env.VITE_VE_IDENTIFIERS === 'short'
|
||||||
|
? 'short'
|
||||||
|
: createReadableVanillaExtractIdentifiers(projectRoot),
|
||||||
|
}),
|
||||||
wasm(),
|
wasm(),
|
||||||
react(),
|
react(),
|
||||||
VitePWA({
|
VitePWA({
|
||||||
@@ -358,4 +371,4 @@ export default defineConfig({
|
|||||||
plugins: [inject({ Buffer: ['buffer', 'Buffer'] })],
|
plugins: [inject({ Buffer: ['buffer', 'Buffer'] })],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user