From 7e12b53e9fb16f053f9d43ad0d4c5889944118c9 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Mon, 1 Jun 2026 16:28:43 -0400 Subject: [PATCH 01/12] Add Storybook for desktop platform with devices/ stories - .storybook/main.ts: webpack5 config reusing desktop alias/babel/define patterns - .storybook/mocks/electron.ts: KB2 stub so platform.tsx loads without Electron preload - .storybook/preview.ts: injects globalThis._fromPreload, base decorator - .storybook/preview-head.html: font-face + CSS reset via staticDirs fonts - staticDirs: fonts/electron + images served at correct URL paths - tsconfig.desktop.json: include .storybook/ files - devices/device-icon.stories.tsx: 7 stories - devices/row.stories.tsx: 6 stories (incl. BadgedDeviceIDsContext) - devices/device-page.stories.tsx: 6 stories - plans/storybook.md: todo list of remaining shared/ folders Run with: cd shared && yarn storybook --- plans/storybook.md | 24 + shared/.storybook/main.ts | 115 +++ shared/.storybook/mocks/electron.ts | 43 + shared/.storybook/preview-head.html | 45 + shared/.storybook/preview.ts | 57 ++ shared/devices/device-icon.stories.tsx | 45 + shared/devices/device-page.stories.tsx | 75 ++ shared/devices/row.stories.tsx | 80 ++ shared/package.json | 4 + shared/tsconfig.desktop.json | 2 +- shared/yarn.lock | 1044 +++++++++++++++++++++++- 11 files changed, 1501 insertions(+), 33 deletions(-) create mode 100644 plans/storybook.md create mode 100644 shared/.storybook/main.ts create mode 100644 shared/.storybook/mocks/electron.ts create mode 100644 shared/.storybook/preview-head.html create mode 100644 shared/.storybook/preview.ts create mode 100644 shared/devices/device-icon.stories.tsx create mode 100644 shared/devices/device-page.stories.tsx create mode 100644 shared/devices/row.stories.tsx diff --git a/plans/storybook.md b/plans/storybook.md new file mode 100644 index 000000000000..6ac1d5e7e138 --- /dev/null +++ b/plans/storybook.md @@ -0,0 +1,24 @@ +# Storybook TODO + +Top-level `shared/` folders to add stories for. Initial `devices/` is done. + +- [x] devices +- [ ] chat +- [ ] common-adapters +- [ ] crypto +- [ ] fs +- [ ] git +- [ ] login +- [ ] menubar +- [ ] people +- [ ] pinentry +- [ ] profile +- [ ] provision +- [ ] router-v2 +- [ ] settings +- [ ] signup +- [ ] team-building +- [ ] teams +- [ ] tracker +- [ ] unlock-folders +- [ ] wallets diff --git a/shared/.storybook/main.ts b/shared/.storybook/main.ts new file mode 100644 index 000000000000..d2ead444cb99 --- /dev/null +++ b/shared/.storybook/main.ts @@ -0,0 +1,115 @@ +import path from 'path' +import webpack from 'webpack' +import {createRequire} from 'module' +import {fileURLToPath} from 'url' +import type {StorybookConfig} from '@storybook/react-webpack5' + +const require = createRequire(import.meta.url) +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const rootDir = path.resolve(__dirname, '..') +const nullModulePath = path.resolve(rootDir, 'null-module.js') +const ignoredModules = require('../ignored-modules') as Array + +const makeAliases = (): Record => { + // Sort longest-first: webpack checks in insertion order; longer prefixes must come first + // so subpath entries (e.g. 'foo/bar') are matched before their parent package ('foo'). + const sortedModules = [...ignoredModules].sort((a, b) => b.length - a.length) + const alias = sortedModules.reduce>((acc, name) => { + acc[name] = nullModulePath + return acc + }, {}) + return { + ...alias, + 'react-native$': 'react-native-web', + 'react-native-reanimated': false, + 'react-native/Libraries/Image/resolveAssetSource': nullModulePath, + 'react-native-safe-area-context': path.resolve(rootDir, 'desktop/stubs/react-native-safe-area-context.js'), + '@react-native-picker/picker': path.resolve(rootDir, 'desktop/stubs/react-native-picker.js'), + // electron stub MUST come before '@' (insertion order matters for webpack alias matching) + '@/util/electron$': path.resolve(__dirname, 'mocks/electron.ts'), + '@': rootDir, + } +} + +const config: StorybookConfig = { + stories: ['../devices/**/*.stories.tsx'], + addons: [], + framework: { + name: '@storybook/react-webpack5', + options: {}, + }, + staticDirs: [ + {from: '../fonts/electron', to: '/fonts/electron'}, + {from: '../images', to: '/images'}, + ], + typescript: { + check: false, + reactDocgen: false, + }, + webpackFinal: webpackConfig => { + // Aliases + extensions (.tsx/.ts must be listed so webpack resolves index files and bare paths) + webpackConfig.resolve = webpackConfig.resolve ?? {} + webpackConfig.resolve.alias = { + ...(webpackConfig.resolve.alias ?? {}), + ...makeAliases(), + } + webpackConfig.resolve.extensions = ['.tsx', '.ts', '.desktop.tsx', '.desktop.ts', '.js', '.jsx', '.json'] + + // Storybook 10 does not include a JS/TS transpiler by default — add babel-loader + // so that TypeScript and JSX in story files and preview config are compiled. + // We provide explicit presets rather than relying on babel.config.js caller detection + // (the project config only enables @babel/preset-react for test env, not webpack). + webpackConfig.module = webpackConfig.module ?? {rules: []} + webpackConfig.module.rules = webpackConfig.module.rules ?? [] + webpackConfig.module.rules.push({ + test: /\.(tsx?|jsx?)$/, + exclude: /node_modules/, + use: [ + { + loader: require.resolve('babel-loader'), + options: { + presets: [ + ['@babel/preset-env', {targets: {browsers: 'last 2 Chrome versions'}}], + ['@babel/preset-react', {runtime: 'automatic'}], + '@babel/preset-typescript', + ], + // No module-resolver here — webpack handles '@' alias directly so + // the webpack alias overrides (e.g. @/util/electron → mock) apply correctly. + }, + }, + ], + }) + + // Fonts as assets (mirrors desktop/webpack.config.mts) + webpackConfig.module.rules.push({ + test: /\.ttf$/, + type: 'asset/resource', + }) + + // Null-load native-only files (must run before other loaders) + webpackConfig.module.rules.unshift({ + test: /\.(native|ios|android)\.(ts|js)x?$/, + use: ['null-loader'], + }) + + // Platform globals — same as desktop/webpack.config.mts makeDefineValues + webpackConfig.plugins = webpackConfig.plugins ?? [] + webpackConfig.plugins.push( + new webpack.DefinePlugin({ + isMobile: JSON.stringify(false), + isElectron: JSON.stringify(true), + isAndroid: JSON.stringify(false), + isIOS: JSON.stringify(false), + __DEV__: JSON.stringify(true), + __HOT__: JSON.stringify(false), + __PROFILE__: JSON.stringify(false), + __VERSION__: JSON.stringify('storybook'), + __FILE_SUFFIX__: JSON.stringify(''), + }) + ) + + return webpackConfig + }, +} + +export default config diff --git a/shared/.storybook/mocks/electron.ts b/shared/.storybook/mocks/electron.ts new file mode 100644 index 000000000000..58b912b3a9d4 --- /dev/null +++ b/shared/.storybook/mocks/electron.ts @@ -0,0 +1,43 @@ +import type {KB2} from '../../util/electron' + +const stub: KB2 = { + constants: { + assetRoot: '/', + configOverload: {}, + dokanPath: '', + downloadFolder: '', + env: { + APPDATA: '', + HOME: '/tmp', + KEYBASE_AUTOSTART: '', + KEYBASE_CRASH_REPORT: '', + KEYBASE_DEVEL_USE_XDG: '', + KEYBASE_RESTORE_UI: '', + KEYBASE_RUN_MODE: 'prod', + KEYBASE_START_UI: '', + KEYBASE_XDG_OVERRIDE: '', + LANG: 'en_US.UTF-8', + LC_ALL: '', + LC_TIME: '', + LOCALAPPDATA: '', + XDG_CACHE_HOME: '', + XDG_CONFIG_HOME: '', + XDG_DATA_HOME: '', + XDG_DOWNLOAD_DIR: '', + XDG_RUNTIME_DIR: '', + }, + helloDetails: {argv: [], clientType: 2 as const, desc: 'Main Renderer', pid: 0, version: ''}, + isRenderer: true, + pathSep: '/' as const, + platform: 'darwin' as const, + startDarkMode: false, + windowsBinPath: '', + }, + functions: { + mainWindowDispatch: () => {}, + }, +} + +export default stub +export const injectPreload = () => {} +export const waitOnKB2Loaded = (cb: () => void) => cb() diff --git a/shared/.storybook/preview-head.html b/shared/.storybook/preview-head.html new file mode 100644 index 000000000000..b4c061fd2f70 --- /dev/null +++ b/shared/.storybook/preview-head.html @@ -0,0 +1,45 @@ + diff --git a/shared/.storybook/preview.ts b/shared/.storybook/preview.ts new file mode 100644 index 000000000000..15d394e2b1d1 --- /dev/null +++ b/shared/.storybook/preview.ts @@ -0,0 +1,57 @@ +import React from 'react' +import type {Preview} from '@storybook/react' +import type {KB2} from '../util/electron' + +// Inject a minimal KB2 stub so util/electron.tsx's getStashed() doesn't throw. +// The real app sets this from the Electron preload script; storybook sets it here. +const kb2Stub: KB2 = { + constants: { + assetRoot: '/', + configOverload: {}, + dokanPath: '', + downloadFolder: '', + env: { + APPDATA: '', + HOME: '/tmp', + KEYBASE_AUTOSTART: '', + KEYBASE_CRASH_REPORT: '', + KEYBASE_DEVEL_USE_XDG: '', + KEYBASE_RESTORE_UI: '', + KEYBASE_RUN_MODE: 'prod', + KEYBASE_START_UI: '', + KEYBASE_XDG_OVERRIDE: '', + LANG: 'en_US.UTF-8', + LC_ALL: '', + LC_TIME: '', + LOCALAPPDATA: '', + XDG_CACHE_HOME: '', + XDG_CONFIG_HOME: '', + XDG_DATA_HOME: '', + XDG_DOWNLOAD_DIR: '', + XDG_RUNTIME_DIR: '', + }, + helloDetails: {argv: [], clientType: 2 as const, desc: 'Main Renderer', pid: 0, version: ''}, + isRenderer: true, + pathSep: '/' as const, + platform: 'darwin' as const, + startDarkMode: false, + windowsBinPath: '', + }, + functions: { + mainWindowDispatch: () => {}, + }, +} +// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access +;(globalThis as any)._fromPreload = kb2Stub + +const preview: Preview = { + decorators: [ + Story => + React.createElement('div', {style: {background: '#ffffff', padding: 16}}, React.createElement(Story)), + ], + parameters: { + layout: 'fullscreen', + }, +} + +export default preview diff --git a/shared/devices/device-icon.stories.tsx b/shared/devices/device-icon.stories.tsx new file mode 100644 index 000000000000..1d4591265a30 --- /dev/null +++ b/shared/devices/device-icon.stories.tsx @@ -0,0 +1,45 @@ +import type {Meta, StoryObj} from '@storybook/react' +import type * as T from '@/constants/types' +import DeviceIcon from './device-icon' + +const makeDevice = ( + type: T.Devices.DeviceType, + deviceNumberOfType: number = 0 +): T.Devices.Device => ({ + created: Date.now(), + currentDevice: false, + deviceID: 'test-id', + deviceNumberOfType, + lastUsed: Date.now(), + name: 'test device', + type, +}) + +const meta: Meta = { + component: DeviceIcon, + title: 'Devices/DeviceIcon', +} +export default meta +type Story = StoryObj + +export const DesktopSize32: Story = { + args: {device: makeDevice('desktop', 0), size: 32}, +} +export const DesktopSize32Current: Story = { + args: {device: makeDevice('desktop', 0), size: 32, current: true}, +} +export const DesktopSize64: Story = { + args: {device: makeDevice('desktop', 1), size: 64}, +} +export const MobileSize32: Story = { + args: {device: makeDevice('mobile', 0), size: 32}, +} +export const MobileSize64: Story = { + args: {device: makeDevice('mobile', 0), size: 64}, +} +export const PaperKeySize32: Story = { + args: {device: makeDevice('backup', 0), size: 32}, +} +export const PaperKeySize64: Story = { + args: {device: makeDevice('backup', 0), size: 64}, +} diff --git a/shared/devices/device-page.stories.tsx b/shared/devices/device-page.stories.tsx new file mode 100644 index 000000000000..35e244e2decc --- /dev/null +++ b/shared/devices/device-page.stories.tsx @@ -0,0 +1,75 @@ +import type {Meta, StoryObj} from '@storybook/react' +import type * as T from '@/constants/types' +import DevicePage from './device-page' + +const now = Date.now() +const monthAgo = now - 30 * 24 * 60 * 60 * 1000 +const weekAgo = now - 7 * 24 * 60 * 60 * 1000 +const yearAgo = now - 365 * 24 * 60 * 60 * 1000 + +const makeDevice = (overrides: Partial = {}): T.Devices.Device => ({ + created: yearAgo, + currentDevice: false, + deviceID: 'device-001', + deviceNumberOfType: 0, + lastUsed: weekAgo, + name: 'My Device', + type: 'desktop', + provisionerName: 'chrisnojima-mac', + ...overrides, +}) + +const meta: Meta = { + component: DevicePage, + title: 'Devices/DevicePage', + args: {canRevoke: true}, +} +export default meta +type Story = StoryObj + +export const DesktopActive: Story = { + args: { + device: makeDevice({name: 'work-laptop', currentDevice: false}), + canRevoke: true, + }, +} + +export const CurrentDevice: Story = { + args: { + device: makeDevice({name: 'chrisnojima-mac', currentDevice: true, lastUsed: now}), + canRevoke: false, + }, +} + +export const LastDevice: Story = { + args: { + device: makeDevice({name: 'only-device', currentDevice: true}), + canRevoke: false, + }, +} + +export const MobileDevice: Story = { + args: { + device: makeDevice({name: 'iPhone 15', type: 'mobile', deviceNumberOfType: 1}), + canRevoke: true, + }, +} + +export const PaperKey: Story = { + args: { + device: makeDevice({name: 'My paper key', type: 'backup', created: monthAgo}), + canRevoke: true, + }, +} + +export const RevokedDevice: Story = { + args: { + device: makeDevice({ + name: 'old-laptop', + revokedAt: monthAgo, + revokedByName: 'chrisnojima-mac', + lastUsed: monthAgo, + }), + canRevoke: false, + }, +} diff --git a/shared/devices/row.stories.tsx b/shared/devices/row.stories.tsx new file mode 100644 index 000000000000..ff679c30bb9d --- /dev/null +++ b/shared/devices/row.stories.tsx @@ -0,0 +1,80 @@ +import type {Meta, StoryObj} from '@storybook/react' +import type * as T from '@/constants/types' +import DeviceRow, {BadgedDeviceIDsContext} from './row' + +const now = Date.now() +const weekAgo = now - 7 * 24 * 60 * 60 * 1000 + +const makeDevice = (overrides: Partial = {}): T.Devices.Device => ({ + created: weekAgo, + currentDevice: false, + deviceID: 'device-001', + deviceNumberOfType: 0, + lastUsed: weekAgo, + name: 'My Device', + type: 'desktop', + ...overrides, +}) + +const meta: Meta = { + component: DeviceRow, + title: 'Devices/DeviceRow', + args: { + canRevoke: true, + firstItem: true, + }, +} +export default meta +type Story = StoryObj + +export const DesktopCurrent: Story = { + args: { + device: makeDevice({name: 'chrisnojima-mac', currentDevice: true}), + }, +} + +export const DesktopActive: Story = { + args: { + device: makeDevice({name: 'work-laptop', currentDevice: false}), + firstItem: false, + }, +} + +export const MobileActive: Story = { + args: { + device: makeDevice({name: 'iPhone 15', type: 'mobile'}), + firstItem: false, + }, +} + +export const PaperKey: Story = { + args: { + device: makeDevice({name: 'Paper key', type: 'backup'}), + firstItem: false, + }, +} + +export const Revoked: Story = { + args: { + device: makeDevice({ + name: 'old-laptop', + revokedAt: weekAgo, + revokedByName: 'chrisnojima-mac', + }), + firstItem: false, + }, +} + +export const NewBadge: Story = { + decorators: [ + Story => ( + + + + ), + ], + args: { + device: makeDevice({name: 'new-phone', type: 'mobile', deviceID: 'device-new'}), + firstItem: false, + }, +} diff --git a/shared/package.json b/shared/package.json index 54802b630aa0..e1527728c6d9 100644 --- a/shared/package.json +++ b/shared/package.json @@ -64,6 +64,7 @@ "rn:start": "npx expo start --clear 2>&1", "rn:start:log": "npx expo start --clear 2>&1 | tee /tmp/metro.log", "rn:start:legacy": "./react-native/packageAndBuild.sh", + "storybook": "storybook dev -p 6006 --config-dir .storybook", "sync:kb-modules": "yarn run _helper sync-local-rnmodules", "test:e2e:desktop": "playwright test --config tests/e2e/electron/playwright.config.ts", "test:e2e:desktop:branch": "playwright test --config tests/e2e/electron/playwright.config.ts tests/e2e/electron/flows/team-member.test.ts && yarn test:e2e:desktop:report", @@ -153,6 +154,8 @@ "@react-native/babel-preset": "0.85.3", "@react-native/eslint-config": "0.85.3", "@react-native/metro-config": "0.85.3", + "@storybook/react": "10.4.1", + "@storybook/react-webpack5": "10.4.1", "@testing-library/dom": "10.4.1", "@testing-library/react": "16.3.2", "@types/google-libphonenumber": "7.4.30", @@ -184,6 +187,7 @@ "null-loader": "4.0.1", "patch-package": "8.0.1", "react-refresh": "0.18.0", + "storybook": "10.4.1", "style-loader": "4.0.0", "terser-webpack-plugin": "5.6.1", "typescript": "6.0.3", diff --git a/shared/tsconfig.desktop.json b/shared/tsconfig.desktop.json index 656d2551129f..cf08a4d443eb 100644 --- a/shared/tsconfig.desktop.json +++ b/shared/tsconfig.desktop.json @@ -7,7 +7,7 @@ "outDir": "./.tsOuts/.tsOut-desktop/emit", "tsBuildInfoFile": "./.tsOuts/.tsOut-desktop/cache" }, - "include": ["./**/*.mts", "./**/*.mjs", "./**/*.ts", "./**/*.tsx"], + "include": ["./**/*.mts", "./**/*.mjs", "./**/*.ts", "./**/*.tsx", "./.storybook/**/*.ts", "./.storybook/**/*.tsx"], "exclude": [ "**/node_modules", "./desktop/dist", diff --git a/shared/yarn.lock b/shared/yarn.lock index a40c8e63f1aa..eb20aa1fb0a3 100644 --- a/shared/yarn.lock +++ b/shared/yarn.lock @@ -2,6 +2,11 @@ # yarn lockfile v1 +"@adobe/css-tools@^4.4.0": + version "4.5.0" + resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.5.0.tgz#b5b71a25a4d16afa2482592ddfa62fccc60bc7d1" + integrity sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q== + "@asamuzakjp/css-color@^3.2.0": version "3.2.0" resolved "https://registry.yarnpkg.com/@asamuzakjp/css-color/-/css-color-3.2.0.tgz#cc42f5b85c593f79f1fa4f25d2b9b321e61d1794" @@ -13,7 +18,7 @@ "@csstools/css-tokenizer" "^3.0.3" lru-cache "^10.4.3" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.20.0", "@babel/code-frame@^7.27.1", "@babel/code-frame@^7.29.0", "@babel/code-frame@^7.29.7": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.16.7", "@babel/code-frame@^7.20.0", "@babel/code-frame@^7.27.1", "@babel/code-frame@^7.29.0", "@babel/code-frame@^7.29.7": version "7.29.7" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.29.7.tgz#f2fbbfea87c44a21590ec515b778b2c26d8866e7" integrity sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw== @@ -27,7 +32,7 @@ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.29.7.tgz#6f0237f0f36d2e51c0570a636faed9d2d0efe629" integrity sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg== -"@babel/core@7.29.7", "@babel/core@^7.20.0", "@babel/core@^7.23.9", "@babel/core@^7.24.4", "@babel/core@^7.25.2", "@babel/core@^7.27.4": +"@babel/core@7.29.7", "@babel/core@^7.18.9", "@babel/core@^7.20.0", "@babel/core@^7.23.9", "@babel/core@^7.24.4", "@babel/core@^7.25.2", "@babel/core@^7.27.4", "@babel/core@^7.28.0": version "7.29.7" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.29.7.tgz#80c10b17248082968b57a857b91640971f2070f7" integrity sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA== @@ -1062,7 +1067,7 @@ "@babel/parser" "^7.29.7" "@babel/types" "^7.29.7" -"@babel/traverse@^7.29.0", "@babel/traverse@^7.29.7": +"@babel/traverse@^7.18.9", "@babel/traverse@^7.28.0", "@babel/traverse@^7.29.0", "@babel/traverse@^7.29.7": version "7.29.7" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.29.7.tgz#c47b07a41b95da0907d026b5dd894d98de7d2f2d" integrity sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw== @@ -1075,7 +1080,7 @@ "@babel/types" "^7.29.7" debug "^4.3.1" -"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.26.0", "@babel/types@^7.27.3", "@babel/types@^7.28.2", "@babel/types@^7.29.0", "@babel/types@^7.29.7", "@babel/types@^7.4.4": +"@babel/types@^7.0.0", "@babel/types@^7.18.9", "@babel/types@^7.20.7", "@babel/types@^7.26.0", "@babel/types@^7.27.3", "@babel/types@^7.28.2", "@babel/types@^7.29.0", "@babel/types@^7.29.7", "@babel/types@^7.4.4": version "7.29.7" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.29.7.tgz#8005e31d82712ee7adaef6e23c63b71a62770a92" integrity sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA== @@ -1217,6 +1222,14 @@ "@emnapi/wasi-threads" "1.2.1" tslib "^2.4.0" +"@emnapi/core@1.9.2": + version "1.9.2" + resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.9.2.tgz#3870265ecffc7352d01ead62d8d83d8358a2d034" + integrity sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA== + dependencies: + "@emnapi/wasi-threads" "1.2.1" + tslib "^2.4.0" + "@emnapi/runtime@1.10.0": version "1.10.0" resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.10.0.tgz#4b260c0d3534204e98c6110b8db1a987d26ec87c" @@ -1224,6 +1237,13 @@ dependencies: tslib "^2.4.0" +"@emnapi/runtime@1.9.2": + version "1.9.2" + resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.9.2.tgz#8b469a3db160817cadb1de9050211a9d1ea84fa2" + integrity sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw== + dependencies: + tslib "^2.4.0" + "@emnapi/wasi-threads@1.2.1": version "1.2.1" resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz#28fed21a1ba1ce797c44a070abc94d42f3ae8548" @@ -1236,6 +1256,136 @@ resolved "https://registry.yarnpkg.com/@epic-web/invariant/-/invariant-1.0.0.tgz#1073e5dee6dd540410784990eb73e4acd25c9813" integrity sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA== +"@esbuild/aix-ppc64@0.27.7": + version "0.27.7" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz#82b74f92aa78d720b714162939fb248c90addf53" + integrity sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg== + +"@esbuild/android-arm64@0.27.7": + version "0.27.7" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz#f78cb8a3121fc205a53285adb24972db385d185d" + integrity sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ== + +"@esbuild/android-arm@0.27.7": + version "0.27.7" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.27.7.tgz#593e10a1450bbfcac6cb321f61f468453bac209d" + integrity sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ== + +"@esbuild/android-x64@0.27.7": + version "0.27.7" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.27.7.tgz#453143d073326033d2d22caf9e48de4bae274b07" + integrity sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg== + +"@esbuild/darwin-arm64@0.27.7": + version "0.27.7" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz#6f23000fb9b40b7e04b7d0606c0693bd0632f322" + integrity sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw== + +"@esbuild/darwin-x64@0.27.7": + version "0.27.7" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz#27393dd18bb1263c663979c5f1576e00c2d024be" + integrity sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ== + +"@esbuild/freebsd-arm64@0.27.7": + version "0.27.7" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz#22e4638fa502d1c0027077324c97640e3adf3a62" + integrity sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w== + +"@esbuild/freebsd-x64@0.27.7": + version "0.27.7" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz#9224b8e4fea924ce2194e3efc3e9aebf822192d6" + integrity sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ== + +"@esbuild/linux-arm64@0.27.7": + version "0.27.7" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz#4f5d1c27527d817b35684ae21419e57c2bda0966" + integrity sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A== + +"@esbuild/linux-arm@0.27.7": + version "0.27.7" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz#b9e9d070c8c1c0449cf12b20eac37d70a4595921" + integrity sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA== + +"@esbuild/linux-ia32@0.27.7": + version "0.27.7" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz#3f80fb696aa96051a94047f35c85b08b21c36f9e" + integrity sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg== + +"@esbuild/linux-loong64@0.27.7": + version "0.27.7" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz#9be1f2c28210b13ebb4156221bba356fe1675205" + integrity sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q== + +"@esbuild/linux-mips64el@0.27.7": + version "0.27.7" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz#4ab5ee67a3dfcbcb5e8fd7883dae6e735b1163b8" + integrity sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw== + +"@esbuild/linux-ppc64@0.27.7": + version "0.27.7" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz#dac78c689f6499459c4321e5c15032c12307e7ea" + integrity sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ== + +"@esbuild/linux-riscv64@0.27.7": + version "0.27.7" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz#050f7d3b355c3a98308e935bc4d6325da91b0027" + integrity sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ== + +"@esbuild/linux-s390x@0.27.7": + version "0.27.7" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz#d61f715ce61d43fe5844ad0d8f463f88cbe4fef6" + integrity sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw== + +"@esbuild/linux-x64@0.27.7": + version "0.27.7" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz#ca8e1aa478fc8209257bf3ac8f79c4dc2982f32a" + integrity sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA== + +"@esbuild/netbsd-arm64@0.27.7": + version "0.27.7" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz#1650f2c1b948deeb3ef948f2fc30614723c09690" + integrity sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w== + +"@esbuild/netbsd-x64@0.27.7": + version "0.27.7" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz#65772ab342c4b3319bf0705a211050aac1b6e320" + integrity sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw== + +"@esbuild/openbsd-arm64@0.27.7": + version "0.27.7" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz#37ed7cfa66549d7955852fce37d0c3de4e715ea1" + integrity sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A== + +"@esbuild/openbsd-x64@0.27.7": + version "0.27.7" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz#01bf3d385855ef50cb33db7c4b52f957c34cd179" + integrity sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg== + +"@esbuild/openharmony-arm64@0.27.7": + version "0.27.7" + resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz#6c1f94b34086599aabda4eac8f638294b9877410" + integrity sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw== + +"@esbuild/sunos-x64@0.27.7": + version "0.27.7" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz#4b0dd17ae0a6941d2d0fd35a906392517071a90d" + integrity sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA== + +"@esbuild/win32-arm64@0.27.7": + version "0.27.7" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz#34193ab5565d6ff68ca928ac04be75102ccb2e77" + integrity sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA== + +"@esbuild/win32-ia32@0.27.7": + version "0.27.7" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz#eb67f0e4482515d8c1894ede631c327a4da9fc4d" + integrity sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw== + +"@esbuild/win32-x64@0.27.7": + version "0.27.7" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz#8fe30b3088b89b4873c3a6cc87597ae3920c0a8b" + integrity sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg== + "@eslint-community/eslint-utils@^4.4.0", "@eslint-community/eslint-utils@^4.8.0", "@eslint-community/eslint-utils@^4.9.1": version "4.9.1" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz#4e90af67bc51ddee6cdef5284edf572ec376b595" @@ -2315,6 +2465,214 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@oxc-parser/binding-android-arm-eabi@0.127.0": + version "0.127.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.127.0.tgz#b75e796249ee22f632e40e942746c4bf648cee92" + integrity sha512-0LC7ye4hvqbIKxAzThzvswgHLFu2AURKzYLeSVvLdu2TBOYWQDmHnTqPLeA597BcUCxiLqLsS4CJ5uoI5WYWCQ== + +"@oxc-parser/binding-android-arm64@0.127.0": + version "0.127.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.127.0.tgz#e264467fe39f80018f62fa0dae82db0b80260444" + integrity sha512-b5jtVTH6AU5CJXHNdj7Jj9IEiR9yVjjnwHzPJhGyHGPdcsZSzBCkS9GBbV33niRMvKthDwQRFRJfI4a+k4PvYg== + +"@oxc-parser/binding-darwin-arm64@0.127.0": + version "0.127.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.127.0.tgz#0576d35109c00dcc6277200ba2eca7b47e07f1b1" + integrity sha512-obCE8B7ISKkJidjlhv9xRGJPOSDG2Yu6PRga9Ruaz35uintHxbp1Ki/Yc71wx4rj3Edrm0a1kzG1TAwit0wFpg== + +"@oxc-parser/binding-darwin-x64@0.127.0": + version "0.127.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.127.0.tgz#efa1ba49075aa318ff540a1c2f8a442017417206" + integrity sha512-JL6Xb5IwPQT8rUzlpsX7E+AgfcdNklXNPFp8pjCQQ5MQOQo5rtEB2ui+3Hgg9Sn7Y9Egj6YOLLiHhLpdAe12Aw== + +"@oxc-parser/binding-freebsd-x64@0.127.0": + version "0.127.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.127.0.tgz#817ba3c508d751d94d6e6fd86af69ddaa27da531" + integrity sha512-SDQ/3MQFw58fqQz3Z1PhSKFF3JoCF4gmlNjziDm8X02tTahCw0qJbd7FGPDKw1i4VTBZene9JPyC3mHtSvi+wA== + +"@oxc-parser/binding-linux-arm-gnueabihf@0.127.0": + version "0.127.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.127.0.tgz#b1c3096c654771998480316ef10d1e5d29edc79b" + integrity sha512-Av+D1MIqzV0YMGPT9we2SIZaMKD7Cxs4CvXSx/yxaWHewZjYEjScpOf5igc8IILASViw4WTnjlwUdI1KzVtDHQ== + +"@oxc-parser/binding-linux-arm-musleabihf@0.127.0": + version "0.127.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.127.0.tgz#c44a8f10e6c903685825aebf1289fc2086aed61e" + integrity sha512-Cs2fdJ8cPpFdeebj6p4dag8A4+56hPvZ0AhQQzlaLswGz1tz7bXt1nETLeorrM9+AMcWFFkqxcXwDGfTVidY8g== + +"@oxc-parser/binding-linux-arm64-gnu@0.127.0": + version "0.127.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.127.0.tgz#61c245abfab6f63045915b5c9cfa7d335ad7c440" + integrity sha512-qdOfTcT6SY8gsJrrV92uyEUyjqMGPpIB5JZUG6QN5dukYd+7/j0kX6MwK1DgQj39jtUYixxPiaRUiEN1+0CXgQ== + +"@oxc-parser/binding-linux-arm64-musl@0.127.0": + version "0.127.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.127.0.tgz#358bbd90e5c85b6c35125f5a6ff084e09b694c04" + integrity sha512-EoTCZneNFU/P2qrpEM+RHmQwt+CvDkyGESG6qhr7KaegXLZwePfbrkCDfAk8/rhxbDUVGsZILX+2tqPzFtoFWA== + +"@oxc-parser/binding-linux-ppc64-gnu@0.127.0": + version "0.127.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.127.0.tgz#b7ea7b51bf54db4c42819187f760e069d433dac3" + integrity sha512-zALjmZYgxFLHjXeudcDF0xFGNydTAtkAeXAr2EuC17ywCyFxcmQra4w0BMde0Yi/re4Bi4iwEoEXtYN7l6eBLQ== + +"@oxc-parser/binding-linux-riscv64-gnu@0.127.0": + version "0.127.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.127.0.tgz#3a3b10d160988df50bbbcd631c6af39de3dd451d" + integrity sha512-fPP8M6zQLS7Jz7o9d5ArUSuAuSK3e+WCYVrCpdzeCOejidtZExJ9tjhDrAd3HEPqARBCPmdpqxESPFqy44vkBQ== + +"@oxc-parser/binding-linux-riscv64-musl@0.127.0": + version "0.127.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.127.0.tgz#3787d37e1d0a15ee239f51610298500321b31730" + integrity sha512-7IcC4Ao02oGpfnjt+X/oF4U2mllo2qoSkw5xxiXNKL9MCTsTiAC6616beOuehdxGcnz1bRoPC1RQ2f1GQDdN+g== + +"@oxc-parser/binding-linux-s390x-gnu@0.127.0": + version "0.127.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.127.0.tgz#b71a16cbba115a4696498f9149bc54cc4e1df9cd" + integrity sha512-pbXIhiNFHoqWeqDNLiJ9JkpHz1IM9k4DXa66x+1GTWMG7iLxtkXgE53iiuKSXwmk3zIYmaPVfBvgcAhS583K4Q== + +"@oxc-parser/binding-linux-x64-gnu@0.127.0": + version "0.127.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.127.0.tgz#71527dd0284ba727d35a93c841c91192af3ebdec" + integrity sha512-MYCguB9RvBvlSd6gbuNI7QwiLoCCAlGnlRJFPrzLI6U1/9wkC/WK6LtBAUln55H1Ctqw45PWmqrobKoMhsYQzQ== + +"@oxc-parser/binding-linux-x64-musl@0.127.0": + version "0.127.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.127.0.tgz#16830afa4b001f349cebb93e12b278e72601cb3f" + integrity sha512-5eY0B/bxf1xIUxb4NOTvOI3KWtBQfPWYyKAzgcrCt0mDibSZygVpO1Pz8bkeiSZ5Jj9+M09dkggG3H8I5d0Uyg== + +"@oxc-parser/binding-openharmony-arm64@0.127.0": + version "0.127.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.127.0.tgz#a41c71d249cb597dc357038eb1cbe3ce732453f8" + integrity sha512-Gld0ajrFTUXNtdw20fVBuTQx66FA75nIVg+//pPfR3sXkuABB4mTBhl3r9JNzrJpgW//qiwxf0nWXUWGJSL3UQ== + +"@oxc-parser/binding-wasm32-wasi@0.127.0": + version "0.127.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.127.0.tgz#b1efcdb433b30ed4a3ad912fa03da3834bd4845d" + integrity sha512-T6KVD7rhLzFlwGRXMnxUFfkCZD8FHnb968wVXW1mXzgRFc5RNXOBY2mPPDZ77x5Ln76ltLMgtPg0cOkU1NSrEQ== + dependencies: + "@emnapi/core" "1.9.2" + "@emnapi/runtime" "1.9.2" + "@napi-rs/wasm-runtime" "^1.1.4" + +"@oxc-parser/binding-win32-arm64-msvc@0.127.0": + version "0.127.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.127.0.tgz#b62b5e328126323d41ae1ee7adc95537c4c4423a" + integrity sha512-Ujvw4X+LD1CCGULcsQcvb4YNVoBGqt+JHgNNzGGaCImELiZLk477ifUH53gIbE7EKd933NdTi25JWEr9K2HwXw== + +"@oxc-parser/binding-win32-ia32-msvc@0.127.0": + version "0.127.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.127.0.tgz#dac30de6971dbe63aa5722be9a4cc070fd3c650e" + integrity sha512-0cwxKO7KHQQQfo4Uf4B2SQrhgm+cJaP9OvFFhx52Tkg4bezsacu83GB2/In5bC415Ueeym+kXdnge/57rbSfTw== + +"@oxc-parser/binding-win32-x64-msvc@0.127.0": + version "0.127.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.127.0.tgz#a2df879b0803f72b350a7567365cee5b8978edf0" + integrity sha512-rOrnSQSCbhI2kowr9XxE7m9a8oQXnBHjnS6j95LxxAnEZ0+Fz20WlRXG4ondQb+ejjt2KOsa65sE6++L6kUd+w== + +"@oxc-project/types@^0.127.0": + version "0.127.0" + resolved "https://registry.yarnpkg.com/@oxc-project/types/-/types-0.127.0.tgz#8374fcdfb4a641861218daa5700c447c00b66663" + integrity sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ== + +"@oxc-resolver/binding-android-arm-eabi@11.20.0": + version "11.20.0" + resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.20.0.tgz#6040f744ee2350f1eee744ea7b16092653c6e221" + integrity sha512-IjfWOXRgJFNdORDl+Uf1aibNgZY2guOD3zmOhx1BGVb/MIiqlFTdmjpQNplSN58lhWehnX4UNqC3QwpUo8pjJg== + +"@oxc-resolver/binding-android-arm64@11.20.0": + version "11.20.0" + resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.20.0.tgz#6bf3c61f1245c84a13602f18d6326f42306e69b0" + integrity sha512-QqslZAuFQG8Q9xm7JuIn8JUbvywhSBMVhuQHtYW+auirZJloS41oxUUaBXk7uUhZJgp44c5zQLeVvmFaDQB+2Q== + +"@oxc-resolver/binding-darwin-arm64@11.20.0": + version "11.20.0" + resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.20.0.tgz#b6fdb43ac0b67bf89c09e2865b848759bc1bcbb6" + integrity sha512-MUcavykj2ewlR+kc5arpg4tC2RvzJkUxWtNv74pf7lcNk00GpIpN43vXMj+j6r4eMmfZhlb8hueKoIb8e9kAGQ== + +"@oxc-resolver/binding-darwin-x64@11.20.0": + version "11.20.0" + resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.20.0.tgz#fc065954554eca454fa50ce3e7643f33ebd14977" + integrity sha512-BGB16nRUK5Etiv//ihPyzj8Lj1px0mhh4YIfe0FDf045ywknfSm0GEbiRESpr6Q4K82AvnyaRIhhluHByvS4bg== + +"@oxc-resolver/binding-freebsd-x64@11.20.0": + version "11.20.0" + resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.20.0.tgz#79d9c0262d959fb8eff09dfb2079184132607482" + integrity sha512-JZgtePaqj3qmD5XFHJaSLWzHRxQu0LaPkdoM1KJXYADvAaa83ijXHclV3ej3CueeW0wxfIAbGCZVP45J0CA7uQ== + +"@oxc-resolver/binding-linux-arm-gnueabihf@11.20.0": + version "11.20.0" + resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.20.0.tgz#67d47c22f31f98d42597b258b9daf6acab441bf3" + integrity sha512-hOQ/p3ry3v3SchUBXicrrnszaI/UmYzM4wtS4RGfwgVUX7a+HbyQSzJ5aOzu+o6XZkFkS3ZXN4PZAzhOb77OSg== + +"@oxc-resolver/binding-linux-arm-musleabihf@11.20.0": + version "11.20.0" + resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.20.0.tgz#da835d77174c74c49a371a0409c8a6b802bf814e" + integrity sha512-2ArPksaw0AqeuGBfoS715VF+JvJQAhD2niWgjE5hVO+L+nAfikVQopvngCMX9x4BD8itWoQ3dnikrQyl5Ho5Jg== + +"@oxc-resolver/binding-linux-arm64-gnu@11.20.0": + version "11.20.0" + resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.20.0.tgz#06123d54159307a9731e70f898e9b751c9670560" + integrity sha512-0bJnmYFp62JdZ4nVMDUZ/C58BCZOCcqgKtnUlp7L9Ojf/czIN+3j72YlLPeWLkzlr6SlYvIQA4SGV/HyO0d+qg== + +"@oxc-resolver/binding-linux-arm64-musl@11.20.0": + version "11.20.0" + resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.20.0.tgz#a9df55e94243526e92a1c646f11e844d243aa2a3" + integrity sha512-wKHHzPKZo7Ufhv/Bt6yxT7FOgnIgW4gwXcJUipkShGp68W3wGVqvr1Sr0fY65lN0Oy6y41+g2kIDvkgZaMMUkw== + +"@oxc-resolver/binding-linux-ppc64-gnu@11.20.0": + version "11.20.0" + resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.20.0.tgz#45559c56a6c8cb9ea8cafffca03c79d2a04097e1" + integrity sha512-RN8goF7Ie0B79L4i4G6OeBocTgSC56vJbQ65VJje+oXnldVpLnOU7j/AQ/dP94TcCS+Yh6WG8u3Qt4ETteXFNQ== + +"@oxc-resolver/binding-linux-riscv64-gnu@11.20.0": + version "11.20.0" + resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.20.0.tgz#8687b6e6cf3592e3cd76a316580d7fcd146e3f1d" + integrity sha512-5l1yU6/xQEqLZRzxqmMxJfWPslpwCmBsdDGaBvABPehxquCXDC7dd7oraNdKSJUMDXSM7VvVj8H2D2FTjU7oWw== + +"@oxc-resolver/binding-linux-riscv64-musl@11.20.0": + version "11.20.0" + resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.20.0.tgz#90ee1c47c2e0cf2c7ed6f542544e4c528d82f978" + integrity sha512-xHEvkbgz6UC+A3JOyDQy76LkUaxsNSfIr3/GV8slwZsnuooJiIB34gzJfsyvR4JdCYNUUPsRJc/w/oWkODu+hg== + +"@oxc-resolver/binding-linux-s390x-gnu@11.20.0": + version "11.20.0" + resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.20.0.tgz#3fe4d600f79978e766a746405bfa54fe05f27a5c" + integrity sha512-aWPDUUmSeyHvlW+SoEUd+JIJsQhVhu6a5tBpDRMu058naPAchTgAVGCFy35zjbnFlt0i8hLWziff6HX0D3LU4g== + +"@oxc-resolver/binding-linux-x64-gnu@11.20.0": + version "11.20.0" + resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.20.0.tgz#bda339de35865aa54f28862e55829607c8d4e923" + integrity sha512-x2YeSimvhJjKLVD8KSu8f/rqU1potcdEMkApIPJqjZWN7c2Fpt4g2X32WDg1p+XDAmyT7nuQGe0vnhvXeLbH+g== + +"@oxc-resolver/binding-linux-x64-musl@11.20.0": + version "11.20.0" + resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.20.0.tgz#c0d5c660faee13a916162f8d83be50015cb72bb5" + integrity sha512-kcRLEIxpZefeYfLChjpgFf3ilBzRDZ+yobMrpRsQlSrxuFGtm3U6PMU7AaEpMqo3NfDGVyJJseAjnRLzMFHjwQ== + +"@oxc-resolver/binding-openharmony-arm64@11.20.0": + version "11.20.0" + resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-openharmony-arm64/-/binding-openharmony-arm64-11.20.0.tgz#f4fe37c4c5cf0804be3af07a53684780d5f9d582" + integrity sha512-HHcfnApSZGtKhTiHqe8OZruOZe5XuFQH5/E0Yhj3u8fnFvzkM4/k6WjacUf4SvA0SPEAbfbgYmVPuo0VX/fIBQ== + +"@oxc-resolver/binding-wasm32-wasi@11.20.0": + version "11.20.0" + resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-11.20.0.tgz#3fbbaf065df34b44d45d8391250aeabf220acf5e" + integrity sha512-Tn0y1XOFYHNfK1wp1Z5QK8Rcld/bsOwRISQXfqAZ5IBpv8Gz1IvV39fUWNprqNdRizgcvFhOzWwFun2zkJsyBg== + dependencies: + "@emnapi/core" "1.10.0" + "@emnapi/runtime" "1.10.0" + "@napi-rs/wasm-runtime" "^1.1.4" + +"@oxc-resolver/binding-win32-arm64-msvc@11.20.0": + version "11.20.0" + resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.20.0.tgz#00e204aac6dddd1fa909d80aa53be6bee10e6b91" + integrity sha512-qPi25YNPe4YenS8MgsQU2+bIFHxxpLx1LVna2444cEHqNPhNjvWf9zqj4aWE43H9LpAsTmkkAlA3eL5ElBU3mA== + +"@oxc-resolver/binding-win32-x64-msvc@11.20.0": + version "11.20.0" + resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.20.0.tgz#56d62047fa2ae7c2695bc901ff16303c8b6429c7" + integrity sha512-Wb14jWEW8huH6It9F6sXd9vrYmIS7pMrgkU6sxpLxkP+9z+wRgs71hUEhRpcn8FOXAFa27FVWfY2tRpbfTzfLw== + "@peculiar/asn1-cms@^2.6.0", "@peculiar/asn1-cms@^2.7.0": version "2.7.0" resolved "https://registry.yarnpkg.com/@peculiar/asn1-cms/-/asn1-cms-2.7.0.tgz#8e0eb656f4fc85f7c621dd442fa2d298faa84984" @@ -2921,6 +3279,96 @@ dependencies: "@sinonjs/commons" "^3.0.1" +"@storybook/builder-webpack5@10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@storybook/builder-webpack5/-/builder-webpack5-10.4.1.tgz#e5705713f21b73b186d5a10d42d016f40407ed91" + integrity sha512-3Ah4jUjg8nEms/5JV6odtQj9+pQ1DT/04s/V6dZKThGdl85YTrYUZV5OTgbNxYbmQn/TwpWWjQlcW8ulpo2WBw== + dependencies: + "@storybook/core-webpack" "10.4.1" + case-sensitive-paths-webpack-plugin "^2.4.0" + cjs-module-lexer "^1.2.3" + css-loader "^7.1.2" + es-module-lexer "^1.5.0" + fork-ts-checker-webpack-plugin "^9.1.0" + html-webpack-plugin "^5.5.0" + magic-string "^0.30.5" + style-loader "^4.0.0" + terser-webpack-plugin "^5.3.17" + ts-dedent "^2.0.0" + webpack "5" + webpack-dev-middleware "^6.1.2" + webpack-hot-middleware "^2.25.1" + webpack-virtual-modules "^0.6.0" + +"@storybook/core-webpack@10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@storybook/core-webpack/-/core-webpack-10.4.1.tgz#9ddef26b8afacf96cb36deee4012632c9bd78956" + integrity sha512-Wert/4ou5WRl8WYWWS8bBW7Lxa/ASMEuQ3EVuG3SITAtPNvKDKqTFBjZLx9eJSefkX6fJ3yG85FFUOPsv6GemQ== + dependencies: + ts-dedent "^2.0.0" + +"@storybook/global@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@storybook/global/-/global-5.0.0.tgz#b793d34b94f572c1d7d9e0f44fac4e0dbc9572ed" + integrity sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ== + +"@storybook/icons@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@storybook/icons/-/icons-2.0.2.tgz#765b49b902a2bdf6f3a5a1aa8dc0cdcd3c55c5b5" + integrity sha512-KZBCpXsshAIjczYNXR/rlxEtCUX/eAbpFNwKi8bcOomrLA4t/SyPz5RF+lVPO2oZBUE4sAkt43mfJUevQDSEEw== + +"@storybook/preset-react-webpack@10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@storybook/preset-react-webpack/-/preset-react-webpack-10.4.1.tgz#ec3e3b1216d06e53b375ce7a9b5594f6c870184a" + integrity sha512-uAR/C/oDZYhReaYpD4Rd5S4VWcXP2XO8+BwXwanKt4UHbYfOw7AQgBTeZ/6Wns/0xIXhOoA1rxO5TA2wDLUjLA== + dependencies: + "@storybook/core-webpack" "10.4.1" + "@storybook/react-docgen-typescript-plugin" "1.0.6--canary.9.0c3f3b7.0" + "@types/semver" "^7.7.1" + magic-string "^0.30.5" + react-docgen "^7.1.1" + resolve "^1.22.8" + semver "^7.7.3" + tsconfig-paths "^4.2.0" + webpack "5" + +"@storybook/react-docgen-typescript-plugin@1.0.6--canary.9.0c3f3b7.0": + version "1.0.6--canary.9.0c3f3b7.0" + resolved "https://registry.yarnpkg.com/@storybook/react-docgen-typescript-plugin/-/react-docgen-typescript-plugin-1.0.6--canary.9.0c3f3b7.0.tgz#7f10f3c641f32e4513a8b6ffb5036933e7059534" + integrity sha512-KUqXC3oa9JuQ0kZJLBhVdS4lOneKTOopnNBK4tUAgoxWQ3u/IjzdueZjFr7gyBrXMoU6duutk3RQR9u8ZpYJ4Q== + dependencies: + debug "^4.1.1" + endent "^2.0.1" + find-cache-dir "^3.3.1" + flat-cache "^3.0.4" + micromatch "^4.0.2" + react-docgen-typescript "^2.2.2" + tslib "^2.0.0" + +"@storybook/react-dom-shim@10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@storybook/react-dom-shim/-/react-dom-shim-10.4.1.tgz#18836217a1ba5fc4040878a6e33bc0fe4dad5e4a" + integrity sha512-6QFqfDNH4DMrt7yHKRfpqRopsVUc/Az+sXIdJ39IetYnHUxL3nW4NVaPc6uy/8Qi8urzUyEXL/nn7cpSIP2aPQ== + +"@storybook/react-webpack5@10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@storybook/react-webpack5/-/react-webpack5-10.4.1.tgz#32631dfa70d508ff7d6da98b8d5ec173252f91a9" + integrity sha512-2jF231DrEk70I8+wVakCnKtpweGFNfxdaov883Rve0TFvhxZs42Y9PpKzSf4rusvSrWc9jdWuJ2k7ERbS50MLg== + dependencies: + "@storybook/builder-webpack5" "10.4.1" + "@storybook/preset-react-webpack" "10.4.1" + "@storybook/react" "10.4.1" + +"@storybook/react@10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@storybook/react/-/react-10.4.1.tgz#078926c18d29a7af51a16588170953c8beea0c45" + integrity sha512-WuYz4NaUk4gmFAMliSpCbV8w6jP5OY9juBfw1huwzu2S/k5FhnVXwmrUaL0fmf3Bq/7NgkzmBBbZr6I6LuHayQ== + dependencies: + "@storybook/global" "^5.0.0" + "@storybook/react-dom-shim" "10.4.1" + react-docgen "^8.0.2" + react-docgen-typescript "^2.2.2" + "@testing-library/dom@10.4.1": version "10.4.1" resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-10.4.1.tgz#d444f8a889e9a46e9a3b4f3b88e0fcb3efb6cf95" @@ -2935,6 +3383,18 @@ picocolors "1.1.1" pretty-format "^27.0.2" +"@testing-library/jest-dom@^6.9.1": + version "6.9.1" + resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz#7613a04e146dd2976d24ddf019730d57a89d56c2" + integrity sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA== + dependencies: + "@adobe/css-tools" "^4.4.0" + aria-query "^5.0.0" + css.escape "^1.5.1" + dom-accessibility-api "^0.6.3" + picocolors "^1.1.1" + redent "^3.0.0" + "@testing-library/react@16.3.2": version "16.3.2" resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-16.3.2.tgz#672883b7acb8e775fc0492d9e9d25e06e89786d0" @@ -2942,6 +3402,11 @@ dependencies: "@babel/runtime" "^7.12.5" +"@testing-library/user-event@^14.6.1": + version "14.6.1" + resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.6.1.tgz#13e09a32d7a8b7060fe38304788ebf4197cd2149" + integrity sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw== + "@tybys/wasm-util@^0.10.1": version "0.10.2" resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.2.tgz#12b3a1b33db1f9cad4ddff1f604ab7dd00bf464e" @@ -2954,7 +3419,7 @@ resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.4.tgz#1a31c3d378850d2778dabb6374d036dcba4ba708" integrity sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw== -"@types/babel__core@^7.20.5": +"@types/babel__core@^7.18.0", "@types/babel__core@^7.20.5": version "7.20.5" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017" integrity sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA== @@ -2980,7 +3445,7 @@ "@babel/parser" "^7.1.0" "@babel/types" "^7.0.0" -"@types/babel__traverse@*": +"@types/babel__traverse@*", "@types/babel__traverse@^7.18.0", "@types/babel__traverse@^7.20.7": version "7.28.0" resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.28.0.tgz#07d713d6cce0d265c9849db0cbe62d3f61f36f74" integrity sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q== @@ -3002,6 +3467,14 @@ dependencies: "@types/node" "*" +"@types/chai@^5.2.2": + version "5.2.3" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-5.2.3.tgz#8e9cd9e1c3581fa6b341a5aed5588eb285be0b4a" + integrity sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA== + dependencies: + "@types/deep-eql" "*" + assertion-error "^2.0.1" + "@types/connect-history-api-fallback@^1.5.4": version "1.5.4" resolved "https://registry.yarnpkg.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz#7de71645a103056b48ac3ce07b3520b819c1d5b3" @@ -3017,6 +3490,16 @@ dependencies: "@types/node" "*" +"@types/deep-eql@*": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/deep-eql/-/deep-eql-4.0.2.tgz#334311971d3a07121e7eb91b684a605e7eea9cbd" + integrity sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw== + +"@types/doctrine@^0.0.9": + version "0.0.9" + resolved "https://registry.yarnpkg.com/@types/doctrine/-/doctrine-0.0.9.tgz#d86a5f452a15e3e3113b99e39616a9baa0f9863f" + integrity sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA== + "@types/emscripten@^1.41.5": version "1.41.5" resolved "https://registry.yarnpkg.com/@types/emscripten/-/emscripten-1.41.5.tgz#5670e4b52b098691cb844b84ee48c9176699b68d" @@ -3194,11 +3677,21 @@ dependencies: csstype "^3.2.2" +"@types/resolve@^1.20.2": + version "1.20.6" + resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.6.tgz#e6e60dad29c2c8c206c026e6dd8d6d1bdda850b8" + integrity sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ== + "@types/retry@0.12.2": version "0.12.2" resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.2.tgz#ed279a64fa438bb69f2480eda44937912bb7480a" integrity sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow== +"@types/semver@^7.7.1": + version "7.7.1" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.7.1.tgz#3ce3af1a5524ef327d2da9e4fd8b6d95c8d70528" + integrity sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA== + "@types/send@*": version "1.2.1" resolved "https://registry.yarnpkg.com/@types/send/-/send-1.2.1.tgz#6a784e45543c18c774c049bff6d3dbaf045c9c74" @@ -3594,6 +4087,40 @@ resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.12.2.tgz#72da0da48d72b1e87831b9c0308931d3f4669027" integrity sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA== +"@vitest/expect@3.2.4": + version "3.2.4" + resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-3.2.4.tgz#8362124cd811a5ee11c5768207b9df53d34f2433" + integrity sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig== + dependencies: + "@types/chai" "^5.2.2" + "@vitest/spy" "3.2.4" + "@vitest/utils" "3.2.4" + chai "^5.2.0" + tinyrainbow "^2.0.0" + +"@vitest/pretty-format@3.2.4": + version "3.2.4" + resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-3.2.4.tgz#3c102f79e82b204a26c7a5921bf47d534919d3b4" + integrity sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA== + dependencies: + tinyrainbow "^2.0.0" + +"@vitest/spy@3.2.4": + version "3.2.4" + resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-3.2.4.tgz#cc18f26f40f3f028da6620046881f4e4518c2599" + integrity sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw== + dependencies: + tinyspy "^4.0.3" + +"@vitest/utils@3.2.4": + version "3.2.4" + resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-3.2.4.tgz#c0813bc42d99527fb8c5b138c7a88516bca46fea" + integrity sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA== + dependencies: + "@vitest/pretty-format" "3.2.4" + loupe "^3.1.4" + tinyrainbow "^2.0.0" + "@vscode/sudo-prompt@^9.0.0": version "9.3.2" resolved "https://registry.yarnpkg.com/@vscode/sudo-prompt/-/sudo-prompt-9.3.2.tgz#692ba38df40bd3502ccc4e9f099fbbaedbd5f04e" @@ -3720,6 +4247,11 @@ "@webassemblyjs/ast" "1.14.1" "@xtuc/long" "4.2.2" +"@webcontainer/env@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@webcontainer/env/-/env-1.1.1.tgz#23021b2bb24befeeef53dba8996d1886b7016515" + integrity sha512-6aN99yL695Hi9SuIk1oC88l9o0gmxL1nGWWQ/kNy81HigJ0FoaoTXpytCj6ItzgyCEwA9kF1wixsTuv5cjsgng== + "@xmldom/xmldom@^0.8.8": version "0.8.13" resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.8.13.tgz#00d1dd940b218dff2e49309d410d8bb212159225" @@ -3853,7 +4385,7 @@ ansi-fragments@^0.2.1: slice-ansi "^2.0.0" strip-ansi "^5.0.0" -ansi-html-community@^0.0.8: +ansi-html-community@0.0.8, ansi-html-community@^0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/ansi-html-community/-/ansi-html-community-0.0.8.tgz#69fbc4d6ccbe383f9736934ae34c3f8290f1bf41" integrity sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw== @@ -3934,6 +4466,11 @@ aria-query@5.3.0: dependencies: dequal "^2.0.3" +aria-query@^5.0.0: + version "5.3.2" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.2.tgz#93f81a43480e33a338f19163a3d10a50c01dcd59" + integrity sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw== + array-buffer-byte-length@^1.0.1, array-buffer-byte-length@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz#384d12a37295aec3769ab022ad323a18a51ccf8b" @@ -4036,6 +4573,18 @@ asn1js@^3.0.6: pvutils "^1.1.5" tslib "^2.8.1" +assertion-error@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-2.0.1.tgz#f641a196b335690b1070bf00b6e7593fec190bf7" + integrity sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA== + +ast-types@^0.16.1: + version "0.16.1" + resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.16.1.tgz#7a9da1617c9081bc121faafe91711b4c8bb81da2" + integrity sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg== + dependencies: + tslib "^2.0.1" + astral-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" @@ -4506,6 +5055,22 @@ caniuse-lite@^1.0.30001782: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz#238887ddf5fcfc8c36d872394d0a78a517312a72" integrity sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA== +case-sensitive-paths-webpack-plugin@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz#db64066c6422eed2e08cc14b986ca43796dbc6d4" + integrity sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw== + +chai@^5.2.0: + version "5.3.3" + resolved "https://registry.yarnpkg.com/chai/-/chai-5.3.3.tgz#dd3da955e270916a4bd3f625f4b919996ada7e06" + integrity sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw== + dependencies: + assertion-error "^2.0.1" + check-error "^2.1.1" + deep-eql "^5.0.1" + loupe "^3.1.0" + pathval "^2.0.0" + chalk@^2.0.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" @@ -4528,6 +5093,11 @@ char-regex@^1.0.2: resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== +check-error@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/check-error/-/check-error-2.1.3.tgz#2427361117b70cca8dc89680ead32b157019caf5" + integrity sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA== + chokidar@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" @@ -4543,6 +5113,13 @@ chokidar@^3.6.0: optionalDependencies: fsevents "~2.3.2" +chokidar@^4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30" + integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA== + dependencies: + readdirp "^4.0.1" + chrome-launcher@^0.15.2: version "0.15.2" resolved "https://registry.yarnpkg.com/chrome-launcher/-/chrome-launcher-0.15.2.tgz#4e6404e32200095fdce7f6a1e1004f9bd36fa5da" @@ -4584,6 +5161,11 @@ ci-info@^4.2.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-4.4.0.tgz#7d54eff9f54b45b62401c26032696eb59c8bd18c" integrity sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg== +cjs-module-lexer@^1.2.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz#0f79731eb8cfe1ec72acd4066efac9d61991b00d" + integrity sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q== + cjs-module-lexer@^2.1.0: version "2.2.0" resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz#b3ca5101843389259ade7d88c77bd06ce55849ca" @@ -4731,6 +5313,11 @@ commander@^9.4.0, commander@^9.4.1: resolved "https://registry.yarnpkg.com/commander/-/commander-9.5.0.tgz#bc08d1eb5cedf7ccb797a96199d41c7bc3e60d30" integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ== +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== + compressible@~2.0.18: version "2.0.18" resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" @@ -4820,6 +5407,16 @@ core-util-is@~1.0.0: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== +cosmiconfig@^8.2.0: + version "8.3.6" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.3.6.tgz#060a2b871d66dba6c8538ea1118ba1ac16f5fae3" + integrity sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA== + dependencies: + import-fresh "^3.3.0" + js-yaml "^4.1.0" + parse-json "^5.2.0" + path-type "^4.0.0" + cosmiconfig@^9.0.0: version "9.0.1" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-9.0.1.tgz#df110631a8547b5d1a98915271986f06e3011379" @@ -4861,7 +5458,7 @@ css-in-js-utils@^3.1.0: dependencies: hyphenate-style-name "^1.0.3" -css-loader@7.1.4: +css-loader@7.1.4, css-loader@^7.1.2: version "7.1.4" resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-7.1.4.tgz#8f6bf9f8fc8cbef7d2ef6e80acc6545eaefa90b1" integrity sha512-vv3J9tlOl04WjiMvHQI/9tmIrCxVrj6PFbHemBB1iihpeRbi/I4h033eoFIhwxBBqLhI0KYFS7yvynBFhIZfTw== @@ -4891,6 +5488,11 @@ css-what@^6.0.1: resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.2.2.tgz#cdcc8f9b6977719fdfbd1de7aec24abf756b9dea" integrity sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA== +css.escape@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb" + integrity sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg== + cssesc@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" @@ -4990,17 +5592,27 @@ decode-uri-component@^0.4.1: resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.4.1.tgz#2ac4859663c704be22bf7db760a1494a49ab2cc5" integrity sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ== +dedent@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" + integrity sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA== + dedent@^1.6.0: version "1.7.2" resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.7.2.tgz#34e2264ab538301e27cf7b07bf2369c19baa8dd9" integrity sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA== +deep-eql@^5.0.1: + version "5.0.2" + resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-5.0.2.tgz#4b756d8d770a9257300825d52a2c2cff99c3a341" + integrity sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q== + deep-is@^0.1.3: version "0.1.4" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== -deepmerge@^4.3.0, deepmerge@^4.3.1: +deepmerge@^4.2.2, deepmerge@^4.3.0, deepmerge@^4.3.1: version "4.3.1" resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== @@ -5109,11 +5721,23 @@ doctrine@^2.1.0: dependencies: esutils "^2.0.2" +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + dom-accessibility-api@^0.5.9: version "0.5.16" resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453" integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg== +dom-accessibility-api@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz#993e925cc1d73f2c662e7d75dd5a5445259a8fd8" + integrity sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w== + dom-converter@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768" @@ -5244,6 +5868,15 @@ end-of-stream@^1.1.0: dependencies: once "^1.4.0" +endent@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/endent/-/endent-2.1.0.tgz#5aaba698fb569e5e18e69e1ff7a28ff35373cd88" + integrity sha512-r8VyPX7XL8U01Xgnb1CjZ3XV+z90cXIJ9JPE/R9SEC9vpw2P6CfsRPJmp20DppC5N7ZAMCmjYkJIa744Iyg96w== + dependencies: + dedent "^0.7.0" + fast-json-parse "^1.0.3" + objectorarray "^1.0.5" + enhanced-resolve@^5.22.0: version "5.22.0" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.22.0.tgz#43c5caad657c6fce58fc6142e5ca6fa8528ed460" @@ -5396,6 +6029,11 @@ es-iterator-helpers@^1.2.1: iterator.prototype "^1.1.5" math-intrinsics "^1.1.0" +es-module-lexer@^1.5.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz#9159601561880a85f2734560a9099b2c31e5372a" + integrity sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA== + es-module-lexer@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-2.1.0.tgz#1dfcbb5ea3bbfb63f28e1fc3676c3676d1c9624c" @@ -5434,6 +6072,38 @@ es-to-primitive@^1.3.0: is-date-object "^1.0.5" is-symbol "^1.0.4" +"esbuild@^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0": + version "0.27.7" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.27.7.tgz#bcadce22b2f3fd76f257e3a64f83a64986fea11f" + integrity sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w== + optionalDependencies: + "@esbuild/aix-ppc64" "0.27.7" + "@esbuild/android-arm" "0.27.7" + "@esbuild/android-arm64" "0.27.7" + "@esbuild/android-x64" "0.27.7" + "@esbuild/darwin-arm64" "0.27.7" + "@esbuild/darwin-x64" "0.27.7" + "@esbuild/freebsd-arm64" "0.27.7" + "@esbuild/freebsd-x64" "0.27.7" + "@esbuild/linux-arm" "0.27.7" + "@esbuild/linux-arm64" "0.27.7" + "@esbuild/linux-ia32" "0.27.7" + "@esbuild/linux-loong64" "0.27.7" + "@esbuild/linux-mips64el" "0.27.7" + "@esbuild/linux-ppc64" "0.27.7" + "@esbuild/linux-riscv64" "0.27.7" + "@esbuild/linux-s390x" "0.27.7" + "@esbuild/linux-x64" "0.27.7" + "@esbuild/netbsd-arm64" "0.27.7" + "@esbuild/netbsd-x64" "0.27.7" + "@esbuild/openbsd-arm64" "0.27.7" + "@esbuild/openbsd-x64" "0.27.7" + "@esbuild/openharmony-arm64" "0.27.7" + "@esbuild/sunos-x64" "0.27.7" + "@esbuild/win32-arm64" "0.27.7" + "@esbuild/win32-ia32" "0.27.7" + "@esbuild/win32-x64" "0.27.7" + escalade@^3.1.1, escalade@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" @@ -5645,7 +6315,7 @@ espree@^11.2.0: acorn-jsx "^5.3.2" eslint-visitor-keys "^5.0.1" -esprima@^4.0.0: +esprima@^4.0.0, esprima@~4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== @@ -5987,6 +6657,11 @@ fast-glob@^3.2.9, fast-glob@^3.3.2: merge2 "^1.3.0" micromatch "^4.0.8" +fast-json-parse@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/fast-json-parse/-/fast-json-parse-1.0.3.tgz#43e5c61ee4efa9265633046b770fb682a7577c4d" + integrity sha512-FRWsaZRWEJ1ESVNbDWmsAlqDk96gPQezzLghafp5J4GUKjbCz3OkAHuZs5TuPEtkbVQERysLp9xv6c24fBm8Aw== + fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" @@ -6146,6 +6821,15 @@ find-babel-config@^2.1.1: dependencies: json5 "^2.2.3" +find-cache-dir@^3.3.1: + version "3.3.2" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.2.tgz#b30c5b6eff0730731aea9bbd9dbecbd80256d64b" + integrity sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig== + dependencies: + commondir "^1.0.1" + make-dir "^3.0.2" + pkg-dir "^4.1.0" + find-up@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" @@ -6176,6 +6860,15 @@ find-yarn-workspace-root@^2.0.0: dependencies: micromatch "^4.0.2" +flat-cache@^3.0.4: + version "3.2.0" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee" + integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw== + dependencies: + flatted "^3.2.9" + keyv "^4.5.3" + rimraf "^3.0.2" + flat-cache@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-4.0.1.tgz#0ece39fcb14ee012f4b0410bd33dd9c1f011127c" @@ -6231,6 +6924,24 @@ foreground-child@^3.1.0: cross-spawn "^7.0.6" signal-exit "^4.0.1" +fork-ts-checker-webpack-plugin@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.1.0.tgz#433481c1c228c56af111172fcad7df79318c915a" + integrity sha512-mpafl89VFPJmhnJ1ssH+8wmM2b50n+Rew5x42NeI2U78aRWgtkEtGmctp7iT16UjquJTjorEmIfESj3DxdW84Q== + dependencies: + "@babel/code-frame" "^7.16.7" + chalk "^4.1.2" + chokidar "^4.0.1" + cosmiconfig "^8.2.0" + deepmerge "^4.2.2" + fs-extra "^10.0.0" + memfs "^3.4.1" + minimatch "^3.0.4" + node-abort-controller "^3.0.1" + schema-utils "^3.1.1" + semver "^7.3.5" + tapable "^2.2.1" + forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -6259,6 +6970,11 @@ fs-extra@^8.1.0: jsonfile "^4.0.0" universalify "^0.1.0" +fs-monkey@^1.0.4: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.1.0.tgz#632aa15a20e71828ed56b24303363fb1414e5997" + integrity sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw== + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -6419,7 +7135,7 @@ glob@^13.0.0, glob@^13.0.2: minipass "^7.1.3" path-scurry "^2.0.2" -glob@^7.1.4: +glob@^7.1.3, glob@^7.1.4: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -6622,7 +7338,7 @@ html-minifier-terser@^6.0.2: relateurl "^0.2.7" terser "^5.10.0" -html-webpack-plugin@5.6.7: +html-webpack-plugin@5.6.7, html-webpack-plugin@^5.5.0: version "5.6.7" resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.6.7.tgz#429bab4e12abf3c07e1c608886608e2df2c06b11" integrity sha512-md+vXtdCAe60s1k6AU3dUyMJnDxUyQAwfwPKoLisvgUF1IXjtlLsk2se54+qfL9Mdm26bbwvjJybpNx48NKRLw== @@ -6800,6 +7516,11 @@ imurmurhash@^0.1.4: resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -7743,7 +8464,7 @@ json-stable-stringify@^1.0.2: jsonify "^0.0.1" object-keys "^1.1.1" -json5@^2.1.2, json5@^2.2.3: +json5@^2.1.2, json5@^2.2.2, json5@^2.2.3: version "2.2.3" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== @@ -7784,7 +8505,7 @@ junk@^4.0.1: resolved "https://registry.yarnpkg.com/junk/-/junk-4.0.1.tgz#7ee31f876388c05177fe36529ee714b07b50fbed" integrity sha512-Qush0uP+G8ZScpGMZvHUiRfI0YBWuB3gVBYlI0v0vvOJt5FLicco+IkP0a50LqTTQhmts/m6tP5SWE+USyIvcQ== -keyv@^4.5.4: +keyv@^4.5.3, keyv@^4.5.4: version "4.5.4" resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== @@ -8013,6 +8734,11 @@ lottie-web@5.13.0: resolved "https://registry.yarnpkg.com/lottie-web/-/lottie-web-5.13.0.tgz#441d3df217cc8ba302338c3f168e1a3af0f221d3" integrity sha512-+gfBXl6sxXMPe8tKQm7qzLnUy5DUPJPKIyRHwtpCpyUEYjHYRJC/5gjUvdkuO2c3JllrPtHXH5UJJK8LRYl5yQ== +loupe@^3.1.0, loupe@^3.1.4: + version "3.2.1" + resolved "https://registry.yarnpkg.com/loupe/-/loupe-3.2.1.tgz#0095cf56dc5b7a9a7c08ff5b1a8796ec8ad17e76" + integrity sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ== + lower-case@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28" @@ -8042,6 +8768,20 @@ lz-string@^1.5.0: resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941" integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ== +magic-string@^0.30.5: + version "0.30.21" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.21.tgz#56763ec09a0fa8091df27879fd94d19078c00d91" + integrity sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.5" + +make-dir@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" + integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== + dependencies: + semver "^6.0.0" + make-dir@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-4.0.0.tgz#c3c2307a771277cd9638305f915c29ae741b614e" @@ -8076,6 +8816,13 @@ media-typer@^1.1.0: resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-1.1.0.tgz#6ab74b8f2d3320f2064b2a87a38e7931ff3a5561" integrity sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw== +memfs@^3.4.1, memfs@^3.4.12: + version "3.6.0" + resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.6.0.tgz#d7a2110f86f79dd950a8b6df6d57bc984aa185f6" + integrity sha512-EGowvkkgbMcIChjMTMkESFDbZeSh8xZ7kNSF0hAiAN4Jh6jgHCRS0Ga/+C8y6Au+oqpezRHCfPsmJ2+DwAgiwQ== + dependencies: + fs-monkey "^1.0.4" + memfs@^4.43.1: version "4.57.2" resolved "https://registry.yarnpkg.com/memfs/-/memfs-4.57.2.tgz#5f74e977c9a14681ea10d427b3ce5d7db5f817e7" @@ -8343,6 +9090,13 @@ mime-db@1.52.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.54.0.tgz#cddb3ee4f9c64530dff640236661d42cb6a314f5" integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ== +mime-types@^2.1.31, mime-types@~2.1.24, mime-types@~2.1.34, mime-types@~2.1.35: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + mime-types@^3.0.0, mime-types@^3.0.1: version "3.0.2" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-3.0.2.tgz#39002d4182575d5af036ffa118100f2524b2e2ab" @@ -8350,13 +9104,6 @@ mime-types@^3.0.0, mime-types@^3.0.1: dependencies: mime-db "^1.54.0" -mime-types@~2.1.24, mime-types@~2.1.34, mime-types@~2.1.35: - version "2.1.35" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" - integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== - dependencies: - mime-db "1.52.0" - mime@1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" @@ -8377,6 +9124,11 @@ mimic-fn@^2.1.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== +min-indent@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" + integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== + minimalistic-assert@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" @@ -8527,6 +9279,11 @@ nocache@^3.0.1: resolved "https://registry.yarnpkg.com/nocache/-/nocache-3.0.4.tgz#5b37a56ec6e09fc7d401dceaed2eab40c8bfdf79" integrity sha512-WDD0bdg9mbq6F4mRxEYcPWwfA1vxd0mrvKOyxI7Xj/atfRHVeutzuWByG//jfm4uPzp0y4Kj051EORCBSQMycw== +node-abort-controller@^3.0.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/node-abort-controller/-/node-abort-controller-3.1.1.tgz#a94377e964a9a37ac3976d848cb5c765833b8548" + integrity sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ== + node-exports-info@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/node-exports-info/-/node-exports-info-1.6.0.tgz#1aedafb01a966059c9a5e791a94a94d93f5c2a13" @@ -8682,6 +9439,11 @@ object.values@^1.1.6, object.values@^1.2.1: define-properties "^1.2.1" es-object-atoms "^1.0.0" +objectorarray@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/objectorarray/-/objectorarray-1.0.5.tgz#2c05248bbefabd8f43ad13b41085951aac5e68a5" + integrity sha512-eJJDYkhJFFbBBAxeh8xW+weHlkI28n2ZdQV/J/DNfWfSKlGEf2xcfAbZTv3riEXHAhL9SVOTs2pRmXiSTf78xg== + obuf@^1.0.0, obuf@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" @@ -8727,7 +9489,7 @@ onetime@^5.1.0, onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" -open@^10.0.3: +open@^10.0.3, open@^10.2.0: version "10.2.0" resolved "https://registry.yarnpkg.com/open/-/open-10.2.0.tgz#b9d855be007620e80b6fb05fac98141fe62db73c" integrity sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA== @@ -8800,6 +9562,59 @@ own-keys@^1.0.1: object-keys "^1.1.1" safe-push-apply "^1.0.0" +oxc-parser@^0.127.0: + version "0.127.0" + resolved "https://registry.yarnpkg.com/oxc-parser/-/oxc-parser-0.127.0.tgz#bb14600f5c59fb6b1fbac0ab6ff2cd3495a6df1d" + integrity sha512-bkgD4qHlN7WxLdX8bLXdaU54TtQtAIg/ZBAfm0aje/mo3MRDo3P0hZSgr4U7O3xfX+fQmR5AP04JS/TGcZLcFA== + dependencies: + "@oxc-project/types" "^0.127.0" + optionalDependencies: + "@oxc-parser/binding-android-arm-eabi" "0.127.0" + "@oxc-parser/binding-android-arm64" "0.127.0" + "@oxc-parser/binding-darwin-arm64" "0.127.0" + "@oxc-parser/binding-darwin-x64" "0.127.0" + "@oxc-parser/binding-freebsd-x64" "0.127.0" + "@oxc-parser/binding-linux-arm-gnueabihf" "0.127.0" + "@oxc-parser/binding-linux-arm-musleabihf" "0.127.0" + "@oxc-parser/binding-linux-arm64-gnu" "0.127.0" + "@oxc-parser/binding-linux-arm64-musl" "0.127.0" + "@oxc-parser/binding-linux-ppc64-gnu" "0.127.0" + "@oxc-parser/binding-linux-riscv64-gnu" "0.127.0" + "@oxc-parser/binding-linux-riscv64-musl" "0.127.0" + "@oxc-parser/binding-linux-s390x-gnu" "0.127.0" + "@oxc-parser/binding-linux-x64-gnu" "0.127.0" + "@oxc-parser/binding-linux-x64-musl" "0.127.0" + "@oxc-parser/binding-openharmony-arm64" "0.127.0" + "@oxc-parser/binding-wasm32-wasi" "0.127.0" + "@oxc-parser/binding-win32-arm64-msvc" "0.127.0" + "@oxc-parser/binding-win32-ia32-msvc" "0.127.0" + "@oxc-parser/binding-win32-x64-msvc" "0.127.0" + +oxc-resolver@^11.19.1: + version "11.20.0" + resolved "https://registry.yarnpkg.com/oxc-resolver/-/oxc-resolver-11.20.0.tgz#2b2a7a76de753b34be1d6031da1604c8cb580b08" + integrity sha512-CblytBiV/a/ZXY34dsVU2NxhIOxMXst8CvDCtyBelVITgd7PLrKzbEbA6oKLdPjvDKDzCiW48qzmzZ+mYaqn+g== + optionalDependencies: + "@oxc-resolver/binding-android-arm-eabi" "11.20.0" + "@oxc-resolver/binding-android-arm64" "11.20.0" + "@oxc-resolver/binding-darwin-arm64" "11.20.0" + "@oxc-resolver/binding-darwin-x64" "11.20.0" + "@oxc-resolver/binding-freebsd-x64" "11.20.0" + "@oxc-resolver/binding-linux-arm-gnueabihf" "11.20.0" + "@oxc-resolver/binding-linux-arm-musleabihf" "11.20.0" + "@oxc-resolver/binding-linux-arm64-gnu" "11.20.0" + "@oxc-resolver/binding-linux-arm64-musl" "11.20.0" + "@oxc-resolver/binding-linux-ppc64-gnu" "11.20.0" + "@oxc-resolver/binding-linux-riscv64-gnu" "11.20.0" + "@oxc-resolver/binding-linux-riscv64-musl" "11.20.0" + "@oxc-resolver/binding-linux-s390x-gnu" "11.20.0" + "@oxc-resolver/binding-linux-x64-gnu" "11.20.0" + "@oxc-resolver/binding-linux-x64-musl" "11.20.0" + "@oxc-resolver/binding-openharmony-arm64" "11.20.0" + "@oxc-resolver/binding-wasm32-wasi" "11.20.0" + "@oxc-resolver/binding-win32-arm64-msvc" "11.20.0" + "@oxc-resolver/binding-win32-x64-msvc" "11.20.0" + p-limit@^2.0.0, p-limit@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" @@ -8989,6 +9804,11 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== +pathval@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pathval/-/pathval-2.0.1.tgz#8855c5a2899af072d6ac05d11e46045ad0dc605d" + integrity sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ== + pe-library@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/pe-library/-/pe-library-1.0.1.tgz#02735430885a622576a53cd8827658b7d2fada0e" @@ -9019,7 +9839,7 @@ pirates@^4.0.7: resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.7.tgz#643b4a18c4257c8a65104b73f3049ce9a0a15e22" integrity sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA== -pkg-dir@^4.2.0: +pkg-dir@^4.1.0, pkg-dir@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== @@ -9329,6 +10149,43 @@ react-devtools-core@^6.1.5: shell-quote "^1.6.1" ws "^7" +react-docgen-typescript@^2.2.2: + version "2.4.0" + resolved "https://registry.yarnpkg.com/react-docgen-typescript/-/react-docgen-typescript-2.4.0.tgz#033428b4a6a639d050ac8baf2a5195c596521713" + integrity sha512-ZtAp5XTO5HRzQctjPU0ybY0RRCQO19X/8fxn3w7y2VVTUbGHDKULPTL4ky3vB05euSgG5NpALhEhDPvQ56wvXg== + +react-docgen@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/react-docgen/-/react-docgen-7.1.1.tgz#a7a8e6b923a945acf0b7325a889ddd74fec74a63" + integrity sha512-hlSJDQ2synMPKFZOsKo9Hi8WWZTC7POR8EmWvTSjow+VDgKzkmjQvFm2fk0tmRw+f0vTOIYKlarR0iL4996pdg== + dependencies: + "@babel/core" "^7.18.9" + "@babel/traverse" "^7.18.9" + "@babel/types" "^7.18.9" + "@types/babel__core" "^7.18.0" + "@types/babel__traverse" "^7.18.0" + "@types/doctrine" "^0.0.9" + "@types/resolve" "^1.20.2" + doctrine "^3.0.0" + resolve "^1.22.1" + strip-indent "^4.0.0" + +react-docgen@^8.0.2: + version "8.0.3" + resolved "https://registry.yarnpkg.com/react-docgen/-/react-docgen-8.0.3.tgz#164e5b29f8115f23d69b09966ec835d92bcdc075" + integrity sha512-aEZ9qP+/M+58x2qgfSFEWH1BxLyHe5+qkLNJOZQb5iGS017jpbRnoKhNRrXPeA6RfBrZO5wZrT9DMC1UqE1f1w== + dependencies: + "@babel/core" "^7.28.0" + "@babel/traverse" "^7.28.0" + "@babel/types" "^7.28.2" + "@types/babel__core" "^7.20.5" + "@types/babel__traverse" "^7.20.7" + "@types/doctrine" "^0.0.9" + "@types/resolve" "^1.20.2" + doctrine "^3.0.0" + resolve "^1.22.1" + strip-indent "^4.0.0" + react-dom@19.2.3: version "19.2.3" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.3.tgz#f0b61d7e5c4a86773889fcc1853af3ed5f215b17" @@ -9529,6 +10386,11 @@ readable-stream@^3.0.6, readable-stream@^3.4.0: string_decoder "^1.1.1" util-deprecate "^1.0.1" +readdirp@^4.0.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d" + integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg== + readdirp@~3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" @@ -9536,6 +10398,17 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +recast@^0.23.5: + version "0.23.11" + resolved "https://registry.yarnpkg.com/recast/-/recast-0.23.11.tgz#8885570bb28cf773ba1dc600da7f502f7883f73f" + integrity sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA== + dependencies: + ast-types "^0.16.1" + esprima "~4.0.0" + source-map "~0.6.1" + tiny-invariant "^1.3.3" + tslib "^2.0.1" + rechoir@^0.8.0: version "0.8.0" resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.8.0.tgz#49f866e0d32146142da3ad8f0eff352b3215ff22" @@ -9543,6 +10416,14 @@ rechoir@^0.8.0: dependencies: resolve "^1.20.0" +redent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" + integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg== + dependencies: + indent-string "^4.0.0" + strip-indent "^3.0.0" + reflect-metadata@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.2.2.tgz#400c845b6cba87a21f2c65c4aeb158f4fa4d9c5b" @@ -9685,7 +10566,7 @@ resolve-workspace-root@^2.0.0: resolved "https://registry.yarnpkg.com/resolve-workspace-root/-/resolve-workspace-root-2.0.1.tgz#9cbbf8321ebccaaf0e4ffea5274aa26b611ccd62" integrity sha512-nR23LHAvaI6aHtMg6RWoaHpdR4D881Nydkzi2CixINyg9T00KgaJdJI6Vwty+Ps8WLxZHuxsS0BseWjxSA4C+w== -resolve@^1.20.0, resolve@^1.22.11, resolve@^1.22.8: +resolve@^1.20.0, resolve@^1.22.1, resolve@^1.22.11, resolve@^1.22.8: version "1.22.12" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.12.tgz#f5b2a680897c69c238a13cd16b15671f8b73549f" integrity sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA== @@ -9738,6 +10619,13 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.1.0.tgz#0fe13b9522e1473f51b558ee796e08f11f9b489f" integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw== +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + rrweb-cssom@^0.8.0: version "0.8.0" resolved "https://registry.yarnpkg.com/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz#3021d1b4352fbf3b614aaeed0bc0d5739abe0bc2" @@ -9820,7 +10708,7 @@ scheduler@0.27.0, scheduler@^0.27.0: resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.27.0.tgz#0c4ef82d67d1e5c1e359e8fc76d3a87f045fe5bd" integrity sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q== -schema-utils@^3.0.0: +schema-utils@^3.0.0, schema-utils@^3.1.1: version "3.3.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe" integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== @@ -9852,7 +10740,7 @@ selfsigned@^5.5.0: "@peculiar/x509" "^1.14.2" pkijs "^3.3.3" -semver@^6.3.1: +semver@^6.0.0, semver@^6.3.1: version "6.3.1" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== @@ -10107,7 +10995,7 @@ source-map@^0.5.6: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== -source-map@^0.6.0, source-map@~0.6.0: +source-map@^0.6.0, source-map@~0.6.0, source-map@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== @@ -10187,6 +11075,27 @@ stop-iteration-iterator@^1.1.0: es-errors "^1.3.0" internal-slot "^1.1.0" +storybook@10.4.1: + version "10.4.1" + resolved "https://registry.yarnpkg.com/storybook/-/storybook-10.4.1.tgz#5cd7d11fd16bb7c0ae61b883d65cebe909290820" + integrity sha512-V1Zd2e+gBFufqAQVZ1JR8KLqALsEZ3JYSBnWwQbKa6zCfWWanR6AFMyuOkLt2gZOgGp3h2Riuz88pGNVTQSG0A== + dependencies: + "@storybook/global" "^5.0.0" + "@storybook/icons" "^2.0.2" + "@testing-library/jest-dom" "^6.9.1" + "@testing-library/user-event" "^14.6.1" + "@vitest/expect" "3.2.4" + "@vitest/spy" "3.2.4" + "@webcontainer/env" "^1.1.1" + esbuild "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0" + open "^10.2.0" + oxc-parser "^0.127.0" + oxc-resolver "^11.19.1" + recast "^0.23.5" + semver "^7.7.3" + use-sync-external-store "^1.5.0" + ws "^8.18.0" + stream-buffers@2.2.x: version "2.2.0" resolved "https://registry.yarnpkg.com/stream-buffers/-/stream-buffers-2.2.0.tgz#91d5f5130d1cef96dcfa7f726945188741d09ee4" @@ -10338,6 +11247,11 @@ strip-ansi@^7.0.1: dependencies: ansi-regex "^6.2.2" +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== + strip-bom@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" @@ -10348,6 +11262,18 @@ strip-final-newline@^2.0.0: resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== +strip-indent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" + integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ== + dependencies: + min-indent "^1.0.0" + +strip-indent@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-4.1.1.tgz#aba13de189d4ad9a17f6050e76554ac27585c7af" + integrity sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA== + strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" @@ -10363,7 +11289,7 @@ structured-headers@^0.4.1: resolved "https://registry.yarnpkg.com/structured-headers/-/structured-headers-0.4.1.tgz#77abd9410622c6926261c09b9d16cf10592694d1" integrity sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg== -style-loader@4.0.0: +style-loader@4.0.0, style-loader@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-4.0.0.tgz#0ea96e468f43c69600011e0589cb05c44f3b17a5" integrity sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA== @@ -10431,7 +11357,7 @@ tagged-tag@^1.0.0: resolved "https://registry.yarnpkg.com/tagged-tag/-/tagged-tag-1.0.0.tgz#a0b5917c2864cba54841495abfa3f6b13edcf4d6" integrity sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng== -tapable@^2.0.0, tapable@^2.3.0, tapable@^2.3.3: +tapable@^2.0.0, tapable@^2.2.1, tapable@^2.3.0, tapable@^2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.3.3.tgz#5da7c9992c46038221267985ab28421a8879f160" integrity sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A== @@ -10444,7 +11370,7 @@ terminal-link@^2.1.1: ansi-escapes "^4.2.1" supports-hyperlinks "^2.0.0" -terser-webpack-plugin@5.6.1: +terser-webpack-plugin@5.6.1, terser-webpack-plugin@^5.3.17: version "5.6.1" resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.6.1.tgz#47bc41bd8b8fab8383b62ec763b7394829097e7b" integrity sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ== @@ -10498,6 +11424,11 @@ thunky@^1.0.2: resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== +tiny-invariant@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127" + integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg== + tinyglobby@^0.2.15: version "0.2.16" resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.16.tgz#1c3b7eb953fce42b226bc5a1ee06428281aff3d6" @@ -10506,6 +11437,16 @@ tinyglobby@^0.2.15: fdir "^6.5.0" picomatch "^4.0.4" +tinyrainbow@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/tinyrainbow/-/tinyrainbow-2.0.0.tgz#9509b2162436315e80e3eee0fcce4474d2444294" + integrity sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw== + +tinyspy@^4.0.3: + version "4.0.4" + resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-4.0.4.tgz#d77a002fb53a88aa1429b419c1c92492e0c81f78" + integrity sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q== + tldts-core@^6.1.86: version "6.1.86" resolved "https://registry.yarnpkg.com/tldts-core/-/tldts-core-6.1.86.tgz#a93e6ed9d505cb54c542ce43feb14c73913265d8" @@ -10579,12 +11520,26 @@ ts-api-utils@^2.5.0: resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.5.0.tgz#4acd4a155e22734990a5ed1fe9e97f113bcb37c1" integrity sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA== +ts-dedent@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/ts-dedent/-/ts-dedent-2.2.0.tgz#39e4bd297cd036292ae2394eb3412be63f563bb5" + integrity sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ== + +tsconfig-paths@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz#ef78e19039133446d244beac0fd6a1632e2d107c" + integrity sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg== + dependencies: + json5 "^2.2.2" + minimist "^1.2.6" + strip-bom "^3.0.0" + tslib@^1.9.3: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.0, tslib@^2.0.3, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.8.1: +tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.8.1: version "2.8.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== @@ -10944,6 +11899,17 @@ webpack-cli@7.0.3: rechoir "^0.8.0" webpack-merge "^6.0.1" +webpack-dev-middleware@^6.1.2: + version "6.1.3" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-6.1.3.tgz#79f4103f8c898564c9e96c3a9c2422de50f249bc" + integrity sha512-A4ChP0Qj8oGociTs6UdlRUGANIGrCDL3y+pmQMc+dSsraXHCatFpmMey4mYELA+juqwUqwQsUgJJISXl1KWmiw== + dependencies: + colorette "^2.0.10" + memfs "^3.4.12" + mime-types "^2.1.31" + range-parser "^1.2.1" + schema-utils "^4.0.0" + webpack-dev-middleware@^7.4.2: version "7.4.5" resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-7.4.5.tgz#d4e8720aa29cb03bc158084a94edb4594e3b7ac0" @@ -10990,6 +11956,15 @@ webpack-dev-server@5.2.4: webpack-dev-middleware "^7.4.2" ws "^8.18.0" +webpack-hot-middleware@^2.25.1: + version "2.26.1" + resolved "https://registry.yarnpkg.com/webpack-hot-middleware/-/webpack-hot-middleware-2.26.1.tgz#87214f1e3f9f3acab9271fef9e6ed7b637d719c0" + integrity sha512-khZGfAeJx6I8K9zKohEWWYN6KDlVw2DHownoe+6Vtwj1LP9WFgegXnVMSkZ/dBEBtXFwrkkydsaPFlB7f8wU2A== + dependencies: + ansi-html-community "0.0.8" + html-entities "^2.1.0" + strip-ansi "^6.0.0" + webpack-merge@6.0.1, webpack-merge@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-6.0.1.tgz#50c776868e080574725abc5869bd6e4ef0a16c6a" @@ -11004,7 +11979,12 @@ webpack-sources@^3.5.0: resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.5.0.tgz#87bf7f5801a4e985b1f1c92b64b9620a02f76d08" integrity sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ== -webpack@5.107.2: +webpack-virtual-modules@^0.6.0: + version "0.6.2" + resolved "https://registry.yarnpkg.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz#057faa9065c8acf48f24cb57ac0e77739ab9a7e8" + integrity sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ== + +webpack@5, webpack@5.107.2: version "5.107.2" resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.107.2.tgz#dea14dcb177b46b29de15f952f7303691ee2b596" integrity sha512-v7RhXaJbpMlV0D7hC7lb2EbnxkoeUqf9qhKr6lozx3Q48pmFrqqNRmZFUEGmi7pSwm6fCQ2H1IjvCkHqdpVdjQ== From 03335d160017f067b71d7fa5e549112a1e95488a Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Mon, 1 Jun 2026 16:45:12 -0400 Subject: [PATCH 02/12] Add storybook screenshots to desktop e2e report and baseline - generate-electron-report: parse storybook-desktop/ PNGs, render as separate section with slider diff vs storybook-prev/ baseline - generate-ios-report: add .section-hdr CSS for full-width grid divider - save-baseline now also saves storybook screenshots to storybook-prev/ - package.json: add storybook:screenshot script --- shared/package.json | 1 + shared/tests/e2e/generate-electron-report.mts | 97 +++++++++++++++++-- shared/tests/e2e/generate-ios-report.mts | 1 + 3 files changed, 91 insertions(+), 8 deletions(-) diff --git a/shared/package.json b/shared/package.json index e1527728c6d9..ce1c25de2583 100644 --- a/shared/package.json +++ b/shared/package.json @@ -65,6 +65,7 @@ "rn:start:log": "npx expo start --clear 2>&1 | tee /tmp/metro.log", "rn:start:legacy": "./react-native/packageAndBuild.sh", "storybook": "storybook dev -p 6006 --config-dir .storybook", + "storybook:screenshot": "node --experimental-strip-types scripts/screenshot-storybook.mts", "sync:kb-modules": "yarn run _helper sync-local-rnmodules", "test:e2e:desktop": "playwright test --config tests/e2e/electron/playwright.config.ts", "test:e2e:desktop:branch": "playwright test --config tests/e2e/electron/playwright.config.ts tests/e2e/electron/flows/team-member.test.ts && yarn test:e2e:desktop:report", diff --git a/shared/tests/e2e/generate-electron-report.mts b/shared/tests/e2e/generate-electron-report.mts index 82bece2800a7..4dca7b162270 100644 --- a/shared/tests/e2e/generate-electron-report.mts +++ b/shared/tests/e2e/generate-electron-report.mts @@ -10,6 +10,8 @@ const {PNG} = require('pngjs') as {PNG: {sync: {read: (buf: Buffer) => {data: Bu const resultsPath = 'tests/results/report/results.json' const debugDir = 'tests/results/electron-debug' const prevDir = 'tests/results/electron-prev' +const storybookDir = 'tests/results/storybook-desktop' +const storybookPrevDir = 'tests/results/storybook-prev' const outputPath = 'tests/results/electron-report.html' type Attachment = {name: string; contentType: string; body?: string; path?: string} @@ -21,6 +23,14 @@ type Report = {suites: PlaywrightSuite[]} type DiffResult = {pct: number; changed: number; total: number} +type StorybookCase = { + relPath: string + label: string + screenshotPath: string + prevScreenshotPath: string | null + diff: DiffResult | null +} + type TestCase = { key: string label: string @@ -109,6 +119,41 @@ function parseReport(report: Report): TestCase[] { return cases } +function parseStorybookScreenshots(): StorybookCase[] { + if (!fs.existsSync(storybookDir)) return [] + const cases: StorybookCase[] = [] + function walk(dir: string, rel: string) { + for (const entry of fs.readdirSync(dir, {withFileTypes: true})) { + const fullPath = path.join(dir, entry.name) + const relPath = rel ? `${rel}/${entry.name}` : entry.name + if (entry.isDirectory()) { + walk(fullPath, relPath) + } else if (entry.name.endsWith('.png')) { + const label = relPath.replace('.png', '').replace('/', ' / ').replace(/-/g, ' ') + const prevPath = path.join(storybookPrevDir, relPath) + const prevScreenshotPath = fs.existsSync(prevPath) ? prevPath : null + const diff = prevScreenshotPath ? computeDiff(fullPath, prevScreenshotPath) : null + cases.push({relPath, label, screenshotPath: fullPath, prevScreenshotPath, diff}) + } + } + } + walk(storybookDir, '') + return cases.sort((a, b) => a.relPath.localeCompare(b.relPath)) +} + +function saveStorybookBaseline(cases: StorybookCase[]) { + fs.mkdirSync(storybookPrevDir, {recursive: true}) + let saved = 0 + for (const c of cases) { + if (!fs.existsSync(c.screenshotPath)) continue + const destDir = path.dirname(path.join(storybookPrevDir, c.relPath)) + fs.mkdirSync(destDir, {recursive: true}) + fs.copyFileSync(c.screenshotPath, path.join(storybookPrevDir, c.relPath)) + saved++ + } + console.log(`Storybook baseline saved: ${saved} screenshots to ${storybookPrevDir}/`) +} + function formatDuration(ms: number): string { return ms < 1000 ? `${ms}ms` : `${(ms / 1000).toFixed(1)}s` } @@ -117,20 +162,21 @@ function escapeHtml(s: string): string { return s.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"') } -function buildHtml(cases: TestCase[], timestamp: string): string { +function buildHtml(cases: TestCase[], storybookCases: StorybookCase[], timestamp: string): string { const totalPassed = cases.filter(c => c.passed).length const totalFailed = cases.length - totalPassed const allPassed = totalFailed === 0 - const hasDiff = cases.some(c => c.diff !== null) + const hasDiff = cases.some(c => c.diff !== null) || storybookCases.some(c => c.diff !== null) + + const rel = (p: string) => path.relative(path.dirname(outputPath), p) - const cards = cases.map((c, i) => { + const e2eCards = cases.map((c, i) => { const badge = c.passed ? 'PASS' : 'FAIL' const error = c.errorMessage ? `
${escapeHtml(c.errorMessage)}
` : '' const deltaBadge = c.diff ? `Δ ${c.diff.pct.toFixed(1)}%` : '' - const rel = (p: string) => path.relative(path.dirname(outputPath), p) let visual: string if (c.screenshotPath && c.prevScreenshotPath) { visual = `
@@ -152,7 +198,38 @@ function buildHtml(cases: TestCase[], timestamp: string): string {
` }).join('\n') - return buildPage('Keybase Electron E2E Tests', allPassed, totalPassed, totalFailed, cases.length, hasDiff, timestamp, cards) + const sbOffset = cases.length + const sbCards = storybookCases.map((c, i) => { + const deltaBadge = c.diff + ? `Δ ${c.diff.pct.toFixed(1)}%` + : '' + + let visual: string + if (c.prevScreenshotPath) { + visual = `
+ current + baseline +
+
BASELINE
+
NOW
+
` + } else { + visual = `
${escapeHtml(c.label)}
` + } + + return `
+
${deltaBadge}${escapeHtml(c.label)}
+ ${visual} +
` + }).join('\n') + + const storybookSection = storybookCases.length > 0 + ? `
Storybook · ${storybookCases.length} stories
\n${sbCards}` + : '' + + const allCards = [e2eCards, storybookSection].filter(Boolean).join('\n') + + return buildPage('Keybase Electron E2E Tests', allPassed, totalPassed, totalFailed, cases.length, hasDiff, timestamp, allCards) } function saveBaseline(cases: TestCase[]) { @@ -183,20 +260,24 @@ function main() { process.exit(1) } + const storybookCases = parseStorybookScreenshots() + if (isSaveBaseline) { saveBaseline(cases) + saveStorybookBaseline(storybookCases) return } const timestamp = new Date().toLocaleString() - const html = buildHtml(cases, timestamp) + const html = buildHtml(cases, storybookCases, timestamp) const outDir = path.dirname(outputPath) if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, {recursive: true}) fs.writeFileSync(outputPath, html) - const withDiff = cases.filter(c => c.diff !== null).length + const withDiff = [...cases, ...storybookCases].filter(c => c.diff !== null).length const diffNote = withDiff > 0 ? `, ${withDiff} vs baseline` : '' - console.log(`Report written to ${outputPath} (${cases.filter(c => c.passed).length}/${cases.length} passed${diffNote})`) + const sbNote = storybookCases.length > 0 ? `, ${storybookCases.length} storybook stories` : '' + console.log(`Report written to ${outputPath} (${cases.filter(c => c.passed).length}/${cases.length} passed${diffNote}${sbNote})`) } main() diff --git a/shared/tests/e2e/generate-ios-report.mts b/shared/tests/e2e/generate-ios-report.mts index cb31786dc9b1..452499a8342b 100644 --- a/shared/tests/e2e/generate-ios-report.mts +++ b/shared/tests/e2e/generate-ios-report.mts @@ -244,6 +244,7 @@ h1{font-size:20px;font-weight:600} .grip{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:#fff;border-radius:50%;width:26px;height:26px;display:flex;align-items:center;justify-content:center;font-size:11px;box-shadow:0 1px 4px rgba(0,0,0,.4)} .lbl{position:absolute;bottom:8px;font-size:10px;font-weight:700;letter-spacing:.05em;padding:2px 7px;border-radius:3px;background:rgba(0,0,0,.55);color:#fff;pointer-events:none} .lbl-l{left:8px}.lbl-r{right:8px} +.section-hdr{grid-column:1/-1;font-size:15px;font-weight:600;padding:10px 0 4px;border-bottom:2px solid #ddd;margin-top:8px;color:#444} .expand-btn{position:absolute;bottom:10px;right:10px;background:rgba(0,0,0,.55);color:#fff;border:none;border-radius:5px;padding:5px 8px;font-size:15px;line-height:1;cursor:pointer;opacity:0;transition:opacity .15s;z-index:5} .compare:hover .expand-btn,.solo-wrap:hover .expand-btn{opacity:1} .overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.88);z-index:1000;align-items:center;justify-content:center} From 0ecdc73f33c3cae64e75b7b23fd1759e063286b7 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Tue, 2 Jun 2026 08:37:39 -0400 Subject: [PATCH 03/12] storybook --- plans/storybook.md | 38 +++--- shared/.storybook/main.ts | 8 +- shared/.storybook/preview.ts | 3 + shared/chat/avatars.stories.tsx | 57 ++++++++ .../inbox/row/big-teams-label.stories.tsx | 11 ++ .../chat/inbox/row/teams-divider.stories.tsx | 31 +++++ shared/common-adapters/badge.stories.tsx | 29 +++++ shared/common-adapters/banner.stories.tsx | 62 +++++++++ shared/common-adapters/checkbox.stories.tsx | 41 ++++++ shared/common-adapters/icon.stories.tsx | 58 +++++++++ .../loading-state-view.stories.tsx | 34 +++++ shared/common-adapters/meta.stories.tsx | 35 +++++ .../common-adapters/progress-bar.stories.tsx | 30 +++++ .../common-adapters/radio-button.stories.tsx | 33 +++++ .../common-adapters/search-filter.stories.tsx | 54 ++++++++ .../section-divider.stories.tsx | 42 ++++++ .../common-adapters/switch-toggle.stories.tsx | 26 ++++ shared/common-adapters/tabs.stories.tsx | 42 ++++++ shared/common-adapters/text.stories.tsx | 74 +++++++++++ .../waiting-button.stories.tsx | 34 +++++ shared/crypto/input.stories.tsx | 67 ++++++++++ shared/crypto/output.stories.tsx | 46 +++++++ shared/crypto/recipients.stories.tsx | 36 ++++++ shared/crypto/sub-nav/left-nav.stories.tsx | 30 +++++ shared/crypto/sub-nav/nav-row.stories.tsx | 73 +++++++++++ .../common/comma-separated-name.stories.tsx | 30 +++++ shared/fs/footer/upload.stories.tsx | 64 +++++++++ shared/fs/simple-screens/loading.stories.tsx | 15 +++ shared/git/nav-header.stories.tsx | 11 ++ shared/login/loading.stories.tsx | 41 ++++++ .../login/recover-password/error.stories.tsx | 21 +++ .../explain-device.stories.tsx | 34 +++++ .../recover-password/paper-key.stories.tsx | 24 ++++ .../prompt-reset-account.stories.tsx | 31 +++++ shared/login/reset/waiting.stories.tsx | 26 ++++ shared/login/signup/error.stories.tsx | 28 ++++ shared/login/user-card/index.stories.tsx | 55 ++++++++ shared/menubar/out-of-date.stories.tsx | 54 ++++++++ shared/people/announcement.stories.tsx | 69 ++++++++++ shared/people/follow-notification.stories.tsx | 93 +++++++++++++ shared/people/follow-suggestions.stories.tsx | 64 +++++++++ shared/people/item.stories.tsx | 71 ++++++++++ shared/pinentry/index.desktop.stories.tsx | 70 ++++++++++ shared/profile/modal.stories.tsx | 31 +++++ shared/profile/platform-icon.stories.tsx | 66 ++++++++++ .../provision/code-page/qr-image.stories.tsx | 26 ++++ shared/provision/error.stories.tsx | 75 +++++++++++ shared/provision/paper-key.stories.tsx | 35 +++++ .../provision/select-other-device.stories.tsx | 55 ++++++++ shared/provision/troubleshooting.stories.tsx | 33 +++++ .../header/syncing-folders.stories.tsx | 64 +++++++++ shared/scripts/screenshot-storybook.mts | 102 +++++++++++++++ shared/settings/group.stories.tsx | 70 ++++++++++ shared/settings/proxy.stories.tsx | 11 ++ shared/signup/device-name.stories.tsx | 26 ++++ shared/signup/email.stories.tsx | 40 ++++++ shared/signup/phone-number/index.stories.tsx | 37 ++++++ .../phone-number/verify-body.stories.tsx | 33 +++++ shared/signup/username.stories.tsx | 26 ++++ .../contact-restricted.stories.tsx | 37 ++++++ .../team-building/continue-button.stories.tsx | 26 ++++ .../filtered-service-tab-bar.stories.tsx | 35 +++++ shared/team-building/go-button.stories.tsx | 21 +++ .../team-building/service-tab-bar.stories.tsx | 63 +++++++++ shared/team-building/team-box.stories.tsx | 71 ++++++++++ shared/team-building/user-bubble.stories.tsx | 57 ++++++++ shared/teams/new-team/index.stories.tsx | 23 ++++ shared/teams/role-button.stories.tsx | 37 ++++++ shared/teams/role-picker.stories.tsx | 55 ++++++++ shared/tracker/bio.stories.tsx | 122 ++++++++++++++++++ .../device-list.desktop.stories.tsx | 61 +++++++++ .../paper-key-input.desktop.stories.tsx | 28 ++++ .../success.desktop.stories.tsx | 14 ++ shared/wallets/markdown-memo.stories.tsx | 39 ++++++ shared/wallets/remove-account.stories.tsx | 33 +++++ shared/wallets/wallet-popup.stories.tsx | 39 ++++++ 76 files changed, 3265 insertions(+), 21 deletions(-) create mode 100644 shared/chat/avatars.stories.tsx create mode 100644 shared/chat/inbox/row/big-teams-label.stories.tsx create mode 100644 shared/chat/inbox/row/teams-divider.stories.tsx create mode 100644 shared/common-adapters/badge.stories.tsx create mode 100644 shared/common-adapters/banner.stories.tsx create mode 100644 shared/common-adapters/checkbox.stories.tsx create mode 100644 shared/common-adapters/icon.stories.tsx create mode 100644 shared/common-adapters/loading-state-view.stories.tsx create mode 100644 shared/common-adapters/meta.stories.tsx create mode 100644 shared/common-adapters/progress-bar.stories.tsx create mode 100644 shared/common-adapters/radio-button.stories.tsx create mode 100644 shared/common-adapters/search-filter.stories.tsx create mode 100644 shared/common-adapters/section-divider.stories.tsx create mode 100644 shared/common-adapters/switch-toggle.stories.tsx create mode 100644 shared/common-adapters/tabs.stories.tsx create mode 100644 shared/common-adapters/text.stories.tsx create mode 100644 shared/common-adapters/waiting-button.stories.tsx create mode 100644 shared/crypto/input.stories.tsx create mode 100644 shared/crypto/output.stories.tsx create mode 100644 shared/crypto/recipients.stories.tsx create mode 100644 shared/crypto/sub-nav/left-nav.stories.tsx create mode 100644 shared/crypto/sub-nav/nav-row.stories.tsx create mode 100644 shared/fs/common/comma-separated-name.stories.tsx create mode 100644 shared/fs/footer/upload.stories.tsx create mode 100644 shared/fs/simple-screens/loading.stories.tsx create mode 100644 shared/git/nav-header.stories.tsx create mode 100644 shared/login/loading.stories.tsx create mode 100644 shared/login/recover-password/error.stories.tsx create mode 100644 shared/login/recover-password/explain-device.stories.tsx create mode 100644 shared/login/recover-password/paper-key.stories.tsx create mode 100644 shared/login/recover-password/prompt-reset-account.stories.tsx create mode 100644 shared/login/reset/waiting.stories.tsx create mode 100644 shared/login/signup/error.stories.tsx create mode 100644 shared/login/user-card/index.stories.tsx create mode 100644 shared/menubar/out-of-date.stories.tsx create mode 100644 shared/people/announcement.stories.tsx create mode 100644 shared/people/follow-notification.stories.tsx create mode 100644 shared/people/follow-suggestions.stories.tsx create mode 100644 shared/people/item.stories.tsx create mode 100644 shared/pinentry/index.desktop.stories.tsx create mode 100644 shared/profile/modal.stories.tsx create mode 100644 shared/profile/platform-icon.stories.tsx create mode 100644 shared/provision/code-page/qr-image.stories.tsx create mode 100644 shared/provision/error.stories.tsx create mode 100644 shared/provision/paper-key.stories.tsx create mode 100644 shared/provision/select-other-device.stories.tsx create mode 100644 shared/provision/troubleshooting.stories.tsx create mode 100644 shared/router-v2/header/syncing-folders.stories.tsx create mode 100644 shared/scripts/screenshot-storybook.mts create mode 100644 shared/settings/group.stories.tsx create mode 100644 shared/settings/proxy.stories.tsx create mode 100644 shared/signup/device-name.stories.tsx create mode 100644 shared/signup/email.stories.tsx create mode 100644 shared/signup/phone-number/index.stories.tsx create mode 100644 shared/signup/phone-number/verify-body.stories.tsx create mode 100644 shared/signup/username.stories.tsx create mode 100644 shared/team-building/contact-restricted.stories.tsx create mode 100644 shared/team-building/continue-button.stories.tsx create mode 100644 shared/team-building/filtered-service-tab-bar.stories.tsx create mode 100644 shared/team-building/go-button.stories.tsx create mode 100644 shared/team-building/service-tab-bar.stories.tsx create mode 100644 shared/team-building/team-box.stories.tsx create mode 100644 shared/team-building/user-bubble.stories.tsx create mode 100644 shared/teams/new-team/index.stories.tsx create mode 100644 shared/teams/role-button.stories.tsx create mode 100644 shared/teams/role-picker.stories.tsx create mode 100644 shared/tracker/bio.stories.tsx create mode 100644 shared/unlock-folders/device-list.desktop.stories.tsx create mode 100644 shared/unlock-folders/paper-key-input.desktop.stories.tsx create mode 100644 shared/unlock-folders/success.desktop.stories.tsx create mode 100644 shared/wallets/markdown-memo.stories.tsx create mode 100644 shared/wallets/remove-account.stories.tsx create mode 100644 shared/wallets/wallet-popup.stories.tsx diff --git a/plans/storybook.md b/plans/storybook.md index 6ac1d5e7e138..3f17257efabe 100644 --- a/plans/storybook.md +++ b/plans/storybook.md @@ -3,22 +3,22 @@ Top-level `shared/` folders to add stories for. Initial `devices/` is done. - [x] devices -- [ ] chat -- [ ] common-adapters -- [ ] crypto -- [ ] fs -- [ ] git -- [ ] login -- [ ] menubar -- [ ] people -- [ ] pinentry -- [ ] profile -- [ ] provision -- [ ] router-v2 -- [ ] settings -- [ ] signup -- [ ] team-building -- [ ] teams -- [ ] tracker -- [ ] unlock-folders -- [ ] wallets +- [x] chat +- [x] common-adapters +- [x] crypto +- [x] fs +- [x] git +- [x] login +- [x] menubar +- [x] people +- [x] pinentry +- [x] profile +- [x] provision +- [x] router-v2 +- [x] settings +- [x] signup +- [x] team-building +- [x] teams +- [x] tracker +- [x] unlock-folders +- [x] wallets diff --git a/shared/.storybook/main.ts b/shared/.storybook/main.ts index d2ead444cb99..2141890b12a4 100644 --- a/shared/.storybook/main.ts +++ b/shared/.storybook/main.ts @@ -32,11 +32,15 @@ const makeAliases = (): Record => { } const config: StorybookConfig = { - stories: ['../devices/**/*.stories.tsx'], + stories: ['../**/*.stories.tsx'], addons: [], framework: { name: '@storybook/react-webpack5', - options: {}, + options: { + builder: { + lazyCompilation: false, + }, + }, }, staticDirs: [ {from: '../fonts/electron', to: '/fonts/electron'}, diff --git a/shared/.storybook/preview.ts b/shared/.storybook/preview.ts index 15d394e2b1d1..81cd43c76d25 100644 --- a/shared/.storybook/preview.ts +++ b/shared/.storybook/preview.ts @@ -1,6 +1,7 @@ import React from 'react' import type {Preview} from '@storybook/react' import type {KB2} from '../util/electron' +import {initDesktopStyles} from '@/styles' // Inject a minimal KB2 stub so util/electron.tsx's getStashed() doesn't throw. // The real app sets this from the Electron preload script; storybook sets it here. @@ -44,6 +45,8 @@ const kb2Stub: KB2 = { // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access ;(globalThis as any)._fromPreload = kb2Stub +initDesktopStyles() + const preview: Preview = { decorators: [ Story => diff --git a/shared/chat/avatars.stories.tsx b/shared/chat/avatars.stories.tsx new file mode 100644 index 000000000000..acf8592cbf1a --- /dev/null +++ b/shared/chat/avatars.stories.tsx @@ -0,0 +1,57 @@ +import type {Meta, StoryObj} from '@storybook/react' +import {Avatars, TeamAvatar} from './avatars' + +const meta: Meta = { + component: Avatars, + title: 'Chat/Avatars', +} +export default meta +type Story = StoryObj + +export const SingleUser: Story = { + args: {participantOne: 'alice'}, +} + +export const TwoUsers: Story = { + args: {participantOne: 'alice', participantTwo: 'bob'}, +} + +export const Muted: Story = { + args: {participantOne: 'alice', participantTwo: 'bob', isMuted: true}, +} + +export const Selected: Story = { + args: {participantOne: 'alice', participantTwo: 'bob', isSelected: true}, +} + +export const Locked: Story = { + args: {participantOne: 'alice', participantTwo: 'bob', isLocked: true}, +} + +export const SmallSize: Story = { + args: {participantOne: 'alice', singleSize: 32}, +} + +export const LargeSize: Story = { + args: {participantOne: 'alice', singleSize: 96}, +} + +type TeamStory = StoryObj + +export const Team: TeamStory = { + render: () => ( + + ), +} + +export const TeamMuted: TeamStory = { + render: () => ( + + ), +} + +export const TeamSelected: TeamStory = { + render: () => ( + + ), +} diff --git a/shared/chat/inbox/row/big-teams-label.stories.tsx b/shared/chat/inbox/row/big-teams-label.stories.tsx new file mode 100644 index 000000000000..8eb2232a4ecb --- /dev/null +++ b/shared/chat/inbox/row/big-teams-label.stories.tsx @@ -0,0 +1,11 @@ +import type {Meta, StoryObj} from '@storybook/react' +import {BigTeamsLabel} from './big-teams-label' + +const meta: Meta = { + component: BigTeamsLabel, + title: 'Chat/BigTeamsLabel', +} +export default meta +type Story = StoryObj + +export const Default: Story = {} diff --git a/shared/chat/inbox/row/teams-divider.stories.tsx b/shared/chat/inbox/row/teams-divider.stories.tsx new file mode 100644 index 000000000000..ba55c3b97f6a --- /dev/null +++ b/shared/chat/inbox/row/teams-divider.stories.tsx @@ -0,0 +1,31 @@ +import type {Meta, StoryObj} from '@storybook/react' +import TeamsDivider from './teams-divider' + +const meta: Meta = { + component: TeamsDivider, + title: 'Chat/TeamsDivider', + args: { + toggle: () => {}, + smallTeamsExpanded: false, + showButton: false, + hiddenCount: 0, + }, +} +export default meta +type Story = StoryObj + +export const NoBigTeams: Story = { + args: {showButton: false, hiddenCount: 0, smallTeamsExpanded: false}, +} + +export const WithButton: Story = { + args: {showButton: true, hiddenCount: 12, smallTeamsExpanded: false}, +} + +export const WithButtonAndBadge: Story = { + args: {showButton: true, hiddenCount: 5, smallTeamsExpanded: false, badgeCount: 3}, +} + +export const Expanded: Story = { + args: {showButton: true, hiddenCount: 8, smallTeamsExpanded: true}, +} diff --git a/shared/common-adapters/badge.stories.tsx b/shared/common-adapters/badge.stories.tsx new file mode 100644 index 000000000000..a2c558dcd756 --- /dev/null +++ b/shared/common-adapters/badge.stories.tsx @@ -0,0 +1,29 @@ +import type {Meta, StoryObj} from '@storybook/react' +import Badge from './badge' + +const meta: Meta = { + component: Badge, + title: 'CommonAdapters/Badge', +} +export default meta +type Story = StoryObj + +export const Default: Story = { + args: {badgeNumber: 3}, +} + +export const LargeNumber: Story = { + args: {badgeNumber: 99}, +} + +export const VeryLargeNumber: Story = { + args: {badgeNumber: 1234}, +} + +export const WithBorder: Story = { + args: {badgeNumber: 5, border: true}, +} + +export const Large: Story = { + args: {badgeNumber: 2, height: 24, fontSize: 14}, +} diff --git a/shared/common-adapters/banner.stories.tsx b/shared/common-adapters/banner.stories.tsx new file mode 100644 index 000000000000..7daca28e41de --- /dev/null +++ b/shared/common-adapters/banner.stories.tsx @@ -0,0 +1,62 @@ +import type {Meta, StoryObj} from '@storybook/react' +import {Banner, BannerParagraph} from './banner' + +const meta: Meta = { + component: Banner, + title: 'CommonAdapters/Banner', + args: {color: 'blue', children: 'This is an informational banner message.'}, +} +export default meta +type Story = StoryObj + +export const Blue: Story = { + args: {color: 'blue', children: 'Your device has been revoked. Please provision again.'}, +} + +export const Red: Story = { + args: {color: 'red', children: 'Something went wrong. Please try again.'}, +} + +export const Yellow: Story = { + args: {color: 'yellow', children: 'You are in read-only mode due to a connectivity issue.'}, +} + +export const Green: Story = { + args: {color: 'green', children: 'Your files are fully synced.'}, +} + +export const Grey: Story = { + args: {color: 'grey', children: 'You have no unread messages.'}, +} + +export const WithCloseButton: Story = { + args: { + color: 'blue', + children: 'You have a new message.', + onClose: () => {}, + }, +} + +export const Small: Story = { + args: { + color: 'yellow', + children: 'Low disk space.', + small: true, + }, +} + +export const WithParagraph: Story = { + args: { + color: 'red', + children: ( + {}, text: 'alice'}, + "'s proofs have changed since you last followed them.", + ]} + /> + ), + }, +} diff --git a/shared/common-adapters/checkbox.stories.tsx b/shared/common-adapters/checkbox.stories.tsx new file mode 100644 index 000000000000..3a62773214a3 --- /dev/null +++ b/shared/common-adapters/checkbox.stories.tsx @@ -0,0 +1,41 @@ +import {useState} from 'react' +import type {Meta, StoryObj} from '@storybook/react' +import Checkbox from './checkbox' + +const meta: Meta = { + component: Checkbox, + title: 'CommonAdapters/Checkbox', + args: {label: 'Enable notifications', checked: false, onCheck: () => {}}, +} +export default meta +type Story = StoryObj + +export const Unchecked: Story = { + args: {label: 'Enable notifications', checked: false}, +} + +export const Checked: Story = { + args: {label: 'Enable notifications', checked: true}, +} + +export const Disabled: Story = { + args: {label: 'Disabled option', checked: false, disabled: true}, +} + +export const DisabledChecked: Story = { + args: {label: 'Disabled and checked', checked: true, disabled: true}, +} + +export const WithSubtitle: Story = { + args: { + label: 'Enable sound', + labelSubtitle: 'Play a sound for each new message', + checked: false, + }, +} + +const InteractiveCheckbox = () => { + const [checked, setChecked] = useState(false) + return +} +export const Interactive: Story = {render: () => } diff --git a/shared/common-adapters/icon.stories.tsx b/shared/common-adapters/icon.stories.tsx new file mode 100644 index 000000000000..76dc1b5133a3 --- /dev/null +++ b/shared/common-adapters/icon.stories.tsx @@ -0,0 +1,58 @@ +import type * as React from 'react' +import type {Meta, StoryObj} from '@storybook/react' +import Icon from './icon' +import {Box2} from './box' +import * as Styles from '@/styles' + +const iconShowcase: Array<{type: React.ComponentProps['type']; label: string}> = [ + {label: 'Search', type: 'iconfont-search'}, + {label: 'Close', type: 'iconfont-close'}, + {label: 'Edit', type: 'iconfont-edit'}, + {label: 'Add', type: 'iconfont-new'}, + {label: 'Lock', type: 'iconfont-lock'}, + {label: 'Star', type: 'iconfont-star'}, + {label: 'Check', type: 'iconfont-check'}, + {label: 'Arrow Up', type: 'iconfont-arrow-up'}, + {label: 'Arrow Down', type: 'iconfont-arrow-down'}, +] + +const meta: Meta = { + component: Icon, + title: 'CommonAdapters/Icon', + args: {type: 'iconfont-search'}, +} +export default meta +type Story = StoryObj + +export const Default: Story = { + args: {type: 'iconfont-search'}, +} + +export const Colored: Story = { + args: {type: 'iconfont-star', color: Styles.globalColors.blue}, +} + +export const Large: Story = { + args: {type: 'iconfont-search', sizeType: 'Big'}, +} + +export const Small: Story = { + args: {type: 'iconfont-search', sizeType: 'Small'}, +} + +export const Clickable: Story = { + args: {type: 'iconfont-edit', onClick: () => {}, color: Styles.globalColors.blue}, +} + +export const Showcase: Story = { + render: () => ( + + {iconShowcase.map(({type, label}) => ( + + + {label} + + ))} + + ), +} diff --git a/shared/common-adapters/loading-state-view.stories.tsx b/shared/common-adapters/loading-state-view.stories.tsx new file mode 100644 index 000000000000..b93ecc382885 --- /dev/null +++ b/shared/common-adapters/loading-state-view.stories.tsx @@ -0,0 +1,34 @@ +import type {Meta, StoryObj} from '@storybook/react' +import LoadingStateView from './loading-state-view' + +const meta: Meta = { + component: LoadingStateView, + title: 'CommonAdapters/LoadingStateView', + args: {loading: true}, +} +export default meta +type Story = StoryObj + +export const Loading: Story = { + args: {loading: true}, +} + +export const LoadingWithProgress: Story = { + args: {loading: true, progress: 0.6}, +} + +export const LoadingWhite: Story = { + args: {loading: true, white: true}, + decorators: [ + Story => ( +
+ +
+ ), + ], +} + +export const NotLoading: Story = { + args: {loading: false}, + // Should render nothing +} diff --git a/shared/common-adapters/meta.stories.tsx b/shared/common-adapters/meta.stories.tsx new file mode 100644 index 000000000000..0e93da3f4314 --- /dev/null +++ b/shared/common-adapters/meta.stories.tsx @@ -0,0 +1,35 @@ +import type {Meta, StoryObj} from '@storybook/react' +import MetaTag from './meta' +import * as Styles from '@/styles' + +const meta: Meta = { + component: MetaTag, + title: 'CommonAdapters/Meta', + args: {title: 'new', backgroundColor: Styles.globalColors.blue}, +} +export default meta +type Story = StoryObj + +export const New: Story = { + args: {title: 'new', backgroundColor: Styles.globalColors.blue}, +} + +export const Public: Story = { + args: {title: 'public', backgroundColor: Styles.globalColors.green}, +} + +export const Admin: Story = { + args: {title: 'admin', backgroundColor: Styles.globalColors.blue}, +} + +export const Error: Story = { + args: {title: 'error', backgroundColor: Styles.globalColors.red}, +} + +export const Small: Story = { + args: {title: 'beta', backgroundColor: Styles.globalColors.purple, size: 'Small'}, +} + +export const NoUppercase: Story = { + args: {title: 'keybase.io', backgroundColor: Styles.globalColors.blueGrey, noUppercase: true, color: Styles.globalColors.black_50}, +} diff --git a/shared/common-adapters/progress-bar.stories.tsx b/shared/common-adapters/progress-bar.stories.tsx new file mode 100644 index 000000000000..7aacb2f0b092 --- /dev/null +++ b/shared/common-adapters/progress-bar.stories.tsx @@ -0,0 +1,30 @@ +import type {Meta, StoryObj} from '@storybook/react' +import ProgressBar from './progress-bar' + +const meta: Meta = { + component: ProgressBar, + title: 'CommonAdapters/ProgressBar', + args: {ratio: 0.5}, +} +export default meta +type Story = StoryObj + +export const Empty: Story = { + args: {ratio: 0}, +} + +export const Quarter: Story = { + args: {ratio: 0.25}, +} + +export const Half: Story = { + args: {ratio: 0.5}, +} + +export const ThreeQuarters: Story = { + args: {ratio: 0.75}, +} + +export const Full: Story = { + args: {ratio: 1}, +} diff --git a/shared/common-adapters/radio-button.stories.tsx b/shared/common-adapters/radio-button.stories.tsx new file mode 100644 index 000000000000..e8e206a330cf --- /dev/null +++ b/shared/common-adapters/radio-button.stories.tsx @@ -0,0 +1,33 @@ +import {useState} from 'react' +import type {Meta, StoryObj} from '@storybook/react' +import RadioButton from './radio-button' + +const meta: Meta = { + component: RadioButton, + title: 'CommonAdapters/RadioButton', + args: {label: 'Option A', selected: false, onSelect: () => {}}, +} +export default meta +type Story = StoryObj + +export const Unselected: Story = { + args: {label: 'Option A', selected: false}, +} + +export const Selected: Story = { + args: {label: 'Option A', selected: true}, +} + +export const Disabled: Story = { + args: {label: 'Disabled option', selected: false, disabled: true}, +} + +export const DisabledSelected: Story = { + args: {label: 'Disabled selected', selected: true, disabled: true}, +} + +const InteractiveButton = () => { + const [selected, setSelected] = useState(false) + return +} +export const Interactive: Story = {render: () => } diff --git a/shared/common-adapters/search-filter.stories.tsx b/shared/common-adapters/search-filter.stories.tsx new file mode 100644 index 000000000000..8cb900565fec --- /dev/null +++ b/shared/common-adapters/search-filter.stories.tsx @@ -0,0 +1,54 @@ +import type {Meta, StoryObj} from '@storybook/react' +import SearchFilter from './search-filter' + +const meta: Meta = { + component: SearchFilter, + title: 'CommonAdapters/SearchFilter', + args: { + placeholderText: 'Search', + size: 'full-width', + }, +} +export default meta +type Story = StoryObj + +export const FullWidth: Story = { + args: { + size: 'full-width', + placeholderText: 'Search people or teams', + }, +} + +export const Small: Story = { + args: { + size: 'small', + placeholderText: 'Search', + }, +} + +export const WithIcon: Story = { + args: { + size: 'full-width', + placeholderText: 'Search', + icon: 'iconfont-search', + }, +} + +export const WithValue: Story = { + args: { + size: 'full-width', + placeholderText: 'Search', + valueControlled: true, + value: 'alice', + }, +} + +export const Waiting: Story = { + args: { + size: 'full-width', + placeholderText: 'Search', + waiting: true, + valueControlled: true, + value: 'searching...', + }, +} diff --git a/shared/common-adapters/section-divider.stories.tsx b/shared/common-adapters/section-divider.stories.tsx new file mode 100644 index 000000000000..9bfe9c4e4e85 --- /dev/null +++ b/shared/common-adapters/section-divider.stories.tsx @@ -0,0 +1,42 @@ +import {useState} from 'react' +import type {Meta, StoryObj} from '@storybook/react' +import SectionDivider from './section-divider' + +const meta: Meta = { + component: SectionDivider, + title: 'CommonAdapters/SectionDivider', +} +export default meta +type Story = StoryObj + +export const SimpleLabel: Story = { + args: {label: 'Section Header'}, +} + +export const WithSpinner: Story = { + args: {label: 'Loading section', showSpinner: true}, +} + +const ExpandedSection = () => { + const [collapsed, setCollapsed] = useState(false) + return ( + setCollapsed(c => !c)} + /> + ) +} +export const CollapsibleExpanded: Story = {render: () => } + +const CollapsedSection = () => { + const [collapsed, setCollapsed] = useState(true) + return ( + setCollapsed(c => !c)} + /> + ) +} +export const CollapsibleCollapsed: Story = {render: () => } diff --git a/shared/common-adapters/switch-toggle.stories.tsx b/shared/common-adapters/switch-toggle.stories.tsx new file mode 100644 index 000000000000..b59f3b1d1097 --- /dev/null +++ b/shared/common-adapters/switch-toggle.stories.tsx @@ -0,0 +1,26 @@ +import type {Meta, StoryObj} from '@storybook/react' +import SwitchToggle from './switch-toggle' + +const meta: Meta = { + component: SwitchToggle, + title: 'CommonAdapters/SwitchToggle', + args: {color: 'green', on: false}, +} +export default meta +type Story = StoryObj + +export const GreenOff: Story = { + args: {color: 'green', on: false}, +} + +export const GreenOn: Story = { + args: {color: 'green', on: true}, +} + +export const BlueOn: Story = { + args: {color: 'blue', on: true}, +} + +export const RedOn: Story = { + args: {color: 'red', on: true}, +} diff --git a/shared/common-adapters/tabs.stories.tsx b/shared/common-adapters/tabs.stories.tsx new file mode 100644 index 000000000000..e8fe9772b1e2 --- /dev/null +++ b/shared/common-adapters/tabs.stories.tsx @@ -0,0 +1,42 @@ +import {useState} from 'react' +import type {Meta, StoryObj} from '@storybook/react' +import Tabs from './tabs' +import type {Tab} from './tabs' + +const simpleTabs: Array> = [ + {title: 'profile'}, + {title: 'followers'}, + {title: 'following'}, +] + +const badgedTabs: Array> = [ + {title: 'chat', badgeNumber: 3}, + {title: 'files'}, + {title: 'devices', badgeNumber: 1}, + {title: 'settings'}, +] + +const meta: Meta = { + component: Tabs, + title: 'CommonAdapters/Tabs', +} +export default meta +type Story = StoryObj + +const BasicTabs = () => { + const [selected, setSelected] = useState('profile') + return +} +export const Basic: Story = {render: () => } + +const WithBadgesTabs = () => { + const [selected, setSelected] = useState('chat') + return +} +export const WithBadges: Story = {render: () => } + +const WithProgressTabs = () => { + const [selected, setSelected] = useState('profile') + return +} +export const WithProgress: Story = {render: () => } diff --git a/shared/common-adapters/text.stories.tsx b/shared/common-adapters/text.stories.tsx new file mode 100644 index 000000000000..7e77e27ce64c --- /dev/null +++ b/shared/common-adapters/text.stories.tsx @@ -0,0 +1,74 @@ +import type {Meta, StoryObj} from '@storybook/react' +import Text from './text' +import {Box2} from './box' +import * as Styles from '@/styles' + +const meta: Meta = { + component: Text, + title: 'CommonAdapters/Text', + args: {type: 'Body', children: 'The quick brown fox jumps over the lazy dog'}, +} +export default meta +type Story = StoryObj + +export const Body: Story = { + args: {type: 'Body', children: 'Body text — used for standard content.'}, +} + +export const BodySmall: Story = { + args: {type: 'BodySmall', children: 'BodySmall — secondary information or metadata.'}, +} + +export const BodySemibold: Story = { + args: {type: 'BodySemibold', children: 'BodySemibold — slightly emphasized content.'}, +} + +export const Header: Story = { + args: {type: 'Header', children: 'Header — section titles and dialog headings.'}, +} + +export const HeaderBig: Story = { + args: {type: 'HeaderBig', children: 'HeaderBig — page-level headings.'}, +} + +export const BodyPrimaryLink: Story = { + args: {type: 'BodyPrimaryLink', children: 'Primary link text', onClick: () => {}}, +} + +export const BodySmallError: Story = { + args: {type: 'BodySmallError', children: 'Something went wrong.'}, +} + +export const BodyTiny: Story = { + args: {type: 'BodyTiny', children: 'BodyTiny — captions and fine print.'}, +} + +export const Negative: Story = { + args: {type: 'Body', children: 'Negative (light text for dark backgrounds)', negative: true}, + decorators: [ + Story => ( + + + + ), + ], +} + +export const LineClamp: Story = { + args: { + type: 'Body', + children: 'This is a very long string that will be clamped to a single line because it exceeds the available width in most containers and we want to demonstrate the lineClamp feature.', + lineClamp: 1, + style: {maxWidth: 300}, + }, +} + +export const TypeShowcase: Story = { + render: () => ( + + {(['HeaderBig', 'Header', 'Body', 'BodySemibold', 'BodySmall', 'BodyTiny'] as const).map(type => ( + {type}: The quick brown fox + ))} + + ), +} diff --git a/shared/common-adapters/waiting-button.stories.tsx b/shared/common-adapters/waiting-button.stories.tsx new file mode 100644 index 000000000000..28e7b3ef1114 --- /dev/null +++ b/shared/common-adapters/waiting-button.stories.tsx @@ -0,0 +1,34 @@ +import type {Meta, StoryObj} from '@storybook/react' +import WaitingButton from './waiting-button' + +const meta: Meta = { + component: WaitingButton, + title: 'CommonAdapters/WaitingButton', + args: {label: 'Submit', onClick: () => {}}, +} +export default meta +type Story = StoryObj + +export const Default: Story = { + args: {label: 'Submit', type: 'Default'}, +} + +export const Success: Story = { + args: {label: 'Save', type: 'Success'}, +} + +export const Danger: Story = { + args: {label: 'Delete', type: 'Danger'}, +} + +export const Disabled: Story = { + args: {label: 'Submit', disabled: true}, +} + +export const Small: Story = { + args: {label: 'OK', small: true}, +} + +export const FullWidth: Story = { + args: {label: 'Continue', fullWidth: true}, +} diff --git a/shared/crypto/input.stories.tsx b/shared/crypto/input.stories.tsx new file mode 100644 index 000000000000..f6c72bbe0dc1 --- /dev/null +++ b/shared/crypto/input.stories.tsx @@ -0,0 +1,67 @@ +import type {Meta, StoryObj} from '@storybook/react' +import {CryptoBanner} from './input' +import type {CommonState} from './helpers' + +const makeState = (overrides: Partial = {}): CommonState => ({ + bytesComplete: 0, + bytesTotal: 0, + errorMessage: '', + inProgress: false, + input: '', + inputType: 'text', + output: '', + outputSenderFullname: undefined, + outputSenderUsername: undefined, + outputSigned: false, + outputStatus: undefined, + outputType: undefined, + outputValid: false, + warningMessage: '', + ...overrides, +}) + +const meta: Meta = { + component: CryptoBanner, + title: 'Crypto/CryptoBanner', + args: { + infoMessage: 'Add your cryptographic signature to a message or file.', + state: makeState(), + }, +} +export default meta +type Story = StoryObj + +export const InfoSign: Story = { + args: { + infoMessage: 'Add your cryptographic signature to a message or file.', + state: makeState(), + }, +} + +export const InfoEncrypt: Story = { + args: { + infoMessage: "Encrypt to anyone, even if they're not on Keybase yet.", + state: makeState(), + }, +} + +export const ErrorState: Story = { + args: { + state: makeState({errorMessage: 'This Saltpack format is unexpected. Did you mean to decrypt it?'}), + }, +} + +export const WarningState: Story = { + args: { + state: makeState({warningMessage: 'Note: Encrypted for an SBS user who has not joined Keybase yet.'}), + }, +} + +export const ErrorAndWarning: Story = { + args: { + state: makeState({ + errorMessage: 'Failed to sign text.', + warningMessage: 'Warning: something looks off.', + }), + }, +} diff --git a/shared/crypto/output.stories.tsx b/shared/crypto/output.stories.tsx new file mode 100644 index 000000000000..0ffbf62def36 --- /dev/null +++ b/shared/crypto/output.stories.tsx @@ -0,0 +1,46 @@ +import type {Meta, StoryObj} from '@storybook/react' +import {OutputInfoBanner} from './output' +import * as Kb from '@/common-adapters' + +const meta: Meta = { + component: OutputInfoBanner, + title: 'Crypto/OutputInfoBanner', + args: { + outputStatus: 'success', + }, +} +export default meta +type Story = StoryObj + +export const SignSuccess: Story = { + args: { + outputStatus: 'success', + children: ( + + This is your signed message, using Saltpack. Anyone who has it can verify you signed it. + + ), + }, +} + +export const EncryptSuccess: Story = { + args: { + outputStatus: 'success', + children: ( + + This is your encrypted message, using Saltpack. + + ), + }, +} + +export const NotSuccess: Story = { + args: { + outputStatus: 'pending', + children: ( + + This should not be visible. + + ), + }, +} diff --git a/shared/crypto/recipients.stories.tsx b/shared/crypto/recipients.stories.tsx new file mode 100644 index 000000000000..fcb39567c152 --- /dev/null +++ b/shared/crypto/recipients.stories.tsx @@ -0,0 +1,36 @@ +import type {Meta, StoryObj} from '@storybook/react' +import Recipients from './recipients' + +const meta: Meta = { + component: Recipients, + title: 'Crypto/Recipients', + args: { + inProgress: false, + onAddRecipients: () => {}, + onClearRecipients: () => {}, + recipients: [], + }, +} +export default meta +type Story = StoryObj + +export const Empty: Story = {} + +export const OneRecipient: Story = { + args: { + recipients: ['alice'], + }, +} + +export const MultipleRecipients: Story = { + args: { + recipients: ['alice', 'bob', 'charlie'], + }, +} + +export const InProgress: Story = { + args: { + recipients: ['alice', 'bob'], + inProgress: true, + }, +} diff --git a/shared/crypto/sub-nav/left-nav.stories.tsx b/shared/crypto/sub-nav/left-nav.stories.tsx new file mode 100644 index 000000000000..2df7afc9fc42 --- /dev/null +++ b/shared/crypto/sub-nav/left-nav.stories.tsx @@ -0,0 +1,30 @@ +import type {Meta, StoryObj} from '@storybook/react' +import LeftNav from './left-nav.desktop' +import * as Crypto from '@/constants/crypto' + +const meta: Meta = { + component: LeftNav, + title: 'Crypto/LeftNav', + args: { + onClick: () => {}, + selected: Crypto.encryptTab, + }, +} +export default meta +type Story = StoryObj + +export const EncryptSelected: Story = { + args: {selected: Crypto.encryptTab}, +} + +export const DecryptSelected: Story = { + args: {selected: Crypto.decryptTab}, +} + +export const SignSelected: Story = { + args: {selected: Crypto.signTab}, +} + +export const VerifySelected: Story = { + args: {selected: Crypto.verifyTab}, +} diff --git a/shared/crypto/sub-nav/nav-row.stories.tsx b/shared/crypto/sub-nav/nav-row.stories.tsx new file mode 100644 index 000000000000..f90c70fdd71d --- /dev/null +++ b/shared/crypto/sub-nav/nav-row.stories.tsx @@ -0,0 +1,73 @@ +import type {Meta, StoryObj} from '@storybook/react' +import NavRow from './nav-row' + +const meta: Meta = { + component: NavRow, + title: 'Crypto/NavRow', + args: { + tab: 'encryptTab', + onClick: () => {}, + }, +} +export default meta +type Story = StoryObj + +export const DesktopEncrypt: Story = { + args: { + title: 'Encrypt', + icon: 'iconfont-lock', + isSelected: false, + }, +} + +export const DesktopEncryptSelected: Story = { + args: { + title: 'Encrypt', + icon: 'iconfont-lock', + isSelected: true, + }, +} + +export const DesktopDecrypt: Story = { + args: { + title: 'Decrypt', + tab: 'decryptTab', + icon: 'iconfont-unlock', + isSelected: false, + }, +} + +export const DesktopSign: Story = { + args: { + title: 'Sign', + tab: 'signTab', + icon: 'iconfont-check', + isSelected: false, + }, +} + +export const DesktopVerify: Story = { + args: { + title: 'Verify', + tab: 'verifyTab', + icon: 'iconfont-verify', + isSelected: false, + }, +} + +export const MobileEncrypt: Story = { + args: { + title: 'Encrypt', + description: "Encrypt to anyone, even if they're not on Keybase yet.", + illustration: 'icon-encrypt-64', + }, +} + +export const MobileDecrypt: Story = { + args: { + title: 'Decrypt', + tab: 'decryptTab', + description: 'Decrypt any ciphertext or .encrypted.saltpack file.', + illustration: 'icon-decrypt-64', + }, +} diff --git a/shared/fs/common/comma-separated-name.stories.tsx b/shared/fs/common/comma-separated-name.stories.tsx new file mode 100644 index 000000000000..9e49e12e566d --- /dev/null +++ b/shared/fs/common/comma-separated-name.stories.tsx @@ -0,0 +1,30 @@ +import type {Meta, StoryObj} from '@storybook/react' +import CommaSeparatedName from './comma-separated-name' + +const meta: Meta = { + component: CommaSeparatedName, + title: 'FS/CommaSeparatedName', + args: {type: 'Body'}, +} +export default meta +type Story = StoryObj + +export const SingleName: Story = { + args: {name: 'alice', type: 'Body'}, +} + +export const TwoNames: Story = { + args: {name: 'alice,bob', type: 'Body'}, +} + +export const MultipleNames: Story = { + args: {name: 'alice,bob,charlie,dave', type: 'BodySmall'}, +} + +export const Selectable: Story = { + args: {name: 'alice,bob', type: 'BodySemibold', selectable: true}, +} + +export const Centered: Story = { + args: {name: 'alice,bob,charlie', type: 'Header', center: true}, +} diff --git a/shared/fs/footer/upload.stories.tsx b/shared/fs/footer/upload.stories.tsx new file mode 100644 index 000000000000..7e9167d958dc --- /dev/null +++ b/shared/fs/footer/upload.stories.tsx @@ -0,0 +1,64 @@ +import type {Meta, StoryObj} from '@storybook/react' +import Upload from './upload' + +const meta: Meta = { + component: Upload, + title: 'FS/Upload', + args: { + showing: true, + files: 0, + totalSyncingBytes: 0, + timeLeft: '', + }, +} +export default meta +type Story = StoryObj + +export const Showing: Story = { + args: { + showing: true, + files: 3, + fileName: 'photo.jpg', + totalSyncingBytes: 1024 * 1024, + timeLeft: '2 minutes', + }, +} + +export const SingleFile: Story = { + args: { + showing: true, + files: 1, + fileName: 'document.pdf', + totalSyncingBytes: 512 * 1024, + timeLeft: '30 seconds', + }, +} + +export const Done: Story = { + args: { + showing: true, + files: 0, + totalSyncingBytes: 0, + timeLeft: '', + }, +} + +export const SmallMode: Story = { + args: { + showing: true, + files: 2, + fileName: 'report.docx', + totalSyncingBytes: 256 * 1024, + timeLeft: '1 minute', + smallMode: true, + }, +} + +export const Hidden: Story = { + args: { + showing: false, + files: 0, + totalSyncingBytes: 0, + timeLeft: '', + }, +} diff --git a/shared/fs/simple-screens/loading.stories.tsx b/shared/fs/simple-screens/loading.stories.tsx new file mode 100644 index 000000000000..09253a116306 --- /dev/null +++ b/shared/fs/simple-screens/loading.stories.tsx @@ -0,0 +1,15 @@ +import type {Meta, StoryObj} from '@storybook/react' +import LoadingScreen from './loading' + +const meta: Meta = { + component: LoadingScreen, + title: 'FS/LoadingScreen', +} +export default meta +type Story = StoryObj + +export const Default: Story = {} + +export const WithReason: Story = { + args: {why: ' listing files'}, +} diff --git a/shared/git/nav-header.stories.tsx b/shared/git/nav-header.stories.tsx new file mode 100644 index 000000000000..85ded46718a2 --- /dev/null +++ b/shared/git/nav-header.stories.tsx @@ -0,0 +1,11 @@ +import type {Meta, StoryObj} from '@storybook/react' +import {HeaderTitle} from './nav-header' + +const meta: Meta = { + component: HeaderTitle, + title: 'Git/NavHeaderTitle', +} +export default meta +type Story = StoryObj + +export const Default: Story = {} diff --git a/shared/login/loading.stories.tsx b/shared/login/loading.stories.tsx new file mode 100644 index 000000000000..2bdb264ff5c4 --- /dev/null +++ b/shared/login/loading.stories.tsx @@ -0,0 +1,41 @@ +import type {Meta, StoryObj} from '@storybook/react' +import {Splash} from './loading' + +const meta: Meta = { + component: Splash, + title: 'Login/Splash', +} +export default meta +type Story = StoryObj + +export const Loading: Story = { + args: { + failed: '', + status: 'Loading...', + }, +} + +export const StillTrying: Story = { + args: { + failed: '', + status: 'Loading... (still trying)', + }, +} + +export const Failed: Story = { + args: { + failed: 'connection refused', + status: '', + onRetry: () => {}, + }, +} + +export const FailedWithFeedback: Story = { + args: { + allowFeedback: true, + failed: 'connection refused', + status: '', + onRetry: () => {}, + onFeedback: () => {}, + }, +} diff --git a/shared/login/recover-password/error.stories.tsx b/shared/login/recover-password/error.stories.tsx new file mode 100644 index 000000000000..ff4258e4b700 --- /dev/null +++ b/shared/login/recover-password/error.stories.tsx @@ -0,0 +1,21 @@ +import type {Meta, StoryObj} from '@storybook/react' +import ConnectedError from './error' + +const meta: Meta = { + component: ConnectedError, + title: 'Login/RecoverPasswordError', +} +export default meta +type Story = StoryObj + +export const GenericError: Story = { + args: { + route: {params: {error: 'An unexpected error occurred. Please try again.'}}, + }, +} + +export const NetworkError: Story = { + args: { + route: {params: {error: 'Network error: could not reach Keybase servers.'}}, + }, +} diff --git a/shared/login/recover-password/explain-device.stories.tsx b/shared/login/recover-password/explain-device.stories.tsx new file mode 100644 index 000000000000..0fd79c9e56a1 --- /dev/null +++ b/shared/login/recover-password/explain-device.stories.tsx @@ -0,0 +1,34 @@ +import type {Meta, StoryObj} from '@storybook/react' +import * as T from '@/constants/types' +import ExplainDevice from './explain-device' + +const meta: Meta = { + component: ExplainDevice, + title: 'Login/RecoverPasswordExplainDevice', +} +export default meta +type Story = StoryObj + +export const Desktop: Story = { + args: { + route: { + params: { + deviceName: 'work-laptop', + deviceType: T.RPCGen.DeviceType.desktop, + username: 'chrisnojima', + }, + }, + }, +} + +export const Mobile: Story = { + args: { + route: { + params: { + deviceName: 'iPhone 15', + deviceType: T.RPCGen.DeviceType.mobile, + username: 'chrisnojima', + }, + }, + }, +} diff --git a/shared/login/recover-password/paper-key.stories.tsx b/shared/login/recover-password/paper-key.stories.tsx new file mode 100644 index 000000000000..203a52e08d03 --- /dev/null +++ b/shared/login/recover-password/paper-key.stories.tsx @@ -0,0 +1,24 @@ +import type {Meta, StoryObj} from '@storybook/react' +import PaperKey from './paper-key' + +const meta: Meta = { + component: PaperKey, + title: 'Login/RecoverPasswordPaperKey', + args: { + route: {params: {}}, + }, +} +export default meta +type Story = StoryObj + +export const Empty: Story = { + args: { + route: {params: {}}, + }, +} + +export const WithError: Story = { + args: { + route: {params: {error: 'Incorrect paper key. Please try again.'}}, + }, +} diff --git a/shared/login/recover-password/prompt-reset-account.stories.tsx b/shared/login/recover-password/prompt-reset-account.stories.tsx new file mode 100644 index 000000000000..813404436ab6 --- /dev/null +++ b/shared/login/recover-password/prompt-reset-account.stories.tsx @@ -0,0 +1,31 @@ +import type {Meta, StoryObj} from '@storybook/react' +import PromptReset from './prompt-reset-shared' + +const meta: Meta = { + component: PromptReset, + title: 'Login/RecoverPasswordPromptReset', +} +export default meta +type Story = StoryObj + +export const AccountReset: Story = { + args: { + skipPassword: true, + username: 'chrisnojima', + }, +} + +export const ResetPassword: Story = { + args: { + resetPassword: true, + skipPassword: false, + username: 'chrisnojima', + }, +} + +export const KnowPassword: Story = { + args: { + skipPassword: false, + username: 'chrisnojima', + }, +} diff --git a/shared/login/reset/waiting.stories.tsx b/shared/login/reset/waiting.stories.tsx new file mode 100644 index 000000000000..09582494fc86 --- /dev/null +++ b/shared/login/reset/waiting.stories.tsx @@ -0,0 +1,26 @@ +import type {Meta, StoryObj} from '@storybook/react' +import Waiting from './waiting' + +const meta: Meta = { + component: Waiting, + title: 'Login/ResetWaiting', +} +export default meta +type Story = StoryObj + +const futureTime = Date.now() + 7 * 24 * 60 * 60 * 1000 // 7 days from now + +export const CheckEmailOrPhone: Story = { + args: { + pipelineStarted: false, + username: 'chrisnojima', + }, +} + +export const PipelineStarted: Story = { + args: { + endTime: futureTime, + pipelineStarted: true, + username: 'chrisnojima', + }, +} diff --git a/shared/login/signup/error.stories.tsx b/shared/login/signup/error.stories.tsx new file mode 100644 index 000000000000..d0a2ab7eff82 --- /dev/null +++ b/shared/login/signup/error.stories.tsx @@ -0,0 +1,28 @@ +import type {Meta, StoryObj} from '@storybook/react' +import ConnectedSignupError from './error' + +const meta: Meta = { + component: ConnectedSignupError, + title: 'Login/SignupError', +} +export default meta +type Story = StoryObj + +export const GenericError: Story = { + args: { + route: {params: {errorMessage: 'An unexpected error occurred. Please try again.'}}, + }, +} + +export const NetworkError: Story = { + args: { + // errorCode 8 is scapinetworkerror + route: {params: {errorCode: 8, errorMessage: 'Network error.'}}, + }, +} + +export const NoMessage: Story = { + args: { + route: {params: {}}, + }, +} diff --git a/shared/login/user-card/index.stories.tsx b/shared/login/user-card/index.stories.tsx new file mode 100644 index 000000000000..6e3c20b358c7 --- /dev/null +++ b/shared/login/user-card/index.stories.tsx @@ -0,0 +1,55 @@ +import type {Meta, StoryObj} from '@storybook/react' +import * as Kb from '@/common-adapters' +import UserCard from './index' + +const meta: Meta = { + component: UserCard, + title: 'Login/UserCard', +} +export default meta +type Story = StoryObj + +export const WithUsername: Story = { + args: { + username: 'chrisnojima', + children: ( + + Card content here + + ), + }, +} + +export const WithoutUsername: Story = { + args: { + children: ( + + No username yet + + ), + }, +} + +export const LargeAvatar: Story = { + args: { + username: 'chrisnojima', + avatarSize: 128, + children: ( + + Large avatar + + ), + }, +} + +export const SmallAvatar: Story = { + args: { + username: 'chrisnojima', + avatarSize: 48, + children: ( + + Small avatar + + ), + }, +} diff --git a/shared/menubar/out-of-date.stories.tsx b/shared/menubar/out-of-date.stories.tsx new file mode 100644 index 000000000000..9b73be02eeac --- /dev/null +++ b/shared/menubar/out-of-date.stories.tsx @@ -0,0 +1,54 @@ +import type {Meta, StoryObj} from '@storybook/react' +import type * as T from '@/constants/types' +import OutOfDate from './out-of-date' + +const makeOutOfDate = (overrides: Partial = {}): T.Config.OutOfDate => ({ + critical: false, + message: '', + outOfDate: true, + updating: false, + ...overrides, +}) + +const meta: Meta = { + component: OutOfDate, + title: 'Menubar/OutOfDate', +} +export default meta +type Story = StoryObj + +export const NonCritical: Story = { + args: { + outOfDate: makeOutOfDate(), + }, +} + +export const Critical: Story = { + args: { + outOfDate: makeOutOfDate({critical: true}), + }, +} + +export const NonCriticalWithMessage: Story = { + args: { + outOfDate: makeOutOfDate({message: 'Please update to get the latest security fixes'}), + }, +} + +export const CriticalWithMessage: Story = { + args: { + outOfDate: makeOutOfDate({critical: true, message: 'Critical security patch required'}), + }, +} + +export const Updating: Story = { + args: { + outOfDate: makeOutOfDate({updating: true}), + }, +} + +export const CriticalUpdating: Story = { + args: { + outOfDate: makeOutOfDate({critical: true, updating: true}), + }, +} diff --git a/shared/people/announcement.stories.tsx b/shared/people/announcement.stories.tsx new file mode 100644 index 000000000000..b0debdc9ddf9 --- /dev/null +++ b/shared/people/announcement.stories.tsx @@ -0,0 +1,69 @@ +import type {Meta, StoryObj} from '@storybook/react' +import Announcement from './announcement' + +const meta: Meta = { + component: Announcement, + title: 'People/Announcement', + args: { + badged: false, + dismissable: false, + dismissAnnouncement: () => {}, + getData: () => {}, + id: 1 as any, + text: 'Keybase has a new feature available.', + }, +} +export default meta +type Story = StoryObj + +export const Basic: Story = { + args: { + text: 'Keybase has a new feature available.', + badged: false, + dismissable: false, + }, +} + +export const Badged: Story = { + args: { + text: 'Important update: please review your security settings.', + badged: true, + dismissable: false, + }, +} + +export const WithConfirmButton: Story = { + args: { + text: 'You can now add your phone number for account recovery.', + confirmLabel: 'Add phone', + badged: false, + dismissable: false, + }, +} + +export const Dismissable: Story = { + args: { + text: 'Check out the new Keybase Teams feature.', + confirmLabel: 'Learn more', + badged: false, + dismissable: true, + }, +} + +export const WithIconUrl: Story = { + args: { + text: 'Your Keybase profile has been verified.', + iconUrl: 'https://keybase.io/images/icons/icon-keybase-logo-48@2x.png', + badged: false, + dismissable: false, + }, +} + +export const LongText: Story = { + args: { + text: 'Keybase now supports end-to-end encrypted git repositories for teams. All members of your team will be able to securely collaborate on code without exposing it to outside parties. Visit the Git tab to create your first team repository.', + confirmLabel: 'Open Git', + badged: true, + dismissable: true, + }, +} diff --git a/shared/people/follow-notification.stories.tsx b/shared/people/follow-notification.stories.tsx new file mode 100644 index 000000000000..6039a24eae80 --- /dev/null +++ b/shared/people/follow-notification.stories.tsx @@ -0,0 +1,93 @@ +import type {Meta, StoryObj} from '@storybook/react' +import type {NewFollow} from './follow-notification' +import FollowNotification from './follow-notification' + +const now = new Date() +const hourAgo = new Date(now.getTime() - 60 * 60 * 1000) +const dayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000) + +const makeFollow = (username: string, contactDescription?: string): NewFollow => ({ + username, + ...(contactDescription ? {contactDescription} : {}), +}) + +const meta: Meta = { + component: FollowNotification, + title: 'People/FollowNotification', + args: { + onClickUser: () => {}, + badged: false, + notificationTime: hourAgo, + type: 'follow', + newFollows: [makeFollow('alice')], + }, +} +export default meta +type Story = StoryObj + +export const SingleFollow: Story = { + args: { + newFollows: [makeFollow('alice')], + notificationTime: hourAgo, + type: 'follow', + badged: false, + }, +} + +export const SingleFollowBadged: Story = { + args: { + newFollows: [makeFollow('bob')], + notificationTime: now, + type: 'follow', + badged: true, + }, +} + +export const SingleContact: Story = { + args: { + newFollows: [makeFollow('carol', 'Carol White (+1-555-0001)')], + notificationTime: dayAgo, + type: 'contact', + badged: false, + }, +} + +export const MultiFollow: Story = { + args: { + newFollows: [makeFollow('alice'), makeFollow('bob'), makeFollow('carol')], + notificationTime: hourAgo, + type: 'follow', + badged: false, + }, +} + +export const MultiFollowBadged: Story = { + args: { + newFollows: [makeFollow('dave'), makeFollow('eve')], + notificationTime: now, + type: 'follow', + badged: true, + }, +} + +export const MultiFollowWithAdditional: Story = { + args: { + newFollows: [makeFollow('frank'), makeFollow('grace'), makeFollow('heidi')], + numAdditional: 7, + notificationTime: dayAgo, + type: 'follow', + badged: false, + }, +} + +export const MultiContact: Story = { + args: { + newFollows: [ + makeFollow('user1', 'Contact A (+1-555-0002)'), + makeFollow('user2', 'Contact B (+1-555-0003)'), + ], + notificationTime: hourAgo, + type: 'contact', + badged: false, + }, +} diff --git a/shared/people/follow-suggestions.stories.tsx b/shared/people/follow-suggestions.stories.tsx new file mode 100644 index 000000000000..0c70e4c8112c --- /dev/null +++ b/shared/people/follow-suggestions.stories.tsx @@ -0,0 +1,64 @@ +import type {Meta, StoryObj} from '@storybook/react' +import type {FollowSuggestion} from './follow-suggestions' +import FollowSuggestions from './follow-suggestions' + +const makeSuggestion = (overrides: Partial = {}): FollowSuggestion => ({ + followsMe: false, + fullName: '', + iFollow: false, + username: 'user', + ...overrides, +}) + +const meta: Meta = { + component: FollowSuggestions, + title: 'People/FollowSuggestions', +} +export default meta +type Story = StoryObj + +export const Default: Story = { + args: { + suggestions: [ + makeSuggestion({username: 'alice', fullName: 'Alice Smith', followsMe: true}), + makeSuggestion({username: 'bob', fullName: 'Bob Johnson'}), + makeSuggestion({username: 'carol', fullName: 'Carol White', iFollow: true}), + makeSuggestion({username: 'dave'}), + makeSuggestion({username: 'eve', fullName: 'Eve Brown', followsMe: true, iFollow: true}), + ], + }, +} + +export const FewSuggestions: Story = { + args: { + suggestions: [ + makeSuggestion({username: 'keybase', fullName: 'Keybase'}), + makeSuggestion({username: 'kbnews', fullName: 'Keybase News'}), + ], + }, +} + +export const ManySuggestions: Story = { + args: { + suggestions: [ + makeSuggestion({username: 'alice', fullName: 'Alice Smith'}), + makeSuggestion({username: 'bob', fullName: 'Bob Johnson'}), + makeSuggestion({username: 'carol', fullName: 'Carol White'}), + makeSuggestion({username: 'dave', fullName: 'Dave Davis'}), + makeSuggestion({username: 'eve', fullName: 'Eve Brown'}), + makeSuggestion({username: 'frank', fullName: 'Frank Lee'}), + makeSuggestion({username: 'grace', fullName: 'Grace Park'}), + makeSuggestion({username: 'heidi', fullName: 'Heidi Chan'}), + ], + }, +} + +export const NoFullNames: Story = { + args: { + suggestions: [ + makeSuggestion({username: 'user1'}), + makeSuggestion({username: 'user2'}), + makeSuggestion({username: 'user3'}), + ], + }, +} diff --git a/shared/people/item.stories.tsx b/shared/people/item.stories.tsx new file mode 100644 index 000000000000..4684c3e8dd6e --- /dev/null +++ b/shared/people/item.stories.tsx @@ -0,0 +1,71 @@ +import type {Meta, StoryObj} from '@storybook/react' +import * as Kb from '@/common-adapters' +import PeopleItem from './item' + +const when = new Date('2024-03-15T10:00:00Z') + +const meta: Meta = { + component: PeopleItem, + title: 'People/Item', + args: { + badged: false, + when, + }, +} +export default meta +type Story = StoryObj + +export const BasicText: Story = { + args: { + children: Someone followed you., + badged: false, + when, + }, +} + +export const Badged: Story = { + args: { + children: New activity on your account., + badged: true, + when, + }, +} + +export const WithIcon: Story = { + args: { + children: marcos followed you., + badged: false, + icon: , + when, + }, +} + +export const WithButtons: Story = { + args: { + children: Add your phone number to your account., + badged: true, + icon: , + buttons: [ + {label: 'Add phone', onClick: () => {}}, + {label: 'Skip', mode: 'Secondary' as const, onClick: () => {}}, + ], + when, + }, +} + +export const MultiFormat: Story = { + args: { + children: marcelina, max, and 3 others started following you., + badged: false, + format: 'multi' as const, + when, + }, +} + +export const NoTimestamp: Story = { + args: { + children: A pinned announcement from Keybase., + badged: false, + icon: , + }, +} diff --git a/shared/pinentry/index.desktop.stories.tsx b/shared/pinentry/index.desktop.stories.tsx new file mode 100644 index 000000000000..5909864507e0 --- /dev/null +++ b/shared/pinentry/index.desktop.stories.tsx @@ -0,0 +1,70 @@ +import type {Meta, StoryObj} from '@storybook/react' +import type * as T from '@/constants/types' +import * as RPCGen from '@/constants/rpc/rpc-gen' +import Pinentry from './index.desktop' + +const makeFeature = (overrides: Partial = {}): T.RPCGen.Feature => ({ + allow: true, + defaultValue: false, + label: 'Show typing', + readonly: false, + ...overrides, +}) + +const meta: Meta = { + component: Pinentry, + title: 'Pinentry/Pinentry', + args: { + onCancel: () => {}, + onSubmit: () => {}, + prompt: 'Enter your passphrase', + type: RPCGen.PassphraseType.passPhrase, + }, +} +export default meta +type Story = StoryObj + +export const Passphrase: Story = { + args: { + prompt: 'Enter your Keybase passphrase', + type: RPCGen.PassphraseType.passPhrase, + }, +} + +export const PassphraseWithShowTyping: Story = { + args: { + prompt: 'Enter your Keybase passphrase', + type: RPCGen.PassphraseType.passPhrase, + showTyping: makeFeature({allow: true, defaultValue: false, label: 'Show typing'}), + }, +} + +export const PaperKey: Story = { + args: { + prompt: 'Enter your paper key', + type: RPCGen.PassphraseType.paperKey, + }, +} + +export const PaperKeyWithError: Story = { + args: { + prompt: 'Enter your paper key', + type: RPCGen.PassphraseType.paperKey, + retryLabel: 'Incorrect paper key, please try again', + }, +} + +export const VerifyPassphrase: Story = { + args: { + prompt: 'Verify your passphrase to continue', + type: RPCGen.PassphraseType.verifyPassPhrase, + }, +} + +export const CustomLabels: Story = { + args: { + prompt: 'Unlock your encrypted folder', + type: RPCGen.PassphraseType.passPhrase, + submitLabel: 'Unlock', + }, +} diff --git a/shared/profile/modal.stories.tsx b/shared/profile/modal.stories.tsx new file mode 100644 index 000000000000..7efe220b1f19 --- /dev/null +++ b/shared/profile/modal.stories.tsx @@ -0,0 +1,31 @@ +import type {Meta, StoryObj} from '@storybook/react' +import * as Kb from '@/common-adapters' +import Modal from './modal' + +const meta: Meta = { + component: Modal, + title: 'Profile/Modal', +} +export default meta +type Story = StoryObj + +export const WithCancelButton: Story = { + args: { + children: Modal content goes here., + onCancel: () => {}, + }, +} + +export const WithoutCancelButton: Story = { + args: { + children: Modal content without a cancel button., + }, +} + +export const SkipButton: Story = { + args: { + children: Modal with onCancel but skipButton=true (no button rendered)., + onCancel: () => {}, + skipButton: true, + }, +} diff --git a/shared/profile/platform-icon.stories.tsx b/shared/profile/platform-icon.stories.tsx new file mode 100644 index 000000000000..7c70282ef41a --- /dev/null +++ b/shared/profile/platform-icon.stories.tsx @@ -0,0 +1,66 @@ +import type {Meta, StoryObj} from '@storybook/react' +import PlatformIcon from './platform-icon' + +const meta: Meta = { + component: PlatformIcon, + title: 'Profile/PlatformIcon', +} +export default meta +type Story = StoryObj + +export const Twitter: Story = { + args: { + platform: 'twitter', + overlay: 'iconfont-proof-good', + }, +} + +export const GitHub: Story = { + args: { + platform: 'github', + overlay: 'iconfont-proof-good', + }, +} + +export const Reddit: Story = { + args: { + platform: 'reddit', + overlay: 'iconfont-proof-good', + }, +} + +export const HackerNews: Story = { + args: { + platform: 'hackernews', + overlay: 'iconfont-proof-good', + }, +} + +export const Website: Story = { + args: { + platform: 'web', + overlay: 'iconfont-proof-good', + }, +} + +export const Bitcoin: Story = { + args: { + platform: 'btc', + overlay: 'iconfont-proof-good', + }, +} + +export const PGP: Story = { + args: { + platform: 'pgp', + overlay: 'iconfont-proof-good', + }, +} + +export const BrokenProof: Story = { + args: { + platform: 'twitter', + overlay: 'iconfont-proof-broken', + overlayColor: 'red', + }, +} diff --git a/shared/provision/code-page/qr-image.stories.tsx b/shared/provision/code-page/qr-image.stories.tsx new file mode 100644 index 000000000000..6cdbe33c2e5e --- /dev/null +++ b/shared/provision/code-page/qr-image.stories.tsx @@ -0,0 +1,26 @@ +import type {Meta, StoryObj} from '@storybook/react' +import QrImage from './qr-image' + +const meta: Meta = { + component: QrImage, + title: 'Provision/QrImage', + args: { + code: 'deadbeef cafe babe 1234 5678 abcd efgh ijkl mnop', + }, +} +export default meta +type Story = StoryObj + +export const Default: Story = {} + +export const SmallCells: Story = { + args: { + cellSize: 8, + }, +} + +export const LargeCells: Story = { + args: { + cellSize: 10, + }, +} diff --git a/shared/provision/error.stories.tsx b/shared/provision/error.stories.tsx new file mode 100644 index 000000000000..0debbe1303f7 --- /dev/null +++ b/shared/provision/error.stories.tsx @@ -0,0 +1,75 @@ +import type {Meta, StoryObj} from '@storybook/react' +import * as T from '@/constants/types' +import RenderError from './error' +import type {ProvisionRouteError} from '@/stores/provision' + +const makeError = (overrides: Partial = {}): ProvisionRouteError => ({ + code: T.RPCGen.StatusCode.scgeneric, + desc: 'Something went wrong.', + details: '', + message: 'Generic error', + ...overrides, +}) + +const meta: Meta = { + component: RenderError, + title: 'Provision/Error', +} +export default meta +type Story = StoryObj + +export const NoError: Story = { + args: { + route: {params: {}}, + }, +} + +export const GenericError: Story = { + args: { + route: { + params: { + error: makeError({desc: 'Is the other device using the username you expect? It seems to be different.'}), + }, + }, + }, +} + +export const Offline: Story = { + args: { + route: { + params: { + error: makeError({code: T.RPCGen.StatusCode.scdeviceprovisionoffline}), + }, + }, + }, +} + +export const BadPassword: Story = { + args: { + route: { + params: { + error: makeError({code: T.RPCGen.StatusCode.scbadloginpassword}), + }, + }, + }, +} + +export const NoProvision: Story = { + args: { + route: { + params: { + error: makeError({code: T.RPCGen.StatusCode.scdevicenoprovision}), + }, + }, + }, +} + +export const Deleted: Story = { + args: { + route: { + params: { + error: makeError({code: T.RPCGen.StatusCode.scdeleted}), + }, + }, + }, +} diff --git a/shared/provision/paper-key.stories.tsx b/shared/provision/paper-key.stories.tsx new file mode 100644 index 000000000000..0ae9a48ee8b2 --- /dev/null +++ b/shared/provision/paper-key.stories.tsx @@ -0,0 +1,35 @@ +import type {Meta, StoryObj} from '@storybook/react' +import {PaperKey} from './paper-key' + +const meta: Meta = { + component: PaperKey, + title: 'Provision/PaperKey', + args: { + hint: 'chrisnojima-mac...', + error: '', + waiting: false, + onSubmit: () => {}, + }, +} +export default meta +type Story = StoryObj + +export const Empty: Story = {} + +export const WithHint: Story = { + args: { + hint: 'family laptop...', + }, +} + +export const WithError: Story = { + args: { + error: 'Incorrect paper key. Please try again.', + }, +} + +export const Waiting: Story = { + args: { + waiting: true, + }, +} diff --git a/shared/provision/select-other-device.stories.tsx b/shared/provision/select-other-device.stories.tsx new file mode 100644 index 000000000000..811f5a9c01b1 --- /dev/null +++ b/shared/provision/select-other-device.stories.tsx @@ -0,0 +1,55 @@ +import type {Meta, StoryObj} from '@storybook/react' +import SelectOtherDevice from './select-other-device' +import type {Device} from '@/stores/provision' + +const makeDevice = (overrides: Partial = {}): Device => ({ + deviceNumberOfType: 0, + id: 'device-001' as Device['id'], + name: 'My Device', + type: 'desktop', + ...overrides, +}) + +const meta: Meta = { + component: SelectOtherDevice, + title: 'Provision/SelectOtherDevice', + args: { + onBack: () => {}, + onSelect: () => {}, + onResetAccount: () => {}, + }, +} +export default meta +type Story = StoryObj + +export const MultipleDevices: Story = { + args: { + devices: [ + makeDevice({name: 'work-laptop', type: 'desktop', deviceNumberOfType: 0}), + makeDevice({name: 'iPhone 15', type: 'mobile', deviceNumberOfType: 1, id: 'device-002' as Device['id']}), + makeDevice({name: 'Paper key', type: 'backup', deviceNumberOfType: 0, id: 'device-003' as Device['id']}), + ], + }, +} + +export const SingleDesktop: Story = { + args: { + devices: [makeDevice({name: 'work-laptop', type: 'desktop'})], + }, +} + +export const PasswordRecovery: Story = { + args: { + passwordRecovery: true, + devices: [ + makeDevice({name: 'work-laptop', type: 'desktop'}), + makeDevice({name: 'iPhone 15', type: 'mobile', id: 'device-002' as Device['id']}), + ], + }, +} + +export const Empty: Story = { + args: { + devices: [], + }, +} diff --git a/shared/provision/troubleshooting.stories.tsx b/shared/provision/troubleshooting.stories.tsx new file mode 100644 index 000000000000..d3a59650e7c8 --- /dev/null +++ b/shared/provision/troubleshooting.stories.tsx @@ -0,0 +1,33 @@ +import type {Meta, StoryObj} from '@storybook/react' +import Troubleshooting from './troubleshooting' + +const meta: Meta = { + component: Troubleshooting, + title: 'Provision/Troubleshooting', + args: { + onCancel: () => {}, + }, +} +export default meta +type Story = StoryObj + +export const QRMode: Story = { + args: { + mode: 'QR', + otherDeviceType: 'desktop', + }, +} + +export const TextMode: Story = { + args: { + mode: 'text', + otherDeviceType: 'desktop', + }, +} + +export const OtherDeviceMobile: Story = { + args: { + mode: 'QR', + otherDeviceType: 'mobile', + }, +} diff --git a/shared/router-v2/header/syncing-folders.stories.tsx b/shared/router-v2/header/syncing-folders.stories.tsx new file mode 100644 index 000000000000..877dabef9048 --- /dev/null +++ b/shared/router-v2/header/syncing-folders.stories.tsx @@ -0,0 +1,64 @@ +import type {Meta, StoryObj} from '@storybook/react' +import PieSlice from '@/fs/common/pie-slice' +import * as Kb from '@/common-adapters' + +// SyncingFolders default export wraps store hooks; story the inner pure UI directly. +const SyncingFoldersUI = ({ + progress, + tooltip, + negative, +}: { + progress: number + tooltip: string + negative?: boolean +}) => ( + + + + + Syncing folders... + + + +) + +const meta: Meta = { + component: SyncingFoldersUI, + title: 'RouterV2/SyncingFolders', + args: { + progress: 0.5, + tooltip: '50 MB / 100 MB', + negative: false, + }, +} +export default meta +type Story = StoryObj + +export const HalfwayDone: Story = { + args: { + progress: 0.5, + tooltip: '50 MB / 100 MB', + }, +} + +export const AlmostDone: Story = { + args: { + progress: 0.9, + tooltip: '90 MB / 100 MB', + }, +} + +export const JustStarted: Story = { + args: { + progress: 0.05, + tooltip: '5 MB / 100 MB', + }, +} + +export const NegativeVariant: Story = { + args: { + progress: 0.6, + tooltip: '60 MB / 100 MB', + negative: true, + }, +} diff --git a/shared/scripts/screenshot-storybook.mts b/shared/scripts/screenshot-storybook.mts new file mode 100644 index 000000000000..df781ce493fb --- /dev/null +++ b/shared/scripts/screenshot-storybook.mts @@ -0,0 +1,102 @@ +/** + * Builds storybook statically, serves it, screenshots every story, saves PNGs + * to tests/results/storybook-desktop/. Self-contained — no running dev server needed. + */ +import {chromium} from '@playwright/test' +import {execSync} from 'child_process' +import fs from 'fs' +import http from 'http' +import path from 'path' +import {fileURLToPath, URL as NodeURL} from 'url' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const sharedDir = path.resolve(__dirname, '..') +const buildDir = '/tmp/storybook-static' +const outputDir = path.resolve(__dirname, '../tests/results/storybook-desktop') +const PORT = 6007 +const CONCURRENCY = 6 + +// Build storybook to a static directory +console.log('Building storybook (this compiles everything upfront)...') +execSync(`node_modules/.bin/storybook build --output-dir ${buildDir} --config-dir .storybook`, { + cwd: sharedDir, + stdio: 'inherit', +}) +console.log('Build complete.') + +const MIME: Record = { + '.html': 'text/html', + '.js': 'application/javascript', + '.css': 'text/css', + '.json': 'application/json', + '.png': 'image/png', + '.woff2': 'font/woff2', + '.woff': 'font/woff', + '.ttf': 'font/ttf', + '.ico': 'image/x-icon', + '.svg': 'image/svg+xml', +} +const server = http.createServer((req, res) => { + let urlPath = new NodeURL(req.url ?? '/', `http://localhost`).pathname + if (urlPath === '/') urlPath = '/index.html' + const filePath = path.join(buildDir, urlPath) + try { + const data = fs.readFileSync(filePath) + const ext = path.extname(filePath) + res.writeHead(200, {'Content-Type': MIME[ext] ?? 'application/octet-stream'}) + res.end(data) + } catch { + res.writeHead(404) + res.end('Not found') + } +}) +await new Promise(resolve => server.listen(PORT, resolve)) +const storybookUrl = `http://localhost:${PORT}` +console.log(`Serving static build at ${storybookUrl}`) + +const {entries} = (await (await fetch(`${storybookUrl}/index.json`)).json()) as { + entries: Record +} +const stories = Object.entries(entries).filter(([, e]) => e.type === 'story') +console.log(`Found ${stories.length} stories — concurrency ${CONCURRENCY}`) + +fs.mkdirSync(outputDir, {recursive: true}) + +const executablePath = + process.env['CHROME_PATH'] ?? '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' +const browser = await chromium.launch({executablePath}) + +const queue = [...stories] +let done = 0 + +await Promise.all( + Array.from({length: CONCURRENCY}, async () => { + const page = await browser.newPage({viewport: {width: 1280, height: 800}}) + + while (queue.length) { + const item = queue.shift() + if (!item) break + const [id, {title, name}] = item + try { + await page.goto(`${storybookUrl}/iframe.html?id=${id}&viewMode=story`, { + waitUntil: 'load', + timeout: 10000, + }) + + const storyDir = path.join(outputDir, title.replaceAll('/', '-')) + fs.mkdirSync(storyDir, {recursive: true}) + const file = path.join(storyDir, `${name.replaceAll(/\s+/g, '-')}.png`) + await page.screenshot({path: file, fullPage: true}) + done++ + console.log(` [${done}/${stories.length}] ${title}/${name}`) + } catch (err) { + console.warn(` SKIP ${title}/${name}: ${(err as Error).message.split('\n')[0]}`) + } + } + await page.close() + }) +) + +await browser.close() +server.close() +console.log(`\nDone — ${stories.length} screenshots in ${outputDir}`) diff --git a/shared/settings/group.stories.tsx b/shared/settings/group.stories.tsx new file mode 100644 index 000000000000..d67e5f6bdfca --- /dev/null +++ b/shared/settings/group.stories.tsx @@ -0,0 +1,70 @@ +import type {Meta, StoryObj} from '@storybook/react' +import Group from './group' + +const meta: Meta = { + component: Group, + title: 'Settings/Group', + args: { + allowEdit: true, + groupName: 'email', + onToggle: () => {}, + unsubscribedFromAll: false, + }, +} +export default meta +type Story = StoryObj + +export const WithSettings: Story = { + args: { + title: 'Email notifications', + settings: [ + {description: 'When someone follows me', name: 'follow', subscribed: true}, + {description: 'When someone requests access to a team I manage', name: 'teamRequest', subscribed: false}, + {description: 'New team announcements', name: 'teamAnnounce', subscribed: true}, + ], + }, +} + +export const WithUnsubscribeAll: Story = { + args: { + title: 'Email notifications', + unsub: 'email', + settings: [ + {description: 'When someone follows me', name: 'follow', subscribed: true}, + {description: 'New team announcements', name: 'teamAnnounce', subscribed: true}, + ], + unsubscribedFromAll: false, + onToggleUnsubscribeAll: () => {}, + }, +} + +export const UnsubscribedFromAll: Story = { + args: { + title: 'Email notifications', + unsub: 'email', + settings: [ + {description: 'When someone follows me', name: 'follow', subscribed: false}, + {description: 'New team announcements', name: 'teamAnnounce', subscribed: false}, + ], + unsubscribedFromAll: true, + onToggleUnsubscribeAll: () => {}, + }, +} + +export const Disabled: Story = { + args: { + allowEdit: false, + title: 'Push notifications', + label: 'Enable in your device settings first.', + settings: [ + {description: 'New messages', name: 'chat', subscribed: false}, + {description: 'Someone follows you', name: 'follow', subscribed: false}, + ], + }, +} + +export const NoSettings: Story = { + args: { + title: 'No items group', + }, +} diff --git a/shared/settings/proxy.stories.tsx b/shared/settings/proxy.stories.tsx new file mode 100644 index 000000000000..a9ebf6b8fe60 --- /dev/null +++ b/shared/settings/proxy.stories.tsx @@ -0,0 +1,11 @@ +import type {Meta, StoryObj} from '@storybook/react' +import {ProxySettings} from './proxy' + +const meta: Meta = { + component: ProxySettings, + title: 'Settings/Proxy', +} +export default meta +type Story = StoryObj + +export const Default: Story = {} diff --git a/shared/signup/device-name.stories.tsx b/shared/signup/device-name.stories.tsx new file mode 100644 index 000000000000..373b2adffa4a --- /dev/null +++ b/shared/signup/device-name.stories.tsx @@ -0,0 +1,26 @@ +import type {Meta, StoryObj} from '@storybook/react' +import ConnectedEnterDevicename from './device-name' + +// ConnectedEnterDevicename reads inviteCode/username from route.params and manages +// its own local state. The RPC calls are triggered by user interaction only. +const meta: Meta = { + component: ConnectedEnterDevicename, + title: 'Signup/EnterDevicename', + args: { + route: {params: {}}, + }, +} +export default meta +type Story = StoryObj + +export const Empty: Story = { + args: { + route: {params: {}}, + }, +} + +export const WithUsername: Story = { + args: { + route: {params: {username: 'chrisnojima', inviteCode: 'abc123'}}, + }, +} diff --git a/shared/signup/email.stories.tsx b/shared/signup/email.stories.tsx new file mode 100644 index 000000000000..6541f9e5344b --- /dev/null +++ b/shared/signup/email.stories.tsx @@ -0,0 +1,40 @@ +import type {Meta, StoryObj} from '@storybook/react' +import {EnterEmailBody} from './email' + +const meta: Meta = { + component: EnterEmailBody, + title: 'Signup/EnterEmail', + args: { + onChangeEmail: () => {}, + onContinue: () => {}, + email: '', + searchable: true, + onChangeSearchable: () => {}, + showSearchable: true, + iconType: 'icon-email-add-96', + }, +} +export default meta +type Story = StoryObj + +export const Empty: Story = {} + +export const Filled: Story = { + args: { + email: 'user@example.com', + }, +} + +export const NotSearchable: Story = { + args: { + email: 'private@example.com', + searchable: false, + }, +} + +export const HideSearchable: Story = { + args: { + showSearchable: false, + iconType: 'icon-email-add-64', + }, +} diff --git a/shared/signup/phone-number/index.stories.tsx b/shared/signup/phone-number/index.stories.tsx new file mode 100644 index 000000000000..12d2cacca201 --- /dev/null +++ b/shared/signup/phone-number/index.stories.tsx @@ -0,0 +1,37 @@ +import type {Meta, StoryObj} from '@storybook/react' +import {EnterPhoneNumberBody} from './index' + +const meta: Meta = { + component: EnterPhoneNumberBody, + title: 'Signup/EnterPhoneNumber', + args: { + onChangeNumber: () => {}, + onContinue: () => {}, + searchable: true, + iconType: 'icon-phone-number-add-96', + }, +} +export default meta +type Story = StoryObj + +export const Empty: Story = {} + +export const WithSearchable: Story = { + args: { + onChangeSearchable: () => {}, + searchable: true, + }, +} + +export const NotSearchable: Story = { + args: { + onChangeSearchable: () => {}, + searchable: false, + }, +} + +export const SmallIcon: Story = { + args: { + iconType: 'icon-phone-number-add-64', + }, +} diff --git a/shared/signup/phone-number/verify-body.stories.tsx b/shared/signup/phone-number/verify-body.stories.tsx new file mode 100644 index 000000000000..5937d1d30690 --- /dev/null +++ b/shared/signup/phone-number/verify-body.stories.tsx @@ -0,0 +1,33 @@ +import type {Meta, StoryObj} from '@storybook/react' +import VerifyBody from './verify-body' + +const meta: Meta = { + component: VerifyBody, + title: 'Signup/PhoneNumberVerifyBody', + args: { + onChangeCode: () => {}, + onResend: () => {}, + resendWaiting: false, + }, +} +export default meta +type Story = StoryObj + +export const Empty: Story = { + args: { + code: '', + }, +} + +export const Filled: Story = { + args: { + code: '123456', + }, +} + +export const ResendWaiting: Story = { + args: { + code: '', + resendWaiting: true, + }, +} diff --git a/shared/signup/username.stories.tsx b/shared/signup/username.stories.tsx new file mode 100644 index 000000000000..ec12596996f0 --- /dev/null +++ b/shared/signup/username.stories.tsx @@ -0,0 +1,26 @@ +import type {Meta, StoryObj} from '@storybook/react' +import ConnectedEnterUsername from './username' + +// ConnectedEnterUsername reads inviteCode/username from route.params and manages +// its own local state. RPC calls are triggered by user interaction only. +const meta: Meta = { + component: ConnectedEnterUsername, + title: 'Signup/EnterUsername', + args: { + route: {params: {}}, + }, +} +export default meta +type Story = StoryObj + +export const Empty: Story = { + args: { + route: {params: {}}, + }, +} + +export const Prefilled: Story = { + args: { + route: {params: {username: 'chrisnojima'}}, + }, +} diff --git a/shared/team-building/contact-restricted.stories.tsx b/shared/team-building/contact-restricted.stories.tsx new file mode 100644 index 000000000000..8fa735aba543 --- /dev/null +++ b/shared/team-building/contact-restricted.stories.tsx @@ -0,0 +1,37 @@ +import type {Meta, StoryObj} from '@storybook/react' +import {ContactRestricted} from './contact-restricted' + +const meta: Meta = { + component: ContactRestricted, + title: 'TeamBuilding/ContactRestricted', +} +export default meta +type Story = StoryObj + +export const NewFolder: Story = { + args: { + source: 'newFolder', + usernames: ['restricteduser'], + }, +} + +export const TeamAddAllFailedSolo: Story = { + args: { + source: 'teamAddAllFailed', + usernames: ['restricteduser'], + }, +} + +export const TeamAddAllFailedMultiple: Story = { + args: { + source: 'teamAddAllFailed', + usernames: ['alice', 'bob', 'charlie'], + }, +} + +export const TeamAddSomeFailed: Story = { + args: { + source: 'teamAddSomeFailed', + usernames: ['alice', 'bob'], + }, +} diff --git a/shared/team-building/continue-button.stories.tsx b/shared/team-building/continue-button.stories.tsx new file mode 100644 index 000000000000..af077b0f1bcf --- /dev/null +++ b/shared/team-building/continue-button.stories.tsx @@ -0,0 +1,26 @@ +import type {Meta, StoryObj} from '@storybook/react' +import ContinueButton from './continue-button' + +const meta: Meta = { + component: ContinueButton, + title: 'TeamBuilding/ContinueButton', + args: { + label: 'Continue', + onClick: () => {}, + disabled: false, + }, +} +export default meta +type Story = StoryObj + +export const Enabled: Story = { + args: {label: 'Continue', disabled: false}, +} + +export const Disabled: Story = { + args: {label: 'Continue', disabled: true}, +} + +export const CustomLabel: Story = { + args: {label: 'Add to team', disabled: false}, +} diff --git a/shared/team-building/filtered-service-tab-bar.stories.tsx b/shared/team-building/filtered-service-tab-bar.stories.tsx new file mode 100644 index 000000000000..b49e4ecb6faa --- /dev/null +++ b/shared/team-building/filtered-service-tab-bar.stories.tsx @@ -0,0 +1,35 @@ +import type {Meta, StoryObj} from '@storybook/react' +import type * as T from '@/constants/types' +import {FilteredServiceTabBar} from './filtered-service-tab-bar' + +const meta: Meta = { + component: FilteredServiceTabBar, + title: 'TeamBuilding/FilteredServiceTabBar', + args: { + selectedService: 'keybase', + onChangeService: () => {}, + }, +} +export default meta +type Story = StoryObj + +export const AllServices: Story = {} + +export const KeybaseOnly: Story = { + // Returns null (single keybase service), rendered as empty + args: {filterServices: ['keybase'] as Array}, +} + +export const SocialServicesOnly: Story = { + args: { + filterServices: ['twitter', 'github', 'reddit'] as Array, + selectedService: 'twitter', + }, +} + +export const ContactServicesOnly: Story = { + args: { + filterServices: ['phone', 'email'] as Array, + selectedService: 'phone', + }, +} diff --git a/shared/team-building/go-button.stories.tsx b/shared/team-building/go-button.stories.tsx new file mode 100644 index 000000000000..f9f7c68421dc --- /dev/null +++ b/shared/team-building/go-button.stories.tsx @@ -0,0 +1,21 @@ +import type {Meta, StoryObj} from '@storybook/react' +import GoButton from './go-button' + +const meta: Meta = { + component: GoButton, + title: 'TeamBuilding/GoButton', + args: { + label: 'Start', + onClick: () => {}, + }, +} +export default meta +type Story = StoryObj + +export const LabelStart: Story = { + args: {label: 'Start'}, +} + +export const LabelAdd: Story = { + args: {label: 'Add'}, +} diff --git a/shared/team-building/service-tab-bar.stories.tsx b/shared/team-building/service-tab-bar.stories.tsx new file mode 100644 index 000000000000..59ba692dca5b --- /dev/null +++ b/shared/team-building/service-tab-bar.stories.tsx @@ -0,0 +1,63 @@ +import type {Meta, StoryObj} from '@storybook/react' +import type * as T from '@/constants/types' +import {ServiceTabBar} from './service-tab-bar' + +const allServices: Array = [ + 'keybase', + 'phone', + 'email', + 'twitter', + 'github', + 'reddit', + 'hackernews', +] + +const meta: Meta = { + component: ServiceTabBar, + title: 'TeamBuilding/ServiceTabBar', + args: { + services: allServices, + selectedService: 'keybase', + onChangeService: () => {}, + }, +} +export default meta +type Story = StoryObj + +export const KeybaseSelected: Story = { + args: {selectedService: 'keybase'}, +} + +export const TwitterSelected: Story = { + args: {selectedService: 'twitter'}, +} + +export const PhoneSelected: Story = { + args: {selectedService: 'phone'}, +} + +export const EmailSelected: Story = { + args: {selectedService: 'email'}, +} + +export const FewServices: Story = { + args: { + services: ['keybase', 'twitter', 'github'] as Array, + selectedService: 'keybase', + }, +} + +export const WithServicesShown: Story = { + args: { + services: allServices, + selectedService: 'keybase', + servicesShown: 3, + }, +} + +export const MinimalBorder: Story = { + args: { + selectedService: 'github', + minimalBorder: true, + }, +} diff --git a/shared/team-building/team-box.stories.tsx b/shared/team-building/team-box.stories.tsx new file mode 100644 index 000000000000..9b2bf29e6504 --- /dev/null +++ b/shared/team-building/team-box.stories.tsx @@ -0,0 +1,71 @@ +import type {Meta, StoryObj} from '@storybook/react' +import type * as T from '@/constants/types' +import TeamBox from './team-box' + +const makeUser = ( + username: string, + service: T.TB.ServiceIdWithContact = 'keybase', + prettyName = '' +): T.TB.SelectedUser => ({ + prettyName, + service, + userId: `${username}@${service}`, + username, +}) + +const meta: Meta = { + component: TeamBox, + title: 'TeamBuilding/TeamBox', + args: { + allowPhoneEmail: true, + onChangeText: () => {}, + onEnterKeyDown: () => {}, + onDownArrowKeyDown: () => {}, + onUpArrowKeyDown: () => {}, + onRemove: () => {}, + onFinishTeamBuilding: () => {}, + searchString: '', + teamSoFar: [], + }, +} +export default meta +type Story = StoryObj + +export const Empty: Story = {} + +export const OneKeybaseUser: Story = { + args: { + teamSoFar: [makeUser('chrisnojima')], + }, +} + +export const MixedServices: Story = { + args: { + teamSoFar: [ + makeUser('chrisnojima'), + makeUser('twitterfriend', 'twitter'), + makeUser('15551234567', 'phone'), + makeUser('user@example.com', 'email'), + ], + }, +} + +export const ManyUsers: Story = { + args: { + teamSoFar: [ + makeUser('alice'), + makeUser('bob'), + makeUser('charlie'), + makeUser('david'), + makeUser('eve'), + makeUser('frank'), + ], + }, +} + +export const WithGoButtonLabel: Story = { + args: { + teamSoFar: [makeUser('chrisnojima')], + goButtonLabel: 'Add', + }, +} diff --git a/shared/team-building/user-bubble.stories.tsx b/shared/team-building/user-bubble.stories.tsx new file mode 100644 index 000000000000..aa3ba3404c5e --- /dev/null +++ b/shared/team-building/user-bubble.stories.tsx @@ -0,0 +1,57 @@ +import type {Meta, StoryObj} from '@storybook/react' +import type * as T from '@/constants/types' +import UserBubble from './user-bubble' + +const meta: Meta = { + component: UserBubble, + title: 'TeamBuilding/UserBubble', + args: { + username: 'chrisnojima', + service: 'keybase' as T.TB.ServiceIdWithContact, + tooltip: 'chrisnojima', + onRemove: () => {}, + }, +} +export default meta +type Story = StoryObj + +export const KeybaseUser: Story = { + args: { + username: 'chrisnojima', + service: 'keybase', + tooltip: 'chrisnojima', + }, +} + +export const TwitterUser: Story = { + args: { + username: 'twitteruser', + service: 'twitter' as T.TB.ServiceIdWithContact, + tooltip: 'twitteruser@twitter', + }, +} + +export const GitHubUser: Story = { + args: { + username: 'githubuser', + service: 'github' as T.TB.ServiceIdWithContact, + tooltip: 'githubuser@github', + }, +} + +export const PhoneContact: Story = { + args: { + // E164 format without leading '+' + username: '15551234567', + service: 'phone' as T.TB.ServiceIdWithContact, + tooltip: '+1 (555) 123-4567', + }, +} + +export const EmailContact: Story = { + args: { + username: 'user@example.com', + service: 'email' as T.TB.ServiceIdWithContact, + tooltip: 'user@example.com', + }, +} diff --git a/shared/teams/new-team/index.stories.tsx b/shared/teams/new-team/index.stories.tsx new file mode 100644 index 000000000000..9e3708aa165c --- /dev/null +++ b/shared/teams/new-team/index.stories.tsx @@ -0,0 +1,23 @@ +import type {Meta, StoryObj} from '@storybook/react' +import {CreateNewTeam} from './index' + +const meta: Meta = { + component: CreateNewTeam, + title: 'Teams/CreateNewTeam', + args: { + onCancel: () => {}, + onSubmit: () => {}, + }, +} +export default meta +type Story = StoryObj + +export const NewTopLevelTeam: Story = {} + +export const NewSubteam: Story = { + args: {baseTeam: 'keybase'}, +} + +export const NewDeepSubteam: Story = { + args: {baseTeam: 'keybase.design'}, +} diff --git a/shared/teams/role-button.stories.tsx b/shared/teams/role-button.stories.tsx new file mode 100644 index 000000000000..5aedc55cd1ee --- /dev/null +++ b/shared/teams/role-button.stories.tsx @@ -0,0 +1,37 @@ +import type {Meta, StoryObj} from '@storybook/react' +import RoleButton from './role-button' + +const meta: Meta = { + component: RoleButton, + title: 'Teams/RoleButton', + args: { + selectedRole: 'reader', + onClick: () => {}, + }, +} +export default meta +type Story = StoryObj + +export const Reader: Story = { + args: {selectedRole: 'reader'}, +} + +export const Writer: Story = { + args: {selectedRole: 'writer'}, +} + +export const Admin: Story = { + args: {selectedRole: 'admin'}, +} + +export const Owner: Story = { + args: {selectedRole: 'owner'}, +} + +export const Bot: Story = { + args: {selectedRole: 'bot'}, +} + +export const Loading: Story = { + args: {selectedRole: 'admin', loading: true}, +} diff --git a/shared/teams/role-picker.stories.tsx b/shared/teams/role-picker.stories.tsx new file mode 100644 index 000000000000..7b6bb744a3a2 --- /dev/null +++ b/shared/teams/role-picker.stories.tsx @@ -0,0 +1,55 @@ +import type {Meta, StoryObj} from '@storybook/react' +import {FloatingRolePicker} from './role-picker' + +// FloatingRolePicker wraps RolePicker (which is not directly exported). +// Stories use open={true} to render the picker content visibly. + +const meta: Meta = { + component: FloatingRolePicker, + title: 'Teams/RolePicker', + args: { + onConfirm: () => {}, + open: true, + }, +} +export default meta +type Story = StoryObj + +export const DefaultReader: Story = { + args: {presetRole: 'reader'}, +} + +export const PresetWriter: Story = { + args: {presetRole: 'writer'}, +} + +export const PresetAdmin: Story = { + args: {presetRole: 'admin'}, +} + +export const PresetOwner: Story = { + args: {presetRole: 'owner'}, +} + +export const WithCancelButton: Story = { + args: {presetRole: 'reader', onCancel: () => {}}, +} + +export const WithSetIndividually: Story = { + args: {presetRole: 'reader', includeSetIndividually: true, onCancel: () => {}}, +} + +export const PluralRoles: Story = { + args: {presetRole: 'writer', plural: true}, +} + +export const DisabledOwner: Story = { + args: { + presetRole: 'reader', + disabledRoles: {owner: 'Cannot invite an owner via email.'}, + }, +} + +export const Waiting: Story = { + args: {presetRole: 'writer', waiting: true}, +} diff --git a/shared/tracker/bio.stories.tsx b/shared/tracker/bio.stories.tsx new file mode 100644 index 000000000000..3cad69c1cfdd --- /dev/null +++ b/shared/tracker/bio.stories.tsx @@ -0,0 +1,122 @@ +import type {Meta, StoryObj} from '@storybook/react' +import Bio from './bio' + +const meta: Meta = { + component: Bio, + title: 'Tracker/Bio', + args: { + blocked: false, + followThem: false, + followsYou: false, + hidFromFollowers: false, + inTracker: false, + username: 'alice', + }, +} +export default meta +type Story = StoryObj + +export const Basic: Story = { + args: { + username: 'alice', + fullname: 'Alice Smith', + bio: 'Software engineer and open source enthusiast.', + location: 'San Francisco, CA', + followersCount: 142, + followingCount: 37, + followThem: false, + followsYou: false, + }, +} + +export const FollowsYou: Story = { + args: { + username: 'bob', + fullname: 'Bob Johnson', + bio: 'Cryptographer and privacy advocate.', + followersCount: 500, + followingCount: 120, + followThem: false, + followsYou: true, + }, +} + +export const FollowEachOther: Story = { + args: { + username: 'carol', + fullname: 'Carol White', + bio: 'Building secure communications for everyone.', + location: 'New York, NY', + followersCount: 88, + followingCount: 55, + followThem: true, + followsYou: true, + }, +} + +export const YouFollowThem: Story = { + args: { + username: 'dave', + fullname: 'Dave Davis', + bio: 'Open source contributor.', + followersCount: 210, + followingCount: 90, + followThem: true, + followsYou: false, + }, +} + +export const InTracker: Story = { + args: { + username: 'eve', + fullname: 'Eve Brown', + bio: 'This is a longer bio that will get clamped when displayed inside the tracker popup because inTracker is true and lineClamp kicks in.', + location: 'Austin, TX — a long location that gets truncated too', + followersCount: 1024, + followingCount: 256, + followThem: false, + followsYou: true, + inTracker: true, + }, +} + +export const Blocked: Story = { + args: { + username: 'badactor', + fullname: 'Bad Actor', + blocked: true, + followThem: false, + followsYou: false, + }, +} + +export const HidFromFollowers: Story = { + args: { + username: 'quietuser', + fullname: 'Quiet User', + bio: 'Prefers to stay off the radar.', + hidFromFollowers: true, + followThem: false, + followsYou: false, + }, +} + +export const NoBioOrLocation: Story = { + args: { + username: 'minimal', + fullname: 'Minimal User', + followersCount: 0, + followingCount: 0, + followThem: false, + followsYou: false, + }, +} + +export const WithSbsDescription: Story = { + args: { + username: '', + sbsDescription: 'frank@proton.me on ProtonMail', + followThem: false, + followsYou: false, + }, +} diff --git a/shared/unlock-folders/device-list.desktop.stories.tsx b/shared/unlock-folders/device-list.desktop.stories.tsx new file mode 100644 index 000000000000..61b9fc68b787 --- /dev/null +++ b/shared/unlock-folders/device-list.desktop.stories.tsx @@ -0,0 +1,61 @@ +import type {Meta, StoryObj} from '@storybook/react' +import type {UnlockFolderDevice} from './store' +import DeviceList from './device-list.desktop' + +const makeDevice = (overrides: Partial = {}): UnlockFolderDevice => ({ + deviceID: 'device-001' as UnlockFolderDevice['deviceID'], + name: 'My Device', + type: 'desktop', + ...overrides, +}) + +const meta: Meta = { + component: DeviceList, + title: 'UnlockFolders/DeviceList', + args: { + toPaperKeyInput: () => {}, + }, +} +export default meta +type Story = StoryObj + +export const SingleDesktop: Story = { + args: { + devices: [makeDevice({name: 'work-laptop', type: 'desktop'})], + }, +} + +export const MultipleDevices: Story = { + args: { + devices: [ + makeDevice({deviceID: 'd1' as UnlockFolderDevice['deviceID'], name: 'work-laptop', type: 'desktop'}), + makeDevice({deviceID: 'd2' as UnlockFolderDevice['deviceID'], name: 'iPhone 15', type: 'mobile'}), + makeDevice({deviceID: 'd3' as UnlockFolderDevice['deviceID'], name: 'Paper key', type: 'backup'}), + ], + }, +} + +export const MobileOnly: Story = { + args: { + devices: [makeDevice({name: 'iPhone 15', type: 'mobile'})], + }, +} + +export const PaperKeyOnly: Story = { + args: { + devices: [makeDevice({name: 'My paper key backup', type: 'backup'})], + }, +} + +export const ManyDevices: Story = { + args: { + devices: [ + makeDevice({deviceID: 'd1' as UnlockFolderDevice['deviceID'], name: 'home-desktop', type: 'desktop'}), + makeDevice({deviceID: 'd2' as UnlockFolderDevice['deviceID'], name: 'work-laptop', type: 'desktop'}), + makeDevice({deviceID: 'd3' as UnlockFolderDevice['deviceID'], name: 'iPhone 15', type: 'mobile'}), + makeDevice({deviceID: 'd4' as UnlockFolderDevice['deviceID'], name: 'iPad Pro', type: 'mobile'}), + makeDevice({deviceID: 'd5' as UnlockFolderDevice['deviceID'], name: 'Paper key alpha', type: 'backup'}), + makeDevice({deviceID: 'd6' as UnlockFolderDevice['deviceID'], name: 'Paper key beta', type: 'backup'}), + ], + }, +} diff --git a/shared/unlock-folders/paper-key-input.desktop.stories.tsx b/shared/unlock-folders/paper-key-input.desktop.stories.tsx new file mode 100644 index 000000000000..adf44b893eaf --- /dev/null +++ b/shared/unlock-folders/paper-key-input.desktop.stories.tsx @@ -0,0 +1,28 @@ +import type {Meta, StoryObj} from '@storybook/react' +import PaperKeyInput from './paper-key-input.desktop' + +const meta: Meta = { + component: PaperKeyInput, + title: 'UnlockFolders/PaperKeyInput', + args: { + onBack: () => {}, + onContinue: () => {}, + waiting: false, + }, +} +export default meta +type Story = StoryObj + +export const Default: Story = {} + +export const WithError: Story = { + args: { + paperkeyError: 'Incorrect paper key. Please try again.', + }, +} + +export const Waiting: Story = { + args: { + waiting: true, + }, +} diff --git a/shared/unlock-folders/success.desktop.stories.tsx b/shared/unlock-folders/success.desktop.stories.tsx new file mode 100644 index 000000000000..42dad673212d --- /dev/null +++ b/shared/unlock-folders/success.desktop.stories.tsx @@ -0,0 +1,14 @@ +import type {Meta, StoryObj} from '@storybook/react' +import Success from './success.desktop' + +const meta: Meta = { + component: Success, + title: 'UnlockFolders/Success', + args: { + onClose: () => {}, + }, +} +export default meta +type Story = StoryObj + +export const Default: Story = {} diff --git a/shared/wallets/markdown-memo.stories.tsx b/shared/wallets/markdown-memo.stories.tsx new file mode 100644 index 000000000000..ac1da56dc0d5 --- /dev/null +++ b/shared/wallets/markdown-memo.stories.tsx @@ -0,0 +1,39 @@ +import type {Meta, StoryObj} from '@storybook/react' +import MarkdownMemo from './markdown-memo' + +const meta: Meta = { + component: MarkdownMemo, + title: 'Wallets/MarkdownMemo', +} +export default meta +type Story = StoryObj + +export const PlainText: Story = { + args: {memo: 'Payment for coffee'}, +} + +export const LongText: Story = { + args: { + memo: 'This is a longer payment memo that goes into more detail about the transaction and why it was made.', + }, +} + +export const WithMarkdownBold: Story = { + args: {memo: 'Payment for **coffee** and *snacks*'}, +} + +export const WithMarkdownLink: Story = { + args: {memo: 'See invoice at https://example.com/invoice/123'}, +} + +export const EmptyMemo: Story = { + // renders null when memo is empty + args: {memo: ''}, +} + +export const HideDivider: Story = { + args: { + memo: 'Payment without divider', + hideDivider: true, + }, +} diff --git a/shared/wallets/remove-account.stories.tsx b/shared/wallets/remove-account.stories.tsx new file mode 100644 index 000000000000..16e20d733ae2 --- /dev/null +++ b/shared/wallets/remove-account.stories.tsx @@ -0,0 +1,33 @@ +import type {Meta, StoryObj} from '@storybook/react' +import RemoveAccount from './remove-account' + +const meta: Meta = { + component: RemoveAccount, + title: 'Wallets/RemoveAccount', +} +export default meta +type Story = StoryObj + +export const WithBalance: Story = { + args: { + accountID: 'GABC1234', + name: 'My Savings', + balanceDescription: '125.0000000 XLM', + }, +} + +export const ZeroBalance: Story = { + args: { + accountID: 'GABC5678', + name: 'Empty Wallet', + balanceDescription: '0.0000000 XLM', + }, +} + +export const LongName: Story = { + args: { + accountID: 'GABC9999', + name: 'My Very Long Wallet Account Name That Might Wrap', + balanceDescription: '9999.9999999 XLM', + }, +} diff --git a/shared/wallets/wallet-popup.stories.tsx b/shared/wallets/wallet-popup.stories.tsx new file mode 100644 index 000000000000..9c61cb3079a7 --- /dev/null +++ b/shared/wallets/wallet-popup.stories.tsx @@ -0,0 +1,39 @@ +import type {Meta, StoryObj} from '@storybook/react' +import * as Kb from '@/common-adapters' +import WalletPopup from './wallet-popup' + +const meta: Meta = { + component: WalletPopup, + title: 'Wallets/WalletPopup', + args: { + children: ( + + Wallet content here + Some descriptive text about this wallet action. + + ), + }, +} +export default meta +type Story = StoryObj + +export const NoButtons: Story = {} + +export const WithButtons: Story = { + args: { + bottomButtons: [ + {}} />, + {}} />, + ], + }, +} + +export const ColumnButtons: Story = { + args: { + buttonBarDirection: 'column', + bottomButtons: [ + {}} />, + {}} />, + ], + }, +} From a484242cd3cea6a9e202d39676aca1501955933e Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Tue, 2 Jun 2026 09:07:59 -0400 Subject: [PATCH 04/12] Add slideshow and lazy loading to e2e HTML reports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `loading="lazy"` on all report images - Slideshow button in header: opens full-screen modal, cycles images at 0.75s with CSS fade-in - Controls bar: prev/next/pause buttons + counter (e.g. 5/233) - Keyboard: ←/→ navigate, Space toggles play/pause, Esc closes --- shared/tests/e2e/generate-electron-report.mts | 12 +-- shared/tests/e2e/generate-ios-report.mts | 96 +++++++++++++++++-- 2 files changed, 95 insertions(+), 13 deletions(-) diff --git a/shared/tests/e2e/generate-electron-report.mts b/shared/tests/e2e/generate-electron-report.mts index 4dca7b162270..54bdc2fffe76 100644 --- a/shared/tests/e2e/generate-electron-report.mts +++ b/shared/tests/e2e/generate-electron-report.mts @@ -180,14 +180,14 @@ function buildHtml(cases: TestCase[], storybookCases: StorybookCase[], timestamp let visual: string if (c.screenshotPath && c.prevScreenshotPath) { visual = `
- current - baseline + current + baseline
BASELINE
NOW
` } else if (c.screenshotPath) { - visual = `
${c.label}
` + visual = `
${c.label}
` } else { visual = `
No screenshot
` } @@ -207,14 +207,14 @@ function buildHtml(cases: TestCase[], storybookCases: StorybookCase[], timestamp let visual: string if (c.prevScreenshotPath) { visual = `
- current - baseline + current + baseline
BASELINE
NOW
` } else { - visual = `
${escapeHtml(c.label)}
` + visual = `
${escapeHtml(c.label)}
` } return `
diff --git a/shared/tests/e2e/generate-ios-report.mts b/shared/tests/e2e/generate-ios-report.mts index 452499a8342b..67568dc0f08b 100644 --- a/shared/tests/e2e/generate-ios-report.mts +++ b/shared/tests/e2e/generate-ios-report.mts @@ -161,16 +161,16 @@ function buildHtml(results: ScreenshotResult[], timestamp: string, title: string let visual: string if (r.screenshotPath && r.prevScreenshotPath) { visual = `
- current - baseline + current + baseline
BASELINE
NOW
` } else if (r.screenshotPath) { - visual = `
${r.name}
` + visual = `
${r.name}
` } else if (r.failureScreenshotPath) { - visual = `
failure
` + visual = `
failure
` } else { visual = `
No screenshot
` } @@ -207,7 +207,7 @@ ${sharedCss(allPassed)}
-

${title}

+

${title}

${passed} passed · ${failed} failed · ${total} total${hasDiff ? ' · vs baseline' : ''}${timestamp}
${cards}
@@ -220,7 +220,11 @@ export function sharedCss(allPassed: boolean): string { return `*{box-sizing:border-box;margin:0;padding:0} body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#f0f0f0;color:#222} header{background:${allPassed ? '#1a7a3a' : '#c0392b'};color:#fff;padding:20px 28px} +.hdr-top{display:flex;align-items:center;gap:12px} h1{font-size:20px;font-weight:600} +#slideshow-btn{background:rgba(255,255,255,.2);border:1px solid rgba(255,255,255,.4);color:#fff;border-radius:5px;padding:4px 10px;font-size:14px;cursor:pointer;line-height:1} +#slideshow-btn:hover{background:rgba(255,255,255,.35)} +#slideshow-btn.active{background:rgba(255,255,255,.35)} .meta{margin-top:6px;font-size:13px;opacity:.85;display:flex;gap:16px;align-items:center} .ts{opacity:.7} .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:16px;padding:20px 28px} @@ -259,7 +263,12 @@ h1{font-size:20px;font-weight:600} .ov-compare .grip{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:#fff;border-radius:50%;width:30px;height:30px;display:flex;align-items:center;justify-content:center;font-size:13px;box-shadow:0 1px 6px rgba(0,0,0,.5)} .ov-compare .lbl{position:absolute;bottom:10px;font-size:11px;font-weight:700;letter-spacing:.05em;padding:3px 9px;border-radius:3px;background:rgba(0,0,0,.55);color:#fff;pointer-events:none} .ov-compare .lbl-l{left:10px}.ov-compare .lbl-r{right:10px} -.ov-solo{display:block;max-height:90vh;max-width:90vw;width:auto;height:auto}` +@keyframes ov-fadein{from{opacity:0}to{opacity:1}} +.ov-solo{display:block;max-height:90vh;max-width:90vw;width:auto;height:auto;animation:ov-fadein .25s ease} +.ov-controls{position:fixed;bottom:20px;left:50%;transform:translateX(-50%);z-index:1002;display:flex;align-items:center;gap:8px;background:rgba(0,0,0,.55);border-radius:24px;padding:6px 14px} +.ov-controls button{background:none;border:none;color:#fff;font-size:18px;line-height:1;cursor:pointer;padding:2px 6px;border-radius:4px;opacity:.85} +.ov-controls button:hover{opacity:1;background:rgba(255,255,255,.15)} +.ov-counter{color:rgba(255,255,255,.8);font-size:12px;font-weight:600;letter-spacing:.04em;min-width:52px;text-align:center}` } export function sliderScript(): string { @@ -286,7 +295,17 @@ document.body.appendChild(overlay) const ovInner = overlay.querySelector('.ov-inner') const ovClose = overlay.querySelector('.ov-close') -function closeOverlay() { overlay.classList.remove('open') } +const ovControls = document.createElement('div') +ovControls.className = 'ov-controls' +ovControls.innerHTML = '' +ovControls.style.display = 'none' +document.body.appendChild(ovControls) +const ovPrev = ovControls.querySelector('.ov-prev') +const ovPlayPause = ovControls.querySelector('.ov-playpause') +const ovNext = ovControls.querySelector('.ov-next') +const ovCounter = ovControls.querySelector('.ov-counter') + +function closeOverlay() { overlay.classList.remove('open'); stopSlideshow() } ovClose.addEventListener('click', closeOverlay) overlay.addEventListener('click', e => { if (e.target === overlay) closeOverlay() }) document.addEventListener('keydown', e => { if (e.key === 'Escape') closeOverlay() }) @@ -334,6 +353,69 @@ document.querySelectorAll('.solo-wrap').forEach(el => { openSolo(el.querySelector('img').src) }) }) + +// ── slideshow ──────────────────────────────────────────────────────────────── +let slideshowTimer = null +let slideshowPlaying = false +let slideshowIdx = 0 +let slideshowImgs = [] +const slideshowBtn = document.getElementById('slideshow-btn') + +function slideshowImages() { + return Array.from(document.querySelectorAll('.grid .card')).flatMap(card => { + const img = card.querySelector('.img-after') ?? card.querySelector('img.solo') + return img ? [img.src] : [] + }) +} + +function showSlide(idx) { + if (!slideshowImgs.length) return + slideshowIdx = ((idx % slideshowImgs.length) + slideshowImgs.length) % slideshowImgs.length + openSolo(slideshowImgs[slideshowIdx]) + ovCounter.textContent = \`\${slideshowIdx + 1}/\${slideshowImgs.length}\` +} + +function scheduleNext() { + clearTimeout(slideshowTimer) + if (slideshowPlaying) slideshowTimer = setTimeout(() => { showSlide(slideshowIdx + 1); scheduleNext() }, 750) +} + +function setSlideshowPlaying(playing) { + slideshowPlaying = playing + ovPlayPause.textContent = playing ? '⏸' : '▶' + slideshowBtn.textContent = playing ? '⏸' : '▶' + slideshowBtn.classList.toggle('active', playing) + if (playing) scheduleNext() + else clearTimeout(slideshowTimer) +} + +function stopSlideshow() { + setSlideshowPlaying(false) + ovControls.style.display = 'none' +} + +ovPrev.addEventListener('click', e => { e.stopPropagation(); showSlide(slideshowIdx - 1); scheduleNext() }) +ovNext.addEventListener('click', e => { e.stopPropagation(); showSlide(slideshowIdx + 1); scheduleNext() }) +ovPlayPause.addEventListener('click', e => { e.stopPropagation(); setSlideshowPlaying(!slideshowPlaying) }) +document.addEventListener('keydown', e => { + if (!overlay.classList.contains('open') || !ovControls.style.display || ovControls.style.display === 'none') return + if (e.key === 'ArrowLeft') { e.preventDefault(); showSlide(slideshowIdx - 1); scheduleNext() } + else if (e.key === 'ArrowRight') { e.preventDefault(); showSlide(slideshowIdx + 1); scheduleNext() } + else if (e.key === ' ') { e.preventDefault(); setSlideshowPlaying(!slideshowPlaying) } +}) + +slideshowBtn.addEventListener('click', () => { + if (ovControls.style.display !== 'none') { + stopSlideshow() + closeOverlay() + } else { + slideshowImgs = slideshowImages() + if (!slideshowImgs.length) return + ovControls.style.display = 'flex' + showSlide(0) + setSlideshowPlaying(true) + } +}) ` } From 5d8d15f85fe686de435ef0d4a18462684b6b90cf Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Tue, 2 Jun 2026 11:37:31 -0400 Subject: [PATCH 05/12] Fix dark mode in storybook screenshots and e2e tests Use page.emulateMedia({colorScheme}) to set prefers-color-scheme via CDP instead of document.documentElement.style.colorScheme. This activates @media (prefers-color-scheme: dark) CSS rules that the real app depends on. - screenshot-storybook.mts: emulateMedia before each light/dark capture - fixtures.ts: emulateMedia replaces setNativeTheme for dark e2e project - playwright.config.ts: add electron-flows-dark project --- shared/.storybook/preview-head.html | 3 +- shared/.storybook/preview.ts | 17 +++- shared/scripts/screenshot-storybook.mts | 23 ++++-- shared/tests/e2e/electron/helpers/fixtures.ts | 10 ++- .../tests/e2e/electron/playwright.config.ts | 1 + shared/tests/e2e/generate-electron-report.mts | 81 ++++++++++--------- shared/tests/e2e/generate-ios-report.mts | 46 ++++++++++- 7 files changed, 129 insertions(+), 52 deletions(-) diff --git a/shared/.storybook/preview-head.html b/shared/.storybook/preview-head.html index b4c061fd2f70..8f9f655741be 100644 --- a/shared/.storybook/preview-head.html +++ b/shared/.storybook/preview-head.html @@ -33,11 +33,12 @@ --size-xxtiny: 2px; --size-xtiny: 4px; --size-tiny: 8px; --size-xsmall: 12px; --size-small: 16px; --size-medium: 24px; --size-mediumLarge: 32px; --size-large: 40px; --size-xlarge: 64px; --size-border-radius: 4px; + color-scheme: light dark; } html { box-sizing: border-box; cursor: default; user-select: none; height: 100%; width: 100%; } *, *:before, *:after { box-sizing: inherit; } - body { height: 100%; width: 100%; background-color: #ffffff; } + body { height: 100%; width: 100%; background-color: light-dark(#ffffff, #191919); } .clickable { cursor: pointer; } .clickable-box2 { cursor: pointer; } diff --git a/shared/.storybook/preview.ts b/shared/.storybook/preview.ts index 81cd43c76d25..242dfe1a408e 100644 --- a/shared/.storybook/preview.ts +++ b/shared/.storybook/preview.ts @@ -2,6 +2,7 @@ import React from 'react' import type {Preview} from '@storybook/react' import type {KB2} from '../util/electron' import {initDesktopStyles} from '@/styles' +import {useDarkModeState} from '@/stores/darkmode' // Inject a minimal KB2 stub so util/electron.tsx's getStashed() doesn't throw. // The real app sets this from the Electron preload script; storybook sets it here. @@ -47,10 +48,22 @@ const kb2Stub: KB2 = { initDesktopStyles() +export const globalTypes = { + darkMode: { + defaultValue: false, + }, +} + const preview: Preview = { decorators: [ - Story => - React.createElement('div', {style: {background: '#ffffff', padding: 16}}, React.createElement(Story)), + (Story, context) => { + const dark = !!context.globals['darkMode'] + // false = don't write to config (no RPC available in storybook) + useDarkModeState.getState().dispatch.setDarkModePreference(dark ? 'alwaysDark' : 'alwaysLight', false) + // Required for light-dark() CSS vars to resolve correctly + document.documentElement.style.colorScheme = dark ? 'dark' : 'light' + return React.createElement('div', {style: {background: 'var(--color-white)', padding: 16}}, React.createElement(Story)) + }, ], parameters: { layout: 'fullscreen', diff --git a/shared/scripts/screenshot-storybook.mts b/shared/scripts/screenshot-storybook.mts index df781ce493fb..1483d837eac2 100644 --- a/shared/scripts/screenshot-storybook.mts +++ b/shared/scripts/screenshot-storybook.mts @@ -68,6 +68,7 @@ const browser = await chromium.launch({executablePath}) const queue = [...stories] let done = 0 +const total = stories.length * 2 await Promise.all( Array.from({length: CONCURRENCY}, async () => { @@ -78,17 +79,27 @@ await Promise.all( if (!item) break const [id, {title, name}] = item try { + const storyDir = path.join(outputDir, title.replaceAll('/', '-')) + fs.mkdirSync(storyDir, {recursive: true}) + const slug = name.replaceAll(/\s+/g, '-') + + await page.emulateMedia({colorScheme: 'light'}) await page.goto(`${storybookUrl}/iframe.html?id=${id}&viewMode=story`, { waitUntil: 'load', timeout: 10000, }) + await page.screenshot({path: path.join(storyDir, `${slug}.png`), fullPage: true}) + done++ + console.log(` [${done}/${total}] ${title}/${name} (light)`) - const storyDir = path.join(outputDir, title.replaceAll('/', '-')) - fs.mkdirSync(storyDir, {recursive: true}) - const file = path.join(storyDir, `${name.replaceAll(/\s+/g, '-')}.png`) - await page.screenshot({path: file, fullPage: true}) + await page.emulateMedia({colorScheme: 'dark'}) + await page.goto(`${storybookUrl}/iframe.html?id=${id}&viewMode=story&globals=darkMode:true`, { + waitUntil: 'load', + timeout: 10000, + }) + await page.screenshot({path: path.join(storyDir, `${slug}-dark.png`), fullPage: true}) done++ - console.log(` [${done}/${stories.length}] ${title}/${name}`) + console.log(` [${done}/${total}] ${title}/${name} (dark)`) } catch (err) { console.warn(` SKIP ${title}/${name}: ${(err as Error).message.split('\n')[0]}`) } @@ -99,4 +110,4 @@ await Promise.all( await browser.close() server.close() -console.log(`\nDone — ${stories.length} screenshots in ${outputDir}`) +console.log(`\nDone — ${total} screenshots in ${outputDir}`) diff --git a/shared/tests/e2e/electron/helpers/fixtures.ts b/shared/tests/e2e/electron/helpers/fixtures.ts index b305494a3ad8..26a42001b3b0 100644 --- a/shared/tests/e2e/electron/helpers/fixtures.ts +++ b/shared/tests/e2e/electron/helpers/fixtures.ts @@ -1,4 +1,4 @@ -import {test as base, type Page} from '@playwright/test' +import {test as base, type Page, type WorkerInfo} from '@playwright/test' import {connectToElectron} from './connect' import {NAV_TAB_CHAT} from '@/tests/e2e/shared/test-ids' @@ -9,12 +9,16 @@ export const test = base.extend<{page: Page}, WorkerFixtures>({ // Playwright requires object destructuring syntax here — it uses static analysis to // detect fixture dependencies, so a plain identifier like `_fixtures` breaks injection. // eslint-disable-next-line no-empty-pattern - async ({}, setup) => { + async ({}, setup, workerInfo: WorkerInfo) => { + const isDark = workerInfo.project.name.endsWith('-dark') const {page} = await connectToElectron() - // Reload to clear any in-memory state left over from previous test runs + // emulateMedia sets prefers-color-scheme via CDP and persists across reloads + await page.emulateMedia({colorScheme: isDark ? 'dark' : 'light'}) + // Reload to clear in-memory state and apply the new color scheme await page.reload() await page.getByTestId(NAV_TAB_CHAT).waitFor({timeout: 30_000}) await setup(page) + await page.emulateMedia({colorScheme: null}) // Do NOT close — that kills the Electron process }, {scope: 'worker'}, diff --git a/shared/tests/e2e/electron/playwright.config.ts b/shared/tests/e2e/electron/playwright.config.ts index 1b33966ac9d3..d360d19b20cf 100644 --- a/shared/tests/e2e/electron/playwright.config.ts +++ b/shared/tests/e2e/electron/playwright.config.ts @@ -18,5 +18,6 @@ export default defineConfig({ }, projects: [ {name: 'electron-flows', testMatch: 'flows/**/*.test.ts'}, + {name: 'electron-flows-dark', testMatch: 'flows/**/*.test.ts'}, ], }) diff --git a/shared/tests/e2e/generate-electron-report.mts b/shared/tests/e2e/generate-electron-report.mts index 54bdc2fffe76..2f2d9925ff63 100644 --- a/shared/tests/e2e/generate-electron-report.mts +++ b/shared/tests/e2e/generate-electron-report.mts @@ -74,46 +74,51 @@ function parseReport(report: Report): TestCase[] { const cases: TestCase[] = [] for (const suite of report.suites) { for (const {suiteName, spec} of flattenSpecs(suite)) { - const key = `${slugify(suiteName)}-${slugify(spec.title)}` - // take the last non-skipped result (handles retries) - const allResults = spec.tests.flatMap(t => t.results) - const result = allResults.filter(r => r.status !== 'skipped').at(-1) ?? allResults.at(-1) - if (!result) continue - - const passed = spec.ok - const durationMs = result.duration - const errorMessage = !passed - ? (result.errors?.[0]?.message?.split('\n')[0] ?? 'test failed') - : null - - const screenshotAtt = result.attachments.find(a => a.name === 'screenshot' && a.contentType === 'image/png') - let screenshotPath: string | null = null - if (screenshotAtt) { - const buf = screenshotAtt.body - ? Buffer.from(screenshotAtt.body, 'base64') - : screenshotAtt.path ? fs.readFileSync(screenshotAtt.path) : null - if (buf) { - fs.mkdirSync(debugDir, {recursive: true}) - screenshotPath = path.join(debugDir, `${key}.png`) - fs.writeFileSync(screenshotPath, buf) - } + const baseKey = `${slugify(suiteName)}-${slugify(spec.title)}` + + // Group by project so light and dark produce separate TestCase entries + const byProject = new Map() + for (const t of spec.tests) { + const proj = t.projectName ?? '' + if (!byProject.has(proj)) byProject.set(proj, []) + byProject.get(proj)!.push(t) } - const prevPath = path.join(prevDir, `${key}.png`) - const prevScreenshotPath = fs.existsSync(prevPath) ? prevPath : null - - const diff = screenshotPath && prevScreenshotPath ? computeDiff(screenshotPath, prevScreenshotPath) : null - - cases.push({ - key, - label: `${suiteName} · ${spec.title}`, - passed, - durationMs, - screenshotPath, - prevScreenshotPath, - diff, - errorMessage, - }) + for (const [projectName, tests] of byProject) { + const isDark = projectName.endsWith('-dark') + const key = isDark ? `${baseKey}-dark` : baseKey + const label = isDark ? `${suiteName} · ${spec.title} (dark)` : `${suiteName} · ${spec.title}` + + // take the last non-skipped result (handles retries) + const allResults = tests.flatMap(t => t.results) + const result = allResults.filter(r => r.status !== 'skipped').at(-1) ?? allResults.at(-1) + if (!result) continue + + const passed = tests.every(t => t.status === 'expected') + const durationMs = result.duration + const errorMessage = !passed + ? (result.errors?.[0]?.message?.split('\n')[0] ?? 'test failed') + : null + + const screenshotAtt = result.attachments.find(a => a.name === 'screenshot' && a.contentType === 'image/png') + let screenshotPath: string | null = null + if (screenshotAtt) { + const buf = screenshotAtt.body + ? Buffer.from(screenshotAtt.body, 'base64') + : screenshotAtt.path ? fs.readFileSync(screenshotAtt.path) : null + if (buf) { + fs.mkdirSync(debugDir, {recursive: true}) + screenshotPath = path.join(debugDir, `${key}.png`) + fs.writeFileSync(screenshotPath, buf) + } + } + + const prevPath = path.join(prevDir, `${key}.png`) + const prevScreenshotPath = fs.existsSync(prevPath) ? prevPath : null + const diff = screenshotPath && prevScreenshotPath ? computeDiff(screenshotPath, prevScreenshotPath) : null + + cases.push({key, label, passed, durationMs, screenshotPath, prevScreenshotPath, diff, errorMessage}) + } } } return cases diff --git a/shared/tests/e2e/generate-ios-report.mts b/shared/tests/e2e/generate-ios-report.mts index 67568dc0f08b..e19b9fd39047 100644 --- a/shared/tests/e2e/generate-ios-report.mts +++ b/shared/tests/e2e/generate-ios-report.mts @@ -209,6 +209,7 @@ ${sharedCss(allPassed)}

${title}

${passed} passed · ${failed} failed · ${total} total${hasDiff ? ' · vs baseline' : ''}${timestamp}
+
${cards}
${sliderScript()} @@ -268,7 +269,13 @@ h1{font-size:20px;font-weight:600} .ov-controls{position:fixed;bottom:20px;left:50%;transform:translateX(-50%);z-index:1002;display:flex;align-items:center;gap:8px;background:rgba(0,0,0,.55);border-radius:24px;padding:6px 14px} .ov-controls button{background:none;border:none;color:#fff;font-size:18px;line-height:1;cursor:pointer;padding:2px 6px;border-radius:4px;opacity:.85} .ov-controls button:hover{opacity:1;background:rgba(255,255,255,.15)} -.ov-counter{color:rgba(255,255,255,.8);font-size:12px;font-weight:600;letter-spacing:.04em;min-width:52px;text-align:center}` +.ov-counter{color:rgba(255,255,255,.8);font-size:12px;font-weight:600;letter-spacing:.04em;min-width:52px;text-align:center} +.filter-wrap{padding:10px 28px 14px} +#filter-input{width:100%;max-width:480px;padding:6px 12px;border-radius:6px;border:1px solid rgba(255,255,255,.3);background:rgba(255,255,255,.15);color:#fff;font-size:13px;outline:none} +#filter-input::placeholder{color:rgba(255,255,255,.6)} +#filter-input:focus{background:rgba(255,255,255,.25);border-color:rgba(255,255,255,.6)} +.card.hidden{display:none} +.section-hdr.hidden{display:none}` } export function sliderScript(): string { @@ -362,7 +369,7 @@ let slideshowImgs = [] const slideshowBtn = document.getElementById('slideshow-btn') function slideshowImages() { - return Array.from(document.querySelectorAll('.grid .card')).flatMap(card => { + return Array.from(document.querySelectorAll('.grid .card:not(.hidden)')).flatMap(card => { const img = card.querySelector('.img-after') ?? card.querySelector('img.solo') return img ? [img.src] : [] }) @@ -416,6 +423,41 @@ slideshowBtn.addEventListener('click', () => { setSlideshowPlaying(true) } }) + +// ── filter ─────────────────────────────────────────────────────────────────── +const filterInput = document.getElementById('filter-input') + +function applyFilter(q) { + const lq = q.toLowerCase() + document.querySelectorAll('.grid .card').forEach(card => { + const name = card.querySelector('.name')?.textContent?.toLowerCase() ?? '' + card.classList.toggle('hidden', lq.length > 0 && !name.includes(lq)) + }) + document.querySelectorAll('.section-hdr').forEach(hdr => { + let el = hdr.nextElementSibling + let anyVisible = false + while (el && !el.classList.contains('section-hdr')) { + if (el.classList.contains('card') && !el.classList.contains('hidden')) { anyVisible = true; break } + el = el.nextElementSibling + } + hdr.classList.toggle('hidden', lq.length > 0 && !anyVisible) + }) +} + +function syncUrl(q) { + const url = new URL(location.href) + if (q) url.searchParams.set('q', q) + else url.searchParams.delete('q') + history.replaceState(null, '', url) +} + +filterInput.addEventListener('input', () => { + applyFilter(filterInput.value) + syncUrl(filterInput.value) +}) + +const initQ = new URL(location.href).searchParams.get('q') ?? '' +if (initQ) { filterInput.value = initQ; applyFilter(initQ) } ` } From d7d89869514ecf5c5d823109db06b7657e63ba8e Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Tue, 2 Jun 2026 11:45:15 -0400 Subject: [PATCH 06/12] WIP --- plans/storybook.md | 24 ------------------------ 1 file changed, 24 deletions(-) delete mode 100644 plans/storybook.md diff --git a/plans/storybook.md b/plans/storybook.md deleted file mode 100644 index 3f17257efabe..000000000000 --- a/plans/storybook.md +++ /dev/null @@ -1,24 +0,0 @@ -# Storybook TODO - -Top-level `shared/` folders to add stories for. Initial `devices/` is done. - -- [x] devices -- [x] chat -- [x] common-adapters -- [x] crypto -- [x] fs -- [x] git -- [x] login -- [x] menubar -- [x] people -- [x] pinentry -- [x] profile -- [x] provision -- [x] router-v2 -- [x] settings -- [x] signup -- [x] team-building -- [x] teams -- [x] tracker -- [x] unlock-folders -- [x] wallets From 5c2f00ec74c1433708b68d433ee532cdf5efc726 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Tue, 2 Jun 2026 11:49:01 -0400 Subject: [PATCH 07/12] Replace chrisnojima with testuser in all stories/tests --- CLAUDE.md | 1 + shared/devices/device-page.stories.tsx | 6 +++--- shared/devices/row.stories.tsx | 4 ++-- shared/login/recover-password/explain-device.stories.tsx | 4 ++-- .../recover-password/prompt-reset-account.stories.tsx | 6 +++--- shared/login/reset/waiting.stories.tsx | 4 ++-- shared/login/user-card/index.stories.tsx | 6 +++--- shared/provision/paper-key.stories.tsx | 2 +- shared/signup/device-name.stories.tsx | 2 +- shared/signup/username.stories.tsx | 2 +- shared/team-building/team-box.stories.tsx | 6 +++--- shared/team-building/user-bubble.stories.tsx | 8 ++++---- 12 files changed, 26 insertions(+), 25 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 1c1bc90b0d08..7dc9583de891 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -3,6 +3,7 @@ - "Was working before" = base branch, not previous commit. Base branch is almost always `nojima/HOTPOT-next-670-clean-2` (not `master`). Always run `gh pr view --json baseRefName` to confirm before any `git diff` or `git log` comparison. - Never use `npm`. Always `yarn`. - Never silently drop features/behavior — ask first, present options. +- In tests/stories, use `testuser` / `testuser-mac` as placeholder usernames — never real usernames like `chrisnojima`. - No DOM elements (`
`, ``, etc.) in plain `.tsx` files — use `Kb.*`. Guard desktop-only DOM with `Styles.isMobile`. - Temp files go in `/tmp/`. - Remove unused code when editing: styles, imports, vars, params, dead helpers. diff --git a/shared/devices/device-page.stories.tsx b/shared/devices/device-page.stories.tsx index 35e244e2decc..9e580db97d51 100644 --- a/shared/devices/device-page.stories.tsx +++ b/shared/devices/device-page.stories.tsx @@ -15,7 +15,7 @@ const makeDevice = (overrides: Partial = {}): T.Devices.Device lastUsed: weekAgo, name: 'My Device', type: 'desktop', - provisionerName: 'chrisnojima-mac', + provisionerName: 'testuser-mac', ...overrides, }) @@ -36,7 +36,7 @@ export const DesktopActive: Story = { export const CurrentDevice: Story = { args: { - device: makeDevice({name: 'chrisnojima-mac', currentDevice: true, lastUsed: now}), + device: makeDevice({name: 'testuser-mac', currentDevice: true, lastUsed: now}), canRevoke: false, }, } @@ -67,7 +67,7 @@ export const RevokedDevice: Story = { device: makeDevice({ name: 'old-laptop', revokedAt: monthAgo, - revokedByName: 'chrisnojima-mac', + revokedByName: 'testuser-mac', lastUsed: monthAgo, }), canRevoke: false, diff --git a/shared/devices/row.stories.tsx b/shared/devices/row.stories.tsx index ff679c30bb9d..d5abe2f5a506 100644 --- a/shared/devices/row.stories.tsx +++ b/shared/devices/row.stories.tsx @@ -29,7 +29,7 @@ type Story = StoryObj export const DesktopCurrent: Story = { args: { - device: makeDevice({name: 'chrisnojima-mac', currentDevice: true}), + device: makeDevice({name: 'testuser-mac', currentDevice: true}), }, } @@ -59,7 +59,7 @@ export const Revoked: Story = { device: makeDevice({ name: 'old-laptop', revokedAt: weekAgo, - revokedByName: 'chrisnojima-mac', + revokedByName: 'testuser-mac', }), firstItem: false, }, diff --git a/shared/login/recover-password/explain-device.stories.tsx b/shared/login/recover-password/explain-device.stories.tsx index 0fd79c9e56a1..31745d3d559c 100644 --- a/shared/login/recover-password/explain-device.stories.tsx +++ b/shared/login/recover-password/explain-device.stories.tsx @@ -15,7 +15,7 @@ export const Desktop: Story = { params: { deviceName: 'work-laptop', deviceType: T.RPCGen.DeviceType.desktop, - username: 'chrisnojima', + username: 'testuser', }, }, }, @@ -27,7 +27,7 @@ export const Mobile: Story = { params: { deviceName: 'iPhone 15', deviceType: T.RPCGen.DeviceType.mobile, - username: 'chrisnojima', + username: 'testuser', }, }, }, diff --git a/shared/login/recover-password/prompt-reset-account.stories.tsx b/shared/login/recover-password/prompt-reset-account.stories.tsx index 813404436ab6..bffb1ece594f 100644 --- a/shared/login/recover-password/prompt-reset-account.stories.tsx +++ b/shared/login/recover-password/prompt-reset-account.stories.tsx @@ -11,7 +11,7 @@ type Story = StoryObj export const AccountReset: Story = { args: { skipPassword: true, - username: 'chrisnojima', + username: 'testuser', }, } @@ -19,13 +19,13 @@ export const ResetPassword: Story = { args: { resetPassword: true, skipPassword: false, - username: 'chrisnojima', + username: 'testuser', }, } export const KnowPassword: Story = { args: { skipPassword: false, - username: 'chrisnojima', + username: 'testuser', }, } diff --git a/shared/login/reset/waiting.stories.tsx b/shared/login/reset/waiting.stories.tsx index 09582494fc86..ee5ee459b03d 100644 --- a/shared/login/reset/waiting.stories.tsx +++ b/shared/login/reset/waiting.stories.tsx @@ -13,7 +13,7 @@ const futureTime = Date.now() + 7 * 24 * 60 * 60 * 1000 // 7 days from now export const CheckEmailOrPhone: Story = { args: { pipelineStarted: false, - username: 'chrisnojima', + username: 'testuser', }, } @@ -21,6 +21,6 @@ export const PipelineStarted: Story = { args: { endTime: futureTime, pipelineStarted: true, - username: 'chrisnojima', + username: 'testuser', }, } diff --git a/shared/login/user-card/index.stories.tsx b/shared/login/user-card/index.stories.tsx index 6e3c20b358c7..ab94c8cd62a3 100644 --- a/shared/login/user-card/index.stories.tsx +++ b/shared/login/user-card/index.stories.tsx @@ -11,7 +11,7 @@ type Story = StoryObj export const WithUsername: Story = { args: { - username: 'chrisnojima', + username: 'testuser', children: ( Card content here @@ -32,7 +32,7 @@ export const WithoutUsername: Story = { export const LargeAvatar: Story = { args: { - username: 'chrisnojima', + username: 'testuser', avatarSize: 128, children: ( @@ -44,7 +44,7 @@ export const LargeAvatar: Story = { export const SmallAvatar: Story = { args: { - username: 'chrisnojima', + username: 'testuser', avatarSize: 48, children: ( diff --git a/shared/provision/paper-key.stories.tsx b/shared/provision/paper-key.stories.tsx index 0ae9a48ee8b2..f83bd9f60f59 100644 --- a/shared/provision/paper-key.stories.tsx +++ b/shared/provision/paper-key.stories.tsx @@ -5,7 +5,7 @@ const meta: Meta = { component: PaperKey, title: 'Provision/PaperKey', args: { - hint: 'chrisnojima-mac...', + hint: 'testuser-mac...', error: '', waiting: false, onSubmit: () => {}, diff --git a/shared/signup/device-name.stories.tsx b/shared/signup/device-name.stories.tsx index 373b2adffa4a..5c650892587c 100644 --- a/shared/signup/device-name.stories.tsx +++ b/shared/signup/device-name.stories.tsx @@ -21,6 +21,6 @@ export const Empty: Story = { export const WithUsername: Story = { args: { - route: {params: {username: 'chrisnojima', inviteCode: 'abc123'}}, + route: {params: {username: 'testuser', inviteCode: 'abc123'}}, }, } diff --git a/shared/signup/username.stories.tsx b/shared/signup/username.stories.tsx index ec12596996f0..c824d9b49293 100644 --- a/shared/signup/username.stories.tsx +++ b/shared/signup/username.stories.tsx @@ -21,6 +21,6 @@ export const Empty: Story = { export const Prefilled: Story = { args: { - route: {params: {username: 'chrisnojima'}}, + route: {params: {username: 'testuser'}}, }, } diff --git a/shared/team-building/team-box.stories.tsx b/shared/team-building/team-box.stories.tsx index 9b2bf29e6504..1b9dca6dad57 100644 --- a/shared/team-building/team-box.stories.tsx +++ b/shared/team-building/team-box.stories.tsx @@ -35,14 +35,14 @@ export const Empty: Story = {} export const OneKeybaseUser: Story = { args: { - teamSoFar: [makeUser('chrisnojima')], + teamSoFar: [makeUser('testuser')], }, } export const MixedServices: Story = { args: { teamSoFar: [ - makeUser('chrisnojima'), + makeUser('testuser'), makeUser('twitterfriend', 'twitter'), makeUser('15551234567', 'phone'), makeUser('user@example.com', 'email'), @@ -65,7 +65,7 @@ export const ManyUsers: Story = { export const WithGoButtonLabel: Story = { args: { - teamSoFar: [makeUser('chrisnojima')], + teamSoFar: [makeUser('testuser')], goButtonLabel: 'Add', }, } diff --git a/shared/team-building/user-bubble.stories.tsx b/shared/team-building/user-bubble.stories.tsx index aa3ba3404c5e..47ab0d4f9c98 100644 --- a/shared/team-building/user-bubble.stories.tsx +++ b/shared/team-building/user-bubble.stories.tsx @@ -6,9 +6,9 @@ const meta: Meta = { component: UserBubble, title: 'TeamBuilding/UserBubble', args: { - username: 'chrisnojima', + username: 'testuser', service: 'keybase' as T.TB.ServiceIdWithContact, - tooltip: 'chrisnojima', + tooltip: 'testuser', onRemove: () => {}, }, } @@ -17,9 +17,9 @@ type Story = StoryObj export const KeybaseUser: Story = { args: { - username: 'chrisnojima', + username: 'testuser', service: 'keybase', - tooltip: 'chrisnojima', + tooltip: 'testuser', }, } From 89886bf897d3a4d99954d4168ad6cdae8ac1c2f4 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Tue, 2 Jun 2026 13:24:02 -0400 Subject: [PATCH 08/12] Address Copilot PR feedback - Use Playwright bundled Chromium when CHROME_PATH is unset (fixes CI) - Skip dark mode dispatch when preference is already correct - Remove unnecessary `as any` cast in announcement story --- shared/.storybook/preview.ts | 5 ++++- shared/people/announcement.stories.tsx | 2 +- shared/scripts/screenshot-storybook.mts | 5 ++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/shared/.storybook/preview.ts b/shared/.storybook/preview.ts index 242dfe1a408e..3f0a75e6f160 100644 --- a/shared/.storybook/preview.ts +++ b/shared/.storybook/preview.ts @@ -58,8 +58,11 @@ const preview: Preview = { decorators: [ (Story, context) => { const dark = !!context.globals['darkMode'] + const target = dark ? 'alwaysDark' : 'alwaysLight' // false = don't write to config (no RPC available in storybook) - useDarkModeState.getState().dispatch.setDarkModePreference(dark ? 'alwaysDark' : 'alwaysLight', false) + if (useDarkModeState.getState().darkModePreference !== target) { + useDarkModeState.getState().dispatch.setDarkModePreference(target, false) + } // Required for light-dark() CSS vars to resolve correctly document.documentElement.style.colorScheme = dark ? 'dark' : 'light' return React.createElement('div', {style: {background: 'var(--color-white)', padding: 16}}, React.createElement(Story)) diff --git a/shared/people/announcement.stories.tsx b/shared/people/announcement.stories.tsx index b0debdc9ddf9..e06ca20c6037 100644 --- a/shared/people/announcement.stories.tsx +++ b/shared/people/announcement.stories.tsx @@ -9,7 +9,7 @@ const meta: Meta = { dismissable: false, dismissAnnouncement: () => {}, getData: () => {}, - id: 1 as any, + id: 1, text: 'Keybase has a new feature available.', }, } diff --git a/shared/scripts/screenshot-storybook.mts b/shared/scripts/screenshot-storybook.mts index 1483d837eac2..9aced04e6a25 100644 --- a/shared/scripts/screenshot-storybook.mts +++ b/shared/scripts/screenshot-storybook.mts @@ -62,9 +62,8 @@ console.log(`Found ${stories.length} stories — concurrency ${CONCURRENCY}`) fs.mkdirSync(outputDir, {recursive: true}) -const executablePath = - process.env['CHROME_PATH'] ?? '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' -const browser = await chromium.launch({executablePath}) +const executablePath = process.env['CHROME_PATH'] +const browser = await chromium.launch(executablePath ? {executablePath} : {}) const queue = [...stories] let done = 0 From 44e444512c85d88e2f24d9b6d09eaea405821306 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Tue, 2 Jun 2026 14:28:27 -0400 Subject: [PATCH 09/12] Address more Copilot PR feedback - Fix webpack resolve.extensions order so .desktop.tsx/.desktop.ts take precedence over .tsx/.ts - Wrap fixture setup() in try/finally so emulateMedia cleanup always runs - Bind static file server to 127.0.0.1 and sandbox request paths to buildDir --- shared/.storybook/main.ts | 2 +- shared/scripts/screenshot-storybook.mts | 7 ++++++- shared/tests/e2e/electron/helpers/fixtures.ts | 7 +++++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/shared/.storybook/main.ts b/shared/.storybook/main.ts index 2141890b12a4..6a6924057c25 100644 --- a/shared/.storybook/main.ts +++ b/shared/.storybook/main.ts @@ -57,7 +57,7 @@ const config: StorybookConfig = { ...(webpackConfig.resolve.alias ?? {}), ...makeAliases(), } - webpackConfig.resolve.extensions = ['.tsx', '.ts', '.desktop.tsx', '.desktop.ts', '.js', '.jsx', '.json'] + webpackConfig.resolve.extensions = ['.desktop.tsx', '.desktop.ts', '.tsx', '.ts', '.js', '.jsx', '.json'] // Storybook 10 does not include a JS/TS transpiler by default — add babel-loader // so that TypeScript and JSX in story files and preview config are compiled. diff --git a/shared/scripts/screenshot-storybook.mts b/shared/scripts/screenshot-storybook.mts index 9aced04e6a25..011ed577d7d6 100644 --- a/shared/scripts/screenshot-storybook.mts +++ b/shared/scripts/screenshot-storybook.mts @@ -40,6 +40,11 @@ const server = http.createServer((req, res) => { let urlPath = new NodeURL(req.url ?? '/', `http://localhost`).pathname if (urlPath === '/') urlPath = '/index.html' const filePath = path.join(buildDir, urlPath) + if (!filePath.startsWith(buildDir + path.sep)) { + res.writeHead(403) + res.end('Forbidden') + return + } try { const data = fs.readFileSync(filePath) const ext = path.extname(filePath) @@ -50,7 +55,7 @@ const server = http.createServer((req, res) => { res.end('Not found') } }) -await new Promise(resolve => server.listen(PORT, resolve)) +await new Promise(resolve => server.listen(PORT, '127.0.0.1', resolve)) const storybookUrl = `http://localhost:${PORT}` console.log(`Serving static build at ${storybookUrl}`) diff --git a/shared/tests/e2e/electron/helpers/fixtures.ts b/shared/tests/e2e/electron/helpers/fixtures.ts index 26a42001b3b0..5aec0b409ebd 100644 --- a/shared/tests/e2e/electron/helpers/fixtures.ts +++ b/shared/tests/e2e/electron/helpers/fixtures.ts @@ -17,8 +17,11 @@ export const test = base.extend<{page: Page}, WorkerFixtures>({ // Reload to clear in-memory state and apply the new color scheme await page.reload() await page.getByTestId(NAV_TAB_CHAT).waitFor({timeout: 30_000}) - await setup(page) - await page.emulateMedia({colorScheme: null}) + try { + await setup(page) + } finally { + await page.emulateMedia({colorScheme: null}) + } // Do NOT close — that kills the Electron process }, {scope: 'worker'}, From f0a646561df3fbbd64003a51d5f8825d3929bb1d Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Tue, 2 Jun 2026 14:40:51 -0400 Subject: [PATCH 10/12] Fix screenshot summary to show done/total instead of total --- shared/scripts/screenshot-storybook.mts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/scripts/screenshot-storybook.mts b/shared/scripts/screenshot-storybook.mts index 011ed577d7d6..779549ae9b74 100644 --- a/shared/scripts/screenshot-storybook.mts +++ b/shared/scripts/screenshot-storybook.mts @@ -114,4 +114,4 @@ await Promise.all( await browser.close() server.close() -console.log(`\nDone — ${total} screenshots in ${outputDir}`) +console.log(`\nDone — ${done}/${total} screenshots in ${outputDir}`) From ad6bb9ec56466b2fe6e678f020b2c217dfe03a31 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Tue, 2 Jun 2026 14:57:29 -0400 Subject: [PATCH 11/12] Address more Copilot PR feedback - Treat flaky-but-passing Playwright tests as passed - Escape label/name in HTML alt attributes - Remove unnecessary ?? fallback on non-nullable projectName --- shared/tests/e2e/generate-electron-report.mts | 6 +++--- shared/tests/e2e/generate-ios-report.mts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/shared/tests/e2e/generate-electron-report.mts b/shared/tests/e2e/generate-electron-report.mts index 2f2d9925ff63..c6f7cdaa0311 100644 --- a/shared/tests/e2e/generate-electron-report.mts +++ b/shared/tests/e2e/generate-electron-report.mts @@ -79,7 +79,7 @@ function parseReport(report: Report): TestCase[] { // Group by project so light and dark produce separate TestCase entries const byProject = new Map() for (const t of spec.tests) { - const proj = t.projectName ?? '' + const proj = t.projectName if (!byProject.has(proj)) byProject.set(proj, []) byProject.get(proj)!.push(t) } @@ -94,7 +94,7 @@ function parseReport(report: Report): TestCase[] { const result = allResults.filter(r => r.status !== 'skipped').at(-1) ?? allResults.at(-1) if (!result) continue - const passed = tests.every(t => t.status === 'expected') + const passed = tests.every(t => t.status === 'expected' || t.status === 'flaky') const durationMs = result.duration const errorMessage = !passed ? (result.errors?.[0]?.message?.split('\n')[0] ?? 'test failed') @@ -192,7 +192,7 @@ function buildHtml(cases: TestCase[], storybookCases: StorybookCase[], timestamp
NOW
` } else if (c.screenshotPath) { - visual = `
${c.label}
` + visual = `
${escapeHtml(c.label)}
` } else { visual = `
No screenshot
` } diff --git a/shared/tests/e2e/generate-ios-report.mts b/shared/tests/e2e/generate-ios-report.mts index e19b9fd39047..6d9edd2d99d9 100644 --- a/shared/tests/e2e/generate-ios-report.mts +++ b/shared/tests/e2e/generate-ios-report.mts @@ -168,7 +168,7 @@ function buildHtml(results: ScreenshotResult[], timestamp: string, title: string
NOW
` } else if (r.screenshotPath) { - visual = `
${r.name}
` + visual = `
${escapeHtml(r.name)}
` } else if (r.failureScreenshotPath) { visual = `
failure
` } else { From 6d7ad7db6cc54ad03da7f30ab6785b572421e6bb Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Tue, 2 Jun 2026 15:26:34 -0400 Subject: [PATCH 12/12] Address more Copilot PR feedback - Clear storybook output dir before each run to remove stale screenshots - Include skipped tests as passing in electron report - Fix storybook label path separator replacement to be global --- shared/scripts/screenshot-storybook.mts | 1 + shared/tests/e2e/generate-electron-report.mts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/shared/scripts/screenshot-storybook.mts b/shared/scripts/screenshot-storybook.mts index 779549ae9b74..078a9f768a1f 100644 --- a/shared/scripts/screenshot-storybook.mts +++ b/shared/scripts/screenshot-storybook.mts @@ -65,6 +65,7 @@ const {entries} = (await (await fetch(`${storybookUrl}/index.json`)).json()) as const stories = Object.entries(entries).filter(([, e]) => e.type === 'story') console.log(`Found ${stories.length} stories — concurrency ${CONCURRENCY}`) +fs.rmSync(outputDir, {recursive: true, force: true}) fs.mkdirSync(outputDir, {recursive: true}) const executablePath = process.env['CHROME_PATH'] diff --git a/shared/tests/e2e/generate-electron-report.mts b/shared/tests/e2e/generate-electron-report.mts index c6f7cdaa0311..838c89c2d43d 100644 --- a/shared/tests/e2e/generate-electron-report.mts +++ b/shared/tests/e2e/generate-electron-report.mts @@ -94,7 +94,7 @@ function parseReport(report: Report): TestCase[] { const result = allResults.filter(r => r.status !== 'skipped').at(-1) ?? allResults.at(-1) if (!result) continue - const passed = tests.every(t => t.status === 'expected' || t.status === 'flaky') + const passed = tests.every(t => t.status === 'expected' || t.status === 'flaky' || t.status === 'skipped') const durationMs = result.duration const errorMessage = !passed ? (result.errors?.[0]?.message?.split('\n')[0] ?? 'test failed') @@ -134,7 +134,7 @@ function parseStorybookScreenshots(): StorybookCase[] { if (entry.isDirectory()) { walk(fullPath, relPath) } else if (entry.name.endsWith('.png')) { - const label = relPath.replace('.png', '').replace('/', ' / ').replace(/-/g, ' ') + const label = relPath.replace(/\.png$/, '').replaceAll('/', ' / ').replace(/-/g, ' ') const prevPath = path.join(storybookPrevDir, relPath) const prevScreenshotPath = fs.existsSync(prevPath) ? prevPath : null const diff = prevScreenshotPath ? computeDiff(fullPath, prevScreenshotPath) : null