Showing preview only (518K chars total). Download the full file or copy to clipboard to get everything.
Repository: react95-io/React95
Branch: master
Commit: 871d5335daeb
Files: 246
Total size: 466.6 KB
Directory structure:
gitextract_rz85rgj9/
├── .babelrc
├── .codesandbox/
│ └── ci.json
├── .editorconfig
├── .eslintrc.js
├── .firebaserc
├── .github/
│ ├── FUNDING.yml
│ └── workflows/
│ ├── ci.yml
│ └── release.yml
├── .gitignore
├── .prettierrc
├── .storybook/
│ ├── decorators/
│ │ └── withGlobalStyle.tsx
│ ├── main.ts
│ ├── manager.css
│ ├── manager.ts
│ ├── preview.ts
│ ├── theme-picker/
│ │ ├── ThemeButton.tsx
│ │ ├── ThemeList.tsx
│ │ ├── ThemeProvider.tsx
│ │ ├── constants.ts
│ │ └── register.ts
│ └── theme.js
├── LICENSE
├── README.md
├── docs/
│ ├── Contributing.stories.mdx
│ ├── Getting-Started.stories.mdx
│ ├── Submit-your-Project.stories.mdx
│ └── Welcome.stories.mdx
├── firebase.json
├── jest.config.js
├── package.json
├── rollup.config.js
├── src/
│ ├── Anchor/
│ │ ├── Anchor.spec.tsx
│ │ ├── Anchor.stories.tsx
│ │ └── Anchor.tsx
│ ├── AppBar/
│ │ ├── AppBar.spec.tsx
│ │ ├── AppBar.stories.tsx
│ │ └── AppBar.tsx
│ ├── Avatar/
│ │ ├── Avatar.spec.tsx
│ │ ├── Avatar.stories.tsx
│ │ └── Avatar.tsx
│ ├── Button/
│ │ ├── Button.spec.tsx
│ │ ├── Button.stories.tsx
│ │ └── Button.tsx
│ ├── Checkbox/
│ │ ├── Checkbox.spec.tsx
│ │ ├── Checkbox.stories.tsx
│ │ └── Checkbox.tsx
│ ├── ColorInput/
│ │ ├── ColorInput.spec.tsx
│ │ ├── ColorInput.stories.tsx
│ │ └── ColorInput.tsx
│ ├── Counter/
│ │ ├── Counter.spec.tsx
│ │ ├── Counter.stories.tsx
│ │ ├── Counter.tsx
│ │ └── Digit.tsx
│ ├── DatePicker/
│ │ ├── DatePicker.stories.tsx
│ │ └── DatePicker.tsx
│ ├── Frame/
│ │ ├── Frame.spec.tsx
│ │ ├── Frame.stories.tsx
│ │ └── Frame.tsx
│ ├── GroupBox/
│ │ ├── GroupBox.spec.tsx
│ │ ├── GroupBox.stories.tsx
│ │ └── GroupBox.tsx
│ ├── Handle/
│ │ ├── Handle.spec.tsx
│ │ ├── Handle.stories.tsx
│ │ └── Handle.tsx
│ ├── Hourglass/
│ │ ├── Hourglass.spec.tsx
│ │ ├── Hourglass.stories.tsx
│ │ ├── Hourglass.tsx
│ │ └── base64hourglass.tsx
│ ├── MenuList/
│ │ ├── MenuList.spec.tsx
│ │ ├── MenuList.stories.tsx
│ │ ├── MenuList.tsx
│ │ ├── MenuListItem.spec.tsx
│ │ └── MenuListItem.tsx
│ ├── Monitor/
│ │ ├── Monitor.spec.tsx
│ │ ├── Monitor.stories.tsx
│ │ └── Monitor.tsx
│ ├── NumberInput/
│ │ ├── NumberInput.spec.tsx
│ │ ├── NumberInput.stories.tsx
│ │ └── NumberInput.tsx
│ ├── ProgressBar/
│ │ ├── ProgressBar.spec.tsx
│ │ ├── ProgressBar.stories.tsx
│ │ └── ProgressBar.tsx
│ ├── Radio/
│ │ ├── Radio.spec.tsx
│ │ ├── Radio.stories.tsx
│ │ └── Radio.tsx
│ ├── ScrollView/
│ │ ├── ScrollView.spec.tsx
│ │ ├── ScrollView.stories.tsx
│ │ └── ScrollView.tsx
│ ├── Select/
│ │ ├── Select.spec.tsx
│ │ ├── Select.stories.data.ts
│ │ ├── Select.stories.tsx
│ │ ├── Select.styles.tsx
│ │ ├── Select.tsx
│ │ ├── Select.types.ts
│ │ ├── SelectNative.spec.tsx
│ │ ├── SelectNative.tsx
│ │ ├── useSelectCommon.tsx
│ │ └── useSelectState.ts
│ ├── Separator/
│ │ ├── Separator.spec.tsx
│ │ ├── Separator.stories.tsx
│ │ └── Separator.tsx
│ ├── Slider/
│ │ ├── Slider.spec.tsx
│ │ ├── Slider.stories.tsx
│ │ └── Slider.tsx
│ ├── Table/
│ │ ├── Table.spec.tsx
│ │ ├── Table.stories.tsx
│ │ ├── Table.tsx
│ │ ├── TableBody.spec.tsx
│ │ ├── TableBody.tsx
│ │ ├── TableDataCell.spec.tsx
│ │ ├── TableDataCell.tsx
│ │ ├── TableHead.spec.tsx
│ │ ├── TableHead.tsx
│ │ ├── TableHeadCell.spec.tsx
│ │ ├── TableHeadCell.tsx
│ │ ├── TableRow.spec.tsx
│ │ └── TableRow.tsx
│ ├── Tabs/
│ │ ├── Tab.spec.tsx
│ │ ├── Tab.tsx
│ │ ├── TabBody.spec.tsx
│ │ ├── TabBody.tsx
│ │ ├── Tabs.spec.tsx
│ │ ├── Tabs.stories.tsx
│ │ └── Tabs.tsx
│ ├── TextInput/
│ │ ├── TextInput.spec.tsx
│ │ ├── TextInput.stories.tsx
│ │ └── TextInput.tsx
│ ├── Toolbar/
│ │ ├── Toolbar.spec.tsx
│ │ └── Toolbar.tsx
│ ├── Tooltip/
│ │ ├── Tooltip.spec.tsx
│ │ ├── Tooltip.stories.tsx
│ │ └── Tooltip.tsx
│ ├── TreeView/
│ │ ├── TreeView.spec.tsx
│ │ ├── TreeView.stories.tsx
│ │ └── TreeView.tsx
│ ├── Window/
│ │ ├── Window.spec.tsx
│ │ ├── Window.stories.tsx
│ │ ├── Window.tsx
│ │ ├── WindowContent.spec.tsx
│ │ ├── WindowContent.tsx
│ │ ├── WindowHeader.spec.tsx
│ │ └── WindowHeader.tsx
│ ├── assets/
│ │ ├── fonts/
│ │ │ └── src/
│ │ │ ├── ms-sans-serif/
│ │ │ │ ├── license.txt
│ │ │ │ └── readme.txt
│ │ │ └── ms-sans-serif-bold/
│ │ │ ├── license.txt
│ │ │ └── readme.txt
│ │ └── images/
│ │ └── logo.psd
│ ├── common/
│ │ ├── SwitchBase.ts
│ │ ├── constants.ts
│ │ ├── hooks/
│ │ │ ├── useControlledOrUncontrolled.ts
│ │ │ ├── useEventCallback.ts
│ │ │ ├── useForkRef.spec.tsx
│ │ │ ├── useForkRef.ts
│ │ │ ├── useId.spec.ts
│ │ │ ├── useId.ts
│ │ │ └── useIsFocusVisible.ts
│ │ ├── index.ts
│ │ ├── styleReset.ts
│ │ ├── system.ts
│ │ ├── themes/
│ │ │ ├── aiee.ts
│ │ │ ├── ash.ts
│ │ │ ├── azureOrange.ts
│ │ │ ├── bee.ts
│ │ │ ├── blackAndWhite.ts
│ │ │ ├── blue.ts
│ │ │ ├── brick.ts
│ │ │ ├── candy.ts
│ │ │ ├── cherry.ts
│ │ │ ├── coldGray.ts
│ │ │ ├── counterStrike.ts
│ │ │ ├── darkTeal.ts
│ │ │ ├── denim.ts
│ │ │ ├── eggplant.ts
│ │ │ ├── fxDev.ts
│ │ │ ├── highContrast.ts
│ │ │ ├── honey.ts
│ │ │ ├── hotChocolate.ts
│ │ │ ├── hotdogStand.ts
│ │ │ ├── index.ts
│ │ │ ├── lilac.ts
│ │ │ ├── lilacRoseDark.ts
│ │ │ ├── maple.ts
│ │ │ ├── marine.ts
│ │ │ ├── matrix.ts
│ │ │ ├── millenium.ts
│ │ │ ├── modernDark.ts
│ │ │ ├── molecule.ts
│ │ │ ├── monochrome.ts
│ │ │ ├── ninjaTurtles.ts
│ │ │ ├── olive.ts
│ │ │ ├── original.ts
│ │ │ ├── pamelaAnderson.ts
│ │ │ ├── peggysPastels.ts
│ │ │ ├── plum.ts
│ │ │ ├── polarized.ts
│ │ │ ├── powerShell.ts
│ │ │ ├── rainyDay.ts
│ │ │ ├── raspberry.ts
│ │ │ ├── redWine.ts
│ │ │ ├── rose.ts
│ │ │ ├── seawater.ts
│ │ │ ├── shelbiTeal.ts
│ │ │ ├── slate.ts
│ │ │ ├── solarizedDark.ts
│ │ │ ├── solarizedLight.ts
│ │ │ ├── spruce.ts
│ │ │ ├── stormClouds.ts
│ │ │ ├── theSixtiesUSA.ts
│ │ │ ├── tokyoDark.ts
│ │ │ ├── toner.ts
│ │ │ ├── tooSexy.ts
│ │ │ ├── travel.ts
│ │ │ ├── types.ts
│ │ │ ├── vaporTeal.ts
│ │ │ ├── vermillion.ts
│ │ │ ├── violetDark.ts
│ │ │ ├── vistaesqueMidnight.ts
│ │ │ ├── water.ts
│ │ │ ├── white.ts
│ │ │ ├── windows1.ts
│ │ │ └── wmii.ts
│ │ └── utils/
│ │ ├── events.spec.tsx
│ │ ├── events.ts
│ │ ├── index.spec.ts
│ │ └── index.ts
│ ├── index.ts
│ ├── legacy/
│ │ ├── Bar.tsx
│ │ ├── Cutout.tsx
│ │ ├── Desktop.tsx
│ │ ├── Divider.tsx
│ │ ├── Fieldset.tsx
│ │ ├── List.tsx
│ │ ├── ListItem.tsx
│ │ ├── NumberField.tsx
│ │ ├── Panel.tsx
│ │ ├── Progress.tsx
│ │ ├── TextField.tsx
│ │ └── Tree.tsx
│ └── types.ts
├── test/
│ ├── setup-test.ts
│ └── utils.tsx
├── tsconfig.build.index.json
├── tsconfig.build.themes.json
├── tsconfig.json
└── types/
├── globals.d.ts
└── themes.d.ts
================================================
FILE CONTENTS
================================================
================================================
FILE: .babelrc
================================================
{
"sourceType": "unambiguous",
"presets": [
[
"@babel/preset-env",
{
"shippedProposals": true,
"loose": true
}
],
"@babel/preset-typescript"
],
"plugins": [
"@babel/plugin-transform-shorthand-properties",
"@babel/plugin-transform-block-scoping",
[
"@babel/plugin-proposal-decorators",
{
"legacy": true
}
],
[
"@babel/plugin-proposal-class-properties",
{
"loose": true
}
],
[
"@babel/plugin-proposal-private-property-in-object",
{
"loose": true
}
],
[
"@babel/plugin-proposal-private-methods",
{
"loose": true
}
],
"@babel/plugin-proposal-export-default-from",
"@babel/plugin-syntax-dynamic-import",
[
"@babel/plugin-proposal-object-rest-spread",
{
"loose": true,
"useBuiltIns": true
}
],
"@babel/plugin-transform-classes",
"@babel/plugin-transform-arrow-functions",
"@babel/plugin-transform-parameters",
"@babel/plugin-transform-destructuring",
"@babel/plugin-transform-spread",
"@babel/plugin-transform-for-of",
"babel-plugin-macros",
"@babel/plugin-proposal-optional-chaining",
"@babel/plugin-proposal-nullish-coalescing-operator"
]
}
================================================
FILE: .codesandbox/ci.json
================================================
{
"buildCommand": "build:prod",
"node": "16",
"sandboxes": [
"react95-template-xkfj0"
]
}
================================================
FILE: .editorconfig
================================================
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = yes
insert_final_newline = yes
================================================
FILE: .eslintrc.js
================================================
module.exports = {
extends: [
'plugin:@typescript-eslint/recommended',
'airbnb',
'plugin:prettier/recommended',
'plugin:react-hooks/recommended'
],
parser: '@typescript-eslint/parser',
plugins: ['react', 'prettier'],
env: {
browser: true,
es6: true,
jest: true
},
rules: {
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-empty-interface': 'off',
'@typescript-eslint/no-use-before-define': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_\\d*$'
}
],
'import/extensions': ['error', { js: 'never', ts: 'never', tsx: 'never' }],
'import/no-unresolved': [
'error',
// TODO: Remove ../../test/utils when TypeScript migration is complete
{ ignore: ['react95', '../../test/utils'] }
],
'import/no-extraneous-dependencies': ['error', { devDependencies: true }],
'import/prefer-default-export': 'off',
'jsx-a11y/label-has-associated-control': ['error', { assert: 'either' }],
'jsx-a11y/label-has-for': 'off',
'no-nested-ternary': 'off',
'prettier/prettier': 'error',
'react/forbid-prop-types': 'off',
'react/jsx-filename-extension': [
'warn',
{ extensions: ['.js', '.jsx', '.tsx'] }
],
'react/jsx-props-no-spreading': 'off',
'react/no-array-index-key': 'off',
'react/prop-types': 'off',
'react/require-default-props': 'off',
'react/static-property-placement': ['error', 'static public field']
},
overrides: [
{
files: ['*.spec.@(js|jsx|ts|tsx)', '*.stories.@(js|jsx|ts|tsx)'],
rules: {
'no-console': 'off'
}
},
{
files: ['*.@(ts|tsx)'],
rules: {
// This is handled by @typescript-eslint/no-unused-vars
'no-undef': 'off'
}
}
],
settings: {
'import/parsers': {
'@typescript-eslint/parser': ['.ts', '.tsx']
},
'import/resolver': {
typescript: {}
}
}
};
================================================
FILE: .firebaserc
================================================
{
"projects": {
"default": "react95-storybook"
}
}
================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
github: arturbien
patreon: arturbien
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
custom: https://www.paypal.me/react95
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on:
pull_request:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Git Checkout
uses: actions/checkout@v2
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: 16
- name: Cache packages
uses: actions/cache@v3
with:
key: node_modules-v4-${{ hashFiles('yarn.lock') }}
path: |-
node_modules
*/node_modules
restore-keys: 'node_modules-v4-'
- name: Yarn install
run: yarn install --ignore-optional --frozen-lockfile
- name: Lint
run: yarn run lint
type-check:
runs-on: ubuntu-latest
steps:
- name: Git Checkout
uses: actions/checkout@v2
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: 16
- name: Cache packages
uses: actions/cache@v3
with:
key: node_modules-v4-${{ hashFiles('yarn.lock') }}
path: |-
node_modules
*/node_modules
restore-keys: 'node_modules-v4-'
- name: Yarn install
run: yarn install --ignore-optional --frozen-lockfile
- name: Type Check
run: yarn run typescript
test:
runs-on: ubuntu-latest
steps:
- name: Git Checkout
uses: actions/checkout@v2
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: 16
- name: Cache packages
uses: actions/cache@v3
with:
key: node_modules-v4-${{ hashFiles('yarn.lock') }}
path: |-
node_modules
*/node_modules
restore-keys: 'node_modules-v4-'
- name: Yarn install
run: yarn install --ignore-optional --frozen-lockfile
- name: Test
run: yarn run test:ci
build-library:
runs-on: ubuntu-latest
steps:
- name: Git Checkout
uses: actions/checkout@v2
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: 16
- name: Cache packages
uses: actions/cache@v3
with:
key: node_modules-v4-${{ hashFiles('yarn.lock') }}
path: |-
node_modules
*/node_modules
restore-keys: 'node_modules-v4-'
- name: Yarn install
run: yarn install --ignore-optional --frozen-lockfile
- name: Build library
run: yarn run build
build-storybook:
runs-on: ubuntu-latest
steps:
- name: Git Checkout
uses: actions/checkout@v2
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: 16
- name: Cache packages
uses: actions/cache@v3
with:
key: node_modules-v4-${{ hashFiles('yarn.lock') }}
path: |-
node_modules
*/node_modules
restore-keys: 'node_modules-v4-'
- name: Yarn install
run: yarn install --ignore-optional --frozen-lockfile
- name: Build Storybook
run: yarn run build:storybook
================================================
FILE: .github/workflows/release.yml
================================================
name: Release
on:
push:
branches:
- master
- next
- beta
- alpha
- '*.x' # maintenance releases
jobs:
release-library:
runs-on: ubuntu-latest
steps:
- name: Git Checkout
uses: actions/checkout@v2
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: 16
- name: Cache packages
uses: actions/cache@v3
with:
key: node_modules-v4-${{ hashFiles('yarn.lock') }}
path: |-
node_modules
*/node_modules
restore-keys: 'node_modules-v4-'
- name: Yarn install
run: yarn install --ignore-optional --frozen-lockfile
- name: Build library
run: yarn run build
- name: Deploy library
run: yarn run semantic-release || true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
release-storybook:
needs:
- release-library
if: github.ref == 'refs/heads/master'
runs-on: ubuntu-latest
steps:
- name: Git Checkout
uses: actions/checkout@v2
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: 16
- name: Cache packages
uses: actions/cache@v3
with:
key: node_modules-v4-${{ hashFiles('yarn.lock') }}
path: |-
node_modules
*/node_modules
restore-keys: 'node_modules-v4-'
- name: Yarn install
run: yarn install --ignore-optional --frozen-lockfile
- name: Build Storybook
run: yarn run build:storybook
- name: Deploy Storybook
run: ./node_modules/.bin/firebase deploy --token=$FIREBASE_TOKEN
env:
FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}
================================================
FILE: .gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# library build
/cjs
/esm
/themes
/images
/fonts
/dist
# storybook
/storybook
/.firebase
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# JetBrains IDEs
.idea/*
================================================
FILE: .prettierrc
================================================
{
"arrowParens": "avoid",
"bracketSpacing": true,
"htmlWhitespaceSensitivity": "css",
"insertPragma": false,
"jsxBracketSameLine": false,
"jsxSingleQuote": true,
"printWidth": 80,
"proseWrap": "preserve",
"quoteProps": "as-needed",
"requirePragma": false,
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "none",
"useTabs": false
}
================================================
FILE: .storybook/decorators/withGlobalStyle.tsx
================================================
import { DecoratorFn } from '@storybook/react';
import React from 'react';
import { createGlobalStyle } from 'styled-components';
import ms_sans_serif from '../../src/assets/fonts/dist/ms_sans_serif.woff2';
import ms_sans_serif_bold from '../../src/assets/fonts/dist/ms_sans_serif_bold.woff2';
import styleReset from '../../src/common/styleReset';
const GlobalStyle = createGlobalStyle`
${styleReset}
@font-face {
font-family: 'ms_sans_serif';
src: url('${ms_sans_serif}') format('woff2');
font-weight: 400;
font-style: normal
}
@font-face {
font-family: 'ms_sans_serif';
src: url('${ms_sans_serif_bold}') format("woff2");
font-weight: bold;
font-style: normal
}
html, body, #root {
height: 100%;
}
#root > * {
height: 100%;
box-sizing: border-box;
}
body {
font-family: 'ms_sans_serif', 'sans-serif';
}
`;
export const withGlobalStyle: DecoratorFn = story => (
<>
<GlobalStyle />
{story()}
</>
);
================================================
FILE: .storybook/main.ts
================================================
import type { StorybookConfig } from '@storybook/react/types';
import type { PropItem } from 'react-docgen-typescript';
const path = require('path');
const storybookConfig: StorybookConfig = {
stories: ['../@(docs|src)/**/*.stories.@(tsx|mdx)'],
addons: [
{
name: '@storybook/addon-docs',
options: {
sourceLoaderOptions: {
injectStoryParameters: false
}
}
},
'@storybook/addon-storysource',
'./theme-picker/register.ts'
],
core: {
builder: 'webpack5'
},
features: {
babelModeV7: true,
storyStoreV7: true,
modernInlineRender: true,
postcss: false
},
typescript: {
check: false,
checkOptions: {},
reactDocgen: 'react-docgen-typescript',
reactDocgenTypescriptOptions: {
shouldExtractLiteralValuesFromEnum: true,
propFilter: (prop: PropItem) =>
prop.parent ? !/node_modules/.test(prop.parent.fileName) : true
}
},
webpackFinal: config => {
config.resolve = {
...config.resolve,
alias: {
...config.resolve?.alias,
react95: path.resolve(__dirname, '../src/index')
}
};
return config;
}
};
module.exports = storybookConfig;
================================================
FILE: .storybook/manager.css
================================================
/* Remove from the sidebar menu stories that contains "unstable" */
a[data-item-id$='-unstable'].sidebar-item,
a[data-item-id*='-unstable-'].sidebar-item,
button[data-item-id$='-unstable'].sidebar-item,
button[data-item-id$='-unstable-'].sidebar-item {
display: none !important;
}
================================================
FILE: .storybook/manager.ts
================================================
import './manager.css';
import { addons } from '@storybook/addons';
import theme from './theme';
addons.setConfig({
theme
});
================================================
FILE: .storybook/preview.ts
================================================
import { DecoratorFn, Parameters } from '@storybook/react';
import { withGlobalStyle } from './decorators/withGlobalStyle';
import { withThemesProvider } from './theme-picker/ThemeProvider';
export const decorators: DecoratorFn[] = [withGlobalStyle, withThemesProvider];
export const parameters: Parameters = {
layout: 'fullscreen',
options: {
storySort: {
order: [
'Docs',
[
'Welcome to React95',
'Getting Started',
'Contributing',
'Submit your Project'
],
'Controls',
'Environment',
'Layout',
'Typography',
'Other'
]
}
}
};
================================================
FILE: .storybook/theme-picker/ThemeButton.tsx
================================================
import React, { useCallback } from 'react';
import { ThemeProvider } from 'styled-components';
import { Button } from '../../src/Button/Button';
import { Theme } from '../../src/types';
export function ThemeButton({
active,
onChoose,
theme
}: {
active: boolean;
onChoose: (themeName: string) => void;
theme: Theme;
}) {
const handleClick = useCallback(() => {
onChoose(theme.name);
}, []);
return (
<ThemeProvider theme={theme}>
<Button active={active} onClick={handleClick}>
{theme.name}
</Button>
</ThemeProvider>
);
}
================================================
FILE: .storybook/theme-picker/ThemeList.tsx
================================================
import { useAddonState } from '@storybook/api';
import React, { useCallback } from 'react';
import styled from 'styled-components';
import themes from '../../src/common/themes';
import { Theme } from '../../src/types';
import { THEMES_ID } from './constants';
import { ThemeButton } from './ThemeButton';
const {
original,
rainyDay,
vaporTeal,
theSixtiesUSA,
olive,
tokyoDark,
rose,
plum,
matrix,
travel,
...otherThemes
} = themes;
const themeList = [
original,
rainyDay,
vaporTeal,
theSixtiesUSA,
olive,
tokyoDark,
rose,
plum,
matrix,
travel,
...Object.values(otherThemes)
];
type ThemesProps = {
active?: boolean;
};
const Wrapper = styled.div<{ theme: Theme }>`
display: grid;
padding: 1em;
gap: 1em;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
grid-template-rows: repeat(auto-fill, 40px);
background-color: ${({ theme }) => theme.material};
`;
export function ThemeList({ active }: ThemesProps) {
const [themeName, setThemeName] = useAddonState(THEMES_ID, 'original');
const handleChoose = useCallback(
(newThemeName: string) => {
setThemeName(newThemeName);
},
[setThemeName]
);
if (!active) {
return <></>;
}
return (
<Wrapper key={THEMES_ID} theme={themes.original}>
{themeList.map(theme => (
<ThemeButton
active={themeName === theme.name}
key={theme.name}
onChoose={handleChoose}
theme={theme}
/>
))}
</Wrapper>
);
}
================================================
FILE: .storybook/theme-picker/ThemeProvider.tsx
================================================
import { useAddonState } from '@storybook/client-api';
import { DecoratorFn } from '@storybook/react';
import React from 'react';
import { ThemeProvider } from 'styled-components';
import themes from '../../src/common/themes/index';
import { THEMES_ID } from './constants';
export const withThemesProvider: DecoratorFn = story => {
const [themeName] = useAddonState(THEMES_ID, 'original');
return (
<ThemeProvider theme={themes[themeName] ?? themes.original}>
{story()}
</ThemeProvider>
);
};
================================================
FILE: .storybook/theme-picker/constants.ts
================================================
export const THEMES_ID = 'storybook/themes';
================================================
FILE: .storybook/theme-picker/register.ts
================================================
import addons, { makeDecorator, types } from '@storybook/addons';
import { THEMES_ID } from './constants';
import { ThemeList } from './ThemeList';
addons.register(THEMES_ID, () => {
addons.addPanel(`${THEMES_ID}/panel`, {
title: 'Themes',
type: types.PANEL,
render: ThemeList
});
});
export default makeDecorator({
name: 'withThemesProvider',
parameterName: 'theme',
wrapper: (getStory, context) => getStory(context)
});
================================================
FILE: .storybook/theme.js
================================================
import { create } from '@storybook/theming';
import brandImage from './logo.png';
export default create({
base: 'light',
brandTitle: 'React95',
brandUrl: 'https://react95.io',
brandImage,
brandTarget: '_self',
// UI
appBg: '#dfdfdf',
appContentBg: '#ffffff',
appBorderColor: '#848584',
appBorderRadius: 0,
// Typography
fontBase: '"ms_sans_serif", sans-serif',
fontCode: 'monospace',
// Text colors
textColor: '#0a0a0a',
textInverseColor: 'rgba(255,255,255,0.9)',
// Toolbar default and active colors
barTextColor: '#c6c6c6',
barSelectedColor: '#fefefe',
barBg: '#060084',
// Form colors
inputBg: '#ffffff',
inputBorder: '#dfdfdf',
inputTextColor: '#0a0a0a',
inputBorderRadius: 0
});
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2019 Artur Bień
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
<h1 align="center">React95</h1>
<p align="center">
<a href="https://www.npmjs.com/package/react95"><img src="https://flat.badgen.net/npm/dt/react95" alt="NPM" /></a>
<a href="https://github.com/arturbien/React95/actions/workflows/release.yml"><img src="https://github.com/arturbien/React95/actions/workflows/release.yml/badge.svg" alt="release status" /></a>
<a href="https://www.npmjs.com/package/react95"><img src="https://flat.badgen.net/npm/v/react95" alt="React95 version" /></a>
<a href="https://www.npmjs.com/package/react95"><img src="https://flat.badgen.net/npm/license/react95" alt="React95 license" /></a>
<a href="https://twitter.com/intent/follow?screen_name=react95_io"><img src="https://img.shields.io/twitter/follow/react95_io" alt="React95 license" /></a>
</p>
<h3 align="center">
<a href="https://storybook.react95.io/?path=/story/window--default">Components</a> -
<a href="https://coins95.web.app/">Demo app</a> -
<a href="https://github.com/react95-io/react95-native">React Native</a> -
<a href="https://join.slack.com/t/react95/shared_invite/enQtOTA1NzEyNjAyNTc4LWYxZjU3NWRiMWJlMGJiMjhkNzE2MDA3ZmZjZDc1YmY0ODdlZjMwZDA1NWJiYWExYmY1NTJmNmE4OWVjNWFhMTE">Slack</a> -
<a href="https://www.paypal.me/react95">PayPal donation 💰</a>
</h3>
<p align="center">
<b>Refreshed</b> Windows95 UI components for your modern React apps. <br /> Built with styled-components 💅</p>

### Support
- [Become a backer or sponsor on Patreon](https://www.patreon.com/arturbien)
- [One-time donation via PayPal](https://www.paypal.me/react95)
## Getting Started
First, install component library and styled-components in your project directory:
```sh
# yarn
$ yarn add react95 styled-components
# npm
$ npm install react95 styled-components
```
Apply style reset, wrap your app with ThemeProvider with theme of your choice... and you are ready to go! 🚀
```jsx
import React from 'react';
import { createGlobalStyle, ThemeProvider } from 'styled-components';
import { MenuList, MenuListItem, Separator, styleReset } from 'react95';
// pick a theme of your choice
import original from 'react95/dist/themes/original';
// original Windows95 font (optionally)
import ms_sans_serif from 'react95/dist/fonts/ms_sans_serif.woff2';
import ms_sans_serif_bold from 'react95/dist/fonts/ms_sans_serif_bold.woff2';
const GlobalStyles = createGlobalStyle`
${styleReset}
@font-face {
font-family: 'ms_sans_serif';
src: url('${ms_sans_serif}') format('woff2');
font-weight: 400;
font-style: normal
}
@font-face {
font-family: 'ms_sans_serif';
src: url('${ms_sans_serif_bold}') format('woff2');
font-weight: bold;
font-style: normal
}
body {
font-family: 'ms_sans_serif';
}
`;
const App = () => (
<div>
<GlobalStyles />
<ThemeProvider theme={original}>
<MenuList>
<MenuListItem>🎤 Sing</MenuListItem>
<MenuListItem>💃🏻 Dance</MenuListItem>
<Separator />
<MenuListItem disabled>😴 Sleep</MenuListItem>
</MenuList>
</ThemeProvider>
</div>
);
export default App;
```
### Submit your project
Apps built with React95 will be featured on the official React95 [website](https://react95.io) 🤟🏻
### Contributing
Any help from UI / UX designers would be EXTREMELY appreciated. The challenge is to come up with new component designs / layouts that are broadly used in modern UIs, that weren't present back in 95.
If you want to help with the project, feel free to open pull requests and submit issues or component proposals. Let's bring this UI back to life ♥️
================================================
FILE: docs/Contributing.stories.mdx
================================================
import { Meta } from '@storybook/addon-docs';
<Meta title='Docs/Contributing' />
# Contributing
Any help from UI/UX designers would be EXTREMELY appreciated. The challenge is
to come up with new component designs/layouts that are broadly used in modern
UIs, that weren't present back in 95.
If you want to help with the project, feel free to [open pull requests][1],
[submit issues or component proposals][2] and join our [Slack channels][3]!
Let's bring this UI back to life ♥️
[1]: https://github.com/arturbien/react95/pulls
[2]: https://github.com/arturbien/React95/issues
[3]: https://join.slack.com/t/react95/shared_invite/enQtOTA1NzEyNjAyNTc4LWYxZjU3NWRiMWJlMGJiMjhkNzE2MDA3ZmZjZDc1YmY0ODdlZjMwZDA1NWJiYWExYmY1NTJmNmE4OWVjNWFhMTE
================================================
FILE: docs/Getting-Started.stories.mdx
================================================
import { Meta } from '@storybook/addon-docs';
<Meta title='Docs/Getting Started' />
# Installation
React95 is available as an [npm package](https://www.npmjs.com/package/react95).
## npm
To install and save your `package.json` dependencies, run:
```sh
# yarn
yarn add react95 styled-components
# npm
npm install -S react95 styled-components
```
In order to have `react95` working properly, you'll also need
[styled-components 💅](https://github.com/styled-components/styled-components),
this way you can use custom themes and get the best of the library 🙂
## Usage
Apply style reset, wrap your app content with ThemeProvider with theme of your
choice... and you are ready to go! 🚀
```jsx
import React from 'react';
import { MenuList, MenuListItem, Separator, styleReset } from 'react95';
import { createGlobalStyle, ThemeProvider } from 'styled-components';
/* Pick a theme of your choice */
import original from 'react95/dist/themes/original';
/* Original Windows95 font (optional) */
import ms_sans_serif from 'react95/dist/fonts/ms_sans_serif.woff2';
import ms_sans_serif_bold from 'react95/dist/fonts/ms_sans_serif_bold.woff2';
const GlobalStyles = createGlobalStyle`
${styleReset}
@font-face {
font-family: 'ms_sans_serif';
src: url('${ms_sans_serif}') format('woff2');
font-weight: 400;
font-style: normal
}
@font-face {
font-family: 'ms_sans_serif';
src: url('${ms_sans_serif_bold}') format('woff2');
font-weight: bold;
font-style: normal
}
body, input, select, textarea {
font-family: 'ms_sans_serif';
}
`;
const App = () => (
<div>
<GlobalStyles />
<ThemeProvider theme={original}>
<MenuList>
<MenuListItem>🎤 Sing</MenuListItem>
<MenuListItem>💃🏻 Dance</MenuListItem>
<Separator />
<MenuListItem disabled>😴 Sleep</MenuListItem>
</MenuList>
</ThemeProvider>
</div>
);
export default App;
```
================================================
FILE: docs/Submit-your-Project.stories.mdx
================================================
import { Meta } from '@storybook/addon-docs';
<Meta title='Docs/Submit your Project' />
# Submit your Project
Apps built with React95 will be featured on the official React95 [website](https://react95.io) 🤟🏻
In order to submit your project, just drop a comment on [this issue](https://github.com/arturbien/React95/issues/25)!
================================================
FILE: docs/Welcome.stories.mdx
================================================
import { Meta } from '@storybook/addon-docs';
<Meta title='Docs/Welcome to React95' />
# Welcome to React95
<a href='https://www.npmjs.com/package/react95'>
<img src='https://flat.badgen.net/npm/dt/react95' alt='NPM' />
</a>
<a href='https://www.npmjs.com/package/react95'>
<img src='https://flat.badgen.net/npm/v/react95' alt='React95 version' />
</a>
<a href='https://www.npmjs.com/package/react95'>
<img
src='https://flat.badgen.net/npm/license/react95'
alt='React95 license'
/>
</a>
<h3>
<a href='?path=/story/controls-button--default'>Components</a>
-
<a href='https://coins95.web.app/'>Demo app</a>
-
<a href='https://react95.io/'>Website</a>
-
<a href='https://join.slack.com/t/react95/shared_invite/enQtOTA1NzEyNjAyNTc4LWYxZjU3NWRiMWJlMGJiMjhkNzE2MDA3ZmZjZDc1YmY0ODdlZjMwZDA1NWJiYWExYmY1NTJmNmE4OWVjNWFhMTE'>
Slack
</a>
-
<a href='https://www.paypal.me/react95'>PayPal donation 💰</a>
</h3>
**Refreshed** Windows 95 UI components for your modern React apps. Built with
[styled-components](https://github.com/styled-components/styled-components) 💅.

### Getting Started
Check out our [getting started](?path=/story/docs-getting-started--page) docs!
### Motivation
Create modern mobile/web applications with the retro and old school Windows 95 style. Our goal is not to exactly recreate Windows95 components, but to provide a solid component library for current scenarios.
### Support
- [Become a backer or sponsor on Patreon](https://www.patreon.com/arturbien)
- [One-time donation via PayPal](https://www.paypal.me/react95)
================================================
FILE: firebase.json
================================================
{
"hosting": {
"public": "storybook",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [
{
"source": "**",
"destination": "/index.html"
}
]
}
}
================================================
FILE: jest.config.js
================================================
module.exports = {
globals: {
'ts-jest': {
diagnostics: false,
isolatedModules: true
}
},
coverageReporters: ['text', 'html'],
preset: 'ts-jest/presets/default-esm',
setupFilesAfterEnv: ['<rootDir>/test/setup-test.ts'],
testEnvironment: 'jsdom'
};
================================================
FILE: package.json
================================================
{
"name": "react95",
"version": "0.0.0-development",
"description": "Refreshed Windows95 UI components for modern web apps - React95",
"keywords": [
"react",
"styled-components",
"windows95",
"components",
"vaporwave"
],
"author": "Artur Bień <artur.bien+react95@gmail.com> (https://www.linkedin.com/in/arturbien/)",
"funding": [
{
"type": "paypal",
"url": "https://www.paypal.me/react95"
},
{
"type": "patreon",
"url": "https://www.patreon.com/arturbien"
}
],
"license": "MIT",
"repository": "git@github.com:arturbien/React95.git",
"homepage": "https://react95.io",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"files": [
"/dist"
],
"publishConfig": {
"access": "public"
},
"scripts": {
"start": "cross-env STORYBOOK_DISPLAY_WARNING=true DISPLAY_WARNING=true start-storybook -p 9009 --no-open",
"build:storybook": "cross-env STORYBOOK_DISPLAY_WARNING=true DISPLAY_WARNING=true build-storybook -o ./storybook",
"build": "rm -rf dist && yarn run build:prod",
"build:dev": "cross-env NODE_ENV=development rollup -c",
"build:prod": "cross-env NODE_ENV=production rollup -c",
"test": "jest ./src",
"test:ci": "jest ./src --maxWorkers=2",
"test:watch": "jest ./src --watch",
"test:coverage": "jest ./src --coverage",
"typescript": "tsc --noEmit",
"lint": "eslint --ext .js,.ts,.tsx src",
"lint:fix": "yarn run lint --fix",
"semantic-release": "semantic-release",
"cz": "git-cz"
},
"peerDependencies": {
"react": ">= 16.8.0",
"react-dom": ">= 16.8.0",
"styled-components": ">= 5.3.3"
},
"devDependencies": {
"@babel/core": "^7.18.9",
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/plugin-proposal-decorators": "^7.18.10",
"@babel/plugin-proposal-export-default-from": "^7.18.10",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6",
"@babel/plugin-proposal-object-rest-spread": "^7.18.9",
"@babel/plugin-proposal-optional-chaining": "^7.18.9",
"@babel/plugin-proposal-private-methods": "^7.18.6",
"@babel/plugin-proposal-private-property-in-object": "^7.18.6",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-arrow-functions": "^7.18.6",
"@babel/plugin-transform-block-scoping": "^7.18.9",
"@babel/plugin-transform-classes": "^7.18.9",
"@babel/plugin-transform-destructuring": "^7.18.9",
"@babel/plugin-transform-for-of": "^7.18.8",
"@babel/plugin-transform-parameters": "^7.18.8",
"@babel/plugin-transform-shorthand-properties": "^7.18.6",
"@babel/plugin-transform-spread": "^7.18.9",
"@babel/preset-env": "^7.18.10",
"@babel/preset-typescript": "^7.18.6",
"@rollup/plugin-typescript": "^8.3.4",
"@storybook/addon-docs": "6.5.10",
"@storybook/addon-storysource": "6.5.10",
"@storybook/builder-webpack5": "^6.5.10",
"@storybook/manager-webpack5": "^6.5.10",
"@storybook/react": "6.5.10",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^12.1.5",
"@testing-library/react-hooks": "^8.0.1",
"@types/jest": "^28.1.6",
"@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6",
"@types/styled-components": "^5.1.25",
"@typescript-eslint/eslint-plugin": "^5.32.0",
"@typescript-eslint/parser": "^5.32.0",
"babel-plugin-macros": "^3.1.0",
"babel-plugin-polyfill-corejs3": "^0.5.3",
"commitizen": "^4.2.5",
"cross-env": "^7.0.3",
"cz-conventional-changelog": "^3.3.0",
"esbuild": "^0.14.53",
"eslint": "^8.21.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-prettier": "^8.5.0",
"eslint-import-resolver-typescript": "^3.4.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsx-a11y": "^6.6.1",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.30.1",
"eslint-plugin-react-hooks": "^4.6.0",
"firebase-tools": "^11.4.2",
"husky": "^8.0.1",
"jest": "^28.1.3",
"jest-environment-jsdom": "^28.1.3",
"jest-styled-components": "^7.0.8",
"lint-staged": "^13.0.3",
"prettier": "^2.7.1",
"react": "^17.0.2",
"react-docgen-typescript": "^2.2.2",
"react-dom": "^17.0.2",
"rimraf": "^3.0.2",
"rollup": "^2.77.2",
"rollup-plugin-copy": "^3.4.0",
"rollup-plugin-esbuild": "^4.9.1",
"rollup-plugin-replace": "^2.2.0",
"semantic-release": "^19.0.3",
"styled-components": "^5.3.5",
"ts-jest": "^28.0.7",
"typescript": "^4.7.4",
"webpack": "5"
},
"dependencies": {},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.js": [
"eslint --fix",
"prettier --write",
"git add"
]
},
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
}
}
================================================
FILE: rollup.config.js
================================================
import typescript from '@rollup/plugin-typescript';
import copy from 'rollup-plugin-copy';
import esbuild from 'rollup-plugin-esbuild';
import replace from 'rollup-plugin-replace';
const NODE_ENV = process.env.NODE_ENV || 'development';
const baseBundle = {
external: id => !/^[./]/.test(id),
plugins: [
replace({
'process.env.NODE_ENV': JSON.stringify(NODE_ENV)
}),
esbuild()
]
};
export default [
{
...baseBundle,
input: ['./src/index.ts', './src/types.ts'],
output: [
{
dir: 'dist',
entryFileNames: '[name].js',
exports: 'auto',
format: 'cjs',
preserveModules: true,
preserveModulesRoot: 'src'
},
{
dir: 'dist',
entryFileNames: '[name].mjs',
exports: 'auto',
format: 'es',
preserveModules: true,
preserveModulesRoot: 'src'
}
],
plugins: [
...baseBundle.plugins,
typescript({
tsconfig: './tsconfig.build.index.json',
declaration: true,
declarationDir: 'dist'
})
]
},
{
...baseBundle,
input: './src/common/themes/index.ts',
output: {
dir: 'dist/themes',
exports: 'default',
format: 'cjs',
preserveModules: true,
preserveModulesRoot: 'src/common/themes'
},
plugins: [
...baseBundle.plugins,
copy({
targets: [
{ src: './src/assets/fonts/dist/*', dest: './dist/fonts' },
{ src: './src/assets/images/*', dest: './dist/images' }
]
}),
typescript({
tsconfig: './tsconfig.build.themes.json',
declaration: true,
declarationDir: 'dist/themes'
})
]
}
];
================================================
FILE: src/Anchor/Anchor.spec.tsx
================================================
import React from 'react';
import { render } from '@testing-library/react';
import { Anchor } from './Anchor';
const defaultProps = {
children: '',
href: ''
};
describe('<Anchor />', () => {
it('should render href', () => {
const { container } = render(
<Anchor {...defaultProps} href='http://yoda.com' />
);
const anchorEl = container.firstChild;
expect(anchorEl).toHaveAttribute('href', 'http://yoda.com');
});
it('should render children', () => {
const { container } = render(
<Anchor {...defaultProps}>You shall pass</Anchor>
);
const anchorEl = container.firstChild;
expect(anchorEl).toHaveTextContent('You shall pass');
});
it('should render custom style', () => {
const { container } = render(
<Anchor {...defaultProps} style={{ color: 'papayawhip' }} />
);
const anchorEl = container.firstChild;
expect(anchorEl).toHaveAttribute('style', 'color: papayawhip;');
});
it('should render custom props', () => {
const customProps = { target: '_blank' };
const { container } = render(<Anchor {...defaultProps} {...customProps} />);
const anchorEl = container.firstChild;
expect(anchorEl).toHaveAttribute('target', '_blank');
});
});
================================================
FILE: src/Anchor/Anchor.stories.tsx
================================================
import { ComponentMeta } from '@storybook/react';
import React from 'react';
import { Anchor } from 'react95';
import styled from 'styled-components';
const Wrapper = styled.div`
padding: 5rem;
background: ${({ theme }) => theme.material};
`;
export default {
title: 'Typography/Anchor',
component: Anchor,
decorators: [story => <Wrapper>{story()}</Wrapper>]
} as ComponentMeta<typeof Anchor>;
export function Default() {
return (
<h1>
Everybody likes{' '}
<Anchor href='https://expensive.toys' target='_blank'>
https://expensive.toys
</Anchor>
</h1>
);
}
Default.story = {
name: 'default'
};
================================================
FILE: src/Anchor/Anchor.tsx
================================================
import React, { forwardRef } from 'react';
import styled from 'styled-components';
import { CommonStyledProps } from '../types';
type AnchorProps = {
children: React.ReactNode;
underline?: boolean;
} & React.AnchorHTMLAttributes<HTMLAnchorElement> &
CommonStyledProps;
const StyledAnchor = styled.a<{ underline: boolean }>`
color: ${({ theme }) => theme.anchor};
font-size: inherit;
text-decoration: ${({ underline }) => (underline ? 'underline' : 'none')};
&:visited {
color: ${({ theme }) => theme.anchorVisited};
}
`;
const Anchor = forwardRef<HTMLAnchorElement, AnchorProps>(
({ children, underline = true, ...otherProps }: AnchorProps, ref) => {
return (
<StyledAnchor ref={ref} underline={underline} {...otherProps}>
{children}
</StyledAnchor>
);
}
);
Anchor.displayName = 'Anchor';
export { Anchor, AnchorProps };
================================================
FILE: src/AppBar/AppBar.spec.tsx
================================================
import { render } from '@testing-library/react';
import React from 'react';
import { AppBar } from './AppBar';
const defaultProps = { children: '' };
describe('<AppBar />', () => {
it('should render header', () => {
const { container } = render(<AppBar {...defaultProps} />);
const headerEl = container.firstElementChild;
expect(headerEl && headerEl.tagName).toBe('HEADER');
});
it('should render children', () => {
const { container } = render(<AppBar>A nice app bar</AppBar>);
const headerEl = container.firstElementChild;
expect(headerEl).toHaveTextContent('A nice app bar');
});
it('should render fixed prop properly', () => {
const { container, rerender } = render(<AppBar {...defaultProps} fixed />);
const headerEl = container.firstElementChild;
expect(headerEl).toHaveStyleRule('position', 'fixed');
rerender(<AppBar {...defaultProps} fixed={false} />);
expect(headerEl).toHaveStyleRule('position', 'absolute');
});
it('should render position prop properly', () => {
const { container } = render(
<AppBar {...defaultProps} position='sticky' />
);
const headerEl = container.firstElementChild;
expect(headerEl).toHaveStyleRule('position', 'sticky');
});
it('should custom style', () => {
const { container } = render(
<AppBar {...defaultProps} style={{ backgroundColor: 'papayawhip' }} />
);
const headerEl = container.firstElementChild;
expect(headerEl).toHaveAttribute('style', 'background-color: papayawhip;');
});
it('should render custom props', () => {
const customProps = { title: 'cool-header' };
const { container } = render(<AppBar {...defaultProps} {...customProps} />);
const headerEl = container.firstElementChild;
expect(headerEl).toHaveAttribute('title', 'cool-header');
});
});
================================================
FILE: src/AppBar/AppBar.stories.tsx
================================================
import { ComponentMeta } from '@storybook/react';
import React, { useState } from 'react';
import {
AppBar,
Button,
MenuList,
MenuListItem,
Separator,
TextInput,
Toolbar
} from 'react95';
import styled from 'styled-components';
import logoIMG from '../assets/images/logo.png';
const Wrapper = styled.div`
padding: 5rem;
background: ${({ theme }) => theme.desktopBackground};
`;
export default {
title: 'Environment/AppBar',
component: AppBar,
decorators: [story => <Wrapper>{story()}</Wrapper>]
} as ComponentMeta<typeof AppBar>;
export function Default() {
const [open, setOpen] = useState(false);
return (
<AppBar>
<Toolbar style={{ justifyContent: 'space-between' }}>
<div style={{ position: 'relative', display: 'inline-block' }}>
<Button
onClick={() => setOpen(!open)}
active={open}
style={{ fontWeight: 'bold' }}
>
<img
src={logoIMG}
alt='react95 logo'
style={{ height: '20px', marginRight: 4 }}
/>
Start
</Button>
{open && (
<MenuList
style={{
position: 'absolute',
left: '0',
top: '100%'
}}
onClick={() => setOpen(false)}
>
<MenuListItem>
<span role='img' aria-label='👨💻'>
👨💻
</span>
Profile
</MenuListItem>
<MenuListItem>
<span role='img' aria-label='📁'>
📁
</span>
My account
</MenuListItem>
<Separator />
<MenuListItem disabled>
<span role='img' aria-label='🔙'>
🔙
</span>
Logout
</MenuListItem>
</MenuList>
)}
</div>
<TextInput placeholder='Search...' width={150} />
</Toolbar>
</AppBar>
);
}
Default.story = {
name: 'default'
};
================================================
FILE: src/AppBar/AppBar.tsx
================================================
import React, { forwardRef } from 'react';
import styled, { CSSProperties } from 'styled-components';
import { createBorderStyles, createBoxStyles } from '../common';
import { CommonStyledProps } from '../types';
type AppBarProps = {
children: React.ReactNode;
/** @deprecated Use `position` instead */
fixed?: boolean;
position?: CSSProperties['position'];
} & React.HTMLAttributes<HTMLElement> &
CommonStyledProps;
const StyledAppBar = styled.header<AppBarProps>`
${createBorderStyles()};
${createBoxStyles()};
position: ${props => props.position ?? (props.fixed ? 'fixed' : 'absolute')};
top: 0;
right: 0;
left: auto;
display: flex;
flex-direction: column;
width: 100%;
`;
const AppBar = forwardRef<HTMLElement, AppBarProps>(
({ children, fixed = true, position = 'fixed', ...otherProps }, ref) => {
return (
<StyledAppBar
fixed={fixed}
position={fixed !== false ? position : undefined}
ref={ref}
{...otherProps}
>
{children}
</StyledAppBar>
);
}
);
AppBar.displayName = 'AppBar';
export { AppBar, AppBarProps };
================================================
FILE: src/Avatar/Avatar.spec.tsx
================================================
import { render } from '@testing-library/react';
import React from 'react';
import { renderWithTheme, theme } from '../../test/utils';
import { Avatar } from './Avatar';
describe('<Avatar />', () => {
it('should render component', () => {
const { container } = render(<Avatar />);
expect(container).toBeInTheDocument();
});
it('should render children', () => {
const { container } = render(<Avatar>Avatar children</Avatar>);
const avatarEl = container.firstElementChild;
expect(avatarEl && avatarEl.innerHTML).toBe('Avatar children');
});
it('should handle border properly', () => {
const { container, rerender } = renderWithTheme(
<Avatar noBorder={false} />
);
const avatarEl = container.firstElementChild;
expect(avatarEl).toHaveStyleRule(
'border-top',
`2px solid ${theme.borderDark}`
);
rerender(<Avatar noBorder />);
expect(avatarEl).not.toHaveStyleRule('border-top', '');
});
it('should handle square properly', () => {
const { container, rerender } = render(<Avatar square />);
const avatarEl = container.firstElementChild;
expect(avatarEl).toHaveStyleRule('border-radius', '0');
rerender(<Avatar square={false} />);
expect(avatarEl).toHaveStyleRule('border-radius', '50%');
});
it('should render with source', async () => {
const catGif = 'https://cdn2.thecatapi.com/images/1ac.gif';
const { findByAltText } = render(<Avatar src={catGif} alt='cat avatar' />);
const imageEl = (await findByAltText('cat avatar')) as HTMLImageElement;
expect(imageEl && imageEl.src).toBe(catGif);
});
it('should render source with priority over children', async () => {
const catGif = 'https://cdn2.thecatapi.com/images/1ac.gif';
const { queryByText } = render(
<Avatar src={catGif} alt='cat avatar'>
Cats are cool
</Avatar>
);
const content = await queryByText(/cats are cool/i);
expect(content).toBeNull();
});
describe('prop: size', () => {
it('should set proper size', () => {
const { container } = renderWithTheme(<Avatar size='85%' />);
const avatarEl = container.firstElementChild;
expect(avatarEl).toHaveStyleRule('width', '85%');
expect(avatarEl).toHaveStyleRule('height', '85%');
});
it('when passed a number, sets size in px', () => {
const { container } = renderWithTheme(<Avatar size={25} />);
const avatarEl = container.firstElementChild;
expect(avatarEl).toHaveStyleRule('width', '25px');
expect(avatarEl).toHaveStyleRule('height', '25px');
});
});
});
================================================
FILE: src/Avatar/Avatar.stories.tsx
================================================
import { ComponentMeta } from '@storybook/react';
import React from 'react';
import { Avatar } from 'react95';
import styled from 'styled-components';
const Wrapper = styled.div`
padding: 5rem;
background: ${({ theme }) => theme.material};
& > div > * {
margin-right: 1rem;
}
`;
export default {
title: 'Other/Avatar',
component: Avatar,
decorators: [story => <Wrapper>{story()}</Wrapper>]
} as ComponentMeta<typeof Avatar>;
export function Default() {
return (
<div style={{ display: 'inline-flex' }}>
<Avatar size={50} src='https://placekitten.com/100/100' />
<Avatar noBorder size={50} src='https://placedog.net/100/100' />
<Avatar size={50} style={{ background: 'palevioletred' }}>
AK
</Avatar>
<Avatar square size={50}>
<span role='img' aria-label='🚀'>
🚀
</span>
</Avatar>
</div>
);
}
Default.story = {
name: 'default'
};
================================================
FILE: src/Avatar/Avatar.tsx
================================================
import React, { forwardRef } from 'react';
import styled from 'styled-components';
import { getSize } from '../common/utils';
import { CommonStyledProps } from '../types';
type AvatarProps = {
alt?: string;
children?: React.ReactNode;
noBorder?: boolean;
size?: string | number;
square?: boolean;
src?: string;
} & React.HTMLAttributes<HTMLDivElement> &
CommonStyledProps;
const StyledAvatar = styled.div<
Pick<AvatarProps, 'noBorder' | 'square' | 'src'> & { size?: string }
>`
display: inline-block;
box-sizing: border-box;
object-fit: contain;
${({ size }) =>
`
height: ${size};
width: ${size};
`}
border-radius: ${({ square }) => (square ? 0 : '50%')};
overflow: hidden;
${({ noBorder, theme }) =>
!noBorder &&
`
border-top: 2px solid ${theme.borderDark};
border-left: 2px solid ${theme.borderDark};
border-bottom: 2px solid ${theme.borderLightest};
border-right: 2px solid ${theme.borderLightest};
background: ${theme.material};
`}
${({ src }) =>
!src &&
`
display: flex;
align-items: center;
justify-content: space-around;
font-weight: bold;
font-size: 1rem;
`}
`;
const StyledAvatarImg = styled.img`
display: block;
object-fit: contain;
width: 100%;
height: 100%;
`;
const Avatar = forwardRef<HTMLDivElement, AvatarProps>(
(
{
alt = '',
children,
noBorder = false,
size = 35,
square = false,
src,
...otherProps
},
ref
) => {
return (
<StyledAvatar
noBorder={noBorder}
ref={ref}
size={getSize(size)}
square={square}
src={src}
{...otherProps}
>
{src ? <StyledAvatarImg src={src} alt={alt} /> : children}
</StyledAvatar>
);
}
);
Avatar.displayName = 'Avatar';
export { Avatar, AvatarProps };
================================================
FILE: src/Button/Button.spec.tsx
================================================
import { fireEvent, render } from '@testing-library/react';
import React from 'react';
import { renderWithTheme, theme } from '../../test/utils';
import { blockSizes } from '../common/system';
import { Button } from './Button';
const defaultProps = {
children: 'click me'
};
describe('<Button />', () => {
it('should render button', () => {
const { getByRole } = render(<Button {...defaultProps} />);
const button = getByRole('button');
expect(button).toBeInTheDocument();
expect(button.tagName).toBe('BUTTON');
});
it('should handle different types', () => {
const { getByRole } = render(<Button {...defaultProps} type='submit' />);
const button = getByRole('button');
expect(button).toHaveAttribute('type', 'submit');
});
it('should handle click properly', () => {
const onButtonClick = jest.fn();
const { getByRole } = render(
<Button {...defaultProps} onClick={onButtonClick} />
);
const button = getByRole('button');
fireEvent.click(button);
expect(onButtonClick).toHaveBeenCalled();
});
it('should handle disabled for all variants', () => {
const { getByRole, rerender } = renderWithTheme(
<Button {...defaultProps} disabled />
);
const button = getByRole('button');
const disabledTextShadow = `1px 1px ${theme.materialTextDisabledShadow}`;
expect(button).toHaveStyleRule('color', theme.materialTextDisabled);
expect(button).toHaveStyleRule('text-shadow', disabledTextShadow);
rerender(<Button {...defaultProps} variant='menu' />);
expect(button).toHaveStyleRule('color', theme.materialTextDisabled);
expect(button).toHaveStyleRule('text-shadow', disabledTextShadow);
rerender(<Button {...defaultProps} variant='flat' />);
expect(button).toHaveStyleRule('color', theme.materialTextDisabled);
expect(button).toHaveStyleRule('text-shadow', disabledTextShadow);
rerender(<Button {...defaultProps} variant='thin' />);
expect(button).toHaveStyleRule('color', theme.materialTextDisabled);
expect(button).toHaveStyleRule('text-shadow', disabledTextShadow);
});
it('should handle fullWidth prop', () => {
const { getByRole, rerender } = render(
<Button {...defaultProps} fullWidth />
);
const button = getByRole('button');
expect(button).toHaveStyleRule('width', '100%');
rerender(<Button {...defaultProps} fullWidth={false} />);
expect(button).toHaveStyleRule('width', 'auto');
});
it('should handle button sizes properly', () => {
const { getByRole, rerender } = render(
<Button {...defaultProps} size='sm' />
);
const button = getByRole('button');
expect(button).toHaveStyleRule('height', blockSizes.sm);
rerender(<Button {...defaultProps} size='lg' />);
expect(button).toHaveStyleRule('height', blockSizes.lg);
});
it('should handle square prop', () => {
const { getByRole } = render(<Button {...defaultProps} square size='md' />);
const button = getByRole('button');
expect(button).toHaveStyleRule('padding', '0');
expect(button).toHaveStyleRule('width', blockSizes.md);
});
it('should render children', () => {
const { getByRole } = render(<Button {...defaultProps} />);
const button = getByRole('button');
expect(button.innerHTML).toBe('click me');
});
describe('prop: disabled', () => {
it('should render disabled', () => {
const { getByRole } = render(<Button {...defaultProps} disabled />);
const button = getByRole('button');
expect(button).toHaveAttribute('disabled');
});
it('should not fire click when disabled', () => {
const onButtonClick = jest.fn();
const { getByRole } = render(<Button {...defaultProps} disabled />);
const button = getByRole('button');
fireEvent.click(button);
expect(onButtonClick).not.toHaveBeenCalled();
});
});
});
================================================
FILE: src/Button/Button.stories.tsx
================================================
import { ComponentMeta } from '@storybook/react';
import React, { useState } from 'react';
import {
Button,
MenuList,
MenuListItem,
ScrollView,
Separator,
Toolbar,
Window,
WindowContent,
WindowHeader
} from 'react95';
import styled from 'styled-components';
const Wrapper = styled.div`
padding: 5rem;
background: ${({ theme }) => theme.material};
#default-buttons button {
margin-bottom: 1rem;
margin-right: 1rem;
}
#cutout {
background: ${({ theme }) => theme.canvas};
padding: 1rem;
width: 300px;
}
`;
export default {
title: 'Controls/Button',
component: Button,
decorators: [story => <Wrapper>{story()}</Wrapper>]
} as ComponentMeta<typeof Button>;
export function Default() {
return (
<div id='default-buttons'>
<Button>Default</Button>
<br />
<Button primary>Primary</Button>
<br />
<Button disabled>Disabled</Button>
<br />
<Button active>Active</Button>
<br />
<Button square>
<span role='img' aria-label='recycle'>
♻︎
</span>
</Button>
<br />
<Button fullWidth>Full width</Button>
<br />
<Button size='sm'>Size small</Button>
<Button size='lg'>Size large</Button>
</div>
);
}
Default.story = {
name: 'default'
};
export function Raised() {
return (
<div id='default-buttons'>
<Button variant='raised'>Default</Button>
<br />
<Button variant='raised' primary>
Primary
</Button>
<br />
<Button variant='raised' disabled>
Disabled
</Button>
<br />
<Button variant='raised' active>
Active
</Button>
<br />
<Button variant='raised' square>
<span role='img' aria-label='recycle'>
♻︎
</span>
</Button>
<br />
<Button variant='raised' fullWidth>
Full width
</Button>
<br />
<Button variant='raised' size='sm'>
Size small
</Button>
<Button variant='raised' size='lg'>
Size large
</Button>
</div>
);
}
Raised.story = {
name: 'raised'
};
export function Flat() {
return (
<Window>
<WindowContent>
<ScrollView id='cutout'>
<p style={{ lineHeight: 1.3 }}>
When you want to use Buttons on a light background (like scrollable
content), just use the flat variant:
</p>
<div
style={{
marginTop: '1.5rem'
}}
>
<Toolbar>
<Button variant='flat' primary style={{ marginRight: '0.5rem' }}>
Primary
</Button>
<Button variant='flat' style={{ marginRight: '0.5rem' }}>
Regular
</Button>
<Button variant='flat' disabled>
Disabled
</Button>
</Toolbar>
</div>
</ScrollView>
</WindowContent>
</Window>
);
}
Flat.story = {
name: 'flat'
};
const imageSrc =
'https://image.freepik.com/foto-gratuito/la-frutta-fresca-del-kiwi-tagliata-a-meta-con-la-decorazione-completa-del-pezzo-e-bella-sulla-tavola-di-legno_47436-1.jpg';
export function Thin() {
const [open, setOpen] = useState(false);
return (
<Window style={{ maxWidth: '250px' }}>
<WindowHeader>
<span role='img' aria-label='Kiwi'>
🥝
</span>
Kiwi.app
</WindowHeader>
<Toolbar noPadding>
<Button variant='thin' disabled>
Upload
</Button>
<Button variant='thin' disabled>
Save
</Button>
<div
style={{
position: 'relative',
display: 'inline-block',
alignSelf: 'left'
}}
>
<Button
variant='thin'
onClick={() => setOpen(!open)}
size='sm'
active={open}
>
Share
</Button>
{open && (
<MenuList
style={{
position: 'absolute',
right: '0',
top: '100%',
zIndex: '9999'
}}
onClick={() => setOpen(false)}
>
<MenuListItem size='sm'>Copy link</MenuListItem>
<Separator />
<MenuListItem size='sm'>Facebook</MenuListItem>
<MenuListItem size='sm'>Twitter</MenuListItem>
<MenuListItem size='sm'>Instagram</MenuListItem>
<Separator />
<MenuListItem size='sm' disabled>
MySpace
</MenuListItem>
</MenuList>
)}
</div>
</Toolbar>
<WindowContent style={{ padding: '0.25rem' }}>
<ScrollView>
<img
style={{ width: '100%', height: '1uto', display: 'block' }}
src={imageSrc}
alt='kiwi'
/>
</ScrollView>
</WindowContent>
</Window>
);
}
Thin.story = {
name: 'thin'
};
================================================
FILE: src/Button/Button.tsx
================================================
import React, { forwardRef } from 'react';
import styled, { css } from 'styled-components';
import {
createBorderStyles,
createBoxStyles,
createDisabledTextStyles,
createFlatBoxStyles,
createHatchedBackground,
focusOutline
} from '../common';
import { blockSizes } from '../common/system';
import { noOp } from '../common/utils';
import { CommonStyledProps, Sizes } from '../types';
type ButtonProps = {
active?: boolean;
children?: React.ReactNode;
disabled?: boolean;
fullWidth?: boolean;
onClick?: React.ButtonHTMLAttributes<HTMLButtonElement>['onClick'];
onTouchStart?: React.ButtonHTMLAttributes<HTMLButtonElement>['onTouchStart'];
primary?: boolean;
size?: Sizes;
square?: boolean;
type?: string;
} & (
| {
variant?: 'default' | 'raised' | 'flat' | 'thin';
}
| {
/** @deprecated Use `thin` */
variant?: 'menu';
}
) &
Omit<
React.ButtonHTMLAttributes<HTMLButtonElement>,
'disabled' | 'onClick' | 'onTouchStart' | 'type'
> &
CommonStyledProps;
type StyledButtonProps = Pick<
ButtonProps,
| 'active'
| 'disabled'
| 'fullWidth'
| 'primary'
| 'size'
| 'square'
| 'variant'
>;
const commonButtonStyles = css<StyledButtonProps>`
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
height: ${({ size = 'md' }) => blockSizes[size]};
width: ${({ fullWidth, size = 'md', square }) =>
fullWidth ? '100%' : square ? blockSizes[size] : 'auto'};
padding: ${({ square }) => (square ? 0 : `0 10px`)};
font-size: 1rem;
user-select: none;
&:active {
padding-top: ${({ disabled }) => !disabled && '2px'};
}
padding-top: ${({ active, disabled }) => active && !disabled && '2px'};
&:after {
content: '';
position: absolute;
display: block;
top: 0;
left: 0;
height: 100%;
width: 100%;
}
&:not(:disabled) {
cursor: pointer;
}
font-family: inherit;
`;
export const StyledButton = styled.button<StyledButtonProps>`
${({ active, disabled, primary, theme, variant }) =>
variant === 'flat'
? css`
${createFlatBoxStyles()}
${primary
? `
border: 2px solid ${theme.checkmark};
outline: 2px solid ${theme.flatDark};
outline-offset: -4px;
`
: `
border: 2px solid ${theme.flatDark};
outline: 2px solid transparent;
outline-offset: -4px;
`}
&:focus:after, &:active:after {
${!active && !disabled && focusOutline}
outline-offset: -4px;
}
`
: variant === 'menu' || variant === 'thin'
? css`
${createBoxStyles()};
border: 2px solid transparent;
&:hover,
&:focus {
${!disabled &&
!active &&
createBorderStyles({ style: 'buttonThin' })}
}
&:active {
${!disabled && createBorderStyles({ style: 'buttonThinPressed' })}
}
${active && createBorderStyles({ style: 'buttonThinPressed' })}
${disabled && createDisabledTextStyles()}
`
: css`
${createBoxStyles()};
border: none;
${disabled && createDisabledTextStyles()}
${active
? createHatchedBackground({
mainColor: theme.material,
secondaryColor: theme.borderLightest
})
: ''}
&:before {
box-sizing: border-box;
content: '';
position: absolute;
${primary
? css`
left: 2px;
top: 2px;
width: calc(100% - 4px);
height: calc(100% - 4px);
outline: 2px solid ${theme.borderDarkest};
`
: css`
left: 0;
top: 0;
width: 100%;
height: 100%;
`}
${active
? createBorderStyles({
style: variant === 'raised' ? 'window' : 'button',
invert: true
})
: createBorderStyles({
style: variant === 'raised' ? 'window' : 'button',
invert: false
})}
}
&:active:before {
${!disabled &&
createBorderStyles({
style: variant === 'raised' ? 'window' : 'button',
invert: true
})}
}
&:focus:after,
&:active:after {
${!disabled && focusOutline}
outline-offset: -8px;
}
&:active:focus:after,
&:active:after {
top: ${active ? '0' : '1px'};
}
`}
${commonButtonStyles}
`;
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(
{
onClick,
disabled = false,
children,
type = 'button',
fullWidth = false,
size = 'md',
square = false,
active = false,
onTouchStart = noOp,
primary = false,
variant = 'default',
...otherProps
},
ref
) => {
return (
<StyledButton
active={active}
disabled={disabled}
$disabled={disabled}
fullWidth={fullWidth}
onClick={disabled ? undefined : onClick}
onTouchStart={onTouchStart}
primary={primary}
ref={ref}
size={size}
square={square}
type={type}
variant={variant}
{...otherProps}
>
{children}
</StyledButton>
);
}
);
Button.displayName = 'Button';
export { Button, ButtonProps };
================================================
FILE: src/Checkbox/Checkbox.spec.tsx
================================================
import React from 'react';
import { renderWithTheme } from '../../test/utils';
import { Checkbox } from './Checkbox';
describe('<Checkbox />', () => {
describe('label', () => {
it('renders', () => {
const labelText = 'Swag';
const { getByLabelText } = renderWithTheme(
<Checkbox label={labelText} />
);
expect(getByLabelText(labelText)).toBeInTheDocument();
});
});
describe('prop: onChange', () => {
it('should call onChange when uncontrolled', () => {
const handleChange = jest.fn(event => event.target.checked);
const { getByRole } = renderWithTheme(
<Checkbox onChange={handleChange} />
);
getByRole('checkbox').click();
expect(handleChange).toHaveBeenCalledTimes(1);
// event.target.check is true
expect(handleChange.mock.results[0].value).toBe(true);
});
it('should call onChange when controlled', () => {
const checked = true;
const handleChange = jest.fn(event => event.target.checked);
const { getByRole } = renderWithTheme(
<Checkbox onChange={handleChange} checked={checked} />
);
getByRole('checkbox').click();
expect(handleChange).toHaveBeenCalledTimes(1);
expect(handleChange.mock.results[0].value).toBe(!checked);
});
});
describe('prop: disabled', () => {
it('should disable checkbox', () => {
const handleChange = jest.fn();
const { getByRole } = renderWithTheme(
<Checkbox disabled onChange={handleChange} />
);
const checkbox = getByRole('checkbox');
expect(checkbox).toHaveAttribute('disabled');
checkbox.click();
expect(handleChange).not.toHaveBeenCalled();
});
it('should be overridden by props', () => {
const { getByRole, rerender } = renderWithTheme(<Checkbox disabled />);
rerender(<Checkbox disabled={false} />);
const checkbox = getByRole('checkbox');
expect(checkbox).not.toHaveAttribute('disabled');
});
});
describe('prop: indeterminate', () => {
it('renders indeterminate state', () => {
const { getByRole } = renderWithTheme(<Checkbox indeterminate />);
const checkbox = getByRole('checkbox');
// don't set native 'indeterminate' attribute because
// different browsers treat it differently
// instead we're setting 'data-indeterminate' attribute
expect(checkbox).toHaveAttribute('data-indeterminate');
expect(checkbox).not.toHaveAttribute('indeterminate');
expect(getByRole('presentation').firstChild).toHaveAttribute(
'data-testid',
'indeterminateIcon'
);
});
it('replaces checked icon', () => {
const { getByRole, rerender } = renderWithTheme(<Checkbox checked />);
expect(getByRole('checkbox')).toHaveAttribute(
'data-indeterminate',
'false'
);
rerender(<Checkbox checked indeterminate />);
expect(getByRole('checkbox')).toHaveAttribute(
'data-indeterminate',
'true'
);
expect(getByRole('presentation').firstChild).toHaveAttribute(
'data-testid',
'indeterminateIcon'
);
});
});
describe('controlled', () => {
it('should check the checkbox', () => {
const { getByRole, rerender } = renderWithTheme(
<Checkbox checked={false} />
);
rerender(<Checkbox checked />);
const checkbox = getByRole('checkbox') as HTMLInputElement;
expect(checkbox.checked).toBe(true);
expect(getByRole('checkbox')).toHaveAttribute('checked');
expect(getByRole('presentation').firstChild).toHaveAttribute(
'data-testid',
'checkmarkIcon'
);
});
it('should uncheck the checkbox', () => {
const { getByRole, rerender } = renderWithTheme(<Checkbox checked />);
rerender(<Checkbox checked={false} />);
const checkbox = getByRole('checkbox') as HTMLInputElement;
expect(checkbox.checked).toBe(false);
expect(getByRole('checkbox')).not.toHaveAttribute('checked');
expect(getByRole('presentation').firstChild).toBeNull();
// check if proper icon was rendered
});
});
describe('uncontrolled', () => {
it('can change checked state uncontrolled starting from defaultChecked', () => {
const { getByRole } = renderWithTheme(<Checkbox defaultChecked />);
const checkbox = getByRole('checkbox') as HTMLInputElement;
expect(checkbox.checked).toBe(true);
checkbox.click();
expect(checkbox.checked).toBe(false);
checkbox.click();
expect(checkbox.checked).toBe(true);
});
});
});
================================================
FILE: src/Checkbox/Checkbox.stories.tsx
================================================
import React, { useState } from 'react';
import styled from 'styled-components';
import { ComponentMeta } from '@storybook/react';
import { Checkbox, GroupBox, ScrollView } from 'react95';
const Wrapper = styled.div`
background: ${({ theme }) => theme.material};
padding: 5rem;
#cutout {
background: ${({ theme }) => theme.canvas};
padding: 1rem;
width: 250px;
display: flex;
align-items: center;
justify-content: space-around;
}
`;
export default {
title: 'Controls/Checkbox',
component: Checkbox,
decorators: [story => <Wrapper>{story()}</Wrapper>]
} as ComponentMeta<typeof Checkbox>;
export function Default() {
const [state, setState] = useState({
cheese: true,
bacon: false,
broccoli: false
});
const { cheese, bacon, broccoli } = state;
const ingredientsArr = Object.values(state).map(val => (val ? 1 : 0));
const possibleIngredients = Object.keys(state).length;
const chosenIngredients = ingredientsArr.reduce<number>((a, b) => a + b, 0);
const isIndeterminate = ![0, possibleIngredients].includes(chosenIngredients);
const toggleAll = () => {
console.log(ingredientsArr);
if (isIndeterminate) {
setState({
cheese: true,
bacon: true,
broccoli: true
});
} else if (ingredientsArr[0] === 1) {
setState({
cheese: false,
bacon: false,
broccoli: false
});
} else {
setState({
cheese: true,
bacon: true,
broccoli: true
});
}
};
const toggleIngredient = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value as 'cheese' | 'bacon' | 'broccoli';
setState({
...state,
[value]: !state[value]
});
};
return (
<div style={{ maxWidth: '250px' }}>
<GroupBox label='Pizza toppings'>
<Checkbox
name='allToppings'
label='All'
value='allToppings'
indeterminate={isIndeterminate}
checked={
!isIndeterminate && chosenIngredients === possibleIngredients
}
onChange={toggleAll}
/>
<div style={{ paddingLeft: '1.5rem' }}>
<Checkbox
checked={!!cheese}
onChange={toggleIngredient}
value='cheese'
label='🧀 Extra cheese'
name='ingredients'
/>
<br />
<Checkbox
checked={!!bacon}
onChange={toggleIngredient}
value='bacon'
label='🥓 Bacon'
name='ingredients'
/>
<br />
<Checkbox
checked={!!broccoli}
onChange={toggleIngredient}
value='broccoli'
label='🥦 Broccoli'
name='ingredients'
/>
</div>
</GroupBox>
<Checkbox
name='shipping'
value='shipping'
label='Free shipping'
defaultChecked
disabled
style={{ marginTop: '1rem' }}
/>
</div>
);
}
Default.story = {
name: 'default'
};
export function Flat() {
const [state, setState] = useState({
cheese: true,
bacon: false,
broccoli: false
});
const { cheese, bacon, broccoli } = state;
const ingredientsArr = Object.values(state).map(val => (val ? 1 : 0));
const possibleIngredients = Object.keys(state).length;
const chosenIngredients = ingredientsArr.reduce<number>((a, b) => a + b, 0);
const isIndeterminate = ![0, possibleIngredients].includes(chosenIngredients);
const toggleAll = () => {
console.log(ingredientsArr);
if (isIndeterminate) {
setState({
cheese: true,
bacon: true,
broccoli: true
});
} else if (ingredientsArr[0] === 1) {
setState({
cheese: false,
bacon: false,
broccoli: false
});
} else {
setState({
cheese: true,
bacon: true,
broccoli: true
});
}
};
const toggleIngredient = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value as 'cheese' | 'bacon' | 'broccoli';
setState({
...state,
[value]: !state[value]
});
};
return (
<ScrollView id='cutout'>
<div style={{ maxWidth: '250px' }}>
<GroupBox variant='flat' label='Pizza toppings'>
<Checkbox
variant='flat'
name='allToppings'
label='All'
value='allToppings'
indeterminate={isIndeterminate}
checked={
!isIndeterminate && chosenIngredients === possibleIngredients
}
onChange={toggleAll}
/>
<div style={{ paddingLeft: '1.5rem' }}>
<Checkbox
variant='flat'
checked={!!cheese}
onChange={toggleIngredient}
value='cheese'
label='🧀 Extra cheese'
name='ingredients'
/>
<br />
<Checkbox
variant='flat'
checked={!!bacon}
onChange={toggleIngredient}
value='bacon'
label='🥓 Bacon'
name='ingredients'
/>
<br />
<Checkbox
variant='flat'
checked={!!broccoli}
onChange={toggleIngredient}
value='broccoli'
label='🥦 Broccoli'
name='ingredients'
/>
</div>
</GroupBox>
<Checkbox
variant='flat'
name='shipping'
value='shipping'
label='Free shipping'
defaultChecked
disabled
style={{ marginTop: '1rem' }}
/>
</div>
</ScrollView>
);
}
Flat.story = {
name: 'flat'
};
================================================
FILE: src/Checkbox/Checkbox.tsx
================================================
import React, { forwardRef, useCallback } from 'react';
import styled, { css } from 'styled-components';
import { createHatchedBackground } from '../common';
import useControlledOrUncontrolled from '../common/hooks/useControlledOrUncontrolled';
import {
LabelText,
size,
StyledInput,
StyledLabel
} from '../common/SwitchBase';
import { noOp } from '../common/utils';
import { StyledScrollView } from '../ScrollView/ScrollView';
import { CommonThemeProps } from '../types';
type CheckboxProps = {
checked?: boolean;
className?: string;
defaultChecked?: boolean;
disabled?: boolean;
indeterminate?: boolean;
label?: number | string;
name?: string;
onChange?: React.ChangeEventHandler<HTMLInputElement>;
style?: React.CSSProperties;
value?: number | string;
variant?: 'default' | 'flat';
} & Omit<
React.InputHTMLAttributes<HTMLInputElement>,
| 'checked'
| 'className'
| 'defaultChecked'
| 'disabled'
| 'label'
| 'name'
| 'onChange'
| 'style'
| 'value'
>;
type CheckmarkProps = {
$disabled: boolean;
variant: 'default' | 'flat';
};
const sharedCheckboxStyles = css`
width: ${size}px;
height: ${size}px;
display: flex;
align-items: center;
justify-content: space-around;
margin-right: 0.5rem;
`;
const StyledCheckbox = styled(StyledScrollView)<CommonThemeProps>`
${sharedCheckboxStyles}
width: ${size}px;
height: ${size}px;
background: ${({ $disabled, theme }) =>
$disabled ? theme.material : theme.canvas};
&:before {
box-shadow: none;
}
`;
const StyledFlatCheckbox = styled.div<CommonThemeProps>`
position: relative;
box-sizing: border-box;
display: inline-block;
background: ${({ $disabled, theme }) =>
$disabled ? theme.flatLight : theme.canvas};
${sharedCheckboxStyles}
width: ${size - 4}px;
height: ${size - 4}px;
outline: none;
border: 2px solid ${({ theme }) => theme.flatDark};
background: ${({ $disabled, theme }) =>
$disabled ? theme.flatLight : theme.canvas};
`;
const CheckmarkIcon = styled.span.attrs(() => ({
'data-testid': 'checkmarkIcon'
}))<CheckmarkProps>`
display: inline-block;
position: relative;
width: 100%;
height: 100%;
&:after {
content: '';
display: block;
position: absolute;
left: 50%;
top: calc(50% - 1px);
width: 3px;
height: 7px;
border: solid
${({ $disabled, theme }) =>
$disabled ? theme.checkmarkDisabled : theme.checkmark};
border-width: 0 3px 3px 0;
transform: translate(-50%, -50%) rotate(45deg);
border-color: ${p =>
p.$disabled ? p.theme.checkmarkDisabled : p.theme.checkmark};
}
`;
const IndeterminateIcon = styled.span.attrs(() => ({
'data-testid': 'indeterminateIcon'
}))<CheckmarkProps>`
display: inline-block;
position: relative;
width: 100%;
height: 100%;
&:after {
content: '';
display: block;
width: 100%;
height: 100%;
${({ $disabled, theme }) =>
createHatchedBackground({
mainColor: $disabled ? theme.checkmarkDisabled : theme.checkmark
})}
background-position: 0px 0px, 2px 2px;
}
`;
const CheckboxComponents = {
flat: StyledFlatCheckbox,
default: StyledCheckbox
};
const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
(
{
checked,
className = '',
defaultChecked = false,
disabled = false,
indeterminate = false,
label = '',
onChange = noOp,
style = {},
value,
variant = 'default',
...otherProps
},
ref
) => {
const [state, setState] = useControlledOrUncontrolled({
defaultValue: defaultChecked,
onChange,
readOnly: otherProps.readOnly ?? disabled,
value: checked
});
const handleChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const newState = e.target.checked;
setState(newState);
onChange(e);
},
[onChange, setState]
);
const CheckboxComponent = CheckboxComponents[variant];
let Icon = null;
if (indeterminate) {
Icon = IndeterminateIcon;
} else if (state) {
Icon = CheckmarkIcon;
}
return (
<StyledLabel $disabled={disabled} className={className} style={style}>
<StyledInput
disabled={disabled}
onChange={disabled ? undefined : handleChange}
readOnly={disabled}
type='checkbox'
value={value}
checked={state}
data-indeterminate={indeterminate}
ref={ref}
{...otherProps}
/>
<CheckboxComponent $disabled={disabled} role='presentation'>
{Icon && <Icon $disabled={disabled} variant={variant} />}
</CheckboxComponent>
{label && <LabelText>{label}</LabelText>}
</StyledLabel>
);
}
);
Checkbox.displayName = 'Checkbox';
export { Checkbox, CheckboxProps };
================================================
FILE: src/ColorInput/ColorInput.spec.tsx
================================================
import { fireEvent } from '@testing-library/react';
import React from 'react';
import { renderWithTheme } from '../../test/utils';
import { ColorInput } from './ColorInput';
function rgb2hex(str: string) {
const rgb = str.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);
function hex(x: string) {
return `0${parseInt(x, 10).toString(16)}`.slice(-2);
}
return rgb ? `#${hex(rgb[1])}${hex(rgb[2])}${hex(rgb[3])}` : '';
}
describe('<ColorInput />', () => {
it('should call handlers', () => {
const color = '#f0f0dd';
const onChange = jest.fn();
const { container } = renderWithTheme(<ColorInput onChange={onChange} />);
const input = container.querySelector(`[type="color"]`) as HTMLInputElement;
fireEvent.change(input, { target: { value: color } });
expect(onChange).toBeCalledTimes(1);
});
it('should properly pass value to input element', () => {
const color = '#f0f0dd';
const { container } = renderWithTheme(<ColorInput value={color} />);
const input = container.querySelector(`[type="color"]`) as HTMLInputElement;
expect(input.value).toBe(color);
});
it('should display current color', () => {
const color = '#f0f0dd';
const { getByRole } = renderWithTheme(<ColorInput value={color} />);
const colorPreview = getByRole('presentation');
const displayedColor = window.getComputedStyle(
colorPreview,
null
).background;
const displayedColorHex = rgb2hex(displayedColor);
expect(displayedColorHex).toBe(color);
});
describe('prop: disabled', () => {
it('should render disabled input', () => {
const { container } = renderWithTheme(<ColorInput disabled />);
const input = container.querySelector(`[type="color"]`);
expect(input).toHaveAttribute('disabled');
});
it('should be overridden by props', () => {
const { container, rerender } = renderWithTheme(<ColorInput disabled />);
rerender(<ColorInput disabled={false} />);
const input = container.querySelector(`[type="color"]`);
expect(input).not.toHaveAttribute('disabled');
});
});
});
================================================
FILE: src/ColorInput/ColorInput.stories.tsx
================================================
import { ComponentMeta } from '@storybook/react';
import React from 'react';
import styled from 'styled-components';
import { ColorInput, ScrollView } from 'react95';
const Wrapper = styled.div`
background: ${({ theme }) => theme.material};
padding: 5rem;
& > span {
margin-left: 1rem;
margin-right: 0.5rem;
}
#cutout {
background: ${({ theme }) => theme.canvas};
display: inline-block;
}
.content {
padding: 1rem;
& > div {
margin: 1rem 0;
}
& > div > span {
margin-right: 0.5rem;
}
}
`;
export default {
title: 'Controls/ColorInput',
component: ColorInput,
decorators: [story => <Wrapper>{story()}</Wrapper>]
} as ComponentMeta<typeof ColorInput>;
export function Default() {
return (
<>
<span>enabled: </span>
<ColorInput defaultValue='#00f' />
<span>disabled: </span>
<ColorInput disabled defaultValue='#00f' />
</>
);
}
Default.story = {
name: 'default'
};
export function Flat() {
return (
<ScrollView id='cutout'>
<div className='content'>
<div>
<span>enabled: </span>
<ColorInput variant='flat' defaultValue='#00f' />
</div>
<div>
<span>disabled: </span>
<ColorInput variant='flat' disabled defaultValue='#00f' />
</div>
</div>
</ScrollView>
);
}
Flat.story = {
name: 'flat'
};
================================================
FILE: src/ColorInput/ColorInput.tsx
================================================
import React, { forwardRef } from 'react';
import styled, { css } from 'styled-components';
import { StyledButton } from '../Button/Button';
import { focusOutline } from '../common';
import useControlledOrUncontrolled from '../common/hooks/useControlledOrUncontrolled';
import { noOp } from '../common/utils';
import { Separator } from '../Separator/Separator';
import { CommonStyledProps } from '../types';
type ColorInputProps = {
defaultValue?: string;
disabled?: boolean;
onChange?: React.ChangeEventHandler<HTMLInputElement>;
value?: string;
variant?: 'default' | 'flat';
} & Omit<
React.InputHTMLAttributes<HTMLInputElement>,
'defaultValue' | 'disabled' | 'onChange' | 'value'
> &
CommonStyledProps;
const Trigger = styled(StyledButton)`
padding-left: 8px;
`;
const StyledSeparator = styled(Separator)`
height: 21px;
position: relative;
top: 0;
`;
export const StyledColorInput = styled.input`
box-sizing: border-box;
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
opacity: 0;
z-index: 1;
cursor: pointer;
&:disabled {
cursor: default;
}
`;
// TODO replace with SVG icon
const ColorPreview = styled.div<{
color: string;
$disabled: boolean;
}>`
box-sizing: border-box;
height: 19px;
display: inline-block;
width: 35px;
margin-right: 5px;
background: ${({ color }) => color};
${({ $disabled }) =>
$disabled
? css`
border: 2px solid ${({ theme }) => theme.materialTextDisabled};
filter: drop-shadow(
1px 1px 0px ${({ theme }) => theme.materialTextDisabledShadow}
);
`
: css`
border: 2px solid ${({ theme }) => theme.materialText};
`}
${StyledColorInput}:focus:not(:active) + &:after {
content: '';
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
${focusOutline}
outline-offset: -8px;
}
`;
const ChevronIcon = styled.span<
Required<Pick<ColorInputProps, 'variant'>> & {
$disabled: boolean;
}
>`
width: 0px;
height: 0px;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
display: inline-block;
margin-left: 6px;
${({ $disabled }) =>
$disabled
? css`
border-top: 6px solid ${({ theme }) => theme.materialTextDisabled};
filter: drop-shadow(
1px 1px 0px ${({ theme }) => theme.materialTextDisabledShadow}
);
`
: css`
border-top: 6px solid ${({ theme }) => theme.materialText};
`}
&:after {
content: '';
box-sizing: border-box;
position: absolute;
top: ${({ variant }) => (variant === 'flat' ? '6px' : '8px')};
right: 8px;
width: 16px;
height: 19px;
}
`;
// TODO make sure all aria and role attributes are in place
const ColorInput = forwardRef<HTMLInputElement, ColorInputProps>(
(
{
value,
defaultValue,
onChange = noOp,
disabled = false,
variant = 'default',
...otherProps
},
ref
) => {
const [valueDerived, setValueState] = useControlledOrUncontrolled({
defaultValue,
onChange,
readOnly: otherProps.readOnly ?? disabled,
value
});
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const color = e.target.value;
setValueState(color);
onChange(e);
};
return (
// we only need button styles, so we display
// it as a div and reset type attribute
<Trigger disabled={disabled} as='div' variant={variant} size='md'>
<StyledColorInput
onChange={handleChange}
readOnly={disabled}
disabled={disabled}
value={valueDerived ?? '#008080'}
type='color'
ref={ref}
{...otherProps}
/>
<ColorPreview
$disabled={disabled}
color={valueDerived ?? '#008080'}
role='presentation'
/>
{variant === 'default' && <StyledSeparator orientation='vertical' />}
<ChevronIcon $disabled={disabled} variant={variant} />
</Trigger>
);
}
);
ColorInput.displayName = 'ColorInput';
export { ColorInput, ColorInputProps };
================================================
FILE: src/Counter/Counter.spec.tsx
================================================
import React from 'react';
import { renderWithTheme } from '../../test/utils';
import { Counter } from './Counter';
describe('<Counter />', () => {
it('should render', () => {
const { container } = renderWithTheme(<Counter />);
const counter = container.firstElementChild;
expect(counter).toBeInTheDocument();
});
it('should handle custom style', () => {
const { container } = renderWithTheme(
<Counter style={{ backgroundColor: 'papayawhip' }} />
);
const counter = container.firstElementChild;
expect(counter).toHaveAttribute('style', 'background-color: papayawhip;');
});
it('should handle custom props', () => {
const customProps: React.HTMLAttributes<HTMLDivElement> = {
title: 'potatoe'
};
const { container } = renderWithTheme(<Counter {...customProps} />);
const counter = container.firstElementChild;
expect(counter).toHaveAttribute('title', 'potatoe');
});
describe('prop: minLength', () => {
it('renders correct number of digits', () => {
const { container } = renderWithTheme(
<Counter value={32} minLength={7} />
);
const counter = container.firstElementChild;
expect(counter && counter.childElementCount).toBe(7);
});
it('value length takes priority if bigger than minLength', () => {
const { container } = renderWithTheme(
<Counter value={1234} minLength={2} />
);
const counter = container.firstElementChild;
expect(counter && counter.childElementCount).toBe(4);
});
});
});
================================================
FILE: src/Counter/Counter.stories.tsx
================================================
import { ComponentMeta } from '@storybook/react';
import React, { useState } from 'react';
import { Button, Counter, Frame } from 'react95';
import styled from 'styled-components';
const Wrapper = styled.div`
padding: 5rem;
background: ${({ theme }) => theme.desktopBackground};
.counter-wrapper {
display: flex;
margin-top: 1rem;
}
.counter-wrapper button {
margin-left: 0.5rem;
height: 51px;
}
.wrapper {
padding: 1rem;
}
`;
export default {
title: 'Other/Counter',
component: Counter,
decorators: [story => <Wrapper>{story()}</Wrapper>]
} as ComponentMeta<typeof Counter>;
export function Default() {
const [count, setCount] = useState(13);
const handleClick = () => setCount(count + 1);
return (
<Frame className='wrapper'>
<Counter value={123456789} minLength={11} size='lg' />
<div className='counter-wrapper'>
<Counter value={count} minLength={3} />
<Button onClick={handleClick}>Click!</Button>
</div>
</Frame>
);
}
Default.story = {
name: 'default'
};
================================================
FILE: src/Counter/Counter.tsx
================================================
import React, { forwardRef, useMemo } from 'react';
import styled from 'styled-components';
import { createBorderStyles } from '../common';
import { CommonStyledProps, Sizes } from '../types';
import { Digit } from './Digit';
type CounterProps = {
minLength?: number;
size?: Sizes | 'xl';
value?: number;
} & React.HTMLAttributes<HTMLDivElement> &
CommonStyledProps;
const CounterWrapper = styled.div`
${createBorderStyles({ style: 'status' })}
display: inline-flex;
background: #000000;
`;
const pixelSizes = {
sm: 1,
md: 2,
lg: 3,
xl: 4
};
const Counter = forwardRef<HTMLDivElement, CounterProps>(
({ value = 0, minLength = 3, size = 'md', ...otherProps }, ref) => {
const digits = useMemo(
() => value.toString().padStart(minLength, '0').split(''),
[minLength, value]
);
return (
<CounterWrapper ref={ref} {...otherProps}>
{digits.map((digit, i) => (
<Digit digit={digit} pixelSize={pixelSizes[size]} key={i} />
))}
</CounterWrapper>
);
}
);
Counter.displayName = 'Counter';
export { Counter, CounterProps };
================================================
FILE: src/Counter/Digit.tsx
================================================
import React from 'react';
import styled, { css } from 'styled-components';
import { createHatchedBackground } from '../common';
import { CommonStyledProps } from '../types';
type DigitProps = {
pixelSize?: number;
digit?: number | string;
} & React.HTMLAttributes<HTMLDivElement> &
CommonStyledProps;
const DigitWrapper = styled.div<Required<Pick<DigitProps, 'pixelSize'>>>`
position: relative;
--react95-digit-primary-color: #ff0102;
--react95-digit-secondary-color: #740201;
--react95-digit-bg-color: #000000;
${({ pixelSize }) => css`
width: ${11 * pixelSize}px;
height: ${21 * pixelSize}px;
margin: ${pixelSize}px;
span,
span:before,
span:after {
box-sizing: border-box;
display: inline-block;
position: absolute;
}
span.active,
span.active:before,
span.active:after {
background: var(--react95-digit-primary-color);
}
span:not(.active),
span:not(.active):before,
span:not(.active):after {
${createHatchedBackground({
mainColor: 'var(--react95-digit-bg-color)',
secondaryColor: 'var(--react95-digit-secondary-color)',
pixelSize
})}
}
span.horizontal,
span.horizontal:before,
span.horizontal:after {
height: ${pixelSize}px;
border-left: ${pixelSize}px solid var(--react95-digit-bg-color);
border-right: ${pixelSize}px solid var(--react95-digit-bg-color);
}
span.horizontal.active,
span.horizontal.active:before,
span.horizontal.active:after {
height: ${pixelSize}px;
border-left: ${pixelSize}px solid var(--react95-digit-primary-color);
border-right: ${pixelSize}px solid var(--react95-digit-primary-color);
}
span.horizontal {
left: ${pixelSize}px;
width: ${9 * pixelSize}px;
}
span.horizontal:before {
content: '';
width: 100%;
top: ${pixelSize}px;
left: ${0}px;
}
span.horizontal:after {
content: '';
width: calc(100% - ${pixelSize * 2}px);
top: ${2 * pixelSize}px;
left: ${pixelSize}px;
}
span.horizontal.top {
top: 0;
}
span.horizontal.bottom {
bottom: 0;
transform: rotateX(180deg);
}
span.center,
span.center:before,
span.center:after {
height: ${pixelSize}px;
border-left: ${pixelSize}px solid var(--react95-digit-bg-color);
border-right: ${pixelSize}px solid var(--react95-digit-bg-color);
}
span.center.active,
span.center.active:before,
span.center.active:after {
border-left: ${pixelSize}px solid var(--react95-digit-primary-color);
border-right: ${pixelSize}px solid var(--react95-digit-primary-color);
}
span.center {
top: 50%;
transform: translateY(-50%);
left: ${pixelSize}px;
width: ${9 * pixelSize}px;
}
span.center:before,
span.center:after {
content: '';
width: 100%;
}
span.center:before {
top: ${pixelSize}px;
}
span.center:after {
bottom: ${pixelSize}px;
}
span.vertical,
span.vertical:before,
span.vertical:after {
width: ${pixelSize}px;
border-top: ${pixelSize}px solid var(--react95-digit-bg-color);
border-bottom: ${pixelSize}px solid var(--react95-digit-bg-color);
}
span.vertical {
height: ${11 * pixelSize}px;
}
span.vertical.left {
left: 0;
}
span.vertical.right {
right: 0;
transform: rotateY(180deg);
}
span.vertical.top {
top: 0px;
}
span.vertical.bottom {
bottom: 0px;
}
span.vertical:before {
content: '';
height: 100%;
top: ${0}px;
left: ${pixelSize}px;
}
span.vertical:after {
content: '';
height: calc(100% - ${pixelSize * 2}px);
top: ${pixelSize}px;
left: ${pixelSize * 2}px;
}
`}
`;
const segments = [
'horizontal top',
'center',
'horizontal bottom',
'vertical top left',
'vertical top right',
'vertical bottom left',
'vertical bottom right'
];
const digitActiveSegments = [
[1, 0, 1, 1, 1, 1, 1], // 0
[0, 0, 0, 0, 1, 0, 1], // 1
[1, 1, 1, 0, 1, 1, 0], // 2
[1, 1, 1, 0, 1, 0, 1], // 3
[0, 1, 0, 1, 1, 0, 1], // 4
[1, 1, 1, 1, 0, 0, 1], // 5
[1, 1, 1, 1, 0, 1, 1], // 6
[1, 0, 0, 0, 1, 0, 1], // 7
[1, 1, 1, 1, 1, 1, 1], // 8
[1, 1, 1, 1, 1, 0, 1] // 9
];
function Digit({ digit = 0, pixelSize = 2, ...otherProps }: DigitProps) {
const segmentClasses = digitActiveSegments[Number(digit)].map((isActive, i) =>
isActive ? `${segments[i]} active` : segments[i]
);
return (
<DigitWrapper pixelSize={pixelSize} {...otherProps}>
{segmentClasses.map((className, i) => (
<span className={className} key={i} />
))}
</DigitWrapper>
);
}
export { Digit, DigitProps };
================================================
FILE: src/DatePicker/DatePicker.stories.tsx
================================================
/* eslint-disable camelcase, react/jsx-pascal-case */
import { ComponentMeta } from '@storybook/react';
import React from 'react';
import { DatePicker__UNSTABLE } from 'react95';
import styled from 'styled-components';
const Wrapper = styled.div`
padding: 5rem;
background: ${({ theme }) => theme.desktopBackground};
`;
export default {
title: 'DatePicker__UNSTABLE',
component: DatePicker__UNSTABLE,
decorators: [story => <Wrapper>{story()}</Wrapper>]
} as ComponentMeta<typeof DatePicker__UNSTABLE>;
export function Default() {
return <DatePicker__UNSTABLE onAccept={date => console.log(date)} />;
}
Default.story = {
name: 'default'
};
================================================
FILE: src/DatePicker/DatePicker.tsx
================================================
import React, { forwardRef, useCallback, useMemo, useState } from 'react';
import styled from 'styled-components';
import { Button } from '../Button/Button';
import { NumberInput } from '../NumberInput/NumberInput';
import { ScrollView } from '../ScrollView/ScrollView';
import { Select } from '../Select/Select';
import { Toolbar } from '../Toolbar/Toolbar';
import { Window, WindowContent, WindowHeader } from '../Window/Window';
type DatePickerProps = {
className?: string;
date?: string;
onAccept?: (chosenDate: string) => void;
onCancel?: React.MouseEventHandler<HTMLButtonElement>;
shadow?: boolean;
};
const Calendar = styled(ScrollView)`
width: 234px;
margin: 1rem 0;
background: ${({ theme }) => theme.canvas};
`;
const WeekDays = styled.div`
display: flex;
background: ${({ theme }) => theme.materialDark};
color: #dfe0e3;
`;
const Dates = styled.div`
display: flex;
flex-wrap: wrap;
`;
const DateItem = styled.div`
text-align: center;
height: 1.5em;
line-height: 1.5em;
width: 14.28%;
`;
const DateItemContent = styled.span<{ active: boolean }>`
cursor: pointer;
background: ${({ active, theme }) =>
active ? theme.hoverBackground : 'transparent'};
color: ${({ active, theme }) =>
active ? theme.canvasTextInvert : theme.canvasText};
&:hover {
border: 2px dashed
${({ theme, active }) => (active ? 'none' : theme.materialDark)};
}
`;
const months = [
{ value: 0, label: 'January' },
{ value: 1, label: 'February' },
{ value: 2, label: 'March' },
{ value: 3, label: 'April' },
{ value: 4, label: 'May' },
{ value: 5, label: 'June' },
{ value: 6, label: 'July' },
{ value: 7, label: 'August' },
{ value: 8, label: 'September' },
{ value: 9, label: 'October' },
{ value: 10, label: 'November' },
{ value: 11, label: 'December' }
];
function daysInMonth(year: number, month: number) {
return new Date(year, month + 1, 0).getDate();
}
function dayIndex(year: number, month: number, day: number) {
return new Date(year, month, day).getDay();
}
function convertDateToState(stringDate: string) {
const date = new Date(Date.parse(stringDate));
const day = date.getUTCDate();
const month = date.getUTCMonth();
const year = date.getUTCFullYear();
return { day, month, year };
}
const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(
(
{
className,
date: initialDate = new Date().toISOString(),
onAccept,
onCancel,
shadow = true
},
ref
) => {
const [date, setDate] = useState(() => convertDateToState(initialDate));
const { year, month, day } = date;
const handleMonthSelect = useCallback(
({ value: monthSelected }: { value: number }) => {
setDate(currentDate => ({ ...currentDate, month: monthSelected }));
},
[]
);
const handleYearSelect = useCallback((yearSelected: number) => {
setDate(currentDate => ({ ...currentDate, year: yearSelected }));
}, []);
const handleDaySelect = useCallback((daySelected: number) => {
setDate(currentDate => ({ ...currentDate, day: daySelected }));
}, []);
const handleAccept = useCallback(() => {
const chosenDate = [date.year, date.month + 1, date.day]
.map(part => String(part).padStart(2, '0'))
.join('-');
onAccept?.(chosenDate);
}, [date.day, date.month, date.year, onAccept]);
const dayPickerItems = useMemo(() => {
const items: React.ReactNode[] = Array.from({ length: 42 });
const firstDayIndex = dayIndex(year, month, 1);
let itemDay = day;
const daysNumber = daysInMonth(year, month);
itemDay = itemDay < daysNumber ? itemDay : daysNumber;
items.forEach((_, i) => {
if (i >= firstDayIndex && i < daysNumber + firstDayIndex) {
const dayNumber = i - firstDayIndex + 1;
items[i] = (
<DateItem
key={i}
onClick={() => {
handleDaySelect(dayNumber);
}}
>
<DateItemContent active={dayNumber === itemDay}>
{dayNumber}
</DateItemContent>
</DateItem>
);
} else {
items[i] = <DateItem key={i} />;
}
});
return items;
}, [day, handleDaySelect, month, year]);
return (
<Window
className={className}
ref={ref}
shadow={shadow}
style={{ margin: 20 }}
>
<WindowHeader>
<span role='img' aria-label='📆'>
📆
</span>
Date
</WindowHeader>
<WindowContent>
<Toolbar noPadding style={{ justifyContent: 'space-between' }}>
<Select
options={months}
value={month}
onChange={handleMonthSelect}
width={128}
menuMaxHeight={200}
/>
<NumberInput value={year} onChange={handleYearSelect} width={100} />
</Toolbar>
<Calendar>
<WeekDays>
<DateItem>S</DateItem>
<DateItem>M</DateItem>
<DateItem>T</DateItem>
<DateItem>W</DateItem>
<DateItem>T</DateItem>
<DateItem>F</DateItem>
<DateItem>S</DateItem>
</WeekDays>
<Dates>{dayPickerItems}</Dates>
</Calendar>
<Toolbar noPadding style={{ justifyContent: 'space-between' }}>
<Button fullWidth onClick={onCancel} disabled={!onCancel}>
Cancel
</Button>
<Button
fullWidth
onClick={onAccept ? handleAccept : undefined}
disabled={!onAccept}
>
OK
</Button>
</Toolbar>
</WindowContent>
</Window>
);
}
);
DatePicker.displayName = 'DatePicker';
// eslint-disable-next-line camelcase
export { DatePicker as DatePicker__UNSTABLE, DatePickerProps };
================================================
FILE: src/Frame/Frame.spec.tsx
================================================
import { render } from '@testing-library/react';
import React from 'react';
import { Frame } from './Frame';
describe('<Frame />', () => {
it('should render frame', () => {
const { container } = render(<Frame />);
const frame = container.firstElementChild;
expect(frame).toBeInTheDocument();
});
it('should render custom styles', () => {
const { container } = render(
<Frame style={{ backgroundColor: 'papayawhip' }} />
);
const frame = container.firstElementChild;
expect(frame).toHaveAttribute('style', 'background-color: papayawhip;');
});
it('should render children', async () => {
const { findByText } = render(
<Frame>
<span>Cool frame</span>
</Frame>
);
const content = await findByText(/cool frame/i);
expect(content).toBeInTheDocument();
});
it('should render custom props', () => {
const customProps = { title: 'frame' };
const { container } = render(<Frame {...customProps} />);
const frame = container.firstElementChild;
expect(frame).toHaveAttribute('title', 'frame');
});
});
================================================
FILE: src/Frame/Frame.stories.tsx
================================================
import { ComponentMeta } from '@storybook/react';
import React from 'react';
import { Frame } from 'react95';
import styled from 'styled-components';
const Wrapper = styled.div`
padding: 5rem;
background: ${({ theme }) => theme.material};
#default-buttons button {
margin-bottom: 1rem;
margin-right: 1rem;
}
#cutout {
background: ${({ theme }) => theme.canvas};
padding: 1rem;
width: 300px;
}
`;
export default {
title: 'Layout/Frame',
component: Frame,
decorators: [story => <Wrapper>{story()}</Wrapper>]
} as ComponentMeta<typeof Frame>;
export function Default() {
return (
<Frame
variant='outside'
shadow
style={{ padding: '0.5rem', lineHeight: '1.5', width: 600 }}
>
<p style={{ padding: '0.5rem' }}>
This is a frame of the 'window' variant, the default. Notice
the subtle difference in borders. The lightest border is not on the edge
of this frame.
</p>
<Frame variant='inside' style={{ margin: '1rem', padding: '1rem' }}>
This frame of the 'button' variant on the other hand has the
lightest border on the edge. Use this frame inside 'window'
frames.
<br />
<Frame
variant='field'
style={{
marginTop: '1rem',
padding: '1rem',
height: 200,
width: 100
}}
>
A field frame variant is used to display content.
</Frame>
</Frame>
<Frame
variant='well'
style={{ marginTop: '1rem', padding: '0.1rem 0.25rem', width: '100%' }}
>
The 'status' variant of a frame is often used as a status bar
at the end of the window.
</Frame>
</Frame>
);
}
Default.story = {
name: 'default'
};
================================================
FILE: src/Frame/Frame.tsx
================================================
import React, { forwardRef } from 'react';
import styled, { css } from 'styled-components';
import { createBorderStyles, createBoxStyles } from '../common';
import { CommonStyledProps } from '../types';
type FrameProps = {
children?: React.ReactNode;
shadow?: boolean;
} & (
| {
variant?: 'window' | 'button' | 'field' | 'status';
}
| {
/** @deprecated Use 'window', 'button' or 'status' */
variant?: 'outside' | 'inside' | 'well';
}
) &
React.HTMLAttributes<HTMLDivElement> &
CommonStyledProps;
const createFrameStyles = (variant: FrameProps['variant']) => {
switch (variant) {
case 'status':
case 'well':
return css`
${createBorderStyles({ style: 'status' })}
`;
case 'window':
case 'outside':
return css`
${createBorderStyles({ style: 'window' })}
`;
case 'field':
return css`
${createBorderStyles({ style: 'field' })}
`;
default:
return css`
${createBorderStyles()}
`;
}
};
const StyledFrame = styled.div<Required<Pick<FrameProps, 'variant'>>>`
position: relative;
font-size: 1rem;
${({ variant }) => createFrameStyles(variant)}
${({ variant }) =>
createBoxStyles(
variant === 'field'
? { background: 'canvas', color: 'canvasText' }
: undefined
)}
`;
const Frame = forwardRef<HTMLDivElement, FrameProps>(
({ children, shadow = false, variant = 'window', ...otherProps }, ref) => {
return (
<StyledFrame ref={ref} shadow={shadow} variant={variant} {...otherProps}>
{children}
</StyledFrame>
);
}
);
Frame.displayName = 'Frame';
export { Frame, FrameProps };
================================================
FILE: src/GroupBox/GroupBox.spec.tsx
================================================
import React from 'react';
import { renderWithTheme, theme } from '../../test/utils';
import { GroupBox } from './GroupBox';
describe('<GroupBox />', () => {
it('renders GroupBox', () => {
const { container } = renderWithTheme(<GroupBox />);
const groupBox = container.firstChild as HTMLFieldSetElement;
expect(groupBox).toBeInTheDocument();
});
it('renders children', () => {
const textContent = 'Hi there!';
const { getByText } = renderWithTheme(
<GroupBox>
<span>{textContent}</span>
</GroupBox>
);
expect(getByText(textContent)).toBeInTheDocument();
});
describe('prop: label', () => {
it('renders Label', () => {
const labelText = 'Name:';
const { container } = renderWithTheme(<GroupBox label={labelText} />);
const groupBox = container.firstChild as HTMLFieldSetElement;
const legend = groupBox.querySelector('legend');
expect(legend?.textContent).toBe(labelText);
});
it('when not provided, <legend /> element is not rendered', () => {
const { container } = renderWithTheme(<GroupBox />);
const groupBox = container.firstChild as HTMLFieldSetElement;
const legend = groupBox.querySelector('legend');
expect(legend).not.toBeInTheDocument();
});
});
describe('prop: disabled', () => {
it('renders with disabled text content', () => {
const { container } = renderWithTheme(<GroupBox disabled />);
const groupBox = container.firstChild as HTMLFieldSetElement;
expect(groupBox).toHaveAttribute('aria-disabled', 'true');
expect(groupBox).toHaveStyleRule('color', theme.materialTextDisabled);
expect(groupBox).toHaveStyleRule(
'text-shadow',
`1px 1px ${theme.materialTextDisabledShadow}`
);
});
});
});
================================================
FILE: src/GroupBox/GroupBox.stories.tsx
================================================
import { ComponentMeta } from '@storybook/react';
import React, { useState } from 'react';
import { Checkbox, GroupBox, ScrollView, Window, WindowContent } from 'react95';
import styled from 'styled-components';
const Wrapper = styled.div`
padding: 5rem;
background: ${({ theme }) => theme.desktopBackground};
`;
export default {
title: 'Controls/GroupBox',
component: GroupBox,
decorators: [story => <Wrapper>{story()}</Wrapper>]
} as ComponentMeta<typeof GroupBox>;
export function Default() {
return (
<Window>
<WindowContent>
<GroupBox label='Label here'>
Some content here
<span role='img' aria-label='😍'>
😍
</span>
</GroupBox>
<br />
<GroupBox label='Disabled' disabled>
Some content here
<span role='img' aria-label='😍'>
😍
</span>
</GroupBox>
</WindowContent>
</Window>
);
}
Default.story = {
name: 'default'
};
export function Flat() {
return (
<Window>
<WindowContent>
<ScrollView
style={{ padding: '1rem', background: 'white', width: '300px' }}
>
<GroupBox variant='flat' label='Label here'>
Some content here
<span role='img' aria-label='😍'>
😍
</span>
</GroupBox>
<br />
<GroupBox variant='flat' label='Disabled' disabled>
Some content here
<span role='img' aria-label='😍'>
😍
</span>
</GroupBox>
</ScrollView>
</WindowContent>
</Window>
);
}
Flat.story = {
name: 'flat'
};
export function ToggleExample() {
const [state, setState] = useState(true);
return (
<Window>
<WindowContent>
<GroupBox
disabled={state}
label={
<Checkbox
style={{ margin: 0 }}
label='Enable'
checked={!state}
onChange={() => setState(!state)}
/>
}
>
Some content here
<span role='img' aria-label='emoji in love'>
😍
</span>
</GroupBox>
</WindowContent>
</Window>
);
}
ToggleExample.story = {
name: 'toggle example'
};
================================================
FILE: src/GroupBox/GroupBox.tsx
================================================
import React, { forwardRef } from 'react';
import styled, { css } from 'styled-components';
import { createDisabledTextStyles } from '../common';
import { CommonStyledProps } from '../types';
type GroupBoxProps = {
label?: React.ReactNode;
children?: React.ReactNode;
disabled?: boolean;
variant?: 'default' | 'flat';
} & React.FieldsetHTMLAttributes<HTMLFieldSetElement> &
CommonStyledProps;
const StyledFieldset = styled.fieldset<
Pick<GroupBoxProps, 'variant'> & { $disabled: boolean }
>`
position: relative;
border: 2px solid
${({ theme, variant }) =>
variant === 'flat' ? theme.flatDark : theme.borderLightest};
padding: 16px;
margin-top: 8px;
font-size: 1rem;
color: ${({ theme }) => theme.materialText};
${({ variant }) =>
variant !== 'flat' &&
css`
box-shadow: -1px -1px 0 1px ${({ theme }) => theme.borderDark},
inset -1px -1px 0 1px ${({ theme }) => theme.borderDark};
`}
${props => props.$disabled && createDisabledTextStyles()}
`;
const StyledLegend = styled.legend<Pick<GroupBoxProps, 'variant'>>`
display: flex;
position: absolute;
top: 0;
left: 8px;
transform: translateY(calc(-50% - 2px));
padding: 0 8px;
font-size: 1rem;
background: ${({ theme, variant }) =>
variant === 'flat' ? theme.canvas : theme.material};
`;
const GroupBox = forwardRef<HTMLFieldSetElement, GroupBoxProps>(
(
{ label, disabled = false, variant = 'default', children, ...otherProps },
ref
) => {
return (
<StyledFieldset
aria-disabled={disabled}
$disabled={disabled}
variant={variant}
ref={ref}
{...otherProps}
>
{label && <StyledLegend variant={variant}>{label}</StyledLegend>}
{children}
</StyledFieldset>
);
}
);
GroupBox.displayName = 'GroupBox';
export { GroupBox, GroupBoxProps };
================================================
FILE: src/Handle/Handle.spec.tsx
================================================
import React from 'react';
import { renderWithTheme } from '../../test/utils';
import { Handle } from './Handle';
describe('<Handle />', () => {
it('should render bar', () => {
const { container } = renderWithTheme(<Handle />);
const barEl = container.firstChild;
expect(barEl).toBeInTheDocument();
});
it('should handle custom style', () => {
const { container } = renderWithTheme(
<Handle style={{ backgroundColor: 'papayawhip' }} />
);
const barEl = container.firstChild;
expect(barEl).toHaveAttribute('style', 'background-color: papayawhip;');
});
it('should handle custom props', () => {
const customProps = { title: 'potatoe' };
const { container } = renderWithTheme(<Handle {...customProps} />);
const barEl = container.firstChild;
expect(barEl).toHaveAttribute('title', 'potatoe');
});
describe('prop: size', () => {
it('should set proper size', () => {
const { container } = renderWithTheme(<Handle size='85%' />);
const barEl = container.firstChild;
expect(barEl).toHaveStyleRule('height', '85%');
});
it('when passed a number, sets size in px', () => {
const { container } = renderWithTheme(<Handle size={25} />);
const barEl = container.firstChild;
expect(barEl).toHaveStyleRule('height', '25px');
});
});
});
================================================
FILE: src/Handle/Handle.stories.tsx
================================================
import { ComponentMeta } from '@storybook/react';
import React from 'react';
import { AppBar, Button, Handle, Toolbar } from 'react95';
import styled from 'styled-components';
const Wrapper = styled.div`
padding: 5rem;
background: ${({ theme }) => theme.desktopBackground};
`;
export default {
title: 'Controls/Handle',
component: Handle,
decorators: [story => <Wrapper>{story()}</Wrapper>]
} as ComponentMeta<typeof Handle>;
export function Default() {
return (
<AppBar>
<Toolbar>
<Handle size={35} />
<Button variant='menu'>Edit</Button>
<Button variant='menu' disabled>
Save
</Button>
<Handle size={35} />
</Toolbar>
</AppBar>
);
}
Default.story = {
name: 'default'
};
================================================
FILE: src/Handle/Handle.tsx
================================================
import React from 'react';
import styled from 'styled-components';
import { CommonStyledProps } from '../types';
import { getSize } from '../common/utils';
type HandleProps = {
size?: string | number;
} & React.HTMLAttributes<HTMLDivElement> &
CommonStyledProps;
// TODO: add horizontal variant
// TODO: allow user to specify number of bars (like 3 horizontal bars for drag handle)
const Handle = styled.div<HandleProps>`
${({ theme, size = '100%' }) => `
display: inline-block;
box-sizing: border-box;
height: ${getSize(size)};
width: 5px;
border-top: 2px solid ${theme.borderLightest};
border-left: 2px solid ${theme.borderLightest};
border-bottom: 2px solid ${theme.borderDark};
border-right: 2px solid ${theme.borderDark};
background: ${theme.material};
`}
`;
Handle.displayName = 'Handle';
export { Handle, HandleProps };
================================================
FILE: src/Hourglass/Hourglass.spec.tsx
================================================
import { render } from '@testing-library/react';
import React from 'react';
import { Hourglass } from './Hourglass';
describe('<Hourglass />', () => {
it('should render hourglass', () => {
const { container } = render(<Hourglass />);
const hourglass = container.firstElementChild;
expect(hourglass).toBeInTheDocument();
});
it('should render correct size', () => {
const { container } = render(<Hourglass size='66px' />);
const hourglass = container.firstElementChild;
const computedStyles = hourglass
? window.getComputedStyle(hourglass)
: null;
expect(computedStyles?.width).toBe('66px');
expect(computedStyles?.height).toBe('66px');
});
it('should handle custom props', () => {
const customProps: React.HTMLAttributes<HTMLSpanElement> = {
title: 'hourglass'
};
const { container } = render(<Hourglass {...customProps} />);
const hourglass = container.firstElementChild;
expect(hourglass).toHaveAttribute('title', 'hourglass');
});
});
================================================
FILE: src/Hourglass/Hourglass.stories.tsx
================================================
import { ComponentMeta } from '@storybook/react';
import React from 'react';
import { Hourglass } from 'react95';
import styled from 'styled-components';
const Wrapper = styled.div`
padding: 5rem;
background: ${({ theme }) => theme.desktopBackground};
`;
export default {
title: 'Other/Hourglass',
component: Hourglass,
decorators: [story => <Wrapper>{story()}</Wrapper>]
} as ComponentMeta<typeof Hourglass>;
export function Default() {
return <Hourglass size={32} style={{ margin: 20 }} />;
}
Default.story = {
name: 'default'
};
================================================
FILE: src/Hourglass/Hourglass.tsx
================================================
import React, { forwardRef } from 'react';
import styled from 'styled-components';
import { getSize } from '../common/utils';
import { CommonStyledProps } from '../types';
import base64hourglass from './base64hourglass';
type HourglassProps = {
size?: string | number;
} & React.HTMLAttributes<HTMLDivElement> &
CommonStyledProps;
const StyledContainer = styled.div<Required<Pick<HourglassProps, 'size'>>>`
display: inline-block;
height: ${({ size }) => getSize(size)};
width: ${({ size }) => getSize(size)};
`;
const StyledHourglass = styled.span`
display: block;
background: ${base64hourglass};
background-size: cover;
width: 100%;
height: 100%;
`;
const Hourglass = forwardRef<HTMLSpanElement, HourglassProps>(
({ size = 30, ...otherProps }, ref) => {
return (
<StyledContainer size={size} ref={ref} {...otherProps}>
<StyledHourglass />
</StyledContainer>
);
}
);
Hourglass.displayName = 'Hourglass';
export { Hourglass, HourglassProps };
================================================
FILE: src/Hourglass/base64hourglass.tsx
================================================
const base64hourglass =
"url('data:image/gif;base64,R0lGODlhPAA8APQAADc3N6+vr4+Pj05OTvn5+V1dXZ+fn29vby8vLw8PD/X19d/f37S0tJSUlLq6und3d39/f9XV1c/Pz+bm5qamphkZGWZmZsbGxr+/v+rq6tra2u/v7yIiIv///wAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQFBAAfACH+I1Jlc2l6ZWQgb24gaHR0cHM6Ly9lemdpZi5jb20vcmVzaXplACwAAAAAPAA8AAAF/+AnjmRpnmiqrmzrvnAsz3Rt37jr7Xzv/8BebhQsGn1D0XFZTH6YUGQySvU4fYKAdsvtdi1Cp3In6ZjP6HTawBMTyWbFYk6v18/snXvsKXciUApmeVZ7PH6ATIIdhHtPcB0TDQ1gQBCTBINthpBnAUEaa5tuh2mfQKFojZx9aRMSEhA7FLAbonqsfmoUOxFqmriknWm8Hr6/q8IeCAAAx2cTERG2aBTNHMGOj8a/v8WF2m/c3cSj4SQ8C92n4Ocm6evm7ui9CosdBPbs8yo8E2YO5PE74Q+gwIElCnYImA3hux3/Fh50yCciw3YUt2GQtiiDtGQO4f3al1GkGpIDeXlg0KDhXpoMLBtMVPaMnJlv/HjUtIkzHA8HEya4tLkhqICGV4bZVAMyaaul3ZpOUQoVz8wbpaoyvWojq1ZVXGt4/QoM49SnZMs6GktW6hC2X93mgKtVbtceWbzo9VIJKdYqUJwCPiJ4cJOzhg+/TWwko+PHkCNLdhgCACH5BAUEAB8ALAAAAAABAAEAAAUD4BcCACH5BAUEAB8ALBYADAAQAA0AAAVFYCeOZPmVaKqimeO+MPxFXv3d+F17Cm3nuJ1ic7lAdroapUjABZCfnQb4ef6k1OHGULtsNk3qjVKLiIFkj/mMIygU4VwIACH5BAUEAB8ALAAAAAABAAEAAAUD4BcCACH5BAUEAB8ALBkAIwAKAAcAAAUp4CdehrGI6Ed5XpSKa4teguBoGlVPAXuJBpam5/l9gh7NZrFQiDJMRQgAIfkEBQQAHwAsAAAAAAEAAQAABQPgFwIAIfkEBQQAHwAsFgAPABAAIQAABVBgJ45kaZ5oakZB67bZ+M10bd94ru987//AoHBILNYYAsGlR/F4IkwnlLeZTBQ9UlaWwzweERHjuzAKFZkMYYZWm4mOw0ETfdanO8Vms7aFAAAh+QQFBAAfACwAAAAAAQABAAAFA+AXAgAh+QQFBAAfACwZABIACgAeAAAFUGAnjmRpnij5rerqtu4Hx3Rt33iu758iZrUZa1TDCASLGsXjiSiZzmFnM5n4TNJSdmREElfL5lO8cgwGACbgrAkwPat3+x1naggKRS+f/4QAACH5BAUEAB8ALAAAAAABAAEAAAUD4BcCACH5BAUEAB8ALBYAIwAQAA0AAAVE4CeOXdmNaGqeabu27SUIC5xSnifZKK7zl8djkCsIaylGziNaakaEzcbH/Cwl0k9kuWxyPYptzrZULA7otFpNIK1eoxAAIfkEBQQAHwAsAAAAAAEAAQAABQPgFwIAIfkEBQQAHwAsAAAAAAEAAQAABQPgFwIAIfkEBQQAHwAsAAAAAAEAAQAABQPgFwIAIfkEBQQAHwAsAAAAAAEAAQAABQPgFwIAIfkEBQQAHwAsAAAAAAEAAQAABQPgFwIAIfkEBQQAHwAsAAAAAAEAAQAABQPgFwIAIfkEBQQAHwAsAAAAAAEAAQAABQPgFwIAIfkEBQQAHwAsAAAAAAEAAQAABQPgFwIAIfkEBQQAHwAsAAAAAAEAAQAABQPgFwIAIfkECQQAHwAsDgAEACAANAAABTHgJ45kaZ5oqq5s675wLM90bd94ru987//AoHBILBqPyKRyyWw6n9CodEqtWq/Y7CoEACH5BAUEAB8ALAAAAAA8ADwAAAX/4CeOZGmeaKqubFt6biy3Xj3fuFjveU/vPJ/wBAQOj6RiEClUGpk9IMAJxQEdmQK1Grt2OhutkvurOb7f8JaM8qLT4iKbuDu/0erxfOS+4+NPex9mfn55coIfCAuFhoBLbDUAjI1vh4FkOxSVd5eQXB4GnI5rXAAbo6R6VTUFqKmWjzasNaKwsaVIHhAEt3cLTjBQA6++XwoHuUM1vMYdyMorwoN8wkC2t9A8s102204Wxana3DNAAQO1FjUCEDXhvuTT5nUdEwOiGxa8BBDwXxKaLTiAKoMFRvJy9CmmoFcHAgrQSEiwKwICDwU0pAMQIdmnboR8TfwWrJyMPrAiz1DkNs2aSRbe6hnr99LEvDJ9IB5DQ8Dhm36glNh5COGBAmQNHrbz+WXBFChOTqFx5+GBxwYCmL1ZcPHmMiWuvkTgECzBBUvrvH4tErbDWCcYDB2IBPbV2yJJ72SZ46TtXSB5v2RIp1ZXXbFkgWxCc68mk752E3tY/OZeIsiIaxi9o+BBokGH3SZ+4FPbZ8yiPQxNeDl0hNUeHWcKjYb1Zx20bd/GzRaV7t28gRSYELvw7pIfgVcLplwF8+bOo0Ffjmm6zerWrxvPzoe79w8hAAAh+QQJBAAfACwBAAEAOgA6AAAFRuAnjmRpnmiqrmzrvnAsz3Rt33iu73zv/8CgcEgsGo/IpHLJbDqf0Kh0Sq1ar9isdsvter/gsHhMLpvP6LR6zW673/D4MgQAIfkEBQQAHwAsAAAAADwAPAAABf/gJ45kaZ5oqq5s675wLM90bd94ru987//AoHBILBqPyJxnyTQym6nn0ilVSa9XGHY7jXKx2m/WK36Gy1CUVCBpu9+OtNqDeNslgip5Gej4/4ATcidLAICHHQF6c0x9iH+CXV6Gj36KZnsejgsREQSACp0Yg0ydEZWWi4RPjgdLG48apEuogJeDJVKtr7GzHrV/t5KrjX6uHhQMF4cKCwujTxHOwKmYjHzGTw+VEVIK1MGqJrrZTNuP3U/f4IniuazlSwMUFMugE/j47NW4JOQdx9bsoybMgxV4ALEIGAis4MFiCZkUaLPgUAYHGDF+Yucw0y5z3Lzt63hNUzwP5xCRpWOyDhxJYtgiStBQEVCGAAEM6MLp0p0/hMdgIZI17AOTntZgmowo9BBRgz9/EfQ54h8BBS39bKDXwBc9CrVejkNYKRLUSWGpivhXtt9PSpXEvmNiwYDdu3jzFB3LAa9fAxbUGkXjtmSZh4TPJM4kRgbhvVEL9xhTEongJJgza97MubPnz6BDix5NurTp0yJCAAAh+QQJBAAfACwEAA4ANAAgAAAFMeAnjmRpnmiqrmzrvnAsz3Rt33iu73zv/8CgcEgsGo/IpHLJbDqf0Kh0Sq1ar9jsKgQAIfkEBQQAHwAsAAAAADwAPAAABf/gJ45kaZ5oqq5s6bVwLHu0bN8uXeM8rP+9YOoHFBpHRN1xmSwue02A82lrFjaOKbVl3XQ6WeWWm7x+v+HdeFj2ntHaNbL9jUAI5/RLTurWOR53eXFbfh0RgB4PCm9hfCKGiDSLb18Bjx+RiR4HjG8TA3trmkSdZxuhalSkRA2VBqpPrD+ulR0Go3SHmz8CeG8bFqJMupJNHr5nCsKxQccTg4oUNA0YCYG/HQQQYsSlnmCUFLUXgm8EAsPeP6Zf2baV2+rEmTrt8PDyzS7O9uD4b5YV2VGjGw52/wB+CaYjlQcpNBAQioHwy4QMCxe4i3BKGIQN3K7AArBATz8anUDADcgQDMGCbQkknDKAh4ABNxQ0gpnoQ8eDVAUO0ADAzUNMhbZMQiG4R4mOo0gb8eTCQgeEqJVM7juCDWvWJnI4ev2aZIwHl2PfZIBIZBXKtAsLgC1kJu0GuWXNaoB7d67ZlWP75jVLw4JXwW35PNSJFPFUrmIb402smFNCW44N5kJ5+dTkx+vuAfus+VHF0X4xzeHsObXq1ZY7ZN76mt0C0rRf1zuWW/du175PHAu+YjhxFcCPm6CsHHnv5kig6w4BACH5BAkEAB8ALAEAAQA6ADoAAAVG4CeOZGmeaKqubOu+cCzPdG3feK7vfO//wKBwSCwaj8ikcslsOp/QqHRKrVqv2Kx2y+16v+CweEwum8/otHrNbrvf8PgyBAAh+QQFBAAfACwAAAAAPAA8AAAF/+AnjmRpnmiqrmzrvnAsz3Rt37jr7Xzv/8BebhQsGn1D0XFZTH6YUGQySvU4fYKAdsvtdi1Cp3In6ZjP6HTawBMTyWbFYk6v18/snXvsKXciUApmeVZ7PH6ATIIdhHtPcB0TDQ1gQBCTBINthpBnAUEaa5tuh2mfQKFojZx9aRMSEhA7FLAbonqsfmoUOxFqmriknWm8Hr6/q8IeCAAAx2cTERG2aBTNHMGOj8a/v8WF2m/c3cSj4SQ8C92n4Ocm6evm7ui9CosdBPbs8yo8E2YO5PE74Q+gwIElCnYImA3hux3/Fh50yCciw3YUt2GQtiiDtGQO4f3al1GkGpIDeXlg0KDhXpoMLBtMVPaMnJlv/HjUtIkzHA8HEya4tLkhqICGV4bZVAMyaaul3ZpOUQoVz8wbpaoyvWojq1ZVXGt4/QoM49SnZMs6GktW6hC2X93mgKtVbtceWbzo9VIJKdYqUJwCPiJ4cJOzhg+/TWwko+PHkCNLdhgCACH5BAUEAB8ALAAAAAABAAEAAAUD4BcCADs=')";
export default base64hourglass;
================================================
FILE: src/MenuList/MenuList.spec.tsx
================================================
import React from 'react';
import { renderWithTheme } from '../../test/utils';
import { MenuList } from './MenuList';
describe('<MenuList />', () => {
it('renders MenuList', () => {
const { container } = renderWithTheme(<MenuList />);
const menuList = container.firstElementChild;
expect(menuList).toBeInTheDocument();
});
it('is an ul', () => {
const { container } = renderWithTheme(<MenuList />);
const menuList = container.firstElementChild;
expect(menuList?.tagName).toBe('UL');
});
it('renders children', () => {
const textContent = 'Hi there!';
const { getByText } = renderWithTheme(
<MenuList>
<span>{textContent}</span>
</MenuList>
);
expect(getByText(textContent)).toBeInTheDocument();
});
describe('prop: inline', () => {
it('renders inline', () => {
const { container } = renderWithTheme(<MenuList inline />);
const menuList = container.firstElementChild;
expect(menuList).toHaveStyleRule('display', 'inline-flex');
expect(menuList).toHaveStyleRule('align-items', 'center');
});
});
describe('prop: fullWidth', () => {
it('has 100% width', () => {
const { container } = renderWithTheme(<MenuList fullWidth />);
const menuList = container.firstElementChild;
expect(menuList).toHaveStyleRule('width', '100%');
});
});
});
================================================
FILE: src/MenuList/MenuList.stories.tsx
================================================
import { ComponentMeta } from '@storybook/react';
import React from 'react';
import { Handle, MenuList, MenuListItem, Separator } from 'react95';
import styled from 'styled-components';
const Wrapper = styled.div`
padding: 5rem;
background: ${({ theme }) => theme.desktopBackground};
display: flex;
align-items: center;
& > * {
margin-right: 1rem;
}
`;
export default {
title: 'Controls/MenuList',
component: MenuList,
subcomponents: { MenuListItem },
decorators: [story => <Wrapper>{story()}</Wrapper>]
} as ComponentMeta<typeof MenuList>;
export function Default() {
return (
<>
<MenuList>
<MenuListItem primary>Photos</MenuListItem>
<MenuListItem
as='a'
// TODO: Come up with a more elegant way to allow props when `as` is used
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
href='https://expensive.toys'
target='_blank'
>
Link
</MenuListItem>
<MenuListItem disabled>Other</MenuListItem>
</MenuList>
<MenuList inline>
<MenuListItem square disabled>
<span role='img' aria-label='🌿'>
🌿
</span>
</MenuListItem>
<Handle size={38} />
<MenuListItem>Tackle</MenuListItem>
<MenuListItem>Growl</MenuListItem>
<MenuListItem disabled>Razor Leaf</MenuListItem>
</MenuList>
<MenuList>
<MenuListItem primary size='sm'>
View
</MenuListItem>
<Separator />
<MenuListItem size='sm'>Paste</MenuListItem>
<MenuListItem size='sm'>Paste Shortcut</MenuListItem>
<MenuListItem size='sm'>Undo Copy</MenuListItem>
<Separator />
<MenuListItem size='sm'>Properties</MenuListItem>
</MenuList>
<MenuList>
<MenuListItem square>
<span role='img' aria-label='😎'>
😎
</span>
</MenuListItem>
<MenuListItem square>
<span role='img' aria-label='🤖'>
🤖
</span>
</MenuListItem>
<MenuListItem square>
<span role='img' aria-label='🎁'>
🎁
</span>
</MenuListItem>
</MenuList>
</>
);
}
Default.story = {
name: 'default'
};
================================================
FILE: src/MenuList/MenuList.tsx
================================================
import React from 'react';
import styled from 'styled-components';
import { createBorderStyles, createBoxStyles } from '../common';
import { CommonStyledProps } from '../types';
type MenuListProps = React.HTMLAttributes<HTMLUListElement> & {
fullWidth?: boolean;
shadow?: boolean;
inline?: boolean;
} & CommonStyledProps;
// TODO keyboard controls
const MenuList = styled.ul.attrs(() => ({
role: 'menu'
}))<MenuListProps>`
box-sizing: border-box;
width: ${props => (props.fullWidth ? '100%' : 'auto')};
padding: 4px;
${createBorderStyles({ style: 'window' })}
${createBoxStyles()}
${props =>
props.inline &&
`
display: inline-flex;
align-items: center;
`}
list-style: none;
position: relative;
`;
MenuList.displayName = 'MenuList';
export * from './MenuListItem';
export { MenuList, MenuListProps };
================================================
FILE: src/MenuList/MenuListItem.spec.tsx
================================================
import React from 'react';
import { renderWithTheme, theme } from '../../test/utils';
import { blockSizes } from '../common/system';
import { MenuListItem } from './MenuListItem';
const defaultSize = 'lg';
describe('<MenuListItem />', () => {
it('renders MenuListItem', () => {
const { getByRole } = renderWithTheme(<MenuListItem />);
const menuListItem = getByRole('menuitem');
expect(menuListItem).toBeInTheDocument();
expect(menuListItem).not.toHaveAttribute('aria-disabled');
});
it('renders children', () => {
const textContent = 'Hi there!';
const { getByText } = renderWithTheme(
<MenuListItem>
<span>{textContent}</span>
</MenuListItem>
);
expect(getByText(textContent)).toBeInTheDocument();
});
it('should have a default role of menuitem', () => {
const { getByRole } = renderWithTheme(<MenuListItem />);
const menuListItem = getByRole('menuitem');
expect(menuListItem).toHaveAttribute('role', 'menuitem');
});
it('should render with custom role', () => {
const { getByRole } = renderWithTheme(<MenuListItem role='option' />);
const menuListItem = getByRole('option');
expect(menuListItem).toHaveAttribute('role', 'option');
});
// it('should have a tabIndex of -1 by default', () => {
// const { getByRole } = renderWithTheme(<MenuListItem role='option' />);
// const menuListItem = getByRole('menuitem');
// expect(menuListItem).toHaveAttribute('tabIndex', '-1');
// });
describe('prop: disabled', () => {
it('should not trigger onClick callback', () => {
const clickHandler = jest.fn();
const { getByRole } = renderWithTheme(
<MenuListItem disabled onClick={clickHandler} />
);
const menuListItem = getByRole('menuitem') as HTMLElement;
menuListItem.click();
expect(clickHandler).not.toBeCalled();
expect(menuListItem).toHaveAttribute('aria-disabled', 'true');
});
it('renders with disabled styles ', () => {
const { getByRole } = renderWithTheme(<MenuListItem disabled />);
const menuListItem = getByRole('menuitem');
expect(menuListItem).toHaveStyleRule('pointer-events', 'none');
expect(menuListItem).toHaveStyleRule('color', theme.materialTextDisabled);
expect(menuListItem).toHaveStyleRule(
'text-shadow',
`1px 1px ${theme.materialTextDisabledShadow}`
);
});
});
describe('prop: onClick', () => {
it('should be called when clicked', () => {
const clickHandler = jest.fn();
const { getByRole } = renderWithTheme(
<MenuListItem onClick={clickHandler} />
);
const menuListItem = getByRole('menuitem') as HTMLElement;
menuListItem.click();
expect(clickHandler).toHaveBeenCalledTimes(1);
});
});
describe('prop: square', () => {
it('should render square MenuListItem', () => {
const { getByRole } = renderWithTheme(<MenuListItem square />);
const menuListItem = getByRole('menuitem');
expect(menuListItem).toHaveStyleRule('width', blockSizes[defaultSize]);
expect(menuListItem).toHaveStyleRule('height', blockSizes[defaultSize]);
});
});
describe('prop: size', () => {
it('should define MenuListItem height', () => {
const size = 'sm';
const { getByRole } = renderWithTheme(<MenuListItem size={size} />);
const menuListItem = getByRole('menuitem');
expect(menuListItem).toHaveStyleRule('height', blockSizes[size]);
});
});
});
================================================
FILE: src/MenuList/MenuListItem.tsx
================================================
import React, { forwardRef } from 'react';
import styled from 'styled-components';
import { createDisabledTextStyles } from '../common';
import { blockSizes } from '../common/system';
import { CommonStyledProps, Sizes } from '../types';
type MenuListItemProps = {
disabled?: boolean;
square?: boolean;
primary?: boolean;
size?: Sizes;
} & React.HTMLAttributes<HTMLLIElement> &
CommonStyledProps;
export const StyledMenuListItem = styled.li<{
$disabled?: boolean;
square?: boolean;
primary?: boolean;
size: Sizes;
}>`
box-sizing: border-box;
display: flex;
align-items: center;
position: relative;
height: ${props => blockSizes[props.size]};
width: ${props => (props.square ? blockSizes[props.size] : 'auto')};
padding: 0 8px;
font-size: 1rem;
white-space: nowrap;
justify-content: ${props =>
props.square ? 'space-around' : 'space-between'};
text-align: center;
line-height: ${props => blockSizes[props.size]};
color: ${({ theme }) => theme.materialText};
pointer-events: ${({ $disabled }) => ($disabled ? 'none' : 'auto')};
font-weight: ${({ primary }) => (primary ? 'bold' : 'normal')};
&:hover {
${({ theme, $disabled }) =>
!$disabled &&
`
color: ${theme.materialTextInvert};
background: ${theme.hoverBackground};
`}
cursor: default;
}
${props => props.$disabled && createDisabledTextStyles()}
`;
const MenuListItem = forwardRef<HTMLLIElement, MenuListItemProps>(
(
{
size = 'lg',
disabled,
// tabIndex: tabIndexProp,
square,
children,
onClick,
primary,
...otherProps
},
ref
) => {
// let tabIndex;
// if (!disabled) {
// tabIndex = tabIndexProp !== undefined ? tabIndexProp : -1;
// }
return (
<StyledMenuListItem
$disabled={disabled}
size={size}
square={square}
onClick={disabled ? undefined : onClick}
primary={primary}
// tabIndex={tabIndex}
role='menuitem'
ref={ref}
aria-disabled={disabled}
{...otherProps}
>
{children}
</StyledMenuListItem>
);
}
);
MenuListItem.displayName = 'MenuListItem';
export { MenuListItem, MenuListItemProps };
================================================
FILE: src/Monitor/Monitor.spec.tsx
================================================
import React from 'react';
import { renderWithTheme } from '../../test/utils';
import { Monitor } from './Monitor';
describe('<Monitor />', () => {
it('should render', () => {
const { container } = renderWithTheme(<Monitor />);
const monitorElement = container.firstElementChild;
expect(monitorElement).toBeInTheDocument();
});
it('should handle custom props', () => {
const customProps: React.HTMLAttributes<HTMLDivElement> = {
title: 'potatoe'
};
const { container } = renderWithTheme(<Monitor {...customProps} />);
const monitorElement = container.firstElementChild;
expect(monitorElement).toHaveAttribute('title', 'potatoe');
});
describe('prop: backgroundStyles', () => {
it('should forward styles to background element', () => {
const { getByTestId } = renderWithTheme(
<Monitor backgroundStyles={{ backgroundColor: 'papayawhip' }} />
);
const backgroundElement = getByTestId('background');
expect(backgroundElement).toHaveAttribute(
'style',
'background-color: papayawhip;'
);
});
});
describe('prop: children', () => {
it('children should be rendered in background element', () => {
const { getByTestId } = renderWithTheme(<Monitor>Hi!</Monitor>);
const backgroundElement = getByTestId('background');
expect(backgroundElement.innerHTML).toBe('Hi!');
});
});
});
================================================
FILE: src/Monitor/Monitor.stories.tsx
================================================
import { ComponentMeta } from '@storybook/react';
import React from 'react';
import { Monitor } from 'react95';
import styled from 'styled-components';
const Wrapper = styled.div`
padding: 5rem;
background: ${({ theme }) => theme.desktopBackground};
`;
export default {
title: 'Other/Monitor',
component: Monitor,
decorators: [story => <Wrapper>{story()}</Wrapper>]
} as ComponentMeta<typeof Monitor>;
export function Default() {
return <Monitor backgroundStyles={{ background: 'blue' }} />;
}
Default.story = {
name: 'default'
};
================================================
FILE: src/Monitor/Monitor.tsx
================================================
import React, { forwardRef } from 'react';
import styled from 'styled-components';
import { StyledScrollView } from '../ScrollView/ScrollView';
type MonitorProps = {
backgroundStyles?: React.CSSProperties;
children?: React.ReactNode;
};
const Wrapper = styled.div`
position: relative;
display: inline-block;
padding-bottom: 26px;
`;
const Inner = styled.div`
position: relative;
`;
const MonitorBody = styled.div`
position: relative;
z-index: 1;
box-sizing: border-box;
width: 195px;
height: 155px;
padding: 12px;
background: ${({ theme }) => theme.material};
border-top: 4px solid ${({ theme }) => theme.borderLightest};
border-left: 4px solid ${({ theme }) => theme.borderLightest};
border-bottom: 4px solid ${({ theme }) => theme.borderDark};
border-right: 4px solid ${({ theme }) => theme.borderDark};
outline: 1px dotted ${({ theme }) => theme.material};
outline-offset: -3px;
&:before {
content: '';
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
outline: 1px dotted ${({ theme }) => theme.material};
}
box-shadow: 1px 1px 0 1px ${({ theme }) => theme.borderDarkest};
&:after {
content: '';
display: inline-block;
position: absolute;
bottom: 4px;
right: 12px;
width: 10px;
border-top: 2px solid #4d9046;
border-bottom: 2px solid #07ff00;
}
`;
const Background = styled(StyledScrollView).attrs(() => ({
'data-testid': 'background'
}))`
width: 100%;
height: 100%;
`;
const Stand = styled.div`
box-sizing: border-box;
position: absolute;
top: calc(100% + 2px);
left: 50%;
transform: translateX(-50%);
height: 10px;
width: 50%;
background: ${({ theme }) => theme.material};
border-left: 2px solid ${({ theme }) => theme.borderLightest};
border-bottom: 2px solid ${({ theme }) => theme.borderDarkest};
border-right: 2px solid ${({ theme }) => theme.borderDarkest};
box-shadow: inset 0px 0px 0px 2px ${({ theme }) => theme.borderDark};
&:before {
content: '';
position: absolute;
top: calc(100% + 2px);
left: 50%;
transform: translateX(-50%);
width: 80%;
height: 8px;
background: ${({ theme }) => theme.material};
border-left: 2px solid ${({ theme }) => theme.borderLightest};
border-right: 2px solid ${({ theme }) => theme.borderDarkest};
box-shadow: inset 0px 0px 0px 2px ${({ theme }) => theme.borderDark};
}
&:after {
content: '';
position: absolute;
top: calc(100% + 8px);
left: 50%;
transform: translateX(-50%);
width: 150%;
height: 4px;
background: ${({ theme }) => theme.material};
border: 2px solid ${({ theme }) => theme.borderDark};
border-bottom: none;
box-shadow: inset 1px 1px 0px 1px ${({ theme }) => theme.borderLightest},
1px 1px 0 1px ${({ theme }) => theme.borderDarkest};
}
`;
const Monitor = forwardRef<HTMLDivElement, MonitorProps>(
({ backgroundStyles, children, ...otherProps }, ref) => {
return (
<Wrapper ref={ref} {...otherProps}>
<Inner>
<MonitorBody>
<Background style={backgroundStyles}>{children}</Background>
</MonitorBody>
<Stand />
</Inner>
</Wrapper>
);
}
);
Monitor.displayName = 'Monitor';
export { Monitor, MonitorProps };
================================================
FILE: src/NumberInput/NumberInput.spec.tsx
================================================
import { fireEvent } from '@testing-library/react';
import React from 'react';
import { renderWithTheme } from '../../test/utils';
import { NumberInput } from './NumberInput';
// TODO: should we pass number or string to callbacks?
describe('<NumberInput />', () => {
it('should call onChange on spin buttons click', () => {
const handleChange = jest.fn();
const { getByTestId } = renderWithTheme(
<NumberInput onChange={handleChange} defaultValue={2} />
);
const spinButton = getByTestId('increment');
spinButton.click();
expect(handleChange).toHaveBeenCalledTimes(1);
expect(handleChange).toHaveBeenCalledWith(3);
});
it('should call onChange on blur after keyboard input', () => {
const handleChange = jest.fn();
const { container } = renderWithTheme(
<NumberInput onChange={handleChange} defaultValue={0} />
);
const input = container.querySelector('input') as HTMLInputElement;
input.focus();
fireEvent.change(input, { target: { value: '777' } });
expect(handleChange).toHaveBeenCalledTimes(0);
input.blur();
expect(handleChange).toHaveBeenCalledTimes(1);
expect(handleChange).toHaveBeenCalledWith(777);
});
// TODO: this test passes even tho it fails in real-life
it('should not call onChange on blur, when clicked element is one of the spin buttons', () => {
const handleChange = jest.fn();
const { getByTestId, container } = renderWithTheme(
<NumberInput onChange={handleChange} value={0} />
);
const input = container.querySelector('input') as HTMLInputElement;
const incrementButton = getByTestId('increment');
input.focus();
fireEvent.keyDown(document.activeElement as HTMLInputElement, {
key: '2'
});
incrementButton.click();
expect(handleChange).toHaveBeenCalledTimes(1);
});
it('should give correct result after user changes input value and then clicks increment button', () => {
const handleChange = jest.fn();
const { container, getByTestId } = renderWithTheme(
<NumberInput onChange={handleChange} defaultValue={0} />
);
const input = container.querySelector('input') as HTMLInputElement;
const incrementButton = getByTestId('increment');
fireEvent.change(input, { target: { value: '2' } });
incrementButton.click();
expect(handleChange).toHaveBeenCalledWith(3);
});
it('should reach max value', () => {
const { getByTestId, container } = renderWithTheme(
<NumberInput defaultValue={90} min={0} max={100} step={10} />
);
const input = container.querySelector('input') as HTMLInputElement;
const incrementButton = getByTestId('increment');
incrementButton.click();
expect(input.value).toBe('100');
});
it('should reach min value', () => {
const { getByTestId, container } = renderWithTheme(
<NumberInput defaultValue={10} min={0} max={100} step={10} />
);
const input = container.querySelector('input') as HTMLInputElement;
const decrementButton = getByTestId('decrement');
decrementButton.click();
expect(input.value).toBe('0');
});
describe('prop: step', () => {
it('should be 1 by default', () => {
const { getByTestId, container } = renderWithTheme(
<NumberInput defaultValue={0} />
);
const input = container.querySelector('input') as HTMLInputElement;
const incrementButton = getByTestId('increment');
incrementButton.click();
expect(input.value).toBe('1');
});
it('should change value by specified step', () => {
const { getByTestId, container } = renderWithTheme(
<NumberInput defaultValue={10} step={3} />
);
const input = container.querySelector('input') as HTMLInputElement;
const decrementButton = getByTestId('decrement');
decrementButton.click();
expect(input.value).toBe('7');
});
it('should handle decimal step', () => {
const { getByTestId, container } = renderWithTheme(
<NumberInput defaultValue={10} step={0.3} />
);
const input = container.querySelector('input') as HTMLInputElement;
const decrementButton = getByTestId('decrement');
decrementButton.click();
expect(input.value).toBe('9.7');
});
});
describe('prop: disabled', () => {
it('should render disabled', () => {
const { getByTestId, container } = renderWithTheme(
<NumberInput defaultValue={10} disabled />
);
const input = container.querySelector('input') as HTMLInputElement;
const incrementButton = getByTestId('increment');
const decrementButton = getByTestId('decrement');
expect(input).toHaveAttribute('disabled');
expect(incrementButton).toHaveAttribute('disabled');
expect(decrementButton).toHaveAttribute('disabled');
});
it('should not react to button clicks', () => {
const { getByTestId, container } = renderWithTheme(
<NumberInput defaultValue={10} disabled />
);
const input = container.querySelector('input') as HTMLInputElement;
const incrementButton = getByTestId('increment');
const decrementButton = getByTestId('decrement');
incrementButton.click();
expect(input.value).toBe('10');
decrementButton.click();
expect(input.value).toBe('10');
});
});
describe('prop: width', () => {
it('should render component of specified width', () => {
const { container } = renderWithTheme(
<NumberInput defaultValue={10} disabled width={93} />
);
expect(
getComputedStyle(container.firstElementChild as HTMLInputElement).width
).toBe('93px');
});
it('should handle %', () => {
const { container } = renderWithTheme(
<NumberInput defaultValue={10} disabled width='93%' />
);
expect(
getComputedStyle(container.firstElementChild as HTMLInputElement).width
).toBe('93%');
});
});
});
================================================
FILE: src/NumberInput/NumberInput.stories.tsx
================================================
import { ComponentMeta } from '@storybook/react';
import React from 'react';
import { ScrollView, NumberInput } from 'react95';
import styled from 'styled-components';
const Wrapper = styled.div`
background: ${({ theme }) => theme.material};
padding: 5rem;
& > * {
margin-bottom: 1rem;
}
#cutout {
background: ${({ theme }) => theme.canvas};
padding: 2rem;
width: 300px;
& > div > * {
margin-bottom: 1rem;
}
}
`;
export default {
title: 'Controls/NumberInput',
component: NumberInput,
decorators: [story => <Wrapper>{story()}</Wrapper>]
} as ComponentMeta<typeof NumberInput>;
export function Default() {
return (
<>
<NumberInput defaultValue={3} step={1.5} min={1.5} max={9} width={130} />
<br />
<NumberInput defaultValue={1995} width={130} />
<br />
<NumberInput disabled defaultValue={2020} width={130} />
</>
);
}
Default.story = {
name: 'default'
};
export function Flat() {
return (
<ScrollView id='cutout'>
<p>
When you want to use NumberInput on a light background (like scrollable
content), just use the flat variant:
</p>
<NumberInput
variant='flat'
defaultValue={1.5}
min={0}
max={9}
width='130px'
/>
<br />
<NumberInput variant='flat' defaultValue={1995} width='130px' />
<br />
<NumberInput variant='flat' disabled defaultValue={2020} width='130px' />
</ScrollView>
);
}
Flat.story = {
name: 'flat'
};
================================================
FILE: src/NumberInput/NumberInput.tsx
================================================
import React, { forwardRef, useCallback } from 'react';
import styled, { css } from 'styled-components';
import { Button } from '../Button/Button';
import useControlledOrUncontrolled from '../common/hooks/useControlledOrUncontrolled';
import { blockSizes } from '../common/system';
import { clamp, getSize } from '../common/utils';
import { TextInput } from '../TextInput/TextInput';
import { CommonStyledProps } from '../types';
type NumberInputProps = {
className?: string;
defaultValue?: number;
disabled?: boolean;
max?: number;
min?: number;
readOnly?: boolean;
step?: number;
onChange?: (value: number) => void;
style?: React.CSSProperties;
value?: number;
variant?: 'default' | 'flat';
width?: string | number;
} & CommonStyledProps;
const StyledNumberInputWrapper = styled.div`
display: inline-flex;
align-items: center;
`;
const StyledButton = styled(Button)`
width: 30px;
padding: 0;
flex-shrink: 0;
${({ variant }) =>
variant === 'flat'
? css`
height: calc(50% - 1px);
`
: css`
height: 50%;
`}
`;
const StyledButtonWrapper = styled.div<Pick<NumberInputProps, 'variant'>>`
display: flex;
flex-direction: column;
flex-wrap: nowrap;
justify-content: space-between;
${({ variant }) =>
variant === 'flat'
? css`
height: calc(${blockSizes.md} - 4px);
`
: css`
height: ${blockSizes.md};
margin-left: 2px;
`}
`;
const StyledButtonIcon = styled.span<{ invert?: boolean }>`
width: 0px;
height: 0px;
display: inline-block;
${({ invert }) =>
invert
? css`
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-bottom: 4px solid ${({ theme }) => theme.materialText};
`
: css`
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 4px solid ${({ theme }) => theme.materialText};
`}
${StyledButton}:disabled & {
filter: drop-shadow(
1px 1px 0px ${({ theme }) => theme.materialTextDisabledShadow}
);
${({ invert }) =>
invert
? css`
border-bottom-color: ${({ theme }) => theme.materialTextDisabled};
`
: css`
border-top-color: ${({ theme }) => theme.materialTextDisabled};
`}
}
`;
const NumberInput = forwardRef<HTMLInputElement, NumberInputProps>(
(
{
className,
defaultValue,
disabled = false,
max,
min,
onChange,
readOnly,
step = 1,
style,
value,
variant = 'default',
width,
...otherProps
},
ref
) => {
const [valueDerived, setValueState] = useControlledOrUncontrolled({
defaultValue,
onChange,
readOnly,
value
});
const handleInputChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = parseFloat(e.target.value);
setValueState(newValue);
},
[setValueState]
);
const handleClick = useCallback(
(delta: number) => {
const newValue = clamp(
parseFloat(((valueDerived ?? 0) + delta).toFixed(2)),
min ?? null,
max ?? null
);
setValueState(newValue);
onChange?.(newValue);
},
[max, min, onChange, setValueState, valueDerived]
);
const onBlur = useCallback(() => {
if (valueDerived !== undefined) {
onChange?.(valueDerived);
}
}, [onChange, valueDerived]);
const stepUp = useCallback(() => {
handleClick(step);
}, [handleClick, step]);
const stepDown = useCallback(() => {
handleClick(-step);
}, [handleClick, step]);
const buttonVariant = variant === 'flat' ? 'flat' : 'raised';
return (
<StyledNumberInputWrapper
className={className}
style={{
...style,
width: width !== undefined ? getSize(width) : 'auto'
}}
{...otherProps}
>
<TextInput
value={valueDerived}
variant={variant}
onChange={handleInputChange}
disabled={disabled}
type='number'
readOnly={readOnly}
ref={ref}
fullWidth
onBlur={onBlur}
/>
<StyledButtonWrapper variant={variant}>
<StyledButton
data-testid='increment'
variant={buttonVariant}
disabled={disabled || readOnly}
onClick={stepUp}
>
<StyledButtonIcon invert />
</StyledButton>
<StyledButton
data-testid='decrement'
variant={buttonVariant}
disabled={disabled || readOnly}
onClick={stepDown}
>
<StyledButtonIcon />
</StyledButton>
</StyledButtonWrapper>
</StyledNumberInputWrapper>
);
}
);
NumberInput.displayName = 'NumberInput';
export { NumberInput, NumberInputProps };
================================================
FILE: src/ProgressBar/ProgressBar.spec.tsx
================================================
import React from 'react';
import { renderWithTheme } from '../../test/utils';
import { ProgressBar } from './ProgressBar';
describe('<ProgressBar />', () => {
it('renders ProgressBar', () => {
const value = 32;
const { getByRole } = renderWithTheme(<ProgressBar value={value} />);
const progressBar = getByRole('progressbar');
expect(progressBar).toBeInTheDocument();
expect(progressBar).toHaveAttribute('aria-valuenow', value.toString());
});
describe('prop: variant', () => {
describe('variant: "default"', () => {
it('displays current percentage value', () => {
const value = 32;
const { queryByTestId } = renderWithTheme(
<ProgressBar value={value} />
);
expect(queryByTestId('defaultProgress1')?.textContent).toBe(
`${value}%`
);
expect(queryByTestId('defaultProgress2')?.textContent).toBe(
`${value}%`
);
expect(queryByTestId('defaultProgress2')).toHaveStyleRule(
'clip-path',
`polygon( 0 0, ${value}% 0, ${value}% 100%, 0 100% )`
);
expect(queryByTestId('indeterminateProgress')).not.toBeInTheDocument();
});
});
describe('variant: "tile"', () => {
it('Renders "tile" progress', () => {
const { queryByTestId } = renderWithTheme(
<ProgressBar variant='tile' />
);
expect(queryByTestId('defaultProgress1')).not.toBeInTheDocument();
expect(queryByTestId('defaultProgress2')).not.toBeInTheDocument();
expect(queryByTestId('tileProgress')).toBeInTheDocument();
});
// it('Renders correct number of tiles', () => {
// const value = 34;
// const { queryByTestId } = renderWithTheme(
// <Progress variant='tile' value={value} />
// );
// const tileProgress = queryByTestId('tileProgress');
// const tileProgressWidth = tileProgress.getBoundingClientRect().width;
// const tile = tileProgress.firstChild;
// const tileWidth = tile.getBoundingClientRect().width;
// const targetTileNumber = Math.floor(
// ((value / 100) * tileProgressWidth) / tileWidth
// );
// expect(tileProgress.childElementCount).toBe(targetTileNumber);
// });
});
});
describe('prop: hideValue', () => {
it('renders progress bars, but does not show value', () => {
const value = 32;
const { queryByTestId } = renderWithTheme(
<ProgressBar hideValue value={value} />
);
expect(queryByTestId('defaultProgress1')).toBeInTheDocument();
expect(queryByTestId('defaultProgress2')).toBeInTheDocument();
expect(queryByTestId('defaultProgress1')).toBeEmptyDOMElement();
expect(queryByTestId('defaultProgress2')).toBeEmptyDOMElement();
});
});
});
================================================
FILE: src/ProgressBar/ProgressBar.stories.tsx
================================================
import { ComponentMeta } from '@storybook/react';
import React, { useEffect, useState } from 'react';
import { ProgressBar } from 'react95';
import styled from 'styled-components';
const Wrapper = styled.div`
background: ${({ theme }) => theme.material};
padding: 5rem;
`;
export default {
title: 'Controls/ProgressBar',
component: ProgressBar,
decorators: [story => <Wrapper>{story()}</Wrapper>]
} as ComponentMeta<typeof ProgressBar>;
export function Default() {
const [percent, setPercent] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setPercent(previousPercent => {
if (previousPercent === 100) {
return 0;
}
const diff = Math.random() * 10;
return Math.min(previousPercent + diff, 100);
});
}, 500);
return () => {
clearInterval(timer);
};
}, []);
return <ProgressBar value={Math.floor(percent)} />;
}
Default.story = {
name: 'default'
};
export function Tile() {
const [percent, setPercent] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setPercent(previousPercent => {
if (previousPercent === 100) {
return 0;
}
const diff = Math.random() * 10;
return Math.min(previousPercent + diff, 100);
});
}, 500);
return () => {
clearInterval(timer);
};
}, []);
return <ProgressBar variant='tile' value={Math.floor(percent)} />;
}
Tile.story = {
name: 'tile'
};
export function HideValue() {
const [percent, setPercent] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setPercent(previousPercent => {
if (previousPercent === 100) {
return 0;
}
const diff = Math.random() * 10;
return Math.min(previousPercent + diff, 100);
});
}, 500);
return () => {
clearInterval(timer);
};
}, []);
return <ProgressBar hideValue value={Math.floor(percent)} />;
}
HideValue.story = {
name: 'hide value'
};
================================================
FILE: src/ProgressBar/ProgressBar.tsx
================================================
import React, {
forwardRef,
useCallback,
useEffect,
useRef,
useState
} from 'react';
import styled, { css } from 'styled-components';
import { blockSizes } from '../common/system';
import { StyledScrollView } from '../ScrollView/ScrollView';
import { CommonStyledProps } from '../types';
type ProgressBarProps = {
hideValue?: boolean;
shadow?: boolean;
value?: number;
variant?: 'default' | 'tile';
} & React.HTMLAttributes<HTMLDivElement> &
CommonStyledProps;
const Wrapper = styled.div<Required<Pick<ProgressBarProps, 'variant'>>>`
display: inline-block;
height: ${blockSizes.md};
width: 100%;
`;
const ProgressCutout = styled(StyledScrollView)<
Required<Pick<ProgressBarProps, 'variant'>>
>`
width: 100%;
height: 100%;
position: relative;
text-align: center;
padding: 0;
overflow: hidden;
&:before {
z-index: 1;
}
`;
const commonBarStyles = css`
width: calc(100% - 4px);
height: calc(100% - 4px);
display: flex;
align-items: center;
justify-content: space-around;
`;
const WhiteBar = styled.div`
position: relative;
top: 4px;
${commonBarStyles}
background: ${({ theme }) => theme.canvas};
color: #000;
margin-left: 2px;
margin-top: -2px;
color: ${({ theme }) => theme.materialText};
`;
const BlueBar = styled.div<Pick<ProgressBarProps, 'value'>>`
position: absolute;
top: 2px;
left: 2px;
${commonBarStyles}
color: ${({ theme }) => theme.materialTextInvert};
background: ${({ theme }) => theme.progress};
clip-path: polygon(
0 0,
${({ value = 0 }) => value}% 0,
${({ value = 0 }) => value}% 100%,
0 100%
);
transition: 0.4s linear clip-path;
`;
const TilesWrapper = styled.div`
width: calc(100% - 6px);
height: calc(100% - 8px);
position: absolute;
left: 3px;
top: 4px;
box-sizing: border-box;
display: inline-flex;
`;
const tileWidth = 17;
const Tile = styled.span`
display: inline-block;
width: ${tileWidth}px;
box-sizing: border-box;
height: 100%;
background: ${({ theme }) => theme.progress};
border-color: ${({ theme }) => theme.material};
border-width: 0px 1px;
border-style: solid;
`;
const ProgressBar = forwardRef<HTMLDivElement, ProgressBarProps>(
(
{
hideValue = false,
shadow = true,
value,
variant = 'default',
...otherProps
},
ref
) => {
const displayValue = hideValue ? null : `${value}%`;
const tilesWrapperRef = useRef<HTMLDivElement | null>(null);
const [tiles, setTiles] = useState([]);
// TODO debounce this function
const updateTilesNumber = useCallback(() => {
if (!tilesWrapperRef.current || value === undefined) {
return;
}
const progressWidth =
tilesWrapperRef.current.getBoundingClientRect().width;
const newTilesNumber = Math.round(
((value / 100) * progressWidth) / tileWidth
);
setTiles(Array.from({ length: newTilesNumber }));
}, [value]);
useEffect(() => {
updateTilesNumber();
window.addEventListener('resize', updateTilesNumber);
return () => window.removeEventListener('resize', updateTilesNumber);
}, [updateTilesNumber]);
return (
<Wrapper
aria-valuenow={value !== undefined ? Math.round(value) : undefined}
ref={ref}
role='progressbar'
variant={variant}
{...otherProps}
>
<ProgressCutout variant={variant} shadow={shadow}>
{variant === 'default' ? (
<>
<WhiteBar data-testid='defaultProgress1'>{displayValue}</WhiteBar>
<BlueBar data-testid='defaultProgress2' value={value}>
{displayValue}
</BlueBar>
</>
) : (
<TilesWrapper ref={tilesWrapperRef} data-testid='tileProgress'>
{tiles.map((_, index) => (
<Tile key={index} />
))}
</TilesWrapper>
)}
</ProgressCutout>
</Wrapper>
);
}
);
ProgressBar.displayName = 'ProgressBar';
export { ProgressBar, ProgressBarProps };
================================================
FILE: src/Radio/Radio.spec.tsx
================================================
import React from 'react';
import { renderWithTheme } from '../../test/utils';
import { Radio } from './Radio';
describe('<Radio />', () => {
describe('label', () => {
it('renders', () => {
const labelText = 'Swag';
const { getByLabelText } = renderWithTheme(<Radio label={labelText} />);
expect(getByLabelText(labelText)).toBeInTheDocument();
});
});
describe('prop: onChange', () => {
it('should be called when Radio is clicked', () => {
const handleChange = jest.fn(event => event.target.checked);
const { getByRole } = renderWithTheme(
<Radio onChange={handleChange} value='swag' />
);
getByRole('radio').click();
expect(handleChange).toHaveBeenCalledTimes(1);
});
});
describe('prop: disabled', () => {
it('should disable radio', () => {
const handleChange = jest.fn();
const { getByRole } = renderWithTheme(<Radio disabled />);
const checkbox = getByRole('radio');
expect(checkbox).toHaveAttribute('disabled');
checkbox.click();
expect(handleChange).not.toHaveBeenCalled();
});
it('should be overridden by props', () => {
const { getByRole, rerender } = renderWithTheme(<Radio disabled />);
rerender(<Radio disabled={false} />);
const checkbox = getByRole('radio');
expect(checkbox).not.toHaveAttribute('disabled');
});
});
describe('controlled', () => {
it('should check the radio', () => {
const { getByRole, rerender } = renderWithTheme(
<Radio checked={false} readOnly />
);
rerender(<Radio checked readOnly />);
const checkbox = getByRole('radio') as HTMLInputElement;
expect(checkbox.checked).toBe(true);
expect(getByRole('radio')).toHaveAttribute('checked');
expect(getByRole('presentation').firstChild).toHaveAttribute(
'data-testid',
'checkmarkIcon'
);
});
it('should uncheck the checkbox', () => {
const { getByRole, rerender } = renderWithTheme(
<Radio checked readOnly />
);
rerender(<Radio checked={false} readOnly />);
const checkbox = getByRole('radio') as HTMLInputElement;
expect(checkbox.checked).toBe(false);
expect(getByRole('radio')).not.toHaveAttribute('checked');
expect(getByRole('presentation').firstChild).toBeNull();
});
});
});
================================================
FILE: src/Radio/Radio.stories.tsx
================================================
import { ComponentMeta } from '@storybook/react';
import React, { useState } from 'react';
import { GroupBox, Radio, ScrollView, Window, WindowContent } from 'react95';
import styled from 'styled-components';
const Wrapper = styled.div`
padding: 5rem;
background: ${({ theme }) => theme.desktopBackground};
#cutout {
background: ${({ theme }) => theme.canvas};
color: ${({ theme }) => theme.materialText};
padding: 1rem;
width: 300px;
& p {
margin-bottom: 2rem;
}
}
`;
export default {
title: 'Controls/Radio',
component: Radio,
decorators: [story => <Wrapper>{story()}</Wrapper>]
} as ComponentMeta<typeof Radio>;
export function Default() {
const [state, setState] = useState('Pear');
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) =>
setState(e.target.value);
return (
<Window>
<WindowContent>
<GroupBox label='Fruits'>
<Radio
checked={state === 'Pear'}
onChange={handleChange}
value='Pear'
label='🍐 Pear'
name='fruits'
/>
<br />
<Radio
checked={state === 'Orange'}
onChange={handleChange}
value='Orange'
label='🍊 Orange'
name='fruits'
/>
<br />
<Radio
checked={state === 'Kiwi'}
onChange={handleChange}
value='Kiwi'
label='🥝 Kiwi'
name='fruits'
/>
<br />
<Radio
checked={state === 'Grape'}
onChange={handleChange}
value='Grape'
label='🍇 Grape'
name='fruits'
disabled
/>
</GroupBox>
</WindowContent>
</Window>
);
}
Default.story = {
name: 'default'
};
export function Flat() {
const [state, setState] = useState('Pear');
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) =>
setState(e.target.value);
return (
<Window>
<WindowContent>
<ScrollView id='cutout'>
<p>
When you want to use radio buttons on a light background (like
scrollable content), just use the flat variant:
</p>
<GroupBox variant='flat' label='Fruits'>
<Radio
variant='flat'
checked={state === 'Pear'}
onChange={handleChange}
value='Pear'
label='🍐 Pear'
name='fruits'
/>
<br />
<Radio
variant='flat'
checked={state === 'Orange'}
onChange={handleChange}
value='Orange'
label='🍊 Orange'
name='fruits'
/>
<br />
<Radio
variant='flat'
checked={state === 'Kiwi'}
onChange={handleChange}
value='Kiwi'
label='🥝 Kiwi'
name='fruits'
/>
<br />
<Radio
variant='flat'
checked={state === 'Grape'}
onChange={handleChange}
value='Grape'
label='🍇 Grape'
name='fruits'
disabled
/>
</GroupBox>
</ScrollView>
</WindowContent>
</Window>
);
}
Flat.story = {
name: 'flat'
};
================================================
FILE: src/Radio/Radio.tsx
================================================
import React, { forwardRef } from 'react';
import styled, { css, CSSProperties } from 'styled-components';
import { createFlatBoxStyles } from '../common';
import {
LabelText,
size,
StyledInput,
StyledLabel
} from '../common/SwitchBase';
import { StyledScrollView } from '../ScrollView/ScrollView';
import { CommonStyledProps } from '../types';
type RadioVariant = 'default' | 'flat';
type RadioProps = {
checked?: boolean;
className?: string;
disabled?: boolean;
label?: string | number;
name?: string;
onChange?: React.ChangeEventHandler<HTMLInputElement>;
style?: CSSProperties;
value?: string | number | boolean;
variant?: RadioVariant;
} & Omit<
React.InputHTMLAttributes<HTMLInputElement>,
'checked' | 'className' | 'disabled' | 'name' | 'onChange' | 'style' | 'value'
> &
CommonStyledProps;
const sharedCheckboxStyles = css`
width: ${size}px;
height: ${size}px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: space-around;
margin-right: 0.5rem;
`;
type StyledCheckboxProps = {
$disabled: boolean;
};
const StyledCheckbox = styled(StyledScrollView)<StyledCheckboxProps>`
${sharedCheckboxStyles}
background: ${({ $disabled, theme }) =>
$disabled ? theme.material : theme.canvas};
&:before {
content: '';
position: absolute;
left: 0px;
top: 0px;
width: calc(100% - 4px);
height: calc(100% - 4px);
border-radius: 50%;
box-shadow: none;
}
`;
const StyledFlatCheckbox = styled.div<StyledCheckboxProps>`
${createFlatBoxStyles()}
${sharedCheckboxStyles}
outline: none;
background: ${({ $disabled, theme }) =>
$disabled ? theme.flatLight : theme.canvas};
&:before {
content: '';
display: inline-block;
position: absolute;
top: 0;
left: 0;
width: calc(100% - 4px);
height: calc(100% - 4px);
border: 2px solid ${({ theme }) => theme.flatDark};
border-radius: 50%;
}
`;
type IconProps = {
'data-testid': 'checkmarkIcon';
$disabled: boolean;
variant: RadioVariant;
};
const Icon = styled.span.attrs(() => ({
'data-testid': 'checkmarkIcon'
}))<IconProps>`
position: absolute;
content: '';
display: inline-block;
top: 50%;
left: 50%;
width: 6px;
height: 6px;
transform: translate(-50%, -50%);
border-radius: 50%;
background: ${p =>
p.$disabled ? p.theme.checkmarkDisabled : p.theme.checkmark};
`;
const CheckboxComponents = {
flat: StyledFlatCheckbox,
default: StyledCheckbox
};
const Radio = forwardRef<HTMLInputElement, RadioProps>(
(
{
checked,
className = '',
disabled = false,
label = '',
onChange,
style = {},
variant = 'default',
...otherProps
},
ref
) => {
const CheckboxComponent = CheckboxComponents[variant];
return (
<StyledLabel $disabled={disabled} className={className} style={style}>
<CheckboxComponent $disabled={disabled} role='presentation'>
{checked && <Icon $disabled={disabled} variant={variant} />}
</CheckboxComponent>
<StyledInput
disabled={disabled}
onChange={disabled ? undefined : onChange}
readOnly={disabled}
type='radio'
checked={checked}
ref={ref}
{...otherProps}
/>
{label && <LabelText>{label}</LabelText>}
</StyledLabel>
);
}
);
Radio.displayName = 'Radio';
export { Radio, RadioProps };
================================================
FILE: src/ScrollView/ScrollView.spec.tsx
================================================
import { render } from '@testing-library/react';
import React from 'react';
import { ScrollView } from './ScrollView';
describe('<ScrollView />', () => {
it('should render scrollview', () => {
const { container } = render(<ScrollView />);
const scrollView = container.firstElementChild;
expect(scrollView).toBeInTheDocument();
});
it('should render custom styles', () => {
const { container } = render(
<ScrollView style={{ backgroundColor: 'papayawhip' }} />
);
const scrollView = container.firstElementChild;
expect(scrollView).toHaveAttribute(
'style',
'background-color: papayawhip;'
);
});
it('should render children', async () => {
const { findByText } = render(
<ScrollView>
<span>Cool ScrollView</span>
</ScrollView>
);
const content = await findByText(/cool scrollview/i);
expect(content).toBeInTheDocument();
});
it('should render custom props', () => {
const customProps = { title: 'scrollview' };
const { container } = render(<ScrollView {...customProps} />);
const scrollView = container.firstElementChild;
expect(scrollView).toHaveAttribute('title', 'scrollview');
});
});
================================================
FILE: src/ScrollView/ScrollView.stories.tsx
================================================
import { ComponentMeta } from '@storybook/react';
import React from 'react';
import { ScrollView, Window, WindowContent } from 'react95';
import styled from 'styled-components';
const Wrapper = styled.div`
padding: 5rem;
background: ${({ theme }) => theme.desktopBackground};
`;
export default {
title: 'Layout/ScrollView',
component: ScrollView,
decorators: [story => <Wrapper>{story()}</Wrapper>]
} as ComponentMeta<typeof ScrollView>;
export function Default() {
return (
<Window>
<WindowContent>
<ScrollView style={{ width: '300px', height: '200px' }}>
<div>
<p style={{ width: 400 }}>
React95 is the best UI library ever created
</p>
<p>React95 is the best UI library ever created</p>
<p>React95 is the best UI library ever created</p>
<p>React95 is the best UI library ever created</p>
<p>React95 is the best UI library ever created</p>
<p>React95 is the best UI library ever created</p>
<p>React95 is the best UI library ever created</p>
<p>React95 is the best UI library ever created</p>
<p>React95 is the best UI library ever created</p>
</div>
</ScrollView>
</WindowContent>
</Window>
);
}
Default.story = {
name: 'default'
};
================================================
FILE: src/ScrollView/ScrollView.tsx
================================================
import React, { forwardRef } from 'react';
import styled from 'styled-components';
import { insetShadow, createScrollbars } from '../common';
import { CommonStyledProps } from '../types';
type ScrollViewProps = {
children?: React.ReactNode;
shadow?: boolean;
} & React.HTMLAttributes<HTMLDivElement> &
CommonStyledProps;
export const StyledScrollView = styled.div<Pick<ScrollViewProps, 'shadow'>>`
position: relative;
box-sizing: border-box;
padding: 2px;
font-size: 1rem;
border-style: solid;
border-width: 2px;
border-left-color: ${({ theme }) => theme.borderDark};
border-top-color: ${({ theme }) => theme.borderDark};
border-right-color: ${({ theme }) => theme.borderLightest};
border-bottom-color: ${({ theme }) => theme.borderLightest};
line-height: 1.5;
&:before {
position: absolute;
left: 0;
top: 0;
content: '';
width: calc(100% - 4px);
height: calc(100% - 4px);
border-style: solid;
border-width: 2px;
border-left-color: ${({ theme }) => theme.borderDarkest};
border-top-color: ${({ theme }) => theme.borderDarkest};
border-right-color: ${({ theme }) => theme.borderLight};
border-bottom-color: ${({ theme }) => theme.borderLight};
pointer-events: none;
${props => props.shadow && `box-shadow:${insetShadow};`}
}
`;
const Content = styled.div`
box-sizing: border-box;
width: 100%;
height: 100%;
padding: 4px;
overflow: auto;
${createScrollbars()}
`;
const ScrollView = forwardRef<HTMLDivElement, ScrollViewProps>(
({ children, shadow = true, ...otherProps }, ref) => {
return (
<StyledScrollView ref={ref} shadow={shadow} {...otherProps}>
<Content>{children}</Content>
</StyledScrollView>
);
}
);
ScrollView.displayName = 'ScrollView';
export { ScrollView, ScrollViewProps };
================================================
FILE: src/Select/Select.spec.tsx
================================================
import { fireEvent, screen, waitFor } from '@testing-library/react';
import React from 'react';
import { renderWithTheme } from '../../test/utils';
import { noOp } from '../common/utils';
import { Select } from './Select';
import { SelectOption, SelectRef } from './Select.types';
const options: SelectOption<number>[] = [
{ label: 'ten', value: 10 },
{ label: 'twenty', value: 20 },
{ label: 'thirty', value: 30 }
];
describe('<Select />', () => {
it('should be able to mount the component', () => {
const { container } = renderWithTheme(
<Select defaultValue={10} options={options} />
);
const input = container.querySelector('input') as HTMLInputElement;
expect(input.value).toBe('10');
});
it('renders dropdown button with icon', () => {
renderWithTheme(<Select defaultValue={10} options={options} />);
const button = screen.getByTestId('select-button');
expect(button).toBeInTheDocument();
// we render styled.button, but as='div'
// because it's used only for aesthetic purposes
expect(button.tagName).not.toBe('BUTTON');
expect(button.firstChild).toHaveAttribute('data-testid', 'select-icon');
});
it('the trigger is in tab order', () => {
renderWithTheme(<Select defaultValue={10} options={options} />);
expect(screen.getByRole('button')).toHaveProperty('tabIndex', 1);
});
it('should accept null child', () => {
renderWithTheme(<Select defaultValue={10} options={[...options, null]} />);
});
it('should have an input with [type="hidden"] and string value by default', () => {
const { container } = renderWithTheme(
<Select defaultValue={10} options={options} />
);
const input = container.querySelector('input');
expect(input).toHaveAttribute('type', 'hidden');
expect(input).toHaveAttribute('value', '10');
});
it('passes through the blur event when menu is closed', () => {
const handleBlur = jest.fn();
renderWithTheme(
<Select
onBlur={handleBlur}
options={[
{ label: 'ten', value: '10' },
{ label: 'none', value: '' }
]}
/>
);
const trigger = screen.getByRole('button');
fireEvent.focus(trigger);
fireEvent.blur(trigger);
expect(handleBlur).toHaveBeenCalledTimes(1);
});
it('should ignore onBlur when the menu opens', () => {
// mousedown calls focus while click opens moving the focus to an item
// this means the trigger is blurred immediately
const handleBlur = jest.fn();
renderWithTheme(
<Select
onBlur={handleBlur}
defaultValue=''
onMouseDown={event => {
// simulating certain platforms that focus on mousedown
if (event.defaultPrevented === false) {
event.currentTarget.focus();
}
}}
options={[
{ label: 'ten', value: '10' },
{ label: 'none', value: '' }
]}
/>
);
const trigger = screen.getByRole('button');
fireEvent.mouseDown(trigger);
expect(handleBlur).toHaveBeenCalledTimes(0);
expect(screen.getByRole('listbox')).toBeInTheDocument();
const o = screen.getAllByRole('option');
fireEvent.mouseDown(o[0]);
o[0].click();
expect(handleBlur).toHaveBeenCalledTimes(0);
expect(screen.queryByRole('listbox', { hidden: false })).toBe(null);
});
it('options should have a data-value attribute', () => {
renderWithTheme(
<Select defaultValue={10} onOpen={noOp} open options={options} />
);
const o = screen.getAllByRole('option');
expect(o[0]).toHaveAttribute('data-value', '10');
expect(o[1]).toHaveAttribute('data-value', '20');
});
it('should call onClose when user clicks outside of component', async () => {
const handleClose = jest.fn();
renderWithTheme(
<div>
<Select onClose={handleClose} options={options} />
<div data-testid='el'>swag</div>
</div>
);
expect(handleClose).toHaveBeenCalledTimes(0);
fireEvent.mouseDown(screen.getByRole('button'));
fireEvent.mouseDown(screen.getByText('swag'));
expect(handleClose).toHaveBeenCalledTimes(1);
});
it('should open and close on mouseDown', async () => {
renderWithTheme(<Select options={options} />);
fireEvent.mouseDown(screen.getByRole('button'));
expect(screen.getByRole('listbox')).toBeInTheDocument();
fireEvent.mouseDown(screen.getByRole('button'));
expect(screen.queryByRole('listbox')).not.toBeInTheDocument();
});
describe('prop: inputProps', () => {
it('should apply additional props to trigger element', () => {
renderWithTheme(
<Select
defaultValue={10}
inputProps={{ 'data-testid': 'SelectInput' }}
options={options}
/>
);
expect(screen.getByTestId('SelectInput')).toHaveProperty(
'tagName',
'INPUT'
);
});
});
describe('prop: menuMaxHeight', () => {
it('sets max-height to dropdown', () => {
renderWithTheme(
<Select
defaultValue={10}
onOpen={noOp}
open
options={options}
menuMaxHeight={220}
/>
);
const listbox = screen.getByRole('listbox') as HTMLElement;
expect(
listbox.getAttribute('style')?.includes('max-height: 220px')
).toBeTruthy();
});
});
describe('prop: onClose, onFocus, onKeyDown, onOpen', () => {
it('passes event through', () => {
const handler = jest.fn();
renderWithTheme(
<>
<Select
defaultValue={10}
onClose={handler}
onFocus={handler}
onKeyDown={handler}
onOpen={handler}
options={options}
/>
<div>outside</div>
</>
);
const button = screen.getByRole('button');
fireEvent.focus(button);
expect(handler).toHaveBeenCalledWith(
expect.objectContaining({ type: 'focus' })
);
handler.mockClear();
fireEvent.keyDown(button);
expect(handler).toHaveBeenCalledWith(
expect.objectContaining({ type: 'keydown' })
);
handler.mockClear();
fireEvent.mouseDown(button);
expect(handler).toHaveBeenCalledWith({
fromEvent: expect.objectContaining({ type: 'mousedown' })
});
handler.mockClear();
fireEvent.mouseDown(screen.getByText('outside'));
expect(handler).toHaveBeenCalledWith({
fromEvent: expect.objectContaining({ type: 'mousedown' })
});
handler.mockClear();
});
});
describe('prop: onChange', () => {
it('should get selected option from arguments', () => {
const onChange = jest.fn();
renderWithTheme(
<Select onChange={onChange} defaultValue={0} options={options} />
);
fireEvent.mouseDown(screen.getByRole('button'));
const option = screen.getAllByRole('option')[1];
fireEvent.mouseEnter(option);
fireEvent.click(option);
expect(onChange).toHaveBeenCalledTimes(1);
expect(onChange).toHaveBeenCalledWith(options[1], {
fromEvent: expect.anything()
});
});
});
describe('prop: readOnly', () => {
it('should not trigger any event with readOnly', () => {
renderWithTheme(<Select readOnly defaultValue={10} options={options} />);
screen.getByRole('button').focus();
const focusedButton = document.activeElement as HTMLElement;
fireEvent.keyDown(focusedButton, { key: 'ArrowDown' });
expect(screen.queryByRole('listbox')).not.toBeInTheDocument();
fireEvent.keyUp(focusedButton, { key: 'ArrowDown' });
expect(screen.queryByRole('listbox')).not.toBeInTheDocument();
});
});
describe('prop: value', () => {
it('should select the option based on the value', () => {
renderWithTheme(
<Select defaultValue={20} options={options} open onOpen={noOp} />
);
const o = screen.getAllByRole('option');
expect(o[0]).not.toHaveAttribute('aria-selected');
expect(o[1]).toHaveAttribute('aria-selected', 'true');
expect(o[2]).not.toHaveAttribute('aria-selected');
});
it('should select only the option that matches the object', () => {
const obj1 = { id: 1 };
const obj2 = { id: 2 };
renderWithTheme(
<Select
open
onOpen={noOp}
defaultValue={obj1}
options={[
{ label: '1', value: obj1 },
{ label: '2', value: obj2 }
]}
/>
);
const o = screen.getAllByRole('option');
expect(o[0]).toHaveAttribute('aria-selected', 'true');
expect(o[1]).not.toHaveAttribute('aria-selected');
});
it('should be able to use an object', () => {
const value = {};
renderWithTheme(
<Select
open
onOpen={noOp}
defaultValue={value}
options={[...options, { value, label: 'object-label' }]}
/>
);
expect(screen.getByRole('button')).toHaveTextContent('object-label');
});
});
describe('prop: open (controlled)', () => {
// TODO add more tests
it('should be open when initially true', () => {
renderWithTheme(<Select open onOpen={noOp} options={options} />);
expect(screen.getByRole('listbox')).toBeInTheDocument();
});
it('open only with the left mouse button click', () => {
// Right/middle mouse click shouldn't open the Select
renderWithTheme(<Select defaultValue={10} options={options} />);
const trigger = screen.getByRole('button');
// If clicked by the right/middle mouse button, no options list should be opened
fireEvent.mouseDown(trigger, { button: 1 });
expect(screen.queryByRole('listbox')).not.toBeInTheDocument();
fireEvent.mouseDown(trigger, { button: 2 });
expect(screen.queryByRole('listbox')).not.toBeInTheDocument();
});
});
describe('prop: formatDisplay', () => {
it('should use the prop to render the value', () => {
const formatDisplay = (x: SelectOption<number>) =>
`0b${Number(x.value).toString(2)}`;
renderWithTheme(
<Select
formatDisplay={formatDisplay}
options={[{ value: 2, label: '2' }]}
/>
);
expect(screen.getByRole('button')).toHaveTextContent('0b10');
});
});
describe('prop: ref', () => {
it('should be able to return the input node via a ref object', () => {
const ref = React.createRef<SelectRef>();
renderWithTheme(<Select ref={ref} defaultValue='' />);
expect(ref.current?.node).toHaveProperty('tagName', 'INPUT');
});
it('should be able focus the trigger imperatively', () => {
const ref = React.createRef<SelectRef>();
renderWithTheme(<Select ref={ref} defaultValue='' />);
ref.current?.focus();
expect(screen.getByRole('button')).toHaveFocus();
});
});
describe('spread props', () => {
it('should apply additional props to trigger element', () => {
renderWithTheme(
<Select data-test='SelectDisplay' defaultValue={10} options={options} />
);
expect(screen.getByRole('button')).toHaveAttribute(
'data-test',
'SelectDisplay'
);
});
});
describe('keyboard', () => {
it.each(['Space', 'ArrowUp', 'ArrowDown', 'Home', 'End'])(
`should open menu when pressed %s key on select`,
code => {
renderWithTheme(
<Select defaultValue='' options={[{ label: 'none', value: '' }]} />
);
screen.getByRole('button').focus();
const focusedButton = document.activeElement as HTMLButtonElement;
fireEvent.keyDown(focusedButton, { code });
expect(
screen.getByRole('listbox', { hidden: false })
).toBeInTheDocument();
fireEvent.keyUp(focusedButton, { code });
expect(
screen.getByRole('listbox', { hidden: false })
).toBeInTheDocument();
}
);
it('closes menu when pressing Escape', async () => {
const onClose = jest.fn();
renderWithTheme(
<Select defaultValue={10} options={options} onClose={onClose} />
);
const button = screen.getByRole('button');
fireEvent.mouseDown(button);
const listbox = screen.getByRole('listbox');
expect(screen.getByRole('option', { name: 'ten' })).toBeInTheDocument();
fireEvent.keyDown(listbox, { code: 'ArrowDown' });
expect(screen.getByRole('option', { name: 'twenty' })).toHaveFocus();
fireEvent.keyDown(listbox, { code: 'Escape' });
await waitFor(() => {
expect(onClose).toHaveBeenCalled();
});
expect(listbox).not.toBeInTheDocument();
expect(button).toHaveFocus();
});
it.each(['Enter', 'Space', 'Tab'])(
'selects the active option by pressing %s, closes menu and maintains focus',
async keyCode => {
const onClose = jest.fn();
const onKeyDown = jest.fn();
renderWithTheme(
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
<div onKeyDown={onKeyDown}>
<Select defaultValue={10} options={options} onClose={onClose} />
</div>
);
const button = screen.getByRole('button');
fireEvent.mouseDown(button);
const listbox = screen.getByRole('listbox');
expect(screen.getByRole('option', { name: 'ten' })).toBeInTheDocument();
fireEvent.keyDown(listbox, { code: 'ArrowDown' });
expect(screen.getByRole('option', { name: 'twenty' })).toHaveFocus();
fireEvent.keyDown(listbox, { code: keyCode });
await waitFor(() => {
expect(onClose).toHaveBeenCalled();
});
expect(listbox).not.toBeInTheDocument();
expect(button).toHaveFocus();
expect(onKeyDown).toHaveBeenCalledWith(
expect.objectContaining({ defaultPrevented: true })
);
}
);
it('passes through Enter, Escape, Tab and Shift + Tab when closed', () => {
const onKeyDown = jest.fn();
renderWithTheme(
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
<div onKeyDown={onKeyDown}>
<Select defaultValue={10} options={options} />
</div>
);
const button = screen.getByRole('button');
const eventOptions = [
{ code: 'Enter' },
{ code: 'Escape' },
{ code: 'Tab' },
{ code: 'Tab', shiftKey: true }
];
eventOptions.forEach(eventOption => {
fireEvent.keyDown(button, eventOption);
expect(onKeyDown).toHaveBeenCalledWith(
expect.objectContaining({ defaultPrevented: false })
);
});
});
it('passes through keyDown events when modifier keys are pressed', () => {
const onKeyDown = jest.fn();
renderWithTheme(
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
<div onKeyDown={onKeyDown}>
<Select defaultValue={10} options={options} />
</div>
);
const button = screen.getByRole('button');
button.focus();
const eventOptions = [
{ altKey: true },
{ ctrlKey: true },
{ metaKey: true },
{ shiftKey: true }
];
eventOptions.forEach(eventOption => {
fireEvent.keyDown(button, { ...eventOption, code: 'KeyT' });
expect(button).toHaveTextContent('ten');
expect(onKeyDown).toHaveBeenCalledWith(
expect.objectContaining({ defaultPrevented: false })
);
});
});
it('moves options using ArrowUp, ArrowDown, Home and End', async () => {
renderWithTheme(<Select defaultValue={10} options={options} />);
const button = screen.getByRole('button');
fireEvent.mouseDown(button);
const listbox = screen.getByRole('listbox');
expect(screen.getByRole('option', { name: 'ten' })).toBeInTheDocument();
fireEvent.keyDown(listbox, { code: 'ArrowDown' });
expect(screen.getByRole('option', { name: 'twenty' })).toHaveFocus();
fireEvent.keyDown(listbox, { code: 'ArrowUp' });
expect(screen.getByRole('option', { name: 'ten' })).toHaveFocus();
fireEvent.keyDown(listbox, { code: 'End' });
expect(screen.getByRole('option', { name: 'thirty' })).toHaveFocus();
fireEvent.keyDown(listbox, { code: 'Home' });
expect(screen.getByRole('option', { name: 'ten' })).toHaveFocus();
});
it('cycles through options when pressing the same key (open menu)', async () => {
renderWithTheme(<Select defaultValue={10} options={options} />);
const button = screen.getByRole('button');
fireEvent.mouseDown(button);
const listbox = screen.getByRole('listbox');
expect(screen.getByRole('option', { name: 'ten' })).toBeInTheDocument();
fireEvent.keyDown(listbox, { code: 'KeyT' });
expect(screen.getByRole('option', { name: 'ten' })).toHaveFocus();
fireEvent.keyDown(listbox, { code:
gitextract_rz85rgj9/
├── .babelrc
├── .codesandbox/
│ └── ci.json
├── .editorconfig
├── .eslintrc.js
├── .firebaserc
├── .github/
│ ├── FUNDING.yml
│ └── workflows/
│ ├── ci.yml
│ └── release.yml
├── .gitignore
├── .prettierrc
├── .storybook/
│ ├── decorators/
│ │ └── withGlobalStyle.tsx
│ ├── main.ts
│ ├── manager.css
│ ├── manager.ts
│ ├── preview.ts
│ ├── theme-picker/
│ │ ├── ThemeButton.tsx
│ │ ├── ThemeList.tsx
│ │ ├── ThemeProvider.tsx
│ │ ├── constants.ts
│ │ └── register.ts
│ └── theme.js
├── LICENSE
├── README.md
├── docs/
│ ├── Contributing.stories.mdx
│ ├── Getting-Started.stories.mdx
│ ├── Submit-your-Project.stories.mdx
│ └── Welcome.stories.mdx
├── firebase.json
├── jest.config.js
├── package.json
├── rollup.config.js
├── src/
│ ├── Anchor/
│ │ ├── Anchor.spec.tsx
│ │ ├── Anchor.stories.tsx
│ │ └── Anchor.tsx
│ ├── AppBar/
│ │ ├── AppBar.spec.tsx
│ │ ├── AppBar.stories.tsx
│ │ └── AppBar.tsx
│ ├── Avatar/
│ │ ├── Avatar.spec.tsx
│ │ ├── Avatar.stories.tsx
│ │ └── Avatar.tsx
│ ├── Button/
│ │ ├── Button.spec.tsx
│ │ ├── Button.stories.tsx
│ │ └── Button.tsx
│ ├── Checkbox/
│ │ ├── Checkbox.spec.tsx
│ │ ├── Checkbox.stories.tsx
│ │ └── Checkbox.tsx
│ ├── ColorInput/
│ │ ├── ColorInput.spec.tsx
│ │ ├── ColorInput.stories.tsx
│ │ └── ColorInput.tsx
│ ├── Counter/
│ │ ├── Counter.spec.tsx
│ │ ├── Counter.stories.tsx
│ │ ├── Counter.tsx
│ │ └── Digit.tsx
│ ├── DatePicker/
│ │ ├── DatePicker.stories.tsx
│ │ └── DatePicker.tsx
│ ├── Frame/
│ │ ├── Frame.spec.tsx
│ │ ├── Frame.stories.tsx
│ │ └── Frame.tsx
│ ├── GroupBox/
│ │ ├── GroupBox.spec.tsx
│ │ ├── GroupBox.stories.tsx
│ │ └── GroupBox.tsx
│ ├── Handle/
│ │ ├── Handle.spec.tsx
│ │ ├── Handle.stories.tsx
│ │ └── Handle.tsx
│ ├── Hourglass/
│ │ ├── Hourglass.spec.tsx
│ │ ├── Hourglass.stories.tsx
│ │ ├── Hourglass.tsx
│ │ └── base64hourglass.tsx
│ ├── MenuList/
│ │ ├── MenuList.spec.tsx
│ │ ├── MenuList.stories.tsx
│ │ ├── MenuList.tsx
│ │ ├── MenuListItem.spec.tsx
│ │ └── MenuListItem.tsx
│ ├── Monitor/
│ │ ├── Monitor.spec.tsx
│ │ ├── Monitor.stories.tsx
│ │ └── Monitor.tsx
│ ├── NumberInput/
│ │ ├── NumberInput.spec.tsx
│ │ ├── NumberInput.stories.tsx
│ │ └── NumberInput.tsx
│ ├── ProgressBar/
│ │ ├── ProgressBar.spec.tsx
│ │ ├── ProgressBar.stories.tsx
│ │ └── ProgressBar.tsx
│ ├── Radio/
│ │ ├── Radio.spec.tsx
│ │ ├── Radio.stories.tsx
│ │ └── Radio.tsx
│ ├── ScrollView/
│ │ ├── ScrollView.spec.tsx
│ │ ├── ScrollView.stories.tsx
│ │ └── ScrollView.tsx
│ ├── Select/
│ │ ├── Select.spec.tsx
│ │ ├── Select.stories.data.ts
│ │ ├── Select.stories.tsx
│ │ ├── Select.styles.tsx
│ │ ├── Select.tsx
│ │ ├── Select.types.ts
│ │ ├── SelectNative.spec.tsx
│ │ ├── SelectNative.tsx
│ │ ├── useSelectCommon.tsx
│ │ └── useSelectState.ts
│ ├── Separator/
│ │ ├── Separator.spec.tsx
│ │ ├── Separator.stories.tsx
│ │ └── Separator.tsx
│ ├── Slider/
│ │ ├── Slider.spec.tsx
│ │ ├── Slider.stories.tsx
│ │ └── Slider.tsx
│ ├── Table/
│ │ ├── Table.spec.tsx
│ │ ├── Table.stories.tsx
│ │ ├── Table.tsx
│ │ ├── TableBody.spec.tsx
│ │ ├── TableBody.tsx
│ │ ├── TableDataCell.spec.tsx
│ │ ├── TableDataCell.tsx
│ │ ├── TableHead.spec.tsx
│ │ ├── TableHead.tsx
│ │ ├── TableHeadCell.spec.tsx
│ │ ├── TableHeadCell.tsx
│ │ ├── TableRow.spec.tsx
│ │ └── TableRow.tsx
│ ├── Tabs/
│ │ ├── Tab.spec.tsx
│ │ ├── Tab.tsx
│ │ ├── TabBody.spec.tsx
│ │ ├── TabBody.tsx
│ │ ├── Tabs.spec.tsx
│ │ ├── Tabs.stories.tsx
│ │ └── Tabs.tsx
│ ├── TextInput/
│ │ ├── TextInput.spec.tsx
│ │ ├── TextInput.stories.tsx
│ │ └── TextInput.tsx
│ ├── Toolbar/
│ │ ├── Toolbar.spec.tsx
│ │ └── Toolbar.tsx
│ ├── Tooltip/
│ │ ├── Tooltip.spec.tsx
│ │ ├── Tooltip.stories.tsx
│ │ └── Tooltip.tsx
│ ├── TreeView/
│ │ ├── TreeView.spec.tsx
│ │ ├── TreeView.stories.tsx
│ │ └── TreeView.tsx
│ ├── Window/
│ │ ├── Window.spec.tsx
│ │ ├── Window.stories.tsx
│ │ ├── Window.tsx
│ │ ├── WindowContent.spec.tsx
│ │ ├── WindowContent.tsx
│ │ ├── WindowHeader.spec.tsx
│ │ └── WindowHeader.tsx
│ ├── assets/
│ │ ├── fonts/
│ │ │ └── src/
│ │ │ ├── ms-sans-serif/
│ │ │ │ ├── license.txt
│ │ │ │ └── readme.txt
│ │ │ └── ms-sans-serif-bold/
│ │ │ ├── license.txt
│ │ │ └── readme.txt
│ │ └── images/
│ │ └── logo.psd
│ ├── common/
│ │ ├── SwitchBase.ts
│ │ ├── constants.ts
│ │ ├── hooks/
│ │ │ ├── useControlledOrUncontrolled.ts
│ │ │ ├── useEventCallback.ts
│ │ │ ├── useForkRef.spec.tsx
│ │ │ ├── useForkRef.ts
│ │ │ ├── useId.spec.ts
│ │ │ ├── useId.ts
│ │ │ └── useIsFocusVisible.ts
│ │ ├── index.ts
│ │ ├── styleReset.ts
│ │ ├── system.ts
│ │ ├── themes/
│ │ │ ├── aiee.ts
│ │ │ ├── ash.ts
│ │ │ ├── azureOrange.ts
│ │ │ ├── bee.ts
│ │ │ ├── blackAndWhite.ts
│ │ │ ├── blue.ts
│ │ │ ├── brick.ts
│ │ │ ├── candy.ts
│ │ │ ├── cherry.ts
│ │ │ ├── coldGray.ts
│ │ │ ├── counterStrike.ts
│ │ │ ├── darkTeal.ts
│ │ │ ├── denim.ts
│ │ │ ├── eggplant.ts
│ │ │ ├── fxDev.ts
│ │ │ ├── highContrast.ts
│ │ │ ├── honey.ts
│ │ │ ├── hotChocolate.ts
│ │ │ ├── hotdogStand.ts
│ │ │ ├── index.ts
│ │ │ ├── lilac.ts
│ │ │ ├── lilacRoseDark.ts
│ │ │ ├── maple.ts
│ │ │ ├── marine.ts
│ │ │ ├── matrix.ts
│ │ │ ├── millenium.ts
│ │ │ ├── modernDark.ts
│ │ │ ├── molecule.ts
│ │ │ ├── monochrome.ts
│ │ │ ├── ninjaTurtles.ts
│ │ │ ├── olive.ts
│ │ │ ├── original.ts
│ │ │ ├── pamelaAnderson.ts
│ │ │ ├── peggysPastels.ts
│ │ │ ├── plum.ts
│ │ │ ├── polarized.ts
│ │ │ ├── powerShell.ts
│ │ │ ├── rainyDay.ts
│ │ │ ├── raspberry.ts
│ │ │ ├── redWine.ts
│ │ │ ├── rose.ts
│ │ │ ├── seawater.ts
│ │ │ ├── shelbiTeal.ts
│ │ │ ├── slate.ts
│ │ │ ├── solarizedDark.ts
│ │ │ ├── solarizedLight.ts
│ │ │ ├── spruce.ts
│ │ │ ├── stormClouds.ts
│ │ │ ├── theSixtiesUSA.ts
│ │ │ ├── tokyoDark.ts
│ │ │ ├── toner.ts
│ │ │ ├── tooSexy.ts
│ │ │ ├── travel.ts
│ │ │ ├── types.ts
│ │ │ ├── vaporTeal.ts
│ │ │ ├── vermillion.ts
│ │ │ ├── violetDark.ts
│ │ │ ├── vistaesqueMidnight.ts
│ │ │ ├── water.ts
│ │ │ ├── white.ts
│ │ │ ├── windows1.ts
│ │ │ └── wmii.ts
│ │ └── utils/
│ │ ├── events.spec.tsx
│ │ ├── events.ts
│ │ ├── index.spec.ts
│ │ └── index.ts
│ ├── index.ts
│ ├── legacy/
│ │ ├── Bar.tsx
│ │ ├── Cutout.tsx
│ │ ├── Desktop.tsx
│ │ ├── Divider.tsx
│ │ ├── Fieldset.tsx
│ │ ├── List.tsx
│ │ ├── ListItem.tsx
│ │ ├── NumberField.tsx
│ │ ├── Panel.tsx
│ │ ├── Progress.tsx
│ │ ├── TextField.tsx
│ │ └── Tree.tsx
│ └── types.ts
├── test/
│ ├── setup-test.ts
│ └── utils.tsx
├── tsconfig.build.index.json
├── tsconfig.build.themes.json
├── tsconfig.json
└── types/
├── globals.d.ts
└── themes.d.ts
SYMBOL INDEX (199 symbols across 108 files)
FILE: .storybook/theme-picker/ThemeButton.tsx
function ThemeButton (line 6) | function ThemeButton({
FILE: .storybook/theme-picker/ThemeList.tsx
type ThemesProps (line 38) | type ThemesProps = {
function ThemeList (line 51) | function ThemeList({ active }: ThemesProps) {
FILE: .storybook/theme-picker/constants.ts
constant THEMES_ID (line 1) | const THEMES_ID = 'storybook/themes';
FILE: rollup.config.js
constant NODE_ENV (line 6) | const NODE_ENV = process.env.NODE_ENV || 'development';
FILE: src/Anchor/Anchor.stories.tsx
function Default (line 17) | function Default() {
FILE: src/Anchor/Anchor.tsx
type AnchorProps (line 6) | type AnchorProps = {
FILE: src/AppBar/AppBar.stories.tsx
function Default (line 26) | function Default() {
FILE: src/AppBar/AppBar.tsx
type AppBarProps (line 7) | type AppBarProps = {
FILE: src/Avatar/Avatar.stories.tsx
function Default (line 20) | function Default() {
FILE: src/Avatar/Avatar.tsx
type AvatarProps (line 6) | type AvatarProps = {
FILE: src/Button/Button.stories.tsx
function Default (line 37) | function Default() {
function Raised (line 66) | function Raised() {
function Flat (line 107) | function Flat() {
function Thin (line 145) | function Thin() {
FILE: src/Button/Button.tsx
type ButtonProps (line 15) | type ButtonProps = {
type StyledButtonProps (line 41) | type StyledButtonProps = Pick<
FILE: src/Checkbox/Checkbox.stories.tsx
function Default (line 27) | function Default() {
function Flat (line 127) | function Flat() {
FILE: src/Checkbox/Checkbox.tsx
type CheckboxProps (line 16) | type CheckboxProps = {
type CheckmarkProps (line 41) | type CheckmarkProps = {
FILE: src/ColorInput/ColorInput.spec.tsx
function rgb2hex (line 6) | function rgb2hex(str: string) {
FILE: src/ColorInput/ColorInput.stories.tsx
function Default (line 36) | function Default() {
function Flat (line 51) | function Flat() {
FILE: src/ColorInput/ColorInput.tsx
type ColorInputProps (line 10) | type ColorInputProps = {
FILE: src/Counter/Counter.stories.tsx
function Default (line 28) | function Default() {
FILE: src/Counter/Counter.tsx
type CounterProps (line 8) | type CounterProps = {
FILE: src/Counter/Digit.tsx
type DigitProps (line 7) | type DigitProps = {
function Digit (line 176) | function Digit({ digit = 0, pixelSize = 2, ...otherProps }: DigitProps) {
FILE: src/DatePicker/DatePicker.stories.tsx
function Default (line 18) | function Default() {
FILE: src/DatePicker/DatePicker.tsx
type DatePickerProps (line 11) | type DatePickerProps = {
function daysInMonth (line 72) | function daysInMonth(year: number, month: number) {
function dayIndex (line 76) | function dayIndex(year: number, month: number, day: number) {
function convertDateToState (line 80) | function convertDateToState(stringDate: string) {
FILE: src/Frame/Frame.stories.tsx
function Default (line 27) | function Default() {
FILE: src/Frame/Frame.tsx
type FrameProps (line 6) | type FrameProps = {
FILE: src/GroupBox/GroupBox.stories.tsx
function Default (line 17) | function Default() {
function Flat (line 43) | function Flat() {
function ToggleExample (line 73) | function ToggleExample() {
FILE: src/GroupBox/GroupBox.tsx
type GroupBoxProps (line 6) | type GroupBoxProps = {
FILE: src/Handle/Handle.stories.tsx
function Default (line 17) | function Default() {
FILE: src/Handle/Handle.tsx
type HandleProps (line 6) | type HandleProps = {
FILE: src/Hourglass/Hourglass.stories.tsx
function Default (line 17) | function Default() {
FILE: src/Hourglass/Hourglass.tsx
type HourglassProps (line 7) | type HourglassProps = {
FILE: src/MenuList/MenuList.stories.tsx
function Default (line 23) | function Default() {
FILE: src/MenuList/MenuList.tsx
type MenuListProps (line 7) | type MenuListProps = React.HTMLAttributes<HTMLUListElement> & {
FILE: src/MenuList/MenuListItem.tsx
type MenuListItemProps (line 8) | type MenuListItemProps = {
FILE: src/Monitor/Monitor.stories.tsx
function Default (line 17) | function Default() {
FILE: src/Monitor/Monitor.tsx
type MonitorProps (line 6) | type MonitorProps = {
FILE: src/NumberInput/NumberInput.stories.tsx
function Default (line 29) | function Default() {
function Flat (line 45) | function Flat() {
FILE: src/NumberInput/NumberInput.tsx
type NumberInputProps (line 11) | type NumberInputProps = {
FILE: src/ProgressBar/ProgressBar.stories.tsx
function Default (line 17) | function Default() {
function Tile (line 42) | function Tile() {
function HideValue (line 67) | function HideValue() {
FILE: src/ProgressBar/ProgressBar.tsx
type ProgressBarProps (line 14) | type ProgressBarProps = {
FILE: src/Radio/Radio.stories.tsx
function Default (line 26) | function Default() {
function Flat (line 77) | function Flat() {
FILE: src/Radio/Radio.tsx
type RadioVariant (line 14) | type RadioVariant = 'default' | 'flat';
type RadioProps (line 16) | type RadioProps = {
type StyledCheckboxProps (line 42) | type StyledCheckboxProps = {
type IconProps (line 81) | type IconProps = {
FILE: src/ScrollView/ScrollView.stories.tsx
function Default (line 17) | function Default() {
FILE: src/ScrollView/ScrollView.tsx
type ScrollViewProps (line 6) | type ScrollViewProps = {
FILE: src/Select/Select.stories.tsx
function Default (line 58) | function Default() {
function Flat (line 107) | function Flat() {
function CustomDisplayFormatting (line 157) | function CustomDisplayFormatting() {
FILE: src/Select/Select.styles.tsx
type CommonSelectStyleProps (line 16) | type CommonSelectStyleProps = {
FILE: src/Select/Select.tsx
type SelectProps (line 22) | type SelectProps<T> = SelectInnerProps<T> &
function SelectInnerOption (line 29) | function SelectInnerOption<T>({
function SelectInner (line 76) | function SelectInner<T>(
FILE: src/Select/Select.types.ts
type SelectChangeEventTargetValue (line 4) | type SelectChangeEventTargetValue<T> = { value: T; name: string | undefi...
type SelectChangeEvent (line 6) | type SelectChangeEvent<T> =
type SelectOption (line 19) | type SelectOption<T> = {
type SelectRef (line 24) | type SelectRef = Pick<HTMLInputElement, 'value' | 'focus'> & {
type SelectVariants (line 28) | type SelectVariants = 'default' | 'flat';
type SelectFormatDisplayCallback (line 30) | type SelectFormatDisplayCallback<T> = (
type SelectCommonProps (line 34) | type SelectCommonProps<T> = {
type SelectInnerProps (line 56) | type SelectInnerProps<T> = {
FILE: src/Select/SelectNative.tsx
type SelectNativeProps (line 9) | type SelectNativeProps = SelectCommonProps<string> &
FILE: src/Select/useSelectState.ts
constant TYPING_RESET_DELAY (line 15) | const TYPING_RESET_DELAY = 1000;
FILE: src/Separator/Separator.stories.tsx
function Default (line 18) | function Default() {
FILE: src/Separator/Separator.tsx
type SeparatorProps (line 5) | type SeparatorProps = {
FILE: src/Slider/Slider.spec.tsx
function createTouches (line 8) | function createTouches(
FILE: src/Slider/Slider.stories.tsx
function Default (line 43) | function Default() {
function Flat (line 122) | function Flat() {
FILE: src/Slider/Slider.tsx
type SliderOnChangeHandler (line 27) | type SliderOnChangeHandler = (value: number) => void;
type SliderProps (line 29) | type SliderProps = {
function percentToValue (line 50) | function percentToValue(percent: number, min: number, max: number) {
function trackFinger (line 54) | function trackFinger(
function ownerDocument (line 82) | function ownerDocument(node?: Element) {
function findClosest (line 86) | function findClosest(values: number[], currentValue: number) {
type StyledSliderProps (line 111) | type StyledSliderProps = Pick<
FILE: src/Table/Table.stories.tsx
function Default (line 35) | function Default() {
FILE: src/Table/Table.tsx
type TableProps (line 6) | type TableProps = {
FILE: src/Table/TableBody.spec.tsx
function mountInTable (line 8) | function mountInTable(node: React.ReactNode) {
FILE: src/Table/TableBody.tsx
type TableBodyProps (line 6) | type TableBodyProps = {
FILE: src/Table/TableDataCell.spec.tsx
function mountInTable (line 8) | function mountInTable(node: React.ReactNode) {
FILE: src/Table/TableDataCell.tsx
type TableDataCellProps (line 5) | type TableDataCellProps = {
FILE: src/Table/TableHead.spec.tsx
function mountInTable (line 8) | function mountInTable(node: React.ReactNode) {
FILE: src/Table/TableHead.tsx
type TableHeadProps (line 5) | type TableHeadProps = {
FILE: src/Table/TableHeadCell.spec.tsx
function mountInTable (line 8) | function mountInTable(node: React.ReactNode) {
FILE: src/Table/TableHeadCell.tsx
type TableHeadCellProps (line 7) | type TableHeadCellProps = {
FILE: src/Table/TableRow.spec.tsx
function mountInTable (line 7) | function mountInTable(node: React.ReactNode) {
FILE: src/Table/TableRow.tsx
type TableRowProps (line 5) | type TableRowProps = {
FILE: src/Tabs/Tab.tsx
type TabProps (line 8) | type TabProps = {
FILE: src/Tabs/TabBody.tsx
type TabBodyProps (line 7) | type TabBodyProps = {
FILE: src/Tabs/Tabs.stories.tsx
function Default (line 29) | function Default() {
function MultiRow (line 89) | function MultiRow() {
FILE: src/Tabs/Tabs.tsx
type TabsProps (line 8) | type TabsProps = {
function splitToChunks (line 51) | function splitToChunks<T>(array: T[], parts: number) {
FILE: src/TextInput/TextInput.stories.tsx
function Default (line 27) | function Default() {
function Flat (line 69) | function Flat() {
FILE: src/TextInput/TextInput.tsx
type TextInputInputProps (line 13) | type TextInputInputProps = {
type TextInputTextAreaProps (line 23) | type TextInputTextAreaProps = {
type TextInputProps (line 31) | type TextInputProps = {
type WrapperProps (line 42) | type WrapperProps = Pick<TextInputProps, 'fullWidth' | 'variant'> &
type InputProps (line 68) | type InputProps = Pick<TextInputProps, 'disabled' | 'fullWidth' | 'varia...
FILE: src/Toolbar/Toolbar.tsx
type ToolbarProps (line 4) | type ToolbarProps = {
FILE: src/Tooltip/Tooltip.stories.tsx
function Default (line 17) | function Default() {
FILE: src/Tooltip/Tooltip.tsx
type TooltipPosition (line 8) | type TooltipPosition = 'top' | 'bottom' | 'left' | 'right';
type TooltipProps (line 10) | type TooltipProps = {
FILE: src/TreeView/TreeView.stories.tsx
function getIds (line 89) | function getIds(item: TreeLeaf<string>) {
function Basic (line 97) | function Basic() {
function Controlled (line 111) | function Controlled() {
function Disabled (line 144) | function Disabled() {
function DisabledTreeItems (line 158) | function DisabledTreeItems() {
FILE: src/TreeView/TreeView.tsx
type TreeLeaf (line 8) | type TreeLeaf<T> = {
type TreeViewProps (line 16) | type TreeViewProps<T> = {
type TreeBranchProps (line 32) | type TreeBranchProps<T> = {
function toggleItem (line 211) | function toggleItem<T>(state: T[], id: T) {
function preventDefault (line 217) | function preventDefault(event: React.SyntheticEvent) {
function TreeBranch (line 221) | function TreeBranch<T>({
function TreeInner (line 308) | function TreeInner<T>(
FILE: src/Window/Window.stories.tsx
function Default (line 72) | function Default() {
FILE: src/Window/Window.tsx
type WindowProps (line 6) | type WindowProps = {
FILE: src/Window/WindowContent.tsx
type WindowContentProps (line 5) | type WindowContentProps = {
FILE: src/Window/WindowHeader.tsx
type WindowHeaderProps (line 6) | type WindowHeaderProps = {
FILE: src/common/constants.ts
constant KEYBOARD_KEY_CODES (line 1) | const KEYBOARD_KEY_CODES = {
FILE: src/common/hooks/useControlledOrUncontrolled.ts
function useControlledOrUncontrolled (line 3) | function useControlledOrUncontrolled<T>({
FILE: src/common/hooks/useEventCallback.ts
function useEventCallback (line 9) | function useEventCallback<Args extends unknown[], Return>(
FILE: src/common/hooks/useForkRef.spec.tsx
function Component (line 18) | function Component(props: { innerRef: React.RefObject<HTMLDivElement> }) {
type OuterProps (line 56) | type OuterProps = {
function Inner (line 72) | function Inner() {
function Div (line 86) | function Div(
FILE: src/common/hooks/useForkRef.ts
function setRef (line 5) | function setRef<T>(
function useForkRef (line 17) | function useForkRef<T>(
FILE: src/common/hooks/useId.ts
function makeId (line 3) | function makeId() {
FILE: src/common/hooks/useIsFocusVisible.ts
function focusTriggersKeyboardModality (line 34) | function focusTriggersKeyboardModality(
function handleKeyDown (line 63) | function handleKeyDown(event: KeyboardEvent) {
function handlePointerDown (line 77) | function handlePointerDown() {
function handleVisibilityChange (line 81) | function handleVisibilityChange(this: Document) {
function prepare (line 93) | function prepare(doc: Document) {
function teardown (line 101) | function teardown(doc: Document) {
function isFocusVisible (line 109) | function isFocusVisible(event: React.FocusEvent) {
function handleBlurVisible (line 128) | function handleBlurVisible() {
function useIsFocusVisible (line 140) | function useIsFocusVisible<T extends Element = HTMLElement>() {
FILE: src/common/index.ts
type BorderStyles (line 66) | type BorderStyles =
type BorderStyle (line 76) | type BorderStyle = {
FILE: src/common/themes/types.ts
type Color (line 1) | type Color = string;
type Theme (line 3) | type Theme = {
type WindowsTheme (line 37) | type WindowsTheme = {
FILE: src/common/utils/events.spec.tsx
type FireEventMap (line 9) | type FireEventMap = Record<string, EventType>;
FILE: src/common/utils/events.ts
function isReactFocusEvent (line 28) | function isReactFocusEvent<T>(
function isReactKeyboardEvent (line 34) | function isReactKeyboardEvent<T>(
function isReactMouseEvent (line 40) | function isReactMouseEvent<T>(
FILE: src/common/utils/index.ts
function clamp (line 5) | function clamp(value: number, min: number | null, max: number | null) {
function linearGradient (line 15) | function linearGradient(left: string, right: string) {
function mapFromWindowsTheme (line 19) | function mapFromWindowsTheme(
function getDecimalPrecision (line 88) | function getDecimalPrecision(num: number) {
function roundValueToStep (line 102) | function roundValueToStep(value: number, step: number, min: number) {
function getSize (line 107) | function getSize(value: string | number) {
FILE: src/legacy/Bar.tsx
type BarProps (line 4) | type BarProps = HandleProps;
FILE: src/legacy/Cutout.tsx
type CutoutProps (line 4) | type CutoutProps = ScrollViewProps;
FILE: src/legacy/Desktop.tsx
type DesktopProps (line 4) | type DesktopProps = MonitorProps;
FILE: src/legacy/Divider.tsx
type DividerProps (line 4) | type DividerProps = SeparatorProps;
FILE: src/legacy/Fieldset.tsx
type FieldsetProps (line 4) | type FieldsetProps = GroupBoxProps;
FILE: src/legacy/List.tsx
type ListProps (line 4) | type ListProps = MenuListProps;
FILE: src/legacy/ListItem.tsx
type ListItemProps (line 8) | type ListItemProps = MenuListItemProps;
FILE: src/legacy/NumberField.tsx
type NumberFieldProps (line 4) | type NumberFieldProps = NumberInputProps;
FILE: src/legacy/Panel.tsx
type PanelProps (line 4) | type PanelProps = FrameProps;
FILE: src/legacy/Progress.tsx
type ProgressProps (line 4) | type ProgressProps = ProgressBarProps;
FILE: src/legacy/TextField.tsx
type TextFieldProps (line 4) | type TextFieldProps = TextInputProps;
FILE: src/legacy/Tree.tsx
type TreeProps (line 4) | type TreeProps<T> = TreeViewProps<T>;
FILE: src/types.ts
type Sizes (line 5) | type Sizes = 'sm' | 'md' | 'lg';
type Orientation (line 7) | type Orientation = 'horizontal' | 'vertical';
type Direction (line 9) | type Direction = 'up' | 'down' | 'left' | 'right';
type DimensionValue (line 11) | type DimensionValue = undefined | number | string;
type CommonStyledProps (line 13) | type CommonStyledProps = {
type HTMLDataAttributes (line 22) | type HTMLDataAttributes = Record<`data-${string}`, any>;
type CommonThemeProps (line 24) | type CommonThemeProps = {
FILE: test/utils.tsx
class Touch (line 12) | class Touch {
method constructor (line 23) | constructor({
method identifier (line 43) | get identifier() {
method pageX (line 47) | get pageX() {
method pageY (line 51) | get pageY() {
method clientX (line 55) | get clientX() {
method clientY (line 59) | get clientY() {
FILE: types/themes.d.ts
type DefaultTheme (line 7) | interface DefaultTheme extends Theme {}
Condensed preview — 246 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (510K chars).
[
{
"path": ".babelrc",
"chars": 1323,
"preview": "{\n \"sourceType\": \"unambiguous\",\n \"presets\": [\n [\n \"@babel/preset-env\",\n {\n \"shippedProposals\": tru"
},
{
"path": ".codesandbox/ci.json",
"chars": 102,
"preview": "{\n \"buildCommand\": \"build:prod\",\n \"node\": \"16\",\n \"sandboxes\": [\n \"react95-template-xkfj0\"\n ]\n}\n"
},
{
"path": ".editorconfig",
"chars": 145,
"preview": "root = true\n\n[*]\nindent_style = space\nindent_size = 2\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = yes\nin"
},
{
"path": ".eslintrc.js",
"chars": 2057,
"preview": "module.exports = {\n extends: [\n 'plugin:@typescript-eslint/recommended',\n 'airbnb',\n 'plugin:prettier/recommen"
},
{
"path": ".firebaserc",
"chars": 59,
"preview": "{\n \"projects\": {\n \"default\": \"react95-storybook\"\n }\n}\n"
},
{
"path": ".github/FUNDING.yml",
"chars": 320,
"preview": "# These are supported funding model platforms\n\ngithub: arturbien\npatreon: arturbien\nopen_collective: # Replace with a si"
},
{
"path": ".github/workflows/ci.yml",
"chars": 3099,
"preview": "name: CI\n\non:\n pull_request:\n\njobs:\n lint:\n runs-on: ubuntu-latest\n steps:\n - name: Git Checkout\n us"
},
{
"path": ".github/workflows/release.yml",
"chars": 1830,
"preview": "name: Release\n\non:\n push:\n branches:\n - master\n - next\n - beta\n - alpha\n - '*.x' # maintena"
},
{
"path": ".gitignore",
"chars": 406,
"preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
},
{
"path": ".prettierrc",
"chars": 380,
"preview": "{\n \"arrowParens\": \"avoid\",\n \"bracketSpacing\": true,\n \"htmlWhitespaceSensitivity\": \"css\",\n \"insertPragma\": false,\n \""
},
{
"path": ".storybook/decorators/withGlobalStyle.tsx",
"chars": 982,
"preview": "import { DecoratorFn } from '@storybook/react';\nimport React from 'react';\nimport { createGlobalStyle } from 'styled-com"
},
{
"path": ".storybook/main.ts",
"chars": 1209,
"preview": "import type { StorybookConfig } from '@storybook/react/types';\nimport type { PropItem } from 'react-docgen-typescript';\n"
},
{
"path": ".storybook/manager.css",
"chars": 283,
"preview": "/* Remove from the sidebar menu stories that contains \"unstable\" */\na[data-item-id$='-unstable'].sidebar-item,\na[data-it"
},
{
"path": ".storybook/manager.ts",
"chars": 130,
"preview": "import './manager.css';\n\nimport { addons } from '@storybook/addons';\nimport theme from './theme';\n\naddons.setConfig({\n "
},
{
"path": ".storybook/preview.ts",
"chars": 658,
"preview": "import { DecoratorFn, Parameters } from '@storybook/react';\nimport { withGlobalStyle } from './decorators/withGlobalStyl"
},
{
"path": ".storybook/theme-picker/ThemeButton.tsx",
"chars": 575,
"preview": "import React, { useCallback } from 'react';\nimport { ThemeProvider } from 'styled-components';\nimport { Button } from '."
},
{
"path": ".storybook/theme-picker/ThemeList.tsx",
"chars": 1522,
"preview": "import { useAddonState } from '@storybook/api';\nimport React, { useCallback } from 'react';\nimport styled from 'styled-c"
},
{
"path": ".storybook/theme-picker/ThemeProvider.tsx",
"chars": 516,
"preview": "import { useAddonState } from '@storybook/client-api';\nimport { DecoratorFn } from '@storybook/react';\nimport React from"
},
{
"path": ".storybook/theme-picker/constants.ts",
"chars": 45,
"preview": "export const THEMES_ID = 'storybook/themes';\n"
},
{
"path": ".storybook/theme-picker/register.ts",
"chars": 446,
"preview": "import addons, { makeDecorator, types } from '@storybook/addons';\nimport { THEMES_ID } from './constants';\nimport { Them"
},
{
"path": ".storybook/theme.js",
"chars": 741,
"preview": "import { create } from '@storybook/theming';\n\nimport brandImage from './logo.png';\n\nexport default create({\n base: 'lig"
},
{
"path": "LICENSE",
"chars": 1067,
"preview": "MIT License\n\nCopyright (c) 2019 Artur Bień\n\nPermission is hereby granted, free of charge, to any person obtaining a copy"
},
{
"path": "README.md",
"chars": 3680,
"preview": "<h1 align=\"center\">React95</h1>\n\n<p align=\"center\">\n <a href=\"https://www.npmjs.com/package/react95\"><img src=\"https://"
},
{
"path": "docs/Contributing.stories.mdx",
"chars": 741,
"preview": "import { Meta } from '@storybook/addon-docs';\n\n<Meta title='Docs/Contributing' />\n\n# Contributing\n\nAny help from UI/UX d"
},
{
"path": "docs/Getting-Started.stories.mdx",
"chars": 1924,
"preview": "import { Meta } from '@storybook/addon-docs';\n\n<Meta title='Docs/Getting Started' />\n\n# Installation\n\nReact95 is availab"
},
{
"path": "docs/Submit-your-Project.stories.mdx",
"chars": 330,
"preview": "import { Meta } from '@storybook/addon-docs';\n\n<Meta title='Docs/Submit your Project' />\n\n# Submit your Project\n\nApps bu"
},
{
"path": "docs/Welcome.stories.mdx",
"chars": 1754,
"preview": "import { Meta } from '@storybook/addon-docs';\n\n<Meta title='Docs/Welcome to React95' />\n\n# Welcome to React95\n\n<a href='"
},
{
"path": "firebase.json",
"chars": 239,
"preview": "{\n \"hosting\": {\n \"public\": \"storybook\",\n \"ignore\": [\n \"firebase.json\",\n \"**/.*\",\n \"**/node_modules"
},
{
"path": "jest.config.js",
"chars": 280,
"preview": "module.exports = {\n globals: {\n 'ts-jest': {\n diagnostics: false,\n isolatedModules: true\n }\n },\n cove"
},
{
"path": "package.json",
"chars": 4919,
"preview": "{\n \"name\": \"react95\",\n \"version\": \"0.0.0-development\",\n \"description\": \"Refreshed Windows95 UI components for modern "
},
{
"path": "rollup.config.js",
"chars": 1710,
"preview": "import typescript from '@rollup/plugin-typescript';\nimport copy from 'rollup-plugin-copy';\nimport esbuild from 'rollup-p"
},
{
"path": "src/Anchor/Anchor.spec.tsx",
"chars": 1247,
"preview": "import React from 'react';\n\nimport { render } from '@testing-library/react';\n\nimport { Anchor } from './Anchor';\n\nconst "
},
{
"path": "src/Anchor/Anchor.stories.tsx",
"chars": 647,
"preview": "import { ComponentMeta } from '@storybook/react';\nimport React from 'react';\nimport { Anchor } from 'react95';\nimport st"
},
{
"path": "src/Anchor/Anchor.tsx",
"chars": 879,
"preview": "import React, { forwardRef } from 'react';\nimport styled from 'styled-components';\n\nimport { CommonStyledProps } from '."
},
{
"path": "src/AppBar/AppBar.spec.tsx",
"chars": 1845,
"preview": "import { render } from '@testing-library/react';\nimport React from 'react';\n\nimport { AppBar } from './AppBar';\n\nconst d"
},
{
"path": "src/AppBar/AppBar.stories.tsx",
"chars": 2110,
"preview": "import { ComponentMeta } from '@storybook/react';\nimport React, { useState } from 'react';\nimport {\n AppBar,\n Button,\n"
},
{
"path": "src/AppBar/AppBar.tsx",
"chars": 1121,
"preview": "import React, { forwardRef } from 'react';\nimport styled, { CSSProperties } from 'styled-components';\n\nimport { createBo"
},
{
"path": "src/Avatar/Avatar.spec.tsx",
"chars": 2614,
"preview": "import { render } from '@testing-library/react';\nimport React from 'react';\n\nimport { renderWithTheme, theme } from '../"
},
{
"path": "src/Avatar/Avatar.stories.tsx",
"chars": 933,
"preview": "import { ComponentMeta } from '@storybook/react';\nimport React from 'react';\nimport { Avatar } from 'react95';\nimport st"
},
{
"path": "src/Avatar/Avatar.tsx",
"chars": 1859,
"preview": "import React, { forwardRef } from 'react';\nimport styled from 'styled-components';\nimport { getSize } from '../common/ut"
},
{
"path": "src/Button/Button.spec.tsx",
"chars": 3899,
"preview": "import { fireEvent, render } from '@testing-library/react';\nimport React from 'react';\n\nimport { renderWithTheme, theme "
},
{
"path": "src/Button/Button.stories.tsx",
"chars": 5063,
"preview": "import { ComponentMeta } from '@storybook/react';\nimport React, { useState } from 'react';\nimport {\n Button,\n MenuList"
},
{
"path": "src/Button/Button.tsx",
"chars": 5717,
"preview": "import React, { forwardRef } from 'react';\nimport styled, { css } from 'styled-components';\nimport {\n createBorderStyle"
},
{
"path": "src/Checkbox/Checkbox.spec.tsx",
"chars": 4633,
"preview": "import React from 'react';\n\nimport { renderWithTheme } from '../../test/utils';\nimport { Checkbox } from './Checkbox';\n\n"
},
{
"path": "src/Checkbox/Checkbox.stories.tsx",
"chars": 5792,
"preview": "import React, { useState } from 'react';\nimport styled from 'styled-components';\n\nimport { ComponentMeta } from '@storyb"
},
{
"path": "src/Checkbox/Checkbox.tsx",
"chars": 4860,
"preview": "import React, { forwardRef, useCallback } from 'react';\nimport styled, { css } from 'styled-components';\n\nimport { creat"
},
{
"path": "src/ColorInput/ColorInput.spec.tsx",
"chars": 2108,
"preview": "import { fireEvent } from '@testing-library/react';\nimport React from 'react';\nimport { renderWithTheme } from '../../te"
},
{
"path": "src/ColorInput/ColorInput.stories.tsx",
"chars": 1400,
"preview": "import { ComponentMeta } from '@storybook/react';\nimport React from 'react';\nimport styled from 'styled-components';\n\nim"
},
{
"path": "src/ColorInput/ColorInput.tsx",
"chars": 4190,
"preview": "import React, { forwardRef } from 'react';\nimport styled, { css } from 'styled-components';\nimport { StyledButton } from"
},
{
"path": "src/Counter/Counter.spec.tsx",
"chars": 1559,
"preview": "import React from 'react';\n\nimport { renderWithTheme } from '../../test/utils';\n\nimport { Counter } from './Counter';\n\nd"
},
{
"path": "src/Counter/Counter.stories.tsx",
"chars": 1060,
"preview": "import { ComponentMeta } from '@storybook/react';\nimport React, { useState } from 'react';\nimport { Button, Counter, Fra"
},
{
"path": "src/Counter/Counter.tsx",
"chars": 1111,
"preview": "import React, { forwardRef, useMemo } from 'react';\nimport styled from 'styled-components';\n\nimport { createBorderStyles"
},
{
"path": "src/Counter/Digit.tsx",
"chars": 4833,
"preview": "import React from 'react';\nimport styled, { css } from 'styled-components';\n\nimport { createHatchedBackground } from '.."
},
{
"path": "src/DatePicker/DatePicker.stories.tsx",
"chars": 657,
"preview": "/* eslint-disable camelcase, react/jsx-pascal-case */\nimport { ComponentMeta } from '@storybook/react';\nimport React fro"
},
{
"path": "src/DatePicker/DatePicker.tsx",
"chars": 5998,
"preview": "import React, { forwardRef, useCallback, useMemo, useState } from 'react';\nimport styled from 'styled-components';\n\nimpo"
},
{
"path": "src/Frame/Frame.spec.tsx",
"chars": 1101,
"preview": "import { render } from '@testing-library/react';\nimport React from 'react';\n\nimport { Frame } from './Frame';\n\ndescribe("
},
{
"path": "src/Frame/Frame.stories.tsx",
"chars": 1835,
"preview": "import { ComponentMeta } from '@storybook/react';\nimport React from 'react';\nimport { Frame } from 'react95';\nimport sty"
},
{
"path": "src/Frame/Frame.tsx",
"chars": 1683,
"preview": "import React, { forwardRef } from 'react';\nimport styled, { css } from 'styled-components';\nimport { createBorderStyles,"
},
{
"path": "src/GroupBox/GroupBox.spec.tsx",
"chars": 1805,
"preview": "import React from 'react';\n\nimport { renderWithTheme, theme } from '../../test/utils';\n\nimport { GroupBox } from './Grou"
},
{
"path": "src/GroupBox/GroupBox.stories.tsx",
"chars": 2283,
"preview": "import { ComponentMeta } from '@storybook/react';\nimport React, { useState } from 'react';\nimport { Checkbox, GroupBox, "
},
{
"path": "src/GroupBox/GroupBox.tsx",
"chars": 1864,
"preview": "import React, { forwardRef } from 'react';\nimport styled, { css } from 'styled-components';\nimport { createDisabledTextS"
},
{
"path": "src/Handle/Handle.spec.tsx",
"chars": 1352,
"preview": "import React from 'react';\n\nimport { renderWithTheme } from '../../test/utils';\n\nimport { Handle } from './Handle';\n\ndes"
},
{
"path": "src/Handle/Handle.stories.tsx",
"chars": 762,
"preview": "import { ComponentMeta } from '@storybook/react';\nimport React from 'react';\nimport { AppBar, Button, Handle, Toolbar } "
},
{
"path": "src/Handle/Handle.tsx",
"chars": 855,
"preview": "import React from 'react';\nimport styled from 'styled-components';\nimport { CommonStyledProps } from '../types';\nimport "
},
{
"path": "src/Hourglass/Hourglass.spec.tsx",
"chars": 1027,
"preview": "import { render } from '@testing-library/react';\nimport React from 'react';\n\nimport { Hourglass } from './Hourglass';\n\nd"
},
{
"path": "src/Hourglass/Hourglass.stories.tsx",
"chars": 550,
"preview": "import { ComponentMeta } from '@storybook/react';\nimport React from 'react';\nimport { Hourglass } from 'react95';\nimport"
},
{
"path": "src/Hourglass/Hourglass.tsx",
"chars": 1000,
"preview": "import React, { forwardRef } from 'react';\nimport styled from 'styled-components';\nimport { getSize } from '../common/ut"
},
{
"path": "src/Hourglass/base64hourglass.tsx",
"chars": 4711,
"preview": "const base64hourglass =\n \"url('data:image/gif;base64,R0lGODlhPAA8APQAADc3N6+vr4+Pj05OTvn5+V1dXZ+fn29vby8vLw8PD/X19d/f37"
},
{
"path": "src/MenuList/MenuList.spec.tsx",
"chars": 1377,
"preview": "import React from 'react';\n\nimport { renderWithTheme } from '../../test/utils';\n\nimport { MenuList } from './MenuList';\n"
},
{
"path": "src/MenuList/MenuList.stories.tsx",
"chars": 2306,
"preview": "import { ComponentMeta } from '@storybook/react';\nimport React from 'react';\nimport { Handle, MenuList, MenuListItem, Se"
},
{
"path": "src/MenuList/MenuList.tsx",
"chars": 848,
"preview": "import React from 'react';\n\nimport styled from 'styled-components';\nimport { createBorderStyles, createBoxStyles } from "
},
{
"path": "src/MenuList/MenuListItem.spec.tsx",
"chars": 3513,
"preview": "import React from 'react';\n\nimport { renderWithTheme, theme } from '../../test/utils';\nimport { blockSizes } from '../co"
},
{
"path": "src/MenuList/MenuListItem.tsx",
"chars": 2253,
"preview": "import React, { forwardRef } from 'react';\n\nimport styled from 'styled-components';\nimport { createDisabledTextStyles } "
},
{
"path": "src/Monitor/Monitor.spec.tsx",
"chars": 1422,
"preview": "import React from 'react';\n\nimport { renderWithTheme } from '../../test/utils';\n\nimport { Monitor } from './Monitor';\n\nd"
},
{
"path": "src/Monitor/Monitor.stories.tsx",
"chars": 549,
"preview": "import { ComponentMeta } from '@storybook/react';\nimport React from 'react';\nimport { Monitor } from 'react95';\nimport s"
},
{
"path": "src/Monitor/Monitor.tsx",
"chars": 3313,
"preview": "import React, { forwardRef } from 'react';\nimport styled from 'styled-components';\n\nimport { StyledScrollView } from '.."
},
{
"path": "src/NumberInput/NumberInput.spec.tsx",
"chars": 5965,
"preview": "import { fireEvent } from '@testing-library/react';\nimport React from 'react';\n\nimport { renderWithTheme } from '../../t"
},
{
"path": "src/NumberInput/NumberInput.stories.tsx",
"chars": 1533,
"preview": "import { ComponentMeta } from '@storybook/react';\nimport React from 'react';\nimport { ScrollView, NumberInput } from 're"
},
{
"path": "src/NumberInput/NumberInput.tsx",
"chars": 5036,
"preview": "import React, { forwardRef, useCallback } from 'react';\nimport styled, { css } from 'styled-components';\n\nimport { Butto"
},
{
"path": "src/ProgressBar/ProgressBar.spec.tsx",
"chars": 2843,
"preview": "import React from 'react';\n\nimport { renderWithTheme } from '../../test/utils';\nimport { ProgressBar } from './ProgressB"
},
{
"path": "src/ProgressBar/ProgressBar.stories.tsx",
"chars": 2029,
"preview": "import { ComponentMeta } from '@storybook/react';\nimport React, { useEffect, useState } from 'react';\nimport { ProgressB"
},
{
"path": "src/ProgressBar/ProgressBar.tsx",
"chars": 4078,
"preview": "import React, {\n forwardRef,\n useCallback,\n useEffect,\n useRef,\n useState\n} from 'react';\nimport styled, { css } fr"
},
{
"path": "src/Radio/Radio.spec.tsx",
"chars": 2378,
"preview": "import React from 'react';\n\nimport { renderWithTheme } from '../../test/utils';\nimport { Radio } from './Radio';\n\ndescri"
},
{
"path": "src/Radio/Radio.stories.tsx",
"chars": 3411,
"preview": "import { ComponentMeta } from '@storybook/react';\nimport React, { useState } from 'react';\nimport { GroupBox, Radio, Scr"
},
{
"path": "src/Radio/Radio.tsx",
"chars": 3445,
"preview": "import React, { forwardRef } from 'react';\nimport styled, { css, CSSProperties } from 'styled-components';\n\nimport { cre"
},
{
"path": "src/ScrollView/ScrollView.spec.tsx",
"chars": 1214,
"preview": "import { render } from '@testing-library/react';\nimport React from 'react';\n\nimport { ScrollView } from './ScrollView';\n"
},
{
"path": "src/ScrollView/ScrollView.stories.tsx",
"chars": 1347,
"preview": "import { ComponentMeta } from '@storybook/react';\nimport React from 'react';\nimport { ScrollView, Window, WindowContent "
},
{
"path": "src/ScrollView/ScrollView.tsx",
"chars": 1827,
"preview": "import React, { forwardRef } from 'react';\nimport styled from 'styled-components';\nimport { insetShadow, createScrollbar"
},
{
"path": "src/Select/Select.spec.tsx",
"chars": 24139,
"preview": "import { fireEvent, screen, waitFor } from '@testing-library/react';\nimport React from 'react';\nimport { renderWithTheme"
},
{
"path": "src/Select/Select.stories.data.ts",
"chars": 2096,
"preview": "export const PokemonOptions = [\n 'Bulbasaur',\n 'Ivysaur',\n 'Venusaur',\n 'Charmander',\n 'Charmeleon',\n 'Charizard',"
},
{
"path": "src/Select/Select.stories.tsx",
"chars": 3936,
"preview": "/* eslint-disable no-console */\n\nimport { ComponentMeta } from '@storybook/react';\nimport React from 'react';\nimport {\n "
},
{
"path": "src/Select/Select.styles.tsx",
"chars": 5340,
"preview": "import styled, { css } from 'styled-components';\n\nimport { StyledButton as Button } from '../Button/Button';\nimport {\n "
},
{
"path": "src/Select/Select.tsx",
"chars": 6554,
"preview": "import React, {\n forwardRef,\n useCallback,\n useImperativeHandle,\n useMemo,\n useRef\n} from 'react';\n\nimport { useId "
},
{
"path": "src/Select/Select.types.ts",
"chars": 1964,
"preview": "import React from 'react';\nimport { HTMLDataAttributes } from '../types';\n\ntype SelectChangeEventTargetValue<T> = { valu"
},
{
"path": "src/Select/SelectNative.spec.tsx",
"chars": 2368,
"preview": "// Bsased on https://github.com/mui-org/material-ui\n\nimport { fireEvent, screen } from '@testing-library/react';\nimport "
},
{
"path": "src/Select/SelectNative.tsx",
"chars": 2180,
"preview": "import React, { forwardRef, useCallback } from 'react';\n\nimport { noOp } from '../common/utils';\n\nimport { StyledInner, "
},
{
"path": "src/Select/useSelectCommon.tsx",
"chars": 1898,
"preview": "import React, { useMemo } from 'react';\nimport useControlledOrUncontrolled from '../common/hooks/useControlledOrUncontro"
},
{
"path": "src/Select/useSelectState.ts",
"chars": 16130,
"preview": "import React, {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState\n} from 'react';\n\nimport { KEYBOARD_KEY_CODES"
},
{
"path": "src/Separator/Separator.spec.tsx",
"chars": 2318,
"preview": "import React from 'react';\n\nimport { renderWithTheme } from '../../test/utils';\n\nimport { Separator } from './Separator'"
},
{
"path": "src/Separator/Separator.stories.tsx",
"chars": 1072,
"preview": "import { ComponentMeta } from '@storybook/react';\nimport React from 'react';\nimport styled from 'styled-components';\n\nim"
},
{
"path": "src/Separator/Separator.tsx",
"chars": 735,
"preview": "import styled from 'styled-components';\nimport { getSize } from '../common/utils';\nimport { Orientation } from '../types"
},
{
"path": "src/Slider/Slider.spec.tsx",
"chars": 12097,
"preview": "// Pretty much straight out copied from https://github.com/mui-org/material-ui 😂\nimport { fireEvent } from '@testing-lib"
},
{
"path": "src/Slider/Slider.stories.tsx",
"chars": 3460,
"preview": "import { ComponentMeta } from '@storybook/react';\nimport React from 'react';\nimport { ScrollView, Slider, SliderOnChange"
},
{
"path": "src/Slider/Slider.tsx",
"chars": 17137,
"preview": "// helper functions and event handling basically copied from Material UI (https://github.com/mui-org/material-ui) Slider"
},
{
"path": "src/Table/Table.spec.tsx",
"chars": 695,
"preview": "import React from 'react';\n\nimport { renderWithTheme } from '../../test/utils';\n\nimport { Table } from './Table';\n\ndescr"
},
{
"path": "src/Table/Table.stories.tsx",
"chars": 2222,
"preview": "import { ComponentMeta } from '@storybook/react';\nimport React from 'react';\nimport {\n Table,\n TableBody,\n TableDataC"
},
{
"path": "src/Table/Table.tsx",
"chars": 1002,
"preview": "import React, { forwardRef } from 'react';\nimport styled from 'styled-components';\nimport { StyledScrollView } from '../"
},
{
"path": "src/Table/TableBody.spec.tsx",
"chars": 785,
"preview": "import React from 'react';\n\nimport { renderWithTheme } from '../../test/utils';\n\nimport { TableBody } from './TableBody'"
},
{
"path": "src/Table/TableBody.tsx",
"chars": 791,
"preview": "import React, { forwardRef } from 'react';\nimport styled from 'styled-components';\nimport { insetShadow } from '../commo"
},
{
"path": "src/Table/TableDataCell.spec.tsx",
"chars": 780,
"preview": "import React from 'react';\n\nimport { renderWithTheme } from '../../test/utils';\n\nimport { TableDataCell } from './TableD"
},
{
"path": "src/Table/TableDataCell.tsx",
"chars": 646,
"preview": "import React, { forwardRef } from 'react';\nimport styled from 'styled-components';\nimport { CommonStyledProps } from '.."
},
{
"path": "src/Table/TableHead.spec.tsx",
"chars": 785,
"preview": "import React from 'react';\n\nimport { renderWithTheme } from '../../test/utils';\n\nimport { TableHead } from './TableHead'"
},
{
"path": "src/Table/TableHead.tsx",
"chars": 656,
"preview": "import React, { forwardRef } from 'react';\nimport styled from 'styled-components';\nimport { CommonStyledProps } from '.."
},
{
"path": "src/Table/TableHeadCell.spec.tsx",
"chars": 1829,
"preview": "import React from 'react';\n\nimport { renderWithTheme } from '../../test/utils';\n\nimport { TableHeadCell } from './TableH"
},
{
"path": "src/Table/TableHeadCell.tsx",
"chars": 2273,
"preview": "import React, { forwardRef } from 'react';\nimport styled, { css } from 'styled-components';\nimport { createBorderStyles,"
},
{
"path": "src/Table/TableRow.spec.tsx",
"chars": 768,
"preview": "import React from 'react';\nimport { renderWithTheme } from '../../test/utils';\n\nimport { TableRow } from './TableRow';\n\n"
},
{
"path": "src/Table/TableRow.tsx",
"chars": 905,
"preview": "import React, { forwardRef } from 'react';\nimport styled from 'styled-components';\nimport { blockSizes } from '../common"
},
{
"path": "src/Tabs/Tab.spec.tsx",
"chars": 950,
"preview": "import React from 'react';\n\nimport { renderWithTheme } from '../../test/utils';\nimport { Tab } from './Tab';\n\ndescribe('"
},
{
"path": "src/Tabs/Tab.tsx",
"chars": 2274,
"preview": "import React, { forwardRef } from 'react';\nimport styled from 'styled-components';\n\nimport { createBorderStyles, createB"
},
{
"path": "src/Tabs/TabBody.spec.tsx",
"chars": 410,
"preview": "import React from 'react';\n\nimport { renderWithTheme } from '../../test/utils';\nimport { TabBody } from './TabBody';\n\nde"
},
{
"path": "src/Tabs/TabBody.tsx",
"chars": 776,
"preview": "import React, { forwardRef } from 'react';\n\nimport styled from 'styled-components';\nimport { createBorderStyles, createB"
},
{
"path": "src/Tabs/Tabs.spec.tsx",
"chars": 3329,
"preview": "import { fireEvent } from '@testing-library/react';\nimport React from 'react';\n\nimport { Tab } from '..';\nimport { rende"
},
{
"path": "src/Tabs/Tabs.stories.tsx",
"chars": 3533,
"preview": "import { ComponentMeta } from '@storybook/react';\nimport React, { useState } from 'react';\nimport {\n Anchor,\n Checkbox"
},
{
"path": "src/Tabs/Tabs.tsx",
"chars": 2773,
"preview": "import React, { forwardRef, useMemo } from 'react';\n\nimport styled from 'styled-components';\nimport { noOp } from '../co"
},
{
"path": "src/TextInput/TextInput.spec.tsx",
"chars": 4076,
"preview": "// Pretty much straight out copied from https://github.com/mui-org/material-ui 😂\n\nimport React from 'react';\nimport { fi"
},
{
"path": "src/TextInput/TextInput.stories.tsx",
"chars": 3238,
"preview": "import { ComponentMeta } from '@storybook/react';\nimport React, { useState } from 'react';\nimport { Button, ScrollView, "
},
{
"path": "src/TextInput/TextInput.tsx",
"chars": 3927,
"preview": "import React, { forwardRef, useMemo } from 'react';\nimport styled, { css } from 'styled-components';\nimport {\n createDi"
},
{
"path": "src/Toolbar/Toolbar.spec.tsx",
"chars": 786,
"preview": "import { render } from '@testing-library/react';\nimport React from 'react';\n\nimport { Toolbar } from './Toolbar';\n\ndescr"
},
{
"path": "src/Toolbar/Toolbar.tsx",
"chars": 688,
"preview": "import React, { forwardRef } from 'react';\nimport styled from 'styled-components';\n\ntype ToolbarProps = {\n children?: R"
},
{
"path": "src/Tooltip/Tooltip.spec.tsx",
"chars": 6446,
"preview": "import { fireEvent, render, waitFor } from '@testing-library/react';\nimport React from 'react';\n\nimport { Tooltip, Toolt"
},
{
"path": "src/Tooltip/Tooltip.stories.tsx",
"chars": 628,
"preview": "import { ComponentMeta } from '@storybook/react';\nimport React from 'react';\nimport { Button, Tooltip } from 'react95';\n"
},
{
"path": "src/Tooltip/Tooltip.tsx",
"chars": 5261,
"preview": "import React, { forwardRef, useState } from 'react';\nimport styled from 'styled-components';\n\nimport { shadow } from '.."
},
{
"path": "src/TreeView/TreeView.spec.tsx",
"chars": 4726,
"preview": "import React from 'react';\n\nimport { renderWithTheme } from '../../test/utils';\nimport { TreeView } from './TreeView';\n\n"
},
{
"path": "src/TreeView/TreeView.stories.tsx",
"chars": 4171,
"preview": "import { ComponentMeta } from '@storybook/react';\nimport React, { useCallback, useState } from 'react';\nimport { GroupBo"
},
{
"path": "src/TreeView/TreeView.tsx",
"chars": 9660,
"preview": "import React, { forwardRef, useCallback } from 'react';\nimport styled, { css } from 'styled-components';\n\nimport useCont"
},
{
"path": "src/Window/Window.spec.tsx",
"chars": 1320,
"preview": "import React, { createRef } from 'react';\n\nimport { renderWithTheme } from '../../test/utils';\n\nimport { Window } from '"
},
{
"path": "src/Window/Window.stories.tsx",
"chars": 2739,
"preview": "import { ComponentMeta } from '@storybook/react';\nimport React from 'react';\nimport {\n Button,\n Frame,\n Toolbar,\n Wi"
},
{
"path": "src/Window/Window.tsx",
"chars": 1823,
"preview": "import React, { forwardRef } from 'react';\nimport styled, { css } from 'styled-components';\nimport { createBorderStyles,"
},
{
"path": "src/Window/WindowContent.spec.tsx",
"chars": 638,
"preview": "import React from 'react';\n\nimport { renderWithTheme } from '../../test/utils';\n\nimport { WindowContent } from './Window"
},
{
"path": "src/Window/WindowContent.tsx",
"chars": 667,
"preview": "import React, { forwardRef } from 'react';\nimport styled from 'styled-components';\nimport { CommonStyledProps } from '.."
},
{
"path": "src/Window/WindowHeader.spec.tsx",
"chars": 1822,
"preview": "import React from 'react';\n\nimport { renderWithTheme, theme } from '../../test/utils';\n\nimport { WindowHeader } from './"
},
{
"path": "src/Window/WindowHeader.tsx",
"chars": 1467,
"preview": "import React, { forwardRef } from 'react';\nimport styled, { css } from 'styled-components';\nimport { StyledButton } from"
},
{
"path": "src/assets/fonts/src/ms-sans-serif/license.txt",
"chars": 215,
"preview": "The FontStruction “MS Sans Serif”\n(https://fontstruct.com/fontstructions/show/1384746) by “lou” is licensed\nunder a Crea"
},
{
"path": "src/assets/fonts/src/ms-sans-serif/readme.txt",
"chars": 1105,
"preview": "The font file in this archive was created using Fontstruct the free, online\nfont-building tool.\nThis font was created by"
},
{
"path": "src/assets/fonts/src/ms-sans-serif-bold/license.txt",
"chars": 220,
"preview": "The FontStruction “MS Sans Serif Bold”\n(https://fontstruct.com/fontstructions/show/1384862) by “lou” is licensed\nunder a"
},
{
"path": "src/assets/fonts/src/ms-sans-serif-bold/readme.txt",
"chars": 1105,
"preview": "The font file in this archive was created using Fontstruct the free, online\nfont-building tool.\nThis font was created by"
},
{
"path": "src/common/SwitchBase.ts",
"chars": 1240,
"preview": "import styled, { css } from 'styled-components';\n\nimport { createDisabledTextStyles, focusOutline } from '.';\nimport { S"
},
{
"path": "src/common/constants.ts",
"chars": 241,
"preview": "export const KEYBOARD_KEY_CODES = {\n ARROW_DOWN: 'ArrowDown',\n ARROW_LEFT: 'ArrowLeft',\n ARROW_RIGHT: 'ArrowRight',\n "
},
{
"path": "src/common/hooks/useControlledOrUncontrolled.ts",
"chars": 1713,
"preview": "import React, { useState, useCallback } from 'react';\n\nexport default function useControlledOrUncontrolled<T>({\n defaul"
},
{
"path": "src/common/hooks/useEventCallback.ts",
"chars": 653,
"preview": "import * as React from 'react';\n\nconst useEnhancedEffect =\n typeof window !== 'undefined' ? React.useLayoutEffect : Rea"
},
{
"path": "src/common/hooks/useForkRef.spec.tsx",
"chars": 4075,
"preview": "import { render } from '@testing-library/react';\nimport React, { useCallback, useEffect, useRef, useState } from 'react'"
},
{
"path": "src/common/hooks/useForkRef.ts",
"chars": 995,
"preview": "// Straight out copied from https://github.com/mui-org/material-ui 😂\n\nimport { useMemo } from 'react';\n\nfunction setRef<"
},
{
"path": "src/common/hooks/useId.spec.ts",
"chars": 430,
"preview": "import { renderHook } from '@testing-library/react-hooks';\nimport { useId } from './useId';\n\ndescribe(useId, () => {\n i"
},
{
"path": "src/common/hooks/useId.ts",
"chars": 359,
"preview": "import { useMemo } from 'react';\n\nfunction makeId() {\n const chars =\n '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghi"
},
{
"path": "src/common/hooks/useIsFocusVisible.ts",
"chars": 4800,
"preview": "// Straight out copied from https://github.com/mui-org/material-ui 😂\n// based on https://github.com/WICG/focus-visible/b"
},
{
"path": "src/common/index.ts",
"chars": 8243,
"preview": "import { css } from 'styled-components';\nimport { Color, CommonThemeProps, Theme } from '../types';\n\nexport const shadow"
},
{
"path": "src/common/styleReset.ts",
"chars": 1564,
"preview": "export default `\n html,\nbody,\ndiv,\nspan,\napplet,\nobject,\niframe,\nh1,\nh2,\nh3,\nh4,\nh5,\nh6,\np,\nblockquote,\npre,\na,\nabbr,\na"
},
{
"path": "src/common/system.ts",
"chars": 165,
"preview": "// TODO - implement styled-system\n\nimport { Sizes } from '../types';\n\nexport const blockSizes: Record<Sizes, string> = {"
},
{
"path": "src/common/themes/aiee.ts",
"chars": 1360,
"preview": "/* \"AIEE\" by tPenguinLTG\n * https://www.deviantart.com/tpenguinltg/art/Aiee-668092636\n */\n\nimport { Theme } from './type"
},
{
"path": "src/common/themes/ash.ts",
"chars": 1361,
"preview": "/* \"Ash\" by tPenguinLTG\n * https://www.deviantart.com/tpenguinltg/art/Ash-575566643\n */\nimport { Theme } from './types';"
},
{
"path": "src/common/themes/azureOrange.ts",
"chars": 947,
"preview": "import { Theme } from './types';\n\nexport default {\n name: 'azureOrange',\n\n anchor: '#1034a6',\n anchorVisited: '#44038"
},
{
"path": "src/common/themes/bee.ts",
"chars": 939,
"preview": "import { Theme } from './types';\n\nexport default {\n name: 'bee',\n\n anchor: '#1034a6',\n anchorVisited: '#440381',\n bo"
},
{
"path": "src/common/themes/blackAndWhite.ts",
"chars": 949,
"preview": "import { Theme } from './types';\n\nexport default {\n name: 'blackAndWhite',\n\n anchor: '#1034a6',\n anchorVisited: '#440"
},
{
"path": "src/common/themes/blue.ts",
"chars": 1410,
"preview": "/* \"Blue\" by tPenguinLTG\n * https://www.deviantart.com/tpenguinltg/art/Blue-525167751\n */\n\nimport { Theme } from './type"
},
{
"path": "src/common/themes/brick.ts",
"chars": 941,
"preview": "import { Theme } from './types';\n\nexport default {\n name: 'brick',\n\n anchor: '#1034a6',\n anchorVisited: '#440381',\n "
},
{
"path": "src/common/themes/candy.ts",
"chars": 941,
"preview": "import { Theme } from './types';\n\nexport default {\n name: 'candy',\n\n anchor: '#1034a6',\n anchorVisited: '#440381',\n "
},
{
"path": "src/common/themes/cherry.ts",
"chars": 1432,
"preview": "/* \"Cherry\" by tPenguinLTG\n * https://www.deviantart.com/tpenguinltg/art/Cherry-747961418\n */\n\nimport { Theme } from './"
},
{
"path": "src/common/themes/coldGray.ts",
"chars": 969,
"preview": "import { Theme } from './types';\n\nexport default {\n name: 'coldGray',\n\n anchor: '#8d88c2',\n anchorVisited: '#440381',"
},
{
"path": "src/common/themes/counterStrike.ts",
"chars": 949,
"preview": "import { Theme } from './types';\n\nexport default {\n name: 'counterStrike',\n\n anchor: '#1034a6',\n anchorVisited: '#440"
},
{
"path": "src/common/themes/darkTeal.ts",
"chars": 1435,
"preview": "/* \"Teal for Shelbi - Dark\" by tPenguinLTG\n * https://www.deviantart.com/tpenguinltg/art/Teal-for-Shelbi-Dark-631177772\n"
},
{
"path": "src/common/themes/denim.ts",
"chars": 1399,
"preview": "/* \"Denim\" by tPenguinLTG\n * https://www.deviantart.com/tpenguinltg/art/Denim-870494744\n */\n\nimport { Theme } from './ty"
},
{
"path": "src/common/themes/eggplant.ts",
"chars": 944,
"preview": "import { Theme } from './types';\n\nexport default {\n name: 'eggplant',\n\n anchor: '#1034a6',\n anchorVisited: '#440381',"
},
{
"path": "src/common/themes/fxDev.ts",
"chars": 1412,
"preview": "/* \"FxDev\" by tPenguinLTG\n * https://www.deviantart.com/tpenguinltg/art/FxDev-701274128\n */\n\nimport { Theme } from './ty"
},
{
"path": "src/common/themes/highContrast.ts",
"chars": 948,
"preview": "import { Theme } from './types';\n\nexport default {\n name: 'highContrast',\n\n anchor: '#1034a6',\n anchorVisited: '#4403"
},
{
"path": "src/common/themes/honey.ts",
"chars": 1398,
"preview": "/* \"Honey\" by tPenguinLTG\n * https://www.deviantart.com/tpenguinltg/art/Honey-632126512\n */\n\nimport { Theme } from './ty"
},
{
"path": "src/common/themes/hotChocolate.ts",
"chars": 1448,
"preview": "/* \"Hot Chocolate\" by tPenguinLTG\n * https://www.deviantart.com/tpenguinltg/art/Hot-Chocolate-654380979\n */\n\nimport { Th"
},
{
"path": "src/common/themes/hotdogStand.ts",
"chars": 1365,
"preview": "/* \"Hotdog Stand\" by tPenguinLTG\n * https://www.deviantart.com/tpenguinltg/art/Hotdog-Stand-525606846\n */\n\nimport { Them"
},
{
"path": "src/common/themes/index.ts",
"chars": 2823,
"preview": "import aiee from './aiee';\nimport ash from './ash';\nimport azureOrange from './azureOrange';\nimport bee from './bee';\nim"
},
{
"path": "src/common/themes/lilac.ts",
"chars": 941,
"preview": "import { Theme } from './types';\n\nexport default {\n name: 'lilac',\n\n anchor: '#1034a6',\n anchorVisited: '#440381',\n "
},
{
"path": "src/common/themes/lilacRoseDark.ts",
"chars": 974,
"preview": "import { Theme } from './types';\n\nexport default {\n name: 'lilacRoseDark',\n\n anchor: '#a65387',\n anchorVisited: '#440"
},
{
"path": "src/common/themes/maple.ts",
"chars": 941,
"preview": "import { Theme } from './types';\n\nexport default {\n name: 'maple',\n\n anchor: '#1034a6',\n anchorVisited: '#440381',\n "
},
{
"path": "src/common/themes/marine.ts",
"chars": 942,
"preview": "import { Theme } from './types';\n\nexport default {\n name: 'marine',\n\n anchor: '#1034a6',\n anchorVisited: '#440381',\n "
},
{
"path": "src/common/themes/matrix.ts",
"chars": 942,
"preview": "import { Theme } from './types';\n\nexport default {\n name: 'matrix',\n\n anchor: '#1034a6',\n anchorVisited: '#440381',\n "
},
{
"path": "src/common/themes/millenium.ts",
"chars": 973,
"preview": "import { Theme } from './types';\n\nexport default {\n name: 'millenium',\n\n anchor: '#1034a6',\n anchorVisited: '#440381'"
},
{
"path": "src/common/themes/modernDark.ts",
"chars": 950,
"preview": "import { Theme } from './types';\n\nexport default {\n name: 'modernDark',\n\n anchor: '#1034a6',\n anchorVisited: '#440381"
},
{
"path": "src/common/themes/molecule.ts",
"chars": 944,
"preview": "import { Theme } from './types';\n\nexport default {\n name: 'molecule',\n\n anchor: '#1034a6',\n anchorVisited: '#440381',"
},
{
"path": "src/common/themes/monochrome.ts",
"chars": 0,
"preview": ""
},
{
"path": "src/common/themes/ninjaTurtles.ts",
"chars": 948,
"preview": "import { Theme } from './types';\n\nexport default {\n name: 'ninjaTurtles',\n\n anchor: '#1034a6',\n anchorVisited: '#4403"
},
{
"path": "src/common/themes/olive.ts",
"chars": 941,
"preview": "import { Theme } from './types';\n\nexport default {\n name: 'olive',\n\n anchor: '#1034a6',\n anchorVisited: '#440381',\n "
},
{
"path": "src/common/themes/original.ts",
"chars": 944,
"preview": "import { Theme } from './types';\n\nexport default {\n name: 'original',\n\n anchor: '#1034a6',\n anchorVisited: '#440381',"
},
{
"path": "src/common/themes/pamelaAnderson.ts",
"chars": 950,
"preview": "import { Theme } from './types';\n\nexport default {\n name: 'pamelaAnderson',\n\n anchor: '#1034a6',\n anchorVisited: '#44"
},
{
"path": "src/common/themes/peggysPastels.ts",
"chars": 1436,
"preview": "/* \"Peggy's Pastels\" by tPenguinLTG\n * https://www.deviantart.com/tpenguinltg/art/Peggy-s-Pastels-505540096\n */\n\nimport "
},
{
"path": "src/common/themes/plum.ts",
"chars": 940,
"preview": "import { Theme } from './types';\n\nexport default {\n name: 'plum',\n\n anchor: '#1034a6',\n anchorVisited: '#440381',\n b"
},
{
"path": "src/common/themes/polarized.ts",
"chars": 1423,
"preview": "/* \"Polarized\" by tPenguinLTG\n * https://www.deviantart.com/tpenguinltg/art/Polarized-557712217\n */\n\nimport { Theme } fr"
},
{
"path": "src/common/themes/powerShell.ts",
"chars": 1432,
"preview": "/* \"PowerShell\" by tPenguinLTG\n * https://www.deviantart.com/tpenguinltg/art/PowerShell-646065752\n */\n\nimport { Theme } "
},
{
"path": "src/common/themes/rainyDay.ts",
"chars": 944,
"preview": "import { Theme } from './types';\n\nexport default {\n name: 'rainyDay',\n\n anchor: '#1034a6',\n anchorVisited: '#440381',"
},
{
"path": "src/common/themes/raspberry.ts",
"chars": 1432,
"preview": "/* \"Raspberry\" by tPenguinLTG\n * https://www.deviantart.com/tpenguinltg/art/Raspberry-539289720\n */\n\nimport { Theme } fr"
},
{
"path": "src/common/themes/redWine.ts",
"chars": 1376,
"preview": "/* \"Red Wine\" by tPenguinLTG\n * https://www.deviantart.com/tpenguinltg/art/Red-Wine-545729607\n */\n\nimport { Theme } from"
},
{
"path": "src/common/themes/rose.ts",
"chars": 940,
"preview": "import { Theme } from './types';\n\nexport default {\n name: 'rose',\n\n anchor: '#1034a6',\n anchorVisited: '#440381',\n b"
},
{
"path": "src/common/themes/seawater.ts",
"chars": 1418,
"preview": "/* \"Seawater\" by tPenguinLTG\n * https://www.deviantart.com/tpenguinltg/art/Seawater-736002425\n */\n\nimport { Theme } from"
}
]
// ... and 46 more files (download for full content)
About this extraction
This page contains the full source code of the react95-io/React95 GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 246 files (466.6 KB), approximately 136.4k tokens, and a symbol index with 199 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.