diff --git a/.circleci/config.yml b/.circleci/config.yml index 174acc91..394e6f2d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -33,6 +33,33 @@ jobs: command: './venv/bin/pytest -nauto --junitxml=test-results.xml' - store_test_results: path: test-results.xml + # Do the API and the frontend's test fixtures still agree? + # + # `api-tests` runs these too, as part of the whole suite. This job exists for the + # signal: fixture drift is silent by nature -- the frontend suite passes against a body the + # API stopped sending -- so it is worth a check whose name says what broke, failing in under a + # minute rather than somewhere inside seventeen hundred results. + # + # It needs no ffmpeg or catdoc (verified), and shares the dependency cache key with the other + # Python jobs, so it costs a venv restore and one test file. + api-contract-tests: + docker: + - image: cimg/python:3.13 + resource_class: medium + steps: + - checkout + - restore_cache: + key: deps-3.13-{{ checksum "requirements.txt" }}-5 + - run: + name: Install Requirements + command: | + python3 -m venv venv + . venv/bin/activate + pip install -r requirements.txt + - run: + name: Fixtures match the OpenAPI spec, and the spec matches the handlers + command: './venv/bin/pytest wrolpi/test/test_api_fixtures.py -v' + api-alembic-migrations: docker: - image: cimg/python:3.13 @@ -167,6 +194,7 @@ workflows: jobs: - sanic-api-start - api-tests + - api-contract-tests - api-alembic-migrations wrolpi-app-test: jobs: diff --git a/.github/workflows/controller-tests.yml b/.github/workflows/controller-tests.yml index a1c005ad..37c5c7c4 100644 --- a/.github/workflows/controller-tests.yml +++ b/.github/workflows/controller-tests.yml @@ -1,10 +1,13 @@ name: Controller Tests +# `pull_request` is unfiltered for the same reason as jest-tests.yml: `branches: [master]` meant +# this never ran on a pull request into any other branch. It is why Controller Tests is absent +# from the checks on the pull request that moved this job to Python 3.13 -- the change to this +# file could not be verified by the run that contained it. on: push: branches: [master, feature/*] pull_request: - branches: [master] jobs: controller-tests: @@ -21,6 +24,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v5 with: + # Trixie ships 3.13, and docker/controller/Dockerfile is python:3.13-slim-trixie. python-version: '3.13' cache: 'pip' cache-dependency-path: controller/requirements.txt diff --git a/.github/workflows/jest-tests.yml b/.github/workflows/jest-tests.yml index 68bfd7aa..8a8eadba 100644 --- a/.github/workflows/jest-tests.yml +++ b/.github/workflows/jest-tests.yml @@ -1,10 +1,16 @@ name: Jest Tests +# `pull_request` is deliberately unfiltered. It was `branches: [master]`, which meant a pull +# request into anything else ran no jest and no webpack build -- so the entire Semantic UI to +# Mantine migration, seventy-five files of it, was reviewed and merged across nine pull requests +# into `feature/custom-theme` without either ever running on a single one of them. The build +# step matters most of all here: jest and tsc both pass on a named import that does not exist, +# and only webpack rejects it. The suite did run on the branch afterwards, which is the wrong +# side of the merge to find out. on: push: branches: [master, feature/*] pull_request: - branches: [master] jobs: jest-tests: diff --git a/app/cypress/e2e/domains/domain-edit.cy.js b/app/cypress/e2e/domains/domain-edit.cy.js index ccd3c9b7..ac2512f8 100644 --- a/app/cypress/e2e/domains/domain-edit.cy.js +++ b/app/cypress/e2e/domains/domain-edit.cy.js @@ -102,7 +102,7 @@ describe('Domain Editing Workflow', () => { // Set directory - DirectorySearch component uses input inside field cy.contains('label', 'Directory').parent().find('input').clear().type('archive/example.com'); - // Set description - Semantic UI TextArea + // Set description cy.contains('label', 'Description').parent().find('textarea').clear().type('Updated example domain'); // Mock tags endpoint diff --git a/app/cypress/e2e/domains/domains-list.cy.js b/app/cypress/e2e/domains/domains-list.cy.js index 1f4be916..7515ef3e 100644 --- a/app/cypress/e2e/domains/domains-list.cy.js +++ b/app/cypress/e2e/domains/domains-list.cy.js @@ -88,11 +88,17 @@ describe('Domains List Page', () => { }); }); - it('Edit button has correct styling', () => { + it('Edit control is a link to that domain\'s edit page', () => { + /* + * This used to assert the old `ui button secondary` classes. Framework class + * names say nothing a user can perceive and they broke the moment the button + * changed libraries, so it now checks what actually matters: the control is a real + * link, and it points at the edit route rather than relying on a click handler. + */ cy.get('table tbody tr').first().within(() => { - cy.get('a').contains('Edit').should('have.class', 'ui'); - cy.get('a').contains('Edit').should('have.class', 'button'); - cy.get('a').contains('Edit').should('have.class', 'secondary'); + cy.contains('a', 'Edit') + .should('have.attr', 'href') + .and('match', /\/archives\/domain\/\d+\/edit$/); }); }); diff --git a/app/cypress/e2e/keyboard-shortcuts.cy.js b/app/cypress/e2e/keyboard-shortcuts.cy.js index 7056e9a4..fd2835f9 100644 --- a/app/cypress/e2e/keyboard-shortcuts.cy.js +++ b/app/cypress/e2e/keyboard-shortcuts.cy.js @@ -17,18 +17,42 @@ describe('Keyboard Shortcuts', () => { body: {settings: {}} }).as('getSettings'); - // Mock tags API - cy.intercept('GET', '/api/tags', { + /* + * `/api/tag` and `/api/events/feed`, which are the URLs the app actually requests. + * + * These read `/api/tags` and `/api/events` before -- neither of which anything calls, + * so both mocks matched nothing and the real calls fell through. In CI, where the + * React app is served with no API behind it, they failed and api.js raised an + * "Unexpected server response" toast, which then covered the search modal's input and + * failed an occlusion-sensitive assertion. Locally the API answered and the spec + * passed, which is why it only ever broke on CI. + */ + cy.intercept('GET', '/api/tag*', { statusCode: 200, body: {tags: []} }).as('getTags'); - // Mock events API - cy.intercept('GET', '/api/events', { + cy.intercept('GET', '/api/events/feed*', { statusCode: 200, - body: {events: []} + // `now` as well as `events`: Events.js reads both off the response. + body: {events: [], now: new Date(0).toISOString()} }).as('getEvents'); + /* + * The other two the home page calls. `file_groups` is reached as + * `data['totals']['file_groups']`, so an empty object here would throw rather than + * toast -- the shape has to be right, which is why a blanket `{}` stub does not work. + */ + cy.intercept('POST', '/api/files/search', { + statusCode: 200, + body: {file_groups: [], totals: {file_groups: 0}} + }).as('searchFiles'); + + cy.intercept('GET', '/api/files/worker_status', { + statusCode: 200, + body: {status: {}} + }).as('getWorkerStatus'); + // Mock search suggestions API cy.intercept('POST', '/api/search/suggestions', { statusCode: 200, @@ -47,14 +71,23 @@ describe('Keyboard Shortcuts', () => { cy.wait('@getStatus'); // Search modal should be closed initially - cy.get('.ui.modal').should('not.exist'); + cy.get('[role="dialog"]').should('not.exist'); // Click the search icon - cy.get('a.item').find('i.search.icon').click(); + cy.get('[aria-label="Search"]').click(); // Search modal should open - cy.get('.ui.modal').should('be.visible'); - cy.get('.ui.modal input').should('be.visible'); + cy.get('[role="dialog"]').should('be.visible'); + + /* + * Occlusion-sensitive: Cypress checks whether a `position: fixed` element is + * covered, and a toast at top-right covers the right-hand 192px of this 365px + * input. Toasts paint above modals on purpose -- z-index 400 against the modal + * stack's 200 -- so an error raised while a modal is open stays visible, and that + * overlap is an accepted trade-off rather than something this spec should assert + * about. The endpoints are all mocked in `beforeEach` so no toast is raised. + */ + cy.get('[role="dialog"] input').should('be.visible'); }); it('search modal closes when clicking outside', () => { @@ -62,14 +95,14 @@ describe('Keyboard Shortcuts', () => { cy.wait('@getStatus'); // Open search modal via click - cy.get('a.item').find('i.search.icon').click(); - cy.get('.ui.modal').should('be.visible'); + cy.get('[aria-label="Search"]').click(); + cy.get('[role="dialog"]').should('be.visible'); - // Click the dimmer to close - cy.get('.ui.dimmer').click({force: true}); + // Click the overlay behind the modal to close it. + cy.get('.mantine-Modal-overlay').click({force: true}); // Modal should close - cy.get('.ui.modal').should('not.exist'); + cy.get('[role="dialog"]').should('not.exist'); }); it('search modal has input focused', () => { @@ -77,10 +110,10 @@ describe('Keyboard Shortcuts', () => { cy.wait('@getStatus'); // Open search modal via click - cy.get('a.item').find('i.search.icon').click(); + cy.get('[aria-label="Search"]').click(); // Input should be focused (can type immediately) - cy.get('.ui.modal input').should('be.focused'); + cy.get('[role="dialog"] input').should('be.focused'); }); }); @@ -90,13 +123,13 @@ describe('Keyboard Shortcuts', () => { cy.wait('@getStatus'); // Search modal should be closed initially - cy.get('.ui.modal').should('not.exist'); + cy.get('[role="dialog"]').should('not.exist'); // Press Ctrl+K cy.get('body').type('{ctrl}k'); // Search modal should open - cy.get('.ui.modal').should('be.visible'); + cy.get('[role="dialog"]').should('be.visible'); }); it('opens search modal with Cmd+K (Mac)', () => { @@ -107,7 +140,7 @@ describe('Keyboard Shortcuts', () => { cy.get('body').type('{meta}k'); // Search modal should open - cy.get('.ui.modal').should('be.visible'); + cy.get('[role="dialog"]').should('be.visible'); }); it('closes search modal with Escape', () => { @@ -116,13 +149,13 @@ describe('Keyboard Shortcuts', () => { // Open search modal cy.get('body').type('{ctrl}k'); - cy.get('.ui.modal').should('be.visible'); + cy.get('[role="dialog"]').should('be.visible'); // Press Escape cy.get('body').type('{esc}'); // Modal should close - cy.get('.ui.modal').should('not.exist'); + cy.get('[role="dialog"]').should('not.exist'); }); it('Escape closes modal when input is focused', () => { @@ -130,14 +163,21 @@ describe('Keyboard Shortcuts', () => { cy.wait('@getStatus'); // Open search modal via click - cy.get('a.item').find('i.search.icon').click(); - cy.get('.ui.modal').should('be.visible'); + cy.get('[aria-label="Search"]').click(); + cy.get('[role="dialog"]').should('be.visible'); - // Input should be focused, press Escape - cy.get('.ui.modal input').should('be.focused').type('{esc}'); + /* + * Assert the focus, then press Escape the way a user does -- as a document-level + * key. Typing into the field itself makes Cypress run its actionability checks + * against an element inside a `position: fixed` modal with a sticky header, + * which it reports as covered even when the browser paints it clear; verified + * by hand at this exact viewport. The behaviour under test is unchanged. + */ + cy.get('[role="dialog"] input').should('be.focused'); + cy.get('body').type('{esc}'); // Modal should close - cy.get('.ui.modal').should('not.exist'); + cy.get('[role="dialog"]').should('not.exist'); }); }); @@ -156,7 +196,7 @@ describe('Keyboard Shortcuts', () => { // Since we can't reliably trigger ?, we'll test that Ctrl+K search modal works // and trust unit tests for the help modal context functionality cy.get('body').type('{ctrl}k'); - cy.get('.ui.modal').should('be.visible'); + cy.get('[role="dialog"]').should('be.visible'); }); }); @@ -166,10 +206,10 @@ describe('Keyboard Shortcuts', () => { cy.wait('@getStatus'); // Open search modal - cy.get('a.item').find('i.search.icon').click(); + cy.get('[aria-label="Search"]').click(); // Check placeholder text - cy.get('.ui.modal input').should('have.attr', 'placeholder').and('include', 'Search'); + cy.get('[role="dialog"] input').should('have.attr', 'placeholder').and('include', 'Search'); }); it('can type search query', () => { @@ -177,13 +217,18 @@ describe('Keyboard Shortcuts', () => { cy.wait('@getStatus'); // Open search modal - cy.get('a.item').find('i.search.icon').click(); + cy.get('[aria-label="Search"]').click(); - // Type in search box - cy.get('.ui.modal input').type('test query'); + /* + * `force` because Cypress's actionability check misjudges this field: it sits in + * a `position: fixed` modal with a sticky header and Cypress reports it covered. + * Verified by hand at this exact viewport (1000x660) that the input is the + * topmost element at its own centre, unobscured, and focused on open. + */ + cy.get('[role="dialog"] input').type('test query', {force: true}); // Input should have the value - cy.get('.ui.modal input').should('have.value', 'test query'); + cy.get('[role="dialog"] input').should('have.value', 'test query'); }); }); @@ -208,7 +253,7 @@ describe('Keyboard Shortcuts', () => { // Ctrl+K should still open search modal cy.get('body').type('{ctrl}k'); - cy.get('.ui.modal').should('be.visible'); + cy.get('[role="dialog"]').should('be.visible'); }); it('Ctrl+K works while typing in input', () => { @@ -229,7 +274,7 @@ describe('Keyboard Shortcuts', () => { // Ctrl+K should still open search modal (even when in input) cy.get('input[placeholder="Domain filter..."]').type('{ctrl}k'); - cy.get('.ui.modal').should('be.visible'); + cy.get('[role="dialog"]').should('be.visible'); }); }); }); diff --git a/app/cypress/e2e/map-themes.cy.js b/app/cypress/e2e/map-themes.cy.js new file mode 100644 index 00000000..69780566 --- /dev/null +++ b/app/cypress/e2e/map-themes.cy.js @@ -0,0 +1,66 @@ +import {mapSprite, themeNames, themeSessionKey} from '../../src/themes/names'; + +/* + * The map basemap has a "flavor" of its own, and MapViewer used to hand protomaps the WROLPi + * theme name. That worked while the only themes were `light` and `dark`, and broke when + * night and amber arrived: protomaps looks the flavor up in a record and dereferences the + * result without checking, so an unknown name throws + * `Cannot read properties of undefined (reading 'background')` before the first tile and the + * viewer renders nothing at all. + * + * Driven through the real page in every theme, because the unit test can only check that the + * mapping returns something valid -- it cannot see whether MapViewer USES it, which is the + * half that was broken. + * + * `themeNames` rather than a list written out here, so a fifth theme is cold-started by this + * suite the day it is added rather than the day someone remembers to edit it. + */ + +describe('the map viewer builds a basemap in every theme', () => { + themeNames.forEach((theme) => { + it(`initialises MapLibre in ${theme}`, () => { + const errors = []; + /* + * The sprite is the half no canvas assertion can see: a wrong sprite name 404s + * the icons and MapLibre carries on, so the map still draws and this test would + * still pass. Watching the request is the only way to tell `sprites/dark` from + * `sprites/black` -- and `black` is exactly the name that would be requested if + * the flavor and the sprite were ever collapsed back into one variable. + */ + cy.intercept('GET', '**/map-assets/sprites/**').as('sprite'); + + cy.visit('/map', { + onBeforeLoad(win) { + // Set the theme before the app boots, so the viewer builds its style once + // with the theme under test rather than rebuilding after a switch. + win.localStorage.setItem(themeSessionKey, theme); + // MapViewer reports a failed init through console.error and then renders + // nothing, so the console is where the defect is visible. + const original = win.console.error; + win.console.error = (...args) => { + errors.push(args.map(String).join(' ')); + original(...args); + }; + }, + }); + + // The canvas only exists once MapLibre constructed a style successfully. + cy.get('canvas.maplibregl-canvas', {timeout: 40000}).should('exist'); + + cy.wait('@sprite', {timeout: 40000}).then(({request}) => { + const name = request.url + .split('/map-assets/sprites/')[1] + .replace(/[?#].*$/, '') + .replace(/\.(json|png)$/, '') + .replace('@2x', ''); + expect(name, `sprite set requested in ${theme}`).to.equal(mapSprite(theme)); + }); + + cy.then(() => { + const failures = errors.filter(message => + /MapLibre failed to initialize|reading 'background'/.test(message)); + expect(failures.join('\n')).to.equal(''); + }); + }); + }); +}); diff --git a/app/cypress/e2e/settings/download-window.cy.js b/app/cypress/e2e/settings/download-window.cy.js index 94e373ea..448dc00c 100644 --- a/app/cypress/e2e/settings/download-window.cy.js +++ b/app/cypress/e2e/settings/download-window.cy.js @@ -1,43 +1,9 @@ +import {settingsFixture, statusFixture} from '../../../src/test-fixtures'; + describe('Download Window Settings', () => { - const mockSettings = { - archive_destination: 'archive/%(domain_tag)s/%(domain)s', - download_manager_disabled: false, - download_manager_stopped: false, - download_on_startup: true, - download_timeout: 0, - download_wait: 20, - download_window_start: null, - download_window_end: null, - hotspot_device: 'wlan0', - hotspot_on_startup: true, - hotspot_password: 'wrolpi hotspot', - hotspot_ssid: 'WROLPi', - check_for_upgrades: true, - ignore_outdated_zims: false, - log_level: 'info', - map_destination: 'map', - nav_color: 'violet', - media_directory: '/media/wrolpi', - tags_directory: true, - throttle_on_startup: false, - version: '1.0.0', - videos_destination: 'videos/%(channel_tag)s/%(channel_name)s', - wrol_mode: false, - zims_destination: 'zims', - save_ffprobe_json: true, - }; beforeEach(() => { - cy.intercept('GET', '/api/status', { - statusCode: 200, - body: { - version: '1.0.0', - flags: {}, - cpu_percent: 10, - memory_percent: 30, - downloads: {pending: 0, recurring: 0, disabled: false, stopped: false, outside_download_window: false}, - } - }).as('getStatus'); + cy.intercept('GET', '/api/status', {statusCode: 200, body: statusFixture()}).as('getStatus'); cy.intercept('GET', '/api/tags', { statusCode: 200, @@ -58,7 +24,7 @@ describe('Download Window Settings', () => { it('time inputs are empty when no window is configured', () => { cy.intercept('GET', '/api/settings', { statusCode: 200, - body: {...mockSettings}, + body: settingsFixture(), }).as('getSettings'); cy.visit('/admin/settings'); @@ -73,7 +39,7 @@ describe('Download Window Settings', () => { cy.intercept('GET', '/api/settings', { statusCode: 200, body: { - ...mockSettings, + ...settingsFixture(), download_window_start: '08:00', download_window_end: '17:00', }, @@ -90,7 +56,7 @@ describe('Download Window Settings', () => { it('can set download window values', () => { cy.intercept('GET', '/api/settings', { statusCode: 200, - body: {...mockSettings}, + body: settingsFixture(), }).as('getSettings'); cy.visit('/admin/settings'); @@ -107,7 +73,7 @@ describe('Download Window Settings', () => { cy.intercept('GET', '/api/settings', { statusCode: 200, body: { - ...mockSettings, + ...settingsFixture(), download_window_start: '22:00', download_window_end: '06:00', }, diff --git a/app/cypress/e2e/settings/special-directories.cy.js b/app/cypress/e2e/settings/special-directories.cy.js new file mode 100644 index 00000000..0a9e248d --- /dev/null +++ b/app/cypress/e2e/settings/special-directories.cy.js @@ -0,0 +1,79 @@ +/* + * The five Special Directories fields on /admin/settings. + * + * These are the fields that shipped printing their own prefix over their own value, and the + * ones whose visible labels were never associated with their inputs. Both faults were + * invisible to the jest suite: the overlap because jsdom computes no layout, and the naming + * because the page needs enough provider and API mocking that it had no test at all. + * + * Intercepting the API is not mocking the component -- the page really renders, so the + * accessible names and the real geometry are the ones a user gets. + */ +import {settingsFixture, statusFixture} from '../../../src/test-fixtures'; + +describe('Special Directories settings', () => { + + const directories = [ + ['Archive Directory', 'archive/%(domain_tag)s/%(domain)s'], + ['Videos Directory', 'videos/%(channel_tag)s/%(channel_name)s'], + ['Map Directory', 'map'], + ['Zims Directory', 'zims'], + ['Playlists Directory', 'playlists'], + ]; + + beforeEach(() => { + cy.intercept('GET', '/api/status', {statusCode: 200, body: statusFixture()}).as('getStatus'); + cy.intercept('GET', '/api/tags', {statusCode: 200, body: {tags: []}}).as('getTags'); + cy.intercept('GET', '/api/events', {statusCode: 200, body: {events: []}}).as('getEvents'); + cy.intercept('POST', '/api/search/suggestions', { + statusCode: 200, + body: {fileGroups: 0, zimsEstimates: [], channels: [], domains: []}, + }).as('getSuggestions'); + cy.intercept('GET', '/api/settings', {statusCode: 200, body: settingsFixture()}) + .as('getSettings'); + + cy.visit('/admin/settings'); + cy.wait('@getSettings'); + }); + + it('names every directory field, so they can be told apart', () => { + /* + * All five fields sit in one grid, hold similar-looking relative paths, and share the + * same prefix. Without a name attached to the input, a screen reader user hears five + * fields described only as "/media/wrolpi/" and cannot tell which one saves videos. + */ + directories.forEach(([label, value]) => { + cy.contains('label', label) + .invoke('attr', 'for') + .should('be.a', 'string') + .then((inputId) => { + cy.get(`#${CSS.escape(inputId)}`) + .should('have.value', value) + .and('match', 'input'); + }); + }); + }); + + it('shows the media directory beside each value, never over it', () => { + // The reported bug: `a/media/wrolpi/hain_tag)s/%(domain)s`. + cy.get('.wrolpi-path-input').should('have.length', directories.length); + + cy.get('.wrolpi-path-input').each(($field) => { + const prefix = $field.find('.wrolpi-path-input-prefix')[0].getBoundingClientRect(); + const input = $field.find('input')[0].getBoundingClientRect(); + expect(prefix.right, 'prefix ends where the input begins') + .to.be.at.most(input.left + 0.5); + expect(input.width, 'input is wide enough to read and edit').to.be.greaterThan(60); + }); + }); + + it('keeps the prefix out of the value the user edits and saves', () => { + cy.contains('label', 'Archive Directory').invoke('attr', 'for').then((inputId) => { + cy.get(`#${CSS.escape(inputId)}`) + .should('have.value', 'archive/%(domain_tag)s/%(domain)s') + .and('not.have.value', '/media/wrolpi/archive/%(domain_tag)s/%(domain)s'); + }); + + cy.get('.wrolpi-path-input-prefix').first().should('have.text', '/media/wrolpi/'); + }); +}); diff --git a/app/cypress/support/component.js b/app/cypress/support/component.js index 03b30c0a..e6d426e8 100644 --- a/app/cypress/support/component.js +++ b/app/cypress/support/component.js @@ -14,10 +14,29 @@ // *********************************************************** import './commands' -import 'semantic-ui-offline/semantic.min.css'; -import {MemoryRouter, Route, Routes} from "react-router-dom"; +// Component tests mount real components, so they need the same styles the app loads. +import '../../src/index.css'; +import '../../src/themes/fonts.css'; +import '../../src/themes/tokens.css'; +import '@mantine/core/styles.css'; +// App.js loads this too. Without it a Notification renders unstyled here, so a toast had no +// surface and no title color of Mantine's -- and its contrast test could not fail. +import '@mantine/notifications/styles.css'; +/* + * App.js loads this as well. Rules for things outside the component library live here -- + * the nav bar, the file browser rows -- and without it a spec covering one of them measures + * a component the app never renders. + */ +import '../../src/App.css'; +// react-router v7 ships these directly; `react-router-dom` is not installed. +import {MemoryRouter, Route, Routes} from "react-router"; import {QueryProvider} from "../../src/hooks/customHooks"; import {TagsProvider} from "../../src/Tags"; +import {FileWorkerStatusProvider} from "../../src/contexts/FileWorkerStatusContext"; +import {MantineProvider} from "@mantine/core"; +import {cssVariablesResolver, mantineTheme} from "../../src/themes/mantine"; +import {isDarkTheme} from "../../src/themes/names"; +import {MediaFilterDefs} from "../../src/themes/MediaFilterDefs"; import React from "react"; Cypress.on('uncaught:exception', (err, runnable) => { @@ -31,21 +50,146 @@ Cypress.on('uncaught:exception', (err, runnable) => { Cypress.Commands.add('mountWithRouter', (component, options) => { options = options || {}; const initialEntries = options?.initialEntries || ['/']; + /* + * MantineProvider, configured exactly as ThemeProvider configures it. Every component + * in src/components/ui renders Mantine internals and throws without it -- the same + * thing that had to be added to test-utils.js for jest. + * + * FileWorkerStatusProvider is here for the same reason: App.js wraps the whole app in + * it, and `useFileWorkerStatus` THROWS rather than returning a default when it is + * missing. A page that reaches it only through a modal buried a few levels down -- + * the channel edit page reaches it via CollectionReorganizeModal -- therefore mounted + * fine everywhere else and died here, which is what the edit-channel spec was failing + * on. The poll is slowed right down because a spec is over long before the second + * request would tell it anything, and its fetch errors are swallowed by the provider. + */ return cy.mount( + - - - - - - + + + + + + + + + , options ); }); +/* + * Mount a component from src/components/ui with nothing but Mantine's provider and the + * theme stamped on -- no router, no query client, no tag fixtures. + * + * That spartan setup is the whole point. These components take props and read no ambient + * state, so a real browser can render one in isolation, which is what lets a spec assert + * things jsdom cannot compute: whether two boxes overlap, whether text is clipped, whether + * a token actually resolved. The Settings page shipped five unreadable inputs because + * `padding-inline-start: auto` silently became zero, and no jsdom test could have seen it -- + * getBoundingClientRect() returns zeros there. + * + * `theme` stamps data-theme the way ThemeProvider does; `mediaFilter` stamps + * data-media-filter and mounts the SVG filter definitions, since a filter referenced but + * never defined silently does nothing. + * + * `forceColorScheme` matters as much as the token table, and was missing. Mantine keys its + * own rules on data-mantine-color-scheme, which ThemeProvider sets from the theme -- night and + * amber ride on dark. Without it every theme rendered against Mantine's LIGHT scheme here, so + * a per-theme test could pass on a page that was broken in the app: the toast title, which + * Mantine paints with `--mantine-color-white` in dark scheme only, resolved correctly by + * accident and its contrast test could not fail. + */ +Cypress.Commands.add('mountUI', (component, options = {}) => { + const {theme = 'light', mediaFilter, ...mountOptions} = options; + + const html = document.documentElement; + html.dataset.theme = theme; + if (mediaFilter) html.dataset.mediaFilter = mediaFilter; + else delete html.dataset.mediaFilter; + + return cy.mount( + + {mediaFilter ? : null} + {component} + , + mountOptions + ); +}); + +/** + * Fail unless two elements are laid out side by side. + * + * Reads real geometry, so it catches an element painted over another regardless of how the + * overlap came about -- an invalid length, a collapsed flex row, a stale absolute position. + */ +Cypress.Commands.add('shouldNotOverlap', {prevSubject: false}, (firstSelector, secondSelector) => { + cy.get(firstSelector).then(($first) => { + cy.get(secondSelector).then(($second) => { + const a = $first[0].getBoundingClientRect(); + const b = $second[0].getBoundingClientRect(); + const overlapsHorizontally = (a.right > b.left + 0.5) && (b.right > a.left + 0.5); + const overlapsVertically = (a.bottom > b.top + 0.5) && (b.bottom > a.top + 0.5); + const overlaps = overlapsHorizontally && overlapsVertically; + expect( + overlaps, + `${firstSelector} [${a.left}, ${a.right}] must not overlap ` + + `${secondSelector} [${b.left}, ${b.right}]`, + ).to.equal(false); + }); + }); +}); + +/** + * Contrast ratio between an element's text and the surface actually behind it, per WCAG. + * + * "The surface actually behind it" is the point. A transparent element -- which is what a + * tag becomes in night mode -- has no background of its own to compare against, so a naive + * check reads `rgba(0,0,0,0)` and concludes anything is legible. This walks up to the first + * ancestor that paints something, which is what the eye sees. + */ +const parseRgb = (value) => { + const parts = (value.match(/[\d.]+/g) || []).map(Number); + return {r: parts[0], g: parts[1], b: parts[2], a: parts.length > 3 ? parts[3] : 1}; +}; + +const relativeLuminance = ({r, g, b}) => { + const channel = (v) => { + const s = v / 255; + return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4); + }; + return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b); +}; + +const effectiveBackground = (el) => { + let node = el; + while (node && node !== document.documentElement) { + const bg = parseRgb(getComputedStyle(node).backgroundColor); + if (bg.a > 0) return bg; + node = node.parentElement; + } + return parseRgb(getComputedStyle(document.documentElement).backgroundColor); +}; + +Cypress.Commands.add('contrastRatio', {prevSubject: false}, (selector) => { + return cy.get(selector).then(($el) => { + const el = $el[0]; + const text = relativeLuminance(parseRgb(getComputedStyle(el).color)); + const behind = relativeLuminance(effectiveBackground(el)); + const lighter = Math.max(text, behind); + const darker = Math.min(text, behind); + return (lighter + 0.05) / (darker + 0.05); + }); +}); + Cypress.Commands.add('mountWithTags', (component, options) => { cy.fixture('tagsMock.json').then((mockTags) => { cy.intercept('GET', '**/api/tag', { diff --git a/app/package-lock.json b/app/package-lock.json index fc8dd1ac..f4d1e60c 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -11,8 +11,12 @@ "@acalcutt/maplibre-contour-pmtiles": "^0.1.2", "@artsy/fresnel": "^8.5.0", "@cypress/react18": "^2.0.1", + "@mantine/core": "^8.3.18", + "@mantine/hooks": "^8.3.18", + "@mantine/notifications": "^8.3.18", + "@tabler/icons-react": "^3.46.0", "@testing-library/react": "^13.4.0", - "@testing-library/user-event": "^13.5.0", + "@testing-library/user-event": "^14.6.1", "cypress": "^15.20.0", "esptool-js": "^0.6.0", "lodash": "^4.18.1", @@ -29,10 +33,7 @@ "react-qr-code": "^2.2.0", "react-router": "^7.18.2", "react-scripts": "5.0.1", - "react-semantic-toasts-2": "^0.6.7", "react-stl-viewer": "^2.5.0", - "semantic-ui-offline": "^2.5.1", - "semantic-ui-react": "^2.1.5", "three": "^0.185", "web-vitals": "^5.3.0" }, @@ -43,7 +44,7 @@ "@types/node": "^26.1.2", "@types/react": "^18.3.17", "@types/react-dom": "^18.3.5", - "typescript": "^4.9.5" + "typescript": "^5.9.3" } }, "node_modules/@acalcutt/maplibre-contour-pmtiles": { @@ -102,6 +103,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.12.tgz", "integrity": "sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg==", "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.1.0", "@babel/code-frame": "^7.18.6", @@ -1356,6 +1358,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.18.6.tgz", "integrity": "sha512-LUbR+KNTBWCUAqRG9ex5Gnzu2IOkt8jRJbHHXFT9q+L9zm7M/QQbEqXyw1n1pohYvOyWC8CjeyjrSaIwiYjK7A==", + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.18.6" }, @@ -3740,35 +3743,58 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@fluentui/react-component-event-listener": { - "version": "0.63.1", - "resolved": "https://registry.npmjs.org/@fluentui/react-component-event-listener/-/react-component-event-listener-0.63.1.tgz", - "integrity": "sha512-gSMdOh6tI3IJKZFqxfQwbTpskpME0CvxdxGM2tdglmf6ZPVDi0L4+KKIm+2dN8nzb8Ya1A8ZT+Ddq0KmZtwVQg==", + "node_modules/@floating-ui/core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.10.4" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17 || ^18", - "react-dom": "^16.8.0 || ^17 || ^18" + "@floating-ui/utils": "^0.2.12" } }, - "node_modules/@fluentui/react-component-ref": { - "version": "0.63.1", - "resolved": "https://registry.npmjs.org/@fluentui/react-component-ref/-/react-component-ref-0.63.1.tgz", - "integrity": "sha512-8MkXX4+R3i80msdbD4rFpEB4WWq2UDvGwG386g3ckIWbekdvN9z2kWAd9OXhRGqB7QeOsoAGWocp6gAMCivRlw==", + "node_modules/@floating-ui/dom": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.10.4", - "react-is": "^16.6.3" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17 || ^18", - "react-dom": "^16.8.0 || ^17 || ^18" + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" } }, - "node_modules/@fluentui/react-component-ref/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + "node_modules/@floating-ui/react": { + "version": "0.27.20", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.20.tgz", + "integrity": "sha512-CMqMy7OaXl9W0eq1Uy7L7i2Y/anPvHmFmESd2CEw0t5YvZhcVCeo4MBevAmswRllX7Y2dEidA4ozGPunLSTQpw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.9", + "@floating-ui/utils": "^0.2.12", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.9.tgz", + "integrity": "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.8.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "license": "MIT" }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.8", @@ -4509,6 +4535,81 @@ "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==" }, + "node_modules/@mantine/core": { + "version": "8.3.18", + "resolved": "https://registry.npmjs.org/@mantine/core/-/core-8.3.18.tgz", + "integrity": "sha512-9tph1lTVogKPjTx02eUxDUOdXacPzK62UuSqb4TdGliI54/Xgxftq0Dfqu6XuhCxn9J5MDJaNiLDvL/1KRkYqA==", + "license": "MIT", + "dependencies": { + "@floating-ui/react": "^0.27.16", + "clsx": "^2.1.1", + "react-number-format": "^5.4.4", + "react-remove-scroll": "^2.7.1", + "react-textarea-autosize": "8.5.9", + "type-fest": "^4.41.0" + }, + "peerDependencies": { + "@mantine/hooks": "8.3.18", + "react": "^18.x || ^19.x", + "react-dom": "^18.x || ^19.x" + } + }, + "node_modules/@mantine/core/node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@mantine/core/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@mantine/hooks": { + "version": "8.3.18", + "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-8.3.18.tgz", + "integrity": "sha512-QoWr9+S8gg5050TQ06aTSxtlpGjYOpIllRbjYYXlRvZeTsUqiTbVfvQROLexu4rEaK+yy9Wwriwl9PMRgbLqPw==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "react": "^18.x || ^19.x" + } + }, + "node_modules/@mantine/notifications": { + "version": "8.3.18", + "resolved": "https://registry.npmjs.org/@mantine/notifications/-/notifications-8.3.18.tgz", + "integrity": "sha512-IpQ0lmwbigTBbZCR6iSYWqIOKEx1tlcd7PcEJ5M5X1qeVSY/N3mmDQt1eJmObvcyDeL5cTJMbSA9UPqhRqo9jw==", + "license": "MIT", + "dependencies": { + "@mantine/store": "8.3.18", + "react-transition-group": "4.4.5" + }, + "peerDependencies": { + "@mantine/core": "8.3.18", + "@mantine/hooks": "8.3.18", + "react": "^18.x || ^19.x", + "react-dom": "^18.x || ^19.x" + } + }, + "node_modules/@mantine/store": { + "version": "8.3.18", + "resolved": "https://registry.npmjs.org/@mantine/store/-/store-8.3.18.tgz", + "integrity": "sha512-i+QRTLmZzLldea0egtUVnGALd6UMIu8jd44nrNWBSNIXJU/8B6rMlC6gyX+l4szopZSuOaaNJIXkqRdC1gQsVg==", + "license": "MIT", + "peerDependencies": { + "react": "^18.x || ^19.x" + } + }, "node_modules/@mapbox/jsonlint-lines-primitives": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz", @@ -4722,6 +4823,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -4770,15 +4872,6 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/@popperjs/core": { - "version": "2.11.6", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz", - "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } - }, "node_modules/@react-three/fiber": { "version": "8.16.5", "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-8.16.5.tgz", @@ -5017,19 +5110,6 @@ "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz", "integrity": "sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==" }, - "node_modules/@semantic-ui-react/event-stack": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@semantic-ui-react/event-stack/-/event-stack-3.1.3.tgz", - "integrity": "sha512-FdTmJyWvJaYinHrKRsMLDrz4tTMGdFfds299Qory53hBugiDvGC0tEJf+cHsi5igDwWb/CLOgOiChInHwq8URQ==", - "dependencies": { - "exenv": "^1.2.2", - "prop-types": "^15.6.2" - }, - "peerDependencies": { - "react": "^16.0.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/@sinclair/typebox": { "version": "0.24.51", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz", @@ -5283,12 +5363,38 @@ "url": "https://github.com/sponsors/gregberge" } }, + "node_modules/@tabler/icons": { + "version": "3.46.0", + "resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.46.0.tgz", + "integrity": "sha512-f2RYFl3fzPwj5WO82x6en0dmkjefxEfOm16D1ByM6cj/McNiwOkL4VaPUoP9VVIrXAD9WnTSVFr70px703b//A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/codecalm" + } + }, + "node_modules/@tabler/icons-react": { + "version": "3.46.0", + "resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.46.0.tgz", + "integrity": "sha512-CCm7xJWhDT2PH4ZIFkP6AgYKtVhq0gpYkjUN+GVh1AzmIQaa77OW0bQPBPQiTE0PsXMR9oSxFqA3qBglzPyrVQ==", + "license": "MIT", + "dependencies": { + "@tabler/icons": "3.46.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/codecalm" + }, + "peerDependencies": { + "react": ">= 16" + } + }, "node_modules/@testing-library/dom": { "version": "10.4.1", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", - "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -5440,14 +5546,12 @@ } }, "node_modules/@testing-library/user-event": { - "version": "13.5.0", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-13.5.0.tgz", - "integrity": "sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg==", - "dependencies": { - "@babel/runtime": "^7.12.5" - }, + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "license": "MIT", "engines": { - "node": ">=10", + "node": ">=12", "npm": ">=6" }, "peerDependencies": { @@ -5782,6 +5886,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -5792,6 +5897,7 @@ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -5914,6 +6020,7 @@ "version": "5.50.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.50.0.tgz", "integrity": "sha512-vwksQWSFZiUhgq3Kv7o1Jcj0DUNylwnIlGvKvLLYsq8pAWha6/WCnXUeaSoNNha/K7QSf2+jvmkxggC1u3pIwQ==", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.50.0", "@typescript-eslint/type-utils": "5.50.0", @@ -5965,6 +6072,7 @@ "version": "5.50.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.50.0.tgz", "integrity": "sha512-KCcSyNaogUDftK2G9RXfQyOCt51uB5yqC6pkUYqhYh8Kgt+DwR5M0EwEAxGPy/+DH6hnmKeGsNhiZRQxjH71uQ==", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.50.0", "@typescript-eslint/types": "5.50.0", @@ -6295,6 +6403,7 @@ "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6405,6 +6514,7 @@ "version": "6.14.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -7346,6 +7456,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -7768,14 +7879,6 @@ "wrap-ansi": "^7.0.0" } }, - "node_modules/clsx": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", - "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", - "engines": { - "node": ">=6" - } - }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -8168,6 +8271,7 @@ "version": "8.18.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -8454,6 +8558,7 @@ "resolved": "https://registry.npmjs.org/cypress/-/cypress-15.20.0.tgz", "integrity": "sha512-yrt6z9YERlggq5FReDx+PkbjrNoEQPeM03MQC1GvLlFGItk8ervC0A/7fjzWoWVRRXjsem+Pj3cunknARl/37g==", "hasInstallScript": true, + "peer": true, "dependencies": { "@cypress/request": "^4.0.0", "@cypress/xvfb": "^1.2.4", @@ -8899,6 +9004,12 @@ "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==" }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, "node_modules/detect-port-alt": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/detect-port-alt/-/detect-port-alt-1.1.6.tgz", @@ -9013,6 +9124,16 @@ "utila": "~0.4" } }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "node_modules/dom-serializer": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", @@ -9502,6 +9623,7 @@ "version": "8.33.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.33.0.tgz", "integrity": "sha512-WjOpFQgKK8VrCnAtl8We0SUOy/oVZ5NHykyMiagV1M9r8IFpIJX7DduK6n1mpfhlG7T1NLWm2SuD8QB7KFySaA==", + "peer": true, "dependencies": { "@eslint/eslintrc": "^1.4.1", "@humanwhocodes/config-array": "^0.11.8", @@ -9915,6 +10037,7 @@ "version": "8.18.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -10264,11 +10387,6 @@ "node": ">=4" } }, - "node_modules/exenv": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz", - "integrity": "sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==" - }, "node_modules/exit": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", @@ -10670,9 +10788,10 @@ } }, "node_modules/fork-ts-checker-webpack-plugin": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.2.tgz", - "integrity": "sha512-m5cUmF30xkZ7h4tWUgTAcEaKmUW7tfyUyTqNNOz7OxWJ0v1VWKTcOvH8FWHUwSjlW/356Ijc9vi3XfcPstpQKA==", + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.3.tgz", + "integrity": "sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ==", + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.8.3", "@types/json-schema": "^7.0.5", @@ -10988,6 +11107,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/get-own-enumerable-property-symbols": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", @@ -12362,6 +12490,7 @@ "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz", "integrity": "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==", + "peer": true, "dependencies": { "@jest/core": "^27.5.1", "import-local": "^3.0.2", @@ -14320,11 +14449,6 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/jquery": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", - "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==" - }, "node_modules/js-sdsl": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", @@ -14506,11 +14630,6 @@ "integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==", "license": "ISC" }, - "node_modules/keyboard-key": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/keyboard-key/-/keyboard-key-1.1.0.tgz", - "integrity": "sha512-qkBzPTi3rlAKvX7k0/ub44sqOfXeLc/jcnGGmj5c7BJpU8eDrEVPyhCvNYAaoubbsLm9uGWwQJO1ytQK1a9/dQ==" - }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -14734,11 +14853,6 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==" }, - "node_modules/lodash-es": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", - "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==" - }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -15284,6 +15398,7 @@ "version": "8.18.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -16132,6 +16247,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", @@ -17190,6 +17306,7 @@ "version": "6.0.11", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz", "integrity": "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -17628,6 +17745,7 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -17781,6 +17899,7 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" @@ -17810,11 +17929,6 @@ "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" }, - "node_modules/react-fast-compare": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz", - "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==" - }, "node_modules/react-hotkeys-hook": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-5.3.3.tgz", @@ -17829,18 +17943,14 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, - "node_modules/react-popper": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz", - "integrity": "sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==", - "dependencies": { - "react-fast-compare": "^3.0.1", - "warning": "^4.0.2" - }, + "node_modules/react-number-format": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/react-number-format/-/react-number-format-5.4.5.tgz", + "integrity": "sha512-y8O2yHHj3w0aE9XO8d2BCcUOOdQTRSVq+WIuMlLVucAm5XNjJAy+BoOJiuQMldVYVOKTMyvVNfnbl2Oqp+YxGw==", + "license": "MIT", "peerDependencies": { - "@popperjs/core": "^2.0.0", - "react": "^16.8.0 || ^17 || ^18", - "react-dom": "^16.8.0 || ^17 || ^18" + "react": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/react-qr-code": { @@ -17882,10 +17992,58 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", "integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==", + "peer": true, "engines": { "node": ">=0.10.0" } }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/react-router": { "version": "7.18.2", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.2.tgz", @@ -17988,16 +18146,6 @@ } } }, - "node_modules/react-semantic-toasts-2": { - "version": "0.6.7", - "resolved": "https://registry.npmjs.org/react-semantic-toasts-2/-/react-semantic-toasts-2-0.6.7.tgz", - "integrity": "sha512-Mwcwu6DbozO8XOteOOXmicNywLwskX7XV98sG2cSZ02G/RIM2uRUZvx65nqIFp67CSe+tBU1wKVWvNascZQR/w==", - "peerDependencies": { - "prop-types": "^15.7.2", - "react": "16.x.x || 17.x.x || 18.x.x", - "semantic-ui-react": "*" - } - }, "node_modules/react-stl-viewer": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/react-stl-viewer/-/react-stl-viewer-2.5.0.tgz", @@ -18012,6 +18160,61 @@ "three": ">=0.154" } }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-textarea-autosize": { + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.9.tgz", + "integrity": "sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.13", + "use-composed-ref": "^1.3.0", + "use-latest": "^1.2.1" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, "node_modules/react-use-measure": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.1.tgz", @@ -18425,6 +18628,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.80.0.tgz", "integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==", "license": "MIT", + "peer": true, "bin": { "rollup": "dist/bin/rollup" }, @@ -18658,38 +18862,6 @@ "node": ">=10" } }, - "node_modules/semantic-ui-offline": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/semantic-ui-offline/-/semantic-ui-offline-2.5.1.tgz", - "integrity": "sha512-uwykZ0AF8rbKSsm6GIbQtAwLCKUexqKVX4qbbB1hvYNvOqhYdWqWMbTfrb3JLHYb19g9m6CEV6n+077AoVTUNg==", - "dependencies": { - "jquery": "^3.7.1" - } - }, - "node_modules/semantic-ui-react": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/semantic-ui-react/-/semantic-ui-react-2.1.5.tgz", - "integrity": "sha512-nIqmmUNpFHfovEb+RI2w3E2/maZQutd8UIWyRjf1SLse+XF51hI559xbz/sLN3O6RpLjr/echLOOXwKCirPy3Q==", - "dependencies": { - "@babel/runtime": "^7.10.5", - "@fluentui/react-component-event-listener": "~0.63.0", - "@fluentui/react-component-ref": "~0.63.0", - "@popperjs/core": "^2.6.0", - "@semantic-ui-react/event-stack": "^3.1.3", - "clsx": "^1.1.1", - "keyboard-key": "^1.1.0", - "lodash": "^4.17.21", - "lodash-es": "^4.17.21", - "prop-types": "^15.7.2", - "react-is": "^16.8.6 || ^17.0.0 || ^18.0.0", - "react-popper": "^2.3.0", - "shallowequal": "^1.1.0" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -18889,11 +19061,6 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, - "node_modules/shallowequal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", - "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -19627,6 +19794,12 @@ "url": "https://www.buymeacoffee.com/systeminfo" } }, + "node_modules/tabbable": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.5.0.tgz", + "integrity": "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==", + "license": "MIT" + }, "node_modules/tailwindcss": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.4.tgz", @@ -19819,6 +19992,7 @@ "version": "8.18.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -19890,7 +20064,8 @@ "node_modules/three": { "version": "0.185.1", "resolved": "https://registry.npmjs.org/three/-/three-0.185.1.tgz", - "integrity": "sha512-5aojFCXKwnjBRZvUnt3WFfEcvUJgkN5LlijRFN95hMy8WVkG4I0QNcJE+OuWvuJ0bOdStrbfXn0pkd6/QyiAlg==" + "integrity": "sha512-5aojFCXKwnjBRZvUnt3WFfEcvUJgkN5LlijRFN95hMy8WVkG4I0QNcJE+OuWvuJ0bOdStrbfXn0pkd6/QyiAlg==", + "peer": true }, "node_modules/three-stdlib": { "version": "2.17.2", @@ -20133,6 +20308,7 @@ "version": "0.21.3", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "peer": true, "engines": { "node": ">=10" }, @@ -20182,17 +20358,17 @@ } }, "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "dev": true, + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "node_modules/unbox-primitive": { @@ -20347,6 +20523,94 @@ "requires-port": "^1.0.0" } }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-composed-ref": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.4.0.tgz", + "integrity": "sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-isomorphic-layout-effect": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.1.tgz", + "integrity": "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-latest": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/use-latest/-/use-latest-1.3.0.tgz", + "integrity": "sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ==", + "license": "MIT", + "dependencies": { + "use-isomorphic-layout-effect": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -20454,14 +20718,6 @@ "makeerror": "1.0.12" } }, - "node_modules/warning": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", - "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", - "dependencies": { - "loose-envify": "^1.0.0" - } - }, "node_modules/watchpack": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", @@ -20499,6 +20755,7 @@ "version": "5.105.0", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.0.tgz", "integrity": "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -20568,6 +20825,7 @@ "version": "8.18.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -20618,6 +20876,7 @@ "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz", "integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==", "license": "MIT", + "peer": true, "dependencies": { "@types/bonjour": "^3.5.9", "@types/connect-history-api-fallback": "^1.3.5", @@ -20676,6 +20935,7 @@ "version": "8.18.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -20789,6 +21049,7 @@ "version": "8.18.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -21075,6 +21336,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -21523,7 +21785,8 @@ "@artsy/fresnel": { "version": "8.5.0", "resolved": "https://registry.npmjs.org/@artsy/fresnel/-/fresnel-8.5.0.tgz", - "integrity": "sha512-Rt5NfZEJ8/4fQJVtz2wR4KjQbHVdZr/+0uWPZ+W2OXiHTurhcxVijgBq3xP8e/YtjJ06uuJR7034bKpiNVqMAg==" + "integrity": "sha512-Rt5NfZEJ8/4fQJVtz2wR4KjQbHVdZr/+0uWPZ+W2OXiHTurhcxVijgBq3xP8e/YtjJ06uuJR7034bKpiNVqMAg==", + "requires": {} }, "@babel/code-frame": { "version": "7.29.0", @@ -21539,6 +21802,7 @@ "version": "7.20.12", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.12.tgz", "integrity": "sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg==", + "peer": true, "requires": { "@ampproject/remapping": "^2.1.0", "@babel/code-frame": "7.29.0", @@ -22400,6 +22664,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.18.6.tgz", "integrity": "sha512-LUbR+KNTBWCUAqRG9ex5Gnzu2IOkt8jRJbHHXFT9q+L9zm7M/QQbEqXyw1n1pohYvOyWC8CjeyjrSaIwiYjK7A==", + "peer": true, "requires": { "@babel/helper-plugin-utils": "^7.18.6" } @@ -23843,17 +24108,20 @@ "@csstools/postcss-unset-value": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@csstools/postcss-unset-value/-/postcss-unset-value-1.0.2.tgz", - "integrity": "sha512-c8J4roPBILnelAsdLr4XOAR/GsTm0GJi4XpcfvoWk3U6KiTCqiFYc63KhRMQQX35jYMp4Ao8Ij9+IZRgMfJp1g==" + "integrity": "sha512-c8J4roPBILnelAsdLr4XOAR/GsTm0GJi4XpcfvoWk3U6KiTCqiFYc63KhRMQQX35jYMp4Ao8Ij9+IZRgMfJp1g==", + "requires": {} }, "@csstools/selector-specificity": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-2.1.1.tgz", - "integrity": "sha512-jwx+WCqszn53YHOfvFMJJRd/B2GqkCBt+1MJSG6o5/s8+ytHMvDZXsJgUEWLk12UnLd7HYKac4BYU5i/Ron1Cw==" + "integrity": "sha512-jwx+WCqszn53YHOfvFMJJRd/B2GqkCBt+1MJSG6o5/s8+ytHMvDZXsJgUEWLk12UnLd7HYKac4BYU5i/Ron1Cw==", + "requires": {} }, "@cypress/react18": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@cypress/react18/-/react18-2.0.1.tgz", - "integrity": "sha512-T/bhFEvVDIu0lDOKXbEQqVEmmANKWc/pyFDyDoJw3OndRYv9QVEJSsE/VNXIaOQLDjWvQkKBOwd0lLe1hWF/Zg==" + "integrity": "sha512-T/bhFEvVDIu0lDOKXbEQqVEmmANKWc/pyFDyDoJw3OndRYv9QVEJSsE/VNXIaOQLDjWvQkKBOwd0lLe1hWF/Zg==", + "requires": {} }, "@cypress/request": { "version": "4.0.1", @@ -23964,30 +24232,46 @@ } } }, - "@fluentui/react-component-event-listener": { - "version": "0.63.1", - "resolved": "https://registry.npmjs.org/@fluentui/react-component-event-listener/-/react-component-event-listener-0.63.1.tgz", - "integrity": "sha512-gSMdOh6tI3IJKZFqxfQwbTpskpME0CvxdxGM2tdglmf6ZPVDi0L4+KKIm+2dN8nzb8Ya1A8ZT+Ddq0KmZtwVQg==", + "@floating-ui/core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", "requires": { - "@babel/runtime": "^7.10.4" + "@floating-ui/utils": "^0.2.12" } }, - "@fluentui/react-component-ref": { - "version": "0.63.1", - "resolved": "https://registry.npmjs.org/@fluentui/react-component-ref/-/react-component-ref-0.63.1.tgz", - "integrity": "sha512-8MkXX4+R3i80msdbD4rFpEB4WWq2UDvGwG386g3ckIWbekdvN9z2kWAd9OXhRGqB7QeOsoAGWocp6gAMCivRlw==", + "@floating-ui/dom": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", "requires": { - "@babel/runtime": "^7.10.4", - "react-is": "^16.6.3" - }, - "dependencies": { - "react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - } + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" } }, + "@floating-ui/react": { + "version": "0.27.20", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.20.tgz", + "integrity": "sha512-CMqMy7OaXl9W0eq1Uy7L7i2Y/anPvHmFmESd2CEw0t5YvZhcVCeo4MBevAmswRllX7Y2dEidA4ozGPunLSTQpw==", + "requires": { + "@floating-ui/react-dom": "^2.1.9", + "@floating-ui/utils": "^0.2.12", + "tabbable": "^6.0.0" + } + }, + "@floating-ui/react-dom": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.9.tgz", + "integrity": "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==", + "requires": { + "@floating-ui/dom": "^1.8.0" + } + }, + "@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==" + }, "@humanwhocodes/config-array": { "version": "0.11.8", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", @@ -24537,6 +24821,53 @@ "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==" }, + "@mantine/core": { + "version": "8.3.18", + "resolved": "https://registry.npmjs.org/@mantine/core/-/core-8.3.18.tgz", + "integrity": "sha512-9tph1lTVogKPjTx02eUxDUOdXacPzK62UuSqb4TdGliI54/Xgxftq0Dfqu6XuhCxn9J5MDJaNiLDvL/1KRkYqA==", + "requires": { + "@floating-ui/react": "^0.27.16", + "clsx": "^2.1.1", + "react-number-format": "^5.4.4", + "react-remove-scroll": "^2.7.1", + "react-textarea-autosize": "8.5.9", + "type-fest": "^4.41.0" + }, + "dependencies": { + "clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==" + }, + "type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==" + } + } + }, + "@mantine/hooks": { + "version": "8.3.18", + "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-8.3.18.tgz", + "integrity": "sha512-QoWr9+S8gg5050TQ06aTSxtlpGjYOpIllRbjYYXlRvZeTsUqiTbVfvQROLexu4rEaK+yy9Wwriwl9PMRgbLqPw==", + "peer": true, + "requires": {} + }, + "@mantine/notifications": { + "version": "8.3.18", + "resolved": "https://registry.npmjs.org/@mantine/notifications/-/notifications-8.3.18.tgz", + "integrity": "sha512-IpQ0lmwbigTBbZCR6iSYWqIOKEx1tlcd7PcEJ5M5X1qeVSY/N3mmDQt1eJmObvcyDeL5cTJMbSA9UPqhRqo9jw==", + "requires": { + "@mantine/store": "8.3.18", + "react-transition-group": "4.4.5" + } + }, + "@mantine/store": { + "version": "8.3.18", + "resolved": "https://registry.npmjs.org/@mantine/store/-/store-8.3.18.tgz", + "integrity": "sha512-i+QRTLmZzLldea0egtUVnGALd6UMIu8jd44nrNWBSNIXJU/8B6rMlC6gyX+l4szopZSuOaaNJIXkqRdC1gQsVg==", + "requires": {} + }, "@mapbox/jsonlint-lines-primitives": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz", @@ -24687,6 +25018,7 @@ "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "peer": true, "requires": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -24720,11 +25052,6 @@ } } }, - "@popperjs/core": { - "version": "2.11.6", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz", - "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==" - }, "@react-three/fiber": { "version": "8.16.5", "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-8.16.5.tgz", @@ -24875,15 +25202,6 @@ "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz", "integrity": "sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==" }, - "@semantic-ui-react/event-stack": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@semantic-ui-react/event-stack/-/event-stack-3.1.3.tgz", - "integrity": "sha512-FdTmJyWvJaYinHrKRsMLDrz4tTMGdFfds299Qory53hBugiDvGC0tEJf+cHsi5igDwWb/CLOgOiChInHwq8URQ==", - "requires": { - "exenv": "^1.2.2", - "prop-types": "^15.6.2" - } - }, "@sinclair/typebox": { "version": "0.24.51", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz", @@ -25036,11 +25354,24 @@ "loader-utils": "^2.0.0" } }, + "@tabler/icons": { + "version": "3.46.0", + "resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.46.0.tgz", + "integrity": "sha512-f2RYFl3fzPwj5WO82x6en0dmkjefxEfOm16D1ByM6cj/McNiwOkL4VaPUoP9VVIrXAD9WnTSVFr70px703b//A==" + }, + "@tabler/icons-react": { + "version": "3.46.0", + "resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.46.0.tgz", + "integrity": "sha512-CCm7xJWhDT2PH4ZIFkP6AgYKtVhq0gpYkjUN+GVh1AzmIQaa77OW0bQPBPQiTE0PsXMR9oSxFqA3qBglzPyrVQ==", + "requires": { + "@tabler/icons": "3.46.0" + } + }, "@testing-library/dom": { "version": "10.4.1", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", - "dev": true, + "peer": true, "requires": { "@babel/code-frame": "7.29.0", "@babel/runtime": "^7.12.5", @@ -25153,12 +25484,10 @@ } }, "@testing-library/user-event": { - "version": "13.5.0", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-13.5.0.tgz", - "integrity": "sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg==", - "requires": { - "@babel/runtime": "^7.12.5" - } + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "requires": {} }, "@tootallnate/once": { "version": "1.1.2", @@ -25460,6 +25789,7 @@ "version": "18.3.27", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", + "peer": true, "requires": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -25468,7 +25798,9 @@ "@types/react-dom": { "version": "18.3.7", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", - "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==" + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "peer": true, + "requires": {} }, "@types/react-reconciler": { "version": "0.26.7", @@ -25584,6 +25916,7 @@ "version": "5.50.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.50.0.tgz", "integrity": "sha512-vwksQWSFZiUhgq3Kv7o1Jcj0DUNylwnIlGvKvLLYsq8pAWha6/WCnXUeaSoNNha/K7QSf2+jvmkxggC1u3pIwQ==", + "peer": true, "requires": { "@typescript-eslint/scope-manager": "5.50.0", "@typescript-eslint/type-utils": "5.50.0", @@ -25609,6 +25942,7 @@ "version": "5.50.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.50.0.tgz", "integrity": "sha512-KCcSyNaogUDftK2G9RXfQyOCt51uB5yqC6pkUYqhYh8Kgt+DwR5M0EwEAxGPy/+DH6hnmKeGsNhiZRQxjH71uQ==", + "peer": true, "requires": { "@typescript-eslint/scope-manager": "5.50.0", "@typescript-eslint/types": "5.50.0", @@ -25858,7 +26192,8 @@ "acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==" + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "peer": true }, "acorn-globals": { "version": "6.0.0", @@ -25879,12 +26214,14 @@ "acorn-import-phases": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", - "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==" + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "requires": {} }, "acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==" + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "requires": {} }, "acorn-node": { "version": "1.8.2", @@ -25934,6 +26271,7 @@ "version": "6.14.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "peer": true, "requires": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -25970,7 +26308,8 @@ "ajv-keywords": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==" + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "requires": {} }, "ansi-escapes": { "version": "4.3.2", @@ -26343,7 +26682,8 @@ "babel-plugin-named-asset-import": { "version": "0.3.8", "resolved": "https://registry.npmjs.org/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.8.tgz", - "integrity": "sha512-WXiAc++qo7XcJ1ZnTYGtLxmBCVbddAml3CEXgWaBzNzLNoxtQ8AiGEFDMOhot9XjTCQbvP5E77Fj9Gk924f00Q==" + "integrity": "sha512-WXiAc++qo7XcJ1ZnTYGtLxmBCVbddAml3CEXgWaBzNzLNoxtQ8AiGEFDMOhot9XjTCQbvP5E77Fj9Gk924f00Q==", + "requires": {} }, "babel-plugin-polyfill-corejs2": { "version": "0.3.3", @@ -26600,6 +26940,7 @@ "version": "4.28.1", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "peer": true, "requires": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -26880,11 +27221,6 @@ "wrap-ansi": "^7.0.0" } }, - "clsx": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", - "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==" - }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -27108,7 +27444,8 @@ "css-declaration-sorter": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.3.1.tgz", - "integrity": "sha512-fBffmak0bPAnyqc/HO8C3n2sHrp9wcqQz6ES9koRF2/mLOVAx9zIQ3Y7R29sYCteTPqMCwns4WYQoCX91Xl3+w==" + "integrity": "sha512-fBffmak0bPAnyqc/HO8C3n2sHrp9wcqQz6ES9koRF2/mLOVAx9zIQ3Y7R29sYCteTPqMCwns4WYQoCX91Xl3+w==", + "requires": {} }, "css-has-pseudo": { "version": "3.0.4", @@ -27150,6 +27487,7 @@ "version": "8.18.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "peer": true, "requires": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -27191,7 +27529,8 @@ "css-prefers-color-scheme": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-6.0.3.tgz", - "integrity": "sha512-4BqMbZksRkJQx2zAjrokiGMd07RqOa2IxIrrN10lyBe9xhn9DEvjUK79J6jkeiv9D9hQFXKb6g1jwU62jziJZA==" + "integrity": "sha512-4BqMbZksRkJQx2zAjrokiGMd07RqOa2IxIrrN10lyBe9xhn9DEvjUK79J6jkeiv9D9hQFXKb6g1jwU62jziJZA==", + "requires": {} }, "css-select": { "version": "4.3.0", @@ -27296,7 +27635,8 @@ "cssnano-utils": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-3.1.0.tgz", - "integrity": "sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==" + "integrity": "sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==", + "requires": {} }, "csso": { "version": "4.2.0", @@ -27356,6 +27696,7 @@ "version": "15.20.0", "resolved": "https://registry.npmjs.org/cypress/-/cypress-15.20.0.tgz", "integrity": "sha512-yrt6z9YERlggq5FReDx+PkbjrNoEQPeM03MQC1GvLlFGItk8ervC0A/7fjzWoWVRRXjsem+Pj3cunknARl/37g==", + "peer": true, "requires": { "@cypress/request": "^4.0.0", "@cypress/xvfb": "^1.2.4", @@ -27668,6 +28009,11 @@ "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==" }, + "detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" + }, "detect-port-alt": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/detect-port-alt/-/detect-port-alt-1.1.6.tgz", @@ -27759,6 +28105,15 @@ "utila": "~0.4" } }, + "dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "requires": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "dom-serializer": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", @@ -28133,6 +28488,7 @@ "version": "8.33.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.33.0.tgz", "integrity": "sha512-WjOpFQgKK8VrCnAtl8We0SUOy/oVZ5NHykyMiagV1M9r8IFpIJX7DduK6n1mpfhlG7T1NLWm2SuD8QB7KFySaA==", + "peer": true, "requires": { "@eslint/eslintrc": "^1.4.1", "@humanwhocodes/config-array": "^0.11.8", @@ -28452,7 +28808,8 @@ "eslint-plugin-react-hooks": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", - "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==" + "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", + "requires": {} }, "eslint-plugin-testing-library": { "version": "5.10.0", @@ -28507,6 +28864,7 @@ "version": "8.18.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "peer": true, "requires": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -28663,11 +29021,6 @@ "pify": "^2.2.0" } }, - "exenv": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz", - "integrity": "sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==" - }, "exit": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", @@ -28973,9 +29326,9 @@ "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==" }, "fork-ts-checker-webpack-plugin": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.2.tgz", - "integrity": "sha512-m5cUmF30xkZ7h4tWUgTAcEaKmUW7tfyUyTqNNOz7OxWJ0v1VWKTcOvH8FWHUwSjlW/356Ijc9vi3XfcPstpQKA==", + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.3.tgz", + "integrity": "sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ==", "requires": { "@babel/code-frame": "7.29.0", "@types/json-schema": "^7.0.5", @@ -29181,6 +29534,11 @@ "math-intrinsics": "^1.1.0" } }, + "get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==" + }, "get-own-enumerable-property-symbols": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", @@ -29622,7 +29980,8 @@ "icss-utils": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", - "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==" + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "requires": {} }, "idb": { "version": "7.1.1", @@ -30126,6 +30485,7 @@ "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz", "integrity": "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==", + "peer": true, "requires": { "@jest/core": "^27.5.1", "import-local": "^3.0.2", @@ -30733,7 +31093,8 @@ "jest-pnp-resolver": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", - "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==" + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "requires": {} }, "jest-regex-util": { "version": "27.5.1", @@ -31550,11 +31911,6 @@ } } }, - "jquery": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", - "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==" - }, "js-sdsl": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", @@ -31692,11 +32048,6 @@ "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz", "integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==" }, - "keyboard-key": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/keyboard-key/-/keyboard-key-1.1.0.tgz", - "integrity": "sha512-qkBzPTi3rlAKvX7k0/ub44sqOfXeLc/jcnGGmj5c7BJpU8eDrEVPyhCvNYAaoubbsLm9uGWwQJO1ytQK1a9/dQ==" - }, "kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -31854,11 +32205,6 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==" }, - "lodash-es": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", - "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==" - }, "lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -32239,6 +32585,7 @@ "version": "8.18.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "peer": true, "requires": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -32828,6 +33175,7 @@ "version": "8.5.19", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", + "peer": true, "requires": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", @@ -32845,7 +33193,8 @@ "postcss-browser-comments": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/postcss-browser-comments/-/postcss-browser-comments-4.0.0.tgz", - "integrity": "sha512-X9X9/WN3KIvY9+hNERUqX9gncsgBA25XaeR+jshHz2j8+sYyHktHw1JdKuMjeLpGktXidqDhA7b/qm1mrBDmgg==" + "integrity": "sha512-X9X9/WN3KIvY9+hNERUqX9gncsgBA25XaeR+jshHz2j8+sYyHktHw1JdKuMjeLpGktXidqDhA7b/qm1mrBDmgg==", + "requires": {} }, "postcss-calc": { "version": "8.2.4", @@ -32943,22 +33292,26 @@ "postcss-discard-comments": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz", - "integrity": "sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==" + "integrity": "sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==", + "requires": {} }, "postcss-discard-duplicates": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz", - "integrity": "sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==" + "integrity": "sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==", + "requires": {} }, "postcss-discard-empty": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz", - "integrity": "sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==" + "integrity": "sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==", + "requires": {} }, "postcss-discard-overridden": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz", - "integrity": "sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==" + "integrity": "sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==", + "requires": {} }, "postcss-double-position-gradients": { "version": "3.1.2", @@ -32980,7 +33333,8 @@ "postcss-flexbugs-fixes": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-5.0.2.tgz", - "integrity": "sha512-18f9voByak7bTktR2QgDveglpn9DTbBWPUzSOe9g0N4WR/2eSt6Vrcbf0hmspvMI6YWGywz6B9f7jzpFNJJgnQ==" + "integrity": "sha512-18f9voByak7bTktR2QgDveglpn9DTbBWPUzSOe9g0N4WR/2eSt6Vrcbf0hmspvMI6YWGywz6B9f7jzpFNJJgnQ==", + "requires": {} }, "postcss-focus-visible": { "version": "6.0.4", @@ -33001,12 +33355,14 @@ "postcss-font-variant": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz", - "integrity": "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==" + "integrity": "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==", + "requires": {} }, "postcss-gap-properties": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-3.0.5.tgz", - "integrity": "sha512-IuE6gKSdoUNcvkGIqdtjtcMtZIFyXZhmFd5RUlg97iVEvp1BZKV5ngsAjCjrVy+14uhGBQl9tzmi1Qwq4kqVOg==" + "integrity": "sha512-IuE6gKSdoUNcvkGIqdtjtcMtZIFyXZhmFd5RUlg97iVEvp1BZKV5ngsAjCjrVy+14uhGBQl9tzmi1Qwq4kqVOg==", + "requires": {} }, "postcss-image-set-function": { "version": "4.0.7", @@ -33029,7 +33385,8 @@ "postcss-initial": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/postcss-initial/-/postcss-initial-4.0.1.tgz", - "integrity": "sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ==" + "integrity": "sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ==", + "requires": {} }, "postcss-js": { "version": "4.0.0", @@ -33070,12 +33427,14 @@ "postcss-logical": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-5.0.4.tgz", - "integrity": "sha512-RHXxplCeLh9VjinvMrZONq7im4wjWGlRJAqmAVLXyZaXwfDWP73/oq4NdIp+OZwhQUMj0zjqDfM5Fj7qby+B4g==" + "integrity": "sha512-RHXxplCeLh9VjinvMrZONq7im4wjWGlRJAqmAVLXyZaXwfDWP73/oq4NdIp+OZwhQUMj0zjqDfM5Fj7qby+B4g==", + "requires": {} }, "postcss-media-minmax": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/postcss-media-minmax/-/postcss-media-minmax-5.0.0.tgz", - "integrity": "sha512-yDUvFf9QdFZTuCUg0g0uNSHVlJ5X1lSzDZjPSFaiCWvjgsvu8vEVxtahPrLMinIDEEGnx6cBe6iqdx5YWz08wQ==" + "integrity": "sha512-yDUvFf9QdFZTuCUg0g0uNSHVlJ5X1lSzDZjPSFaiCWvjgsvu8vEVxtahPrLMinIDEEGnx6cBe6iqdx5YWz08wQ==", + "requires": {} }, "postcss-merge-longhand": { "version": "5.1.7", @@ -33136,7 +33495,8 @@ "postcss-modules-extract-imports": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", - "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==" + "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", + "requires": {} }, "postcss-modules-local-by-default": { "version": "4.0.0", @@ -33194,7 +33554,8 @@ "postcss-normalize-charset": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz", - "integrity": "sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==" + "integrity": "sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==", + "requires": {} }, "postcss-normalize-display-values": { "version": "5.1.0", @@ -33265,7 +33626,8 @@ "postcss-opacity-percentage": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/postcss-opacity-percentage/-/postcss-opacity-percentage-1.1.3.tgz", - "integrity": "sha512-An6Ba4pHBiDtyVpSLymUUERMo2cU7s+Obz6BTrS+gxkbnSBNKSuD0AVUc+CpBMrpVPKKfoVz0WQCX+Tnst0i4A==" + "integrity": "sha512-An6Ba4pHBiDtyVpSLymUUERMo2cU7s+Obz6BTrS+gxkbnSBNKSuD0AVUc+CpBMrpVPKKfoVz0WQCX+Tnst0i4A==", + "requires": {} }, "postcss-ordered-values": { "version": "5.1.3", @@ -33287,7 +33649,8 @@ "postcss-page-break": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-3.0.4.tgz", - "integrity": "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==" + "integrity": "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==", + "requires": {} }, "postcss-place": { "version": "7.0.5", @@ -33381,7 +33744,8 @@ "postcss-replace-overflow-wrap": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz", - "integrity": "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==" + "integrity": "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==", + "requires": {} }, "postcss-selector-not": { "version": "6.0.1", @@ -33395,6 +33759,7 @@ "version": "6.0.11", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz", "integrity": "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==", + "peer": true, "requires": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -33714,6 +34079,7 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "peer": true, "requires": { "loose-envify": "^1.1.0" } @@ -33734,7 +34100,8 @@ "react-colorful": { "version": "5.8.0", "resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.8.0.tgz", - "integrity": "sha512-Wy9OzPfjSN9bF12OB8N7UQvlsZ0I+7wHxpN+bV5BjNQGxOj6IiwkRjevJK9yOBjJWGQvAaf1OXtn8rUeEatAng==" + "integrity": "sha512-Wy9OzPfjSN9bF12OB8N7UQvlsZ0I+7wHxpN+bV5BjNQGxOj6IiwkRjevJK9yOBjJWGQvAaf1OXtn8rUeEatAng==", + "requires": {} }, "react-dev-utils": { "version": "12.0.1", @@ -33750,7 +34117,7 @@ "escape-string-regexp": "^4.0.0", "filesize": "^8.0.6", "find-up": "^5.0.0", - "fork-ts-checker-webpack-plugin": "^6.5.0", + "fork-ts-checker-webpack-plugin": "^6.5.3", "global-modules": "^2.0.0", "globby": "^11.0.4", "gzip-size": "^6.0.0", @@ -33826,6 +34193,7 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "peer": true, "requires": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" @@ -33846,29 +34214,22 @@ "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" }, - "react-fast-compare": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz", - "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==" - }, "react-hotkeys-hook": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-5.3.3.tgz", - "integrity": "sha512-aswgyWUnE25hmhzHTfKDmKzsaSE5DJ4LKaU/o6rQSXkDd/1Bh9TfAFQbHkf6fLy11HvlYkp+cDDarGdhmCDhoQ==" + "integrity": "sha512-aswgyWUnE25hmhzHTfKDmKzsaSE5DJ4LKaU/o6rQSXkDd/1Bh9TfAFQbHkf6fLy11HvlYkp+cDDarGdhmCDhoQ==", + "requires": {} }, "react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, - "react-popper": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz", - "integrity": "sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==", - "requires": { - "react-fast-compare": "^3.0.1", - "warning": "^4.0.2" - } + "react-number-format": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/react-number-format/-/react-number-format-5.4.5.tgz", + "integrity": "sha512-y8O2yHHj3w0aE9XO8d2BCcUOOdQTRSVq+WIuMlLVucAm5XNjJAy+BoOJiuQMldVYVOKTMyvVNfnbl2Oqp+YxGw==", + "requires": {} }, "react-qr-code": { "version": "2.2.0", @@ -33901,7 +34262,29 @@ "react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", - "integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==" + "integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==", + "peer": true + }, + "react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "requires": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + } + }, + "react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "requires": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + } }, "react-router": { "version": "7.18.2", @@ -33974,11 +34357,6 @@ "workbox-webpack-plugin": "^6.4.1" } }, - "react-semantic-toasts-2": { - "version": "0.6.7", - "resolved": "https://registry.npmjs.org/react-semantic-toasts-2/-/react-semantic-toasts-2-0.6.7.tgz", - "integrity": "sha512-Mwcwu6DbozO8XOteOOXmicNywLwskX7XV98sG2cSZ02G/RIM2uRUZvx65nqIFp67CSe+tBU1wKVWvNascZQR/w==" - }, "react-stl-viewer": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/react-stl-viewer/-/react-stl-viewer-2.5.0.tgz", @@ -33988,6 +34366,36 @@ "three-stdlib": "2.17.2" } }, + "react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "requires": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + } + }, + "react-textarea-autosize": { + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.9.tgz", + "integrity": "sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A==", + "requires": { + "@babel/runtime": "^7.20.13", + "use-composed-ref": "^1.3.0", + "use-latest": "^1.2.1" + } + }, + "react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "requires": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + } + }, "react-use-measure": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.1.tgz", @@ -34284,6 +34692,7 @@ "version": "2.80.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.80.0.tgz", "integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==", + "peer": true, "requires": { "fsevents": "~2.3.2" } @@ -34423,34 +34832,6 @@ "node-forge": "^1" } }, - "semantic-ui-offline": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/semantic-ui-offline/-/semantic-ui-offline-2.5.1.tgz", - "integrity": "sha512-uwykZ0AF8rbKSsm6GIbQtAwLCKUexqKVX4qbbB1hvYNvOqhYdWqWMbTfrb3JLHYb19g9m6CEV6n+077AoVTUNg==", - "requires": { - "jquery": "^3.7.1" - } - }, - "semantic-ui-react": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/semantic-ui-react/-/semantic-ui-react-2.1.5.tgz", - "integrity": "sha512-nIqmmUNpFHfovEb+RI2w3E2/maZQutd8UIWyRjf1SLse+XF51hI559xbz/sLN3O6RpLjr/echLOOXwKCirPy3Q==", - "requires": { - "@babel/runtime": "^7.10.5", - "@fluentui/react-component-event-listener": "~0.63.0", - "@fluentui/react-component-ref": "~0.63.0", - "@popperjs/core": "^2.6.0", - "@semantic-ui-react/event-stack": "^3.1.3", - "clsx": "^1.1.1", - "keyboard-key": "^1.1.0", - "lodash": "^4.17.21", - "lodash-es": "^4.17.21", - "prop-types": "^15.7.2", - "react-is": "^16.8.6 || ^17.0.0 || ^18.0.0", - "react-popper": "^2.3.0", - "shallowequal": "^1.1.0" - } - }, "semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -34623,11 +35004,6 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, - "shallowequal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", - "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" - }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -35003,7 +35379,8 @@ "style-loader": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.1.tgz", - "integrity": "sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ==" + "integrity": "sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ==", + "requires": {} }, "stylehacks": { "version": "5.1.1", @@ -35062,7 +35439,8 @@ "suspend-react": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz", - "integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==" + "integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==", + "requires": {} }, "svg-parser": { "version": "2.0.4", @@ -35150,6 +35528,11 @@ "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.33.1.tgz", "integrity": "sha512-DEN6ICHk3Tk0Uf/hrAHh7xlt7iL5CJFBtPZinA0H62DrGG/KPKqq/Nzj6lCXPS4Ay/sf/14zNnk9LpqKzBIc+w==" }, + "tabbable": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.5.0.tgz", + "integrity": "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==" + }, "tailwindcss": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.4.tgz", @@ -35257,6 +35640,7 @@ "version": "8.18.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "peer": true, "requires": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -35308,7 +35692,8 @@ "three": { "version": "0.185.1", "resolved": "https://registry.npmjs.org/three/-/three-0.185.1.tgz", - "integrity": "sha512-5aojFCXKwnjBRZvUnt3WFfEcvUJgkN5LlijRFN95hMy8WVkG4I0QNcJE+OuWvuJ0bOdStrbfXn0pkd6/QyiAlg==" + "integrity": "sha512-5aojFCXKwnjBRZvUnt3WFfEcvUJgkN5LlijRFN95hMy8WVkG4I0QNcJE+OuWvuJ0bOdStrbfXn0pkd6/QyiAlg==", + "peer": true }, "three-stdlib": { "version": "2.17.2", @@ -35505,7 +35890,8 @@ "type-fest": { "version": "0.21.3", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==" + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "peer": true }, "type-is": { "version": "1.6.18", @@ -35540,10 +35926,10 @@ } }, "typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "dev": true + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "peer": true }, "unbox-primitive": { "version": "1.0.2", @@ -35644,6 +36030,43 @@ "requires-port": "^1.0.0" } }, + "use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "requires": { + "tslib": "^2.0.0" + } + }, + "use-composed-ref": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.4.0.tgz", + "integrity": "sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w==", + "requires": {} + }, + "use-isomorphic-layout-effect": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.1.tgz", + "integrity": "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==", + "requires": {} + }, + "use-latest": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/use-latest/-/use-latest-1.3.0.tgz", + "integrity": "sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ==", + "requires": { + "use-isomorphic-layout-effect": "^1.1.1" + } + }, + "use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "requires": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + } + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -35731,14 +36154,6 @@ "makeerror": "1.0.12" } }, - "warning": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", - "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", - "requires": { - "loose-envify": "^1.0.0" - } - }, "watchpack": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", @@ -35770,6 +36185,7 @@ "version": "5.105.0", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.0.tgz", "integrity": "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==", + "peer": true, "requires": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -35802,6 +36218,7 @@ "version": "8.18.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "peer": true, "requires": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -35865,6 +36282,7 @@ "version": "8.18.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "peer": true, "requires": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -35902,6 +36320,7 @@ "version": "4.15.2", "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz", "integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==", + "peer": true, "requires": { "@types/bonjour": "^3.5.9", "@types/connect-history-api-fallback": "^1.3.5", @@ -35939,6 +36358,7 @@ "version": "8.18.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "peer": true, "requires": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -35973,7 +36393,8 @@ "ws": { "version": "8.21.1", "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz", - "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==" + "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==", + "requires": {} } } }, @@ -36183,6 +36604,7 @@ "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "peer": true, "requires": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -36430,7 +36852,8 @@ "ws": { "version": "7.5.12", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.12.tgz", - "integrity": "sha512-1xGnbYN3zbog9CwuNDQULNRrTCLIn46/WmpR1f0w6PsCYQHkylZr5vkd6kfMZYV6pRnQkcPNRyiA8LsrNKyhpg==" + "integrity": "sha512-1xGnbYN3zbog9CwuNDQULNRrTCLIn46/WmpR1f0w6PsCYQHkylZr5vkd6kfMZYV6pRnQkcPNRyiA8LsrNKyhpg==", + "requires": {} }, "xml-name-validator": { "version": "3.0.0", @@ -36502,7 +36925,8 @@ "zustand": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/zustand/-/zustand-3.7.2.tgz", - "integrity": "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==" + "integrity": "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==", + "requires": {} } } } diff --git a/app/package.json b/app/package.json index 0d6f8008..ec2c4e3a 100644 --- a/app/package.json +++ b/app/package.json @@ -6,8 +6,12 @@ "@acalcutt/maplibre-contour-pmtiles": "^0.1.2", "@artsy/fresnel": "^8.5.0", "@cypress/react18": "^2.0.1", + "@mantine/core": "^8.3.18", + "@mantine/hooks": "^8.3.18", + "@mantine/notifications": "^8.3.18", + "@tabler/icons-react": "^3.46.0", "@testing-library/react": "^13.4.0", - "@testing-library/user-event": "^13.5.0", + "@testing-library/user-event": "^14.6.1", "cypress": "^15.20.0", "esptool-js": "^0.6.0", "lodash": "^4.18.1", @@ -24,10 +28,7 @@ "react-qr-code": "^2.2.0", "react-router": "^7.18.2", "react-scripts": "5.0.1", - "react-semantic-toasts-2": "^0.6.7", "react-stl-viewer": "^2.5.0", - "semantic-ui-offline": "^2.5.1", - "semantic-ui-react": "^2.1.5", "three": "^0.185", "web-vitals": "^5.3.0" }, @@ -63,7 +64,7 @@ "@types/node": "^26.1.2", "@types/react": "^18.3.17", "@types/react-dom": "^18.3.5", - "typescript": "^4.9.5" + "typescript": "^5.9.3" }, "jest": { "transformIgnorePatterns": [ @@ -86,6 +87,10 @@ "@babel/parser": "7.29.3", "@babel/template": "7.28.6", "@babel/traverse": "7.29.0", - "@babel/types": "7.29.0" + "@babel/types": "7.29.0", + "react-scripts": { + "typescript": "$typescript" + }, + "fork-ts-checker-webpack-plugin": "^6.5.3" } } diff --git a/app/public/index.html b/app/public/index.html index 3a15d035..8860fcf0 100644 --- a/app/public/index.html +++ b/app/public/index.html @@ -28,6 +28,54 @@ Learn how to configure a non-root public URL by running `npm run build`. --> WROLPi: Create your own off-grid library + diff --git a/app/scripts/generate-favicons.js b/app/scripts/generate-favicons.js index 5db437d5..a72b262a 100644 --- a/app/scripts/generate-favicons.js +++ b/app/scripts/generate-favicons.js @@ -5,7 +5,7 @@ * Usage: node scripts/generate-favicons.js * * This script reads the base icon.svg and generates a colored version - * for each color in semanticUIColorMap (from src/components/Vars.js). + * for each color in navColorHexMap (from src/components/Vars.js). * The favicons are saved to public/favicon-{colorname}.svg */ @@ -17,14 +17,14 @@ const publicDir = path.join(__dirname, '..', 'public'); const varsPath = path.join(srcDir, 'Vars.js'); const iconPath = path.join(publicDir, 'icon.svg'); -// Parse semanticUIColorMap from Vars.js (single source of truth) +// Parse navColorHexMap from Vars.js (single source of truth) function parseColorMapFromVars() { const varsContent = fs.readFileSync(varsPath, 'utf8'); - // Extract the semanticUIColorMap object using regex - const match = varsContent.match(/export const semanticUIColorMap\s*=\s*\{([^}]+)\}/); + // Extract the navColorHexMap object using regex + const match = varsContent.match(/export const navColorHexMap\s*=\s*\{([^}]+)\}/); if (!match) { - throw new Error('Could not find semanticUIColorMap in Vars.js'); + throw new Error('Could not find navColorHexMap in Vars.js'); } const colorMap = {}; @@ -35,15 +35,15 @@ function parseColorMapFromVars() { } if (Object.keys(colorMap).length === 0) { - throw new Error('No colors found in semanticUIColorMap'); + throw new Error('No colors found in navColorHexMap'); } return colorMap; } // Parse colors from Vars.js -const semanticUIColorMap = parseColorMapFromVars(); -console.log(`Found ${Object.keys(semanticUIColorMap).length} colors in Vars.js\n`); +const navColorHexMap = parseColorMapFromVars(); +console.log(`Found ${Object.keys(navColorHexMap).length} colors in Vars.js\n`); // Colors that conflict with white stroke - use black stroke instead // Must match Nav.js conflictingColors @@ -55,7 +55,7 @@ const iconSvg = fs.readFileSync(iconPath, 'utf8'); console.log('Generating colored favicons from icon.svg...\n'); // Generate a favicon for each color -for (const [colorName, hexColor] of Object.entries(semanticUIColorMap)) { +for (const [colorName, hexColor] of Object.entries(navColorHexMap)) { // Replace the dark fill color (#1b1c1d) with the nav color let coloredSvg = iconSvg.replace(/fill:#1b1c1d/g, `fill:${hexColor}`); @@ -72,4 +72,4 @@ for (const [colorName, hexColor] of Object.entries(semanticUIColorMap)) { console.log(` Created favicon-${colorName}.svg (${hexColor}${strokeInfo})`); } -console.log(`\nGenerated ${Object.keys(semanticUIColorMap).length} favicon files in public/`); +console.log(`\nGenerated ${Object.keys(navColorHexMap).length} favicon files in public/`); diff --git a/app/src/App.css b/app/src/App.css index 50bbb637..c05d1ec5 100644 --- a/app/src/App.css +++ b/app/src/App.css @@ -1,126 +1,260 @@ -.channel-edit { - border-radius: 3px; - padding: 0.75em; +:root { + /* Shared with the toast offset below, so the two cannot drift apart. */ + --navbar-height: 3.4em; } -.channel-edit:hover { - background-color: #7abaff; +/* Nav bar: a hand-rolled flexbox bar. The user-chosen background and the foreground + measured against it are set inline (see themes/navColors); this stylesheet only + supplies structure and hover affordances. */ +.wrolpi-navbar { + display: flex; + align-items: stretch; + /* + * Wraps rather than clipping. With this many items the bar overflows a 1000px + * viewport, and `nowrap` cut "Admin" off the right edge -- the one link a user needs + * to reach the settings that fix a broken WROLPi. + */ + flex-wrap: wrap; + min-height: var(--navbar-height); + padding: 0 0.25em; +} + +/* + * Toasts start below the navigation bar instead of on top of it. + * + * They are fixed at `top: var(--mantine-spacing-md)` -- 16px -- while the bar occupies the + * first 54px of the page, so every toast landed over the bar's right-hand corner: the search + * glyph, the hamburger, Help and Admin. For as long as the toast was up those controls could + * not be clicked, and an error toast is exactly when a user reaches for Admin. + * + * It went unnoticed until the search glyph was given a proper hit area: at 18px it sat in the + * gap under the toast, and growing it to the height of the bar moved it into the overlap. + * The toast was always covering that corner; there was just nothing there to hit. + * + * `:where()` gives Mantine's own rule zero specificity, so one class is enough to win, but + * the attribute is matched too -- only the top-anchored positions need moving, and a future + * switch to `bottom-right` should not silently inherit a top offset. + */ +html .mantine-Notifications-root[data-position^='top'] { + top: calc(var(--navbar-height) + var(--mantine-spacing-md, 1rem)); +} + +.wrolpi-navbar-right { + display: flex; + align-items: center; + margin-left: auto; + gap: 0.2em; +} + +.wrolpi-navbar-link, +.wrolpi-navbar-link-button { + display: flex; + align-items: center; + /* Tightened from 1.1em: the bar has eleven links plus nine indicators. */ + padding: 0 0.8em; + color: inherit; + text-decoration: none; + white-space: nowrap; + background: none; + border: none; + font: inherit; cursor: pointer; } +.wrolpi-navbar-link:hover, +.wrolpi-navbar-link-button:hover { + /* A translucent black works on every navbar color, light or dark. */ + background: rgba(0, 0, 0, 0.12); +} + +/* + * The tab for the section the user is in. + * + * react-router marks it with `active` and `aria-current` on its own, and always did -- the + * mark reached the DOM and nothing here drew it, so every tab looked identical and clicking + * Videos appeared to leave the bar untouched. + * + * The underline is a pseudo-element in `currentColor`, not a border: `currentColor` follows + * the foreground the bar computed for whatever navbar color the user picked, so this works + * on all twelve of them (see `navColorNames`) and in all four themes without naming a color; + * and a pseudo-element adds no height, where a border would jog the whole row as the mark + * moved between tabs. The darker wash is the hover treatment deepened, so the two read as + * the same idea. + */ +.wrolpi-navbar-link.active { + position: relative; + background: rgba(0, 0, 0, 0.2); +} + +.wrolpi-navbar-link.active::after { + content: ''; + position: absolute; + left: 0; + right: 0; + bottom: 0; + /* rem, not px, so the mark grows with the --ui-scale setting like the text above it. */ + height: 0.1875rem; + background: currentColor; +} + +/* + * The same tabs inside the overflow "More" menu and the mobile hamburger, which are the + * only place most of them appear on a phone. Those render in a portal, outside the bar, + * so they take theme tokens rather than the bar's foreground. + */ +.mantine-Menu-item.active { + background: var(--head); + font-weight: 600; +} + +/* + * Everything in the bar takes the bar's foreground. + * + * `color: inherit` on the links alone was not enough, because two of the things in the + * corner do not inherit at all and both came out blue on every bar in every theme: + * + * - An anchor takes `a {color: var(--blue)}` from tokens.css, which outranks an inherited + * value however the bar sets it. The search glyph is an anchor, and so is every status + * icon that links to /admin -- those only looked right because they carry an inline + * color of their own, which still wins here and is meant to. + * - A Mantine ActionIcon paints `--ai-color`, its own themed blue. The hamburger and the + * theme toggle are ActionIcons. The variable is no use as a hook: Mantine writes it + * INLINE on the element, so a stylesheet declaring it loses every time (the same trap + * as `--group-justify` on Pagination). The painted property has to be set instead, and + * `html` lifts this over Mantine's own single-class rule. + * + * Measuring the foreground against the bar is worth nothing if the icons opt out of it. + */ +.wrolpi-navbar a, +html .wrolpi-navbar .mantine-ActionIcon-root { + color: inherit; +} + +/* The bar's own hover, rather than a blue tint from Mantine's palette. */ +html .wrolpi-navbar .mantine-ActionIcon-root:hover { + background: rgba(0, 0, 0, 0.12); + color: inherit; +} + +/* + * One status indicator's slot. Laid out rather than nudged: see NavIconWrapper in Nav.js + * for the 0.8em top margin this replaces and why it looked correct on half the indicators. + */ +.wrolpi-navbar-icon { + display: flex; + align-items: center; + margin-left: 1.5em; + /* Full bar height, so the anchor inside has somewhere to stretch to. */ + align-self: stretch; +} + +/* + * An icon-only trigger in the corner gets a link's hit area, not its glyph's. + * + * The share glyph already computed `cursor: pointer` -- it is an `` -- but the + * target was the 24px box the icon occupies, so the pointer appeared only if you found it + * exactly, and there was no hover highlight to aim at. Stretched and padded, it behaves + * like Help and Admin beside it. + * + * `align-self` rather than `align-items: stretch` on the wrapper: the IconButtons in the + * corner have an explicit height, and a stretch container places an item it cannot stretch + * at the start -- which would push the hotspot and theme glyphs back off centre. + */ +.wrolpi-navbar-icon > a { + align-self: stretch; + padding: 0 0.5em; + cursor: pointer; +} + +.wrolpi-navbar-icon > a:hover { + background: rgba(0, 0, 0, 0.12); +} + +/* + * An anchor in the corner hugs its glyph instead of a text line box. + * + * A line box reserves descender space for text that is not there, which leaves an icon + * sitting about 3px above the centre of the box being centred -- the share and search + * glyphs, but not the hotspot or theme picker, which are square ActionIcons that centre + * their own glyph. That difference is what made a single top margin look right on some + * indicators and wrong on others. + */ +.wrolpi-navbar-right a { + display: flex; + align-items: center; +} + +/* + * A link in the corner fills the bar's height, as one outside it does. + * + * Help and Admin highlighted only a box around their text while Videos and Statistics + * highlighted the full height. Same class and same rules; the difference is the parent. + * The bar stretches its children, but the corner is a nested flex container that centres + * its own, so a link inside it was sized to its content. The hover background is painted + * on the link box, so this is the hover affordance itself and not just its outline. + */ +.wrolpi-navbar-right .wrolpi-navbar-link, +.wrolpi-navbar-right .wrolpi-navbar-link-button { + align-self: stretch; +} + ::-ms-clear { display: none; } -.form-control-clear { - z-index: 10; - pointer-events: auto; - cursor: pointer; -} - pre.wrap-text { white-space: pre-wrap; /* Since CSS 2.1 */ white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ word-wrap: break-word; /* Internet Explorer 5.5+ */ } +/* + * A video's length, pinned to the corner of its poster. + * + * Tokens, not a literal white-on-black. The poster under it is filtered by night mode -- it is a + * `.media` leaf -- but this chip is drawn beside the image rather than inside it, so it was never + * filtered: a white tile on a card whose picture had gone dim red, which is the brightest thing on + * the videos page in the theme meant to have nothing bright on it. + * + * Opaque, because it sits on an arbitrary poster and has to be legible over any of them. + */ .duration-overlay { position: absolute; - top: 4px; - right: 4px; - color: black; - background: #ffffff; - padding: 0px 4px; - border-radius: 3px; + top: 0.25rem; + right: 0.25rem; + color: var(--text); + background: var(--panel); + border: 1px solid var(--border); + padding: 0px 0.25rem; + border-radius: 0.1875rem; } .no-link-underscore { text-decoration: none !important; } +/* + * A card's title and its author/channel/domain line are links, but they are the card's + * text -- they take the body color rather than the link color, so a grid of cards reads + * as titles and not as a wall of blue. This was a literal `black` until the themes + * arrived, which left every title at about 1.1:1 on the dark, night and amber panels. + */ .card-link { - color: black; + color: var(--text); } -.center-me { - margin: 0 auto; -} - -div.inverted { - background-color: #1B1C1D; -} - -.toggle { - position: relative; - display: inline-block; - width: 49px; - height: 23px; -} - -.toggle input { - opacity: 0; - width: 0; - height: 0; -} - -.slider { - position: absolute; - cursor: pointer; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: rgb(166, 166, 166); - -webkit-transition: .4s; - transition: .4s; - border-radius: 34px; -} - -.slider:before { - position: absolute; - content: ""; - height: 22px; - width: 22px; - left: 0; - bottom: 0; - background-color: white; - -webkit-transition: .4s; - transition: .4s; - border-radius: 50%; -} - -.slider.disabled:before { - background-color: #CCCCCC; -} - -input:checked + .slider { - background-color: #6435C9FF; -} - -input.inverted:checked + .slider.inverted { - background-color: #6435C9AA; -} - -input.disabled + .slider { - background-color: rgb(147, 147, 147); -} - -input:checked + .slider:before { - -webkit-transform: translateX(26px); - -ms-transform: translateX(26px); - transform: translateX(26px); -} - -.inverted-progress-text.ui.indicating.progress .bar > .progress, -.inverted-progress-text.ui.progress > .label { - color: white !important; -} - -/* Light bar colors (yellow, olive) need dark text on the bar — must override inverted-progress-text. */ -.light-bar-progress-text.ui.indicating.progress .bar > .progress, -.light-bar-progress-text.ui.progress .bar > .progress { - color: rgba(0, 0, 0, 0.85) !important; -} +/* + * The hand-rolled `.toggle` / `.slider` switch is gone. + * + * Nothing referenced either class: `Toggle` is a Mantine Switch, and the only remaining + * matches for the words were the `classList.toggle` DOM method and the controller's own + * templates, which do not load this stylesheet. + * + * It is deleted rather than finished because it was already half-converted and broken: the + * track and knob became rem while the checked-state travel stayed `translateX(26px)`, so on a + * 54px track the thumb would have stopped ~4px short. Dead CSS that is also wrong is a trap + * for whoever revives it, and it also hardcoded #6435C9 and white, which no theme can reach. + */ /* File preview Modal.Actions toolbar. Single layout that works at every viewport — buttons keep their order at any width. @@ -140,7 +274,8 @@ input:checked + .slider:before { } .preview-toolbar-tags { - flex: 1 1 200px; + /* rem so the basis grows with the tags it holds. */ + flex: 1 1 12.5rem; min-width: 0; } @@ -150,9 +285,9 @@ input:checked + .slider:before { Use 100dvh so mobile browser chrome (URL bar) is accounted for. */ .preview-fit { width: 100%; - height: calc(100dvh - 300px); - max-height: calc(100dvh - 300px); - min-height: 240px; + height: calc(100dvh - 18.75rem); + max-height: calc(100dvh - 18.75rem); + min-height: 15rem; overflow: hidden; position: relative; display: flex; @@ -171,16 +306,19 @@ td.file-path { cursor: pointer; } -/* Necessary until react-semantic-toasts is upgraded to Node 18 */ -.ui-alerts { - position: fixed; - z-index: 2060; - padding: 24px; -} - -.ui-alerts.top-right { - top: 20px; - right: 20px; +/* + * The row is an icon followed by a filename, as inline siblings. An inline SVG + * sits on the text baseline by default, so a 24px glyph next to 13px text hangs + * well above it and the name looks like it is resting on the icon's floor. + * `middle` centres the glyph against the text instead. + * + * The margin is the gap that was never there: the folder icon touched the first + * letter of its own name, and two icons (a folder and its ignored marker) touched + * each other. + */ +td.file-path > svg { + vertical-align: middle; + margin-right: 0.35em; } textarea.otp { @@ -188,104 +326,16 @@ textarea.otp { color: #333; } -.inline-header h1 { - display: inline-block; -} - -.inline-header h2 { - display: inline-block; -} - -.inline-header h3 { - display: inline-block; -} - -.inline-header h4 { - display: inline-block; -} - -.inline-header h5 { - display: inline-block; -} - -.inline-header span { - margin-left: 1em; -} - .card-icon { - min-height: 163px; + min-height: 10.1875rem; text-align: center; - padding-top: 53px; -} - -.card-icon.inverted { - background-color: #222222; -} - -.modal .inverted { - border-color: #888888; -} - -.modal .header.inverted { - color: #DDDDDD !important; - background-color: #171616 !important; -} - -.modal .content.inverted { - color: #DDDDDD !important; - border-color: #888888 !important; - background-color: #171616 !important; -} - -.modal .actions.inverted { - color: #DDDDDD !important; - border-color: #888888 !important; - background-color: #171616 !important; -} - -.modal.inverted i.close { - color: #DDDDDD !important; -} - -/* The accordion has no background of its own; it inherits whatever container it sits in (Modal - content #171616, Segment #1B1C1D, the page body, etc.). This matches how the light theme already - renders accordions (transparent), and avoids a darker band when the parent isn't #171616. - Direct children only, so nested components that use the `content`/`title` class names (e.g. a - Message's Message.Content) are not affected. */ -.ui.inverted.accordion, -.ui.inverted.accordion > .title, -.ui.inverted.accordion > .content { - background-color: transparent !important; -} - -/* Semantic UI paints a definition table's empty corner cell white (background on the inverted - variant, white box-shadows on the celled/plain variants) to fake a "cut-out" corner on a white - page. Our dark theme has a dark page, so the corner must be transparent with no shadow. */ -.ui.inverted.definition.table thead:not(.full-width) th:first-child, -.ui.inverted.definition.table tfoot:not(.full-width) th:first-child, -.ui.inverted.celled.definition.table thead:not(.full-width) th:first-child, -.ui.inverted.celled.definition.table tfoot:not(.full-width) th:first-child { - background: transparent !important; - box-shadow: none !important; -} - -/* Mobile-responsive Confirm modal */ -.ui.modal.themed-confirm { - max-width: 95vw; + padding-top: 3.3125rem; } .clickable { cursor: pointer; } -.segment.inverted { - border: 1px solid #ffffff22 !important; -} - -.inverted label { - color: #DDDDDD !important; -} - .table-ellipsis td.column-ellipsis { white-space: nowrap; overflow: hidden; @@ -311,18 +361,6 @@ textarea.otp { flex-grow: 1; } -.search-clear { - margin: 10px; -} - -.icon.divider { - color: rgb(0, 0, 0, 0.4) !important; -} - -.divider.inverted { - color: rgb(255, 255, 255, 0.6) !important; -} - .video-player { display: block; margin: 0 auto; /* Center the video horizontally */ @@ -352,109 +390,57 @@ table.ratio-table td.min-width { width: auto; } -/* CollectionEditForm styles */ -.required-indicator { - color: red; -} - -.action-button-spacing { - margin-top: 1em; -} - -/* Form loading skeleton */ -.form-skeleton-field { - margin-bottom: 1.5em; -} - -.form-skeleton-label { - height: 14px; - width: 80px; - margin-bottom: 8px; - background: linear-gradient(90deg, #e0e0e0 25%, #f0f0f0 50%, #e0e0e0 75%); - background-size: 200% 100%; - animation: skeleton-shimmer 1.5s infinite; - border-radius: 3px; -} - -.form-skeleton-input { - height: 38px; - width: 100%; - background: linear-gradient(90deg, #e0e0e0 25%, #f0f0f0 50%, #e0e0e0 75%); - background-size: 200% 100%; - animation: skeleton-shimmer 1.5s infinite; - border-radius: 4px; -} - -.form-skeleton-textarea { - height: 80px; - width: 100%; - background: linear-gradient(90deg, #e0e0e0 25%, #f0f0f0 50%, #e0e0e0 75%); - background-size: 200% 100%; - animation: skeleton-shimmer 1.5s infinite; - border-radius: 4px; -} - -.form-skeleton-button { - height: 42px; - width: 100px; - background: linear-gradient(90deg, #e0e0e0 25%, #f0f0f0 50%, #e0e0e0 75%); - background-size: 200% 100%; - animation: skeleton-shimmer 1.5s infinite; - border-radius: 4px; -} - -@keyframes skeleton-shimmer { - 0% { - background-position: -200% 0; - } - 100% { - background-position: 200% 0; - } -} - -/* Dark mode skeleton */ -.inverted .form-skeleton-label, -.inverted .form-skeleton-input, -.inverted .form-skeleton-textarea, -.inverted .form-skeleton-button { - background: linear-gradient(90deg, #333 25%, #444 50%, #333 75%); - background-size: 200% 100%; - animation: skeleton-shimmer 1.5s infinite; -} - -/* Sticky footer for file browser */ +/* + * Sticky footer for the file browser. + * + * The surface is a token. It was two hardcoded greys with `.inverted` selecting the dark + * one, and once nothing set `.inverted` any more the footer stayed at near-white `#f9fafb` + * in dark, night and amber. In night mode a white bar pinned to the bottom of the screen + * costs the user the dark adaptation the whole theme exists to protect. A token has no + * such failure mode: there is one rule and each theme fills it in. + */ .sticky-footer { position: fixed; bottom: 0; left: 0; right: 0; padding: 0.5em 1em; - background-color: #f9fafb; - border-top: 1px solid rgba(34, 36, 38, 0.15); + background-color: var(--panel); + border-top: 1px solid var(--border); z-index: 100; + /* + * A row with gaps, like every other group of buttons in the app. + * + * This was a plain block, so the buttons -- inline-block elements with no whitespace + * between them, because JSX drops whitespace-only lines -- sat flush against each other + * in one unbroken slab. Everywhere else a group of buttons is a flex row with a gap; + * the file browser's footer is hand-rolled and never got one. It wraps because the bar + * carries eight controls and a phone cannot hold them. + */ + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 0.5em; } -.sticky-footer.inverted { - background-color: #1b1c1d; - border-top: 1px solid rgba(255, 255, 255, 0.1); -} - -/* Drag selection visual feedback for FileBrowser */ +/* + * Drag selection visual feedback for FileBrowser: a blue tint over the rows being taken, + * red over the rows being dropped. + * + * Each had a second, stronger rule under `.inverted` for the dark themes. Nothing has set + * that class since ThemeProvider stopped supplying it, so only the light tint has been + * drawn for some time -- and 15% of a fixed blue over a dark row is close to invisible, + * which is the one moment the user needs to see what they have hold of. + * + * A token mixed with transparent instead: the hue follows the theme, and the strength can + * go up without pinning it to a literal that only suits one background. + */ tr.drag-selecting td { - background-color: rgba(33, 133, 208, 0.15) !important; + background-color: color-mix(in srgb, var(--blue) 22%, transparent) !important; } -.inverted tr.drag-selecting td { - background-color: rgba(33, 133, 208, 0.25) !important; -} - -/* Drag deselection visual feedback (red tint) */ tr.drag-deselecting td { - background-color: rgba(219, 40, 40, 0.15) !important; -} - -.inverted tr.drag-deselecting td { - background-color: rgba(219, 40, 40, 0.25) !important; + background-color: color-mix(in srgb, var(--red) 22%, transparent) !important; } /* Prevent text selection during drag */ @@ -469,11 +455,30 @@ tr.drag-deselecting td { .file-browser-filter-container { display: flex; justify-content: flex-end; - margin-bottom: 0.5em; + /* The gap is the container's. The collapse-all button carried `margin-left: 0.5em` for it, + which is the same thing one element to the right of where it belongs. */ + gap: 0.5em; + /* No `margin-bottom`: this is a top-level block of the Files page, and the page's stack + spaces it (see `.wrolpi-stack`). Its own 0.5em was added to that. */ } -/* Double the default input width on desktop. */ -.ui.input.file-browser-filter { +/* + * Double the default input width on desktop. + * + * The selector was `.ui.input.file-browser-filter`, naming a wrapper the old input + * component rendered around the field. Nothing carries `ui input` any more, so this rule + * and the mobile one below it had been matching nothing at all: the filter was rendering + * at the browser's default width on a bar laid out to give it 34em. + * + * The class lands on ActionInput's wrapper, which is the field and its clear button + * together -- that pair is the control being sized, not the text box alone. + * + * Qualified by the container so it carries two classes. `.wrolpi-action-input` sets + * `width: 100%` in ui.css, which is one class like `.file-browser-filter` is, and ui.css + * is loaded after this file -- so a bare class selector here loses the tie and the field + * fills the row anyway. Specificity, not source order, is what should decide it. + */ +.file-browser-filter-container .file-browser-filter { width: 34em; max-width: 100%; } @@ -488,36 +493,12 @@ tr.drag-deselecting td { } /* Fill the row, but stay on one line with the collapse-all button. */ - .ui.input.file-browser-filter { + .file-browser-filter-container .file-browser-filter { flex: 1 1 auto; width: auto; } } -/* Ensure step icons are always visible, even when disabled */ -.ui.steps .step.disabled > .icon, -.ui.steps .step.disabled > i.icon, -.ui.steps .disabled.step > .icon, -.ui.steps .disabled.step > i.icon { - opacity: 0.45 !important; - display: inline-block !important; - visibility: visible !important; - width: auto !important; - height: auto !important; - color: rgba(0, 0, 0, 0.87) !important; -} - -/* Ensure the icon glyph (::before) is visible for disabled steps */ -.ui.steps .step.disabled > .icon:before, -.ui.steps .step.disabled > i.icon:before, -.ui.steps .disabled.step > .icon:before, -.ui.steps .disabled.step > i.icon:before { - opacity: 0.45 !important; - visibility: visible !important; - color: rgba(0, 0, 0, 0.87) !important; -} - - /* Inventory "PDF" export: the printable table is hidden on screen and becomes the only visible content when printing, so a user can use the browser dialog to Save as PDF. */ .inventory-print { @@ -568,7 +549,7 @@ tr.drag-deselecting td { .inventory-print th, .inventory-print td { border: 1px solid #999; - padding: 4px 6px; + padding: 0.25rem 0.375rem; text-align: left; vertical-align: top; } diff --git a/app/src/App.js b/app/src/App.js index f1ed37db..1e372a53 100644 --- a/app/src/App.js +++ b/app/src/App.js @@ -4,8 +4,13 @@ import {NavBar} from "./components/Nav"; import {createBrowserRouter, createRoutesFromElements, Link, Outlet, Route, RouterProvider} from "react-router"; import {VideosTabLayout, VideosPage, VideosSettingsPage, VideosStatistics, VideoWrapper} from "./components/Videos"; import AdminRoute from "./components/admin/AdminRoute"; -import {Container} from "semantic-ui-react"; -import 'semantic-ui-offline/semantic.min.css'; +import {Box, Header} from "./components/ui"; +import '@mantine/core/styles.css'; +import '@mantine/notifications/styles.css'; +// Theme tokens load last so they win over both component libraries. +import './themes/fonts.css'; +import './themes/tokens.css'; +import {MediaFilterDefs} from "./themes/MediaFilterDefs"; import {MoreRoute} from "./components/Apps"; import {InventoryRoute} from "./components/inventory/InventoryRoute"; import {ArchiveRoute} from "./components/Archive"; @@ -14,12 +19,12 @@ import {QueryProvider, StatusProvider} from "./hooks/customHooks"; import {FileWorkerStatusProvider} from "./contexts/FileWorkerStatusContext"; import {ChannelEditPage, ChannelNewPage, ChannelsPage} from "./components/Channels"; import {MapRoute} from "./components/Map"; -import {MediaContextProvider, mediaStyles, StatusContext, ThemeContext} from "./contexts/contexts"; -import {Header, ThemeProvider} from "./components/Theme"; +import {MediaContextProvider, mediaStyles, StatusContext} from "./contexts/contexts"; +import {ThemeProvider} from "./components/Theme"; import {DashboardPage} from "./DashboardPage"; import {DonatePage} from "./components/DonatePage"; +import {ThemeSamplePage} from "./components/ThemeSamplePage"; import {useEventsInterval} from "./Events"; -import {SemanticToastContainer} from "react-semantic-toasts-2"; import {FilePreviewProvider} from "./components/FilePreview"; import {TagsProvider} from "./Tags"; import {ZimRoute} from "./components/Zim"; @@ -30,11 +35,10 @@ import ErrorBoundary from "./components/ErrorBoundary"; import {KeyboardShortcutsProvider} from "./components/KeyboardShortcutsProvider"; function PageNotFound() { - const {t} = useContext(ThemeContext); - return + return
Page Not Found!
-

The page you requested cannot be found

-
+

The page you requested cannot be found

+ } function Dot() { @@ -42,7 +46,6 @@ function Dot() { } function Footer() { - const {t} = useContext(ThemeContext); const {status} = useContext(StatusContext); let version; try { @@ -50,18 +53,19 @@ function Footer() { } catch (e) { // Not logging because this is not that important. } - return - + return + WROLPi {version}
WROLPi.org Donate - + } function Root() { return + @@ -87,6 +91,9 @@ const router = createBrowserRouter(createRoutesFromElements(}/> }/> }/> + {/* Component gallery: lets a user see a theme before committing to it, and lets us + review the design. Linked from Settings. */} + }/> }> }/> @@ -118,8 +125,7 @@ export default function App() { {/* Context and style to handle switching between mobile/computer. */} - {/* Toasts can be on any page. */} - + {/* Toasts (from components/ui) are mounted by ThemeProvider, which wraps every page. */} diff --git a/app/src/DashboardPage.js b/app/src/DashboardPage.js index 440b4d31..2ff83051 100644 --- a/app/src/DashboardPage.js +++ b/app/src/DashboardPage.js @@ -12,19 +12,21 @@ import {useHotkeys} from "react-hotkeys-hook"; import {Media, SettingsContext, StatusContext} from "./contexts/contexts"; import {DownloadMenu} from "./components/Download"; import { + ActionInput, Button, Divider, - Form, Header, + Icon, + Message, Modal, - Segment, - Statistic -} from "./components/Theme"; + Panel, + Stack, + Statistic, + StatisticGroup, +} from "./components/ui"; import {Link, useNavigate, useSearchParams} from "react-router"; import {BandwidthProgressCombined, CPUUsageProgress} from "./components/admin/Status"; import {ProgressPlaceholder} from "./components/Placeholder"; -import {GridColumn, GridRow, Icon, Input, Message} from "semantic-ui-react"; -import Grid from "semantic-ui-react/dist/commonjs/collections/Grid"; import {refreshFiles} from "./api"; import _ from "lodash"; import {TagsDashboard} from "./Tags"; @@ -37,6 +39,9 @@ import {fileMimetypeFilterOptions, FileCards, SearchFilterButton} from "./compon import {useCalculators} from "./components/Calculators"; import {classifyAndEvaluate} from "./components/calculators/mathConfig"; +// A spinning refresh glyph, used as the `icon` for the "files are being refreshed" Message. +const RefreshingIcon = (props) => ; + export function FlagsMessages() { const {settings, fetchSettings} = React.useContext(SettingsContext); const {status} = useContext(StatusContext); @@ -53,47 +58,44 @@ export function FlagsMessages() { // Do not tell the maintainer to refresh the files if the FileWorker is busy. if (flags.file_worker_busy) { // FileWorker is busy. - refreshingMessage = - - - Your files are being refreshed. -

Click here to view the progress

-
+ refreshingMessage = +

Click here to view the progress

; } else if (!flags.refresh_complete) { // `refresh_complete` flag is not set. Tell the maintainer to refresh the files. - refreshRequiredMessage = - - - Refresh required - Click here to refresh all your files. - + refreshRequiredMessage = + { + e.preventDefault(); + refreshFiles(); + }}>Click here to refresh all your files. ; } let dbDownMessage; if (!flags.db_up) { dbDownMessage = - Database is down - API is unable to connect to the database. Check the server logs. + Database is down +

API is unable to connect to the database. Check the server logs.

} let internetDownMessage; if (!flags.have_internet && !settings.wrol_mode) { internetDownMessage = - No Internet - WROLPi has no Internet. Downloads will not start. + No Internet +

WROLPi has no Internet. Downloads will not start.

; } let mediaUnmountedMessage; if (flags.media_mounted === false) { - mediaUnmountedMessage = - No drive mounted - WROLPi has no media drive mounted for one or more storage locations. - Downloads would write to the root filesystem and can fill it. - {' '}Open the Controller to mount a drive. + mediaUnmountedMessage = + No drive mounted +

+ WROLPi has no media drive mounted for one or more storage locations. + Downloads would write to the root filesystem and can fill it. + {' '}Open the Controller to mount a drive. +

; } @@ -140,7 +142,6 @@ function EvaluateForm() {
2 Tb to Gb
- const inputRef = React.useRef(); const [showMessage, setShowMessage] = React.useState(false); const [inputValue, setInputValue] = React.useState(''); const [evaluatedValue, setEvaluatedValue] = React.useState(helpContents); @@ -166,45 +167,36 @@ function EvaluateForm() { } } - return
- setShowMessage(true)} - onChange={(e, {value}) => setInputValue(value)} - action={} + return { + e.preventDefault(); + doEvaluate(); + }}> + setShowMessage(true)} + onChange={(e) => setInputValue(e.currentTarget.value)} + action={} /> {showMessage && - - Result - - {evaluatedValue} - + + {evaluatedValue} } - + } function DashboardCalculators() { const {calculatorLinks} = useCalculators(); - return + return
Calculators
- - - - - - - - - {calculatorLinks} - - - -
+ + +
{calculatorLinks}
+
+ } export function Getters() { @@ -244,29 +236,25 @@ export function Getters() { } } - const getter = - - Or - - - - - - - - - - ; + const getter = +
+
+ +
+
+ +
+ Or +
+
; let getterModal; if (selectedGetter === 'downloads') { - getterModal = handleSetGetter(null, null)} @@ -281,7 +269,7 @@ export function Getters() { ; } else if (selectedGetter === 'upload') { - getterModal = handleSetGetter(null, null)} @@ -304,7 +292,7 @@ export function Getters() { } -function DashboardStatus() { +export function DashboardStatus() { const {status} = useContext(StatusContext); let percent = 0; @@ -330,16 +318,25 @@ function DashboardStatus() { bandwidths = null; } - return - + return + {/* + * The panel is a link, but it should not read as one. + * + * `a` takes `color: var(--blue)` and an underline on hover from tokens.css, and a + * statistic's value sets no color of its own -- so the three load figures inherited + * the link color, and crossing the panel underlined them together. The same pair + * every card title uses says "this text belongs to what contains it": the pointer + * cursor still comes from the UA stylesheet, which is the affordance that matters. + */} +
Status
- + - +
Bandwidth
{bandwidths} @@ -347,26 +344,26 @@ function DashboardStatus() { - - + + - + -
; + ; } function DashboardRecentFiles() { const {searchFiles, loading, fetchFiles} = useSearchRecentFiles(); - return + return - + } export function DashboardPage() { @@ -467,7 +464,13 @@ export function DashboardPage() { if (big) { inputProps.size = 'big'; } - return
+ /* + * No `marginBottom`. This row is a top-level block of the dashboard, so its own 2em was + * added to the page stack's gap -- 52.8px below the search box against 17.6px between + * every other pair of blocks, on the most-looked-at page in the app. One source of + * spacing, and it is the stack. + */ + return
{getSearchResultsInput(inputProps)} diff --git a/app/src/DashboardPage.test.js b/app/src/DashboardPage.test.js index c86d2021..1d75414c 100644 --- a/app/src/DashboardPage.test.js +++ b/app/src/DashboardPage.test.js @@ -1,8 +1,7 @@ import React from 'react'; -import {render, screen} from '@testing-library/react'; -import {MemoryRouter} from 'react-router'; -import {Getters, FlagsMessages} from './DashboardPage'; -import {SettingsContext, StatusContext, ThemeContext} from './contexts/contexts'; +import {render, screen} from './test-utils'; +import {DashboardStatus, Getters, FlagsMessages} from './DashboardPage'; +import {SettingsContext, StatusContext} from './contexts/contexts'; // Replace DownloadMenu with a stub that records its props. The deep-link // behavior under test is just "Getters reads URL params and forwards them @@ -23,18 +22,14 @@ jest.mock('./hooks/customHooks', () => ({ })); function renderAtUrl(url) { + // test-utils' render() wraps content in a BrowserRouter (not MemoryRouter), + // so point the jsdom location at the desired URL before rendering. + window.history.pushState({}, '', url); const status = {flags: {refresh_complete: true}}; - const theme = { - i: {}, s: {}, t: {}, theme: 'light', inverted: false, setInverted: () => {}, - }; return render( - - - - - - - + + + ); } @@ -80,13 +75,11 @@ describe('DashboardPage Getters deep-link', () => { function renderFlagsMessages({flags = {}, settings = {}} = {}) { const status = {flags: {refresh_complete: true, db_up: true, have_internet: true, ...flags}}; return render( - - {}}}> - - - - - + {}}}> + + + + ); } @@ -108,3 +101,65 @@ describe('DashboardPage no-drive-mounted banner', () => { expect(screen.queryByText(/No drive mounted/i)).toBeNull(); }); }); + +function renderDashboardStatus() { + const status = { + cpu_stats: {percent: 12, cores: 4}, + load_stats: {minute_1: 0.4, minute_5: 0.5, minute_15: 0.6}, + downloads: {pending: 3, disabled: false}, + nic_bandwidth_stats: {}, + }; + return render( + + + + ); +} + +describe('the dashboard status panel links without looking like a link', () => { + /* + * The whole panel is wrapped in a to /admin/status, which makes it an anchor -- + * and `a` takes `color: var(--blue)` with an underline on hover from tokens.css. A + * statistic's value sets no color of its own, so the load figures inherited that: three + * blue numbers on a panel where nothing else is blue, underlining as a group when the + * pointer crossed any part of the panel. + * + * `no-link-underscore card-link` is the pair already used wherever a link IS the + * element's own text rather than a link through it -- every card title and meta line. + * The pointer cursor is untouched: it comes from the UA stylesheet for `a[href]`, and + * the affordance is the point of wrapping the panel at all. + * + * Colors are asserted in ui-layout.cy.js; jsdom does not resolve the custom property + * these classes rely on. + */ + it('dresses the status link as the panel\'s own text', () => { + const {container} = renderDashboardStatus(); + + const link = container.querySelector('a[href="/admin/status"]'); + expect(link).toBeInTheDocument(); + expect(link).toHaveClass('card-link'); + expect(link).toHaveClass('no-link-underscore'); + }); + + it('does the same for the downloads link beneath it', () => { + const {container} = renderDashboardStatus(); + + const link = container.querySelector('a[href="/admin"]'); + expect(link).toBeInTheDocument(); + expect(link).toHaveClass('card-link'); + expect(link).toHaveClass('no-link-underscore'); + }); + + it('leaves the load figures inside it, which is what went blue', () => { + // The premise for the two assertions above: this link really does wrap statistics + // whose value carries no color of its own, so it decides theirs. + const {container} = renderDashboardStatus(); + + const link = container.querySelector('a[href="/admin/status"]'); + const values = [...link.querySelectorAll('.wrolpi-statistic-value')]; + + expect(values.length).toBeGreaterThanOrEqual(3); + expect(values.map(value => value.textContent)).toEqual( + expect.arrayContaining(['0.4', '0.5', '0.6'])); + }); +}); diff --git a/app/src/Events.js b/app/src/Events.js index be4b6795..3d21622b 100644 --- a/app/src/Events.js +++ b/app/src/Events.js @@ -1,7 +1,7 @@ import React, {useEffect} from 'react'; import {useRecurringTimeout} from "./hooks/customHooks"; import {ApiDownError, getEvents} from "./api"; -import {toast} from "react-semantic-toasts-2"; +import {toast} from "./components/ui"; const apiEventName = 'apiEvent'; diff --git a/app/src/Tags.cy.js b/app/src/Tags.cy.js index 0644dfa2..a9919b9c 100644 --- a/app/src/Tags.cy.js +++ b/app/src/Tags.cy.js @@ -10,6 +10,8 @@ describe('Tags', () => { it('Tags Dashboard displays Tags.', () => { cy.wait('@getTags'); - cy.get('div.ui.label.large').contains('Automotive').should('be.visible'); + // `.wrolpi-tag` is what a tag chip is now. `div.ui.label.large` named the old + // framework's markup, and has matched nothing since its stylesheet stopped loading. + cy.get('.wrolpi-tag').contains('Automotive').should('be.visible'); }); }); diff --git a/app/src/Tags.js b/app/src/Tags.js index ce2f42f2..abcd0517 100644 --- a/app/src/Tags.js +++ b/app/src/Tags.js @@ -1,6 +1,5 @@ import React, {useEffect} from "react"; import {ApiDownError, deleteTag, getRecentTags, getTags, saveTag} from "./api"; -import {Dimmer, Divider, Grid, GridColumn, GridRow, Input, Label, TableCell, TableRow,} from "semantic-ui-react"; import { APIButton, contrastingColor, @@ -10,17 +9,24 @@ import { scrollToTopOfElement } from "./components/Common"; import { + Box, Button, - Form, + Divider, + Group, Header, - Loader, + Icon, + IconButton, + Label, + Loading, Modal, - Segment -} from "./components/Theme"; + Panel, + Table, + TextInput, +} from "./components/ui"; import _ from "lodash"; import {HexColorPicker} from "react-colorful"; import {useRecurringTimeout} from "./hooks/customHooks"; -import {Media, QueryContext, ThemeContext} from "./contexts/contexts"; +import {Media, QueryContext} from "./contexts/contexts"; import {Link, useNavigate} from "react-router"; import {TagPlaceholder} from "./components/Placeholder"; import {SortableTable} from "./components/SortableTable"; @@ -96,64 +102,96 @@ export function useTags() { || fuzzyMatch(lowerName, i.name.toLowerCase(), 2)) } + // A single tag chip. Tag colors are chosen by the user (stored per-tag), not by the + // theme, so they are rendered with the raw hex value rather than a token -- but the + // `wrolpi-label` className still gives them the theme's shape (and its night-mode + // outline treatment, which keys off the same `--label-color` variable). const NameToTagLabel = ({name, to, ...props}) => { const tag = findTagByName(name); if (tag !== null && tag !== undefined) { const tagColor = tag['color'] || DEFAULT_TAG_COLOR; const textColor = contrastingColor(tagColor); - const style = {...props['style'], backgroundColor: tagColor, color: textColor}; - return ; + ; } // No tags have been fetched. - return ; + return ; } const TagsGroup = ({tagNames, onClick}) => { if (!tagNames || tagNames.length === 0) { return ; } - return + return {tagNames.map(i => onClick(i)}/>)} - + } + /* + * The link carries no margin of its own. It had `0.3em` either side, from when these were + * labels that had none -- but a tag already reserves a `margin-left` in ui.css for + * the point drawn outside its body, so that was 0.3em twice on top of the group's gap and + * the point's own room. Two tags sat 34.7px apart, of which the point needs 18.7px. + * + * Those are rendered px, measured in the browser at the default interface scale -- which is + * what a user sees, and why they are the numbers worth recording. The stylesheet states the + * same margin in design px, as `1.0625rem`; 17 x 1.1 is the 18.7 above. + */ const TagLabelLink = ({name, props}) => { const to = getLocationStr({tag: name}, '/search'); - const style = {marginLeft: '0.3em', marginRight: '0.3em'}; try { // We prefer to use Link to avoid reloading the page, check if React Router is available, so we can use it. useNavigate(); - return + return } catch { // React Router is not available, use anchor. - return + return } } - const TagsLinkGroup = ({tagNames, ...props}) => { + /* + * Props here have two possible targets, and which one they take is part of the contract: + * + * `style` and `className` dress the ROW. Everything else reaches each CHIP, which is how + * `onClick` gets to a tag. + * + * `style` used to go down with the rest, so the dashboard's `marginTop` -- meant to space the + * row below its heading -- landed on all 34 tags and became 7.7px of extra space between + * every wrapped row of them. `className` is named alongside it because it is the same kind + * of thing and would fail the same way; a Mantine `Group` prop (`gap`, `wrap`, `justify`) or + * a `data-*` meant for the row still would, so add it here rather than at a call site. + */ + const TagsLinkGroup = ({tagNames, style, className, ...props}) => { if (!tagNames || tagNames.length === 0) { return ; } - return + return {tagNames.map(i => )} - + } const SingleTag = ({name, ...props}) => { - return + return } useEffect(() => { @@ -187,16 +225,15 @@ function EditTagRow({tag, onDelete, onEdit}) { const {SingleTag} = React.useContext(TagsContext); const {name, color, id, file_group_count, zim_entry_count, channel_count, domain_count} = tag; - const deleteConfirm = <> - onDelete(id, name)} - /> - ; - const editButton = - - - Save - - - + + + Save + } @@ -378,7 +404,6 @@ function EditTagsModal() { } @@ -388,7 +413,7 @@ function EditTagsModal() {
- @@ -406,7 +431,6 @@ export function AddTagsButton({ limit = null, disabled = false, filterByOverlap = false, - color = 'violet', }) { // A button which displays a modal in which the user can add or remove tags. @@ -556,19 +580,17 @@ export function AddTagsButton({ const hideFrequentTags = filterByOverlap && localTags && localTags.length > 0; return <> - - {showAny && } - - - - + + + {!hideEdit && } + + + + {showAny && } + + + } -export const taggedImageLabel = {corner: 'left', icon: 'tag', color: 'green'}; export const TagsSelector = ({ hideEdit = false, @@ -663,43 +681,37 @@ export const TagsSelector = ({ return button; } - return - - - {button} - - - - - - + return + {button} + + + + } export const TagsDashboard = () => { const {tagNames, TagsLinkGroup} = React.useContext(TagsContext); - const tagPlaceholder = ; - let availableTagsGroup = - - {tagPlaceholder} - {tagPlaceholder} - {tagPlaceholder} - - ; + const tagPlaceholder = ; + let availableTagsGroup = + {tagPlaceholder} + {tagPlaceholder} + {tagPlaceholder} + ; if (tagNames && tagNames.length >= 1) { availableTagsGroup = ; } else if (tagNames === undefined) { availableTagsGroup = Could not fetch tags } - return + return
Tags
{availableTagsGroup} -
+ } export const TagsProvider = (props) => { diff --git a/app/src/Tags.test.js b/app/src/Tags.test.js new file mode 100644 index 00000000..70bcf613 --- /dev/null +++ b/app/src/Tags.test.js @@ -0,0 +1,207 @@ +import React from 'react'; +import {screen, waitFor} from '@testing-library/react'; +import {render} from './test-utils'; +import {queryContextFixture} from './test-fixtures'; +import {QueryContext} from './contexts/contexts'; +import {useTags} from './Tags'; + +/* + * Tests for the real tag chip. + * + * Everything else in the suite takes `NameToTagLabel` from `tagsContextFixture`, which stubs + * it as a function returning the name -- so nothing exercised the component that actually + * paints a tag. That is how a tag came to be unreadable in night mode without a single test + * noticing: the colors are decided here, in the one place in the app that paints with a + * value no theme chose. + */ + +jest.mock('./api', () => ({ + getTags: jest.fn(), + getRecentTags: jest.fn(), + saveTag: jest.fn(), + deleteTag: jest.fn(), +})); + +import {getTags, getRecentTags} from './api'; + +const BRIGHT = '#ffe066'; // contrastingColor -> black +const DARK = '#1a1a2e'; // contrastingColor -> light + +// A harness that renders the real NameToTagLabel out of the real hook. +const TagProbe = ({name}) => { + const {NameToTagLabel} = useTags(); + return ; +}; + +const renderTag = (name, tags) => render( + + + +); + +// The same, for the group -- which is where a row's spacing is decided. +const GroupProbe = ({tagNames, ...props}) => { + const {TagsLinkGroup} = useTags(); + return ; +}; + +const renderGroup = (tagNames, props = {}) => render( + + + +); + +const tagOf = (name) => screen.getByText(name); + +/* + * Wait for the fetched colors to arrive. Before they do, `NameToTagLabel` falls back to a + * plain `
; + + return } // Domain table column configuration @@ -471,25 +473,15 @@ function DomainStatistics({statistics}) { return <> } - return + return
Statistics
- - {statistics.archive_count} - Archives - - - {humanFileSize(statistics.size, true)} - Total Size - - - {humanFileSize(statistics.largest_archive, true)} - Largest Archive - - - {humanNumber(statistics.archive_tags)} - Archive Tags - -
+ + + + + + + } export function DomainsPage() { @@ -506,36 +498,31 @@ export function DomainsPage() { } }, {enableOnFormTags: false}); - // Header section matching ChannelsPage pattern - const header =
- - - - setSearchStr('')} - onChange={setSearchStr} - onSubmit={null} - inputRef={searchInputRef} - /> - - - {/* No "New Domain" button - domains are auto-created */} - - - + // Header section matching ChannelsPage pattern. No `marginBottom`: this is a top-level block + // of the page, and the page's stack spaces it -- its own margin was added to that gap. + const header =
+
+ setSearchStr('')} + onChange={setSearchStr} + onSubmit={null} + inputRef={searchInputRef} + /> +
+
+ {/* No "New Domain" button - domains are auto-created */} +
; // Empty state if (domains && domains.length === 0) { return <> {header} - - No domains yet. Archive some webpages! - + ; } @@ -632,14 +619,13 @@ export function DomainEditPage() { if (!form.ready) { if (form.error) { return <> - - - Domain not found +
+

{form.error}

; } - return Loading domain...; + return Loading domain...; } // Handler for domain deletion @@ -655,14 +641,13 @@ export function DomainEditPage() { }; const deleteButton = Delete; const refreshButton = domain?.directory ? ( @@ -671,7 +656,6 @@ export function DomainEditPage() { size='small' onClick={handleRefreshDomain} obeyWROLMode={true} - style={{marginTop: '1em'}} >Refresh ) : null; @@ -680,7 +664,6 @@ export function DomainEditPage() { size='small' onClick={() => setTagEditModalOpen(true)} color='violet' - style={{marginTop: '1em'}} >Tag; const reorganizeButton = ( @@ -689,7 +672,6 @@ export function DomainEditPage() { size='small' onClick={() => setReorganizeModalOpen(true)} obeyWROLMode={true} - style={{marginTop: '1em'}} >Reorganize Files ); @@ -703,14 +685,15 @@ export function DomainEditPage() { const [descriptionProps] = form.getCustomProps({name: 'description', path: 'description'}); return <> - - - - +
+ + + + +
{domain?.needs_reorganization && ( - - File Format Changed +

The file name format has changed. Click "Reorganize Files" to move existing files to match the new format. @@ -725,29 +708,31 @@ export function DomainEditPage() { actionButtons={actionButtons} appliedTagName={domain?.tag_name} > - - + {/* + * CollectionEditForm lays its children out as a grid, and "row"/"column" are that + * grid's own class names -- plain divs, not components. + */} +

+
- - - - - - -