Move insomnia-components in the insomnia package (#5259)
* Move insomnia-components in the insomnia package * remove extra tooltip components * remove button from portal plugin * remove insomniaComponent injection * move spec editor sidebar * move breadcrumb component * move toggle-switch * move table/notice-table * remove unused switch component * move header to app-header * move card * move card-container to wrapper-home * Remove unused multi-switch and radio-group * move svg icon and auto-run convert-svg command * move list-group * move button components into themed-button * move dropdown * remove auto-conversion of svg from npm scripts * track svg icon components to git * remove react-switch and framer-motion * remove sidebar panel, fix lint * remove animate blocks * fix types, remove value from dropdown item * remove button * ignore svgr config * update dropdown unit tests * allow unreachable * oauth e2e test: update locator to make the Create button specific * fix type errors Co-authored-by: jackkav <jackkav@gmail.com>
@ -14,5 +14,4 @@ screenshots/
|
||||
**/__snapshots__/
|
||||
**/dist/
|
||||
**/.cache/
|
||||
**/svgr/
|
||||
*.md
|
||||
|
@ -1,3 +0,0 @@
|
||||
dist
|
||||
assets/svgr
|
||||
src/assets/svgr
|
@ -1,15 +0,0 @@
|
||||
const { OFF, TYPESCRIPT_CONVERSION } = require('eslint-config-helpers');
|
||||
|
||||
/** @type { import('eslint').Linter.Config } */
|
||||
module.exports = {
|
||||
extends: '../../.eslintrc.js',
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect',
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': OFF(TYPESCRIPT_CONVERSION),
|
||||
'@typescript-eslint/no-unnecessary-type-constraint': OFF('by an unfortunate quirk of how TypeScript differentiates between casts and JSX, this it is necessary for generic JSX to do extend unknown'),
|
||||
},
|
||||
};
|
4
packages/insomnia-components/.gitignore
vendored
@ -1,4 +0,0 @@
|
||||
.cache
|
||||
src/assets/svgr
|
||||
assets/svgr
|
||||
dist
|
@ -1,7 +0,0 @@
|
||||
# Ignore everything by default
|
||||
# NOTE: NPM publish will never ignore package.json, package-lock.json, README, LICENSE, CHANGELOG
|
||||
# https://npm.github.io/publishing-pkgs-docs/publishing/the-npmignore-file.html
|
||||
*
|
||||
|
||||
# Don't ignore dist folder
|
||||
!dist/**
|
@ -1 +0,0 @@
|
||||
16.14.2
|
@ -1,14 +0,0 @@
|
||||
# insomnia-components
|
||||
|
||||
`insomnia-components` is a React component library of UI elements needed to build [Insomnia](https://insomnia.rest).
|
||||
|
||||
## SVGs
|
||||
|
||||
We use [SVGR](https://react-svgr.com) on the source SVGs to minify and convert them to React components. This conversion happens during `npm run bootstrap`.
|
||||
|
||||
The generated icons can automatically be bound to the theme library built into Insomnia, by following a few rules. In the source SVG:
|
||||
|
||||
1. All background colors should be black (#000)
|
||||
1. All foreground colors should be white (#FFF)
|
||||
|
||||
SVGR converts these colors to `fill=''` and `fill='currentColor'` respectively. These icons are exposed via `svg-icon.js`, which sets the css `fill` and `color` of the SVG according to the required theme.
|
@ -1,14 +0,0 @@
|
||||
/** @type { import('@jest/types').Config.InitialOptions } */
|
||||
module.exports = {
|
||||
preset: '../../jest-preset.js',
|
||||
globals: {
|
||||
'ts-jest': {
|
||||
isolatedModules: true,
|
||||
},
|
||||
},
|
||||
fakeTimers: {
|
||||
enableGlobally: true,
|
||||
},
|
||||
setupFilesAfterEnv: ['./src/jest/setup-after-env.ts'],
|
||||
testEnvironment: 'jsdom',
|
||||
};
|
17535
packages/insomnia-components/package-lock.json
generated
@ -1,78 +0,0 @@
|
||||
{
|
||||
"name": "insomnia-components",
|
||||
"version": "3.6.1-beta.0",
|
||||
"author": "Kong <office@konghq.com>",
|
||||
"description": "Insomnia UI component library",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Kong/insomnia.git",
|
||||
"directory": "packages/insomnia-components"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/Kong/insomnia/issues"
|
||||
},
|
||||
"main": "./dist/commonjs/index.js",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"require": "./dist/commonjs/index.js",
|
||||
"import": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"prebuild": "npm run convert-svg",
|
||||
"build:source": "vite build",
|
||||
"build:types": "tsc --build tsconfig.build.json",
|
||||
"build": "concurrently --names source,types \"npm run build:source\" \"npm run build:types\"",
|
||||
"clean": "rimraf dist && npm run clean:svg",
|
||||
"clean:svg": "rimraf src/assets/svgr",
|
||||
"clean:svg:index": "rimraf src/assets/svgr/index.tsx",
|
||||
"convert-svg": "npm run clean:svg && svgr --config-file svgr.config.js --out-dir src/assets/svgr src/assets && npm run clean:svg:index",
|
||||
"jest:watch": "jest --watch",
|
||||
"prepare": "npm run convert-svg && npm run build",
|
||||
"lint": "eslint . --ext .js,.ts,.tsx --cache",
|
||||
"lint:fix": "npm run lint -- --fix",
|
||||
"test": "jest",
|
||||
"watch": "cross-env ESBUILD_WATCH=true esr esbuild.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "^28.1.0",
|
||||
"@svgr/cli": "^5.5.0",
|
||||
"@testing-library/jest-dom": "^5.16.4",
|
||||
"@testing-library/react": "^12.1.5",
|
||||
"@types/ramda": "^0.27.44",
|
||||
"@types/react": "^17.0.39",
|
||||
"@types/react-dom": "^17.0.11",
|
||||
"concurrently": "^7.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"esbuild": "^0.14.27",
|
||||
"esbuild-runner": "^2.2.1",
|
||||
"jest": "^28.1.0",
|
||||
"jest-environment-jsdom": "^28.0.2",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-use": "^17.2.4",
|
||||
"styled-components": "^5.3.3",
|
||||
"type-fest": "^2.12.0",
|
||||
"typescript": "^4.5.5",
|
||||
"vite": "^2.9.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/styled-components": "^5.1.23",
|
||||
"class-autobind-decorator": "^3.0.1",
|
||||
"classnames": "^2.3.1",
|
||||
"framer-motion": "4.1.17",
|
||||
"fuzzysort": "^1.2.1",
|
||||
"ramda": "^0.27.1",
|
||||
"react-switch": "^6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-use": "^17.2.4",
|
||||
"styled-components": "^5.3.3"
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const CardContainer = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding-top: var(--padding-md);
|
||||
`;
|
@ -1,34 +0,0 @@
|
||||
import React, { FC, memo } from 'react';
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
const StyledDivider = styled.div<{ hasName: boolean }>`
|
||||
border-bottom: 1px solid var(--hl-sm);
|
||||
overflow: visible !important;
|
||||
height: 0;
|
||||
margin: var(--padding-md) var(--padding-md) var(--padding-md) var(--padding-md);
|
||||
${({ hasName }) =>
|
||||
!hasName &&
|
||||
css`
|
||||
margin: var(--padding-xs) 0;
|
||||
`}
|
||||
`;
|
||||
|
||||
const StyledLabel = styled.span`
|
||||
position: relative;
|
||||
left: calc(-1 * var(--padding-sm));
|
||||
top: -0.7rem;
|
||||
color: var(--hl);
|
||||
padding-right: 1em;
|
||||
background: var(--color-bg);
|
||||
font-size: var(--font-size-xs);
|
||||
text-transform: uppercase;
|
||||
font-family: var(--font-default);
|
||||
`;
|
||||
|
||||
export const DropdownDivider: FC = memo(({ children }) => (
|
||||
<StyledDivider hasName={!!children}>
|
||||
{children && <StyledLabel>{children}</StyledLabel>}
|
||||
</StyledDivider>
|
||||
));
|
||||
|
||||
DropdownDivider.displayName = 'DropdownDivider';
|
@ -1,157 +0,0 @@
|
||||
import { autoBindMethodsForReact } from 'class-autobind-decorator';
|
||||
import classnames from 'classnames';
|
||||
import React, { PureComponent, ReactNode } from 'react';
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
interface ControlledDropdown<T> {
|
||||
value: T;
|
||||
onClick?: (value: T, event: React.MouseEvent<HTMLButtonElement>) => void | Promise<void>;
|
||||
}
|
||||
|
||||
interface UncontrolledDropdown {
|
||||
onClick?: React.MouseEventHandler<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
const isControlledInput = <T extends unknown>(dropdown: DropdownItemProps<T>): dropdown is ControlledDropdown<T> => (
|
||||
Object.prototype.hasOwnProperty.call(dropdown, 'value')
|
||||
);
|
||||
|
||||
const isUncontrolledInput = <T extends unknown>(dropdown: DropdownItemProps<T>): dropdown is UncontrolledDropdown => (
|
||||
!isControlledInput(dropdown)
|
||||
);
|
||||
|
||||
export type DropdownItemProps<T = string> = {
|
||||
buttonClass?: string;
|
||||
stayOpenAfterClick?: boolean;
|
||||
disabled?: boolean;
|
||||
children?: ReactNode;
|
||||
icon?: ReactNode;
|
||||
right?: ReactNode;
|
||||
className?: string;
|
||||
color?: string;
|
||||
selected?: boolean;
|
||||
} & (ControlledDropdown<T> | UncontrolledDropdown);
|
||||
|
||||
const StyledButton = styled.button<{ selected?: boolean }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 15rem;
|
||||
font-size: var(--font-size-md);
|
||||
text-align: left;
|
||||
padding-right: var(--padding-md);
|
||||
padding-left: var(--padding-sm);
|
||||
height: var(--line-height-xs);
|
||||
width: 100%;
|
||||
color: var(--color-font) !important;
|
||||
background-color: var(--color-bg);
|
||||
white-space: nowrap;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
|
||||
&:hover:not(:disabled),
|
||||
&:active:not(:disabled) {
|
||||
background: var(--hl-xs);
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background: var(--hl-md);
|
||||
}
|
||||
|
||||
${({ selected }) => selected && css`
|
||||
background: var(--hl-xs) !important;
|
||||
font-weight: bold;
|
||||
`};
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledRightNode = styled.span`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--hl-xl);
|
||||
margin-left: auto;
|
||||
padding-left: var(--padding-lg);
|
||||
`;
|
||||
|
||||
const StyledInner = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
`;
|
||||
|
||||
const StyledText = styled.div`
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
& > *:not(:first-child) {
|
||||
margin-left: 0.3em;
|
||||
}
|
||||
input + label {
|
||||
padding-top: 0px !important;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledIconContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: var(--padding-xs);
|
||||
padding-right: var(--padding-md);
|
||||
`;
|
||||
|
||||
@autoBindMethodsForReact
|
||||
export class DropdownItem<T> extends PureComponent<DropdownItemProps<T>> {
|
||||
_handleClick(event: React.MouseEvent<HTMLButtonElement>) {
|
||||
const { stayOpenAfterClick, onClick, disabled } = this.props;
|
||||
|
||||
if (stayOpenAfterClick) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
if (!onClick || disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isControlledInput<T>(this.props)) {
|
||||
this.props.onClick?.(this.props.value, event);
|
||||
} else if (isUncontrolledInput(this.props)) {
|
||||
this.props.onClick?.(event);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
buttonClass,
|
||||
children,
|
||||
className,
|
||||
color,
|
||||
disabled,
|
||||
right,
|
||||
icon,
|
||||
selected,
|
||||
} = this.props;
|
||||
|
||||
const styles = color ? { color } : {};
|
||||
|
||||
const inner = (
|
||||
<StyledInner className={className}>
|
||||
<StyledText style={styles}>{children}</StyledText>
|
||||
</StyledInner>
|
||||
);
|
||||
return (
|
||||
<StyledButton
|
||||
className={classnames(className, buttonClass)}
|
||||
type="button"
|
||||
onClick={this._handleClick}
|
||||
disabled={disabled}
|
||||
selected={selected}
|
||||
>
|
||||
{icon && <StyledIconContainer>{icon}</StyledIconContainer>}
|
||||
{inner}
|
||||
{right && <StyledRightNode>{right}</StyledRightNode>}
|
||||
</StyledButton>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,594 +0,0 @@
|
||||
import { autoBindMethodsForReact } from 'class-autobind-decorator';
|
||||
import classnames from 'classnames';
|
||||
import fuzzySort from 'fuzzysort';
|
||||
import { any, equals } from 'ramda';
|
||||
import React, {
|
||||
CSSProperties,
|
||||
Fragment,
|
||||
PureComponent,
|
||||
ReactChild,
|
||||
ReactFragment,
|
||||
ReactNode,
|
||||
ReactPortal,
|
||||
} from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
import { SvgIcon } from '../svg-icon';
|
||||
import { DropdownDivider } from './dropdown-divider';
|
||||
import { DropdownItem } from './dropdown-item';
|
||||
|
||||
export interface DropdownProps {
|
||||
children: ReactNode;
|
||||
renderButton: ReactNode | ((props: { open: boolean }) => void);
|
||||
right?: boolean;
|
||||
outline?: boolean;
|
||||
wide?: boolean;
|
||||
onOpen?: () => void;
|
||||
onHide?: () => void;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
beside?: boolean;
|
||||
}
|
||||
|
||||
const StyledDropdown = styled.div`
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
text-align: left;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledNoResults = styled.div`
|
||||
text-align: center;
|
||||
padding: var(--padding-md);
|
||||
color: var(--color-warning);
|
||||
`;
|
||||
|
||||
const StyledBackdrop = styled.div`
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
content: ' ';
|
||||
`;
|
||||
|
||||
const StyledFilter = styled.div<{ filtering: boolean }>`
|
||||
border: 1px solid var(--hl-md);
|
||||
margin: var(--padding-xs);
|
||||
width: auto;
|
||||
border-radius: var(--radius-sm);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding-left: var(--padding-sm);
|
||||
color: var(--hl);
|
||||
|
||||
// Can't "display: none" this because we need to be able to type
|
||||
// in it. So, we'll just store it off screen
|
||||
${({ filtering }) =>
|
||||
filtering
|
||||
? css`
|
||||
position: static;
|
||||
left: auto;
|
||||
`
|
||||
: css`
|
||||
position: absolute;
|
||||
left: -9999999px;
|
||||
`}
|
||||
|
||||
input {
|
||||
margin: 0;
|
||||
padding: var(--padding-xs) var(--padding-sm);
|
||||
color: var(--color-font);
|
||||
outline: 0;
|
||||
border: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledMenu = styled.div`
|
||||
z-index: 99999;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border: 1px solid var(--hl-sm);
|
||||
box-shadow: 0 0 1rem 0 rgba(0, 0, 0, 0.1);
|
||||
box-sizing: border-box;
|
||||
background: var(--color-bg);
|
||||
max-height: 100px;
|
||||
|
||||
// Separate it from it's surroundings a bit
|
||||
margin: var(--padding-xxs) 3px;
|
||||
|
||||
padding-top: var(--radius-md);
|
||||
padding-bottom: var(--radius-md);
|
||||
|
||||
border-radius: var(--radius-md);
|
||||
overflow: auto;
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
button:disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
li button:hover:not(:disabled),
|
||||
li.active button:not(:disabled) {
|
||||
background: var(--hl-xs);
|
||||
}
|
||||
|
||||
li.active button:not(:disabled) {
|
||||
background: var(--hl-sm);
|
||||
}
|
||||
`;
|
||||
|
||||
interface State {
|
||||
open: boolean;
|
||||
dropUp: boolean;
|
||||
filter: string;
|
||||
filterVisible: boolean;
|
||||
filterItems: number[] | null;
|
||||
filterActiveIndex: number;
|
||||
forcedPosition: null | {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
uniquenessKey: number;
|
||||
}
|
||||
|
||||
const isComponent = (match: string) => (child: ReactNode) => any(equals(match), [
|
||||
// @ts-expect-error this is required by our API for Dropdown
|
||||
child.type.name,
|
||||
// @ts-expect-error this is required by our API for Dropdown
|
||||
child.type.displayName,
|
||||
]);
|
||||
|
||||
const isDropdownItem = isComponent(DropdownItem.name);
|
||||
const isDropdownDivider = isComponent(DropdownDivider.name);
|
||||
|
||||
@autoBindMethodsForReact
|
||||
export class Dropdown extends PureComponent<DropdownProps, State> {
|
||||
// Save body overflow so we can revert it when needed
|
||||
defaultBodyOverflow = document.body.style.overflow;
|
||||
dropdownsContainer: HTMLElement | null = null;
|
||||
_dropdownList: HTMLDivElement | null = null;
|
||||
_cachedMinWidth: string | null = null;
|
||||
_node: HTMLDivElement | null = null;
|
||||
_filter: HTMLInputElement | null = null;
|
||||
|
||||
state: State = {
|
||||
open: false,
|
||||
dropUp: false,
|
||||
// Filter Stuff
|
||||
filter: '',
|
||||
filterVisible: false,
|
||||
filterItems: null,
|
||||
filterActiveIndex: 0,
|
||||
// Position
|
||||
forcedPosition: null,
|
||||
// Use this to force new menu every time dropdown opens
|
||||
uniquenessKey: 0,
|
||||
};
|
||||
|
||||
constructor(props: DropdownProps) {
|
||||
super(props);
|
||||
this.dropdownsContainer = document.querySelector('#dropdowns-container');
|
||||
|
||||
if (!this.dropdownsContainer) {
|
||||
this.dropdownsContainer = document.createElement('div');
|
||||
this.dropdownsContainer.id = 'dropdowns-container';
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
this.dropdownsContainer.style = [
|
||||
// Position off-screen so we don't see it
|
||||
'position: fixed',
|
||||
'right: -90000px',
|
||||
'left: -90000px', // Make same size as viewport so we can use it to calculate the
|
||||
// position of the dropdown accurately.
|
||||
'width: 100vw',
|
||||
'height: 100vh',
|
||||
].join('; ');
|
||||
document.body.appendChild(this.dropdownsContainer);
|
||||
}
|
||||
}
|
||||
|
||||
_handleCheckFilterSubmit(event: React.KeyboardEvent<HTMLInputElement>) {
|
||||
if (event.key === 'Enter') {
|
||||
// Listen for the Enter key and "click" on the active list item
|
||||
const selector = `li[data-filter-index="${this.state.filterActiveIndex}"] button`;
|
||||
this._dropdownList?.querySelector<HTMLButtonElement>(selector)?.click();
|
||||
}
|
||||
}
|
||||
|
||||
_handleChangeFilter(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
const newFilter = event.target.value;
|
||||
|
||||
// Nothing to do if the filter didn't change
|
||||
if (newFilter === this.state.filter) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter the list items that are filterable (have data-filter-index property)
|
||||
const filterItems = [];
|
||||
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
for (const listItem of (this._dropdownList?.querySelectorAll('li') || [])) {
|
||||
if (!listItem.hasAttribute('data-filter-index')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const hasMatch = !!fuzzySort.single(newFilter, listItem.textContent);
|
||||
|
||||
if (!newFilter || hasMatch) {
|
||||
const filterIndex = listItem.getAttribute('data-filter-index');
|
||||
filterItems.push(parseInt(filterIndex, 10));
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
filter: newFilter,
|
||||
filterItems: newFilter ? filterItems : null,
|
||||
filterActiveIndex: filterItems[0] || -1,
|
||||
filterVisible: this.state.filterVisible ? true : newFilter.length > 0,
|
||||
});
|
||||
}
|
||||
|
||||
_handleDropdownNavigation(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||
const { key, shiftKey } = event;
|
||||
// Handle tab and arrows to move up and down dropdown entries
|
||||
const { filterItems, filterActiveIndex } = this.state;
|
||||
|
||||
if (['Tab', 'ArrowDown', 'ArrowUp'].includes(key)) {
|
||||
event.preventDefault();
|
||||
const items = filterItems || [];
|
||||
|
||||
if (!filterItems) {
|
||||
for (const li of (this._dropdownList?.querySelectorAll('li') || [])) {
|
||||
if (li.hasAttribute('data-filter-index')) {
|
||||
const filterIndex = li.getAttribute('data-filter-index');
|
||||
items.push(parseInt(filterIndex, 10));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const i = items.indexOf(filterActiveIndex);
|
||||
|
||||
if (key === 'ArrowUp' || (key === 'Tab' && shiftKey)) {
|
||||
const nextI = i > 0 ? items[i - 1] : items[items.length - 1];
|
||||
this.setState({
|
||||
filterActiveIndex: nextI,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
filterActiveIndex: items[i + 1] || items[0],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this._filter?.focus();
|
||||
}
|
||||
|
||||
_handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||
if (!this.state.open) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Catch all key presses (like global app hotkeys) if we're open
|
||||
event.stopPropagation();
|
||||
|
||||
this._handleDropdownNavigation(event);
|
||||
|
||||
if (event.key === 'Escape') {
|
||||
this.hide();
|
||||
}
|
||||
}
|
||||
|
||||
_checkSizeAndPosition() {
|
||||
if (!this.state.open || !this._dropdownList) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get dropdown menu
|
||||
const dropdownList = this._dropdownList;
|
||||
|
||||
// Compute the size of all the menus
|
||||
let dropdownBtnRect = this._node?.getBoundingClientRect();
|
||||
|
||||
const bodyRect = this.dropdownsContainer?.getBoundingClientRect();
|
||||
const dropdownListRect = dropdownList.getBoundingClientRect();
|
||||
const { forcedPosition } = this.state;
|
||||
|
||||
if (forcedPosition && bodyRect !== undefined) {
|
||||
dropdownBtnRect = {
|
||||
left: forcedPosition.x,
|
||||
right: bodyRect.width - forcedPosition.x,
|
||||
top: forcedPosition.y,
|
||||
bottom: bodyRect.height - forcedPosition.y,
|
||||
width: 100,
|
||||
height: 10,
|
||||
x: 0,
|
||||
y: 0,
|
||||
toJSON: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
// Should it drop up?
|
||||
const bodyHeight = bodyRect?.height || 0;
|
||||
const dropdownTop = dropdownBtnRect?.top || 0;
|
||||
const dropUp = dropdownTop > bodyHeight - 200;
|
||||
// Reset all the things so we can start fresh
|
||||
this._dropdownList.style.left = 'initial';
|
||||
this._dropdownList.style.right = 'initial';
|
||||
this._dropdownList.style.top = 'initial';
|
||||
this._dropdownList.style.bottom = 'initial';
|
||||
this._dropdownList.style.minWidth = this._cachedMinWidth || 'initial';
|
||||
this._dropdownList.style.maxWidth = 'initial';
|
||||
|
||||
// Cache the width so we can use it later. This is because the dropdown width
|
||||
// could shrink once we start filtering items out.
|
||||
if (!this._cachedMinWidth) {
|
||||
this._cachedMinWidth = dropdownListRect.width + 'px';
|
||||
}
|
||||
|
||||
const screenMargin = 6;
|
||||
const { right, wide } = this.props;
|
||||
|
||||
if (dropdownBtnRect === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!bodyRect) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (right || wide) {
|
||||
const { right: originalRight } = dropdownBtnRect;
|
||||
// Prevent dropdown from squishing against left side of screen
|
||||
const right = Math.max(dropdownListRect.width + screenMargin, originalRight);
|
||||
const { beside } = this.props;
|
||||
const offset = beside ? dropdownBtnRect.width - 40 : 0;
|
||||
this._dropdownList.style.right = `${bodyRect.width - right + offset}px`;
|
||||
this._dropdownList.style.maxWidth = `${Math.min(dropdownListRect.width, right + offset)}px`;
|
||||
}
|
||||
|
||||
if (!right || wide) {
|
||||
const { left: originalLeft } = dropdownBtnRect;
|
||||
const { beside } = this.props;
|
||||
const offset = beside ? dropdownBtnRect.width - 40 : 0;
|
||||
// Prevent dropdown from squishing against right side of screen
|
||||
const left = Math.min(bodyRect.width - dropdownListRect.width - screenMargin, originalLeft);
|
||||
this._dropdownList.style.left = `${left + offset}px`;
|
||||
this._dropdownList.style.maxWidth = `${Math.min(
|
||||
dropdownListRect.width,
|
||||
bodyRect.width - left - offset,
|
||||
)}px`;
|
||||
}
|
||||
|
||||
if (dropUp) {
|
||||
const { top } = dropdownBtnRect;
|
||||
this._dropdownList.style.bottom = `${bodyRect.height - top}px`;
|
||||
this._dropdownList.style.maxHeight = `${top - screenMargin}px`;
|
||||
} else {
|
||||
const { bottom } = dropdownBtnRect;
|
||||
this._dropdownList.style.top = `${bottom}px`;
|
||||
this._dropdownList.style.maxHeight = `${bodyRect.height - bottom - screenMargin}px`;
|
||||
}
|
||||
}
|
||||
|
||||
_handleClick(event: React.MouseEvent<HTMLDivElement>) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.toggle();
|
||||
}
|
||||
|
||||
static _handleMouseDown(event: React.MouseEvent<HTMLDivElement>) {
|
||||
// Intercept mouse down so that clicks don't trigger things like drag and drop.
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
_getFlattenedChildren(children: ReactNode) {
|
||||
let newChildren: (ReactChild | ReactFragment | ReactPortal)[] = [];
|
||||
|
||||
for (const child of React.Children.toArray(children)) {
|
||||
if (!child) {
|
||||
// Ignore null components
|
||||
continue;
|
||||
}
|
||||
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
if (child.type === Fragment) {
|
||||
newChildren = [
|
||||
...newChildren,
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
...this._getFlattenedChildren(child.props.children),
|
||||
];
|
||||
} else if (Array.isArray(child)) {
|
||||
newChildren = [
|
||||
...newChildren,
|
||||
...this._getFlattenedChildren(child),
|
||||
];
|
||||
} else {
|
||||
newChildren.push(child);
|
||||
}
|
||||
}
|
||||
|
||||
return newChildren;
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this._checkSizeAndPosition();
|
||||
}
|
||||
|
||||
hide() {
|
||||
// Focus the dropdown button after hiding
|
||||
if (this._node) {
|
||||
const button = this._node.querySelector('button');
|
||||
|
||||
button?.focus();
|
||||
}
|
||||
|
||||
this.setState({
|
||||
open: false,
|
||||
});
|
||||
this.props.onHide?.();
|
||||
}
|
||||
|
||||
show(filterVisible = false, forcedPosition = null) {
|
||||
const bodyHeight = document.body.getBoundingClientRect().height;
|
||||
|
||||
const dropdownTop = this._node?.getBoundingClientRect().top || 0;
|
||||
|
||||
const dropUp = dropdownTop > bodyHeight - 200;
|
||||
this.setState({
|
||||
open: true,
|
||||
dropUp,
|
||||
forcedPosition,
|
||||
filterVisible,
|
||||
filter: '',
|
||||
filterItems: null,
|
||||
filterActiveIndex: -1,
|
||||
uniquenessKey: this.state.uniquenessKey + 1,
|
||||
});
|
||||
this.props.onOpen?.();
|
||||
}
|
||||
|
||||
toggle(filterVisible = false) {
|
||||
if (this.state.open) {
|
||||
this.hide();
|
||||
document.body.style.overflow = this.defaultBodyOverflow;
|
||||
} else {
|
||||
this.show(filterVisible);
|
||||
// Prevent body from scrolling when dropdown is open
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className, style, children, renderButton } = this.props;
|
||||
const {
|
||||
open,
|
||||
uniquenessKey,
|
||||
filterVisible,
|
||||
filterActiveIndex,
|
||||
filterItems,
|
||||
filter,
|
||||
} = this.state;
|
||||
const menuClasses = classnames({
|
||||
'theme--dropdown__menu': true,
|
||||
});
|
||||
|
||||
const allChildren = this._getFlattenedChildren(children);
|
||||
|
||||
const visibleChildren = allChildren.filter((child, i) => {
|
||||
if (!isDropdownItem(child)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// It's visible if its index is in the filterItems
|
||||
return !filterItems || filterItems.includes(i);
|
||||
});
|
||||
const dropdownItems: ReactNode[] = [];
|
||||
|
||||
for (let i = 0; i < allChildren.length; i++) {
|
||||
const child = allChildren[i];
|
||||
|
||||
if (isDropdownItem(child)) {
|
||||
const active = i === filterActiveIndex;
|
||||
const hide = !visibleChildren.includes(child);
|
||||
|
||||
dropdownItems.push(
|
||||
<li
|
||||
key={i}
|
||||
data-filter-index={i}
|
||||
className={classnames({ active, hide })}
|
||||
>
|
||||
{child}
|
||||
</li>,
|
||||
);
|
||||
} else if (isDropdownDivider(child)) {
|
||||
const currentIndex = visibleChildren.indexOf(child);
|
||||
const nextChild = visibleChildren[currentIndex + 1];
|
||||
|
||||
// Only show the divider if the next child is a DropdownItem
|
||||
if (nextChild && isDropdownItem(nextChild)) {
|
||||
dropdownItems.push(<li key={i}>{child}</li>);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const noResults = filter && filterItems && filterItems.length === 0;
|
||||
const button =
|
||||
typeof renderButton === 'function'
|
||||
? renderButton({
|
||||
open,
|
||||
})
|
||||
: renderButton;
|
||||
return (
|
||||
<StyledDropdown
|
||||
style={style}
|
||||
className={className}
|
||||
ref={ref => {
|
||||
this._node = ref;
|
||||
}}
|
||||
onClick={this._handleClick}
|
||||
onKeyDown={this._handleKeyDown}
|
||||
tabIndex={-1}
|
||||
onMouseDown={Dropdown._handleMouseDown}
|
||||
>
|
||||
<Fragment key="button">{button}</Fragment>
|
||||
{this.dropdownsContainer && ReactDOM.createPortal(
|
||||
<div
|
||||
key="item"
|
||||
className={menuClasses}
|
||||
aria-hidden={!open}
|
||||
>
|
||||
{open && <StyledBackdrop className="theme--transparent-overlay" />}
|
||||
{open && (
|
||||
<StyledMenu
|
||||
key={uniquenessKey}
|
||||
ref={ref => {
|
||||
this._dropdownList = ref;
|
||||
}}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<StyledFilter filtering={filterVisible}>
|
||||
<SvgIcon icon="search" />
|
||||
<input
|
||||
type="text"
|
||||
onChange={this._handleChangeFilter}
|
||||
ref={ref => {
|
||||
this._filter = ref;
|
||||
|
||||
// Automatically focus the filter element when mounted so we can start typing
|
||||
if (this._filter) {
|
||||
this._filter.focus();
|
||||
}
|
||||
}}
|
||||
onKeyPress={this._handleCheckFilterSubmit}
|
||||
/>
|
||||
</StyledFilter>
|
||||
{noResults && <StyledNoResults>No match</StyledNoResults>}
|
||||
<ul
|
||||
className={classnames({
|
||||
hide: noResults,
|
||||
})}
|
||||
>
|
||||
{dropdownItems}
|
||||
</ul>
|
||||
</StyledMenu>
|
||||
)}
|
||||
</div>,
|
||||
this.dropdownsContainer,
|
||||
)}
|
||||
</StyledDropdown>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
export { Dropdown, type DropdownProps } from './dropdown';
|
||||
export { DropdownItem, type DropdownItemProps } from './dropdown-item';
|
||||
export { DropdownDivider } from './dropdown-divider';
|
@ -1,48 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import React, { FC, memo, ReactNode } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export interface HeaderProps {
|
||||
className?: string;
|
||||
gridLeft?: ReactNode;
|
||||
gridCenter?: ReactNode;
|
||||
gridRight?: ReactNode;
|
||||
}
|
||||
|
||||
const StyledHeader = styled.div`
|
||||
border-bottom: 1px solid var(--hl-md);
|
||||
padding: var(--padding-xxs) var(--padding-sm);
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1.5fr 2fr;
|
||||
grid-template-rows: 1fr;
|
||||
grid-template-areas: 'header_left header_center header_right';
|
||||
.header_left {
|
||||
grid-area: header_left;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.header_center {
|
||||
grid-area: header_center;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.header_right {
|
||||
grid-area: header_right;
|
||||
text-align: right;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Header: FC<HeaderProps> = memo(({ className, gridLeft, gridCenter, gridRight }) => (
|
||||
<StyledHeader className={classNames('app-header theme--app-header', className)}>
|
||||
<div className="header_left">{gridLeft}</div>
|
||||
<div className="header_center">{gridCenter}</div>
|
||||
<div className="header_right">{gridRight}</div>
|
||||
</StyledHeader>
|
||||
));
|
||||
|
||||
Header.displayName = 'Header';
|
@ -1,32 +0,0 @@
|
||||
import React, { CSSProperties, FC, ReactNode } from 'react';
|
||||
|
||||
import { SvgIcon } from './svg-icon';
|
||||
import { Tooltip } from './tooltip';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
position?: 'bottom' | 'top' | 'right' | 'left';
|
||||
delay?: number;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
info?: boolean;
|
||||
}
|
||||
|
||||
export const HelpTooltip: FC<Props> = ({
|
||||
children,
|
||||
className,
|
||||
style,
|
||||
info,
|
||||
position,
|
||||
delay,
|
||||
}) => (
|
||||
<Tooltip
|
||||
position={position}
|
||||
delay={delay}
|
||||
className={className}
|
||||
message={children}
|
||||
style={style}
|
||||
>
|
||||
{info ? <SvgIcon icon="info" /> : <SvgIcon icon="question" />}
|
||||
</Tooltip>
|
||||
);
|
@ -1,27 +0,0 @@
|
||||
export * from './button';
|
||||
export * from './dropdown';
|
||||
export * from './sidebar';
|
||||
export * from './list-group';
|
||||
|
||||
export { Breadcrumb, type BreadcrumbProps, type CrumbProps } from './breadcrumb';
|
||||
export { CardContainer } from './card-container';
|
||||
export { Card, type CardProps } from './card';
|
||||
export { Header, type HeaderProps } from './header';
|
||||
export { MultiSwitch } from './multi-switch';
|
||||
export { NoticeTable, type NoticeTableProps, type Notice } from './notice-table';
|
||||
export { RadioButtonGroup, type RadioButtonGroupProps } from './radio-button-group';
|
||||
export { SvgIcon, type SvgIconProps, type IconEnum, type ThemeEnum, type IconId } from './svg-icon';
|
||||
export { Switch, type SwitchProps, type SwitchItem } from './switch';
|
||||
export {
|
||||
Table,
|
||||
type TableProps,
|
||||
TableBody,
|
||||
TableData,
|
||||
type TableDataProps,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
type TableHeaderProps,
|
||||
TableRow,
|
||||
} from './table';
|
||||
export { ToggleSwitch, type ToggleSwitchProps } from './toggle-switch';
|
||||
export { Tooltip, type TooltipProps } from './tooltip';
|
@ -1,2 +0,0 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import '@testing-library/jest-dom/extend-expect';
|
@ -1,40 +0,0 @@
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import type { RadioButtonGroupProps } from './radio-button-group';
|
||||
import { RadioButtonGroup } from './radio-button-group';
|
||||
|
||||
const ThemedButtonGroup = styled(RadioButtonGroup)<RadioButtonGroupProps>`
|
||||
&& {
|
||||
font-weight: 500;
|
||||
background: var(--hl-xs);
|
||||
color: var(--color-font);
|
||||
border: 0;
|
||||
border-radius: 100px;
|
||||
align-content: space-evenly;
|
||||
padding: var(--padding-xxs);
|
||||
transform: scale(0.9);
|
||||
transformOrigin: 'center';
|
||||
|
||||
label {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
span {
|
||||
text-transform: uppercase;
|
||||
padding: var(--padding-xs) var(--padding-xxs);
|
||||
color: var(--hl);
|
||||
background: transparent;
|
||||
font-size: var(--font-size-xs);
|
||||
margin: 0 auto;
|
||||
min-width: 4rem;
|
||||
}
|
||||
|
||||
input:checked + span {
|
||||
color: var(--color-font);
|
||||
background: var(--color-bg);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const MultiSwitch: FunctionComponent<RadioButtonGroupProps> = props => <ThemedButtonGroup {...props} />;
|
@ -1,80 +0,0 @@
|
||||
import React, { FunctionComponent, useCallback } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export interface RadioButtonGroupProps {
|
||||
name: string;
|
||||
onChange: (value: string) => any;
|
||||
choices: {
|
||||
label: string;
|
||||
value: string;
|
||||
}[];
|
||||
className?: string;
|
||||
selectedValue: string;
|
||||
}
|
||||
|
||||
const StyledRadioButtonGroup = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: var(--padding-xs);
|
||||
border: 1px solid var(--hl-xs);
|
||||
border-radius: var(--radius--sm);
|
||||
|
||||
& > * :not(:last-child) {
|
||||
margin-right: var(--padding-xs);
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledRadioButtonBtn = styled.label`
|
||||
cursor: pointer;
|
||||
color: var(--color-font);
|
||||
flex-grow: 1;
|
||||
justify-content: center;
|
||||
|
||||
input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
span {
|
||||
text-align: center;
|
||||
padding: var(--padding-sm) var(--padding-xs);
|
||||
display: block;
|
||||
border-radius: var(--line-height-sm);
|
||||
}
|
||||
|
||||
input:checked + span {
|
||||
background-color: var(--hl-xs);
|
||||
}
|
||||
`;
|
||||
|
||||
export const RadioButtonGroup: FunctionComponent<RadioButtonGroupProps> = ({
|
||||
name,
|
||||
choices,
|
||||
onChange,
|
||||
className,
|
||||
selectedValue,
|
||||
}) => {
|
||||
const handleChange = useCallback(event => {
|
||||
if (typeof onChange !== 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
onChange(event.currentTarget.value);
|
||||
}, [onChange]);
|
||||
|
||||
return (
|
||||
<StyledRadioButtonGroup className={className}>
|
||||
{choices.map(({ label, value }) => (
|
||||
<StyledRadioButtonBtn key={value}>
|
||||
<input
|
||||
type="radio"
|
||||
name={name}
|
||||
value={value}
|
||||
checked={selectedValue === value}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<span>{label}</span>
|
||||
</StyledRadioButtonBtn>
|
||||
))}
|
||||
</StyledRadioButtonGroup>
|
||||
);
|
||||
};
|
@ -1,30 +0,0 @@
|
||||
import { motion } from 'framer-motion';
|
||||
import React, { FunctionComponent, ReactNode } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export interface SidebarPanelProps {
|
||||
children: ReactNode;
|
||||
childrenVisible: boolean;
|
||||
}
|
||||
|
||||
const StyledPanel = styled(motion.div)`
|
||||
height: 0;
|
||||
`;
|
||||
|
||||
export const SidebarPanel: FunctionComponent<SidebarPanelProps> = ({ childrenVisible, children }) => (
|
||||
<StyledPanel
|
||||
initial={{
|
||||
height: childrenVisible ? '100%' : '0px',
|
||||
}}
|
||||
animate={{
|
||||
height: childrenVisible ? '100%' : '0px',
|
||||
}}
|
||||
transition={{
|
||||
duration: 0.2,
|
||||
ease: 'easeInOut',
|
||||
delay: 0,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</StyledPanel>
|
||||
);
|
@ -1,94 +0,0 @@
|
||||
import React, { Fragment, FunctionComponent } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export interface SwitchItem {
|
||||
label: string;
|
||||
selected: boolean;
|
||||
}
|
||||
|
||||
export interface SwitchProps {
|
||||
className?: string;
|
||||
onClick?: (event: React.SyntheticEvent<HTMLInputElement>) => any;
|
||||
optionItems?: SwitchItem[];
|
||||
}
|
||||
|
||||
const StyledSwitch = styled.div`
|
||||
margin: auto;
|
||||
.switch {
|
||||
position: relative;
|
||||
height: 34px;
|
||||
width: 140px;
|
||||
background: var(--hl-xs);
|
||||
border-radius: 17px;
|
||||
}
|
||||
.switch-label {
|
||||
position: relative;
|
||||
font-weight: bold;
|
||||
z-index: 2;
|
||||
float: left;
|
||||
width: 67px;
|
||||
line-height: 34px;
|
||||
font-size: 11px;
|
||||
color: var(--hl);
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
padding-top: 0px;
|
||||
}
|
||||
.switch-label-off {
|
||||
padding-left: 2px;
|
||||
}
|
||||
.switch-label-on {
|
||||
padding-right: 2px;
|
||||
}
|
||||
.switch-input {
|
||||
display: none;
|
||||
}
|
||||
.switch-input:checked + .switch-label {
|
||||
color: var(--color-font);
|
||||
text-shadow: 0 1px rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
.switch-input:checked + .switch-label-on ~ .switch-selection {
|
||||
left: 71px;
|
||||
}
|
||||
.switch-selection {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 4px;
|
||||
left: 4px;
|
||||
display: block;
|
||||
width: 65px;
|
||||
height: 26px;
|
||||
border-radius: 13px;
|
||||
background-color: var(--color-bg);
|
||||
transition: left 0.2s ease-out;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Switch: FunctionComponent<SwitchProps> = ({ className, onClick, optionItems }) => (
|
||||
<StyledSwitch className={className}>
|
||||
<div className="switch">
|
||||
{optionItems?.map((item, i) => {
|
||||
return (
|
||||
<Fragment key={item.label}>
|
||||
<input
|
||||
type="radio"
|
||||
className="switch-input"
|
||||
name="switch"
|
||||
onClick={onClick}
|
||||
value={item.label}
|
||||
id={item.label}
|
||||
defaultChecked={item.selected}
|
||||
/>
|
||||
<label
|
||||
htmlFor={item.label}
|
||||
className={`switch-label ${i === 0 ? 'switch-label-off' : 'switch-label-on'}`}
|
||||
>
|
||||
{item.label}
|
||||
</label>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
<span className="switch-selection" />
|
||||
</div>
|
||||
</StyledSwitch>
|
||||
);
|
@ -1,112 +0,0 @@
|
||||
import { ReactNode } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
/***********/
|
||||
/* <table> *
|
||||
/***********/
|
||||
export interface TableProps {
|
||||
children: ReactNode;
|
||||
striped?: boolean;
|
||||
outlined?: boolean;
|
||||
compact?: boolean;
|
||||
headings?: ReactNode[];
|
||||
}
|
||||
|
||||
export const Table = styled.table<TableProps>`
|
||||
width: 100%;
|
||||
border-spacing: 0;
|
||||
border-collapse: collapse;
|
||||
|
||||
td,
|
||||
th {
|
||||
padding: ${({ compact }) => (compact ? 'var(--padding-xs)' : 'var(--padding-sm)')}
|
||||
${({ compact }) => (compact ? 'var(--padding-sm)' : 'var(--padding-md)')};
|
||||
}
|
||||
|
||||
${({ striped }) =>
|
||||
striped &&
|
||||
`
|
||||
tbody tr:nth-child(odd) {
|
||||
background: var(--hl-xs);
|
||||
}`}
|
||||
|
||||
${({ outlined }) =>
|
||||
outlined &&
|
||||
`
|
||||
& {
|
||||
th {
|
||||
background: var(--hl-xxs);
|
||||
}
|
||||
|
||||
&,
|
||||
td {
|
||||
border: 1px solid var(--hl-sm);
|
||||
}
|
||||
|
||||
tr.table--no-outline-row td {
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
& {
|
||||
border-radius: 3px;
|
||||
border-collapse: unset;
|
||||
}
|
||||
|
||||
td {
|
||||
border-left: 0;
|
||||
border-bottom: 0;
|
||||
|
||||
&:last-child {
|
||||
border-right: 0;
|
||||
}
|
||||
}
|
||||
}`}
|
||||
`;
|
||||
|
||||
/********/
|
||||
/* <tr> */
|
||||
/********/
|
||||
export const TableRow = styled.tr``;
|
||||
|
||||
/********/
|
||||
/* <td> */
|
||||
/********/
|
||||
export interface TableDataProps {
|
||||
compact?: boolean;
|
||||
align?: 'center' | 'left';
|
||||
}
|
||||
|
||||
export const TableData = styled.td<TableDataProps>`
|
||||
vertical-align: top;
|
||||
padding: 0 var(--padding-md);
|
||||
text-align: ${({ align }) => align || 'left'};
|
||||
`;
|
||||
|
||||
/********/
|
||||
|
||||
/* <th> */
|
||||
|
||||
/********/
|
||||
export interface TableHeaderProps {
|
||||
compact?: boolean;
|
||||
align?: 'center' | 'left';
|
||||
}
|
||||
|
||||
export const TableHeader = styled.th<TableHeaderProps>`
|
||||
vertical-align: top;
|
||||
padding: 0 var(--padding-md);
|
||||
text-align: ${({ align }) => align || 'left'};
|
||||
`;
|
||||
|
||||
/***********/
|
||||
/* <thead> */
|
||||
/***********/
|
||||
export const TableHead = styled.thead``;
|
||||
|
||||
/***********/
|
||||
|
||||
/* <tbody> */
|
||||
|
||||
/***********/
|
||||
export const TableBody = styled.tbody``;
|
@ -1,85 +0,0 @@
|
||||
import React, { FunctionComponent, ReactNode, useCallback, useEffect, useState } from 'react';
|
||||
import ReactSwitch from 'react-switch';
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
// @ts-expect-error Issue with commonJS export in vite
|
||||
const Switch = ReactSwitch.default;
|
||||
export interface ToggleSwitchProps {
|
||||
labelClassName?: string;
|
||||
switchClassName?: string;
|
||||
checked?: boolean;
|
||||
disabled?: boolean;
|
||||
onChange(checked: boolean, arg1: Event, arg2: string): void | Promise<void>;
|
||||
label?: ReactNode;
|
||||
}
|
||||
|
||||
const ThemedSwitch = styled(Switch)<{ checked: boolean }>`
|
||||
&& {
|
||||
.react-switch-bg {
|
||||
background: ${({ checked }) => (checked ? 'var(--color-surprise)' : 'var(--hl-xl)')} !important;
|
||||
|
||||
${({ checked }) => checked && css`
|
||||
path {
|
||||
fill: var(--color-font-surprise) !important
|
||||
}
|
||||
`}
|
||||
}
|
||||
|
||||
vertical-align: middle;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledLabel = styled.label`
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
margin-left: var(--padding-md);
|
||||
}
|
||||
`;
|
||||
|
||||
export const ToggleSwitch: FunctionComponent<ToggleSwitchProps> = ({
|
||||
labelClassName,
|
||||
switchClassName = '',
|
||||
checked: checkedProp,
|
||||
onChange,
|
||||
disabled,
|
||||
label,
|
||||
...props
|
||||
}) => {
|
||||
const [checked, setChecked] = useState(Boolean(checkedProp));
|
||||
|
||||
// If prop changes and differs from state, update state
|
||||
useEffect(() => {
|
||||
setChecked(Boolean(checkedProp));
|
||||
}, [checkedProp]);
|
||||
|
||||
const callback = useCallback((c, a, b) => {
|
||||
setChecked(c);
|
||||
onChange(c, a, b);
|
||||
}, [onChange]);
|
||||
|
||||
const toggle = (
|
||||
<ThemedSwitch
|
||||
className={switchClassName}
|
||||
checked={checked}
|
||||
disabled={disabled}
|
||||
onChange={callback}
|
||||
height={20}
|
||||
width={40}
|
||||
{...(props as Record<string, any>)}
|
||||
/>
|
||||
);
|
||||
|
||||
if (label) {
|
||||
return (
|
||||
<StyledLabel className={labelClassName}>
|
||||
{toggle}
|
||||
<span>{label}</span>
|
||||
</StyledLabel>
|
||||
);
|
||||
} else {
|
||||
return toggle;
|
||||
}
|
||||
};
|
@ -1,81 +0,0 @@
|
||||
import { jest } from '@jest/globals';
|
||||
import { ByRoleMatcher, ByRoleOptions, fireEvent, render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { Tooltip } from './tooltip';
|
||||
|
||||
type QueryByRole = (
|
||||
text: ByRoleMatcher,
|
||||
options?: ByRoleOptions | undefined,
|
||||
) => HTMLElement | null;
|
||||
|
||||
const expectNoTooltip = (queryByRole: QueryByRole) => {
|
||||
expect(queryByRole('tooltip', { hidden: true })).toBeNull();
|
||||
expect(queryByRole('tooltip', {})).toBeNull();
|
||||
};
|
||||
|
||||
describe('<Tooltip />', () => {
|
||||
it('should show and hide the tooltip correctly', async () => {
|
||||
const childText = 'some child';
|
||||
const delay = 200;
|
||||
const message = 'message';
|
||||
|
||||
const { getByRole, getByText, queryByRole, unmount } = render(
|
||||
<Tooltip message={message} delay={delay}>
|
||||
{childText}
|
||||
</Tooltip>,
|
||||
);
|
||||
|
||||
expect(getByRole('tooltip', { hidden: true })).toBeTruthy();
|
||||
|
||||
fireEvent.mouseEnter(getByText(childText));
|
||||
|
||||
// Should open after the configured delay, wait a sufficient amount
|
||||
jest.advanceTimersByTime(delay * 2);
|
||||
|
||||
expect(getByRole('tooltip')).toBeTruthy();
|
||||
|
||||
fireEvent.mouseLeave(getByText(childText));
|
||||
|
||||
// Should close after 100ms default, wait a sufficient amount
|
||||
jest.advanceTimersByTime(delay);
|
||||
|
||||
expect(getByRole('tooltip', { hidden: true })).toBeTruthy();
|
||||
|
||||
unmount();
|
||||
|
||||
expectNoTooltip(queryByRole);
|
||||
});
|
||||
|
||||
it('should not render tooltip if no message exists', () => {
|
||||
const childText = 'some child';
|
||||
const message = '';
|
||||
const { queryByRole, getByText } = render(<Tooltip message={message}>{childText}</Tooltip>);
|
||||
|
||||
expectNoTooltip(queryByRole);
|
||||
expect(getByText(childText)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should unmount successfully if message is empty in the first render then and non-empty after update', async () => {
|
||||
const childText = 'some child';
|
||||
const initialMessage = '';
|
||||
const newMessage = 'message';
|
||||
|
||||
const { queryByRole, getByRole, getByText, queryByText, rerender, unmount } = render(
|
||||
<Tooltip message={initialMessage}>{childText}</Tooltip>,
|
||||
);
|
||||
|
||||
expectNoTooltip(queryByRole);
|
||||
expect(getByText(childText)).toBeTruthy();
|
||||
|
||||
rerender(<Tooltip message={newMessage}>{childText}</Tooltip>);
|
||||
|
||||
expect(getByRole('tooltip', { hidden: true })).toBeTruthy();
|
||||
expect(getByText(childText)).toBeTruthy();
|
||||
|
||||
unmount();
|
||||
|
||||
expectNoTooltip(queryByRole);
|
||||
expect(queryByText(childText)).toBeFalsy();
|
||||
});
|
||||
});
|
@ -1,254 +0,0 @@
|
||||
import { autoBindMethodsForReact } from 'class-autobind-decorator';
|
||||
import classnames from 'classnames';
|
||||
import React, { CSSProperties, MouseEvent, PureComponent, ReactNode } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export interface TooltipProps {
|
||||
children: ReactNode;
|
||||
message: ReactNode;
|
||||
position?: 'bottom' | 'top' | 'right' | 'left';
|
||||
className?: string;
|
||||
delay?: number;
|
||||
selectable?: boolean;
|
||||
wide?: boolean;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
interface State {
|
||||
visible: boolean;
|
||||
movedToBody: boolean;
|
||||
}
|
||||
|
||||
const StyledTooltip = styled.div`
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
`;
|
||||
|
||||
const StyledTooltipBubble = styled.div`
|
||||
position: fixed;
|
||||
left: -999999px;
|
||||
opacity: 0;
|
||||
background: var(--color-bg);
|
||||
border: 1px solid var(--hl-sm);
|
||||
box-shadow: 0 0 1em rgba(0, 0, 0, 0.1);
|
||||
color: var(--color-font);
|
||||
padding: var(--padding-sm) var(--padding-md);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: var(--font-size-sm);
|
||||
max-width: 20rem;
|
||||
text-align: center;
|
||||
z-index: 10;
|
||||
white-space: normal !important;
|
||||
word-wrap: break-word;
|
||||
|
||||
&.tooltip__bubble--visible {
|
||||
opacity: 1;
|
||||
z-index: 99999;
|
||||
transition: opacity 200ms;
|
||||
|
||||
// Back to normal
|
||||
height: auto;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
&.tooltip__bubble--wide {
|
||||
max-width: 30rem;
|
||||
}
|
||||
`;
|
||||
|
||||
@autoBindMethodsForReact
|
||||
export class Tooltip extends PureComponent<TooltipProps, State> {
|
||||
_showTimeout: NodeJS.Timeout | null = null;
|
||||
_hideTimeout: NodeJS.Timeout | null = null;
|
||||
|
||||
_tooltip: HTMLDivElement | null = null;
|
||||
_bubble: HTMLDivElement | null = null;
|
||||
_id = Math.random() + '';
|
||||
|
||||
state: State = {
|
||||
visible: false,
|
||||
movedToBody: false,
|
||||
};
|
||||
|
||||
_handleStopClick(event: MouseEvent) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
_handleMouseEnter() {
|
||||
if (this._showTimeout !== null) {
|
||||
clearTimeout(this._showTimeout);
|
||||
}
|
||||
if (this._hideTimeout !== null) {
|
||||
clearTimeout(this._hideTimeout);
|
||||
}
|
||||
this._showTimeout = setTimeout(() => {
|
||||
const tooltip = this._tooltip;
|
||||
const bubble = this._bubble;
|
||||
|
||||
if (!tooltip) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!bubble) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tooltipRect = tooltip.getBoundingClientRect();
|
||||
const bubbleRect = bubble.getBoundingClientRect();
|
||||
const margin = 3;
|
||||
let left = 0;
|
||||
let top = 0;
|
||||
|
||||
switch (this.props.position) {
|
||||
case 'right':
|
||||
top = tooltipRect.top - bubbleRect.height / 2 + tooltipRect.height / 2;
|
||||
left = tooltipRect.left + tooltipRect.width + margin;
|
||||
break;
|
||||
|
||||
case 'left':
|
||||
top = tooltipRect.top - bubbleRect.height / 2 + tooltipRect.height / 2;
|
||||
left = tooltipRect.left - bubbleRect.width - margin;
|
||||
break;
|
||||
|
||||
case 'bottom':
|
||||
top = tooltipRect.top + tooltipRect.height + margin;
|
||||
left = tooltipRect.left - bubbleRect.width / 2 + tooltipRect.width / 2;
|
||||
break;
|
||||
|
||||
case 'top':
|
||||
default:
|
||||
top = tooltipRect.top - bubbleRect.height - margin;
|
||||
left = tooltipRect.left - bubbleRect.width / 2 + tooltipRect.width / 2;
|
||||
break;
|
||||
}
|
||||
|
||||
bubble.style.left = `${Math.max(0, left)}px`;
|
||||
bubble.style.top = `${Math.max(0, top)}px`;
|
||||
this.setState({
|
||||
visible: true,
|
||||
});
|
||||
}, this.props.delay || 400);
|
||||
}
|
||||
|
||||
_handleMouseLeave(): void {
|
||||
if (this._showTimeout !== null) {
|
||||
clearTimeout(this._showTimeout);
|
||||
}
|
||||
if (this._hideTimeout !== null) {
|
||||
clearTimeout(this._hideTimeout);
|
||||
}
|
||||
this._hideTimeout = setTimeout(() => {
|
||||
this.setState({
|
||||
visible: false,
|
||||
});
|
||||
const bubble = this._bubble;
|
||||
|
||||
if (!bubble) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset positioning stuff
|
||||
bubble.style.left = '';
|
||||
bubble.style.top = '';
|
||||
bubble.style.bottom = '';
|
||||
bubble.style.right = '';
|
||||
}, 100);
|
||||
}
|
||||
|
||||
_getContainer() {
|
||||
let container = document.querySelector<HTMLElement>('#tooltips-container');
|
||||
|
||||
if (!container) {
|
||||
container = document.createElement('div');
|
||||
container.id = 'tooltips-container';
|
||||
container.style.zIndex = '1000000';
|
||||
container.style.position = 'relative';
|
||||
document.body && document.body.appendChild(container);
|
||||
}
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
_moveBubbleToBody() {
|
||||
if (this._bubble) {
|
||||
const el = ReactDOM.findDOMNode(this._bubble);
|
||||
el && this._getContainer().appendChild(el);
|
||||
this.setState({
|
||||
movedToBody: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_removeBubbleFromBody() {
|
||||
if (this._bubble) {
|
||||
const el = ReactDOM.findDOMNode(this._bubble);
|
||||
if (this._getContainer().contains(el)) {
|
||||
el && this._getContainer().removeChild(el);
|
||||
}
|
||||
this.setState({
|
||||
movedToBody: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// Move the element to the body so we can position absolutely
|
||||
this._moveBubbleToBody();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
// If the bubble has not been moved to body, move it.
|
||||
// This can happen if there is no message during the first mount but a message is provided during on a subsequent render.
|
||||
if (!this.state.movedToBody) {
|
||||
this._moveBubbleToBody();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// Remove the element from the body
|
||||
this._removeBubbleFromBody();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { children, message, className, selectable, wide, style } = this.props;
|
||||
const { visible } = this.state;
|
||||
|
||||
if (!message) {
|
||||
return children;
|
||||
}
|
||||
|
||||
const tooltipClasses = classnames(className, 'tooltip');
|
||||
const bubbleClasses = classnames('tooltip__bubble theme--tooltip', {
|
||||
'tooltip__bubble--visible': visible,
|
||||
'tooltip__bubble--wide': wide,
|
||||
selectable: selectable,
|
||||
});
|
||||
return (
|
||||
<StyledTooltip
|
||||
className={tooltipClasses}
|
||||
ref={ref => {
|
||||
this._tooltip = ref;
|
||||
}}
|
||||
id={this._id}
|
||||
onMouseEnter={this._handleMouseEnter}
|
||||
onMouseLeave={this._handleMouseLeave}
|
||||
style={style}
|
||||
>
|
||||
<StyledTooltipBubble
|
||||
className={bubbleClasses}
|
||||
onClick={this._handleStopClick}
|
||||
role="tooltip"
|
||||
aria-hidden={!visible}
|
||||
aria-describedby={this._id}
|
||||
ref={ref => {
|
||||
this._bubble = ref;
|
||||
}}
|
||||
>
|
||||
{message}
|
||||
</StyledTooltipBubble>
|
||||
{children}
|
||||
</StyledTooltip>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"experimentalDecorators": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react",
|
||||
"lib": ["DOM"],
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"exclude": [
|
||||
"**/*.test.ts",
|
||||
"**/*.test.tsx",
|
||||
"src/jest",
|
||||
],
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
{
|
||||
"extends": "./tsconfig.build.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": [
|
||||
".eslintrc.js",
|
||||
"jest.config.js",
|
||||
"src",
|
||||
"svgr.config.js",
|
||||
"vite.config.ts",
|
||||
],
|
||||
"exclude": [
|
||||
"dist",
|
||||
],
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
const __DEV__ = mode !== 'production';
|
||||
|
||||
return {
|
||||
build: {
|
||||
sourcemap: __DEV__,
|
||||
lib: {
|
||||
entry: './src/index.ts',
|
||||
fileName: format => {
|
||||
if (format === 'cjs') {
|
||||
return 'commonjs/index.js';
|
||||
}
|
||||
return 'index.js';
|
||||
},
|
||||
// We use CommonJS output for running tests in jest.
|
||||
formats: ['cjs', 'es'],
|
||||
name: 'insomnia-components',
|
||||
},
|
||||
emptyOutDir: false,
|
||||
rollupOptions: {
|
||||
external: ['react', 'react-dom', 'styled-components', 'react-use'],
|
||||
},
|
||||
},
|
||||
define: {
|
||||
'process.env.NODE_ENV': JSON.stringify(mode),
|
||||
},
|
||||
};
|
||||
});
|
@ -17,7 +17,8 @@ test('can make oauth2 requests', async ({ app, page }) => {
|
||||
});
|
||||
|
||||
await page.locator('[data-testid="project"]').click();
|
||||
await page.locator('text=Create').click();
|
||||
const projectView = page.locator('#wrapper');
|
||||
await projectView.locator('text=Create').click();
|
||||
|
||||
const text = await loadFixture('oauth.yaml');
|
||||
await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text);
|
||||
|
@ -4,3 +4,5 @@ send-request
|
||||
coverage
|
||||
src/main.min.js
|
||||
src/preload.js
|
||||
**/svgr
|
||||
svgr.config.js
|
||||
|
@ -37,7 +37,8 @@
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"type-check": "tsc --noEmit --project tsconfig.build.json",
|
||||
"type-check:watch": "npm run type-check -- --watch"
|
||||
"type-check:watch": "npm run type-check -- --watch",
|
||||
"convert-svg": "npm_config_yes=true npx @svgr/cli@6.4.0 --no-index --config-file svgr.config.js --out-dir src/ui/components/assets/svgr src/ui/components/assets/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@getinsomnia/node-libcurl": "2.3.5-2",
|
||||
@ -177,7 +178,6 @@
|
||||
"graphql": "^16.3.0",
|
||||
"graphql-language-service": "^4.1.5",
|
||||
"highlight.js": "^11.5.1",
|
||||
"insomnia-components": "3.6.1-beta.0",
|
||||
"jest": "^28.1.0",
|
||||
"jest-environment-jsdom": "^28.0.2",
|
||||
"jest-mock": "^28.0.2",
|
||||
|
@ -21,7 +21,6 @@ const toSchema = <T>(obj: T): Schema<T> => {
|
||||
|
||||
return output as Schema<T>;
|
||||
};
|
||||
|
||||
export const baseModelSchema: Schema<BaseModel> = {
|
||||
_id: () => 'id',
|
||||
created: () => 1234,
|
||||
|
@ -66,7 +66,6 @@ export interface PrivateProperties {
|
||||
axios: typeof axios;
|
||||
analytics: typeof analytics;
|
||||
loadRendererModules: () => Promise<{
|
||||
insomniaComponents: any;
|
||||
ReactDOM: typeof ReactDOM;
|
||||
React: typeof React;
|
||||
} | {}>;
|
||||
@ -226,12 +225,10 @@ export function init(renderPurpose: RenderPurpose = RENDER_PURPOSE_GENERAL): {
|
||||
|
||||
const ReactDOM = await import('react-dom');
|
||||
const React = await import('react');
|
||||
const insomniaComponents = await import('insomnia-components');
|
||||
|
||||
return {
|
||||
ReactDOM,
|
||||
React,
|
||||
insomniaComponents,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
@ -537,12 +537,12 @@ export function describeChanges<T extends BaseModel>(a: T, b: T): string[] {
|
||||
const bStr = deterministicStringify(bValue);
|
||||
|
||||
if (aValue === undefined && bValue !== undefined) {
|
||||
changes.push(`+${key}`);
|
||||
changes.push(`+${String(key)}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (aValue !== undefined && bValue === undefined) {
|
||||
changes.push(`-${key}`);
|
||||
changes.push(`-${String(key)}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { Breadcrumb, BreadcrumbProps, Header as _Header } from 'insomnia-components';
|
||||
import classNames from 'classnames';
|
||||
import React, { FC, Fragment, ReactNode } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import coreLogo from '../images/insomnia-logo.svg';
|
||||
import { selectIsLoggedIn } from '../redux/selectors';
|
||||
import { Breadcrumb, BreadcrumbProps } from './breadcrumb';
|
||||
import { SettingsButton } from './buttons/settings-button';
|
||||
import { AccountDropdownButton } from './dropdowns/account-dropdown/account-dropdown';
|
||||
import { GitHubStarsButton } from './github-stars-button';
|
||||
@ -13,12 +14,6 @@ const LogoWraper = styled.div({
|
||||
display: 'flex',
|
||||
});
|
||||
|
||||
const Header = styled(_Header)({
|
||||
'&&': {
|
||||
whiteSpace: 'nowrap',
|
||||
},
|
||||
});
|
||||
|
||||
const RightWrapper = styled.div({
|
||||
transformOrigin: 'right',
|
||||
transform: 'scale(0.85)',
|
||||
@ -37,6 +32,55 @@ export interface AppHeaderProps {
|
||||
gridRight?: ReactNode;
|
||||
}
|
||||
|
||||
export interface HeaderProps {
|
||||
className?: string;
|
||||
gridLeft?: ReactNode;
|
||||
gridCenter?: ReactNode;
|
||||
gridRight?: ReactNode;
|
||||
}
|
||||
|
||||
const StyledHeader = styled.div({
|
||||
borderBottom: '1px solid var(--hl-md)',
|
||||
padding: 'var(--padding-xxs) var(--padding-sm)',
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '2fr 1.5fr 2fr',
|
||||
gridTemplateRows: '1fr',
|
||||
gridTemplateAreas: "'header_left header_center header_right'",
|
||||
'.header_left': {
|
||||
gridArea: 'header_left',
|
||||
textAlign: 'left',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
'.header_center': {
|
||||
gridArea: 'header_center',
|
||||
textAlign: 'center',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
'.header_right': {
|
||||
gridArea: 'header_right',
|
||||
textAlign: 'right',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
|
||||
'&&': {
|
||||
whiteSpace: 'nowrap',
|
||||
},
|
||||
});
|
||||
|
||||
const Header: FC<HeaderProps> = ({ className, gridLeft, gridCenter, gridRight }) => (
|
||||
<StyledHeader className={classNames('app-header theme--app-header', className)}>
|
||||
<div className="header_left">{gridLeft}</div>
|
||||
<div className="header_center">{gridCenter}</div>
|
||||
<div className="header_right">{gridRight}</div>
|
||||
</StyledHeader>
|
||||
);
|
||||
|
||||
Header.displayName = 'Header';
|
||||
|
||||
export const AppHeader: FC<AppHeaderProps> = ({
|
||||
breadcrumbProps,
|
||||
gridCenter,
|
||||
|
Before Width: | Height: | Size: 226 B After Width: | Height: | Size: 226 B |
Before Width: | Height: | Size: 652 B After Width: | Height: | Size: 652 B |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 640 B After Width: | Height: | Size: 640 B |
Before Width: | Height: | Size: 520 B After Width: | Height: | Size: 520 B |
Before Width: | Height: | Size: 200 B After Width: | Height: | Size: 200 B |
Before Width: | Height: | Size: 177 B After Width: | Height: | Size: 177 B |
Before Width: | Height: | Size: 177 B After Width: | Height: | Size: 177 B |
Before Width: | Height: | Size: 483 B After Width: | Height: | Size: 483 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 586 B After Width: | Height: | Size: 586 B |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 941 B After Width: | Height: | Size: 941 B |
Before Width: | Height: | Size: 330 B After Width: | Height: | Size: 330 B |
Before Width: | Height: | Size: 853 B After Width: | Height: | Size: 853 B |
Before Width: | Height: | Size: 667 B After Width: | Height: | Size: 667 B |
Before Width: | Height: | Size: 190 B After Width: | Height: | Size: 190 B |
Before Width: | Height: | Size: 398 B After Width: | Height: | Size: 398 B |
Before Width: | Height: | Size: 359 B After Width: | Height: | Size: 359 B |
Before Width: | Height: | Size: 809 B After Width: | Height: | Size: 809 B |
Before Width: | Height: | Size: 430 B After Width: | Height: | Size: 430 B |
Before Width: | Height: | Size: 858 B After Width: | Height: | Size: 858 B |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 778 B After Width: | Height: | Size: 778 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 638 B After Width: | Height: | Size: 638 B |
Before Width: | Height: | Size: 222 B After Width: | Height: | Size: 222 B |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 244 B After Width: | Height: | Size: 244 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 268 B After Width: | Height: | Size: 268 B |
Before Width: | Height: | Size: 679 B After Width: | Height: | Size: 679 B |
Before Width: | Height: | Size: 318 B After Width: | Height: | Size: 318 B |
Before Width: | Height: | Size: 465 B After Width: | Height: | Size: 465 B |
Before Width: | Height: | Size: 597 B After Width: | Height: | Size: 597 B |
Before Width: | Height: | Size: 879 B After Width: | Height: | Size: 879 B |
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 406 B |
Before Width: | Height: | Size: 428 B After Width: | Height: | Size: 428 B |
Before Width: | Height: | Size: 611 B After Width: | Height: | Size: 611 B |
Before Width: | Height: | Size: 701 B After Width: | Height: | Size: 701 B |
Before Width: | Height: | Size: 897 B After Width: | Height: | Size: 897 B |
Before Width: | Height: | Size: 274 B After Width: | Height: | Size: 274 B |
Before Width: | Height: | Size: 769 B After Width: | Height: | Size: 769 B |
Before Width: | Height: | Size: 744 B After Width: | Height: | Size: 744 B |
Before Width: | Height: | Size: 292 B After Width: | Height: | Size: 292 B |
Before Width: | Height: | Size: 328 B After Width: | Height: | Size: 328 B |
Before Width: | Height: | Size: 696 B After Width: | Height: | Size: 696 B |
Before Width: | Height: | Size: 543 B After Width: | Height: | Size: 543 B |
Before Width: | Height: | Size: 521 B After Width: | Height: | Size: 521 B |
Before Width: | Height: | Size: 250 B After Width: | Height: | Size: 250 B |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 348 B After Width: | Height: | Size: 348 B |
Before Width: | Height: | Size: 522 B After Width: | Height: | Size: 522 B |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 334 B After Width: | Height: | Size: 334 B |
@ -0,0 +1,14 @@
|
||||
import React, { SVGProps, memo } from 'react';
|
||||
export const SvgIcnArrowRight = memo<SVGProps<SVGSVGElement>>(props => (
|
||||
<svg
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 12 12"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
role="img"
|
||||
{...props}
|
||||
>
|
||||
<circle cx={6} cy={6} r={6} />
|
||||
<path fill="currentColor" d="M3 5v2h3v2l4-3-4-3v2z" />
|
||||
</svg>
|
||||
));
|
@ -0,0 +1,20 @@
|
||||
import React, { SVGProps, memo } from 'react';
|
||||
export const SvgIcnBitbucketLogo = memo<SVGProps<SVGSVGElement>>(props => (
|
||||
<svg
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
role="img"
|
||||
{...props}
|
||||
>
|
||||
<path fill="none" d="M0 0h14v14H0z" />
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M2.078 3.111A.32.32 0 0 1 2.325 3l9.35.002a.32.32 0 0 1 .32.372l-1.36 8.357a.32.32 0 0 1-.32.269H3.79a.436.436 0 0 1-.426-.364l-1.36-8.264a.32.32 0 0 1 .074-.26Zm3.565 6.433h2.754l.667-4.091H4.897l.746 4.09Z"
|
||||
fill=""
|
||||
/>
|
||||
</svg>
|
||||
));
|
@ -0,0 +1,17 @@
|
||||
import React, { SVGProps, memo } from 'react';
|
||||
export const SvgIcnBrackets = memo<SVGProps<SVGSVGElement>>(props => (
|
||||
<svg
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 12 12"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
role="img"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M4.504 0C3.22.07 2.312.432 1.873 1.237c-.231.424-.273.877-.26 1.268.014.376.085.765.145 1.092l.004.024c.065.356.116.645.125.903.01.254-.026.402-.077.495-.068.125-.356.467-1.805.468H0v1.5h.005c1.473 0 1.765.324 1.82.417.037.061.07.17.062.385-.009.223-.057.475-.122.801l-.006.026a6.578 6.578 0 0 0-.145 1.01c-.015.37.03.808.274 1.216.507.844 1.59 1.145 3.112 1.145l-.432-1.51c-1.105-.056-1.343-.324-1.394-.407-.036-.061-.07-.17-.061-.385.009-.223.057-.475.122-.8l.006-.027c.059-.294.131-.656.145-1.009.015-.37-.03-.809-.274-1.217-.096-.159-.212-.299-.347-.42a1.93 1.93 0 0 0 .362-.475c.231-.423.273-.877.26-1.267a7.801 7.801 0 0 0-.145-1.093l-.004-.024c-.065-.356-.116-.645-.125-.903-.01-.254.026-.402.077-.494.068-.126.357-.469 1.81-.469L4.504 0Zm2.992 0L7 1.487c1.453 0 1.742.343 1.81.469.05.092.086.24.077.494-.01.258-.06.547-.125.903l-.004.024a7.8 7.8 0 0 0-.144 1.093c-.014.39.028.844.259 1.267.098.18.219.337.362.474a1.834 1.834 0 0 0-.347.421c-.245.408-.289.847-.274 1.217.014.353.086.715.145 1.01l.006.025c.065.326.113.578.122.801.009.216-.025.324-.061.385-.05.083-.29.351-1.394.407L7 11.987c1.523 0 2.605-.301 3.112-1.145.245-.408.289-.847.274-1.217-.014-.353-.086-.715-.145-1.01l-.006-.025c-.065-.326-.113-.578-.122-.8-.009-.216.025-.325.061-.386.056-.093.349-.417 1.826-.417v-1.5c-1.453 0-1.742-.343-1.81-.468-.05-.093-.086-.24-.077-.495.01-.258.06-.547.125-.903l.004-.024c.06-.327.13-.716.144-1.092.014-.39-.028-.844-.259-1.268C9.687.432 8.78.07 7.496 0Z"
|
||||
/>
|
||||
</svg>
|
||||
));
|
@ -0,0 +1,6 @@
|
||||
import React, { SVGProps, memo } from 'react';
|
||||
export const SvgIcnBug = memo<SVGProps<SVGSVGElement>>(props => (
|
||||
<svg viewBox="0 0 111 111" width="1em" height="1em" role="img" {...props}>
|
||||
<path d="M110.081 62.515c-.102 3.716-3.245 6.63-6.962 6.63H91.32v3.412c0 4.662-1.04 9.08-2.9 13.037l12.842 12.842a6.823 6.823 0 1 1-9.649 9.649l-11.67-11.67a30.575 30.575 0 0 1-19.326 6.845V51.236a2.559 2.559 0 0 0-2.558-2.559h-5.117a2.559 2.559 0 0 0-2.559 2.559v52.024a30.575 30.575 0 0 1-19.325-6.846l-11.67 11.671a6.823 6.823 0 0 1-9.65-9.649L22.58 85.594a30.583 30.583 0 0 1-2.9-13.037v-3.411H7.882c-3.718 0-6.86-2.915-6.962-6.631A6.823 6.823 0 0 1 7.74 55.5h11.94V42.975l-9.942-9.942a6.823 6.823 0 0 1 9.65-9.65l11.647 11.648h48.93l11.648-11.647a6.823 6.823 0 1 1 9.649 9.65l-9.942 9.94V55.5h11.941a6.823 6.823 0 0 1 6.82 7.015ZM55.713.917c-13.188 0-23.88 10.691-23.88 23.88h47.76c0-13.189-10.69-23.88-23.88-23.88Z" />
|
||||
</svg>
|
||||
));
|
@ -0,0 +1,20 @@
|
||||
import React, { SVGProps, memo } from 'react';
|
||||
export const SvgIcnBurgerMenu = memo<SVGProps<SVGSVGElement>>(props => (
|
||||
<svg
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
role="img"
|
||||
{...props}
|
||||
>
|
||||
<path fill="none" d="M0 0h14v14H0z" />
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M11.5 10a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-9a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h9Zm0-4a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-9a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h9Zm0-4a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-9a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h9Z"
|
||||
fill=""
|
||||
/>
|
||||
</svg>
|
||||
));
|
@ -0,0 +1,13 @@
|
||||
import React, { SVGProps, memo } from 'react';
|
||||
export const SvgIcnCheckmark = memo<SVGProps<SVGSVGElement>>(props => (
|
||||
<svg
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 14 14"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
role="img"
|
||||
{...props}
|
||||
>
|
||||
<path d="m11.753 2 1.244 1.23-8.375 8.473L1 8.081l1.237-1.238L4.614 9.22 11.753 2Z" />
|
||||
</svg>
|
||||
));
|