Compare commits
17 Commits
feature/ov
...
feat/e2e-e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0144044c55 | ||
|
|
a6da37e231 | ||
|
|
9aeedd1d03 | ||
|
|
f7d4e6810b | ||
|
|
b740ffa52c | ||
|
|
f555e36e98 | ||
|
|
de11ab508f | ||
|
|
dc2fe4905b | ||
|
|
2864669dde | ||
|
|
7dae3a3c02 | ||
|
|
bdf62e2172 | ||
|
|
29104dfe2d | ||
|
|
785c9b21cf | ||
|
|
3fee1f2081 | ||
|
|
5f9968d81e | ||
|
|
f7baf238e3 | ||
|
|
bab42efd08 |
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:
|
||||
|
||||
20
CHANGELOG.md
@@ -6,10 +6,25 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- ✨ Add comments feature to the editor #1330
|
||||
- ✨(backend) Comments on text editor #1330
|
||||
|
||||
### Changed
|
||||
|
||||
- ⚡️(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
|
||||
|
||||
- ♿(frontend) improve accessibility:
|
||||
- ♿(frontend) improve share modal button accessibility #1626
|
||||
- ♿(frontend) improve screen reader support in DocShare modal #1628
|
||||
- 🐛(frontend) fix toolbar not activated when reader #1640
|
||||
- 🐛(frontend) preserve left panel width on window resize #1588
|
||||
- 🐛(frontend) prevent duplicate as first character in title #1595
|
||||
|
||||
## [3.10.0] - 2025-11-18
|
||||
|
||||
@@ -17,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
|
||||
|
||||
@@ -40,13 +56,11 @@ and this project adheres to
|
||||
### Security
|
||||
|
||||
- mitigate role escalation in the ask_for_access viewset #1580
|
||||
- 🐛(frontend) preserve left panel width on window resize #1588
|
||||
|
||||
### Removed
|
||||
|
||||
- 🔥(backend) remove api managing templates
|
||||
|
||||
|
||||
## [3.9.0] - 2025-11-10
|
||||
|
||||
### Added
|
||||
@@ -54,7 +68,6 @@ and this project adheres to
|
||||
- ✨(frontend) create skeleton component for DocEditor #1491
|
||||
- ✨(frontend) add an EmojiPicker in the document tree and title #1381
|
||||
- ✨(frontend) ajustable left panel #1456
|
||||
- ✨ Add comments feature to the editor #1330
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -183,7 +196,6 @@ and this project adheres to
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(backend) Comments on text editor #1309
|
||||
- 👷(CI) add bundle size check job #1268
|
||||
- ✨(frontend) use title first emoji as doc icon in tree #1289
|
||||
|
||||
|
||||
@@ -32,9 +32,9 @@ Then, set the `FRONTEND_CSS_URL` environment variable to the URL of your custom
|
||||
|
||||
----
|
||||
|
||||
# **Your logo** 📝
|
||||
# **Your Docs icon** 📝
|
||||
|
||||
You can add your own logo in the header from the theme customization file.
|
||||
You can add your own Docs icon in the header from the theme customization file.
|
||||
|
||||
### Settings 🔧
|
||||
|
||||
@@ -44,9 +44,9 @@ THEME_CUSTOMIZATION_FILE_PATH=<path>
|
||||
|
||||
### Example of JSON
|
||||
|
||||
You can activate it with the `header.logo` configuration: https://github.com/suitenumerique/docs/blob/main/src/helm/env.d/dev/configuration/theme/demo.json
|
||||
You can activate it with the `header.icon` configuration: https://github.com/suitenumerique/docs/blob/main/src/helm/env.d/dev/configuration/theme/demo.json
|
||||
|
||||
This configuration is optional. If not set, the default logo will be used.
|
||||
This configuration is optional. If not set, the default icon will be used.
|
||||
|
||||
----
|
||||
|
||||
|
||||
BIN
src/frontend/apps/e2e/__tests__/app-impress/assets/panojpg.jpeg
Normal file
|
After Width: | Height: | Size: 231 KiB |
BIN
src/frontend/apps/e2e/__tests__/app-impress/assets/panopng.png
Normal file
|
After Width: | Height: | Size: 839 KiB |
@@ -24,15 +24,13 @@ test.beforeEach(async ({ page }) => {
|
||||
});
|
||||
|
||||
test.describe('Doc Editor', () => {
|
||||
test('it checks default toolbar buttons are displayed', async ({
|
||||
test('it checks toolbar buttons are displayed', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
await createDoc(page, 'doc-toolbar', browserName, 1);
|
||||
|
||||
const editor = page.locator('.ProseMirror');
|
||||
await editor.click();
|
||||
await editor.fill('test content');
|
||||
const editor = await writeInEditor({ page, text: 'test content' });
|
||||
|
||||
await editor
|
||||
.getByText('test content', {
|
||||
@@ -41,6 +39,9 @@ test.describe('Doc Editor', () => {
|
||||
.selectText();
|
||||
|
||||
const toolbar = page.locator('.bn-formatting-toolbar');
|
||||
await expect(
|
||||
toolbar.locator('button[data-test="comment-toolbar-button"]'),
|
||||
).toBeVisible();
|
||||
await expect(toolbar.locator('button[data-test="bold"]')).toBeVisible();
|
||||
await expect(toolbar.locator('button[data-test="italic"]')).toBeVisible();
|
||||
await expect(
|
||||
@@ -63,6 +64,53 @@ test.describe('Doc Editor', () => {
|
||||
await expect(
|
||||
toolbar.locator('button[data-test="createLink"]'),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
toolbar.locator('button[data-test="ai-actions"]'),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
toolbar.locator('button[data-test="convertMarkdown"]'),
|
||||
).toBeVisible();
|
||||
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
await page.locator('.bn-block-outer').last().click();
|
||||
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
const fileChooserPromise = page.waitForEvent('filechooser');
|
||||
await page.locator('.bn-block-outer').last().fill('/');
|
||||
await page.getByText('Resizable image with caption').click();
|
||||
await page.getByText('Upload image').click();
|
||||
|
||||
const fileChooser = await fileChooserPromise;
|
||||
await fileChooser.setFiles(
|
||||
path.join(__dirname, 'assets/logo-suite-numerique.png'),
|
||||
);
|
||||
|
||||
const image = page
|
||||
.locator('.--docs--editor-container img.bn-visual-media')
|
||||
.first();
|
||||
|
||||
await expect(image).toHaveAttribute('role', 'presentation');
|
||||
|
||||
await image.dblclick();
|
||||
|
||||
await expect(
|
||||
toolbar.locator('button[data-test="comment-toolbar-button"]'),
|
||||
).toBeHidden();
|
||||
await expect(
|
||||
toolbar.locator('button[data-test="ai-actions"]'),
|
||||
).toBeHidden();
|
||||
await expect(
|
||||
toolbar.locator('button[data-test="convertMarkdown"]'),
|
||||
).toBeHidden();
|
||||
|
||||
await expect(
|
||||
toolbar.locator('button[data-test="editcaption"]'),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
toolbar.locator('button[data-test="downloadfile"]'),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -241,20 +289,66 @@ test.describe('Doc Editor', () => {
|
||||
await expect(editor.getByText('Hello World Doc persisted 2')).toBeVisible();
|
||||
});
|
||||
|
||||
test('it cannot edit if viewer', async ({ page }) => {
|
||||
await mockedDocument(page, {
|
||||
user_role: 'reader',
|
||||
test('it cannot edit if viewer but see and can get resources', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
const [docTitle] = await createDoc(page, 'doc-viewer', browserName, 1);
|
||||
await verifyDocName(page, docTitle);
|
||||
|
||||
await writeInEditor({ page, text: 'Hello World' });
|
||||
|
||||
await page.getByRole('button', { name: 'Share' }).click();
|
||||
await updateShareLink(page, 'Public', 'Reading');
|
||||
|
||||
// Close the modal
|
||||
await page.getByRole('button', { name: 'close' }).first().click();
|
||||
|
||||
const { otherPage, cleanup } = await connectOtherUserToDoc({
|
||||
browserName,
|
||||
docUrl: page.url(),
|
||||
withoutSignIn: true,
|
||||
docTitle,
|
||||
});
|
||||
|
||||
await goToGridDoc(page);
|
||||
await expect(
|
||||
otherPage.getByLabel('It is the card information').getByText('Reader'),
|
||||
).toBeVisible();
|
||||
|
||||
const card = page.getByLabel('It is the card information');
|
||||
await expect(card).toBeVisible();
|
||||
|
||||
await expect(card.getByText('Reader')).toBeVisible();
|
||||
|
||||
const editor = page.locator('.ProseMirror');
|
||||
// Cannot edit
|
||||
const editor = otherPage.locator('.ProseMirror');
|
||||
await expect(editor).toHaveAttribute('contenteditable', 'false');
|
||||
|
||||
// Owner add a image
|
||||
const fileChooserPromise = page.waitForEvent('filechooser');
|
||||
await page.locator('.bn-block-outer').last().fill('/');
|
||||
await page.getByText('Resizable image with caption').click();
|
||||
await page.getByText('Upload image').click();
|
||||
|
||||
const fileChooser = await fileChooserPromise;
|
||||
await fileChooser.setFiles(
|
||||
path.join(__dirname, 'assets/logo-suite-numerique.png'),
|
||||
);
|
||||
|
||||
// Owner see the image
|
||||
await expect(
|
||||
page.locator('.--docs--editor-container img.bn-visual-media').first(),
|
||||
).toBeVisible();
|
||||
|
||||
// Viewser see the image
|
||||
const viewerImg = otherPage
|
||||
.locator('.--docs--editor-container img.bn-visual-media')
|
||||
.first();
|
||||
await expect(viewerImg).toBeVisible();
|
||||
|
||||
// Viewer can download the image
|
||||
await viewerImg.click();
|
||||
const downloadPromise = otherPage.waitForEvent('download');
|
||||
await otherPage.getByRole('button', { name: 'Download image' }).click();
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toBe('logo-suite-numerique.png');
|
||||
|
||||
await cleanup();
|
||||
});
|
||||
|
||||
test('it adds an image to the doc editor', async ({ page, browserName }) => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -208,7 +208,7 @@ test.describe('Doc Header', () => {
|
||||
await expect(
|
||||
invitationCard.getByText('test.test@invitation.test').first(),
|
||||
).toBeVisible();
|
||||
const invitationRole = invitationCard.getByLabel('doc-role-dropdown');
|
||||
const invitationRole = invitationCard.getByTestId('doc-role-dropdown');
|
||||
await expect(invitationRole).toBeVisible();
|
||||
|
||||
await invitationRole.click();
|
||||
@@ -217,7 +217,7 @@ test.describe('Doc Header', () => {
|
||||
await expect(invitationCard).toBeHidden();
|
||||
|
||||
const memberCard = shareModal.getByLabel('List members card');
|
||||
const roles = memberCard.getByLabel('doc-role-dropdown');
|
||||
const roles = memberCard.getByTestId('doc-role-dropdown');
|
||||
await expect(memberCard).toBeVisible();
|
||||
await expect(
|
||||
memberCard.getByText('test.test@accesses.test').first(),
|
||||
|
||||
@@ -74,7 +74,7 @@ test.describe('Document create member', () => {
|
||||
await expect(list.getByText(email)).toBeVisible();
|
||||
|
||||
// Check roles are displayed
|
||||
await list.getByLabel('doc-role-dropdown').click();
|
||||
await list.getByTestId('doc-role-dropdown').click();
|
||||
await expect(page.getByRole('menuitem', { name: 'Reader' })).toBeVisible();
|
||||
await expect(page.getByRole('menuitem', { name: 'Editor' })).toBeVisible();
|
||||
await expect(page.getByRole('menuitem', { name: 'Owner' })).toBeVisible();
|
||||
@@ -84,7 +84,7 @@ test.describe('Document create member', () => {
|
||||
|
||||
// Validate
|
||||
await page.getByRole('menuitem', { name: 'Administrator' }).click();
|
||||
await page.getByRole('button', { name: /^Invite / }).click();
|
||||
await page.getByTestId('doc-share-invite-button').click();
|
||||
|
||||
// Check invitation added
|
||||
await expect(
|
||||
@@ -128,14 +128,14 @@ test.describe('Document create member', () => {
|
||||
|
||||
// Choose a role
|
||||
const container = page.getByTestId('doc-share-add-member-list');
|
||||
await container.getByLabel('doc-role-dropdown').click();
|
||||
await container.getByTestId('doc-role-dropdown').click();
|
||||
await page.getByRole('menuitem', { name: 'Owner' }).click();
|
||||
|
||||
const responsePromiseCreateInvitation = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('/invitations/') && response.status() === 201,
|
||||
);
|
||||
await page.getByRole('button', { name: /^Invite / }).click();
|
||||
await page.getByTestId('doc-share-invite-button').click();
|
||||
|
||||
// Check invitation sent
|
||||
|
||||
@@ -146,7 +146,7 @@ test.describe('Document create member', () => {
|
||||
await page.getByTestId(`search-user-row-${email}`).click();
|
||||
|
||||
// Choose a role
|
||||
await container.getByLabel('doc-role-dropdown').click();
|
||||
await container.getByTestId('doc-role-dropdown').click();
|
||||
await page.getByRole('menuitem', { name: 'Owner' }).click();
|
||||
|
||||
const responsePromiseCreateInvitationFail = page.waitForResponse(
|
||||
@@ -154,7 +154,7 @@ test.describe('Document create member', () => {
|
||||
response.url().includes('/invitations/') && response.status() === 400,
|
||||
);
|
||||
|
||||
await page.getByRole('button', { name: /^Invite / }).click();
|
||||
await page.getByTestId('doc-share-invite-button').click();
|
||||
await expect(
|
||||
page.getByText(`"${email}" is already invited to the document.`),
|
||||
).toBeVisible();
|
||||
@@ -183,7 +183,7 @@ test.describe('Document create member', () => {
|
||||
|
||||
// Choose a role
|
||||
const container = page.getByTestId('doc-share-add-member-list');
|
||||
await container.getByLabel('doc-role-dropdown').click();
|
||||
await container.getByTestId('doc-role-dropdown').click();
|
||||
await page.getByRole('menuitem', { name: 'Administrator' }).click();
|
||||
|
||||
const responsePromiseCreateInvitation = page.waitForResponse(
|
||||
@@ -191,7 +191,7 @@ test.describe('Document create member', () => {
|
||||
response.url().includes('/invitations/') && response.status() === 201,
|
||||
);
|
||||
|
||||
await page.getByRole('button', { name: /^Invite / }).click();
|
||||
await page.getByTestId('doc-share-invite-button').click();
|
||||
|
||||
// Check invitation sent
|
||||
const responseCreateInvitation = await responsePromiseCreateInvitation;
|
||||
@@ -210,18 +210,14 @@ test.describe('Document create member', () => {
|
||||
response.request().method() === 'PATCH',
|
||||
);
|
||||
|
||||
await userInvitation.getByLabel('doc-role-dropdown').click();
|
||||
await userInvitation.getByTestId('doc-role-dropdown').click();
|
||||
await page.getByRole('menuitem', { name: 'Reader' }).click();
|
||||
|
||||
const responsePatchInvitation = await responsePromisePatchInvitation;
|
||||
expect(responsePatchInvitation.ok()).toBeTruthy();
|
||||
|
||||
const moreActions = userInvitation.getByRole('button', {
|
||||
name: 'Open invitation actions menu',
|
||||
});
|
||||
await moreActions.click();
|
||||
|
||||
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||
await userInvitation.getByTestId('doc-role-dropdown').click();
|
||||
await page.getByRole('menuitem', { name: 'Remove access' }).click();
|
||||
|
||||
await expect(userInvitation).toBeHidden();
|
||||
});
|
||||
@@ -272,7 +268,7 @@ test.describe('Document create member', () => {
|
||||
const container = page.getByTestId(
|
||||
`doc-share-access-request-row-${emailRequest}`,
|
||||
);
|
||||
await container.getByLabel('doc-role-dropdown').click();
|
||||
await container.getByTestId('doc-role-dropdown').click();
|
||||
await page.getByRole('menuitem', { name: 'Administrator' }).click();
|
||||
await container.getByRole('button', { name: 'Approve' }).click();
|
||||
|
||||
|
||||
@@ -152,7 +152,7 @@ test.describe('Document list members', () => {
|
||||
const currentUser = list.getByTestId(
|
||||
`doc-share-member-row-user.test@${browserName}.test`,
|
||||
);
|
||||
const currentUserRole = currentUser.getByLabel('doc-role-dropdown');
|
||||
const currentUserRole = currentUser.getByTestId('doc-role-dropdown');
|
||||
await expect(currentUser).toBeVisible();
|
||||
await expect(currentUserRole).toBeVisible();
|
||||
await currentUserRole.click();
|
||||
@@ -169,7 +169,7 @@ test.describe('Document list members', () => {
|
||||
});
|
||||
const newUserEmail = await addNewMember(page, 0, 'Owner');
|
||||
const newUser = list.getByTestId(`doc-share-member-row-${newUserEmail}`);
|
||||
const newUserRoles = newUser.getByLabel('doc-role-dropdown');
|
||||
const newUserRoles = newUser.getByTestId('doc-role-dropdown');
|
||||
|
||||
await expect(newUser).toBeVisible();
|
||||
|
||||
@@ -214,9 +214,7 @@ test.describe('Document list members', () => {
|
||||
|
||||
const emailMyself = `user.test@${browserName}.test`;
|
||||
const mySelf = list.getByTestId(`doc-share-member-row-${emailMyself}`);
|
||||
const mySelfRole = mySelf.getByRole('button', {
|
||||
name: 'doc-role-dropdown',
|
||||
});
|
||||
const mySelfRole = mySelf.getByTestId('doc-role-dropdown');
|
||||
|
||||
const userOwnerEmail = await addNewMember(page, 0, 'Owner');
|
||||
const userOwner = list.getByTestId(
|
||||
@@ -231,9 +229,7 @@ test.describe('Document list members', () => {
|
||||
const userReader = list.getByTestId(
|
||||
`doc-share-member-row-${userReaderEmail}`,
|
||||
);
|
||||
const userReaderRole = userReader.getByRole('button', {
|
||||
name: 'doc-role-dropdown',
|
||||
});
|
||||
const userReaderRole = userReader.getByTestId('doc-role-dropdown');
|
||||
|
||||
await expect(mySelf).toBeVisible();
|
||||
await expect(userOwner).toBeVisible();
|
||||
|
||||
@@ -226,7 +226,7 @@ test.describe('Doc Tree', () => {
|
||||
const currentUser = list.getByTestId(
|
||||
`doc-share-member-row-user.test@${browserName}.test`,
|
||||
);
|
||||
const currentUserRole = currentUser.getByLabel('doc-role-dropdown');
|
||||
const currentUserRole = currentUser.getByTestId('doc-role-dropdown');
|
||||
await currentUserRole.click();
|
||||
await page.getByRole('menuitem', { name: 'Administrator' }).click();
|
||||
await list.click();
|
||||
|
||||
@@ -156,9 +156,10 @@ test.describe('Header: Override configuration', () => {
|
||||
FRONTEND_THEME: 'dsfr',
|
||||
theme_customization: {
|
||||
header: {
|
||||
logo: {
|
||||
icon: {
|
||||
src: '/assets/logo-gouv.svg',
|
||||
width: '220px',
|
||||
height: 'auto',
|
||||
alt: '',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { createDoc } from './utils-common';
|
||||
import { createDoc, goToGridDoc, verifyDocName } from './utils-common';
|
||||
|
||||
test.describe('Left panel desktop', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
@@ -10,8 +10,12 @@ test.describe('Left panel desktop', () => {
|
||||
test('checks all the elements are visible', async ({ page }) => {
|
||||
await expect(page.getByTestId('left-panel-desktop')).toBeVisible();
|
||||
await expect(page.getByTestId('left-panel-mobile')).toBeHidden();
|
||||
await expect(page.getByTestId('home-button')).toBeVisible();
|
||||
await expect(page.getByTestId('home-button')).toBeHidden();
|
||||
await expect(page.getByTestId('new-doc-button')).toBeVisible();
|
||||
|
||||
await goToGridDoc(page);
|
||||
|
||||
await expect(page.getByTestId('home-button')).toBeVisible();
|
||||
});
|
||||
|
||||
test('checks resize handle is present and functional on document page', async ({
|
||||
@@ -88,6 +92,9 @@ test.describe('Left panel mobile', () => {
|
||||
await expect(languageButton).not.toBeInViewport();
|
||||
await expect(logoutButton).not.toBeInViewport();
|
||||
|
||||
const title = await goToGridDoc(page);
|
||||
await verifyDocName(page, title);
|
||||
|
||||
await header.getByLabel('Open the header menu').click();
|
||||
|
||||
await expect(page.getByTestId('left-panel-mobile')).toBeInViewport();
|
||||
|
||||
@@ -38,9 +38,9 @@ export const addNewMember = async (
|
||||
await page.getByRole('option', { name: users[index].email }).click();
|
||||
|
||||
// Choose a role
|
||||
await page.getByLabel('doc-role-dropdown').click();
|
||||
await page.getByTestId('doc-role-dropdown').click();
|
||||
await page.getByRole('menuitem', { name: role }).click();
|
||||
await page.getByRole('button', { name: /^Invite / }).click();
|
||||
await page.getByTestId('doc-share-invite-button').click();
|
||||
|
||||
return users[index].email;
|
||||
};
|
||||
@@ -74,7 +74,7 @@ export const updateRoleUser = async (
|
||||
const list = page.getByTestId('doc-share-quick-search');
|
||||
|
||||
const currentUser = list.getByTestId(`doc-share-member-row-${email}`);
|
||||
const currentUserRole = currentUser.getByLabel('doc-role-dropdown');
|
||||
const currentUserRole = currentUser.getByTestId('doc-role-dropdown');
|
||||
await currentUserRole.click();
|
||||
await page.getByRole('menuitem', { name: role }).click();
|
||||
await list.click();
|
||||
|
||||
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,57 +1,25 @@
|
||||
import { cunninghamConfig as tokens } from '@gouvfr-lasuite/ui-kit';
|
||||
import { defaultTokens } from '@openfun/cunningham-react';
|
||||
import merge from 'lodash/merge';
|
||||
|
||||
const customColors = {
|
||||
'primary-action': '#1212FF',
|
||||
'primary-bg': '#FAFAFA',
|
||||
'primary-focus': '#0A76F6',
|
||||
'secondary-icon': 'var(--c--theme--colors--primary-text)',
|
||||
'blue-400': '#7AB1E8',
|
||||
'blue-500': '#417DC4',
|
||||
'blue-600': '#3558A2',
|
||||
'brown-400': '#E6BE92',
|
||||
'brown-500': '#BD987A',
|
||||
'brown-600': '#745B47',
|
||||
'cyan-400': '#34BAB5',
|
||||
'cyan-500': '#009099',
|
||||
'cyan-600': '#006A6F',
|
||||
'gold-400': '#FFCA00',
|
||||
'gold-500': '#C3992A',
|
||||
'gold-600': '#695240',
|
||||
'green-400': '#34CB6A',
|
||||
'green-500': '#00A95F',
|
||||
'green-600': '#297254',
|
||||
'olive-400': '#99C221',
|
||||
'olive-500': '#68A532',
|
||||
'olive-600': '#447049',
|
||||
'orange-400': '#FF732C',
|
||||
'orange-500': '#E4794A',
|
||||
'orange-600': '#755348',
|
||||
'pink-400': '#FFB7AE',
|
||||
'pink-500': '#E18B76',
|
||||
'pink-600': '#8D533E',
|
||||
'purple-400': '#CE70CC',
|
||||
'purple-500': '#A558A0',
|
||||
'purple-600': '#6E445A',
|
||||
'yellow-400': '#D8C634',
|
||||
'yellow-500': '#B7A73F',
|
||||
'yellow-600': '#66673D',
|
||||
// Uikit does not provide the full list of tokens.
|
||||
// To be able to override correctly, we need to merge with the default tokens.
|
||||
let mergedColors = merge(
|
||||
defaultTokens.globals.colors,
|
||||
tokens.themes.default.globals.colors,
|
||||
);
|
||||
|
||||
mergedColors = {
|
||||
...mergedColors,
|
||||
'logo-1': '#2845C1',
|
||||
};
|
||||
|
||||
tokens.themes.default.theme = {
|
||||
...tokens.themes.default.theme,
|
||||
tokens.themes.default.globals = {
|
||||
...tokens.themes.default.globals,
|
||||
...{
|
||||
logo: {
|
||||
src: '',
|
||||
alt: '',
|
||||
widthHeader: '',
|
||||
widthFooter: '',
|
||||
},
|
||||
colors: {
|
||||
...tokens.themes.default.theme.colors,
|
||||
...customColors,
|
||||
},
|
||||
colors: mergedColors,
|
||||
font: {
|
||||
...tokens.themes.default.theme.font,
|
||||
...tokens.themes.default.globals.font,
|
||||
families: {
|
||||
base: 'sans-serif',
|
||||
accent: 'sans-serif',
|
||||
@@ -63,39 +31,29 @@ tokens.themes.default.theme = {
|
||||
tokens.themes.default.components = {
|
||||
...tokens.themes.default.components,
|
||||
...{
|
||||
logo: {
|
||||
src: '',
|
||||
alt: '',
|
||||
widthHeader: '',
|
||||
widthFooter: '',
|
||||
},
|
||||
'la-gaufre': false,
|
||||
'home-proconnect': false,
|
||||
'image-system-filter': '',
|
||||
icon: {
|
||||
src: '/assets/icon-docs.svg',
|
||||
width: '32px',
|
||||
height: 'auto',
|
||||
},
|
||||
favicon: {
|
||||
ico: '/assets/favicon-light.ico',
|
||||
'png-light': '/assets/favicon-light.png',
|
||||
'png-dark': '/assets/favicon-dark.png',
|
||||
},
|
||||
button: {
|
||||
...tokens.themes.default.components.button,
|
||||
primary: {
|
||||
...tokens.themes.default.components.button.primary,
|
||||
...{
|
||||
'background--disabled': 'var(--c--theme--colors--greyscale-100)',
|
||||
},
|
||||
disabled: 'var(--c--theme--colors--greyscale-400)',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const dsfrTheme = {
|
||||
dsfr: {
|
||||
theme: {
|
||||
colors: {
|
||||
'secondary-icon': '#C9191E',
|
||||
},
|
||||
logo: {
|
||||
src: '/assets/logo-gouv.svg',
|
||||
widthHeader: '110px',
|
||||
widthFooter: '220px',
|
||||
alt: 'Gouvernement Logo',
|
||||
},
|
||||
globals: {
|
||||
font: {
|
||||
families: {
|
||||
base: 'Marianne, Inter, Roboto Flex Variable, sans-serif',
|
||||
@@ -104,8 +62,19 @@ const dsfrTheme = {
|
||||
},
|
||||
},
|
||||
components: {
|
||||
logo: {
|
||||
src: '/assets/logo-gouv.svg',
|
||||
widthHeader: '110px',
|
||||
widthFooter: '220px',
|
||||
alt: 'Gouvernement Logo',
|
||||
},
|
||||
'la-gaufre': true,
|
||||
'home-proconnect': true,
|
||||
icon: {
|
||||
src: '/assets/icon-docs-dsfr.svg',
|
||||
width: '32px',
|
||||
height: 'auto',
|
||||
},
|
||||
favicon: {
|
||||
ico: '/assets/favicon-dsfr.ico',
|
||||
'png-light': '/assets/favicon-dsfr.png',
|
||||
@@ -117,131 +86,296 @@ const dsfrTheme = {
|
||||
|
||||
const genericTheme = {
|
||||
generic: {
|
||||
theme: {
|
||||
globals: {
|
||||
colors: {
|
||||
'primary-action': '#206EBD',
|
||||
'primary-focus': '#1E64BF',
|
||||
'primary-text': '#2E2C28',
|
||||
'primary-050': '#F8F8F7',
|
||||
'primary-100': '#F0EFEC',
|
||||
'primary-150': '#F4F4FD',
|
||||
'primary-200': '#E8E7E4',
|
||||
'primary-300': '#CFCDC9',
|
||||
'primary-400': '#979592',
|
||||
'primary-500': '#82807D',
|
||||
'primary-600': '#3F3D39',
|
||||
'primary-700': '#2E2C28',
|
||||
'primary-800': '#302E29',
|
||||
'primary-900': '#282622',
|
||||
'primary-950': '#201F1C',
|
||||
'secondary-text': '#fff',
|
||||
'secondary-50': '#F4F7FA',
|
||||
'secondary-100': '#D7E3EE',
|
||||
'secondary-200': '#B8CCE1',
|
||||
'secondary-300': '#99B4D3',
|
||||
'secondary-400': '#7595BE',
|
||||
'secondary-500': '#5874A0',
|
||||
'secondary-600': '#3A5383',
|
||||
'secondary-700': '#1E3462',
|
||||
'secondary-800': '#091B41',
|
||||
'secondary-900': '#08183B',
|
||||
'secondary-950': '#071636',
|
||||
'greyscale-text': '#3C3B38',
|
||||
'greyscale-000': '#fff',
|
||||
'greyscale-050': '#F8F7F7',
|
||||
'greyscale-100': '#F3F3F2',
|
||||
'greyscale-200': '#ECEBEA',
|
||||
'greyscale-250': '#E4E3E2',
|
||||
'greyscale-300': '#D3D2CF',
|
||||
'greyscale-350': '#eee',
|
||||
'greyscale-400': '#96948E',
|
||||
'greyscale-500': '#817E77',
|
||||
'greyscale-600': '#6A6862',
|
||||
'greyscale-700': '#3C3B38',
|
||||
'greyscale-750': '#383632',
|
||||
'greyscale-800': '#2D2B27',
|
||||
'greyscale-900': '#262522',
|
||||
'greyscale-950': '#201F1C',
|
||||
'greyscale-1000': '#181714',
|
||||
'success-text': '#234935',
|
||||
'success-50': '#F3FBF5',
|
||||
'success-100': '#E4F7EA',
|
||||
'success-200': '#CAEED4',
|
||||
'success-300': '#A0E0B5',
|
||||
'success-400': '#6CC88C',
|
||||
'success-500': '#6CC88C',
|
||||
'success-600': '#358D5C',
|
||||
'success-700': '#2D704B',
|
||||
'success-800': '#28583F',
|
||||
'success-900': '#234935',
|
||||
'success-950': '#0F281B',
|
||||
'info-text': '#212445',
|
||||
'info-50': '#F2F6FB',
|
||||
'info-100': '#E2E9F5',
|
||||
'info-200': '#CCD8EE',
|
||||
'info-300': '#A9C0E3',
|
||||
'info-400': '#809DD4',
|
||||
'info-500': '#617BC7',
|
||||
'info-600': '#4A5CBF',
|
||||
'info-700': '#3E49B2',
|
||||
'info-800': '#353C8F',
|
||||
'info-900': '#303771',
|
||||
'info-950': '#212445',
|
||||
'warning-text': '#D97C3A',
|
||||
'warning-50': '#FDF7F1',
|
||||
'warning-100': '#FBEDDC',
|
||||
'warning-200': '#F5D9B9',
|
||||
'warning-300': '#EDBE8C',
|
||||
'warning-400': '#E2985C',
|
||||
'warning-500': '#D97C3A',
|
||||
'warning-600': '#C96330',
|
||||
'warning-700': '#A34B32',
|
||||
'warning-800': '#813B2C',
|
||||
'warning-900': '#693327',
|
||||
'warning-950': '#381713',
|
||||
'danger-action': '#C0182A',
|
||||
'danger-text': '#FFF',
|
||||
'danger-050': '#FDF5F4',
|
||||
'danger-100': '#FBEBE8',
|
||||
'danger-200': '#F9E0DC',
|
||||
'danger-300': '#F3C3BD',
|
||||
'danger-400': '#E26552',
|
||||
'danger-500': '#C91F00',
|
||||
'danger-600': '#A71901',
|
||||
'danger-700': '#562C2B',
|
||||
'danger-800': '#392425',
|
||||
'danger-900': '#311F20',
|
||||
'danger-950': '#2A191A',
|
||||
'blue-400': '#8BAECC',
|
||||
'blue-500': '#567AA2',
|
||||
'blue-600': '#455784',
|
||||
'brown-400': '#E4C090',
|
||||
'brown-500': '#BA9977',
|
||||
'brown-600': '#735C45',
|
||||
'cyan-400': '#5CBEC9',
|
||||
'cyan-500': '#43A1B3',
|
||||
'cyan-600': '#39809B',
|
||||
'gold-400': '#ECBF50',
|
||||
'gold-500': '#DFA038',
|
||||
'gold-600': '#C17B31',
|
||||
'green-400': '#5DBD9A',
|
||||
'green-500': '#3AA183',
|
||||
'green-600': '#2A816D',
|
||||
'olive-400': '#AFD662',
|
||||
'olive-500': '#90BB4B',
|
||||
'olive-600': '#6E9441',
|
||||
'orange-400': '#E2985C',
|
||||
'orange-500': '#D97C3A',
|
||||
'orange-600': '#C96330',
|
||||
'pink-400': '#BE8FC8',
|
||||
'pink-500': '#A563B1',
|
||||
'pink-600': '#8B44A5',
|
||||
'purple-400': '#BE8FC8',
|
||||
'purple-500': '#A563B1',
|
||||
'purple-600': '#8B44A5',
|
||||
'yellow-400': '#EDC947',
|
||||
'yellow-500': '#DBB13A',
|
||||
'yellow-600': '#B88A34',
|
||||
'brand-050': '#EEF1FA',
|
||||
'brand-100': '#DDE2F5',
|
||||
'brand-150': '#CED3F1',
|
||||
'brand-200': '#BEC5F0',
|
||||
'brand-250': '#AFB5F1',
|
||||
'brand-300': '#A0A5F6',
|
||||
'brand-350': '#8F94FD',
|
||||
'brand-400': '#8184FC',
|
||||
'brand-450': '#7576EE',
|
||||
'brand-500': '#6969DF',
|
||||
'brand-550': '#5E5CD0',
|
||||
'brand-600': '#534FC2',
|
||||
'brand-650': '#4844AD',
|
||||
'brand-700': '#3E3B98',
|
||||
'brand-750': '#36347D',
|
||||
'brand-800': '#2D2F5F',
|
||||
'brand-850': '#262848',
|
||||
'brand-900': '#1C1E32',
|
||||
'brand-950': '#11131F',
|
||||
'gray-000': '#FFFFFF',
|
||||
'gray-025': '#F8F8F9',
|
||||
'gray-050': '#F0F0F3',
|
||||
'gray-100': '#E2E2EA',
|
||||
'gray-150': '#D3D4E0',
|
||||
'gray-200': '#C5C6D5',
|
||||
'gray-250': '#B7B7CB',
|
||||
'gray-300': '#A9A9BF',
|
||||
'gray-350': '#9C9CB2',
|
||||
'gray-400': '#8F8FA4',
|
||||
'gray-450': '#828297',
|
||||
'gray-500': '#75758A',
|
||||
'gray-550': '#69697D',
|
||||
'gray-600': '#5D5D70',
|
||||
'gray-650': '#515164',
|
||||
'gray-700': '#454558',
|
||||
'gray-750': '#3A3A4C',
|
||||
'gray-800': '#2F303D',
|
||||
'gray-850': '#25252F',
|
||||
'gray-900': '#1B1B23',
|
||||
'gray-950': '#111114',
|
||||
'gray-1000': '#000000',
|
||||
'info-050': '#EAF2F9',
|
||||
'info-100': '#D5E4F3',
|
||||
'info-150': '#BFD7F0',
|
||||
'info-200': '#A7CAEE',
|
||||
'info-250': '#8DBDEF',
|
||||
'info-300': '#6EB0F2',
|
||||
'info-350': '#50A2F5',
|
||||
'info-400': '#3593F4',
|
||||
'info-450': '#1185ED',
|
||||
'info-500': '#0077DE',
|
||||
'info-550': '#0069CF',
|
||||
'info-600': '#005BC0',
|
||||
'info-650': '#0D4EAA',
|
||||
'info-700': '#124394',
|
||||
'info-750': '#163878',
|
||||
'info-800': '#192F5A',
|
||||
'info-850': '#192541',
|
||||
'info-900': '#141B2D',
|
||||
'info-950': '#0C111C',
|
||||
'success-050': '#E8F1EA',
|
||||
'success-100': '#CFE4D4',
|
||||
'success-150': '#BAD9C1',
|
||||
'success-200': '#A2CFAD',
|
||||
'success-250': '#86C597',
|
||||
'success-300': '#6CBA83',
|
||||
'success-350': '#4FB070',
|
||||
'success-400': '#40A363',
|
||||
'success-450': '#309556',
|
||||
'success-500': '#1E884A',
|
||||
'success-550': '#027B3E',
|
||||
'success-600': '#016D31',
|
||||
'success-650': '#006024',
|
||||
'success-700': '#005317',
|
||||
'success-750': '#0D4511',
|
||||
'success-800': '#11380E',
|
||||
'success-850': '#132A11',
|
||||
'success-900': '#101E0F',
|
||||
'success-950': '#091209',
|
||||
'warning-050': '#F8F0E9',
|
||||
'warning-100': '#F1E0D3',
|
||||
'warning-150': '#ECD0BC',
|
||||
'warning-200': '#E8C0A4',
|
||||
'warning-250': '#E8AE8A',
|
||||
'warning-300': '#EB9970',
|
||||
'warning-350': '#E98456',
|
||||
'warning-400': '#E57036',
|
||||
'warning-450': '#DA5E18',
|
||||
'warning-500': '#CB5000',
|
||||
'warning-550': '#BC4200',
|
||||
'warning-600': '#AD3300',
|
||||
'warning-650': '#9E2300',
|
||||
'warning-700': '#882011',
|
||||
'warning-750': '#731E16',
|
||||
'warning-800': '#58201A',
|
||||
'warning-850': '#401D18',
|
||||
'warning-900': '#2E1714',
|
||||
'warning-950': '#1D0F0D',
|
||||
'error-050': '#F9EFEC',
|
||||
'error-100': '#F4DFD9',
|
||||
'error-150': '#F0CEC6',
|
||||
'error-200': '#EEBCB2',
|
||||
'error-250': '#EEA99D',
|
||||
'error-300': '#EF9486',
|
||||
'error-350': '#F37C6E',
|
||||
'error-400': '#F65F53',
|
||||
'error-450': '#F0463D',
|
||||
'error-500': '#E82322',
|
||||
'error-550': '#D7010E',
|
||||
'error-600': '#C00100',
|
||||
'error-650': '#AA0000',
|
||||
'error-700': '#910C06',
|
||||
'error-750': '#731E16',
|
||||
'error-800': '#58201A',
|
||||
'error-850': '#401D18',
|
||||
'error-900': '#2E1714',
|
||||
'error-950': '#1D0F0D',
|
||||
'red-050': '#FAEFEE',
|
||||
'red-100': '#F4DEDD',
|
||||
'red-150': '#F1CDCB',
|
||||
'red-200': '#EFBBBA',
|
||||
'red-250': '#EEA8A8',
|
||||
'red-300': '#F09394',
|
||||
'red-350': '#F37B7E',
|
||||
'red-400': '#EF6569',
|
||||
'red-450': '#E94A55',
|
||||
'red-500': '#DA3B49',
|
||||
'red-550': '#CA2A3C',
|
||||
'red-600': '#BB1330',
|
||||
'red-650': '#A90021',
|
||||
'red-700': '#910A13',
|
||||
'red-750': '#731E16',
|
||||
'red-800': '#58201A',
|
||||
'red-850': '#411D18',
|
||||
'red-900': '#2E1714',
|
||||
'red-950': '#1D0F0D',
|
||||
'orange-050': '#F8F0E9',
|
||||
'orange-100': '#F1E0D3',
|
||||
'orange-150': '#ECD0BD',
|
||||
'orange-200': '#EABFA6',
|
||||
'orange-250': '#EBAC90',
|
||||
'orange-300': '#EC9772',
|
||||
'orange-350': '#E5845A',
|
||||
'orange-400': '#D6774D',
|
||||
'orange-450': '#C86A40',
|
||||
'orange-500': '#B95D33',
|
||||
'orange-550': '#AB5025',
|
||||
'orange-600': '#9D4315',
|
||||
'orange-650': '#8F3600',
|
||||
'orange-700': '#812900',
|
||||
'orange-750': '#6C2511',
|
||||
'orange-800': '#572017',
|
||||
'orange-850': '#401D18',
|
||||
'orange-900': '#2E1714',
|
||||
'orange-950': '#1D0F0D',
|
||||
'brown-050': '#F6F0E8',
|
||||
'brown-100': '#F1E0D3',
|
||||
'brown-150': '#EBD0BA',
|
||||
'brown-200': '#E2C0A6',
|
||||
'brown-250': '#D4B398',
|
||||
'brown-300': '#C6A58B',
|
||||
'brown-350': '#B8987E',
|
||||
'brown-400': '#AA8B71',
|
||||
'brown-450': '#9D7E65',
|
||||
'brown-500': '#8F7158',
|
||||
'brown-550': '#82654C',
|
||||
'brown-600': '#765841',
|
||||
'brown-650': '#694C35',
|
||||
'brown-700': '#5D412A',
|
||||
'brown-750': '#51361E',
|
||||
'brown-800': '#452A13',
|
||||
'brown-850': '#392008',
|
||||
'brown-900': '#29180A',
|
||||
'brown-950': '#1B0F08',
|
||||
'yellow-050': '#F3F0E7',
|
||||
'yellow-100': '#E9E2CF',
|
||||
'yellow-150': '#E1D4B7',
|
||||
'yellow-200': '#D9C599',
|
||||
'yellow-250': '#D2B677',
|
||||
'yellow-300': '#CAA756',
|
||||
'yellow-350': '#C2972E',
|
||||
'yellow-400': '#B98900',
|
||||
'yellow-450': '#AB7B00',
|
||||
'yellow-500': '#9D6E00',
|
||||
'yellow-550': '#916100',
|
||||
'yellow-600': '#855400',
|
||||
'yellow-650': '#784700',
|
||||
'yellow-700': '#6C3A00',
|
||||
'yellow-750': '#5F2E00',
|
||||
'yellow-800': '#512302',
|
||||
'yellow-850': '#3E1D10',
|
||||
'yellow-900': '#2D1711',
|
||||
'yellow-950': '#1D0F0D',
|
||||
'green-050': '#E6F1E9',
|
||||
'green-100': '#CFE4D5',
|
||||
'green-150': '#B8D8C1',
|
||||
'green-200': '#A0CFAE',
|
||||
'green-250': '#84C59A',
|
||||
'green-300': '#65BA86',
|
||||
'green-350': '#45B173',
|
||||
'green-400': '#23A562',
|
||||
'green-450': '#029755',
|
||||
'green-500': '#008948',
|
||||
'green-550': '#017B3B',
|
||||
'green-600': '#006E2E',
|
||||
'green-650': '#006022',
|
||||
'green-700': '#005314',
|
||||
'green-750': '#0D4510',
|
||||
'green-800': '#11380E',
|
||||
'green-850': '#132A11',
|
||||
'green-900': '#101E0F',
|
||||
'green-950': '#091209',
|
||||
'blue1-050': '#EBF1F9',
|
||||
'blue1-100': '#D6E4F4',
|
||||
'blue1-150': '#C1D7F0',
|
||||
'blue1-200': '#AACAEF',
|
||||
'blue1-250': '#8FBCEF',
|
||||
'blue1-300': '#7CAFEB',
|
||||
'blue1-350': '#68A1E4',
|
||||
'blue1-400': '#5B94D6',
|
||||
'blue1-450': '#4E86C7',
|
||||
'blue1-500': '#4279B9',
|
||||
'blue1-550': '#356CAC',
|
||||
'blue1-600': '#28609E',
|
||||
'blue1-650': '#1B5390',
|
||||
'blue1-700': '#0B4783',
|
||||
'blue1-750': '#0F3C6E',
|
||||
'blue1-800': '#133059',
|
||||
'blue1-850': '#152641',
|
||||
'blue1-900': '#121C2D',
|
||||
'blue1-950': '#0B111C',
|
||||
'blue2-050': '#E7F3F4',
|
||||
'blue2-100': '#CEE7E9',
|
||||
'blue2-150': '#B2DCE0',
|
||||
'blue2-200': '#91D1D7',
|
||||
'blue2-250': '#68C7D0',
|
||||
'blue2-300': '#43BBC5',
|
||||
'blue2-350': '#00AFBA',
|
||||
'blue2-400': '#01A0AA',
|
||||
'blue2-450': '#00929D',
|
||||
'blue2-500': '#00848F',
|
||||
'blue2-550': '#007682',
|
||||
'blue2-600': '#016874',
|
||||
'blue2-650': '#005B67',
|
||||
'blue2-700': '#004E5A',
|
||||
'blue2-750': '#00424E',
|
||||
'blue2-800': '#003642',
|
||||
'blue2-850': '#002A38',
|
||||
'blue2-900': '#061E28',
|
||||
'blue2-950': '#071219',
|
||||
'purple-050': '#F7F0F6',
|
||||
'purple-100': '#EEE0EE',
|
||||
'purple-150': '#E7D1E7',
|
||||
'purple-200': '#DBBFE4',
|
||||
'purple-250': '#D3AEE2',
|
||||
'purple-300': '#CB99E1',
|
||||
'purple-350': '#C188D9',
|
||||
'purple-400': '#B47BCB',
|
||||
'purple-450': '#A66EBD',
|
||||
'purple-500': '#9961AF',
|
||||
'purple-550': '#8B55A1',
|
||||
'purple-600': '#7E4894',
|
||||
'purple-650': '#723C87',
|
||||
'purple-700': '#633376',
|
||||
'purple-750': '#552A65',
|
||||
'purple-800': '#452551',
|
||||
'purple-850': '#35213D',
|
||||
'purple-900': '#261A2C',
|
||||
'purple-950': '#17111C',
|
||||
'pink-050': '#F8EFF4',
|
||||
'pink-100': '#F0DFEA',
|
||||
'pink-150': '#EACEDF',
|
||||
'pink-200': '#E9BBD1',
|
||||
'pink-250': '#E9A7C2',
|
||||
'pink-300': '#E095B4',
|
||||
'pink-350': '#D685A8',
|
||||
'pink-400': '#C7799B',
|
||||
'pink-450': '#B86C8D',
|
||||
'pink-500': '#AA5F80',
|
||||
'pink-550': '#9C5374',
|
||||
'pink-600': '#8E4767',
|
||||
'pink-650': '#813B5B',
|
||||
'pink-700': '#732E4F',
|
||||
'pink-750': '#632643',
|
||||
'pink-800': '#521F38',
|
||||
'pink-850': '#3E1C2B',
|
||||
'pink-900': '#2D171F',
|
||||
'pink-950': '#1C0E12',
|
||||
},
|
||||
font: {
|
||||
families: {
|
||||
@@ -250,18 +384,6 @@ const genericTheme = {
|
||||
},
|
||||
},
|
||||
},
|
||||
components: {
|
||||
button: {
|
||||
primary: {
|
||||
background: {
|
||||
'color-hover': 'var(--c--theme--colors--primary-focus)',
|
||||
'color-active': 'var(--c--theme--colors--primary-focus)',
|
||||
'color-focus': 'var(--c--theme--colors--primary-focus)',
|
||||
},
|
||||
},
|
||||
},
|
||||
'image-system-filter': 'saturate(0.2)',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -35,13 +35,13 @@
|
||||
"@fontsource-variable/material-symbols-outlined": "5.2.28",
|
||||
"@fontsource/material-icons": "5.2.7",
|
||||
"@gouvfr-lasuite/integration": "1.0.3",
|
||||
"@gouvfr-lasuite/ui-kit": "0.16.2",
|
||||
"@gouvfr-lasuite/ui-kit": "0.18.0",
|
||||
"@hocuspocus/provider": "3.4.0",
|
||||
"@mantine/core": "8.3.6",
|
||||
"@mantine/hooks": "8.3.6",
|
||||
"@openfun/cunningham-react": "3.2.3",
|
||||
"@openfun/cunningham-react": "4.0.0",
|
||||
"@react-pdf/renderer": "4.3.1",
|
||||
"@sentry/nextjs": "10.22.0",
|
||||
"@sentry/nextjs": "10.27.0",
|
||||
"@tanstack/react-query": "5.90.6",
|
||||
"@tiptap/extensions": "*",
|
||||
"canvg": "4.0.3",
|
||||
|
||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.1 KiB |
12
src/frontend/apps/impress/public/assets/icon-docs-dsfr.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<svg viewBox="0 0 32 33" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M21.6305 29.5812C22.7983 29.2538 23.9166 28.6562 24.6505 27.6003C25.3749 26.5663 25.5789 25.2547 25.5789 23.9925V5.50099C25.5789 5.17358 25.5611 4.84557 25.5216 4.52148C26.1016 4.74961 26.5486 5.12658 26.8626 5.65239C27.2331 6.25024 27.4184 7.03757 27.4184 8.01435V26.7964C27.4184 28.1184 27.0942 29.1078 26.4458 29.7646C25.7974 30.4214 24.8207 30.7498 23.5155 30.7498H16.4209C16.5889 30.7204 16.7574 30.6901 16.9262 30.659C18.4067 30.3944 19.9713 30.0354 21.6185 29.5846L21.6305 29.5812Z"
|
||||
fill="#C9191E"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M4.58203 26.405V7.5977C4.58203 6.45251 4.88938 5.58519 5.50408 4.99575C6.1272 4.40631 6.95242 4.08212 7.97972 4.02318C9.49542 3.93055 10.9311 3.80425 12.2868 3.64425C13.6425 3.47584 14.9393 3.28217 16.1771 3.06324C17.4234 2.8443 18.6359 2.60011 19.8148 2.33065C21.0274 2.04435 21.9578 2.1875 22.6062 2.7601C23.2546 3.33269 23.5788 4.24632 23.5788 5.50099V23.9925C23.5788 25.0956 23.3893 25.9166 23.0104 26.4555C22.6315 27.0029 21.9915 27.4028 21.0905 27.6554C19.4906 28.0933 17.9833 28.4386 16.5687 28.6912C15.154 28.9522 13.7731 29.1501 12.4258 29.2848C11.0785 29.4196 9.69751 29.5248 8.28286 29.6006C7.11241 29.668 6.20299 29.4238 5.5546 28.868C4.90622 28.3207 4.58203 27.4997 4.58203 26.405ZM9.20865 11.0124C11.0635 10.8944 12.7632 10.7131 14.3075 10.4683C14.6822 10.4072 15.0564 10.3436 15.4291 10.2776C15.8192 10.2085 16.1013 9.86859 16.1013 9.47337C16.1013 8.96154 15.638 8.57609 15.135 8.66189C14.846 8.71118 14.5555 8.75909 14.2635 8.80562C12.7346 9.04923 11.0452 9.22998 9.19523 9.3477C8.91819 9.36558 8.69776 9.45188 8.55608 9.62391C8.42209 9.78661 8.35645 9.98229 8.35645 10.2053C8.35645 10.4321 8.43296 10.6295 8.58568 10.7918L8.58783 10.7939C8.75336 10.9595 8.96369 11.0311 9.20865 11.0124ZM9.20801 15.206C11.0631 15.088 12.763 14.9066 14.3075 14.6619C15.8588 14.4089 17.3936 14.1138 18.9112 13.7766C19.2191 13.7081 19.4498 13.6003 19.5652 13.433C19.6786 13.2721 19.7347 13.0876 19.7347 12.8832C19.7347 12.6526 19.6469 12.454 19.476 12.2926C19.2921 12.1189 19.0348 12.0784 18.7304 12.1411L18.7285 12.1415C17.2823 12.4694 15.794 12.7553 14.2635 12.9992C12.7346 13.2428 11.0452 13.4235 9.19523 13.5413C8.91819 13.5591 8.69776 13.6454 8.55608 13.8175C8.42276 13.9794 8.35645 14.1705 8.35645 14.3863C8.35645 14.6203 8.43209 14.8223 8.58558 14.9854L8.59 14.9896C8.75499 15.1449 8.96316 15.2155 9.20551 15.2062L9.20801 15.206ZM9.20847 19.3994C11.0634 19.2729 12.7631 19.0874 14.3075 18.8427C15.8589 18.5982 17.3934 18.3073 18.9112 17.97C19.2199 17.9014 19.4508 17.7891 19.566 17.6127C19.6783 17.4529 19.7347 17.2733 19.7347 17.0766C19.7347 16.8461 19.6469 16.6474 19.476 16.4861C19.2921 16.3123 19.0348 16.2718 18.7304 16.3345L18.729 16.3348C17.2827 16.6543 15.7942 16.9361 14.2635 17.18C12.7345 17.4236 11.045 17.6086 9.19495 17.7347C8.91804 17.7526 8.69771 17.8389 8.55608 18.0109C8.42276 18.1728 8.35645 18.3639 8.35645 18.5797C8.35645 18.8137 8.43209 19.0158 8.58558 19.1789L8.59 19.183C8.75499 19.3383 8.96316 19.4089 9.20551 19.3996L9.20847 19.3994ZM14.3075 23.007C12.7632 23.2518 11.0635 23.4331 9.20867 23.5512C8.9637 23.5698 8.75337 23.4982 8.58783 23.3326L8.58572 23.3305C8.433 23.1682 8.35645 22.9708 8.35645 22.7441C8.35645 22.521 8.42209 22.3253 8.55608 22.1626C8.69776 21.9906 8.91827 21.9043 9.19531 21.8864C11.0453 21.7687 12.7346 21.588 14.2635 21.3443C14.5555 21.2978 14.846 21.2499 15.135 21.2006C15.638 21.1148 16.1013 21.5003 16.1013 22.0121C16.1013 22.4073 15.8192 22.7472 15.4291 22.8163C15.0564 22.8823 14.6822 22.9459 14.3075 23.007Z"
|
||||
fill="#2845C1"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
@@ -1,12 +1,4 @@
|
||||
<svg viewBox="0 0 32 33" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M21.6305 29.5812C22.7983 29.2538 23.9166 28.6562 24.6505 27.6003C25.3749 26.5663 25.5789 25.2547 25.5789 23.9925V5.50099C25.5789 5.17358 25.5611 4.84557 25.5216 4.52148C26.1016 4.74961 26.5486 5.12658 26.8626 5.65239C27.2331 6.25024 27.4184 7.03757 27.4184 8.01435V26.7964C27.4184 28.1184 27.0942 29.1078 26.4458 29.7646C25.7974 30.4214 24.8207 30.7498 23.5155 30.7498H16.4209C16.5889 30.7204 16.7574 30.6901 16.9262 30.659C18.4067 30.3944 19.9713 30.0354 21.6185 29.5846L21.6305 29.5812Z"
|
||||
fill="#C9191E"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M4.58203 26.405V7.5977C4.58203 6.45251 4.88938 5.58519 5.50408 4.99575C6.1272 4.40631 6.95242 4.08212 7.97972 4.02318C9.49542 3.93055 10.9311 3.80425 12.2868 3.64425C13.6425 3.47584 14.9393 3.28217 16.1771 3.06324C17.4234 2.8443 18.6359 2.60011 19.8148 2.33065C21.0274 2.04435 21.9578 2.1875 22.6062 2.7601C23.2546 3.33269 23.5788 4.24632 23.5788 5.50099V23.9925C23.5788 25.0956 23.3893 25.9166 23.0104 26.4555C22.6315 27.0029 21.9915 27.4028 21.0905 27.6554C19.4906 28.0933 17.9833 28.4386 16.5687 28.6912C15.154 28.9522 13.7731 29.1501 12.4258 29.2848C11.0785 29.4196 9.69751 29.5248 8.28286 29.6006C7.11241 29.668 6.20299 29.4238 5.5546 28.868C4.90622 28.3207 4.58203 27.4997 4.58203 26.405ZM9.20865 11.0124C11.0635 10.8944 12.7632 10.7131 14.3075 10.4683C14.6822 10.4072 15.0564 10.3436 15.4291 10.2776C15.8192 10.2085 16.1013 9.86859 16.1013 9.47337C16.1013 8.96154 15.638 8.57609 15.135 8.66189C14.846 8.71118 14.5555 8.75909 14.2635 8.80562C12.7346 9.04923 11.0452 9.22998 9.19523 9.3477C8.91819 9.36558 8.69776 9.45188 8.55608 9.62391C8.42209 9.78661 8.35645 9.98229 8.35645 10.2053C8.35645 10.4321 8.43296 10.6295 8.58568 10.7918L8.58783 10.7939C8.75336 10.9595 8.96369 11.0311 9.20865 11.0124ZM9.20801 15.206C11.0631 15.088 12.763 14.9066 14.3075 14.6619C15.8588 14.4089 17.3936 14.1138 18.9112 13.7766C19.2191 13.7081 19.4498 13.6003 19.5652 13.433C19.6786 13.2721 19.7347 13.0876 19.7347 12.8832C19.7347 12.6526 19.6469 12.454 19.476 12.2926C19.2921 12.1189 19.0348 12.0784 18.7304 12.1411L18.7285 12.1415C17.2823 12.4694 15.794 12.7553 14.2635 12.9992C12.7346 13.2428 11.0452 13.4235 9.19523 13.5413C8.91819 13.5591 8.69776 13.6454 8.55608 13.8175C8.42276 13.9794 8.35645 14.1705 8.35645 14.3863C8.35645 14.6203 8.43209 14.8223 8.58558 14.9854L8.59 14.9896C8.75499 15.1449 8.96316 15.2155 9.20551 15.2062L9.20801 15.206ZM9.20847 19.3994C11.0634 19.2729 12.7631 19.0874 14.3075 18.8427C15.8589 18.5982 17.3934 18.3073 18.9112 17.97C19.2199 17.9014 19.4508 17.7891 19.566 17.6127C19.6783 17.4529 19.7347 17.2733 19.7347 17.0766C19.7347 16.8461 19.6469 16.6474 19.476 16.4861C19.2921 16.3123 19.0348 16.2718 18.7304 16.3345L18.729 16.3348C17.2827 16.6543 15.7942 16.9361 14.2635 17.18C12.7345 17.4236 11.045 17.6086 9.19495 17.7347C8.91804 17.7526 8.69771 17.8389 8.55608 18.0109C8.42276 18.1728 8.35645 18.3639 8.35645 18.5797C8.35645 18.8137 8.43209 19.0158 8.58558 19.1789L8.59 19.183C8.75499 19.3383 8.96316 19.4089 9.20551 19.3996L9.20847 19.3994ZM14.3075 23.007C12.7632 23.2518 11.0635 23.4331 9.20867 23.5512C8.9637 23.5698 8.75337 23.4982 8.58783 23.3326L8.58572 23.3305C8.433 23.1682 8.35645 22.9708 8.35645 22.7441C8.35645 22.521 8.42209 22.3253 8.55608 22.1626C8.69776 21.9906 8.91827 21.9043 9.19531 21.8864C11.0453 21.7687 12.7346 21.588 14.2635 21.3443C14.5555 21.2978 14.846 21.2499 15.135 21.2006C15.638 21.1148 16.1013 21.5003 16.1013 22.0121C16.1013 22.4073 15.8192 22.7472 15.4291 22.8163C15.0564 22.8823 14.6822 22.9459 14.3075 23.007Z"
|
||||
fill="#000091"
|
||||
/>
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21.6305 28.8312C22.7983 28.5038 23.9166 27.9062 24.6505 26.8503C25.3749 25.8163 25.5789 24.5047 25.5789 23.2425V4.75099C25.5789 4.42358 25.5611 4.09557 25.5216 3.77148C26.1016 3.99961 26.5486 4.37658 26.8626 4.90239C27.2331 5.50024 27.4184 6.28757 27.4184 7.26435V26.0464C27.4184 27.3684 27.0942 28.3578 26.4458 29.0146C25.7974 29.6714 24.8207 29.9998 23.5155 29.9998H16.4209C16.5889 29.9704 16.7574 29.9401 16.9262 29.909C18.4067 29.6444 19.9713 29.2854 21.6185 28.8346L21.6305 28.8312Z" fill="#4747A3"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.58203 25.655V6.8477C4.58203 5.70251 4.88938 4.83519 5.50408 4.24575C6.1272 3.65631 6.95242 3.33212 7.97972 3.27318C9.49542 3.18055 10.9311 3.05425 12.2868 2.89425C13.6425 2.72584 14.9393 2.53217 16.1771 2.31324C17.4234 2.0943 18.6359 1.85011 19.8148 1.58065C21.0274 1.29435 21.9578 1.4375 22.6062 2.0101C23.2546 2.58269 23.5788 3.49632 23.5788 4.75099V23.2425C23.5788 24.3456 23.3893 25.1666 23.0104 25.7055C22.6315 26.2529 21.9915 26.6528 21.0905 26.9054C19.4906 27.3433 17.9833 27.6886 16.5687 27.9412C15.154 28.2022 13.7731 28.4001 12.4258 28.5348C11.0785 28.6696 9.69751 28.7748 8.28286 28.8506C7.11241 28.918 6.20299 28.6738 5.5546 28.118C4.90622 27.5707 4.58203 26.7497 4.58203 25.655ZM9.20865 10.2624C11.0635 10.1444 12.7632 9.96305 14.3075 9.71831C14.6822 9.65722 15.0564 9.5936 15.4291 9.52759C15.8192 9.45851 16.1013 9.11859 16.1013 8.72337C16.1013 8.21154 15.638 7.82609 15.135 7.91189C14.846 7.96118 14.5555 8.00909 14.2635 8.05562C12.7346 8.29923 11.0452 8.47998 9.19523 8.5977C8.91819 8.61558 8.69776 8.70188 8.55608 8.87391C8.42209 9.03661 8.35645 9.23229 8.35645 9.45535C8.35645 9.68212 8.43296 9.87951 8.58568 10.0418L8.58783 10.0439C8.75336 10.2095 8.96369 10.2811 9.20865 10.2624ZM9.20801 14.456C11.0631 14.338 12.763 14.1566 14.3075 13.9119C15.8588 13.6589 17.3936 13.3638 18.9112 13.0266C19.2191 12.9581 19.4498 12.8503 19.5652 12.683C19.6786 12.5221 19.7347 12.3376 19.7347 12.1332C19.7347 11.9026 19.6469 11.704 19.476 11.5426C19.2921 11.3689 19.0348 11.3284 18.7304 11.3911L18.7285 11.3915C17.2823 11.7194 15.794 12.0053 14.2635 12.2492C12.7346 12.4928 11.0452 12.6735 9.19523 12.7913C8.91819 12.8091 8.69776 12.8954 8.55608 13.0675C8.42276 13.2294 8.35645 13.4205 8.35645 13.6363C8.35645 13.8703 8.43209 14.0723 8.58558 14.2354L8.59 14.2396C8.75499 14.3949 8.96316 14.4655 9.20551 14.4562L9.20801 14.456ZM9.20847 18.6494C11.0634 18.5229 12.7631 18.3374 14.3075 18.0927C15.8589 17.8482 17.3934 17.5573 18.9112 17.22C19.2199 17.1514 19.4508 17.0391 19.566 16.8627C19.6783 16.7029 19.7347 16.5233 19.7347 16.3266C19.7347 16.0961 19.6469 15.8974 19.476 15.7361C19.2921 15.5623 19.0348 15.5218 18.7304 15.5845L18.729 15.5848C17.2827 15.9043 15.7942 16.1861 14.2635 16.43C12.7345 16.6736 11.045 16.8586 9.19495 16.9847C8.91804 17.0026 8.69771 17.0889 8.55608 17.2609C8.42276 17.4228 8.35645 17.6139 8.35645 17.8297C8.35645 18.0637 8.43209 18.2658 8.58558 18.4289L8.59 18.433C8.75499 18.5883 8.96316 18.6589 9.20551 18.6496L9.20847 18.6494ZM14.3075 22.257C12.7632 22.5018 11.0635 22.6831 9.20867 22.8012C8.9637 22.8198 8.75337 22.7482 8.58783 22.5826L8.58572 22.5805C8.433 22.4182 8.35645 22.2208 8.35645 21.9941C8.35645 21.771 8.42209 21.5753 8.55608 21.4126C8.69776 21.2406 8.91827 21.1543 9.19531 21.1364C11.0453 21.0187 12.7346 20.838 14.2635 20.5943C14.5555 20.5478 14.846 20.4999 15.135 20.4506C15.638 20.3648 16.1013 20.7503 16.1013 21.2621C16.1013 21.6573 15.8192 21.9972 15.4291 22.0663C15.0564 22.1323 14.6822 22.1959 14.3075 22.257Z" fill="#4747A3"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
@@ -1,7 +1,7 @@
|
||||
<svg viewBox="0 0 32 33" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M21.6305 29.5812C22.7983 29.2538 23.9166 28.6562 24.6505 27.6003C25.3749 26.5663 25.5789 25.2547 25.5789 23.9925V5.50099C25.5789 5.17358 25.5611 4.84557 25.5216 4.52148C26.1016 4.74961 26.5486 5.12658 26.8626 5.65239C27.2331 6.25024 27.4184 7.03757 27.4184 8.01435V26.7964C27.4184 28.1184 27.0942 29.1078 26.4458 29.7646C25.7974 30.4214 24.8207 30.7498 23.5155 30.7498H16.4209C16.5889 30.7204 16.7574 30.6901 16.9262 30.659C18.4067 30.3944 19.9713 30.0354 21.6185 29.5846L21.6305 29.5812Z"
|
||||
fill="var(--c--theme--colors--secondary-icon)"
|
||||
fill="var(--c--globals--colors--red-600)"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
|
||||
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
@@ -14,6 +14,7 @@ export interface BoxProps {
|
||||
as?: HTMLElementType;
|
||||
$align?: CSSProperties['alignItems'];
|
||||
$background?: CSSProperties['background'];
|
||||
$border?: CSSProperties['border'];
|
||||
$color?: CSSProperties['color'];
|
||||
$css?: string | RuleSet<object>;
|
||||
$cursor?: CSSProperties['cursor'];
|
||||
@@ -36,41 +37,34 @@ export interface BoxProps {
|
||||
$position?: CSSProperties['position'];
|
||||
$radius?: CSSProperties['borderRadius'];
|
||||
$shrink?: CSSProperties['flexShrink'];
|
||||
$transition?: CSSProperties['transition'];
|
||||
$width?: CSSProperties['width'];
|
||||
$zIndex?: CSSProperties['zIndex'];
|
||||
$wrap?: CSSProperties['flexWrap'];
|
||||
// Theming props
|
||||
$layer?: 'background' | 'content' | 'border';
|
||||
$theme?:
|
||||
| 'primary'
|
||||
| 'primary-text'
|
||||
| 'secondary'
|
||||
| 'secondary-text'
|
||||
| 'brand'
|
||||
| 'error'
|
||||
| 'gray'
|
||||
| 'info'
|
||||
| 'success'
|
||||
| 'warning'
|
||||
| 'danger'
|
||||
| 'greyscale';
|
||||
$transition?: CSSProperties['transition'];
|
||||
$variation?:
|
||||
| 'text'
|
||||
| '000'
|
||||
| '100'
|
||||
| '200'
|
||||
| '300'
|
||||
| '400'
|
||||
| '500'
|
||||
| '600'
|
||||
| '700'
|
||||
| '800'
|
||||
| '900'
|
||||
| '1000';
|
||||
$width?: CSSProperties['width'];
|
||||
$wrap?: CSSProperties['flexWrap'];
|
||||
$zIndex?: CSSProperties['zIndex'];
|
||||
| 'neutral'
|
||||
| 'contextual'
|
||||
| 'disabled'
|
||||
| (string & {});
|
||||
$scope?: 'surface' | 'semantic' | 'palette' | (string & {});
|
||||
$variation?: 'primary' | 'secondary' | 'tertiary' | (string & {});
|
||||
$withThemeBG?: boolean;
|
||||
$withThemeBorder?: boolean;
|
||||
$withThemeInherited?: boolean;
|
||||
}
|
||||
|
||||
export type BoxType = ComponentPropsWithRef<typeof Box>;
|
||||
|
||||
export const Box = styled('div')<BoxProps>`
|
||||
${({ $align }) => $align && `align-items: ${$align};`}
|
||||
${({ $background }) => $background && `background: ${$background};`}
|
||||
${({ $color }) => $color && `color: ${$color};`}
|
||||
${({ $cursor }) => $cursor && `cursor: ${$cursor};`}
|
||||
${({ $direction }) => `flex-direction: ${$direction || 'column'};`}
|
||||
${({ $display, as }) =>
|
||||
@@ -80,9 +74,9 @@ export const Box = styled('div')<BoxProps>`
|
||||
${({ $height }) => $height && `height: ${$height};`}
|
||||
${({ $hasTransition }) =>
|
||||
$hasTransition && $hasTransition === 'slow'
|
||||
? `transition: all 0.5s ease-in-out;`
|
||||
? `transition: all 0.5s var(--c--globals--transitions--ease-out);`
|
||||
: $hasTransition
|
||||
? `transition: all 0.3s ease-in-out;`
|
||||
? `transition: all var(--c--globals--transitions--duration) var(--c--globals--transitions--ease-out);`
|
||||
: ''}
|
||||
${({ $justify }) => $justify && `justify-content: ${$justify};`}
|
||||
${({ $margin }) => $margin && stylesMargin($margin)}
|
||||
@@ -96,11 +90,85 @@ export const Box = styled('div')<BoxProps>`
|
||||
${({ $position }) => $position && `position: ${$position};`}
|
||||
${({ $radius }) => $radius && `border-radius: ${$radius};`}
|
||||
${({ $shrink }) => $shrink && `flex-shrink: ${$shrink};`}
|
||||
${({ $theme, $variation }) => {
|
||||
if (!$theme || !$variation) {
|
||||
${({
|
||||
$layer = 'border',
|
||||
$theme = 'brand',
|
||||
$variation = 'primary',
|
||||
$scope = 'semantic',
|
||||
$border,
|
||||
$withThemeBorder,
|
||||
$withThemeInherited,
|
||||
}) => {
|
||||
if ($border) {
|
||||
return `border: ${$border};`;
|
||||
}
|
||||
|
||||
if (!$layer || !$scope || !$theme || !$withThemeBorder) {
|
||||
return '';
|
||||
}
|
||||
return `color: var(--c--theme--colors--${$theme}-${$variation});`;
|
||||
|
||||
if ($withThemeInherited) {
|
||||
return `border: inherit;`;
|
||||
}
|
||||
|
||||
return `border: 1px solid var(--c--contextuals--${$layer}--${$scope}${$theme ? `--${$theme}` : ''}${$variation ? `--${$variation}` : ''});`;
|
||||
}}
|
||||
${({
|
||||
$layer = 'background',
|
||||
$theme = 'brand',
|
||||
$variation = 'primary',
|
||||
$scope = 'semantic',
|
||||
$background,
|
||||
$withThemeBG,
|
||||
$withThemeInherited,
|
||||
}) => {
|
||||
if ($background) {
|
||||
return `background: ${$background};`;
|
||||
}
|
||||
|
||||
if (!$layer || !$scope || !$theme || !$withThemeBG) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ($withThemeInherited) {
|
||||
return `background: inherit;`;
|
||||
}
|
||||
|
||||
return `background: var(--c--contextuals--${$layer}--${$scope}${$theme ? `--${$theme}` : ''}${$variation ? `--${$variation}` : ''});`;
|
||||
}}
|
||||
${({
|
||||
$layer = 'content',
|
||||
$theme = 'neutral',
|
||||
$variation = 'primary',
|
||||
$scope = 'semantic',
|
||||
$color,
|
||||
$withThemeBG,
|
||||
$withThemeInherited,
|
||||
}) => {
|
||||
if ($color) {
|
||||
return `color: ${$color};`;
|
||||
}
|
||||
|
||||
if (!$layer || !$scope) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// There is a special case when primary with background
|
||||
if (
|
||||
$withThemeBG &&
|
||||
$layer === 'content' &&
|
||||
$scope === 'semantic' &&
|
||||
$variation === 'primary' &&
|
||||
$theme
|
||||
) {
|
||||
$variation = `on-${$theme}`;
|
||||
}
|
||||
|
||||
if ($withThemeInherited) {
|
||||
return `color: inherit;`;
|
||||
}
|
||||
|
||||
return `color: var(--c--contextuals--${$layer}--${$scope}${$theme ? `--${$theme}` : ''}${$variation ? `--${$variation}` : ''});`;
|
||||
}}
|
||||
${({ $transition }) => $transition && `transition: ${$transition};`}
|
||||
${({ $width }) => $width && `width: ${$width};`}
|
||||
@@ -121,7 +189,7 @@ export const Box = styled('div')<BoxProps>`
|
||||
return (
|
||||
effect &&
|
||||
`
|
||||
transition: all 0.3s ease-in-out;
|
||||
transition: all var(--c--globals--transitions--duration) var(--c--globals--transitions--ease-out);
|
||||
${effect}
|
||||
`
|
||||
);
|
||||
|
||||
@@ -24,8 +24,8 @@ export type BoxButtonType = BoxType & {
|
||||
*/
|
||||
const BoxButton = forwardRef<HTMLDivElement, BoxButtonType>(
|
||||
({ $css, ...props }, ref) => {
|
||||
const theme = props.$theme || 'greyscale';
|
||||
const variation = props.$variation || '400';
|
||||
const theme = props.$theme || 'gray';
|
||||
const variation = props.$variation || 'primary';
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -40,15 +40,14 @@ const BoxButton = forwardRef<HTMLDivElement, BoxButtonType>(
|
||||
border: none;
|
||||
outline: none;
|
||||
font-family: inherit;
|
||||
color: ${props.disabled
|
||||
? `var(--c--theme--colors--${theme}-400) !important`
|
||||
: `inherit`};
|
||||
|
||||
color: ${props.disabled &&
|
||||
`var(--c--contextuals--content--semantic--disabled--primary)`};
|
||||
&:focus-visible {
|
||||
transition: none;
|
||||
outline: 2px solid var(--c--theme--colors--${theme}-${variation});
|
||||
outline: 2px solid
|
||||
var(--c--contextuals--content--semantic--${theme}--${variation});
|
||||
border-radius: 1px;
|
||||
outline-offset: 4px;
|
||||
outline-offset: var(--c--globals--spacings--st);
|
||||
}
|
||||
${$css || ''}
|
||||
`}
|
||||
|
||||
@@ -1,27 +1,23 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
|
||||
import { Box, BoxType } from '.';
|
||||
|
||||
export const Card = ({
|
||||
children,
|
||||
className,
|
||||
$css,
|
||||
...props
|
||||
}: PropsWithChildren<BoxType>) => {
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
|
||||
return (
|
||||
<Box
|
||||
role="region"
|
||||
$withThemeBG
|
||||
$withThemeBorder
|
||||
className={`--docs--card ${className || ''}`}
|
||||
$background="white"
|
||||
$radius="4px"
|
||||
$css={css`
|
||||
border: 1px solid ${colorsTokens['greyscale-200']};
|
||||
${$css}
|
||||
`}
|
||||
$radius="var(--c--globals--spacings--st)"
|
||||
$padding={{ horizontal: 'xs', vertical: '3xs' }}
|
||||
$scope={props.$theme ? props.$scope || 'semantic' : 'surface'}
|
||||
$theme={props.$theme || 'primary'}
|
||||
$variation={props.$theme ? props.$variation || 'tertiary' : ''}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -14,10 +14,11 @@ import { BoxProps } from './Box';
|
||||
|
||||
const StyledPopover = styled(Popover)`
|
||||
background-color: white;
|
||||
border-radius: 4px;
|
||||
box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid #dddddd;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
border-radius: var(--c--globals--spacings--st);
|
||||
box-shadow: 0 0 6px 0 rgba(0, 0, 145, 0.1);
|
||||
border: 1px solid var(--c--contextuals--border--surface--primary);
|
||||
transition: opacity var(--c--globals--transitions--duration)
|
||||
var(--c--globals--transitions--ease-out);
|
||||
`;
|
||||
|
||||
interface StyledButtonProps {
|
||||
@@ -28,18 +29,22 @@ const StyledButton = styled(Button)<StyledButtonProps>`
|
||||
border: none;
|
||||
background: none;
|
||||
outline: none;
|
||||
font-weight: 500;
|
||||
font-size: 0.938rem;
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
font-weight: var(--c--components--button--font-weight);
|
||||
font-size: var(--c--components--button--medium-font-size);
|
||||
padding: var(--c--globals--spacings--0);
|
||||
border-radius: var(--c--globals--spacings--st);
|
||||
color: var(--c--contextuals--content--semantic--brand--tertiary);
|
||||
&:hover {
|
||||
background-color: var(
|
||||
--c--components--button--primary-text--background--color-hover
|
||||
--c--contextuals--background--semantic--contextual--primary
|
||||
);
|
||||
}
|
||||
&:focus-visible {
|
||||
box-shadow: 0 0 0 2px var(--c--theme--colors--primary-400);
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0 0 2px var(--c--globals--colors--brand-400);
|
||||
background-color: var(
|
||||
--c--contextuals--background--semantic--brand--tertiary-hover
|
||||
);
|
||||
border-radius: var(--c--globals--spacings--st);
|
||||
}
|
||||
${({ $css }) => $css};
|
||||
`;
|
||||
|
||||
@@ -13,7 +13,7 @@ export const Icon = ({
|
||||
iconName,
|
||||
disabled,
|
||||
variant = 'outlined',
|
||||
$variation = 'text',
|
||||
$theme = 'neutral',
|
||||
...textProps
|
||||
}: IconProps) => {
|
||||
const hasLabel = 'aria-label' in textProps || 'aria-labelledby' in textProps;
|
||||
@@ -28,7 +28,7 @@ export const Icon = ({
|
||||
'material-icons': variant === 'outlined',
|
||||
'material-symbols-outlined': variant === 'symbols-outlined',
|
||||
})}
|
||||
$variation={disabled ? '300' : $variation}
|
||||
$theme={disabled ? 'disabled' : $theme}
|
||||
aria-disabled={disabled}
|
||||
{...textProps}
|
||||
>
|
||||
|
||||
@@ -39,7 +39,8 @@ export const InfiniteScroll = ({
|
||||
{!isLoading && hasMore && (
|
||||
<Button
|
||||
onClick={() => void next()}
|
||||
color="primary-text"
|
||||
color="brand"
|
||||
variant="bordered"
|
||||
icon={<Icon iconName="arrow_downward" />}
|
||||
>
|
||||
{buttonLabel ?? t('Load more')}
|
||||
|
||||
@@ -24,12 +24,12 @@ export const LoadMoreText = ({
|
||||
className="--docs--load-more"
|
||||
>
|
||||
<Icon
|
||||
$theme="primary"
|
||||
$variation="800"
|
||||
$theme="brand"
|
||||
$variation="secondary"
|
||||
iconName="arrow_downward"
|
||||
$size="md"
|
||||
/>
|
||||
<Text $theme="primary" $variation="800">
|
||||
<Text $theme="brand" $variation="secondary">
|
||||
{t('Load more')}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { tokens } from '@/cunningham';
|
||||
|
||||
import { Box, BoxProps } from './Box';
|
||||
|
||||
const { sizes } = tokens.themes.default.theme.font;
|
||||
const { sizes } = tokens.themes.default.globals.font;
|
||||
type TextSizes = keyof typeof sizes;
|
||||
|
||||
export interface TextProps extends BoxProps {
|
||||
@@ -27,24 +27,14 @@ export const TextStyled = styled(Box)<TextProps>`
|
||||
${({ $size }) =>
|
||||
$size &&
|
||||
`font-size: ${$size in sizes ? sizes[$size as TextSizes] : $size};`}
|
||||
${({ $color }) => $color && `color: ${$color};`}
|
||||
${({ $ellipsis }) =>
|
||||
$ellipsis &&
|
||||
`white-space: nowrap; overflow: hidden; text-overflow: ellipsis;`}
|
||||
`;
|
||||
|
||||
const Text = forwardRef<HTMLElement, ComponentPropsWithRef<typeof TextStyled>>(
|
||||
({ className, ...props }, ref) => {
|
||||
return (
|
||||
<TextStyled
|
||||
ref={ref}
|
||||
as="span"
|
||||
$theme="greyscale"
|
||||
$variation="text"
|
||||
className={className}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
(props, ref) => {
|
||||
return <TextStyled ref={ref} as="span" {...props} />;
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Box, Text, TextType } from '@/components';
|
||||
|
||||
const AlertStyled = styled(Alert)`
|
||||
& .c__button--tertiary:hover {
|
||||
background-color: var(--c--theme--colors--greyscale-200);
|
||||
background-color: var(--c--globals--colors--gray-200);
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -54,8 +54,7 @@ export const TextOnlyErrors = ({
|
||||
causes.map((cause, i) => (
|
||||
<Text
|
||||
key={`causes-${i}`}
|
||||
$theme="danger"
|
||||
$variation="600"
|
||||
$theme="error"
|
||||
$textAlign="center"
|
||||
{...textProps}
|
||||
>
|
||||
@@ -64,12 +63,7 @@ export const TextOnlyErrors = ({
|
||||
))}
|
||||
|
||||
{!causes && (
|
||||
<Text
|
||||
$theme="danger"
|
||||
$variation="600"
|
||||
$textAlign="center"
|
||||
{...textProps}
|
||||
>
|
||||
<Text $theme="error" $textAlign="center" {...textProps}>
|
||||
{defaultMessage || t('Something bad happens, please retry.')}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
@@ -124,18 +124,19 @@ export const DropdownMenu = ({
|
||||
>
|
||||
<Box>{children}</Box>
|
||||
<Icon
|
||||
$variation="600"
|
||||
$css={
|
||||
arrowCss ??
|
||||
css`
|
||||
color: var(--c--theme--colors--primary-600);
|
||||
color: var(--c--globals--colors--brand-600);
|
||||
`
|
||||
}
|
||||
iconName={isOpen ? 'arrow_drop_up' : 'arrow_drop_down'}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<Box ref={blockButtonRef}>{children}</Box>
|
||||
<Box ref={blockButtonRef} $color="inherit">
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
>
|
||||
@@ -147,7 +148,6 @@ export const DropdownMenu = ({
|
||||
>
|
||||
{topMessage && (
|
||||
<Text
|
||||
$variation="700"
|
||||
$wrap="wrap"
|
||||
$size="xs"
|
||||
$weight="bold"
|
||||
@@ -186,8 +186,8 @@ export const DropdownMenu = ({
|
||||
key={option.label}
|
||||
$align="center"
|
||||
$justify="space-between"
|
||||
$background={colorsTokens['greyscale-000']}
|
||||
$color={colorsTokens['primary-600']}
|
||||
$background={colorsTokens['gray-000']}
|
||||
$color={colorsTokens['brand-600']}
|
||||
$padding={{ vertical: 'xs', horizontal: 'base' }}
|
||||
$width="100%"
|
||||
$gap={spacingsTokens['base']}
|
||||
@@ -200,30 +200,39 @@ export const DropdownMenu = ({
|
||||
`}
|
||||
${index === options.length - 1 &&
|
||||
css`
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
border-bottom-left-radius: var(--c--globals--spacings--st);
|
||||
border-bottom-right-radius: var(--c--globals--spacings--st);
|
||||
`}
|
||||
font-size: var(--c--theme--font--sizes--sm);
|
||||
color: var(--c--theme--colors--greyscale-1000);
|
||||
font-weight: 500;
|
||||
font-size: var(--c--globals--font--sizes--sm);
|
||||
color: var(--c--globals--colors--gray-1000);
|
||||
font-weight: var(--c--globals--font--weights--medium);
|
||||
cursor: ${isDisabled ? 'not-allowed' : 'pointer'};
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--c--theme--colors--greyscale-050);
|
||||
background-color: var(
|
||||
--c--contextuals--background--semantic--contextual--primary
|
||||
);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--c--theme--colors--primary-400);
|
||||
outline: 2px solid var(--c--globals--colors--brand-400);
|
||||
outline-offset: -2px;
|
||||
background-color: var(--c--theme--colors--greyscale-050);
|
||||
background-color: var(
|
||||
--c--contextuals--background--semantic--contextual--primary
|
||||
);
|
||||
}
|
||||
|
||||
${isFocused &&
|
||||
/**
|
||||
* TODO: This part seems to have a problem with DocToolBox
|
||||
*/
|
||||
/* ${isFocused &&
|
||||
css`
|
||||
outline-offset: -2px;
|
||||
background-color: var(--c--theme--colors--greyscale-050);
|
||||
`}
|
||||
background-color: var(
|
||||
--c--contextuals--background--semantic--contextual--primary
|
||||
);
|
||||
`} */
|
||||
`}
|
||||
>
|
||||
<Box
|
||||
@@ -234,8 +243,8 @@ export const DropdownMenu = ({
|
||||
{option.icon && typeof option.icon === 'string' && (
|
||||
<Icon
|
||||
$size="20px"
|
||||
$theme="greyscale"
|
||||
$variation={isDisabled ? '400' : '1000'}
|
||||
$theme="gray"
|
||||
$variation={isDisabled ? 'tertiary' : 'primary'}
|
||||
iconName={option.icon}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
@@ -243,7 +252,7 @@ export const DropdownMenu = ({
|
||||
{option.icon &&
|
||||
typeof option.icon !== 'string' &&
|
||||
option.icon}
|
||||
<Text $variation={isDisabled ? '400' : '1000'}>
|
||||
<Text $variation={isDisabled ? 'tertiary' : 'primary'}>
|
||||
{option.label}
|
||||
</Text>
|
||||
</Box>
|
||||
@@ -252,7 +261,7 @@ export const DropdownMenu = ({
|
||||
<Icon
|
||||
iconName="check"
|
||||
$size="20px"
|
||||
$theme="greyscale"
|
||||
$theme="gray"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -33,32 +33,27 @@ export const FilterDropdown = ({
|
||||
<Box
|
||||
$css={css`
|
||||
border: 1px solid
|
||||
${selectedOption
|
||||
? 'var(--c--theme--colors--primary-500)'
|
||||
: 'var(--c--theme--colors--greyscale-250)'};
|
||||
border-radius: 4px;
|
||||
background-color: ${selectedOption
|
||||
? 'var(--c--theme--colors--primary-100)'
|
||||
: 'var(--c--theme--colors--greyscale-000)'};
|
||||
gap: var(--c--theme--spacings--2xs);
|
||||
padding: var(--c--theme--spacings--2xs) var(--c--theme--spacings--xs);
|
||||
var(--c--contextuals--border--semantic--neutral--tertiary);
|
||||
border-radius: var(--c--globals--spacings--st);
|
||||
background-color: var(
|
||||
--c--contextuals--background--semantic--neutral--tertiary
|
||||
);
|
||||
gap: var(--c--globals--spacings--2xs);
|
||||
padding: var(--c--globals--spacings--2xs)
|
||||
var(--c--globals--spacings--xs);
|
||||
`}
|
||||
color="secondary"
|
||||
$direction="row"
|
||||
$align="center"
|
||||
>
|
||||
<Text
|
||||
$weight={400}
|
||||
$variation={selectedOption ? '800' : '600'}
|
||||
$theme={selectedOption ? 'primary' : 'greyscale'}
|
||||
>
|
||||
<Text $weight={400} $variation="tertiary" $theme="neutral">
|
||||
{selectedOption?.label ?? options[0].label}
|
||||
</Text>
|
||||
<Icon
|
||||
$size="16px"
|
||||
$size="s"
|
||||
iconName="keyboard_arrow_down"
|
||||
$variation={selectedOption ? '800' : '600'}
|
||||
$theme={selectedOption ? 'primary' : 'greyscale'}
|
||||
$variation="tertiary"
|
||||
$theme="neutral"
|
||||
/>
|
||||
</Box>
|
||||
</DropdownMenu>
|
||||
|
||||
@@ -38,7 +38,6 @@ export const AlertModal = ({
|
||||
$margin="0"
|
||||
id="alert-modal-title"
|
||||
$align="flex-start"
|
||||
$variation="1000"
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
@@ -47,7 +46,7 @@ export const AlertModal = ({
|
||||
<>
|
||||
<Button
|
||||
aria-label={`${t('Cancel')} - ${title}`}
|
||||
color="secondary"
|
||||
variant="secondary"
|
||||
fullWidth
|
||||
onClick={() => onClose()}
|
||||
>
|
||||
@@ -55,7 +54,7 @@ export const AlertModal = ({
|
||||
</Button>
|
||||
<Button
|
||||
aria-label={confirmLabel ?? t('Confirm')}
|
||||
color="danger"
|
||||
color="error"
|
||||
onClick={onConfirm}
|
||||
>
|
||||
{confirmLabel ?? t('Confirm')}
|
||||
@@ -65,7 +64,7 @@ export const AlertModal = ({
|
||||
>
|
||||
<Box className="--docs--alert-modal">
|
||||
<Box>
|
||||
<Text $variation="600" as="p">
|
||||
<Text $variation="secondary" as="p">
|
||||
{description}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
import { Button, type ButtonProps } from '@openfun/cunningham-react';
|
||||
import React from 'react';
|
||||
|
||||
import { Box } from '@/components';
|
||||
import { Icon } from '@/components';
|
||||
|
||||
export const ButtonCloseModal = (props: ButtonProps) => {
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
size="small"
|
||||
color="primary-text"
|
||||
color="brand"
|
||||
variant="tertiary"
|
||||
icon={
|
||||
<Box as="span" aria-hidden="true" className="material-icons-filled">
|
||||
close
|
||||
</Box>
|
||||
<Icon
|
||||
$withThemeInherited
|
||||
iconName="close"
|
||||
className="material-icons-filled"
|
||||
/>
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -69,7 +69,7 @@ export const QuickSearch = ({
|
||||
label={label}
|
||||
shouldFilter={false}
|
||||
ref={ref}
|
||||
tabIndex={0}
|
||||
tabIndex={-1}
|
||||
value={selectedValue}
|
||||
onValueChange={handleValueChange}
|
||||
>
|
||||
|
||||
@@ -59,11 +59,7 @@ export const QuickSearchGroup = <T,>({
|
||||
);
|
||||
})}
|
||||
{group.emptyString && group.elements.length === 0 && (
|
||||
<Text
|
||||
$variation="500"
|
||||
$margin={{ left: '2xs', bottom: '3xs' }}
|
||||
$size="sm"
|
||||
>
|
||||
<Text $margin={{ left: '2xs', bottom: '3xs' }} $size="sm">
|
||||
{group.emptyString}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
@@ -53,7 +53,7 @@ export const QuickSearchInput = ({
|
||||
$padding={{ horizontal: 'base', vertical: 'sm' }}
|
||||
>
|
||||
{!loading && (
|
||||
<Icon iconName="search" $variation="600" aria-hidden="true" />
|
||||
<Icon iconName="search" $variation="secondary" aria-hidden="true" />
|
||||
)}
|
||||
{loading && (
|
||||
<div>
|
||||
|
||||
@@ -19,25 +19,25 @@ export const QuickSearchStyle = createGlobalStyle`
|
||||
border: none;
|
||||
width: 100%;
|
||||
font-size: 17px;
|
||||
padding: 8px;
|
||||
padding: var(--c--globals--spacings--xs);
|
||||
background: white;
|
||||
outline: none;
|
||||
color: var(--c--theme--colors--greyscale-1000);
|
||||
border-radius: 0;
|
||||
color: var(--c--globals--colors--gray-1000);
|
||||
border-radius: var(--c--globals--spacings--0);
|
||||
|
||||
&::placeholder {
|
||||
color: var(--c--theme--colors--greyscale-500);
|
||||
color: var(--c--globals--colors--gray-500);
|
||||
}
|
||||
}
|
||||
|
||||
[cmdk-item] {
|
||||
content-visibility: auto;
|
||||
cursor: pointer;
|
||||
border-radius: var(--c--theme--spacings--xs);
|
||||
font-size: 14px;
|
||||
border-radius: var(--c--globals--spacings--xs);
|
||||
font-size: var(--c--globals--font--sizes--sm);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: var(--c--globals--spacings--xs);
|
||||
user-select: none;
|
||||
will-change: background, color;
|
||||
transition: all 150ms ease;
|
||||
@@ -49,19 +49,19 @@ export const QuickSearchStyle = createGlobalStyle`
|
||||
|
||||
&:hover,
|
||||
&[data-selected='true'] {
|
||||
background: var(--c--theme--colors--greyscale-100);
|
||||
background: var(--c--contextuals--background--semantic--contextual--primary);
|
||||
.show-right-on-focus {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-disabled='true'] {
|
||||
color: var(--c--theme--colors--greyscale-500);
|
||||
color: var(--c--globals--colors--gray-500);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
& + [cmdk-item] {
|
||||
margin-top: 4px;
|
||||
margin-top: var(--c--globals--spacings--st);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,11 +79,11 @@ export const QuickSearchStyle = createGlobalStyle`
|
||||
kbd {
|
||||
font-size: 12px;
|
||||
min-width: 20px;
|
||||
padding: 4px;
|
||||
padding: var(--c--globals--spacings--st);
|
||||
height: 20px;
|
||||
border-radius: 4px;
|
||||
border-radius: var(--c--globals--spacings--st);
|
||||
color: white;
|
||||
background: var(--c--theme--colors--greyscale-500);
|
||||
background: var(--c--globals--colors--gray-500);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -94,23 +94,23 @@ export const QuickSearchStyle = createGlobalStyle`
|
||||
[cmdk-separator] {
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
background: var(--c--theme--colors--greyscale-500);
|
||||
margin: 4px 0;
|
||||
background: var(--c--globals--colors--gray-500);
|
||||
margin: var(--c--globals--spacings--st) 0;
|
||||
}
|
||||
|
||||
*:not([hidden]) + [cmdk-group] {
|
||||
margin-top: 8px;
|
||||
margin-top: var(--c--globals--spacings--xs);
|
||||
}
|
||||
|
||||
[cmdk-group-heading] {
|
||||
user-select: none;
|
||||
font-size: var(--c--theme--font--sizes--sm);
|
||||
color: var(--c--theme--colors--greyscale-700);
|
||||
font-size: var(--c--globals--font--sizes--sm);
|
||||
color: var(--c--globals--colors--gray-700);
|
||||
font-weight: bold;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: var(--c--theme--spacings--xs);
|
||||
margin-bottom: var(--c--globals--spacings--xs);
|
||||
}
|
||||
|
||||
[cmdk-empty] {
|
||||
@@ -128,9 +128,9 @@ export const QuickSearchStyle = createGlobalStyle`
|
||||
}
|
||||
|
||||
.c__modal__title {
|
||||
font-size: var(--c--theme--font--sizes--xs);
|
||||
padding: var(--c--theme--spacings--base);
|
||||
margin-bottom: 0;
|
||||
font-size: var(--c--globals--font--sizes--xs);
|
||||
padding: var(--c--globals--spacings--base);
|
||||
margin-bottom: var(--c--globals--spacings--0);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,26 +1,16 @@
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { Spacings } from '@/utils';
|
||||
|
||||
import { Box } from '../Box';
|
||||
|
||||
export enum SeparatorVariant {
|
||||
LIGHT = 'light',
|
||||
DARK = 'dark',
|
||||
}
|
||||
|
||||
type Props = {
|
||||
variant?: SeparatorVariant;
|
||||
$withPadding?: boolean;
|
||||
customPadding?: Spacings;
|
||||
};
|
||||
|
||||
export const HorizontalSeparator = ({
|
||||
variant = SeparatorVariant.LIGHT,
|
||||
$withPadding = true,
|
||||
customPadding,
|
||||
}: Props) => {
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
|
||||
const padding = $withPadding
|
||||
? (customPadding ?? 'base')
|
||||
: ('none' as Spacings);
|
||||
@@ -30,11 +20,7 @@ export const HorizontalSeparator = ({
|
||||
$height="1px"
|
||||
$width="100%"
|
||||
$margin={{ vertical: padding }}
|
||||
$background={
|
||||
variant === SeparatorVariant.DARK
|
||||
? '#e5e5e533'
|
||||
: colorsTokens['greyscale-100']
|
||||
}
|
||||
$background="var(--c--contextuals--border--surface--primary)"
|
||||
className="--docs--horizontal-separator"
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -13,7 +13,8 @@ export const SeparatedSection = ({
|
||||
showSeparator = true,
|
||||
children,
|
||||
}: PropsWithChildren<Props>) => {
|
||||
const { colorsTokens, spacingsTokens } = useCunninghamTheme();
|
||||
const { spacingsTokens } = useCunninghamTheme();
|
||||
|
||||
return (
|
||||
<Box
|
||||
$css={css`
|
||||
@@ -21,7 +22,8 @@ export const SeparatedSection = ({
|
||||
padding: ${spacingsTokens['sm']} 0;
|
||||
${showSeparator &&
|
||||
css`
|
||||
border-bottom: 1px solid ${colorsTokens['greyscale-200']};
|
||||
border-bottom: 1px solid
|
||||
var(--c--contextuals--border--surface--primary);
|
||||
`}
|
||||
`}
|
||||
>
|
||||
|
||||
@@ -2,13 +2,13 @@ import { useCunninghamTheme } from '../useCunninghamTheme';
|
||||
|
||||
describe('<useCunninghamTheme />', () => {
|
||||
it('has the logo correctly set', () => {
|
||||
expect(useCunninghamTheme.getState().themeTokens.logo?.src).toBe('');
|
||||
expect(useCunninghamTheme.getState().componentTokens.logo?.src).toBe('');
|
||||
|
||||
// Change theme
|
||||
useCunninghamTheme.getState().setTheme('dsfr');
|
||||
|
||||
const { themeTokens } = useCunninghamTheme.getState();
|
||||
const logo = themeTokens.logo;
|
||||
const { componentTokens } = useCunninghamTheme.getState();
|
||||
const logo = componentTokens.logo;
|
||||
expect(logo?.src).toBe('/assets/logo-gouv.svg');
|
||||
expect(logo?.widthHeader).toBe('110px');
|
||||
expect(logo?.widthFooter).toBe('220px');
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
* Select
|
||||
**/
|
||||
--c--components--forms-select--value-color--disabled: var(
|
||||
--c--theme--colors--greyscale-400
|
||||
--c--globals--colors--gray-400
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -44,6 +44,14 @@
|
||||
contain: content;
|
||||
}
|
||||
|
||||
.c__button:disabled {
|
||||
cursor: unset;
|
||||
}
|
||||
|
||||
.c__button:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.c__button--medium {
|
||||
min-height: var(--c--components--button--medium-height);
|
||||
height: auto;
|
||||
@@ -60,14 +68,11 @@
|
||||
* Tooltip
|
||||
*/
|
||||
.c__tooltip {
|
||||
padding: 4px 6px;
|
||||
padding: var(--c--globals--font--sizes--sm) var(--c--globals--spacings--xxs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Image System
|
||||
*/
|
||||
.c__image-system-filter {
|
||||
filter: var(--c--components--image-system-filter);
|
||||
.c__tooltip .react-aria-OverlayArrow svg {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,21 +5,23 @@ import { tokens } from './cunningham-tokens';
|
||||
|
||||
type Tokens = typeof tokens.themes.default &
|
||||
Partial<(typeof tokens.themes)[keyof typeof tokens.themes]>;
|
||||
type ColorsTokens = Tokens['theme']['colors'];
|
||||
type FontSizesTokens = Tokens['theme']['font']['sizes'];
|
||||
type SpacingsTokens = Tokens['theme']['spacings'];
|
||||
type ColorsTokens = Tokens['globals']['colors'];
|
||||
type FontSizesTokens = Tokens['globals']['font']['sizes'];
|
||||
type SpacingsTokens = Tokens['globals']['spacings'];
|
||||
type ComponentTokens = Tokens['components'];
|
||||
type ContextualTokens = Tokens['contextuals'];
|
||||
export type Theme = keyof typeof tokens.themes;
|
||||
|
||||
interface ThemeStore {
|
||||
colorsTokens: Partial<ColorsTokens>;
|
||||
componentTokens: ComponentTokens;
|
||||
contextualTokens: ContextualTokens;
|
||||
currentTokens: Partial<Tokens>;
|
||||
fontSizesTokens: Partial<FontSizesTokens>;
|
||||
setTheme: (theme: Theme) => void;
|
||||
spacingsTokens: Partial<SpacingsTokens>;
|
||||
theme: Theme;
|
||||
themeTokens: Partial<Tokens['theme']>;
|
||||
themeTokens: Partial<Tokens['globals']>;
|
||||
}
|
||||
|
||||
const getMergedTokens = (theme: Theme) => {
|
||||
@@ -30,14 +32,15 @@ const DEFAULT_THEME: Theme = 'generic';
|
||||
const defaultTokens = getMergedTokens(DEFAULT_THEME);
|
||||
|
||||
const initialState: ThemeStore = {
|
||||
colorsTokens: defaultTokens.theme.colors,
|
||||
colorsTokens: defaultTokens.globals.colors,
|
||||
componentTokens: defaultTokens.components,
|
||||
contextualTokens: defaultTokens.contextuals,
|
||||
currentTokens: tokens.themes[DEFAULT_THEME] as Partial<Tokens>,
|
||||
fontSizesTokens: defaultTokens.theme.font.sizes,
|
||||
fontSizesTokens: defaultTokens.globals.font.sizes,
|
||||
setTheme: () => {},
|
||||
spacingsTokens: defaultTokens.theme.spacings,
|
||||
spacingsTokens: defaultTokens.globals.spacings,
|
||||
theme: DEFAULT_THEME,
|
||||
themeTokens: defaultTokens.theme,
|
||||
themeTokens: defaultTokens.globals,
|
||||
};
|
||||
|
||||
export const useCunninghamTheme = create<ThemeStore>((set) => ({
|
||||
@@ -46,13 +49,14 @@ export const useCunninghamTheme = create<ThemeStore>((set) => ({
|
||||
const newTokens = getMergedTokens(theme);
|
||||
|
||||
set({
|
||||
colorsTokens: newTokens.theme.colors,
|
||||
colorsTokens: newTokens.globals.colors,
|
||||
componentTokens: newTokens.components,
|
||||
contextualTokens: newTokens.contextuals,
|
||||
currentTokens: tokens.themes[theme] as Partial<Tokens>,
|
||||
fontSizesTokens: newTokens.theme.font.sizes,
|
||||
spacingsTokens: newTokens.theme.spacings,
|
||||
fontSizesTokens: newTokens.globals.font.sizes,
|
||||
spacingsTokens: newTokens.globals.spacings,
|
||||
theme,
|
||||
themeTokens: newTokens.theme,
|
||||
themeTokens: newTokens.globals,
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -18,7 +18,8 @@ export const ButtonLogin = () => {
|
||||
return (
|
||||
<Button
|
||||
onClick={() => gotoLogin()}
|
||||
color="primary-text"
|
||||
color="brand"
|
||||
variant="tertiary"
|
||||
aria-label={t('Login')}
|
||||
className="--docs--button-login"
|
||||
>
|
||||
@@ -31,14 +32,15 @@ export const ButtonLogin = () => {
|
||||
<Box
|
||||
$css={css`
|
||||
.--docs--button-logout:focus-visible {
|
||||
box-shadow: 0 0 0 2px ${colorsTokens['primary-400']} !important;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0 0 2px ${colorsTokens['brand-400']} !important;
|
||||
border-radius: var(--c--globals--spacings--st);
|
||||
}
|
||||
`}
|
||||
>
|
||||
<Button
|
||||
onClick={gotoLogout}
|
||||
color="primary-text"
|
||||
color="brand"
|
||||
variant="tertiary"
|
||||
aria-label={t('Logout')}
|
||||
className="--docs--button-logout"
|
||||
>
|
||||
@@ -56,9 +58,13 @@ export const ProConnectButton = () => {
|
||||
onClick={() => gotoLogin()}
|
||||
aria-label={t('Proconnect Login')}
|
||||
$css={css`
|
||||
background-color: var(--c--theme--colors--primary-text);
|
||||
background-color: var(
|
||||
--c--contextuals--background--semantic--brand--primary
|
||||
);
|
||||
&:hover {
|
||||
background-color: var(--c--theme--colors--primary-action);
|
||||
background-color: var(
|
||||
--c--contextuals--background--semantic--brand--primary-hover
|
||||
);
|
||||
}
|
||||
`}
|
||||
$radius="4px"
|
||||
|
||||
@@ -4,15 +4,12 @@ import { tokens } from '@/cunningham';
|
||||
|
||||
import { AvatarSvg } from './AvatarSvg';
|
||||
|
||||
const colors = tokens.themes.default.theme.colors;
|
||||
const colors = tokens.themes.default.globals.colors;
|
||||
|
||||
const avatarsColors = [
|
||||
colors['blue-500'],
|
||||
colors['blue-1-500'],
|
||||
colors['brown-500'],
|
||||
colors['cyan-500'],
|
||||
colors['gold-500'],
|
||||
colors['green-500'],
|
||||
colors['olive-500'],
|
||||
colors['orange-500'],
|
||||
colors['pink-500'],
|
||||
colors['purple-500'],
|
||||
|
||||
@@ -289,11 +289,13 @@ export const BlockNoteReader = ({
|
||||
editor={editor}
|
||||
editable={false}
|
||||
theme="light"
|
||||
aria-label={t('Document version viewer')}
|
||||
aria-label={t('Document viewer')}
|
||||
formattingToolbar={false}
|
||||
slashMenu={false}
|
||||
comments={false}
|
||||
/>
|
||||
>
|
||||
<BlockNoteToolbar />
|
||||
</BlockNoteView>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -91,7 +91,7 @@ export function MarkdownButton() {
|
||||
<Text
|
||||
aria-hidden={true}
|
||||
$css={css`
|
||||
font-family: var(--c--theme--font--families--base);
|
||||
font-family: var(--c--globals--font--families--base);
|
||||
`}
|
||||
$weight="bold"
|
||||
>
|
||||
|
||||
@@ -24,14 +24,14 @@ export const ModalConfirmDownloadUnsafe = ({
|
||||
<>
|
||||
<Button
|
||||
aria-label={t('Cancel the download')}
|
||||
color="secondary"
|
||||
variant="secondary"
|
||||
onClick={() => onClose()}
|
||||
>
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
aria-label={t('Download')}
|
||||
color="danger"
|
||||
color="error"
|
||||
data-testid="modal-download-unsafe-button"
|
||||
onClick={() => {
|
||||
if (onConfirm) {
|
||||
@@ -52,7 +52,6 @@ export const ModalConfirmDownloadUnsafe = ({
|
||||
$gap="0.7rem"
|
||||
$size="h6"
|
||||
$align="flex-start"
|
||||
$variation="1000"
|
||||
$direction="row"
|
||||
$margin="0"
|
||||
>
|
||||
@@ -67,8 +66,10 @@ export const ModalConfirmDownloadUnsafe = ({
|
||||
>
|
||||
<Box>
|
||||
<Box $direction="column" $gap="0.35rem" $margin={{ top: 'sm' }}>
|
||||
<Text $variation="700">{t('This file is flagged as unsafe.')}</Text>
|
||||
<Text $variation="600">
|
||||
<Text $variation="secondary">
|
||||
{t('This file is flagged as unsafe.')}
|
||||
</Text>
|
||||
<Text $variation="secondary">
|
||||
{t('Please download it only if it comes from a trusted source.')}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { useBlockNoteEditor, useComponentsContext } from '@blocknote/react';
|
||||
import {
|
||||
useBlockNoteEditor,
|
||||
useComponentsContext,
|
||||
useSelectedBlocks,
|
||||
} from '@blocknote/react';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
@@ -24,7 +29,18 @@ export const CommentToolbarButton = () => {
|
||||
DocsStyleSchema
|
||||
>();
|
||||
|
||||
if (!editor.isEditable || !Components || !currentDoc?.abilities.comment) {
|
||||
const selectedBlocks = useSelectedBlocks(editor);
|
||||
|
||||
const show = useMemo(() => {
|
||||
return !!selectedBlocks.find((block) => block.content !== undefined);
|
||||
}, [selectedBlocks]);
|
||||
|
||||
if (
|
||||
!show ||
|
||||
!editor.isEditable ||
|
||||
!Components ||
|
||||
!currentDoc?.abilities.comment
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -34,8 +50,10 @@ export const CommentToolbarButton = () => {
|
||||
className="bn-button"
|
||||
onClick={() => {
|
||||
editor.comments?.startPendingComment();
|
||||
editor.formattingToolbar.closeMenu();
|
||||
}}
|
||||
aria-haspopup="dialog"
|
||||
data-test="comment-toolbar-button"
|
||||
>
|
||||
<Box
|
||||
$direction="row"
|
||||
@@ -46,20 +64,18 @@ export const CommentToolbarButton = () => {
|
||||
<Icon
|
||||
iconName="comment"
|
||||
className="--docs--icon-bg"
|
||||
$theme="greyscale"
|
||||
$variation="600"
|
||||
$theme="gray"
|
||||
$padding="0.15rem"
|
||||
$size="16px"
|
||||
$color={colorsTokens['greyscale-600']}
|
||||
$size="md"
|
||||
/>
|
||||
{t('Comment')}
|
||||
</Box>
|
||||
</Components.Generic.Toolbar.Button>
|
||||
<Box
|
||||
$background={colorsTokens['greyscale-100']}
|
||||
$background={colorsTokens['gray-100']}
|
||||
$width="1px"
|
||||
$height="70%"
|
||||
$margin={{ left: '2px' }}
|
||||
$margin={{ left: 'var(--c--globals--spacings--4xs)' }}
|
||||
$css={css`
|
||||
align-self: center;
|
||||
`}
|
||||
|
||||
@@ -11,7 +11,7 @@ export const cssComments = (
|
||||
.bn-thread-mark:not([data-orphan='true']),
|
||||
.bn-thread-mark-selected:not([data-orphan='true']) {
|
||||
background: ${canSeeComment ? '#EDB40066' : 'transparent'};
|
||||
color: var(--c--theme--colors--greyscale-700);
|
||||
color: var(--c--globals--colors--greyscale-700);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ export const cssComments = (
|
||||
max-height: 500px;
|
||||
|
||||
.bn-default-styles {
|
||||
font-family: var(--c--theme--font--families--base);
|
||||
font-family: var(--c--globals--font--families--base);
|
||||
}
|
||||
|
||||
.bn-block {
|
||||
@@ -52,21 +52,22 @@ export const cssComments = (
|
||||
padding: 8px;
|
||||
|
||||
& .bn-editor {
|
||||
padding-left: 32px;
|
||||
padding-left: var(--c--globals--spacings--lg);
|
||||
.bn-inline-content {
|
||||
color: var(--c--theme--colors--greyscale-700);
|
||||
color: var(--c--globals--colors--greyscale-700);
|
||||
}
|
||||
}
|
||||
|
||||
// Emoji
|
||||
& .bn-badge-group {
|
||||
padding-left: 32px;
|
||||
padding-left: var(--c--globals--spacings--lg);
|
||||
.bn-badge label {
|
||||
padding: 0 4px;
|
||||
padding: var(--c--globals--spacings--0)
|
||||
var(--c--globals--spacings--st);
|
||||
background: none;
|
||||
border: 1px solid var(--c--theme--colors--greyscale-300);
|
||||
border-radius: 4px;
|
||||
height: 24px;
|
||||
border: 1px solid var(--c--globals--colors--greyscale-300);
|
||||
border-radius: var(--c--globals--spacings--st);
|
||||
height: var(--c--globals--spacings--md);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,17 +103,17 @@ export const cssComments = (
|
||||
background-color: transparent;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--c--theme--colors--greyscale-100);
|
||||
background-color: var(--c--globals--colors--greyscale-100);
|
||||
}
|
||||
}
|
||||
|
||||
button[role='menuitem'] svg {
|
||||
color: var(--c--theme--colors--greyscale-600);
|
||||
color: var(--c--globals--colors--greyscale-600);
|
||||
}
|
||||
}
|
||||
|
||||
& svg {
|
||||
color: var(--c--theme--colors--info-600);
|
||||
color: var(--c--globals--colors--info-600);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,19 +126,19 @@ export const cssComments = (
|
||||
gap: 0.4rem !important;
|
||||
|
||||
& > button {
|
||||
height: 24px;
|
||||
padding-inline: 4px;
|
||||
height: var(--c--globals--spacings--md);
|
||||
padding-inline: var(--c--globals--spacings--st);
|
||||
|
||||
&[data-test='save'] {
|
||||
border: 1px solid var(--c--theme--colors--info-600);
|
||||
background: var(--c--theme--colors--info-600);
|
||||
border: 1px solid var(--c--globals--colors--info-600);
|
||||
background: var(--c--globals--colors--info-600);
|
||||
color: white;
|
||||
}
|
||||
|
||||
&[data-test='cancel'] {
|
||||
background: white;
|
||||
border: 1px solid var(--c--theme--colors--greyscale-300);
|
||||
color: var(--c--theme--colors--info-600);
|
||||
border: 1px solid var(--c--globals--colors--greyscale-300);
|
||||
color: var(--c--globals--colors--info-600);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -179,19 +180,19 @@ export const cssComments = (
|
||||
|
||||
button {
|
||||
font-size: 0;
|
||||
background: var(--c--theme--colors--info-600);
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
background: var(--c--globals--colors--info-600);
|
||||
width: var(--c--globals--spacings--md);
|
||||
height: var(--c--globals--spacings--md);
|
||||
padding: var(--c--globals--spacings--0);
|
||||
|
||||
&:disabled {
|
||||
background: var(--c--theme--colors--greyscale-300);
|
||||
background: var(--c--globals--colors--greyscale-300);
|
||||
}
|
||||
|
||||
& .mantine-Button-label::before {
|
||||
content: '🡡';
|
||||
font-size: 13px;
|
||||
color: var(--c--theme--colors--greyscale-100);
|
||||
color: var(--c--globals--colors--greyscale-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ import emojidata from './initEmojiCallout';
|
||||
|
||||
const CalloutBlockStyle = createGlobalStyle`
|
||||
.bn-block-content[data-content-type="callout"][data-background-color] {
|
||||
padding: var(--c--theme--spacings--3xs) var(--c--theme--spacings--3xs);
|
||||
border-radius: var(--c--theme--spacings--3xs);
|
||||
padding: var(--c--globals--spacings--3xs) var(--c--globals--spacings--3xs);
|
||||
border-radius: var(--c--globals--spacings--3xs);
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -59,9 +59,15 @@ const UploadLoaderBlockComponent = ({
|
||||
editor,
|
||||
}: UploadLoaderBlockComponentProps) => {
|
||||
const mediaUrl = useMediaUrl();
|
||||
const isEditable = editor.isEditable;
|
||||
|
||||
useEffect(() => {
|
||||
if (!block.props.blockUploadUrl || block.props.type !== 'loading') {
|
||||
const shouldCheckStatus =
|
||||
block.props.blockUploadUrl &&
|
||||
block.props.type === 'loading' &&
|
||||
isEditable;
|
||||
|
||||
if (!shouldCheckStatus) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -69,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);
|
||||
@@ -108,7 +118,7 @@ const UploadLoaderBlockComponent = ({
|
||||
/* During collaboration, another user might have updated the block */
|
||||
}
|
||||
});
|
||||
}, [block, editor, mediaUrl]);
|
||||
}, [block, editor, mediaUrl, isEditable]);
|
||||
|
||||
return (
|
||||
<Box className="bn-visual-media-wrapper" $direction="row" $gap="0.5rem">
|
||||
|
||||
@@ -26,14 +26,19 @@ export const InterlinkingLinkInlineContent = createReactInlineContentSpec(
|
||||
content: 'none',
|
||||
},
|
||||
{
|
||||
render: ({ inlineContent, updateInlineContent }) => {
|
||||
render: ({ editor, inlineContent, updateInlineContent }) => {
|
||||
const { data: doc } = useDoc({ id: inlineContent.props.docId });
|
||||
const isEditable = editor.isEditable;
|
||||
|
||||
/**
|
||||
* Update the content title if the referenced doc title changes
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (doc?.title && doc.title !== inlineContent.props.title) {
|
||||
if (
|
||||
isEditable &&
|
||||
doc?.title &&
|
||||
doc.title !== inlineContent.props.title
|
||||
) {
|
||||
updateInlineContent({
|
||||
type: 'interlinkingLinkInline',
|
||||
props: {
|
||||
@@ -50,7 +55,7 @@ export const InterlinkingLinkInlineContent = createReactInlineContentSpec(
|
||||
* not when inlineContent.props.title changes.
|
||||
*/
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [doc?.title]);
|
||||
}, [doc?.title, isEditable]);
|
||||
|
||||
return <LinkSelected {...inlineContent.props} />;
|
||||
},
|
||||
@@ -86,9 +91,10 @@ const LinkSelected = ({ url, title }: LinkSelectedProps) => {
|
||||
margin-right: 0.2rem;
|
||||
}
|
||||
&:hover {
|
||||
background-color: ${colorsTokens['greyscale-100']};
|
||||
background-color: ${colorsTokens['gray-100']};
|
||||
}
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
transition: background-color var(--c--globals--transitions--duration)
|
||||
var(--c--globals--transitions--ease-out);
|
||||
|
||||
.--docs--doc-deleted & {
|
||||
pointer-events: none;
|
||||
@@ -98,7 +104,7 @@ const LinkSelected = ({ url, title }: LinkSelectedProps) => {
|
||||
{emoji ? (
|
||||
<Text $size="16px">{emoji}</Text>
|
||||
) : (
|
||||
<SelectedPageIcon width={11.5} color={colorsTokens['primary-400']} />
|
||||
<SelectedPageIcon width={11.5} color={colorsTokens['brand-400']} />
|
||||
)}
|
||||
<Text
|
||||
$weight="500"
|
||||
|
||||
@@ -35,11 +35,11 @@ import { DocSearchSubPageContent, DocSearchTarget } from '@/docs/doc-search';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
const inputStyle = css`
|
||||
background-color: var(--c--theme--colors--greyscale-100);
|
||||
background-color: var(--c--globals--colors--gray-100);
|
||||
border: none;
|
||||
outline: none;
|
||||
color: var(--c--theme--colors--greyscale-700);
|
||||
font-size: 16px;
|
||||
color: var(--c--globals--colors--gray-700);
|
||||
font-size: var(--c--globals--font--sizes--md);
|
||||
width: 100%;
|
||||
font-family: 'Inter';
|
||||
`;
|
||||
@@ -86,6 +86,7 @@ export const SearchPage = ({
|
||||
const [search, setSearch] = useState('');
|
||||
const { isDesktop } = useResponsiveStore();
|
||||
const { untitledDocument } = useTrans();
|
||||
const isEditable = editor.isEditable;
|
||||
|
||||
/**
|
||||
* createReactInlineContentSpec add automatically the focus after
|
||||
@@ -101,6 +102,10 @@ export const SearchPage = ({
|
||||
}, [inputRef]);
|
||||
|
||||
const closeSearch = (insertContent: string) => {
|
||||
if (!isEditable) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateInlineContent({
|
||||
type: 'interlinkingSearchInline',
|
||||
props: {
|
||||
@@ -154,8 +159,8 @@ export const SearchPage = ({
|
||||
<Box
|
||||
as="span"
|
||||
className="inline-content"
|
||||
$background={colorsTokens['greyscale-100']}
|
||||
$color="var(--c--theme--colors--greyscale-700)"
|
||||
$background={colorsTokens['gray-100']}
|
||||
$color="var(--c--globals--colors--gray-700)"
|
||||
$direction="row"
|
||||
$radius="3px"
|
||||
$padding="1px"
|
||||
@@ -193,9 +198,9 @@ export const SearchPage = ({
|
||||
<QuickSearch showInput={false}>
|
||||
<Card
|
||||
$css={css`
|
||||
box-shadow: 0 0 3px 0px var(--c--theme--colors--greyscale-200);
|
||||
box-shadow: 0 0 3px 0px var(--c--globals--colors--gray-200);
|
||||
& > div {
|
||||
margin-top: 0;
|
||||
margin-top: var(--c--globals--spacings--0);
|
||||
& [cmdk-group-heading] {
|
||||
padding: 0.4rem;
|
||||
margin: 0;
|
||||
@@ -223,6 +228,10 @@ export const SearchPage = ({
|
||||
search={search}
|
||||
filters={{ target: DocSearchTarget.CURRENT }}
|
||||
onSelect={(doc) => {
|
||||
if (!isEditable) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateInlineContent({
|
||||
type: 'interlinkingSearchInline',
|
||||
props: {
|
||||
@@ -278,8 +287,8 @@ export const SearchPage = ({
|
||||
</Box>
|
||||
|
||||
<Text
|
||||
$size="14px"
|
||||
$color="var(--c--theme--colors--greyscale-1000)"
|
||||
$size="sm"
|
||||
$color="var(--c--globals--colors--gray-1000)"
|
||||
spellCheck="false"
|
||||
>
|
||||
{titleWithoutEmoji}
|
||||
@@ -287,11 +296,7 @@ export const SearchPage = ({
|
||||
</Box>
|
||||
}
|
||||
right={
|
||||
<Icon
|
||||
iconName="keyboard_return"
|
||||
$variation="600"
|
||||
spellCheck="false"
|
||||
/>
|
||||
<Icon iconName="keyboard_return" spellCheck="false" />
|
||||
}
|
||||
/>
|
||||
);
|
||||
@@ -308,7 +313,7 @@ export const SearchPage = ({
|
||||
<Box
|
||||
$css={css`
|
||||
border-top: 1px solid
|
||||
var(--c--theme--colors--greyscale-200);
|
||||
var(--c--globals--colors--gray-200);
|
||||
`}
|
||||
$width="100%"
|
||||
>
|
||||
@@ -323,15 +328,15 @@ export const SearchPage = ({
|
||||
$css={css`
|
||||
&:hover {
|
||||
background-color: var(
|
||||
--c--theme--colors--greyscale-100
|
||||
--c--globals--colors--gray-100
|
||||
);
|
||||
}
|
||||
`}
|
||||
>
|
||||
<AddPageIcon />
|
||||
<Text
|
||||
$size="14px"
|
||||
$color="var(--c--theme--colors--greyscale-1000)"
|
||||
$size="sm"
|
||||
$color="var(--c--globals--colors--gray-1000)"
|
||||
contentEditable={false}
|
||||
>
|
||||
{t('New sub-doc')}
|
||||
|
||||
@@ -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]);
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ export const cssEditor = css`
|
||||
}
|
||||
|
||||
& .bn-editor {
|
||||
color: var(--c--theme--colors--greyscale-700);
|
||||
color: var(--c--globals--colors--greyscale-700);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -104,14 +104,14 @@ export const cssEditor = css`
|
||||
* Callout, Paragraph and Heading blocks
|
||||
*/
|
||||
.bn-block {
|
||||
border-radius: var(--c--theme--spacings--3xs);
|
||||
border-radius: var(--c--globals--spacings--3xs);
|
||||
}
|
||||
.bn-block-outer {
|
||||
border-radius: var(--c--theme--spacings--3xs);
|
||||
border-radius: var(--c--globals--spacings--3xs);
|
||||
}
|
||||
.bn-block > .bn-block-content[data-background-color] {
|
||||
padding: var(--c--theme--spacings--3xs) var(--c--theme--spacings--3xs);
|
||||
border-radius: var(--c--theme--spacings--3xs);
|
||||
padding: var(--c--globals--spacings--3xs) var(--c--globals--spacings--3xs);
|
||||
border-radius: var(--c--globals--spacings--3xs);
|
||||
}
|
||||
.bn-block-content[data-content-type='checkListItem'][data-checked='true']
|
||||
.bn-inline-content {
|
||||
@@ -127,7 +127,7 @@ export const cssEditor = css`
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
a {
|
||||
color: var(--c--theme--colors--greyscale-600);
|
||||
color: var(--c--globals--colors--greyscale-600);
|
||||
cursor: pointer;
|
||||
}
|
||||
.bn-block-group
|
||||
@@ -144,7 +144,7 @@ export const cssEditor = css`
|
||||
* Quotes
|
||||
*/
|
||||
blockquote {
|
||||
border-left: 4px solid var(--c--theme--colors--greyscale-300);
|
||||
border-left: 4px solid var(--c--globals--colors--gray-300);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
|
||||
@@ -174,7 +174,7 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
|
||||
<>
|
||||
<Button
|
||||
aria-label={t('Cancel the download')}
|
||||
color="secondary"
|
||||
variant="secondary"
|
||||
fullWidth
|
||||
onClick={() => onClose()}
|
||||
>
|
||||
@@ -183,7 +183,7 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
|
||||
<Button
|
||||
data-testid="doc-export-download-button"
|
||||
aria-label={t('Download')}
|
||||
color="primary"
|
||||
variant="primary"
|
||||
fullWidth
|
||||
onClick={() => void onSubmit()}
|
||||
disabled={isExporting}
|
||||
@@ -205,7 +205,6 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
|
||||
$margin="0"
|
||||
id="modal-export-title"
|
||||
$size="h6"
|
||||
$variation="1000"
|
||||
$align="flex-start"
|
||||
data-testid="modal-export-title"
|
||||
>
|
||||
@@ -225,7 +224,7 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
|
||||
$gap="1rem"
|
||||
className="--docs--modal-export-content"
|
||||
>
|
||||
<Text $variation="600" $size="sm" as="p">
|
||||
<Text $variation="secondary" $size="sm" as="p">
|
||||
{t('Download your document in a .docx, .odt or .pdf format.')}
|
||||
</Text>
|
||||
<Select
|
||||
|
||||
@@ -2,36 +2,37 @@ import { Button, Modal, ModalSize } from '@openfun/cunningham-react';
|
||||
import { t } from 'i18next';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, BoxButton, Icon, Text } from '@/components';
|
||||
import { Box, BoxButton, Card, Icon, Text } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
|
||||
export const AlertNetwork = () => {
|
||||
const { t } = useTranslation();
|
||||
const { colorsTokens, spacingsTokens } = useCunninghamTheme();
|
||||
const { spacingsTokens } = useCunninghamTheme();
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box>
|
||||
<Box
|
||||
<Card
|
||||
$direction="row"
|
||||
$justify="space-between"
|
||||
$width="100%"
|
||||
$background={colorsTokens['warning-100']}
|
||||
$radius={spacingsTokens['3xs']}
|
||||
$padding="xs"
|
||||
$flex={1}
|
||||
$align="center"
|
||||
$gap={spacingsTokens['2xs']}
|
||||
$css={css`
|
||||
border: 1px solid var(--c--theme--colors--warning-300);
|
||||
`}
|
||||
$theme="warning"
|
||||
>
|
||||
<Box $direction="row" $gap={spacingsTokens['2xs']} $align="center">
|
||||
<Icon iconName="mobiledata_off" $theme="warning" $variation="600" />
|
||||
<Text $theme="warning" $variation="600" $weight={500}>
|
||||
<Box
|
||||
$direction="row"
|
||||
$gap={spacingsTokens['2xs']}
|
||||
$align="center"
|
||||
$withThemeInherited
|
||||
>
|
||||
<Icon iconName="mobiledata_off" $withThemeInherited />
|
||||
<Text $withThemeInherited $weight={500}>
|
||||
{t('Others are editing. Your network prevent changes.')}
|
||||
</Text>
|
||||
</Box>
|
||||
@@ -40,20 +41,20 @@ export const AlertNetwork = () => {
|
||||
$gap={spacingsTokens['3xs']}
|
||||
$align="center"
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
$withThemeInherited
|
||||
>
|
||||
<Icon
|
||||
iconName="info"
|
||||
$theme="warning"
|
||||
$variation="600"
|
||||
$size="16px"
|
||||
$withThemeInherited
|
||||
$size="md"
|
||||
$weight="500"
|
||||
$margin={{ top: 'auto' }}
|
||||
/>
|
||||
<Text $theme="warning" $variation="600" $weight="500" $size="xs">
|
||||
<Text $withThemeInherited $weight="500" $size="xs">
|
||||
{t('Learn more')}
|
||||
</Text>
|
||||
</BoxButton>
|
||||
</Box>
|
||||
</Card>
|
||||
</Box>
|
||||
{isModalOpen && (
|
||||
<AlertNetworkModal onClose={() => setIsModalOpen(false)} />
|
||||
@@ -74,20 +75,14 @@ export const AlertNetworkModal = ({ onClose }: AlertNetworkModalProps) => {
|
||||
onClose={() => onClose()}
|
||||
rightActions={
|
||||
<>
|
||||
<Button aria-label={t('OK')} onClick={onClose} color="danger">
|
||||
<Button aria-label={t('OK')} onClick={onClose} color="error">
|
||||
{t('I understand')}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
size={ModalSize.MEDIUM}
|
||||
title={
|
||||
<Text
|
||||
$size="h6"
|
||||
as="h6"
|
||||
$margin={{ all: '0' }}
|
||||
$align="flex-start"
|
||||
$variation="1000"
|
||||
>
|
||||
<Text $size="h6" as="h6" $margin={{ all: '0' }} $align="flex-start">
|
||||
{t("Why you can't edit the document?")}
|
||||
</Text>
|
||||
}
|
||||
@@ -97,14 +92,14 @@ export const AlertNetworkModal = ({ onClose }: AlertNetworkModalProps) => {
|
||||
className="--docs--modal-alert-network"
|
||||
$margin={{ top: 'md' }}
|
||||
>
|
||||
<Text $size="sm" $variation="600">
|
||||
<Text $size="sm" $variation="secondary">
|
||||
{t(
|
||||
'Others are editing this document. Unfortunately your network blocks WebSockets, the technology enabling real-time co-editing.',
|
||||
)}
|
||||
</Text>
|
||||
<Text
|
||||
$size="sm"
|
||||
$variation="600"
|
||||
$variation="secondary"
|
||||
$margin={{ top: 'xs' }}
|
||||
$weight="bold"
|
||||
$display="inline"
|
||||
@@ -112,7 +107,7 @@ export const AlertNetworkModal = ({ onClose }: AlertNetworkModalProps) => {
|
||||
{t("This means you can't edit until others leave.")}{' '}
|
||||
<Text
|
||||
$size="sm"
|
||||
$variation="600"
|
||||
$variation="secondary"
|
||||
$margin={{ top: 'xs' }}
|
||||
$weight="normal"
|
||||
$display="inline"
|
||||
|
||||
@@ -1,39 +1,33 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, Icon, Text } from '@/components';
|
||||
import { Card, Icon, Text } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
|
||||
export const AlertPublic = ({ isPublicDoc }: { isPublicDoc: boolean }) => {
|
||||
const { t } = useTranslation();
|
||||
const { colorsTokens, spacingsTokens } = useCunninghamTheme();
|
||||
const { spacingsTokens } = useCunninghamTheme();
|
||||
|
||||
return (
|
||||
<Box
|
||||
<Card
|
||||
aria-label={t('Public document')}
|
||||
$color={colorsTokens['primary-800']}
|
||||
$background={colorsTokens['primary-050']}
|
||||
$radius={spacingsTokens['3xs']}
|
||||
$direction="row"
|
||||
$padding="xs"
|
||||
$flex={1}
|
||||
$align="center"
|
||||
$gap={spacingsTokens['2xs']}
|
||||
$css={css`
|
||||
border: 1px solid var(--c--theme--colors--primary-300, #e3e3fd);
|
||||
`}
|
||||
$theme="brand"
|
||||
>
|
||||
<Icon
|
||||
$theme="primary"
|
||||
$variation="800"
|
||||
$withThemeInherited
|
||||
data-testid="public-icon"
|
||||
iconName={isPublicDoc ? 'public' : 'vpn_lock'}
|
||||
/>
|
||||
<Text $theme="primary" $variation="800" $weight="500">
|
||||
<Text $withThemeInherited $weight="500">
|
||||
{isPublicDoc
|
||||
? t('Public document')
|
||||
: t('Document accessible to any connected person')}
|
||||
</Text>
|
||||
</Box>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { useTreeContext } from '@gouvfr-lasuite/ui-kit';
|
||||
import { VariantType, useToastProvider } from '@openfun/cunningham-react';
|
||||
import {
|
||||
Button,
|
||||
VariantType,
|
||||
useToastProvider,
|
||||
} from '@openfun/cunningham-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, BoxButton, Icon, Text } from '@/components';
|
||||
import { Box, Card, Icon } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import {
|
||||
Doc,
|
||||
@@ -17,7 +20,7 @@ export const AlertRestore = ({ doc }: { doc: Doc }) => {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToastProvider();
|
||||
const treeContext = useTreeContext<Doc>();
|
||||
const { colorsTokens, spacingsTokens } = useCunninghamTheme();
|
||||
const { spacingsTokens } = useCunninghamTheme();
|
||||
const { mutate: restoreDoc, error } = useRestoreDoc({
|
||||
listInvalidQueries: [KEY_LIST_DOC, KEY_LIST_DOC_TRASHBIN, KEY_DOC],
|
||||
options: {
|
||||
@@ -44,57 +47,52 @@ export const AlertRestore = ({ doc }: { doc: Doc }) => {
|
||||
});
|
||||
|
||||
return (
|
||||
<Box
|
||||
<Card
|
||||
className="--docs--alert-restore"
|
||||
aria-label={t('Alert deleted document')}
|
||||
$color={colorsTokens['danger-800']}
|
||||
$background={colorsTokens['danger-100']}
|
||||
$radius={spacingsTokens['3xs']}
|
||||
$direction="row"
|
||||
$padding="xs"
|
||||
$flex={1}
|
||||
$align="center"
|
||||
$gap={spacingsTokens['3xs']}
|
||||
$css={css`
|
||||
border: 1px solid var(--c--theme--colors--danger-300, #e3e3fd);
|
||||
`}
|
||||
$justify="space-between"
|
||||
$theme="error"
|
||||
>
|
||||
<Box $direction="row" $align="center" $gap={spacingsTokens['2xs']}>
|
||||
<Box
|
||||
$withThemeInherited
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$gap={spacingsTokens['2xs']}
|
||||
>
|
||||
<Icon
|
||||
$theme="danger"
|
||||
$variation="700"
|
||||
$withThemeInherited
|
||||
data-testid="public-icon"
|
||||
iconName="delete"
|
||||
variant="symbols-outlined"
|
||||
/>
|
||||
<Text $theme="danger" $variation="700" $weight="500">
|
||||
{t('Document deleted')}
|
||||
</Text>
|
||||
{t('Document deleted')}
|
||||
</Box>
|
||||
<BoxButton
|
||||
<Button
|
||||
onClick={() =>
|
||||
restoreDoc({
|
||||
docId: doc.id,
|
||||
})
|
||||
}
|
||||
$direction="row"
|
||||
$gap="0.2rem"
|
||||
$theme="danger"
|
||||
$variation="600"
|
||||
$align="center"
|
||||
color="error"
|
||||
variant="tertiary"
|
||||
size="nano"
|
||||
icon={
|
||||
<Icon
|
||||
iconName="undo"
|
||||
$withThemeInherited
|
||||
$size="18px"
|
||||
variant="symbols-outlined"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
iconName="undo"
|
||||
$theme="danger"
|
||||
$variation="600"
|
||||
$size="18px"
|
||||
variant="symbols-outlined"
|
||||
/>
|
||||
<Text $theme="danger" $variation="600" $size="s" $css="line-height:1;">
|
||||
{t('Restore')}
|
||||
</Text>
|
||||
</BoxButton>
|
||||
</Box>
|
||||
Restore
|
||||
</Button>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -46,20 +46,19 @@ export const BoutonShare = ({
|
||||
<Box
|
||||
$css={css`
|
||||
.c__button--medium {
|
||||
height: 32px;
|
||||
padding: 10px var(--c--theme--spacings--xs);
|
||||
height: var(--c--globals--spacings--lg);
|
||||
padding: 10px var(--c--globals--spacings--xs);
|
||||
gap: 7px;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<Button
|
||||
color="tertiary"
|
||||
aria-label={t('Share button')}
|
||||
variant="secondary"
|
||||
icon={
|
||||
<Icon
|
||||
iconName="group"
|
||||
$theme="primary"
|
||||
$variation="800"
|
||||
$color="inherit"
|
||||
variant="filled"
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
@@ -76,7 +75,8 @@ export const BoutonShare = ({
|
||||
|
||||
return (
|
||||
<Button
|
||||
color="primary-text"
|
||||
color="brand"
|
||||
variant="tertiary"
|
||||
onClick={open}
|
||||
size="medium"
|
||||
disabled={isDisabled}
|
||||
|
||||
@@ -47,15 +47,15 @@ export const DocHeaderInfo = ({ doc }: DocHeaderInfoProps) => {
|
||||
return (
|
||||
<>
|
||||
<Text
|
||||
$variation="600"
|
||||
$variation="tertiary"
|
||||
$size="s"
|
||||
$weight="bold"
|
||||
$theme={isEditable ? 'greyscale' : 'warning'}
|
||||
$theme={isEditable ? 'gray' : 'warning'}
|
||||
>
|
||||
{transRole(isEditable ? doc.user_role || doc.link_role : Role.READER)}
|
||||
·
|
||||
</Text>
|
||||
<Text $variation="600" $size="s">
|
||||
<Text $variation="tertiary" $size="s">
|
||||
{dateToDisplay}
|
||||
</Text>
|
||||
</>
|
||||
@@ -64,11 +64,11 @@ export const DocHeaderInfo = ({ doc }: DocHeaderInfoProps) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Text $variation="400" $size="s">
|
||||
<Text $variation="tertiary" $size="s">
|
||||
{hasChildren ? relativeOnly : dateToDisplay}
|
||||
</Text>
|
||||
{hasChildren && (
|
||||
<Text $variation="400" $size="s">
|
||||
<Text $variation="tertiary" $size="s">
|
||||
•
|
||||
{t('Contains {{count}} sub-documents', {
|
||||
count: childrenCount,
|
||||
|
||||
@@ -43,7 +43,6 @@ export const DocTitleText = () => {
|
||||
as="h2"
|
||||
$margin={{ all: 'none', left: 'none' }}
|
||||
$size={isMobile ? 'h4' : 'h2'}
|
||||
$variation="1000"
|
||||
>
|
||||
{currentDoc?.title || untitledDocument}
|
||||
</Text>
|
||||
@@ -71,10 +70,11 @@ const DocTitleEmojiPicker = ({ doc }: DocTitleProps) => {
|
||||
padding-top: 3px;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: ${colorsTokens['greyscale-100']};
|
||||
border-radius: 4px;
|
||||
background-color: ${colorsTokens['gray-100']};
|
||||
border-radius: var(--c--globals--spacings--st);
|
||||
}
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
transition: background-color var(--c--globals--transitions--duration)
|
||||
var(--c--globals--transitions--ease-out);
|
||||
`}
|
||||
>
|
||||
<DocIcon
|
||||
@@ -95,7 +95,7 @@ const DocTitleEmojiPicker = ({ doc }: DocTitleProps) => {
|
||||
height="25px"
|
||||
aria-hidden="true"
|
||||
aria-label={t('Simple document icon')}
|
||||
color={colorsTokens['primary-500']}
|
||||
color={colorsTokens['brand-500']}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
@@ -107,7 +107,6 @@ const DocTitleEmojiPicker = ({ doc }: DocTitleProps) => {
|
||||
const DocTitleInput = ({ doc }: DocTitleProps) => {
|
||||
const { isDesktop } = useResponsiveStore();
|
||||
const { t } = useTranslation();
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
const { isTopRoot } = useDocUtils(doc);
|
||||
const { untitledDocument } = useTrans();
|
||||
const { emoji, titleWithoutEmoji } = getEmojiAndTitle(doc.title ?? '');
|
||||
@@ -122,11 +121,17 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
|
||||
if (isTopRoot) {
|
||||
const sanitizedTitle = updateDocTitle(doc, inputText);
|
||||
setTitleDisplay(sanitizedTitle);
|
||||
return sanitizedTitle;
|
||||
} else {
|
||||
const sanitizedTitle = updateDocTitle(
|
||||
doc,
|
||||
emoji ? `${emoji} ${inputText}` : inputText,
|
||||
);
|
||||
const { emoji: pastedEmoji } = getEmojiAndTitle(inputText);
|
||||
const textPreservingPastedEmoji = pastedEmoji
|
||||
? `\u200B${inputText}`
|
||||
: inputText;
|
||||
const finalTitle = emoji
|
||||
? `${emoji} ${textPreservingPastedEmoji}`
|
||||
: textPreservingPastedEmoji;
|
||||
|
||||
const sanitizedTitle = updateDocTitle(doc, finalTitle);
|
||||
const { titleWithoutEmoji: sanitizedTitleWithoutEmoji } =
|
||||
getEmojiAndTitle(sanitizedTitle);
|
||||
|
||||
@@ -171,18 +176,19 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
|
||||
onBlurCapture={(event) =>
|
||||
handleTitleSubmit(event.target.textContent || '')
|
||||
}
|
||||
$color={colorsTokens['greyscale-1000']}
|
||||
$padding={{ right: 'big' }}
|
||||
$css={css`
|
||||
&[contenteditable='true']:empty:not(:focus):before {
|
||||
content: '${untitledDocument}';
|
||||
color: grey;
|
||||
color: var(
|
||||
--c--contextuals--content--semantic--neutral--tertiary
|
||||
);
|
||||
pointer-events: none;
|
||||
font-style: italic;
|
||||
}
|
||||
font-size: ${isDesktop
|
||||
? css`var(--c--theme--font--sizes--h2)`
|
||||
: css`var(--c--theme--font--sizes--sm)`};
|
||||
? css`var(--c--globals--font--sizes--h2)`
|
||||
: css`var(--c--globals--font--sizes--sm)`};
|
||||
font-weight: 700;
|
||||
outline: none;
|
||||
`}
|
||||
|
||||
@@ -215,14 +215,9 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
{!isSmallMobile && ModalExport && (
|
||||
<Button
|
||||
data-testid="doc-open-modal-download-button"
|
||||
color="tertiary-text"
|
||||
variant="tertiary"
|
||||
icon={
|
||||
<Icon
|
||||
iconName="download"
|
||||
$theme="primary"
|
||||
$variation="800"
|
||||
aria-hidden={true}
|
||||
/>
|
||||
<Icon iconName="download" $color="inherit" aria-hidden={true} />
|
||||
}
|
||||
onClick={() => {
|
||||
setIsModalExportOpen(true);
|
||||
@@ -236,17 +231,14 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
label={t('Open the document options')}
|
||||
buttonCss={css`
|
||||
padding: ${spacingsTokens['xs']};
|
||||
&:hover {
|
||||
background-color: ${colorsTokens['greyscale-100']};
|
||||
}
|
||||
${isSmallMobile
|
||||
? css`
|
||||
border: 1px solid ${colorsTokens['greyscale-300']};
|
||||
border: 1px solid ${colorsTokens['gray-300']};
|
||||
`
|
||||
: ''}
|
||||
`}
|
||||
>
|
||||
<IconOptions aria-hidden="true" isHorizontal $theme="primary" />
|
||||
<IconOptions aria-hidden="true" isHorizontal $color="inherit" />
|
||||
</DropdownMenu>
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ export const DocIcon = ({
|
||||
emoji,
|
||||
defaultIcon,
|
||||
$size = 'sm',
|
||||
$variation = '1000',
|
||||
$variation = 'secondary',
|
||||
$weight = '400',
|
||||
docId,
|
||||
title,
|
||||
|
||||
@@ -65,7 +65,6 @@ export const DocPage403 = ({ id }: DocProps) => {
|
||||
$padding={{ bottom: '2rem' }}
|
||||
>
|
||||
<Image
|
||||
className="c__image-system-filter"
|
||||
src={img403}
|
||||
alt={t('Image 403')}
|
||||
width={300}
|
||||
@@ -77,7 +76,7 @@ export const DocPage403 = ({ id }: DocProps) => {
|
||||
/>
|
||||
|
||||
<Box $align="center" $gap="0.8rem">
|
||||
<Text as="p" $textAlign="center" $maxWidth="350px" $theme="primary">
|
||||
<Text as="p" $textAlign="center" $maxWidth="350px" $theme="brand">
|
||||
{hasRequested
|
||||
? t('Your access request for this document is pending.')
|
||||
: t('Insufficient access rights to view the document.')}
|
||||
@@ -88,7 +87,6 @@ export const DocPage403 = ({ id }: DocProps) => {
|
||||
as="p"
|
||||
$maxWidth="320px"
|
||||
$textAlign="center"
|
||||
$variation="600"
|
||||
$size="sm"
|
||||
$margin={{ top: '0' }}
|
||||
>
|
||||
@@ -101,8 +99,9 @@ export const DocPage403 = ({ id }: DocProps) => {
|
||||
<Box $direction="row" $gap="0.7rem">
|
||||
<StyledLink href="/">
|
||||
<StyledButton
|
||||
icon={<Icon iconName="house" $theme="primary" />}
|
||||
color="tertiary"
|
||||
icon={<Icon iconName="house" $withThemeInherited />}
|
||||
color="brand"
|
||||
variant="secondary"
|
||||
>
|
||||
{t('Home')}
|
||||
</StyledButton>
|
||||
|
||||
@@ -98,7 +98,7 @@ export const ModalRemoveDoc = ({
|
||||
<Button
|
||||
ref={cancelButtonRef}
|
||||
aria-label={t('Cancel the deletion')}
|
||||
color="secondary"
|
||||
variant="secondary"
|
||||
fullWidth
|
||||
onClick={handleClose}
|
||||
onKeyDown={handleCloseKeyDown}
|
||||
@@ -107,7 +107,7 @@ export const ModalRemoveDoc = ({
|
||||
</Button>
|
||||
<Button
|
||||
aria-label={t('Delete document')}
|
||||
color="danger"
|
||||
color="error"
|
||||
fullWidth
|
||||
onClick={handleDelete}
|
||||
onKeyDown={handleDeleteKeyDown}
|
||||
@@ -130,7 +130,6 @@ export const ModalRemoveDoc = ({
|
||||
id="modal-remove-doc-title"
|
||||
$margin="0"
|
||||
$align="flex-start"
|
||||
$variation="1000"
|
||||
>
|
||||
{t('Delete a doc')}
|
||||
</Text>
|
||||
@@ -144,7 +143,12 @@ export const ModalRemoveDoc = ({
|
||||
>
|
||||
<Box className="--docs--modal-remove-doc">
|
||||
{!isError && (
|
||||
<Text $size="sm" $variation="600" $display="inline-block" as="p">
|
||||
<Text
|
||||
$size="sm"
|
||||
$variation="secondary"
|
||||
$display="inline-block"
|
||||
as="p"
|
||||
>
|
||||
{hasChildren ? (
|
||||
<Trans t={t}>
|
||||
This document and <strong>any sub-documents</strong> will be
|
||||
|
||||
@@ -33,7 +33,7 @@ export const SimpleDocItem = ({
|
||||
showAccesses = false,
|
||||
}: SimpleDocItemProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
|
||||
const { spacingsTokens } = useCunninghamTheme();
|
||||
const { isDesktop } = useResponsiveStore();
|
||||
const { untitledDocument } = useTrans();
|
||||
const { isChild } = useDocUtils(doc);
|
||||
@@ -63,13 +63,13 @@ export const SimpleDocItem = ({
|
||||
<PinnedDocumentIcon
|
||||
aria-hidden="true"
|
||||
data-testid="doc-pinned-icon"
|
||||
color={colorsTokens['primary-500']}
|
||||
color="var(--c--contextuals--content--semantic--info--tertiary)"
|
||||
/>
|
||||
) : isChild ? (
|
||||
<ChildDocument
|
||||
aria-hidden="true"
|
||||
data-testid="doc-child-icon"
|
||||
color={colorsTokens['primary-500']}
|
||||
color="var(--c--contextuals--content--semantic--info--tertiary)"
|
||||
/>
|
||||
) : (
|
||||
<SimpleFileIcon
|
||||
@@ -77,14 +77,13 @@ export const SimpleDocItem = ({
|
||||
height="32px"
|
||||
aria-hidden="true"
|
||||
data-testid="doc-simple-icon"
|
||||
color={colorsTokens['primary-500']}
|
||||
color="var(--c--contextuals--content--semantic--info--tertiary)"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<Box $justify="center" $overflow="auto">
|
||||
<Text
|
||||
$size="sm"
|
||||
$variation="1000"
|
||||
$weight="500"
|
||||
$css={ItemTextCss}
|
||||
data-testid="doc-title"
|
||||
@@ -99,7 +98,7 @@ export const SimpleDocItem = ({
|
||||
$margin={{ top: '-2px' }}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<Text $variation="600" $size="xs">
|
||||
<Text $size="xs" $variation="tertiary">
|
||||
{DateTime.fromISO(doc.updated_at).toRelative()}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
@@ -26,12 +26,13 @@ export const getEmojiAndTitle = (title: string) => {
|
||||
// Use emoji-regex library for comprehensive emoji detection compatible with ES5
|
||||
const regex = emojiRegex();
|
||||
|
||||
// Check if the title starts with an emoji
|
||||
const match = title.match(regex);
|
||||
// Ignore leading spaces when checking for a leading emoji
|
||||
const trimmedTitle = title.trimStart();
|
||||
const match = trimmedTitle.match(regex);
|
||||
|
||||
if (match && title.startsWith(match[0])) {
|
||||
if (match && trimmedTitle.startsWith(match[0])) {
|
||||
const emoji = match[0];
|
||||
const titleWithoutEmoji = title.substring(emoji.length).trim();
|
||||
const titleWithoutEmoji = trimmedTitle.substring(emoji.length).trim();
|
||||
return { emoji, titleWithoutEmoji };
|
||||
}
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ export const DocSearchFilters = ({
|
||||
/>
|
||||
</Box>
|
||||
{hasFilters && (
|
||||
<Button color="primary-text" size="small" onClick={onReset}>
|
||||
<Button color="brand" variant="tertiary" size="small" onClick={onReset}>
|
||||
{t('Reset')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -24,7 +24,11 @@ export const DocSearchItem = ({ doc }: DocSearchItemProps) => {
|
||||
</Box>
|
||||
}
|
||||
right={
|
||||
<Icon iconName="keyboard_return" $theme="primary" $variation="800" />
|
||||
<Icon
|
||||
iconName="keyboard_return"
|
||||
$theme="brand"
|
||||
$variation="secondary"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@@ -85,7 +85,8 @@ const DocSearchModalGlobal = ({
|
||||
aria-label={t('Close the search modal')}
|
||||
onClick={modalProps.onClose}
|
||||
size="small"
|
||||
color="primary-text"
|
||||
color="brand"
|
||||
variant="tertiary"
|
||||
/>
|
||||
</Box>
|
||||
<QuickSearch
|
||||
@@ -112,7 +113,6 @@ const DocSearchModalGlobal = ({
|
||||
$justify="center"
|
||||
>
|
||||
<Image
|
||||
className="c__image-system-filter"
|
||||
width={320}
|
||||
src={EmptySearchIcon}
|
||||
alt={t('No active search')}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<path
|
||||
id="ô€™ "
|
||||
d="M5.99986 12.2256C5.99986 11.7946 6.25238 11.5791 6.75741 11.5791H10.4145C10.802 11.5791 11.2156 11.4811 11.6553 11.2852C12.0994 11.0893 12.5413 10.8302 12.981 10.508C13.4208 10.1902 13.8235 9.84192 14.1892 9.46314C14.5593 9.08437 14.864 8.70778 15.1035 8.33336L15.4496 7.80438C15.5802 7.59105 15.7718 7.48438 16.0243 7.48438C16.1897 7.48438 16.3334 7.5388 16.4553 7.64765C16.5816 7.76084 16.6447 7.91758 16.6447 8.11785C16.6447 8.30506 16.5837 8.48791 16.4618 8.66641L16.2398 9.01254C15.996 9.37389 15.7086 9.7309 15.3778 10.0835C15.0469 10.4362 14.6964 10.7606 14.3263 11.0566C13.9606 11.357 13.6058 11.6073 13.2619 11.8076C12.9179 12.0122 12.6153 12.1472 12.3541 12.2125V12.2386C12.6153 12.2996 12.9179 12.4324 13.2619 12.637C13.6058 12.8416 13.9606 13.0941 14.3263 13.3945C14.6921 13.695 15.0404 14.0193 15.3712 14.3676C15.7065 14.7203 15.996 15.0794 16.2398 15.4451L16.4618 15.7847C16.5837 15.9632 16.6447 16.1461 16.6447 16.3333C16.6447 16.5292 16.5837 16.6838 16.4618 16.797C16.3443 16.9102 16.1963 16.9668 16.0178 16.9668C15.7696 16.9668 15.5802 16.8601 15.4496 16.6468L15.1035 16.1243C14.864 15.7499 14.5593 15.3711 14.1892 14.988C13.8235 14.6092 13.4208 14.2588 12.981 13.9366C12.5413 13.6188 12.0994 13.3619 11.6553 13.166C11.2156 12.9701 10.802 12.8721 10.4145 12.8721H6.75741C6.25238 12.8721 5.99986 12.6566 5.99986 12.2256ZM14.3068 7.64112C14.1065 7.48438 14.0303 7.32329 14.0782 7.15785C14.1261 6.99677 14.2697 6.88139 14.5092 6.81173L17.0822 6.03459C17.2782 5.97364 17.4392 5.99323 17.5655 6.09337C17.6918 6.1935 17.7505 6.34371 17.7418 6.54398L17.6308 9.23457C17.6221 9.48274 17.5437 9.64818 17.3957 9.7309C17.252 9.81362 17.0822 9.78097 16.8863 9.63294L14.3068 7.64112ZM14.3198 16.7251L16.9516 14.8117C17.1519 14.6637 17.3239 14.6354 17.4675 14.7268C17.6112 14.8182 17.6831 14.9858 17.6831 15.2296L17.7157 17.9268C17.7157 18.127 17.6504 18.2729 17.5198 18.3643C17.3935 18.4601 17.2324 18.4753 17.0365 18.41L14.4896 17.5545C14.2545 17.4805 14.1152 17.3608 14.0717 17.1953C14.0281 17.0299 14.1108 16.8732 14.3198 16.7251Z"
|
||||
fill="#000091"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
@@ -9,7 +9,7 @@
|
||||
<path
|
||||
id="v"
|
||||
d="M4.6665 12.667V11.3337H9.39984C10.0998 11.3337 10.7082 11.1114 11.2248 10.667C11.7415 10.2225 11.9998 9.66699 11.9998 9.00033C11.9998 8.33366 11.7415 7.7781 11.2248 7.33366C10.7082 6.88921 10.0998 6.66699 9.39984 6.66699H5.19984L6.93317 8.40033L5.99984 9.33366L2.6665 6.00033L5.99984 2.66699L6.93317 3.60033L5.19984 5.33366H9.39984C10.4776 5.33366 11.4026 5.68366 12.1748 6.38366C12.9471 7.08366 13.3332 7.95588 13.3332 9.00033C13.3332 10.0448 12.9471 10.917 12.1748 11.617C11.4026 12.317 10.4776 12.667 9.39984 12.667H4.6665Z"
|
||||
fill="#000091"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 725 B After Width: | Height: | Size: 730 B |
@@ -1,8 +1,7 @@
|
||||
import { Button } from '@openfun/cunningham-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, Text } from '@/components';
|
||||
import { Box, Card, Text } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { Doc, KEY_DOC, KEY_LIST_DOC } from '@/docs/doc-management';
|
||||
|
||||
@@ -17,29 +16,29 @@ interface DocDesynchronizedProps {
|
||||
|
||||
export const DocDesynchronized = ({ doc }: DocDesynchronizedProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
|
||||
const { spacingsTokens } = useCunninghamTheme();
|
||||
|
||||
const { mutate: updateDocLink } = useUpdateDocLink({
|
||||
listInvalidQueries: [KEY_LIST_DOC, KEY_DOC],
|
||||
});
|
||||
|
||||
return (
|
||||
<Box
|
||||
$background={colorsTokens['primary-100']}
|
||||
<Card
|
||||
$padding="3xs"
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$justify="space-between"
|
||||
$gap={spacingsTokens['4xs']}
|
||||
$color={colorsTokens['primary-800']}
|
||||
$css={css`
|
||||
border: 1px solid ${colorsTokens['primary-300']};
|
||||
border-radius: ${spacingsTokens['2xs']};
|
||||
`}
|
||||
$theme="brand"
|
||||
>
|
||||
<Box $direction="row" $align="center" $gap={spacingsTokens['3xs']}>
|
||||
<Box
|
||||
$withThemeInherited
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$gap={spacingsTokens['3xs']}
|
||||
>
|
||||
<Desync />
|
||||
<Text $size="xs" $theme="primary" $variation="800" $weight="400">
|
||||
<Text $size="xs" $withThemeInherited $weight="400">
|
||||
{t('The link sharing rules differ from the parent document')}
|
||||
</Text>
|
||||
</Box>
|
||||
@@ -53,12 +52,13 @@ export const DocDesynchronized = ({ doc }: DocDesynchronizedProps) => {
|
||||
})
|
||||
}
|
||||
size="small"
|
||||
color="primary-text"
|
||||
color="brand"
|
||||
variant="tertiary"
|
||||
icon={<Undo />}
|
||||
>
|
||||
{t('Restore')}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -41,21 +41,16 @@ export const DocInheritedShareContent = ({
|
||||
}}
|
||||
>
|
||||
<Box $direction="row" $align="center" $gap={spacingsTokens['4xs']}>
|
||||
<Text $variation="1000" $weight="bold" $size="sm">
|
||||
<Text $weight="bold" $size="sm">
|
||||
{t('People with access via the parent document')}
|
||||
</Text>
|
||||
<Box>
|
||||
<StyledLink href={`/docs/${rawAccesses[0].document.id}`}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={
|
||||
<Icon
|
||||
$theme="greyscale"
|
||||
$variation="600"
|
||||
iconName="open_in_new"
|
||||
/>
|
||||
}
|
||||
color="tertiary-text"
|
||||
icon={<Icon iconName="open_in_new" />}
|
||||
color="neutral"
|
||||
variant="tertiary"
|
||||
/>
|
||||
</StyledLink>
|
||||
</Box>
|
||||
|
||||
@@ -18,6 +18,7 @@ type DocRoleDropdownProps = {
|
||||
onSelectRole: (role: Role) => void;
|
||||
rolesAllowed?: Role[];
|
||||
isLastOwner?: boolean;
|
||||
ariaLabel?: string;
|
||||
};
|
||||
|
||||
export const DocRoleDropdown = ({
|
||||
@@ -29,6 +30,7 @@ export const DocRoleDropdown = ({
|
||||
rolesAllowed,
|
||||
access,
|
||||
isLastOwner = false,
|
||||
ariaLabel,
|
||||
}: DocRoleDropdownProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { transRole, translatedRoles } = useTrans();
|
||||
@@ -104,7 +106,12 @@ export const DocRoleDropdown = ({
|
||||
|
||||
if (!canUpdate) {
|
||||
return (
|
||||
<Text aria-label={t('Document role text')} $variation="600">
|
||||
<Text
|
||||
$variation="tertiary"
|
||||
aria-label={t('Document role text')}
|
||||
$weight={500}
|
||||
$size="s"
|
||||
>
|
||||
{transRole(currentRole)}
|
||||
</Text>
|
||||
);
|
||||
@@ -113,11 +120,16 @@ export const DocRoleDropdown = ({
|
||||
return (
|
||||
<DropdownMenu
|
||||
topMessage={topMessage}
|
||||
label="doc-role-dropdown"
|
||||
label={t('{{action}}, current role: {{role}}', {
|
||||
action: ariaLabel,
|
||||
role: transRole(currentRole),
|
||||
})}
|
||||
showArrow={true}
|
||||
arrowCss={css`
|
||||
color: var(--c--theme--colors--primary-800) !important;
|
||||
color: var(--c--contextuals--content--semantic--brand--tertiary);
|
||||
font-size: 16px;
|
||||
`}
|
||||
testId="doc-role-dropdown"
|
||||
options={[
|
||||
...roles,
|
||||
{
|
||||
@@ -126,8 +138,14 @@ export const DocRoleDropdown = ({
|
||||
callback: onRemove,
|
||||
},
|
||||
]}
|
||||
buttonCss={css`
|
||||
&:hover {
|
||||
background: none;
|
||||
}
|
||||
font-size: 12px;
|
||||
`}
|
||||
>
|
||||
<Text $theme="primary" $variation="800">
|
||||
<Text $theme="brand" $variation="tertiary">
|
||||
{transRole(currentRole)}
|
||||
</Text>
|
||||
</DropdownMenu>
|
||||
|
||||
@@ -78,9 +78,13 @@ const DocShareAccessRequestItem = ({ doc, accessRequest }: Props) => {
|
||||
onSelectRole={setRole}
|
||||
canUpdate={doc.abilities.accesses_manage}
|
||||
rolesAllowed={accessRequest.abilities.set_role_to}
|
||||
ariaLabel={t('Change role for {{name}}', {
|
||||
name: accessRequest.user.full_name || accessRequest.user.email,
|
||||
})}
|
||||
/>
|
||||
<Button
|
||||
color="tertiary"
|
||||
color="brand"
|
||||
variant="tertiary"
|
||||
onClick={() =>
|
||||
acceptDocAccessRequests({
|
||||
docId: doc.id,
|
||||
@@ -103,7 +107,7 @@ const DocShareAccessRequestItem = ({ doc, accessRequest }: Props) => {
|
||||
}
|
||||
aria-label={t('Close the access request modal')}
|
||||
>
|
||||
<Icon iconName="close" $variation="600" $size="16px" />
|
||||
<Icon iconName="close" $size="16px" />
|
||||
</BoxButton>
|
||||
)}
|
||||
</Box>
|
||||
@@ -194,7 +198,7 @@ export const ButtonAccessRequest = ({
|
||||
|
||||
if (docAccessError?.status === 404) {
|
||||
return (
|
||||
<Text $maxWidth="320px" $textAlign="center" $variation="600" $size="sm">
|
||||
<Text $maxWidth="320px" $textAlign="center" $size="sm">
|
||||
{t(
|
||||
'As this is a sub-document, please request access to the parent document to enable these features.',
|
||||
)}
|
||||
|
||||
@@ -5,10 +5,9 @@ import {
|
||||
} from '@openfun/cunningham-react';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { APIError } from '@/api';
|
||||
import { Box } from '@/components';
|
||||
import { Box, Card } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { Doc, Role } from '@/docs/doc-management';
|
||||
import { User } from '@/features/auth';
|
||||
@@ -41,7 +40,7 @@ export const DocShareAddMemberList = ({
|
||||
const { toast } = useToastProvider();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
|
||||
const { spacingsTokens } = useCunninghamTheme();
|
||||
const [invitationRole, setInvitationRole] = useState<Role>(Role.EDITOR);
|
||||
const canShare = doc.abilities.accesses_manage;
|
||||
const { mutateAsync: createInvitation } = useCreateDocInvitation();
|
||||
@@ -116,17 +115,16 @@ export const DocShareAddMemberList = ({
|
||||
: t('Invite {{count}} members', { count: selectedUsers.length });
|
||||
|
||||
return (
|
||||
<Box
|
||||
<Card
|
||||
className="--docs--doc-share-add-member-list"
|
||||
data-testid="doc-share-add-member-list"
|
||||
$direction="row"
|
||||
$padding={spacingsTokens.sm}
|
||||
$align="center"
|
||||
$background={colorsTokens['greyscale-050']}
|
||||
$radius={spacingsTokens['3xs']}
|
||||
$css={css`
|
||||
border: 1px solid ${colorsTokens['greyscale-200']};
|
||||
`}
|
||||
className="--docs--doc-share-add-member-list"
|
||||
$padding={spacingsTokens.sm}
|
||||
$scope="surface"
|
||||
$theme="tertiary"
|
||||
$variation=""
|
||||
$border="1px solid var(--c--contextuals--border--semantic--contextual--primary)"
|
||||
>
|
||||
<Box
|
||||
$direction="row"
|
||||
@@ -148,15 +146,17 @@ export const DocShareAddMemberList = ({
|
||||
canUpdate={canShare}
|
||||
currentRole={invitationRole}
|
||||
onSelectRole={setInvitationRole}
|
||||
ariaLabel={t('Invite new members')}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => void onInvite()}
|
||||
disabled={isLoading}
|
||||
aria-label={inviteLabel}
|
||||
data-testid="doc-share-invite-button"
|
||||
>
|
||||
{t('Invite')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { Button } from '@openfun/cunningham-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, Icon, Text } from '@/components';
|
||||
import { Box, BoxButton, Icon, Text } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { User } from '@/features/auth';
|
||||
|
||||
@@ -12,11 +10,11 @@ type Props = {
|
||||
};
|
||||
export const DocShareAddMemberListItem = ({ user, onRemoveUser }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { spacingsTokens, colorsTokens, fontSizesTokens } =
|
||||
useCunninghamTheme();
|
||||
const { spacingsTokens } = useCunninghamTheme();
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="--docs--doc-share-add-member-list-item"
|
||||
data-testid={`doc-share-add-member-${user.email}`}
|
||||
$radius={spacingsTokens['3xs']}
|
||||
$direction="row"
|
||||
@@ -24,30 +22,27 @@ export const DocShareAddMemberListItem = ({ user, onRemoveUser }: Props) => {
|
||||
$justify="center"
|
||||
$align="center"
|
||||
$gap={spacingsTokens['3xs']}
|
||||
$background={colorsTokens['greyscale-250']}
|
||||
$padding={{
|
||||
left: spacingsTokens['xs'],
|
||||
right: spacingsTokens['4xs'],
|
||||
vertical: spacingsTokens['4xs'],
|
||||
}}
|
||||
$css={css`
|
||||
color: ${colorsTokens['greyscale-1000']};
|
||||
font-size: ${fontSizesTokens['xs']};
|
||||
`}
|
||||
className="--docs--doc-share-add-member-list-item"
|
||||
$withThemeBG
|
||||
$theme="neutral"
|
||||
$variation="secondary"
|
||||
>
|
||||
<Text $variation="1000" $size="xs">
|
||||
<Text $withThemeInherited $size="xs">
|
||||
{user.full_name || user.email}
|
||||
</Text>
|
||||
<Button
|
||||
color="tertiary-text"
|
||||
size="nano"
|
||||
<BoxButton
|
||||
onClick={() => onRemoveUser?.(user)}
|
||||
icon={<Icon $variation="600" $size="sm" iconName="close" />}
|
||||
aria-label={t('Remove {{name}} from the invite list', {
|
||||
name: user.full_name || user.email,
|
||||
})}
|
||||
/>
|
||||
$withThemeInherited
|
||||
>
|
||||
<Icon $withThemeInherited $size="sm" iconName="close" />
|
||||
</BoxButton>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,25 +3,13 @@ import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import {
|
||||
Box,
|
||||
DropdownMenu,
|
||||
DropdownMenuOption,
|
||||
Icon,
|
||||
IconOptions,
|
||||
LoadMoreText,
|
||||
Text,
|
||||
} from '@/components';
|
||||
import { Box, BoxButton, Icon, LoadMoreText, Text } from '@/components';
|
||||
import { QuickSearchData, QuickSearchGroup } from '@/components/quick-search';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { Doc, Role } from '@/docs/doc-management';
|
||||
import { User } from '@/features/auth';
|
||||
|
||||
import {
|
||||
useDeleteDocInvitation,
|
||||
useDocInvitationsInfinite,
|
||||
useUpdateDocInvitation,
|
||||
} from '../api';
|
||||
import { useDocInvitationsInfinite, useUpdateDocInvitation } from '../api';
|
||||
import { Invitation } from '../types';
|
||||
|
||||
import { DocRoleDropdown } from './DocRoleDropdown';
|
||||
@@ -61,18 +49,6 @@ export const DocShareInvitationItem = ({
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: removeDocInvitation } = useDeleteDocInvitation({
|
||||
onError: (error) => {
|
||||
toast(
|
||||
error?.data?.role?.[0] ?? t('Error during delete invitation'),
|
||||
VariantType.ERROR,
|
||||
{
|
||||
duration: 4000,
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const onUpdate = (newRole: Role) => {
|
||||
updateDocInvitation({
|
||||
docId: doc.id,
|
||||
@@ -81,19 +57,6 @@ export const DocShareInvitationItem = ({
|
||||
});
|
||||
};
|
||||
|
||||
const onRemove = () => {
|
||||
removeDocInvitation({ invitationId: invitation.id, docId: doc.id });
|
||||
};
|
||||
|
||||
const moreActions: DropdownMenuOption[] = [
|
||||
{
|
||||
label: t('Delete'),
|
||||
icon: 'delete',
|
||||
callback: onRemove,
|
||||
disabled: !canUpdate,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Box
|
||||
$width="100%"
|
||||
@@ -112,17 +75,10 @@ export const DocShareInvitationItem = ({
|
||||
canUpdate={canUpdate}
|
||||
doc={doc}
|
||||
access={invitation}
|
||||
ariaLabel={t('Change role for {{email}}', {
|
||||
email: invitation.email,
|
||||
})}
|
||||
/>
|
||||
|
||||
{canUpdate && (
|
||||
<DropdownMenu
|
||||
data-testid="doc-share-invitation-more-actions"
|
||||
options={moreActions}
|
||||
label={t('Open invitation actions menu')}
|
||||
>
|
||||
<IconOptions isHorizontal $variation="600" aria-hidden="true" />
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
@@ -146,26 +102,22 @@ export const DocShareModalInviteUserRow = ({
|
||||
<SearchUserRow
|
||||
user={user}
|
||||
right={
|
||||
<Box
|
||||
<BoxButton
|
||||
className="right-hover"
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$css={css`
|
||||
contain: content;
|
||||
`}
|
||||
$color="var(--c--theme--colors--greyscale-400)"
|
||||
$cursor="pointer"
|
||||
$theme="brand"
|
||||
$variation="tertiary"
|
||||
$gap="var(--c--globals--spacings--xxxs)"
|
||||
>
|
||||
<Text $theme="primary" $variation="800" $size="sm">
|
||||
<Text $withThemeInherited $size="sm">
|
||||
{t('Add')}
|
||||
</Text>
|
||||
<Icon
|
||||
$theme="primary"
|
||||
$variation="800"
|
||||
iconName="add"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Box>
|
||||
<Icon $withThemeInherited iconName="add" />
|
||||
</BoxButton>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@@ -77,6 +77,9 @@ export const DocShareMemberItem = ({
|
||||
rolesAllowed={access.abilities.set_role_to}
|
||||
access={access}
|
||||
doc={doc}
|
||||
ariaLabel={t('Change role for {{name}}', {
|
||||
name: access.user.full_name || access.user.email,
|
||||
})}
|
||||
/>
|
||||
</Box>
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@ export const DocShareModal = ({ doc, onClose, isRootDoc = true }: Props) => {
|
||||
const [selectedUsers, setSelectedUsers] = useState<User[]>([]);
|
||||
const [userQuery, setUserQuery] = useState('');
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [liveAnnouncement, setLiveAnnouncement] = useState('');
|
||||
|
||||
const [listHeight, setListHeight] = useState<string>('400px');
|
||||
const canShare = doc.abilities.accesses_manage && isRootDoc;
|
||||
@@ -88,6 +89,19 @@ export const DocShareModal = ({ doc, onClose, isRootDoc = true }: Props) => {
|
||||
setSelectedUsers((prev) => [...prev, user]);
|
||||
setUserQuery('');
|
||||
setInputValue('');
|
||||
|
||||
// Announce to screen readers
|
||||
const userName = user.full_name || user.email;
|
||||
setLiveAnnouncement(
|
||||
t(
|
||||
'{{name}} added to invite list. Add more members or press Tab to select role and invite.',
|
||||
{
|
||||
name: userName,
|
||||
},
|
||||
),
|
||||
);
|
||||
// Clear announcement after it's been read
|
||||
setTimeout(() => setLiveAnnouncement(''), 100);
|
||||
};
|
||||
|
||||
const { data: membersQuery } = useDocAccesses({
|
||||
@@ -114,6 +128,16 @@ export const DocShareModal = ({ doc, onClose, isRootDoc = true }: Props) => {
|
||||
}
|
||||
const newArray = [...prevState];
|
||||
newArray.splice(index, 1);
|
||||
|
||||
// Announce to screen readers
|
||||
const userName = row.full_name || row.email;
|
||||
setLiveAnnouncement(
|
||||
t('{{name}} removed from invite list', {
|
||||
name: userName,
|
||||
}),
|
||||
);
|
||||
setTimeout(() => setLiveAnnouncement(''), 100);
|
||||
|
||||
return newArray;
|
||||
});
|
||||
};
|
||||
@@ -175,12 +199,22 @@ export const DocShareModal = ({ doc, onClose, isRootDoc = true }: Props) => {
|
||||
<ButtonCloseModal
|
||||
aria-label={t('Close the share modal')}
|
||||
onClick={onClose}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
</Box>
|
||||
}
|
||||
hideCloseButton
|
||||
>
|
||||
<ShareModalStyle />
|
||||
{/* Screen reader announcements */}
|
||||
<div
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
className="sr-only"
|
||||
>
|
||||
{liveAnnouncement}
|
||||
</div>
|
||||
<Box
|
||||
$height="auto"
|
||||
$maxHeight={canViewAccesses ? modalContentHeight : 'none'}
|
||||
@@ -228,7 +262,7 @@ export const DocShareModal = ({ doc, onClose, isRootDoc = true }: Props) => {
|
||||
<Text
|
||||
$maxWidth="320px"
|
||||
$textAlign="center"
|
||||
$variation="600"
|
||||
$variation="secondary"
|
||||
$size="sm"
|
||||
as="p"
|
||||
>
|
||||
@@ -238,7 +272,7 @@ export const DocShareModal = ({ doc, onClose, isRootDoc = true }: Props) => {
|
||||
</Text>
|
||||
<ButtonAccessRequest
|
||||
docId={doc.id}
|
||||
color="tertiary"
|
||||
variant="secondary"
|
||||
size="small"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Button } from '@openfun/cunningham-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, HorizontalSeparator } from '@/components';
|
||||
import { Box, HorizontalSeparator, Icon } from '@/components';
|
||||
import { Doc, useCopyDocLink } from '@/docs/doc-management';
|
||||
|
||||
import { DocVisibility } from './DocVisibility';
|
||||
@@ -38,18 +38,12 @@ export const DocShareModalFooter = ({
|
||||
<Button
|
||||
fullWidth={false}
|
||||
onClick={copyDocLink}
|
||||
color="tertiary"
|
||||
icon={
|
||||
<span className="material-icons" aria-hidden={true}>
|
||||
add_link
|
||||
</span>
|
||||
}
|
||||
variant="secondary"
|
||||
icon={<Icon iconName="add_link" $withThemeInherited />}
|
||||
>
|
||||
{t('Copy link')}
|
||||
</Button>
|
||||
<Button onClick={onClose} color="primary">
|
||||
{t('OK')}
|
||||
</Button>
|
||||
<Button onClick={onClose}>{t('OK')}</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -34,7 +34,7 @@ interface DocVisibilityProps {
|
||||
export const DocVisibility = ({ doc }: DocVisibilityProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { isDesktop } = useResponsiveStore();
|
||||
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
|
||||
const { spacingsTokens } = useCunninghamTheme();
|
||||
const canManage = doc.abilities.accesses_manage;
|
||||
const docLinkReach = getDocLinkReach(doc);
|
||||
const docLinkRole = getDocLinkRole(doc);
|
||||
@@ -123,7 +123,7 @@ export const DocVisibility = ({ doc }: DocVisibilityProps) => {
|
||||
$gap={spacingsTokens['base']}
|
||||
className="--docs--doc-visibility"
|
||||
>
|
||||
<Text $weight="700" $size="sm" $variation="700">
|
||||
<Text $weight="700" $size="sm">
|
||||
{t('Link settings')}
|
||||
</Text>
|
||||
{isDesynchronized && <DocDesynchronized doc={doc} />}
|
||||
@@ -145,7 +145,9 @@ export const DocVisibility = ({ doc }: DocVisibilityProps) => {
|
||||
testId="doc-visibility"
|
||||
label={t('Document visibility')}
|
||||
arrowCss={css`
|
||||
color: ${colorsTokens['primary-800']} !important;
|
||||
color: var(
|
||||
--c--contextuals--content--semantic--brand--tertiary
|
||||
) !important;
|
||||
`}
|
||||
buttonCss={css`
|
||||
&:hover {
|
||||
@@ -163,24 +165,24 @@ export const DocVisibility = ({ doc }: DocVisibilityProps) => {
|
||||
}
|
||||
options={linkReachOptions}
|
||||
>
|
||||
<Box $direction="row" $align="center" $gap={spacingsTokens['3xs']}>
|
||||
<Box
|
||||
$theme={canManage ? 'brand' : 'gray'}
|
||||
$variation={canManage ? 'tertiary' : 'primary'}
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$gap={spacingsTokens['3xs']}
|
||||
>
|
||||
<Icon
|
||||
$theme={canManage ? 'primary' : 'greyscale'}
|
||||
$variation={canManage ? '800' : '600'}
|
||||
iconName={linkReachChoices[docLinkReach].icon}
|
||||
$withThemeInherited
|
||||
/>
|
||||
<Text
|
||||
$theme={canManage ? 'primary' : 'greyscale'}
|
||||
$variation={canManage ? '800' : '600'}
|
||||
$weight="500"
|
||||
$size="md"
|
||||
>
|
||||
<Text $weight="500" $size="md" $withThemeInherited>
|
||||
{linkReachChoices[docLinkReach].label}
|
||||
</Text>
|
||||
</Box>
|
||||
</DropdownMenu>
|
||||
{isDesktop && (
|
||||
<Text $size="xs" $variation="600" $weight="400">
|
||||
<Text $size="xs" $variation="secondary" $weight="400">
|
||||
{description}
|
||||
</Text>
|
||||
)}
|
||||
@@ -193,6 +195,13 @@ export const DocVisibility = ({ doc }: DocVisibilityProps) => {
|
||||
&:hover {
|
||||
background-color: unset;
|
||||
}
|
||||
font-size: 12px;
|
||||
`}
|
||||
arrowCss={css`
|
||||
color: var(
|
||||
--c--contextuals--content--semantic--brand--tertiary
|
||||
) !important;
|
||||
font-size: 16px;
|
||||
`}
|
||||
disabled={!canManage}
|
||||
showArrow={true}
|
||||
@@ -206,7 +215,7 @@ export const DocVisibility = ({ doc }: DocVisibilityProps) => {
|
||||
}
|
||||
label={t('Document access mode')}
|
||||
>
|
||||
<Text $weight="initial" $variation="600" $theme="primary">
|
||||
<Text $weight="initial" $theme="brand" $variation="tertiary">
|
||||
{linkModeTranslations[docLinkRole]}
|
||||
</Text>
|
||||
</DropdownMenu>
|
||||
@@ -214,7 +223,7 @@ export const DocVisibility = ({ doc }: DocVisibilityProps) => {
|
||||
)}
|
||||
</Box>
|
||||
{!isDesktop && (
|
||||
<Text $size="xs" $variation="600">
|
||||
<Text $size="xs" $variation="secondary">
|
||||
{description}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
@@ -35,16 +35,14 @@ export const SearchUserRow = ({
|
||||
>
|
||||
<UserAvatar
|
||||
fullName={user.full_name || user.email}
|
||||
background={
|
||||
isInvitation ? colorsTokens['greyscale-400'] : undefined
|
||||
}
|
||||
background={isInvitation ? colorsTokens['gray-400'] : undefined}
|
||||
/>
|
||||
<Box $direction="column">
|
||||
<Text $size="sm" $weight="500" $variation="1000">
|
||||
<Text $size="sm" $weight="500">
|
||||
{hasFullName ? user.full_name : user.email}
|
||||
</Text>
|
||||
{hasFullName && (
|
||||
<Text $size="xs" $margin={{ top: '-2px' }} $variation="600">
|
||||
<Text $size="xs" $margin={{ top: '-2px' }} $variation="secondary">
|
||||
{user.email}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
@@ -57,15 +57,15 @@ export const Heading = ({
|
||||
block: 'start',
|
||||
});
|
||||
}}
|
||||
$radius="4px"
|
||||
$background={isActive ? `${colorsTokens['greyscale-100']}` : 'none'}
|
||||
$radius="var(--c--globals--spacings--st)"
|
||||
$background={isActive ? `${colorsTokens['gray-100']}` : 'none'}
|
||||
$css={css`
|
||||
text-align: left;
|
||||
&:focus-visible {
|
||||
/* Scoped focus style: same footprint as hover, with theme shadow */
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px ${colorsTokens['primary-400']};
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0 0 2px ${colorsTokens['brand-400']};
|
||||
border-radius: var(--c--globals--spacings--st);
|
||||
}
|
||||
`}
|
||||
className="--docs--table-content-heading"
|
||||
@@ -76,7 +76,6 @@ export const Heading = ({
|
||||
<Text
|
||||
$width="100%"
|
||||
$padding={{ vertical: 'xtiny', left: leftPaddingMap[level] }}
|
||||
$variation={isActive ? '1000' : '700'}
|
||||
$weight={isHighlight ? 'bold' : 'normal'}
|
||||
$css="overflow-wrap: break-word;"
|
||||
$hasTransition
|
||||
|
||||
@@ -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,132 +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: 0;
|
||||
border: 1px solid ${colorsTokens['greyscale-300']};
|
||||
overflow: hidden;
|
||||
border-radius: ${spacingsTokens['3xs']};
|
||||
background: ${colorsTokens['greyscale-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`
|
||||
&:hover {
|
||||
background: ${colorsTokens['primary-100']};
|
||||
}
|
||||
transition: none !important;
|
||||
transform: rotate(180deg);
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 4px ${colorsTokens['primary-400']};
|
||||
background: ${colorsTokens['primary-100']};
|
||||
width: 90%;
|
||||
height: 90%;
|
||||
box-shadow: 0 0 0 2px ${colorsTokens['brand-400']};
|
||||
border-radius: var(--c--globals--spacings--st);
|
||||
}
|
||||
`}
|
||||
>
|
||||
<Icon
|
||||
iconName="list"
|
||||
$theme="primary"
|
||||
$variation="800"
|
||||
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" $variation="800" $theme="primary">
|
||||
{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['primary-400']};
|
||||
border-radius: 4px;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<Icon iconName="menu_open" $theme="primary" $variation="800" />
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
TreeViewDataType,
|
||||
TreeViewItem,
|
||||
TreeViewNodeProps,
|
||||
useTreeContext,
|
||||
@@ -39,7 +40,7 @@ export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
|
||||
const treeContext = useTreeContext<Doc>();
|
||||
const { untitledDocument } = useTrans();
|
||||
const { node } = props;
|
||||
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
|
||||
const { spacingsTokens } = useCunninghamTheme();
|
||||
const { isDesktop } = useResponsiveStore();
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -68,7 +69,10 @@ export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
|
||||
node.open();
|
||||
|
||||
router.push(`/docs/${createdDoc.id}`);
|
||||
treeContext?.treeData.setChildren(node.data.value.id, allChildren);
|
||||
treeContext?.treeData.setChildren(
|
||||
node.data.value.id,
|
||||
allChildren as TreeViewDataType<Doc>[],
|
||||
);
|
||||
treeContext?.treeData.setSelectedNode(createdDoc);
|
||||
togglePanel();
|
||||
})
|
||||
@@ -114,33 +118,24 @@ export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
|
||||
aria-expanded={hasChildren ? isExpanded : undefined}
|
||||
aria-disabled={isDisabled}
|
||||
$css={css`
|
||||
background-color: ${menuOpen
|
||||
? 'var(--c--theme--colors--greyscale-100)'
|
||||
: 'var(--c--theme--colors--greyscale-000)'};
|
||||
background-color: var(--c--globals--colors--gray-000);
|
||||
.light-doc-item-actions {
|
||||
display: ${menuOpen || !isDesktop ? 'flex' : 'none'};
|
||||
position: absolute;
|
||||
right: 0;
|
||||
background: ${isDesktop
|
||||
? 'var(--c--theme--colors--greyscale-100)'
|
||||
: 'var(--c--theme--colors--greyscale-000)'};
|
||||
}
|
||||
.c__tree-view--node.isSelected {
|
||||
.light-doc-item-actions {
|
||||
background: var(--c--theme--colors--greyscale-100);
|
||||
}
|
||||
right: var(--c--globals--spacings--0);
|
||||
}
|
||||
.c__tree-view--node.isFocused {
|
||||
outline: none !important;
|
||||
box-shadow: 0 0 0 2px var(--c--theme--colors--primary-500) !important;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0 0 2px var(--c--globals--colors--brand-500) !important;
|
||||
border-radius: var(--c--globals--spacings--st);
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--c--theme--colors--greyscale-100);
|
||||
border-radius: 4px;
|
||||
background-color: var(
|
||||
--c--contextuals--background--semantic--gray--tertiary
|
||||
);
|
||||
border-radius: var(--c--globals--spacings--st);
|
||||
.light-doc-item-actions {
|
||||
display: flex;
|
||||
background: var(--c--theme--colors--greyscale-100);
|
||||
}
|
||||
}
|
||||
.row.preview & {
|
||||
@@ -152,7 +147,9 @@ export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
|
||||
<DocIcon
|
||||
emoji={emoji}
|
||||
withEmojiPicker={doc.abilities.partial_update}
|
||||
defaultIcon={<SubPageIcon color={colorsTokens['primary-400']} />}
|
||||
defaultIcon={
|
||||
<SubPageIcon color="var(--c--contextuals--content--semantic--info--tertiary)" />
|
||||
}
|
||||
$size="sm"
|
||||
docId={doc.id}
|
||||
title={doc.title}
|
||||
@@ -187,15 +184,14 @@ export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
|
||||
overflow: hidden;
|
||||
`}
|
||||
>
|
||||
<Text $css={ItemTextCss} $size="sm" $variation="1000">
|
||||
<Text $css={ItemTextCss} $size="sm">
|
||||
{displayTitle}
|
||||
</Text>
|
||||
{doc.nb_accesses_direct >= 1 && (
|
||||
<Icon
|
||||
variant="filled"
|
||||
iconName="group"
|
||||
$size="16px"
|
||||
$variation="400"
|
||||
$size="md"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -215,24 +215,26 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
|
||||
onKeyDown={handleRootKeyDown}
|
||||
$css={css`
|
||||
padding: ${spacingsTokens['2xs']};
|
||||
border-radius: 4px;
|
||||
border-radius: var(--c--globals--spacings--st);
|
||||
width: 100%;
|
||||
background-color: ${rootIsSelected || rootActionsOpen
|
||||
? 'var(--c--theme--colors--greyscale-100)'
|
||||
? 'var(--c--contextuals--background--semantic--contextual--primary)'
|
||||
: 'transparent'};
|
||||
|
||||
&:hover {
|
||||
background-color: var(--c--theme--colors--greyscale-100);
|
||||
background-color: var(
|
||||
--c--contextuals--background--semantic--contextual--primary
|
||||
);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: none !important;
|
||||
box-shadow: 0 0 0 2px var(--c--theme--colors--primary-500) !important;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0 0 2px var(--c--globals--colors--brand-500) !important;
|
||||
border-radius: var(--c--globals--spacings--st);
|
||||
}
|
||||
|
||||
.doc-tree-root-item-actions {
|
||||
display: 'flex';
|
||||
display: flex;
|
||||
opacity: ${rootActionsOpen ? '1' : '0'};
|
||||
|
||||
&:has(.isOpen) {
|
||||
|
||||
@@ -119,7 +119,7 @@ export const DocTreeItemActions = ({
|
||||
: []),
|
||||
{
|
||||
label: t('Duplicate'),
|
||||
icon: <Icon $variation="600" iconName="content_copy" />,
|
||||
icon: <Icon iconName="content_copy" />,
|
||||
isDisabled: !doc.abilities.duplicate,
|
||||
callback: () => {
|
||||
duplicateDoc({
|
||||
@@ -180,8 +180,8 @@ export const DocTreeItemActions = ({
|
||||
}}
|
||||
iconName="more_horiz"
|
||||
variant="filled"
|
||||
$theme="primary"
|
||||
$variation="600"
|
||||
$theme="brand"
|
||||
$variation="secondary"
|
||||
aria-label={t('More options')}
|
||||
/>
|
||||
</DropdownMenu>
|
||||
@@ -195,16 +195,12 @@ export const DocTreeItemActions = ({
|
||||
parentId: doc.id,
|
||||
});
|
||||
}}
|
||||
color="primary"
|
||||
$theme="brand"
|
||||
$variation="secondary"
|
||||
aria-label={t('Add a sub page')}
|
||||
data-testid="doc-tree-item-actions-add-child"
|
||||
>
|
||||
<Icon
|
||||
variant="filled"
|
||||
$variation="800"
|
||||
$theme="primary"
|
||||
iconName="add_box"
|
||||
/>
|
||||
<Icon variant="filled" $color="inherit" iconName="add_box" />
|
||||
</BoxButton>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -74,7 +74,7 @@ export const ModalConfirmationVersion = ({
|
||||
<>
|
||||
<Button
|
||||
aria-label={`${t('Cancel')} - ${t('Warning')}`}
|
||||
color="secondary"
|
||||
variant="secondary"
|
||||
fullWidth
|
||||
onClick={() => onClose()}
|
||||
>
|
||||
@@ -82,7 +82,7 @@ export const ModalConfirmationVersion = ({
|
||||
</Button>
|
||||
<Button
|
||||
aria-label={t('Restore')}
|
||||
color="danger"
|
||||
color="error"
|
||||
fullWidth
|
||||
onClick={() => {
|
||||
if (!version?.content) {
|
||||
@@ -109,7 +109,6 @@ export const ModalConfirmationVersion = ({
|
||||
id="modal-confirmation-version-title"
|
||||
$size="h6"
|
||||
$align="flex-start"
|
||||
$variation="1000"
|
||||
>
|
||||
{t('Warning')}
|
||||
</Text>
|
||||
@@ -117,10 +116,10 @@ export const ModalConfirmationVersion = ({
|
||||
>
|
||||
<Box className="--docs--modal-confirmation-version">
|
||||
<Box>
|
||||
<Text $variation="600" as="p">
|
||||
<Text $variation="secondary" as="p">
|
||||
{t('Your current document will revert to this version.')}
|
||||
</Text>
|
||||
<Text $variation="600" as="p">
|
||||
<Text $variation="secondary" as="p">
|
||||
{t('If a member is editing, his works can be lost.')}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||