Fix What's New dialog layout and improve dev styling/debugging.
All checks were successful
Trigger cinny-mobile / dispatch (push) Successful in 1s
All checks were successful
Trigger cinny-mobile / dispatch (push) Successful in 1s
Portal the updates dialog to document.body with stable data attributes, fix max-width (invalid S800 token), bundle update images for Capacitor, and add readable vanilla-extract class names plus portalContainer tap fixes.
This commit is contained in:
@@ -96,12 +96,6 @@
|
||||
</script>
|
||||
<div id="root"></div>
|
||||
<div id="portalContainer"></div>
|
||||
<style>
|
||||
#portalContainer {
|
||||
position: relative;
|
||||
z-index: 10000;
|
||||
}
|
||||
</style>
|
||||
<script type="module" src="./src/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,38 +3,64 @@ import { color, config, toRem } from 'folds';
|
||||
|
||||
const MOBILE_BREAKPOINT = '480px';
|
||||
|
||||
export const PortalLayer = style({
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
zIndex: config.zIndex.Max,
|
||||
pointerEvents: 'auto',
|
||||
});
|
||||
/** Above folds overlays (9999) and Settings modals. */
|
||||
const UPDATES_DIALOG_Z = 10001;
|
||||
|
||||
/** Dialog + overlay padding must never exceed the viewport height. */
|
||||
const DIALOG_MAX_HEIGHT =
|
||||
'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',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
maxHeight: '100vh',
|
||||
maxWidth: '100vw',
|
||||
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)',
|
||||
pointerEvents: 'none',
|
||||
});
|
||||
|
||||
export const DialogShell = style({
|
||||
/** [data-updates-dialog-panel] */
|
||||
export const Panel = style({
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
maxWidth: `min(${toRem(560)}, calc(100vw - ${config.space.S800}))`,
|
||||
maxWidth: toRem(560),
|
||||
maxHeight: DIALOG_MAX_HEIGHT,
|
||||
overflow: 'hidden',
|
||||
pointerEvents: 'auto',
|
||||
borderRadius: config.radii.R400,
|
||||
backgroundColor: color.Surface.Container,
|
||||
color: color.Surface.OnContainer,
|
||||
boxShadow: config.shadow.E400,
|
||||
'@media': {
|
||||
[`(max-width: ${MOBILE_BREAKPOINT})`]: {
|
||||
maxWidth: `calc(100vw - ${config.space.S400})`,
|
||||
maxWidth: `calc(100vw - 2 * ${config.space.S400})`,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -103,7 +129,7 @@ export const BodyContent = style({
|
||||
});
|
||||
|
||||
export const BodyLoading = style({
|
||||
padding: config.space.S800,
|
||||
padding: config.space.S600,
|
||||
});
|
||||
|
||||
export const Markdown = style({
|
||||
@@ -166,7 +192,7 @@ globalStyle(`${Markdown} code`, {
|
||||
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
|
||||
fontSize: toRem(13),
|
||||
padding: `0 ${config.space.S100}`,
|
||||
borderRadius: config.radii.R200,
|
||||
borderRadius: config.radii.R300,
|
||||
backgroundColor: color.SurfaceVariant.Container,
|
||||
color: color.SurfaceVariant.OnContainer,
|
||||
});
|
||||
|
||||
@@ -1,19 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
IconButton,
|
||||
Overlay,
|
||||
OverlayBackdrop,
|
||||
OverlayCenter,
|
||||
Scroll,
|
||||
Spinner,
|
||||
Text,
|
||||
usePopOutContainer,
|
||||
} from 'folds';
|
||||
import { Box, Button, IconButton, Scroll, Spinner, Text } from 'folds';
|
||||
import { Icon, Icons } from '../icons';
|
||||
import { stopPropagation } from '../../utils/keyboard';
|
||||
import type { ParsedUpdateDoc, UpdateManifest } from '../../data/updateNotes';
|
||||
@@ -37,7 +25,6 @@ export function UpdatesDialog({
|
||||
onClose,
|
||||
}: UpdatesDialogProps) {
|
||||
const [showOlderList, setShowOlderList] = useState(false);
|
||||
const popOutContainer = usePopOutContainer();
|
||||
|
||||
const displayVersion = activeDoc?.version;
|
||||
const displayTitle = activeDoc?.title ?? (displayVersion ? `Version ${displayVersion}` : 'Release notes');
|
||||
@@ -49,29 +36,35 @@ export function UpdatesDialog({
|
||||
setShowOlderList(false);
|
||||
};
|
||||
|
||||
const portalTarget =
|
||||
popOutContainer ??
|
||||
document.getElementById('portalContainer') ??
|
||||
document.body;
|
||||
|
||||
return createPortal(
|
||||
<div className={css.PortalLayer}>
|
||||
<Overlay open backdrop={<OverlayBackdrop />}>
|
||||
<OverlayCenter className={css.OverlayFrame}>
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
initialFocus: false,
|
||||
clickOutsideDeactivates: false,
|
||||
escapeDeactivates: stopPropagation,
|
||||
}}
|
||||
<div
|
||||
data-updates-dialog=""
|
||||
className={css.Root}
|
||||
data-disable-swipe-back="true"
|
||||
data-disable-swipe-reply="true"
|
||||
>
|
||||
<div data-updates-dialog-backdrop="" className={css.Backdrop} aria-hidden="true" />
|
||||
<div data-updates-dialog-frame="" className={css.Frame}>
|
||||
<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.HeroRow}>
|
||||
<img className={css.HeroLogo} src={PaarrotSVG} alt="" draggable={false} />
|
||||
<Box className={css.HeroTitleWrap}>
|
||||
<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'}
|
||||
</Text>
|
||||
</Box>
|
||||
@@ -181,11 +174,10 @@ export function UpdatesDialog({
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Dialog>
|
||||
</div>
|
||||
</FocusTrap>
|
||||
</OverlayCenter>
|
||||
</Overlay>
|
||||
</div>
|
||||
</div>,
|
||||
portalTarget
|
||||
document.body
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,11 @@ const bundledMarkdownByFile = import.meta.glob('../../../public/update/*.md', {
|
||||
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 {
|
||||
const basePath = trimTrailingSlash(import.meta.env.BASE_URL || './');
|
||||
const relative =
|
||||
@@ -88,12 +93,28 @@ function loadBundledDocument(file: string): ParsedUpdateDoc | 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 {
|
||||
if (!src) return undefined;
|
||||
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;
|
||||
|
||||
const normalized = src.replace(/^\.\//, '');
|
||||
return new URL(normalized, `${getUpdateBaseUrl()}/`).href;
|
||||
}
|
||||
|
||||
|
||||
@@ -147,6 +147,27 @@ body.stationery-dark-theme {
|
||||
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 {
|
||||
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 buildConfig from './build.config';
|
||||
import { liveTsxPlugin, readDefaultLiveSource } from './playground-liveTsxPlugin';
|
||||
import {
|
||||
createReadableVanillaExtractIdentifiers,
|
||||
vanillaExtractDebugIdsPlugin,
|
||||
} from './vanillaExtractIdentifiers.js';
|
||||
|
||||
const projectRoot = path.resolve();
|
||||
|
||||
@@ -252,7 +256,7 @@ function corsProxyMiddleware() {
|
||||
};
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
export default defineConfig(() => ({
|
||||
appType: 'spa',
|
||||
publicDir: false,
|
||||
base: buildConfig.base,
|
||||
@@ -304,7 +308,15 @@ export default defineConfig({
|
||||
promiseImportName: (i) => `__tla_${i}`,
|
||||
}),
|
||||
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(),
|
||||
react(),
|
||||
VitePWA({
|
||||
@@ -359,4 +371,4 @@ export default defineConfig({
|
||||
plugins: [inject({ Buffer: ['buffer', 'Buffer'] })],
|
||||
},
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user