mirror of
https://github.com/suitenumerique/docs.git
synced 2026-04-26 01:25:05 +02:00
Compare commits
8 Commits
pr/renovat
...
feat/e2e-e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0144044c55 | ||
|
|
a6da37e231 | ||
|
|
9aeedd1d03 | ||
|
|
f7d4e6810b | ||
|
|
b740ffa52c | ||
|
|
f555e36e98 | ||
|
|
de11ab508f | ||
|
|
dc2fe4905b |
10
.github/workflows/impress.yml
vendored
10
.github/workflows/impress.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
if: github.event_name == 'pull_request' # Makes sense only for pull requests
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: show
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
github.event_name == 'pull_request'
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 50
|
||||
- name: Check that the CHANGELOG has been modified in the current branch
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
- name: Check CHANGELOG max line length
|
||||
run: |
|
||||
max_line_length=$(cat CHANGELOG.md | grep -Ev "^\[.*\]: https://github.com" | wc -L)
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
if: github.event_name == 'pull_request'
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
- name: Install codespell
|
||||
run: pip install --user codespell
|
||||
- name: Check for typos
|
||||
@@ -92,7 +92,7 @@ jobs:
|
||||
working-directory: src/backend
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
|
||||
@@ -15,6 +15,7 @@ and this project adheres to
|
||||
|
||||
- ⚡️(sw) stop to cache external resources likes videos #1655
|
||||
- 💥(frontend) upgrade to ui-kit v2
|
||||
- ⚡️(frontend) improve perf on upload and table of contents #1662
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -31,6 +32,7 @@ and this project adheres to
|
||||
|
||||
- ✨(export) enable ODT export for documents #1524
|
||||
- ✨(frontend) improve mobile UX by showing subdocs count #1540
|
||||
- ✅(e2e) add test to compare generated PDF against reference template #1648
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
BIN
src/frontend/apps/e2e/__tests__/app-impress/assets/panojpg.jpeg
Normal file
BIN
src/frontend/apps/e2e/__tests__/app-impress/assets/panojpg.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 231 KiB |
BIN
src/frontend/apps/e2e/__tests__/app-impress/assets/panopng.png
Normal file
BIN
src/frontend/apps/e2e/__tests__/app-impress/assets/panopng.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 839 KiB |
@@ -1,3 +1,4 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import { expect, test } from '@playwright/test';
|
||||
@@ -13,6 +14,35 @@ import {
|
||||
import { openSuggestionMenu, writeInEditor } from './utils-editor';
|
||||
import { createRootSubPage } from './utils-sub-pages';
|
||||
|
||||
const REGRESSION_FIXTURE_CONTENT = fs.readFileSync(
|
||||
path.join(__dirname, 'assets/content.txt'),
|
||||
'utf-8',
|
||||
);
|
||||
const REGRESSION_SNAPSHOT_NAME = 'doc-export-regression.pdf';
|
||||
const REGRESSION_DOC_TITLE = 'doc-export-regression-reference';
|
||||
|
||||
/**
|
||||
* Playwright snapshots store the raw PDF bytes. However, each export embeds
|
||||
* dynamic metadata (timestamps, font-subset identifiers, etc.) that would make
|
||||
* the snapshot differ at every run. To ensure deterministic comparisons we
|
||||
* strip/neutralize those fields before matching against the reference PDF.
|
||||
*/
|
||||
const sanitizePdfBuffer = (buffer: Buffer) => {
|
||||
const pdfText = buffer.toString('latin1');
|
||||
const neutralized = pdfText
|
||||
// Remove per-export timestamps
|
||||
.replace(/\/CreationDate\s*\(.*?\)/g, '/CreationDate ()')
|
||||
.replace(/\/ModDate\s*\(.*?\)/g, '/ModDate ()')
|
||||
// Remove file identifiers
|
||||
.replace(/\/ID\s*\[<[^>]+>\s*<[^>]+>\]/g, '/ID [<0><0>]')
|
||||
.replace(/D:\d{14}Z/g, 'D:00000000000000Z')
|
||||
// Remove subset font prefixes generated by PDF renderer
|
||||
.replace(/\b[A-Z]{6}\+(Inter18pt-[A-Za-z]+)\b/g, 'STATIC+$1')
|
||||
.replace(/\b[A-Z]{6}\+(GeistMono-[A-Za-z]+)\b/g, 'STATIC+$1');
|
||||
|
||||
return Buffer.from(neutralized, 'latin1');
|
||||
};
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
});
|
||||
@@ -551,4 +581,118 @@ test.describe('Doc Export', () => {
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toBe(`${docChild}.odt`);
|
||||
});
|
||||
|
||||
/**
|
||||
* Regression guard for the full PDF export pipeline.
|
||||
*
|
||||
* Usage reminder:
|
||||
* 1. `npx playwright test __tests__/app-impress/doc-export.spec.ts --update-snapshots -g "full document" --project=chromium`
|
||||
* -> refresh the reference PDF whenever we intentionally change the export output.
|
||||
* 2. `npx playwright test __tests__/app-impress/doc-export.spec.ts -g "full document" --project=chromium`
|
||||
* -> CI (and local runs without --update-snapshots) will compare the PDF to the reference
|
||||
* and fail on any byte-level difference once the dynamic metadata has been sanitized.
|
||||
*/
|
||||
test('it keeps the full document PDF export identical to the reference snapshot', async ({
|
||||
page,
|
||||
browserName,
|
||||
}, testInfo) => {
|
||||
// PDF generation for a large, image-heavy document can be slow in CI.
|
||||
// Give this regression test a higher timeout budget than the default.
|
||||
testInfo.setTimeout(120000);
|
||||
const snapshotPath = testInfo.snapshotPath(REGRESSION_SNAPSHOT_NAME);
|
||||
|
||||
test.skip(
|
||||
!fs.existsSync(snapshotPath) &&
|
||||
testInfo.config.updateSnapshots === 'none',
|
||||
`Missing PDF snapshot at ${snapshotPath}. Run Playwright with --update-snapshots to record it.`,
|
||||
);
|
||||
|
||||
// We must use a deterministic title so that block content (and thus the
|
||||
// exported PDF) stays identical between runs.
|
||||
await createDoc(page, 'doc-export-regression', browserName, 1);
|
||||
const titleInput = page.getByRole('textbox', { name: 'Document title' });
|
||||
await expect(titleInput).toBeVisible();
|
||||
await titleInput.fill(REGRESSION_DOC_TITLE);
|
||||
await titleInput.blur();
|
||||
await verifyDocName(page, REGRESSION_DOC_TITLE);
|
||||
const regressionDoc = REGRESSION_DOC_TITLE;
|
||||
|
||||
const docId = page
|
||||
.url()
|
||||
.split('/docs/')[1]
|
||||
?.split('/')
|
||||
.filter(Boolean)
|
||||
.shift();
|
||||
|
||||
expect(docId).toBeTruthy();
|
||||
|
||||
// Inject the pre-crafted blocknote document via the REST API to avoid
|
||||
// rebuilding it through the UI (which would be slow and flaky).
|
||||
const cookies = await page.context().cookies();
|
||||
const csrfToken = cookies.find(
|
||||
(cookie) => cookie.name === 'csrftoken',
|
||||
)?.value;
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'content-type': 'application/json',
|
||||
};
|
||||
|
||||
if (csrfToken) {
|
||||
headers['X-CSRFToken'] = csrfToken;
|
||||
}
|
||||
|
||||
const updateResponse = await page.request.patch(
|
||||
`http://localhost:8071/api/v1.0/documents/${docId}/`,
|
||||
{
|
||||
headers,
|
||||
data: {
|
||||
content: REGRESSION_FIXTURE_CONTENT,
|
||||
websocket: true,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!updateResponse.ok()) {
|
||||
throw new Error(
|
||||
`Failed to seed document content. Status: ${updateResponse.status()}, body: ${await updateResponse.text()}`,
|
||||
);
|
||||
}
|
||||
|
||||
await page.reload();
|
||||
// After reloading, just ensure the editor container is present before exporting.
|
||||
await expect(page.locator('.--docs--editor-container')).toBeVisible({
|
||||
timeout: 15000,
|
||||
});
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Export the document',
|
||||
})
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
page.getByTestId('doc-open-modal-download-button'),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(page.getByTestId('doc-export-download-button')).toBeEnabled({
|
||||
timeout: 15000,
|
||||
});
|
||||
|
||||
// Export to PDF and confirm the generated bytes match the reference file.
|
||||
const downloadPromise = page.waitForEvent('download', {
|
||||
timeout: 60000,
|
||||
predicate: (download) =>
|
||||
download.suggestedFilename().includes(`${regressionDoc}.pdf`),
|
||||
});
|
||||
|
||||
void page.getByTestId('doc-export-download-button').click();
|
||||
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toBe(`${regressionDoc}.pdf`);
|
||||
|
||||
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
|
||||
const normalizedPdfBuffer = sanitizePdfBuffer(pdfBuffer);
|
||||
|
||||
expect(normalizedPdfBuffer).toMatchSnapshot(REGRESSION_SNAPSHOT_NAME);
|
||||
});
|
||||
});
|
||||
|
||||
Binary file not shown.
29
src/frontend/apps/e2e/print-datauris.js
Normal file
29
src/frontend/apps/e2e/print-datauris.js
Normal file
@@ -0,0 +1,29 @@
|
||||
//utilitary script to print the datauris of the targeted assets
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ASSETS_ROOT = path.resolve(__dirname, '__tests__/app-impress/assets');
|
||||
|
||||
function saveDataUrl(file, mime, outName) {
|
||||
const abs = path.join(ASSETS_ROOT, file);
|
||||
const base64 = fs.readFileSync(abs).toString('base64');
|
||||
const dataUrl = `data:${mime};base64,${base64}`;
|
||||
const outPath = path.join(ASSETS_ROOT, outName);
|
||||
fs.writeFileSync(outPath, dataUrl, 'utf8');
|
||||
console.log(`Wrote ${outName}`);
|
||||
}
|
||||
|
||||
// PNG
|
||||
saveDataUrl('panopng.png', 'image/png', 'pano-png-dataurl.txt');
|
||||
|
||||
// JPG
|
||||
saveDataUrl('panojpg.jpeg', 'image/jpeg', 'pano-jpg-dataurl.txt');
|
||||
|
||||
// SVG
|
||||
const svgPath = path.join(ASSETS_ROOT, 'test.svg');
|
||||
const svgText = fs.readFileSync(svgPath, 'utf8');
|
||||
const svgDataUrl =
|
||||
'data:image/svg+xml;base64,' + Buffer.from(svgText).toString('base64');
|
||||
fs.writeFileSync(path.join(ASSETS_ROOT, 'test-svg-dataurl.txt'), svgDataUrl, 'utf8');
|
||||
console.log('Wrote test-svg-dataurl.txt');
|
||||
@@ -1,6 +1,5 @@
|
||||
import clsx from 'clsx';
|
||||
import { useEffect } from 'react';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, Loading } from '@/components';
|
||||
import { DocHeader } from '@/docs/doc-header/';
|
||||
@@ -97,18 +96,7 @@ export const DocEditor = ({ doc }: DocEditorProps) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{isDesktop && (
|
||||
<Box
|
||||
$height="100vh"
|
||||
$position="absolute"
|
||||
$css={css`
|
||||
top: 72px;
|
||||
right: 20px;
|
||||
`}
|
||||
>
|
||||
<TableContent />
|
||||
</Box>
|
||||
)}
|
||||
{isDesktop && <TableContent />}
|
||||
<DocEditorContainer
|
||||
docHeader={<DocHeader doc={doc} />}
|
||||
docEditor={
|
||||
|
||||
@@ -75,27 +75,31 @@ const UploadLoaderBlockComponent = ({
|
||||
|
||||
loopCheckDocMediaStatus(url)
|
||||
.then((response) => {
|
||||
// Replace the loading block with the resource block (image, audio, video, pdf ...)
|
||||
try {
|
||||
editor.replaceBlocks(
|
||||
[block.id],
|
||||
[
|
||||
{
|
||||
type: block.props.blockUploadType,
|
||||
props: {
|
||||
url: `${mediaUrl}${response.file}`,
|
||||
showPreview: block.props.blockUploadShowPreview,
|
||||
name: block.props.blockUploadName,
|
||||
caption: '',
|
||||
backgroundColor: 'default',
|
||||
textAlignment: 'left',
|
||||
},
|
||||
} as never,
|
||||
],
|
||||
);
|
||||
} catch {
|
||||
/* During collaboration, another user might have updated the block */
|
||||
}
|
||||
// Add random delay to reduce collision probability during collaboration
|
||||
const randomDelay = Math.random() * 800;
|
||||
setTimeout(() => {
|
||||
// Replace the loading block with the resource block (image, audio, video, pdf ...)
|
||||
try {
|
||||
editor.replaceBlocks(
|
||||
[block.id],
|
||||
[
|
||||
{
|
||||
type: block.props.blockUploadType,
|
||||
props: {
|
||||
url: `${mediaUrl}${response.file}`,
|
||||
showPreview: block.props.blockUploadShowPreview,
|
||||
name: block.props.blockUploadName,
|
||||
caption: '',
|
||||
backgroundColor: 'default',
|
||||
textAlignment: 'left',
|
||||
},
|
||||
} as never,
|
||||
],
|
||||
);
|
||||
} catch {
|
||||
/* During collaboration, another user might have updated the block */
|
||||
}
|
||||
}, randomDelay);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error analyzing file:', error);
|
||||
|
||||
@@ -9,11 +9,33 @@ export const useHeadings = (editor: DocsBlockNoteEditor) => {
|
||||
useEffect(() => {
|
||||
setHeadings(editor);
|
||||
|
||||
const unsubscribe = editor?.onChange(() => {
|
||||
setHeadings(editor);
|
||||
let timeoutId: NodeJS.Timeout;
|
||||
const DEBOUNCE_DELAY = 500;
|
||||
const unsubscribe = editor?.onChange((_, context) => {
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
const blocksChanges = context.getChanges();
|
||||
|
||||
if (!blocksChanges.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const blockChanges = blocksChanges[0];
|
||||
|
||||
if (
|
||||
blockChanges.type !== 'update' ||
|
||||
blockChanges.block.type !== 'heading'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
setHeadings(editor);
|
||||
}, DEBOUNCE_DELAY);
|
||||
});
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
resetHeadings();
|
||||
unsubscribe();
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Block } from '@blocknote/core';
|
||||
import { captureException } from '@sentry/nextjs';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -36,73 +37,93 @@ export const useUploadFile = (docId: string) => {
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* When we upload a file it can takes some time to analyze it (e.g. virus scan).
|
||||
* This hook listen to upload end and replace the uploaded block by a uploadLoader
|
||||
* block to show analyzing status.
|
||||
* The uploadLoader block will then handle the status display until the analysis is done
|
||||
* then replaced by the final block (e.g. image, pdf, etc.).
|
||||
* @param editor
|
||||
*/
|
||||
export const useUploadStatus = (editor: DocsBlockNoteEditor) => {
|
||||
const ANALYZE_URL = 'media-check';
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = editor.onChange((_, context) => {
|
||||
const blocksChanges = context.getChanges();
|
||||
|
||||
if (!blocksChanges.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const blockChanges = blocksChanges[0];
|
||||
|
||||
/**
|
||||
* Replace the resource block by a uploadLoader block to show analyzing status
|
||||
*/
|
||||
const replaceBlockWithUploadLoader = useCallback(
|
||||
(block: Block) => {
|
||||
if (
|
||||
blockChanges.source.type !== 'local' ||
|
||||
blockChanges.type !== 'update' ||
|
||||
!('url' in blockChanges.block.props) ||
|
||||
('url' in blockChanges.block.props &&
|
||||
!blockChanges.block.props.url.includes(ANALYZE_URL))
|
||||
!block ||
|
||||
!('url' in block.props) ||
|
||||
('url' in block.props && !block.props.url.includes(ANALYZE_URL))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const blockUploadUrl = blockChanges.block.props.url;
|
||||
const blockUploadType = blockChanges.block.type;
|
||||
const blockUploadName = blockChanges.block.props.name;
|
||||
const blockUploadUrl = block.props.url;
|
||||
const blockUploadType = block.type;
|
||||
const blockUploadName = block.props.name;
|
||||
const blockUploadShowPreview =
|
||||
('showPreview' in blockChanges.block.props &&
|
||||
blockChanges.block.props.showPreview) ||
|
||||
false;
|
||||
('showPreview' in block.props && block.props.showPreview) || false;
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
// Replace the resource block by a uploadLoader block
|
||||
// to show analyzing status
|
||||
try {
|
||||
editor.replaceBlocks(
|
||||
[blockChanges.block.id],
|
||||
[
|
||||
{
|
||||
type: 'uploadLoader',
|
||||
props: {
|
||||
information: t('Analyzing file...'),
|
||||
type: 'loading',
|
||||
blockUploadName,
|
||||
blockUploadType,
|
||||
blockUploadUrl,
|
||||
blockUploadShowPreview,
|
||||
},
|
||||
try {
|
||||
editor.replaceBlocks(
|
||||
[block.id],
|
||||
[
|
||||
{
|
||||
type: 'uploadLoader',
|
||||
props: {
|
||||
information: t('Analyzing file...'),
|
||||
type: 'loading',
|
||||
blockUploadName,
|
||||
blockUploadType,
|
||||
blockUploadUrl,
|
||||
blockUploadShowPreview,
|
||||
},
|
||||
],
|
||||
);
|
||||
} catch (error) {
|
||||
captureException(error, {
|
||||
extra: { info: 'Error replacing block for upload loader' },
|
||||
});
|
||||
}
|
||||
}, 250);
|
||||
},
|
||||
],
|
||||
);
|
||||
} catch (error) {
|
||||
captureException(error, {
|
||||
extra: { info: 'Error replacing block for upload loader' },
|
||||
});
|
||||
}
|
||||
},
|
||||
[editor, t],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const imagesBlocks = editor?.document.filter(
|
||||
(block) =>
|
||||
block.type === 'image' && block.props.url.includes(ANALYZE_URL),
|
||||
);
|
||||
|
||||
imagesBlocks.forEach((block) => {
|
||||
replaceBlockWithUploadLoader(block as Block);
|
||||
});
|
||||
}, [editor, replaceBlockWithUploadLoader]);
|
||||
|
||||
/**
|
||||
* Handle upload end to replace the upload block by a uploadLoader
|
||||
* block to show analyzing status
|
||||
*/
|
||||
useEffect(() => {
|
||||
editor.onUploadEnd((blockId) => {
|
||||
if (!blockId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const innerTimeoutId = setTimeout(() => {
|
||||
const block = editor.getBlock({ id: blockId });
|
||||
|
||||
replaceBlockWithUploadLoader(block as Block);
|
||||
}, 300);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
unsubscribe();
|
||||
clearTimeout(innerTimeoutId);
|
||||
};
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [editor, t]);
|
||||
}, [editor, replaceBlockWithUploadLoader]);
|
||||
};
|
||||
|
||||
@@ -10,15 +10,112 @@ import { MAIN_LAYOUT_ID } from '@/layouts/conf';
|
||||
import { Heading } from './Heading';
|
||||
|
||||
export const TableContent = () => {
|
||||
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
|
||||
const [containerHeight, setContainerHeight] = useState('100vh');
|
||||
|
||||
const { t } = useTranslation();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
/**
|
||||
* Calculate container height based on the scrollable content
|
||||
*/
|
||||
useEffect(() => {
|
||||
const mainLayout = document.getElementById(MAIN_LAYOUT_ID);
|
||||
if (mainLayout) {
|
||||
setContainerHeight(`${mainLayout.scrollHeight}px`);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onOpen = () => {
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
$height={containerHeight}
|
||||
$position="absolute"
|
||||
$css={css`
|
||||
top: 72px;
|
||||
right: 20px;
|
||||
`}
|
||||
>
|
||||
<Box
|
||||
as="nav"
|
||||
id="summaryContainer"
|
||||
$width={!isOpen ? '40px' : '200px'}
|
||||
$height={!isOpen ? '40px' : 'auto'}
|
||||
$maxHeight="calc(50vh - 60px)"
|
||||
$zIndex={1000}
|
||||
$align="center"
|
||||
$padding={isOpen ? 'xs' : '0'}
|
||||
$justify="center"
|
||||
$position="sticky"
|
||||
aria-label={t('Summary')}
|
||||
$css={css`
|
||||
top: var(--c--globals--spacings--0);
|
||||
border: 1px solid ${colorsTokens['brand-100']};
|
||||
overflow: hidden;
|
||||
border-radius: ${spacingsTokens['3xs']};
|
||||
background: ${colorsTokens['gray-000']};
|
||||
${isOpen &&
|
||||
css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
gap: ${spacingsTokens['2xs']};
|
||||
`}
|
||||
`}
|
||||
className="--docs--table-content"
|
||||
>
|
||||
{!isOpen && (
|
||||
<BoxButton
|
||||
onClick={onOpen}
|
||||
$width="100%"
|
||||
$height="100%"
|
||||
$justify="center"
|
||||
$align="center"
|
||||
aria-label={t('Summary')}
|
||||
aria-expanded={isOpen}
|
||||
aria-controls="toc-list"
|
||||
$css={css`
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 4px ${colorsTokens['brand-400']};
|
||||
background: ${colorsTokens['brand-100']};
|
||||
width: 90%;
|
||||
height: 90%;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<Icon
|
||||
$theme="brand"
|
||||
$variation="tertiary"
|
||||
iconName="list"
|
||||
variant="symbols-outlined"
|
||||
/>
|
||||
</BoxButton>
|
||||
)}
|
||||
{isOpen && <TableContentOpened setIsOpen={setIsOpen} />}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const TableContentOpened = ({
|
||||
setIsOpen,
|
||||
}: {
|
||||
setIsOpen: (isOpen: boolean) => void;
|
||||
}) => {
|
||||
const { headings } = useHeadingStore();
|
||||
const { editor } = useEditorStore();
|
||||
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
|
||||
|
||||
const [headingIdHighlight, setHeadingIdHighlight] = useState<string>();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const [isHover, setIsHover] = useState(false);
|
||||
|
||||
/**
|
||||
* Handle scroll to highlight the current heading in the table of content
|
||||
*/
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
if (!headings) {
|
||||
@@ -69,23 +166,10 @@ export const TableContent = () => {
|
||||
.getElementById(MAIN_LAYOUT_ID)
|
||||
?.removeEventListener('scroll', scrollFn);
|
||||
};
|
||||
}, [headings, setHeadingIdHighlight]);
|
||||
|
||||
const onOpen = () => {
|
||||
setIsHover(true);
|
||||
setTimeout(() => {
|
||||
const element = document.getElementById(`heading-${headingIdHighlight}`);
|
||||
|
||||
element?.scrollIntoView({
|
||||
behavior: 'instant',
|
||||
inline: 'center',
|
||||
block: 'center',
|
||||
});
|
||||
}, 0); // 300ms is the transition time of the box
|
||||
};
|
||||
}, [headings]);
|
||||
|
||||
const onClose = () => {
|
||||
setIsHover(false);
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
if (
|
||||
@@ -99,129 +183,69 @@ export const TableContent = () => {
|
||||
|
||||
return (
|
||||
<Box
|
||||
as="nav"
|
||||
id="summaryContainer"
|
||||
$width={!isHover ? '40px' : '200px'}
|
||||
$height={!isHover ? '40px' : 'auto'}
|
||||
$maxHeight="calc(50vh - 60px)"
|
||||
$zIndex={1000}
|
||||
$align="center"
|
||||
$padding={isHover ? 'xs' : '0'}
|
||||
$justify="center"
|
||||
$position="sticky"
|
||||
aria-label={t('Summary')}
|
||||
$width="100%"
|
||||
$overflow="hidden"
|
||||
$css={css`
|
||||
top: var(--c--globals--spacings--0);
|
||||
border: 1px solid ${colorsTokens['brand-100']};
|
||||
overflow: hidden;
|
||||
border-radius: ${spacingsTokens['3xs']};
|
||||
background: ${colorsTokens['gray-000']};
|
||||
${isHover &&
|
||||
css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
gap: ${spacingsTokens['2xs']};
|
||||
`}
|
||||
user-select: none;
|
||||
padding: ${spacingsTokens['4xs']};
|
||||
`}
|
||||
className="--docs--table-content"
|
||||
>
|
||||
{!isHover && (
|
||||
<Box
|
||||
$margin={{ bottom: spacingsTokens.xs }}
|
||||
$direction="row"
|
||||
$justify="space-between"
|
||||
$align="center"
|
||||
>
|
||||
<Text $weight="500" $size="sm">
|
||||
{t('Summary')}
|
||||
</Text>
|
||||
<BoxButton
|
||||
onClick={onOpen}
|
||||
$width="100%"
|
||||
$height="100%"
|
||||
onClick={onClose}
|
||||
$justify="center"
|
||||
$align="center"
|
||||
aria-label={t('Summary')}
|
||||
aria-expanded={isHover}
|
||||
aria-expanded="true"
|
||||
aria-controls="toc-list"
|
||||
$css={css`
|
||||
transition: none !important;
|
||||
transform: rotate(180deg);
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 4px ${colorsTokens['brand-400']};
|
||||
background: ${colorsTokens['brand-100']};
|
||||
width: 90%;
|
||||
height: 90%;
|
||||
box-shadow: 0 0 0 2px ${colorsTokens['brand-400']};
|
||||
border-radius: var(--c--globals--spacings--st);
|
||||
}
|
||||
`}
|
||||
>
|
||||
<Icon
|
||||
$theme="brand"
|
||||
$variation="tertiary"
|
||||
iconName="list"
|
||||
variant="symbols-outlined"
|
||||
/>
|
||||
<Icon iconName="menu_open" $theme="brand" $variation="tertiary" />
|
||||
</BoxButton>
|
||||
)}
|
||||
{isHover && (
|
||||
<Box
|
||||
$width="100%"
|
||||
$overflow="hidden"
|
||||
$css={css`
|
||||
user-select: none;
|
||||
padding: ${spacingsTokens['4xs']};
|
||||
`}
|
||||
>
|
||||
<Box
|
||||
$margin={{ bottom: spacingsTokens.xs }}
|
||||
$direction="row"
|
||||
$justify="space-between"
|
||||
$align="center"
|
||||
>
|
||||
<Text $weight="500" $size="sm">
|
||||
{t('Summary')}
|
||||
</Text>
|
||||
<BoxButton
|
||||
onClick={onClose}
|
||||
$justify="center"
|
||||
$align="center"
|
||||
aria-label={t('Summary')}
|
||||
aria-expanded={isHover}
|
||||
aria-controls="toc-list"
|
||||
$css={css`
|
||||
transition: none !important;
|
||||
transform: rotate(180deg);
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px ${colorsTokens['brand-400']};
|
||||
border-radius: var(--c--globals--spacings--st);
|
||||
}
|
||||
`}
|
||||
>
|
||||
<Icon iconName="menu_open" $theme="brand" $variation="tertiary" />
|
||||
</BoxButton>
|
||||
</Box>
|
||||
<Box
|
||||
as="ul"
|
||||
id="toc-list"
|
||||
role="list"
|
||||
$gap={spacingsTokens['3xs']}
|
||||
$css={css`
|
||||
overflow-y: auto;
|
||||
list-style: none;
|
||||
padding: ${spacingsTokens['3xs']};
|
||||
margin: 0;
|
||||
`}
|
||||
>
|
||||
{headings?.map(
|
||||
(heading) =>
|
||||
heading.contentText && (
|
||||
<Box as="li" role="listitem" key={heading.id}>
|
||||
<Heading
|
||||
editor={editor}
|
||||
headingId={heading.id}
|
||||
level={heading.props.level}
|
||||
text={heading.contentText}
|
||||
isHighlight={headingIdHighlight === heading.id}
|
||||
/>
|
||||
</Box>
|
||||
),
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<Box
|
||||
as="ul"
|
||||
id="toc-list"
|
||||
role="list"
|
||||
$gap={spacingsTokens['3xs']}
|
||||
$css={css`
|
||||
overflow-y: auto;
|
||||
list-style: none;
|
||||
padding: ${spacingsTokens['3xs']};
|
||||
margin: 0;
|
||||
`}
|
||||
>
|
||||
{headings?.map(
|
||||
(heading) =>
|
||||
heading.contentText && (
|
||||
<Box as="li" role="listitem" key={heading.id}>
|
||||
<Heading
|
||||
editor={editor}
|
||||
headingId={heading.id}
|
||||
level={heading.props.level}
|
||||
text={heading.contentText}
|
||||
isHighlight={headingIdHighlight === heading.id}
|
||||
/>
|
||||
</Box>
|
||||
),
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7134,19 +7134,19 @@ bluebird@^3.4.1:
|
||||
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
|
||||
|
||||
body-parser@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-2.2.0.tgz#f7a9656de305249a715b549b7b8fd1ab9dfddcfa"
|
||||
integrity sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-2.2.1.tgz#6df606b0eb0a6e3f783dde91dde182c24c82438c"
|
||||
integrity sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==
|
||||
dependencies:
|
||||
bytes "^3.1.2"
|
||||
content-type "^1.0.5"
|
||||
debug "^4.4.0"
|
||||
debug "^4.4.3"
|
||||
http-errors "^2.0.0"
|
||||
iconv-lite "^0.6.3"
|
||||
iconv-lite "^0.7.0"
|
||||
on-finished "^2.4.1"
|
||||
qs "^6.14.0"
|
||||
raw-body "^3.0.0"
|
||||
type-is "^2.0.0"
|
||||
raw-body "^3.0.1"
|
||||
type-is "^2.0.1"
|
||||
|
||||
boolbase@^1.0.0:
|
||||
version "1.0.0"
|
||||
@@ -7259,7 +7259,7 @@ buffer@^6.0.3:
|
||||
base64-js "^1.3.1"
|
||||
ieee754 "^1.2.1"
|
||||
|
||||
bytes@3.1.2, bytes@^3.1.2:
|
||||
bytes@^3.1.2, bytes@~3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
|
||||
integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==
|
||||
@@ -7975,7 +7975,7 @@ delayed-stream@~1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
||||
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
|
||||
|
||||
depd@2.0.0, depd@^2.0.0:
|
||||
depd@2.0.0, depd@^2.0.0, depd@~2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
|
||||
integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
|
||||
@@ -9747,7 +9747,7 @@ htmlparser2@^10.0.0:
|
||||
domutils "^3.2.1"
|
||||
entities "^6.0.0"
|
||||
|
||||
http-errors@2.0.0, http-errors@^2.0.0:
|
||||
http-errors@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3"
|
||||
integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==
|
||||
@@ -9758,6 +9758,17 @@ http-errors@2.0.0, http-errors@^2.0.0:
|
||||
statuses "2.0.1"
|
||||
toidentifier "1.0.1"
|
||||
|
||||
http-errors@~2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.1.tgz#36d2f65bc909c8790018dd36fb4d93da6caae06b"
|
||||
integrity sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==
|
||||
dependencies:
|
||||
depd "~2.0.0"
|
||||
inherits "~2.0.4"
|
||||
setprototypeof "~1.2.0"
|
||||
statuses "~2.0.2"
|
||||
toidentifier "~1.0.1"
|
||||
|
||||
http-proxy-agent@^7.0.2:
|
||||
version "7.0.2"
|
||||
resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz#9a8b1f246866c028509486585f62b8f2c18c270e"
|
||||
@@ -9843,7 +9854,7 @@ iconv-lite@0.6.3, iconv-lite@^0.6.3:
|
||||
dependencies:
|
||||
safer-buffer ">= 2.1.2 < 3.0.0"
|
||||
|
||||
iconv-lite@0.7.0:
|
||||
iconv-lite@^0.7.0, iconv-lite@~0.7.0:
|
||||
version "0.7.0"
|
||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.7.0.tgz#c50cd80e6746ca8115eb98743afa81aa0e147a3e"
|
||||
integrity sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==
|
||||
@@ -9944,7 +9955,7 @@ inflight@^1.0.4:
|
||||
once "^1.3.0"
|
||||
wrappy "1"
|
||||
|
||||
inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3:
|
||||
inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3, inherits@~2.0.4:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
||||
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||
@@ -10732,17 +10743,17 @@ jest@30.2.0:
|
||||
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
|
||||
|
||||
js-yaml@^3.13.1:
|
||||
version "3.14.1"
|
||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537"
|
||||
integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==
|
||||
version "3.14.2"
|
||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.2.tgz#77485ce1dd7f33c061fd1b16ecea23b55fcb04b0"
|
||||
integrity sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==
|
||||
dependencies:
|
||||
argparse "^1.0.7"
|
||||
esprima "^4.0.0"
|
||||
|
||||
js-yaml@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
|
||||
integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b"
|
||||
integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==
|
||||
dependencies:
|
||||
argparse "^2.0.1"
|
||||
|
||||
@@ -12696,15 +12707,15 @@ range-parser@^1.2.1:
|
||||
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
|
||||
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
|
||||
|
||||
raw-body@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-3.0.1.tgz#ced5cd79a77bbb0496d707f2a0f9e1ae3aecdcb1"
|
||||
integrity sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==
|
||||
raw-body@^3.0.1:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-3.0.2.tgz#3e3ada5ae5568f9095d84376fd3a49b8fb000a51"
|
||||
integrity sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==
|
||||
dependencies:
|
||||
bytes "3.1.2"
|
||||
http-errors "2.0.0"
|
||||
iconv-lite "0.7.0"
|
||||
unpipe "1.0.0"
|
||||
bytes "~3.1.2"
|
||||
http-errors "~2.0.1"
|
||||
iconv-lite "~0.7.0"
|
||||
unpipe "~1.0.0"
|
||||
|
||||
react-arborist@3.4.3:
|
||||
version "3.4.3"
|
||||
@@ -13694,7 +13705,7 @@ setimmediate@^1.0.5:
|
||||
resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
|
||||
integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==
|
||||
|
||||
setprototypeof@1.2.0:
|
||||
setprototypeof@1.2.0, setprototypeof@~1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
|
||||
integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
|
||||
@@ -13953,7 +13964,7 @@ statuses@2.0.1:
|
||||
resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"
|
||||
integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==
|
||||
|
||||
statuses@^2.0.1:
|
||||
statuses@^2.0.1, statuses@~2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.2.tgz#8f75eecef765b5e1cfcdc080da59409ed424e382"
|
||||
integrity sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==
|
||||
@@ -14544,7 +14555,7 @@ to-through@^3.0.0:
|
||||
dependencies:
|
||||
streamx "^2.12.5"
|
||||
|
||||
toidentifier@1.0.1:
|
||||
toidentifier@1.0.1, toidentifier@~1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
|
||||
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
|
||||
@@ -14723,7 +14734,7 @@ type-fest@^4.41.0:
|
||||
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.41.0.tgz#6ae1c8e5731273c2bf1f58ad39cbae2c91a46c58"
|
||||
integrity sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==
|
||||
|
||||
type-is@^2.0.0, type-is@^2.0.1:
|
||||
type-is@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/type-is/-/type-is-2.0.1.tgz#64f6cf03f92fce4015c2b224793f6bdd4b068c97"
|
||||
integrity sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==
|
||||
@@ -14940,7 +14951,7 @@ universalify@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d"
|
||||
integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==
|
||||
|
||||
unpipe@1.0.0:
|
||||
unpipe@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
|
||||
integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==
|
||||
|
||||
@@ -398,9 +398,9 @@ glob-parent@~5.1.2:
|
||||
is-glob "^4.0.1"
|
||||
|
||||
glob@^10.3.10, glob@^10.3.3:
|
||||
version "10.4.5"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956"
|
||||
integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==
|
||||
version "10.5.0"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-10.5.0.tgz#8ec0355919cd3338c28428a23d4f24ecc5fe738c"
|
||||
integrity sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==
|
||||
dependencies:
|
||||
foreground-child "^3.1.0"
|
||||
jackspeak "^3.1.2"
|
||||
|
||||
Reference in New Issue
Block a user