Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e47b02b6f2 | |||
| 75f71368f4 | |||
| 21f5feab3e | |||
| 8ec89a8348 | |||
| 3b80ac7b4e | |||
| 68df717854 | |||
| 2f52dddc84 | |||
| b1231cea7c | |||
| f9f32db854 | |||
| 0d967aba48 |
@@ -6,6 +6,22 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(frontend) integrate configurable Waffle #1795
|
||||
|
||||
### Fixed
|
||||
|
||||
- ✅(e2e) fix e2e test for other browsers #1799
|
||||
- 🐛(frontend) add fallback for unsupported Blocknote languages #1810
|
||||
- 🐛(frontend) fix emojipicker closing in tree #1808
|
||||
|
||||
### Changed
|
||||
|
||||
- ♿(frontend) improve accessibility:
|
||||
- ♿️(frontend) fix subdoc opening and emoji pick focus #1745
|
||||
- ♿️(frontend) Keyboard focus Fixes for docs Tree/Editor #1816
|
||||
|
||||
## [4.4.0] - 2026-01-13
|
||||
|
||||
### Added
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
@@ -1,4 +1,6 @@
|
||||
# Runtime Theming 🎨
|
||||
# Customization Guide 🛠 ️
|
||||
|
||||
## Runtime Theming 🎨
|
||||
|
||||
### How to Use
|
||||
|
||||
@@ -32,7 +34,7 @@ Then, set the `FRONTEND_CSS_URL` environment variable to the URL of your custom
|
||||
|
||||
----
|
||||
|
||||
# Runtime JavaScript Injection 🚀
|
||||
## Runtime JavaScript Injection 🚀
|
||||
|
||||
### How to Use
|
||||
|
||||
@@ -87,7 +89,7 @@ Then, set the `FRONTEND_JS_URL` environment variable to the URL of your custom J
|
||||
|
||||
----
|
||||
|
||||
# **Your Docs icon** 📝
|
||||
## **Your Docs icon** 📝
|
||||
|
||||
You can add your own Docs icon in the header from the theme customization file.
|
||||
|
||||
@@ -105,7 +107,7 @@ This configuration is optional. If not set, the default icon will be used.
|
||||
|
||||
----
|
||||
|
||||
# **Footer Configuration** 📝
|
||||
## **Footer Configuration** 📝
|
||||
|
||||
The footer is configurable from the theme customization file.
|
||||
|
||||
@@ -128,7 +130,7 @@ Below is a visual example of a configured footer ⬇️:
|
||||
|
||||
----
|
||||
|
||||
# **Custom Translations** 📝
|
||||
## **Custom Translations** 📝
|
||||
|
||||
The translations can be partially overridden from the theme customization file.
|
||||
|
||||
@@ -140,4 +142,36 @@ THEME_CUSTOMIZATION_FILE_PATH=<path>
|
||||
|
||||
### Example of JSON
|
||||
|
||||
The json must follow some rules: https://github.com/suitenumerique/docs/blob/main/src/helm/env.d/dev/configuration/theme/demo.json
|
||||
The json must follow some rules: https://github.com/suitenumerique/docs/blob/main/src/helm/env.d/dev/configuration/theme/demo.json
|
||||
|
||||
----
|
||||
|
||||
## **Waffle Configuration** 🧇
|
||||
|
||||
The Waffle (La Gaufre) is a widget that displays a grid of services.
|
||||
|
||||

|
||||
|
||||
### Settings 🔧
|
||||
|
||||
```shellscript
|
||||
THEME_CUSTOMIZATION_FILE_PATH=<path>
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
The Waffle can be configured in the theme customization file with the `waffle` key.
|
||||
|
||||
### Available Properties
|
||||
|
||||
See: [LaGaufreV2Props](https://github.com/suitenumerique/ui-kit/blob/main/src/components/la-gaufre/LaGaufreV2.tsx#L49)
|
||||
|
||||
### Complete Example
|
||||
|
||||
From the theme customization file: https://github.com/suitenumerique/docs/blob/main/src/helm/env.d/dev/configuration/theme/demo.json
|
||||
|
||||
### Behavior
|
||||
|
||||
- If `data.services` is provided, the Waffle will display those services statically
|
||||
- If no data is provided, services can be fetched dynamically from an API endpoint thanks to the `apiUrl` property
|
||||
|
||||
@@ -31,6 +31,13 @@
|
||||
"matchPackageNames": ["django"],
|
||||
"allowedVersions": "<6.0.0"
|
||||
},
|
||||
{
|
||||
|
||||
"groupName": "allowed celery versions",
|
||||
"matchManagers": ["pep621"],
|
||||
"matchPackageNames": ["celery"],
|
||||
"allowedVersions": "<5.6.0"
|
||||
},
|
||||
{
|
||||
"enabled": false,
|
||||
"groupName": "ignored js dependencies",
|
||||
|
||||
@@ -28,7 +28,7 @@ dependencies = [
|
||||
"beautifulsoup4==4.14.3",
|
||||
"boto3==1.42.17",
|
||||
"Brotli==1.2.0",
|
||||
"celery[redis]==5.6.0",
|
||||
"celery[redis]==5.5.3",
|
||||
"django-configurations==2.5.1",
|
||||
"django-cors-headers==4.9.0",
|
||||
"django-countries==8.2.0",
|
||||
|
||||
@@ -58,7 +58,7 @@ test.describe('Doc Comments', () => {
|
||||
await page.getByRole('button', { name: '👍' }).click();
|
||||
|
||||
await expect(
|
||||
thread.getByRole('img', { name: 'E2E Chromium' }).first(),
|
||||
thread.getByRole('img', { name: `E2E ${browserName}` }).first(),
|
||||
).toBeVisible();
|
||||
await expect(thread.getByText('This is a comment').first()).toBeVisible();
|
||||
await expect(thread.getByText(`E2E ${browserName}`).first()).toBeVisible();
|
||||
|
||||
@@ -73,7 +73,7 @@ test.describe('Doc Editor', () => {
|
||||
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
await page.locator('.bn-block-outer').last().click();
|
||||
await page.locator('.ProseMirror').focus();
|
||||
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ test.describe('Inherited share accesses', () => {
|
||||
`doc-share-member-row-user.test@${browserName}.test`,
|
||||
);
|
||||
await expect(user).toBeVisible();
|
||||
await expect(user.getByText('E2E Chromium')).toBeVisible();
|
||||
await expect(user.getByText(`E2E ${browserName}`)).toBeVisible();
|
||||
await expect(user.getByText('Owner')).toBeVisible();
|
||||
|
||||
await page
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
createDoc,
|
||||
expectLoginPage,
|
||||
keyCloakSignIn,
|
||||
randomName,
|
||||
updateDocTitle,
|
||||
verifyDocName,
|
||||
} from './utils-common';
|
||||
@@ -20,50 +19,6 @@ test.describe('Doc Tree', () => {
|
||||
await page.goto('/');
|
||||
});
|
||||
|
||||
test('create new sub pages', async ({ page, browserName }) => {
|
||||
const [titleParent] = await createDoc(
|
||||
page,
|
||||
'doc-tree-content',
|
||||
browserName,
|
||||
1,
|
||||
);
|
||||
await verifyDocName(page, titleParent);
|
||||
const addButton = page.getByTestId('new-doc-button');
|
||||
const docTree = page.getByTestId('doc-tree');
|
||||
|
||||
await expect(addButton).toBeVisible();
|
||||
|
||||
// Wait for and intercept the POST request to create a new page
|
||||
const responsePromise = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('/documents/') &&
|
||||
response.url().includes('/children/') &&
|
||||
response.request().method() === 'POST',
|
||||
);
|
||||
|
||||
await clickOnAddRootSubPage(page);
|
||||
const response = await responsePromise;
|
||||
expect(response.ok()).toBeTruthy();
|
||||
const subPageJson = await response.json();
|
||||
|
||||
await expect(docTree).toBeVisible();
|
||||
const subPageItem = docTree
|
||||
.getByTestId(`doc-sub-page-item-${subPageJson.id}`)
|
||||
.first();
|
||||
|
||||
await expect(subPageItem).toBeVisible();
|
||||
await subPageItem.click();
|
||||
await verifyDocName(page, '');
|
||||
const input = page.getByRole('textbox', { name: 'Document title' });
|
||||
await input.click();
|
||||
const [randomDocName] = randomName('doc-tree-test', browserName, 1);
|
||||
await input.fill(randomDocName);
|
||||
await input.press('Enter');
|
||||
await expect(subPageItem.getByText(randomDocName)).toBeVisible();
|
||||
await page.reload();
|
||||
await expect(subPageItem.getByText(randomDocName)).toBeVisible();
|
||||
});
|
||||
|
||||
test('check the reorder of sub pages', async ({ page, browserName }) => {
|
||||
await createDoc(page, 'doc-tree-content', browserName, 1);
|
||||
const addButton = page.getByTestId('new-doc-button');
|
||||
|
||||
@@ -59,45 +59,90 @@ test.describe('Header', () => {
|
||||
).toBeVisible();
|
||||
|
||||
await expect(header.getByText('English')).toBeVisible();
|
||||
|
||||
await expect(
|
||||
header.getByRole('button', {
|
||||
name: 'Les services de La Suite numérique',
|
||||
}),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('checks La Gauffre interaction', async ({ page }) => {
|
||||
test('checks a custom waffle', async ({ page }) => {
|
||||
await overrideConfig(page, {
|
||||
FRONTEND_THEME: 'dsfr',
|
||||
theme_customization: {
|
||||
waffle: {
|
||||
data: {
|
||||
services: [
|
||||
{
|
||||
name: 'Docs E2E Custom 1',
|
||||
url: 'https://docs.numerique.gouv.fr/',
|
||||
maturity: 'stable',
|
||||
logo: 'https://lasuite.numerique.gouv.fr/assets/products/docs.svg',
|
||||
},
|
||||
{
|
||||
name: 'Docs E2E Custom 2',
|
||||
url: 'https://docs.numerique.gouv.fr/',
|
||||
maturity: 'stable',
|
||||
logo: 'https://lasuite.numerique.gouv.fr/assets/products/docs.svg',
|
||||
},
|
||||
],
|
||||
},
|
||||
showMoreLimit: 9,
|
||||
},
|
||||
},
|
||||
});
|
||||
await page.goto('/');
|
||||
|
||||
const header = page.locator('header').first();
|
||||
|
||||
await expect(
|
||||
header.getByRole('button', {
|
||||
name: 'Les services de La Suite numérique',
|
||||
}),
|
||||
header.getByRole('button', { name: 'Digital LaSuite services' }),
|
||||
).toBeVisible();
|
||||
|
||||
/**
|
||||
* La gaufre load a js file from a remote server,
|
||||
* The Waffle loads a js file from a remote server,
|
||||
* it takes some time to load the file and have the interaction available
|
||||
*/
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
await header
|
||||
.getByRole('button', { name: 'Digital LaSuite services' })
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
page.getByRole('link', { name: 'Docs E2E Custom 1' }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('link', { name: 'Docs E2E Custom 2' }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('checks the waffle dsfr', async ({ page }) => {
|
||||
await overrideConfig(page, {
|
||||
theme_customization: {
|
||||
waffle: {
|
||||
apiUrl: 'https://lasuite.numerique.gouv.fr/api/services',
|
||||
showMoreLimit: 9,
|
||||
},
|
||||
},
|
||||
});
|
||||
await page.goto('/');
|
||||
|
||||
const header = page.locator('header').first();
|
||||
|
||||
await expect(
|
||||
header.getByRole('button', { name: 'Digital LaSuite services' }),
|
||||
).toBeVisible();
|
||||
|
||||
/**
|
||||
* The Waffle loads a js file from a remote server,
|
||||
* it takes some time to load the file and have the interaction available
|
||||
*/
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
await header
|
||||
.getByRole('button', {
|
||||
name: 'Les services de La Suite numérique',
|
||||
name: 'Digital LaSuite services',
|
||||
})
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
page.getByRole('link', { name: 'France Transfert' }),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(page.getByRole('link', { name: 'Tchap' })).toBeVisible();
|
||||
await expect(page.getByRole('link', { name: 'Grist' })).toBeVisible();
|
||||
await expect(page.getByRole('link', { name: 'Visio' })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -124,11 +169,6 @@ test.describe('Header mobile', () => {
|
||||
|
||||
await expect(header.getByLabel('Open the header menu')).toBeVisible();
|
||||
await expect(header.getByTestId('header-icon-docs')).toBeVisible();
|
||||
await expect(
|
||||
header.getByRole('button', {
|
||||
name: 'Les services de La Suite numérique',
|
||||
}),
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -113,9 +113,6 @@ test.describe('Home page', () => {
|
||||
});
|
||||
await expect(languageButton).toBeVisible();
|
||||
|
||||
await expect(
|
||||
header.getByRole('button', { name: 'Les services de La Suite numé' }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
header.getByRole('img', { name: 'Gouvernement Logo' }),
|
||||
).toBeVisible();
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { TestLanguage, createDoc, waitForLanguageSwitch } from './utils-common';
|
||||
import {
|
||||
TestLanguage,
|
||||
createDoc,
|
||||
overrideConfig,
|
||||
waitForLanguageSwitch,
|
||||
} from './utils-common';
|
||||
import { openSuggestionMenu } from './utils-editor';
|
||||
|
||||
test.describe('Language', () => {
|
||||
@@ -107,6 +112,15 @@ test.describe('Language', () => {
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
await overrideConfig(page, {
|
||||
LANGUAGES: [
|
||||
['en-us', 'English'],
|
||||
['fr-fr', 'Français'],
|
||||
['sv-se', 'Svenska'],
|
||||
],
|
||||
LANGUAGE_CODE: 'en-us',
|
||||
});
|
||||
|
||||
await createDoc(page, 'doc-toolbar', browserName, 1);
|
||||
|
||||
const { editor, suggestionMenu } = await openSuggestionMenu({ page });
|
||||
@@ -126,5 +140,14 @@ test.describe('Language', () => {
|
||||
await expect(
|
||||
suggestionMenu.getByText('Titres', { exact: true }),
|
||||
).toBeVisible();
|
||||
|
||||
/**
|
||||
* Swedish is not yet supported in the BlockNote locales, so it should fallback to English
|
||||
*/
|
||||
await waitForLanguageSwitch(page, TestLanguage.Swedish);
|
||||
await openSuggestionMenu({ page });
|
||||
await expect(
|
||||
suggestionMenu.getByText('Headings', { exact: true }),
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -224,7 +224,9 @@ export const updateDocTitle = async (page: Page, title: string) => {
|
||||
await expect(input).toHaveText('');
|
||||
await expect(input).toBeVisible();
|
||||
await input.click();
|
||||
await input.fill(title);
|
||||
await input.fill(title, {
|
||||
force: true,
|
||||
});
|
||||
await input.click();
|
||||
await input.blur();
|
||||
await verifyDocName(page, title);
|
||||
@@ -328,6 +330,10 @@ export const TestLanguage = {
|
||||
label: 'Deutsch',
|
||||
expectedLocale: ['de-de'],
|
||||
},
|
||||
Swedish: {
|
||||
label: 'Svenska',
|
||||
expectedLocale: ['sv-se'],
|
||||
},
|
||||
} as const;
|
||||
|
||||
type TestLanguageKey = keyof typeof TestLanguage;
|
||||
|
||||
@@ -1,58 +1,53 @@
|
||||
import { defaultTokens } from '@gouvfr-lasuite/cunningham-react';
|
||||
import { cunninghamConfig as tokens } from '@gouvfr-lasuite/ui-kit';
|
||||
import merge from 'lodash/merge';
|
||||
import {
|
||||
dsfrGlobals,
|
||||
getUIKitThemesFromGlobals,
|
||||
whiteLabelGlobals,
|
||||
} from '@gouvfr-lasuite/ui-kit';
|
||||
|
||||
// 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.globals = {
|
||||
...tokens.themes.default.globals,
|
||||
...{
|
||||
colors: mergedColors,
|
||||
font: {
|
||||
...tokens.themes.default.globals.font,
|
||||
families: {
|
||||
base: 'sans-serif',
|
||||
accent: 'sans-serif',
|
||||
const themeWhiteLabelLight = getUIKitThemesFromGlobals(whiteLabelGlobals, {
|
||||
prefix: 'default',
|
||||
variants: ['light'],
|
||||
overrides: {
|
||||
globals: {
|
||||
spacing: {
|
||||
'0': '0rem',
|
||||
none: '0rem',
|
||||
auto: 'auto',
|
||||
bx: '2.2rem',
|
||||
full: '100%',
|
||||
'3xs': '0.25rem',
|
||||
'2xs': '0.375rem',
|
||||
},
|
||||
},
|
||||
components: {
|
||||
logo: {
|
||||
src: '',
|
||||
alt: '',
|
||||
widthHeader: '',
|
||||
widthFooter: '',
|
||||
},
|
||||
'home-proconnect': false,
|
||||
icon: {
|
||||
src: '/assets/icon-docs.svg',
|
||||
width: '32px',
|
||||
height: 'auto',
|
||||
},
|
||||
favicon: {
|
||||
'png-light': '/assets/favicon-light.png',
|
||||
'png-dark': '/assets/favicon-dark.png',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const themeDefault = {
|
||||
default: themeWhiteLabelLight['default-light'],
|
||||
};
|
||||
|
||||
tokens.themes.default.components = {
|
||||
...tokens.themes.default.components,
|
||||
...{
|
||||
logo: {
|
||||
src: '',
|
||||
alt: '',
|
||||
widthHeader: '',
|
||||
widthFooter: '',
|
||||
},
|
||||
'la-gaufre': false,
|
||||
'home-proconnect': false,
|
||||
icon: {
|
||||
src: '/assets/icon-docs.svg',
|
||||
width: '32px',
|
||||
height: 'auto',
|
||||
},
|
||||
favicon: {
|
||||
'png-light': '/assets/favicon-light.png',
|
||||
'png-dark': '/assets/favicon-dark.png',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const dsfrTheme = {
|
||||
dsfr: {
|
||||
const themesDSFRLight = getUIKitThemesFromGlobals(dsfrGlobals, {
|
||||
prefix: 'dsfr',
|
||||
variants: ['light'],
|
||||
overrides: {
|
||||
globals: {
|
||||
font: {
|
||||
families: {
|
||||
@@ -68,7 +63,6 @@ const dsfrTheme = {
|
||||
widthFooter: '220px',
|
||||
alt: 'Gouvernement Logo',
|
||||
},
|
||||
'la-gaufre': true,
|
||||
'home-proconnect': true,
|
||||
icon: {
|
||||
src: '/assets/icon-docs-dsfr.svg',
|
||||
@@ -82,317 +76,16 @@ const dsfrTheme = {
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const genericTheme = {
|
||||
generic: {
|
||||
globals: {
|
||||
colors: {
|
||||
'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: {
|
||||
base: 'Inter, Roboto Flex Variable, sans-serif',
|
||||
accent: 'Inter, Roboto Flex Variable, sans-serif',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
const themesDSFR = {
|
||||
dsfr: themesDSFRLight['dsfr-light'],
|
||||
};
|
||||
|
||||
const docsTokens = {
|
||||
...tokens,
|
||||
themes: {
|
||||
...tokens.themes,
|
||||
...dsfrTheme,
|
||||
...genericTheme,
|
||||
...themeDefault,
|
||||
...themesDSFR,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
+16
-1
@@ -5,11 +5,14 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box } from '@/components';
|
||||
|
||||
export const PICKER_HEIGHT = 500;
|
||||
|
||||
interface EmojiPickerProps {
|
||||
emojiData: EmojiMartData;
|
||||
onClickOutside: () => void;
|
||||
onEmojiSelect: ({ native }: { native: string }) => void;
|
||||
withOverlay?: boolean;
|
||||
onEscape?: () => void;
|
||||
}
|
||||
|
||||
export const EmojiPicker = ({
|
||||
@@ -17,14 +20,26 @@ export const EmojiPicker = ({
|
||||
onClickOutside,
|
||||
onEmojiSelect,
|
||||
withOverlay = false,
|
||||
onEscape,
|
||||
}: EmojiPickerProps) => {
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.key === 'Escape') {
|
||||
if (onEscape) {
|
||||
onEscape();
|
||||
} else {
|
||||
onClickOutside();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const pickerContent = (
|
||||
<Box $position="absolute" $zIndex={1000} $margin="2rem 0 0 0">
|
||||
<Box $position="absolute" $zIndex={1000} onKeyDownCapture={handleKeyDown}>
|
||||
<Picker
|
||||
data={emojiData}
|
||||
locale={i18n.resolvedLanguage}
|
||||
autoFocus
|
||||
onClickOutside={onClickOutside}
|
||||
onEmojiSelect={onEmojiSelect}
|
||||
previewPosition="none"
|
||||
@@ -3,6 +3,8 @@ export * from './BoxButton';
|
||||
export * from './Card';
|
||||
export * from './DropButton';
|
||||
export * from './dropdown-menu/DropdownMenu';
|
||||
export * from './Emoji/EmojiPicker';
|
||||
export { default as emojidata } from './Emoji/initEmojiCallout';
|
||||
export * from './quick-search';
|
||||
export * from './Icon';
|
||||
export * from './InfiniteScroll';
|
||||
|
||||
@@ -4,13 +4,14 @@ import { Resource } from 'i18next';
|
||||
import { APIError, errorCauses, fetchAPI } from '@/api';
|
||||
import { Theme } from '@/cunningham/';
|
||||
import { FooterType } from '@/features/footer';
|
||||
import { HeaderType } from '@/features/header';
|
||||
import { HeaderType, WaffleType } from '@/features/header';
|
||||
import { PostHogConf } from '@/services';
|
||||
|
||||
interface ThemeCustomization {
|
||||
footer?: FooterType;
|
||||
translations?: Resource;
|
||||
header?: HeaderType;
|
||||
waffle?: WaffleType;
|
||||
}
|
||||
|
||||
export interface ConfigResponse {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -28,7 +28,7 @@ const getMergedTokens = (theme: Theme) => {
|
||||
return merge({}, tokens.themes['default'], tokens.themes[theme]);
|
||||
};
|
||||
|
||||
const DEFAULT_THEME: Theme = 'generic';
|
||||
const DEFAULT_THEME: Theme = 'default';
|
||||
const defaultTokens = getMergedTokens(DEFAULT_THEME);
|
||||
|
||||
const initialState: ThemeStore = {
|
||||
|
||||
+4
-1
@@ -93,7 +93,10 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
||||
|
||||
useSaveDoc(doc.id, provider.document, isConnectedToCollabServer);
|
||||
const { i18n } = useTranslation();
|
||||
const lang = i18n.resolvedLanguage;
|
||||
let lang = i18n.resolvedLanguage;
|
||||
if (!lang || !(lang in locales)) {
|
||||
lang = 'en';
|
||||
}
|
||||
|
||||
const { uploadFile, errorAttachment } = useUploadFile(doc.id);
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Box, Loading } from '@/components';
|
||||
import { DocHeader } from '@/docs/doc-header/';
|
||||
import {
|
||||
Doc,
|
||||
useDocFocusManagement,
|
||||
useIsCollaborativeEditable,
|
||||
useProviderStore,
|
||||
} from '@/docs/doc-management';
|
||||
@@ -82,7 +83,9 @@ export const DocEditor = ({ doc }: DocEditorProps) => {
|
||||
const readOnly =
|
||||
!doc.abilities.partial_update || !isEditable || isLoading || isDeletedDoc;
|
||||
const { setIsSkeletonVisible } = useSkeletonStore();
|
||||
const isProviderReady = isReady && provider;
|
||||
const isProviderReady = Boolean(isReady && provider);
|
||||
|
||||
useDocFocusManagement(doc.id, isProviderReady);
|
||||
|
||||
useEffect(() => {
|
||||
if (isProviderReady) {
|
||||
|
||||
+1
-9
@@ -12,12 +12,9 @@ import { TFunction } from 'i18next';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { createGlobalStyle, css } from 'styled-components';
|
||||
|
||||
import { Box, BoxButton, Icon } from '@/components';
|
||||
import { Box, BoxButton, EmojiPicker, Icon, emojidata } from '@/components';
|
||||
|
||||
import { DocsBlockNoteEditor } from '../../types';
|
||||
import { EmojiPicker } from '../EmojiPicker';
|
||||
|
||||
import emojidata from './initEmojiCallout';
|
||||
|
||||
const CalloutBlockStyle = createGlobalStyle`
|
||||
.bn-block-content[data-content-type="callout"][data-background-color] {
|
||||
@@ -106,11 +103,6 @@ const CalloutComponent = ({
|
||||
<BoxButton
|
||||
contentEditable={false}
|
||||
onClick={toggleEmojiPicker}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Escape' && openEmojiPicker) {
|
||||
setOpenEmojiPicker(false);
|
||||
}
|
||||
}}
|
||||
$css={css`
|
||||
font-size: 1.125rem;
|
||||
cursor: ${isEditable ? 'pointer' : 'default'};
|
||||
|
||||
-1
@@ -1,5 +1,4 @@
|
||||
export * from './AccessibleImageBlock';
|
||||
export * from './CalloutBlock';
|
||||
export { default as emojidata } from './initEmojiCallout';
|
||||
export * from './PdfBlock';
|
||||
export * from './UploadLoaderBlock';
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from './BlockNoteEditor';
|
||||
export * from './DocEditor';
|
||||
export * from './EmojiPicker';
|
||||
export * from './custom-blocks/';
|
||||
|
||||
@@ -1,11 +1,27 @@
|
||||
import { MouseEvent, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { BoxButton, BoxButtonType, Text, TextType } from '@/components';
|
||||
import { EmojiPicker, emojidata } from '@/docs/doc-editor/';
|
||||
import {
|
||||
Box,
|
||||
BoxButton,
|
||||
BoxButtonType,
|
||||
EmojiPicker,
|
||||
PICKER_HEIGHT,
|
||||
Text,
|
||||
TextType,
|
||||
emojidata,
|
||||
} from '@/components';
|
||||
|
||||
import { useDocTitleUpdate } from '../hooks/useDocTitleUpdate';
|
||||
import { cssSelectors } from '../utils';
|
||||
|
||||
const getClosestTreeItem = (element: HTMLElement | null) =>
|
||||
element?.closest<HTMLElement>(cssSelectors.DOC_TREE_ROW) ??
|
||||
element?.closest<HTMLElement>(cssSelectors.DOC_TREE_NODE) ??
|
||||
element?.closest<HTMLElement>('[role="treeitem"]') ??
|
||||
null;
|
||||
|
||||
type DocIconProps = TextType & {
|
||||
buttonProps?: BoxButtonType;
|
||||
@@ -60,9 +76,24 @@ export const DocIcon = ({
|
||||
|
||||
if (!openEmojiPicker && iconRef.current) {
|
||||
const rect = iconRef.current.getBoundingClientRect();
|
||||
|
||||
const pickerHeight = PICKER_HEIGHT;
|
||||
const spaceBelow = window.innerHeight - rect.bottom;
|
||||
const spaceAbove = rect.top;
|
||||
|
||||
// Position picker above if not enough space below and enough space above
|
||||
const shouldPositionAbove =
|
||||
spaceBelow < pickerHeight && spaceAbove >= pickerHeight;
|
||||
|
||||
// Offset to align the picker properly
|
||||
const ROW_OFFSET_TOP = 55;
|
||||
const ROW_OFFSET_BOTTOM = 10;
|
||||
|
||||
setPickerPosition({
|
||||
top: rect.bottom + window.scrollY + 8,
|
||||
left: rect.left + window.scrollX,
|
||||
top: shouldPositionAbove
|
||||
? rect.top - pickerHeight + ROW_OFFSET_TOP
|
||||
: rect.bottom + ROW_OFFSET_BOTTOM,
|
||||
left: rect.left,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -86,6 +117,30 @@ export const DocIcon = ({
|
||||
setOpenEmojiPicker(false);
|
||||
};
|
||||
|
||||
const handleEscape = () => {
|
||||
setOpenEmojiPicker(false);
|
||||
window.requestAnimationFrame(() => {
|
||||
const localTreeItem = getClosestTreeItem(iconRef.current);
|
||||
const docTree = document.querySelector<HTMLElement>(
|
||||
cssSelectors.DOC_TREE,
|
||||
);
|
||||
const docTreeItem =
|
||||
localTreeItem ||
|
||||
docTree?.querySelector<HTMLElement>(
|
||||
cssSelectors.DOC_TREE_FOCUSED_NODE,
|
||||
) ||
|
||||
docTree?.querySelector<HTMLElement>(
|
||||
cssSelectors.DOC_TREE_SELECTED_ROW,
|
||||
) ||
|
||||
docTree?.querySelector<HTMLElement>(
|
||||
cssSelectors.DOC_TREE_SELECTED_NODE,
|
||||
) ||
|
||||
document.querySelector<HTMLElement>(cssSelectors.DOC_TREE_ROOT);
|
||||
|
||||
docTreeItem?.focus();
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<BoxButton
|
||||
@@ -114,21 +169,22 @@ export const DocIcon = ({
|
||||
</BoxButton>
|
||||
{openEmojiPicker &&
|
||||
createPortal(
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: pickerPosition.top,
|
||||
left: pickerPosition.left,
|
||||
zIndex: 1000,
|
||||
}}
|
||||
<Box
|
||||
$position="fixed"
|
||||
$css={css`
|
||||
top: ${pickerPosition.top}px;
|
||||
left: ${pickerPosition.left}px;
|
||||
z-index: 1000;
|
||||
`}
|
||||
>
|
||||
<EmojiPicker
|
||||
emojiData={emojidata}
|
||||
onEmojiSelect={handleEmojiSelect}
|
||||
onClickOutside={handleClickOutside}
|
||||
withOverlay={true}
|
||||
onEscape={handleEscape}
|
||||
/>
|
||||
</div>,
|
||||
</Box>,
|
||||
document.body,
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export * from './useCollaboration';
|
||||
export * from './useCopyDocLink';
|
||||
export * from './useCreateChildDocTree';
|
||||
export * from './useDocFocusManagement';
|
||||
export * from './useDocTitleUpdate';
|
||||
export * from './useDocUtils';
|
||||
export * from './useIsCollaborativeEditable';
|
||||
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { cssSelectors } from '@/docs/doc-management/utils';
|
||||
|
||||
const isWithin = (el: Element | null, selector: string) =>
|
||||
!!el?.closest(selector);
|
||||
|
||||
export const useDocFocusManagement = (docId?: string, isReady = true) => {
|
||||
// 1) Auto-focus title when opening a doc
|
||||
useEffect(() => {
|
||||
if (!docId || !isReady || typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
const frameId = window.requestAnimationFrame(() => {
|
||||
const titleElement = document.querySelector<HTMLElement>(
|
||||
cssSelectors.DOC_TITLE,
|
||||
);
|
||||
if (!titleElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Avoid stealing focus if user is already in the doc tree or editor.
|
||||
const activeEl = document.activeElement;
|
||||
const active = activeEl instanceof Element ? activeEl : null;
|
||||
const isInDocUI =
|
||||
isWithin(active, cssSelectors.DOC_EDITOR_FOCUS) ||
|
||||
isWithin(active, cssSelectors.DOC_TREE);
|
||||
|
||||
const isBodyFocused = activeEl === document.body;
|
||||
|
||||
if (isBodyFocused && !isInDocUI && activeEl !== titleElement) {
|
||||
titleElement.focus();
|
||||
}
|
||||
});
|
||||
|
||||
return () => window.cancelAnimationFrame(frameId);
|
||||
}, [docId, isReady]);
|
||||
|
||||
// 2) Escape from editor/title -> focus back the selected tree item (or root)
|
||||
useEffect(() => {
|
||||
if (!docId || !isReady || typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleFocusShortcut = (event: KeyboardEvent) => {
|
||||
if (event.key !== 'F6' || event.defaultPrevented) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = event.target instanceof Element ? event.target : null;
|
||||
const activeEl = document.activeElement;
|
||||
const active = activeEl instanceof Element ? activeEl : null;
|
||||
|
||||
const isDocFocus =
|
||||
isWithin(target, cssSelectors.DOC_EDITOR_FOCUS) ||
|
||||
isWithin(active, cssSelectors.DOC_EDITOR_FOCUS) ||
|
||||
isWithin(target, cssSelectors.DOC_TITLE) ||
|
||||
isWithin(active, cssSelectors.DOC_TITLE);
|
||||
|
||||
if (!isDocFocus) {
|
||||
return;
|
||||
}
|
||||
|
||||
const docTree = document.querySelector<HTMLElement>(
|
||||
cssSelectors.DOC_TREE,
|
||||
);
|
||||
|
||||
const docTreeItem =
|
||||
docTree?.querySelector<HTMLElement>(
|
||||
cssSelectors.DOC_TREE_SELECTED_ROW,
|
||||
) ||
|
||||
docTree?.querySelector<HTMLElement>(
|
||||
cssSelectors.DOC_TREE_SELECTED_NODE,
|
||||
) ||
|
||||
document.querySelector<HTMLElement>(cssSelectors.DOC_TREE_ROOT);
|
||||
|
||||
if (!docTreeItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
docTreeItem.focus();
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleFocusShortcut, true);
|
||||
return () =>
|
||||
document.removeEventListener('keydown', handleFocusShortcut, true);
|
||||
}, [docId, isReady]);
|
||||
};
|
||||
@@ -38,3 +38,15 @@ export const getEmojiAndTitle = (title: string) => {
|
||||
|
||||
return { emoji: null, titleWithoutEmoji: title };
|
||||
};
|
||||
|
||||
export const cssSelectors = {
|
||||
DOC_TITLE: '.--docs--doc-title-input[contenteditable="true"]',
|
||||
DOC_TREE_ROOT: '[data-testid="doc-tree-root-item"]',
|
||||
DOC_TREE: '[data-testid="doc-tree"]',
|
||||
DOC_EDITOR_FOCUS: '.--docs--main-editor, .--docs--doc-title-input',
|
||||
DOC_TREE_ROW: '.c__tree-view--row',
|
||||
DOC_TREE_NODE: '.c__tree-view--node',
|
||||
DOC_TREE_FOCUSED_NODE: '.c__tree-view--node.isFocused',
|
||||
DOC_TREE_SELECTED_ROW: '.c__tree-view--row[aria-selected="true"]',
|
||||
DOC_TREE_SELECTED_NODE: '.c__tree-view--node[aria-selected="true"]',
|
||||
} as const;
|
||||
|
||||
@@ -128,6 +128,27 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleRowKeyDown = useCallback((e: React.KeyboardEvent) => {
|
||||
if (e.key !== 'Enter') {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = e.target as HTMLElement | null;
|
||||
if (
|
||||
!target ||
|
||||
!(
|
||||
target.classList.contains('c__tree-view--row') ||
|
||||
target.classList.contains('c__tree-view--node')
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.currentTarget
|
||||
.querySelector<HTMLDivElement>('.c__tree-view--node')
|
||||
?.click();
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* This effect is used to reset the tree when a new document
|
||||
* that is not part of the current tree is loaded.
|
||||
@@ -231,7 +252,7 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
|
||||
{/* Keyboard instructions for screen readers */}
|
||||
<Box id="doc-tree-keyboard-instructions" className="sr-only">
|
||||
{t(
|
||||
'Use arrow keys to navigate between documents. Press Enter to open a document. Press F2 to focus the emoji button when available, then press F2 again to access document actions.',
|
||||
'Use arrow keys to navigate between documents. Press Enter to open a document. Press F2 to focus the emoji button when available, then press F2 again to access document actions. Press F6 to return focus from the document to the tree.',
|
||||
)}
|
||||
</Box>
|
||||
<Box
|
||||
@@ -357,6 +378,9 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
|
||||
}}
|
||||
rootNodeId={treeContext.root.id}
|
||||
renderNode={DocSubPageItem}
|
||||
rowProps={{
|
||||
onKeyDown: handleRowKeyDown,
|
||||
}}
|
||||
/>
|
||||
</Overlayer>
|
||||
)}
|
||||
|
||||
@@ -12,8 +12,8 @@ import { useResponsiveStore } from '@/stores';
|
||||
import { HEADER_HEIGHT } from '../conf';
|
||||
|
||||
import { ButtonTogglePanel } from './ButtonTogglePanel';
|
||||
import { LaGaufre } from './LaGaufre';
|
||||
import { Title } from './Title';
|
||||
import { Waffle } from './Waffle';
|
||||
|
||||
export const Header = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -85,7 +85,7 @@ export const Header = () => {
|
||||
</StyledLink>
|
||||
{!isDesktop ? (
|
||||
<Box $direction="row" $gap={spacingsTokens['sm']}>
|
||||
<LaGaufre />
|
||||
<Waffle />
|
||||
</Box>
|
||||
) : (
|
||||
<Box
|
||||
@@ -96,7 +96,7 @@ export const Header = () => {
|
||||
>
|
||||
<ButtonLogin />
|
||||
<LanguagePicker />
|
||||
<LaGaufre />
|
||||
<Waffle />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import { Gaufre } from '@gouvfr-lasuite/integration';
|
||||
import '@gouvfr-lasuite/integration/dist/css/gaufre.css';
|
||||
import Script from 'next/script';
|
||||
import React from 'react';
|
||||
import { createGlobalStyle } from 'styled-components';
|
||||
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
|
||||
const GaufreStyle = createGlobalStyle`
|
||||
.lasuite-gaufre-btn{
|
||||
box-shadow: inset 0 0 0 0 !important;
|
||||
border-radius: var(--c--components--button--border-radius) !important;
|
||||
transition: all var(--c--globals--transitions--duration) var(--c--globals--transitions--ease-out) !important;
|
||||
&:hover, &:focus-visible {
|
||||
background: var(--c--contextuals--background--semantic--contextual--primary) !important;
|
||||
}
|
||||
color: var(--c--contextuals--content--semantic--brand--tertiary) !important;
|
||||
}
|
||||
`;
|
||||
|
||||
export const LaGaufre = () => {
|
||||
const { componentTokens } = useCunninghamTheme();
|
||||
|
||||
if (!componentTokens['la-gaufre']) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Script
|
||||
src="https://integration.lasuite.numerique.gouv.fr/api/v1/gaufre.js"
|
||||
strategy="lazyOnload"
|
||||
id="lasuite-gaufre-script"
|
||||
/>
|
||||
<GaufreStyle />
|
||||
<Gaufre variant="small" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import { LaGaufreV2, LaGaufreV2Props } from '@gouvfr-lasuite/ui-kit';
|
||||
import React from 'react';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box } from '@/components';
|
||||
import { useConfig } from '@/core';
|
||||
|
||||
type WaffleAPIType = {
|
||||
apiUrl: LaGaufreV2Props['apiUrl'];
|
||||
data?: never;
|
||||
};
|
||||
|
||||
type WaffleDataType = {
|
||||
apiUrl?: never;
|
||||
data?: LaGaufreV2Props['data'];
|
||||
};
|
||||
|
||||
export type WaffleType = Omit<
|
||||
LaGaufreV2Props,
|
||||
'apiUrl' | 'data' | 'widgetPath'
|
||||
> &
|
||||
(WaffleAPIType | WaffleDataType) & {
|
||||
widgetPath?: string;
|
||||
};
|
||||
|
||||
const LaGaufreV2Fixed = LaGaufreV2 as React.ComponentType<WaffleType>;
|
||||
|
||||
export const Waffle = () => {
|
||||
const { data: conf } = useConfig();
|
||||
|
||||
const waffleConfig = conf?.theme_customization?.waffle;
|
||||
|
||||
if (!waffleConfig?.apiUrl && !waffleConfig?.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
$css={css`
|
||||
& > div {
|
||||
display: flex;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<LaGaufreV2Fixed {...waffleConfig} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
export * from './ButtonTogglePanel';
|
||||
export * from './Header';
|
||||
export * from './LaGaufre';
|
||||
export * from './Waffle';
|
||||
export * from './Title';
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Box } from '@/components';
|
||||
import { useConfig } from '@/core';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { ButtonTogglePanel, Title } from '@/features/header/';
|
||||
import { LaGaufre } from '@/features/header/components/LaGaufre';
|
||||
import { Waffle } from '@/features/header/components/Waffle';
|
||||
import { LanguagePicker } from '@/features/language';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
@@ -81,7 +81,7 @@ export const HomeHeader = () => {
|
||||
{!isSmallMobile && (
|
||||
<Box $direction="row" $gap="1rem" $align="center">
|
||||
<LanguagePicker />
|
||||
<LaGaufre />
|
||||
<Waffle />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -72,7 +72,14 @@ export const ResizableLeftPanel = ({
|
||||
<Panel
|
||||
ref={ref}
|
||||
order={0}
|
||||
defaultSize={isDesktop ? panelSizePercent : 0}
|
||||
defaultSize={
|
||||
isDesktop
|
||||
? Math.max(
|
||||
minPanelSizePercent,
|
||||
Math.min(panelSizePercent, maxPanelSizePercent),
|
||||
)
|
||||
: 0
|
||||
}
|
||||
minSize={isDesktop ? minPanelSizePercent : 0}
|
||||
maxSize={isDesktop ? maxPanelSizePercent : 0}
|
||||
onResize={handleResize}
|
||||
|
||||
@@ -139,5 +139,30 @@
|
||||
"src": "/assets/icon-docs.svg",
|
||||
"width": "32px"
|
||||
}
|
||||
},
|
||||
"waffle": {
|
||||
"data": {
|
||||
"services": [
|
||||
{
|
||||
"name": "Docs",
|
||||
"url": "https://docs.numerique.gouv.fr/",
|
||||
"maturity": "stable",
|
||||
"logo": "https://lasuite.numerique.gouv.fr/assets/products/docs.svg"
|
||||
},
|
||||
{
|
||||
"name": "Visio",
|
||||
"url": "https://visio.numerique.gouv.fr/",
|
||||
"maturity": "stable",
|
||||
"logo": "https://lasuite.numerique.gouv.fr/assets/products/visio.svg"
|
||||
},
|
||||
{
|
||||
"name": "Fichiers",
|
||||
"url": "https://fichiers.numerique.gouv.fr/",
|
||||
"maturity": "stable",
|
||||
"logo": "https://lasuite.numerique.gouv.fr/assets/products/fichiers.svg"
|
||||
}
|
||||
]
|
||||
},
|
||||
"showMoreLimit": 9
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user