: never
16 | } & {
17 | id: string
18 | }
19 |
20 | export const withId = (component: FC
) => {
21 | const Component = component as any
22 |
23 | return memo>((props: Record) => {
24 | const handlers: Record = {}
25 |
26 | for (const key in props) {
27 | const value = props[key]
28 |
29 | if (key.startsWith('on') && typeof value === 'function') {
30 | handlers[key] = (...args: any[]) => value(props.id, ...args)
31 | }
32 | }
33 |
34 | return
35 | })
36 | }
37 |
--------------------------------------------------------------------------------
/src/models/suite.ts:
--------------------------------------------------------------------------------
1 | import { bench } from 'lib/benchmark'
2 | import { sotore } from 'lib/sotore'
3 | import { compileTypeScript } from 'lib/tsCompiler'
4 | import { useHash } from 'lib/useHash'
5 | import { useEffect, useRef } from 'react'
6 | import { deserialize, serialize } from './suiteBase64'
7 | import { nanoid } from 'nanoid'
8 |
9 | export type Test = {
10 | id: string
11 | source: string
12 | hz?: number
13 | rme?: number
14 | abort?: () => void
15 | }
16 |
17 | export const suite = sotore({
18 | title: 'Unnamed suite',
19 | author: 'Anonymous',
20 | running: false,
21 | setup: '// Setup\n\n',
22 | tests: [
23 | {
24 | id: nanoid(),
25 | source: '// Test case\n\n',
26 | } as Test,
27 | ],
28 | teardown: '// Teardown\n\n',
29 | })
30 |
31 | const { lay, get, set, subscribe } = suite
32 |
33 | export const updateSetup = (setup: string) => {
34 | lay({ setup })
35 | }
36 |
37 | export const updateTearddown = (teardown: string) => {
38 | lay({ teardown })
39 | }
40 |
41 | export const updateTestValue = (id: string, source: string) => {
42 | const { tests } = get()
43 |
44 | lay({
45 | tests: tests.map((item) => {
46 | if (item.id !== id) {
47 | return item
48 | }
49 |
50 | return {
51 | ...item,
52 | hz: undefined,
53 | rme: undefined,
54 | source,
55 | }
56 | }),
57 | })
58 | }
59 |
60 | export const updateTest = (id: string, patch: Partial) => {
61 | const { tests } = get()
62 |
63 | lay({
64 | tests: tests.map((item) => {
65 | if (item.id !== id) {
66 | return item
67 | }
68 |
69 | return {
70 | ...item,
71 | hz: undefined,
72 | rme: undefined,
73 | ...patch,
74 | }
75 | }),
76 | })
77 | }
78 |
79 | export const addTest = () => {
80 | const { tests } = get()
81 |
82 | lay({
83 | tests: [
84 | {
85 | id: nanoid(),
86 | source: '// Test case\n\n',
87 | },
88 | ...tests,
89 | ],
90 | })
91 | }
92 |
93 | export const removeTest = (id: string) => {
94 | const { tests } = get()
95 |
96 | lay({
97 | tests: tests.filter((item) => item.id !== id),
98 | })
99 | }
100 |
101 | export const stopTest = (id: string) => {
102 | const { tests } = get()
103 | const test = tests.find((test) => test.id === id)
104 |
105 | if (test?.abort) {
106 | test.abort()
107 | }
108 | }
109 |
110 | export const stopAllTests = () => {
111 | const { tests } = get()
112 |
113 | lay({ running: false })
114 |
115 | for (const test of tests) {
116 | if (test.abort) {
117 | test.abort()
118 | }
119 | }
120 | }
121 |
122 | export const runTest = async (id: string) => {
123 | const { setup, tests, teardown } = get()
124 | const test = tests.find((test) => test.id === id)
125 |
126 | if (!test) {
127 | return
128 | }
129 |
130 | const controller = new AbortController()
131 |
132 | updateTest(id, {
133 | abort: () => controller.abort(),
134 | })
135 |
136 | const { hz, rme } = await bench({
137 | setup: compileTypeScript(setup),
138 | fn: compileTypeScript(test.source),
139 | teardown: compileTypeScript(teardown),
140 | signal: controller.signal,
141 | }).catch((e) => {
142 | return {
143 | hz: undefined,
144 | rme: undefined,
145 | }
146 | })
147 |
148 | updateTest(id, {
149 | hz,
150 | rme,
151 | abort: undefined,
152 | })
153 | }
154 |
155 | export const runAllTests = async () => {
156 | const { tests } = get()
157 |
158 | lay({ running: true })
159 |
160 | for (const test of tests) {
161 | if (get().running) {
162 | await runTest(test.id)
163 | }
164 | }
165 |
166 | lay({ running: false })
167 | }
168 |
169 | export const useHashSuite = () => {
170 | const changedRef = useRef()
171 |
172 | useHash(() => {
173 | const { hash } = location
174 |
175 | if (!changedRef.current) {
176 | const newState = deserialize(hash.slice(1))
177 | if (newState) set(newState)
178 | }
179 | changedRef.current = false
180 | })
181 |
182 | useEffect(() => {
183 | const setUrlFromState = () => {
184 | changedRef.current = true
185 | const url = new URL(location.toString())
186 | url.hash = '#' + serialize(get())
187 | location.replace(url)
188 | }
189 |
190 | setUrlFromState()
191 |
192 | return subscribe(setUrlFromState)
193 | }, [])
194 | }
195 |
--------------------------------------------------------------------------------
/src/models/suiteBase64.ts:
--------------------------------------------------------------------------------
1 | import { suite } from 'model/suite'
2 | import { compressToURI, decompressFromURI } from 'lz-ts'
3 | import { nanoid } from 'nanoid'
4 |
5 | type StoreData = {
6 | title: string
7 | author: string
8 | before: string
9 | tests: string[]
10 | after: string
11 | }
12 |
13 | const isStringsArray = (arr: any): arr is string[] => {
14 | return Array.isArray(arr) && arr.every((test) => typeof test === 'string')
15 | }
16 |
17 | export const deserialize = (
18 | content: string,
19 | ): ReturnType | null => {
20 | try {
21 | const data = JSON.parse(decompressFromURI(content))
22 |
23 | if (typeof data !== 'object') {
24 | return null
25 | }
26 |
27 | const { title, author, before, tests, after } = data
28 |
29 | if (!isStringsArray([title, author, before, after])) {
30 | return null
31 | }
32 |
33 | if (!isStringsArray(tests)) {
34 | return null
35 | }
36 |
37 | return {
38 | title,
39 | author,
40 | running: false,
41 | setup: before,
42 | teardown: after,
43 | tests: tests.map((source) => {
44 | return {
45 | id: nanoid(),
46 | source,
47 | running: false,
48 | }
49 | }),
50 | }
51 | } catch (e) {
52 | return null
53 | }
54 | }
55 |
56 | export const serialize = (content: ReturnType) => {
57 | const { title, author, setup: before, tests, teardown: after } = content
58 |
59 | let data: StoreData = {
60 | title,
61 | author,
62 | before,
63 | after,
64 | tests: tests.map((test) => test.source),
65 | }
66 |
67 | return compressToURI(JSON.stringify(data))
68 | }
69 |
--------------------------------------------------------------------------------
/src/pages/Landing.tsx:
--------------------------------------------------------------------------------
1 | import { Layout, Footer, Header } from 'lay/Main'
2 | import { FC } from 'react'
3 |
4 | export const Landing: FC = () => {
5 | return (
6 |
7 |
8 | Your
9 | benchmark
10 | tool.
11 |
12 |
13 |
14 |
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/src/pages/NotFound.tsx:
--------------------------------------------------------------------------------
1 | import { Layout, Footer, Header } from 'lay/Main'
2 | import { FC } from 'react'
3 |
4 | export const NotFound: FC = () => {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 | )
12 | }
13 |
--------------------------------------------------------------------------------
/src/pages/Suite.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react'
2 |
3 | import { Layout, Header, Content, Footer } from 'lay/Suite'
4 | import { useHashSuite } from 'model/suite'
5 |
6 | export const Suite: FC = () => {
7 | useHashSuite()
8 |
9 | return (
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/src/pages/UnderConstruction.tsx:
--------------------------------------------------------------------------------
1 | import { Layout, Footer, Header } from 'lay/Main'
2 | import { FC } from 'react'
3 |
4 | export const UnderConstruction: FC = () => {
5 | return (
6 |
7 |
8 | Under 🏗
9 | construction
10 |
11 |
12 |
13 |
14 | )
15 | }
16 |
--------------------------------------------------------------------------------
/src/ui/Button/Button.module.styl:
--------------------------------------------------------------------------------
1 |
2 | .button {
3 | display: flex
4 | align-items: center
5 | justify-content: center
6 | border: none
7 |
8 | cursor: pointer
9 | user-select: none
10 |
11 | text-decoration: none
12 | transition: 0.2s all
13 | }
14 |
15 | .small {
16 | padding: 8px
17 | border-radius: 8px
18 |
19 | .icon {
20 | width: 16px
21 | height: 16px
22 | }
23 |
24 | .text {
25 | font-weight: 700
26 | text-transform: uppercase
27 | margin: 0 4px
28 | font-size: 16px
29 | line-height: 1
30 | }
31 | }
32 |
33 | .medium {
34 | padding: 12px
35 | border-radius: 12px
36 |
37 | .icon {
38 | width: 18px
39 | height: 18px
40 | }
41 |
42 | .text {
43 | font-weight: 800
44 | text-transform: uppercase
45 | margin: 0 6px
46 | font-size: 18px
47 | line-height: 1
48 | }
49 | }
50 |
51 | .large {
52 | padding: 16px
53 | border-radius: 16px
54 |
55 | .icon {
56 | width: 24px
57 | height: 24px
58 | }
59 |
60 | .text {
61 | font-weight: 800
62 | text-transform: uppercase
63 | margin: 0 8px
64 | font-size: 24px
65 | line-height: 1
66 | }
67 | }
68 |
69 | .link {
70 | background: transparent
71 | }
72 |
73 | .secondary {
74 | color: var(--theme-secondary-text)
75 | background: var(--theme-secondary-fill)
76 |
77 | &:hover {
78 | color: var(--theme-secondary-text-hover)
79 | background: var(--theme-secondary-fill-hover)
80 | }
81 |
82 | &:active {
83 | color: var(--theme-secondary-text-active)
84 | background: var(--theme-secondary-fill-active)
85 | }
86 | }
87 |
88 | .outline {
89 | background: var(--theme-bg-fill)
90 | color: var(--theme-bg-text)
91 | box-shadow: 0 0 0 2px currentColor inset
92 |
93 | &:hover {
94 | color: var(--theme-bg-text-hover)
95 | background: var(--theme-bg-fill-hover)
96 | }
97 |
98 | &:active {
99 | color: var(--theme-bg-text-active)
100 | background: var(--theme-bg-fill-active)
101 | }
102 | }
103 |
104 | .primary {
105 | color: var(--theme-primary-text)
106 | background: var(--theme-primary-fill)
107 |
108 | &:hover {
109 | color: var(--theme-primary-text-hover)
110 | background: var(--theme-primary-fill-hover)
111 | }
112 |
113 | &:active {
114 | color: var(--theme-primary-text-active)
115 | background: var(--theme-primary-fill-active)
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/src/ui/Button/Button.tsx:
--------------------------------------------------------------------------------
1 | import { cnj } from 'cnj'
2 | import { ElementType, FC, MouseEvent } from 'react'
3 |
4 | import { Text } from 'ui/Text'
5 | import { Icon } from 'ui/Icon'
6 | import { Link } from 'ui/Link'
7 |
8 | import {
9 | $button,
10 | $text,
11 | $icon,
12 | $small,
13 | $medium,
14 | $large,
15 | $link,
16 | $secondary,
17 | $primary,
18 | $outline,
19 | } from './Button.module.styl'
20 |
21 | const sizes = {
22 | small: $small,
23 | medium: $medium,
24 | large: $large,
25 | }
26 |
27 | const types = {
28 | primary: $primary,
29 | secondary: $secondary,
30 | outline: $outline,
31 | link: $link,
32 | }
33 |
34 | type Props = {
35 | children?: string
36 | href?: string
37 | icon?: string
38 | size?: keyof typeof sizes
39 | type?: keyof typeof types
40 | onClick?(e: MouseEvent): void
41 | }
42 |
43 | export const Button: FC = (props) => {
44 | const { href, children, size, type, icon, onClick } = props
45 | let Tag = (href ? Link : 'button') as ElementType
46 |
47 | const className = cnj(
48 | $button,
49 | sizes[size ?? 'medium'],
50 | types[type ?? 'secondary'],
51 | )
52 |
53 | return (
54 |
55 | {icon && }
56 | {children && {children}}
57 |
58 | )
59 | }
60 |
--------------------------------------------------------------------------------
/src/ui/Button/index.ts:
--------------------------------------------------------------------------------
1 | export { Button } from './Button'
2 |
--------------------------------------------------------------------------------
/src/ui/ClotGroup/ClotGroup.module.styl:
--------------------------------------------------------------------------------
1 |
2 | $item-round = 4px
3 | $block-round = 12px
4 |
5 | .clot-group {
6 | display: flex
7 | gap: $item-round
8 |
9 | background: var(--theme-delimiter)
10 | box-shadow: 0 0 0 $item-round var(--theme-bg-fill) inset
11 |
12 | > * {
13 | border-radius: $item-round
14 | }
15 | }
16 |
17 | .row {
18 | > :first-child {
19 | border-top-left-radius: $block-round
20 | border-bottom-left-radius: $block-round
21 | }
22 |
23 | > :last-child {
24 | border-top-right-radius: $block-round
25 | border-bottom-right-radius: $block-round
26 | }
27 | }
28 |
29 | .column {
30 | flex-direction: column
31 |
32 | > :first-child {
33 | border-top-left-radius: $block-round
34 | border-top-right-radius: $block-round
35 | }
36 |
37 | > :last-child {
38 | border-bottom-left-radius: $block-round
39 | border-bottom-right-radius: $block-round
40 | }
41 | }
--------------------------------------------------------------------------------
/src/ui/ClotGroup/ClotGroup.tsx:
--------------------------------------------------------------------------------
1 | import cnj from 'cnj'
2 | import { ComponentProps, FC } from 'react'
3 |
4 | import { $clot_group, $row, $column } from './ClotGroup.module.styl'
5 |
6 | type Props = ComponentProps<'div'> & {
7 | column?: boolean
8 | }
9 |
10 | export const ClotGroup: FC = (props) => {
11 | const { children, column, className } = props
12 | const cn = cnj($clot_group, column ? $column : $row, className)
13 |
14 | return {children}
15 | }
16 |
--------------------------------------------------------------------------------
/src/ui/ClotGroup/index.ts:
--------------------------------------------------------------------------------
1 | export { ClotGroup } from './ClotGroup'
2 |
--------------------------------------------------------------------------------
/src/ui/CodeBlock/CodeBlock.module.styl:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | background: var(--theme-surface-fill)
3 | padding: 0 12px
4 | border-radius: 12px
5 | }
6 |
7 | .editor {
8 | position: relative
9 | user-select: none
10 | min-height: 78px
11 |
12 | & > :global(.monaco-editor) {
13 | position: absolute
14 | }
15 |
16 | :global(.current-line) {
17 | border-radius: 3px
18 | }
19 | }
--------------------------------------------------------------------------------
/src/ui/CodeBlock/CodeBlock.tsx:
--------------------------------------------------------------------------------
1 | import cnj from 'cnj'
2 | import { languages, editor } from 'monaco-editor'
3 | import { FC, useCallback, useEffect, useRef } from 'react'
4 | import { ExtendedMonaco, Monaco } from 'ui/Monaco'
5 |
6 | import TsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'
7 |
8 | import theme from './theme'
9 |
10 | import { $wrapper, $editor } from './CodeBlock.module.styl'
11 |
12 | self.MonacoEnvironment = {
13 | getWorker: () => new TsWorker(),
14 | }
15 |
16 | editor.defineTheme('monokai', theme)
17 |
18 | const config: editor.IStandaloneEditorConstructionOptions = {
19 | overviewRulerLanes: 0,
20 | fontSize: 14,
21 | lineHeight: 18 / 14,
22 | fontFamily: 'Fira Code',
23 | fontLigatures: true,
24 |
25 | contextmenu: false,
26 |
27 | minimap: {
28 | enabled: false,
29 | },
30 |
31 | stickyTabStops: true,
32 | tabSize: 2,
33 |
34 | lineNumbersMinChars: 1,
35 |
36 | lineDecorationsWidth: 12,
37 | showFoldingControls: 'never',
38 |
39 | padding: {
40 | top: 12,
41 | bottom: 12,
42 | },
43 |
44 | renderLineHighlightOnlyWhenFocus: true,
45 | cursorBlinking: 'phase',
46 |
47 | scrollBeyondLastLine: false,
48 |
49 | scrollbar: {
50 | alwaysConsumeMouseWheel: false,
51 | vertical: 'hidden',
52 | useShadows: false,
53 | },
54 |
55 | language: 'typescript',
56 | automaticLayout: true,
57 | theme: 'monokai',
58 | }
59 |
60 | languages.typescript.typescriptDefaults.setCompilerOptions({
61 | lib: ['webworker', 'esnext'],
62 | target: languages.typescript.ScriptTarget.Latest,
63 | allowNonTsExtensions: true,
64 | })
65 | languages.typescript.typescriptDefaults.setDiagnosticsOptions({
66 | // https://github.com/Microsoft/TypeScript/blob/main/src/compiler/diagnosticMessages.json
67 | diagnosticCodesToIgnore: [
68 | 1375, // 'await' expressions are only allowed at the top level of a file when that file is a module
69 | 1378, // Top-level 'await' expressions are only allowed when the 'module' option is set to 'esnext' or 'system', and the 'target' option is set to 'es2017' or higher
70 | ],
71 | })
72 |
73 | type Props = {
74 | onChange?(code: string): void
75 | value: string
76 | className?: string
77 | }
78 |
79 | export const CodeBlock: FC = (props) => {
80 | const { onChange, value, className } = props
81 | const innerRef = useRef()
82 |
83 | const editorRef = useCallback((editor: ExtendedMonaco) => {
84 | editor.setValue(value)
85 |
86 | editor.disableKeybinding('editor.action.quickCommand')
87 |
88 | innerRef.current = editor
89 | const container = editor.getContainerDomNode()
90 | container.addEventListener('touchstart', (e) => {
91 | e.stopPropagation()
92 | })
93 | editor.onDidContentSizeChange(() => {
94 | container.style.height = Math.max(editor.getContentHeight(), 78) + 'px'
95 | editor.layout()
96 | })
97 | editor.onDidBlurEditorText(() => {
98 | onChange?.(editor.getValue())
99 | })
100 | }, [])
101 |
102 | useEffect(() => {
103 | let { current } = innerRef
104 | if (current && current.getValue() !== value) {
105 | current.setValue(value)
106 | }
107 | }, [value])
108 |
109 | return (
110 |
111 |
112 |
113 | )
114 | }
115 |
--------------------------------------------------------------------------------
/src/ui/CodeBlock/index.ts:
--------------------------------------------------------------------------------
1 | export { CodeBlock } from './CodeBlock'
2 |
--------------------------------------------------------------------------------
/src/ui/CodeBlock/theme.ts:
--------------------------------------------------------------------------------
1 | import { editor } from 'monaco-editor'
2 |
3 | const theme: editor.IStandaloneThemeData = {
4 | base: 'vs-dark',
5 | inherit: true,
6 | rules: [
7 | {
8 | foreground: '75715e',
9 | token: 'comment',
10 | },
11 | {
12 | foreground: 'e6db74',
13 | token: 'string',
14 | },
15 | {
16 | foreground: 'ae81ff',
17 | token: 'constant.numeric',
18 | },
19 | {
20 | foreground: 'ae81ff',
21 | token: 'constant.language',
22 | },
23 | {
24 | foreground: 'ae81ff',
25 | token: 'constant.character',
26 | },
27 | {
28 | foreground: 'ae81ff',
29 | token: 'constant.other',
30 | },
31 | {
32 | foreground: 'f92672',
33 | token: 'keyword',
34 | },
35 | {
36 | foreground: 'f92672',
37 | token: 'storage',
38 | },
39 | {
40 | foreground: '66d9ef',
41 | fontStyle: 'italic',
42 | token: 'storage.type',
43 | },
44 | {
45 | foreground: 'a6e22e',
46 | fontStyle: 'underline',
47 | token: 'entity.name.class',
48 | },
49 | {
50 | foreground: 'a6e22e',
51 | fontStyle: 'italic underline',
52 | token: 'entity.other.inherited-class',
53 | },
54 | {
55 | foreground: 'a6e22e',
56 | token: 'entity.name.function',
57 | },
58 | {
59 | foreground: 'fd971f',
60 | fontStyle: 'italic',
61 | token: 'variable.parameter',
62 | },
63 | {
64 | foreground: 'f92672',
65 | token: 'entity.name.tag',
66 | },
67 | {
68 | foreground: 'a6e22e',
69 | token: 'entity.other.attribute-name',
70 | },
71 | {
72 | foreground: '66d9ef',
73 | token: 'support.function',
74 | },
75 | {
76 | foreground: '66d9ef',
77 | token: 'support.constant',
78 | },
79 | {
80 | foreground: '66d9ef',
81 | fontStyle: 'italic',
82 | token: 'support.type',
83 | },
84 | {
85 | foreground: '66d9ef',
86 | fontStyle: 'italic',
87 | token: 'support.class',
88 | },
89 | {
90 | foreground: 'f8f8f0',
91 | background: 'f92672',
92 | token: 'invalid',
93 | },
94 | {
95 | foreground: 'f8f8f0',
96 | background: 'ae81ff',
97 | token: 'invalid.deprecated',
98 | },
99 | {
100 | foreground: 'cfcfc2',
101 | token: 'meta.structure.dictionary.json string.quoted.double.json',
102 | },
103 | {
104 | foreground: '75715e',
105 | token: 'meta.diff',
106 | },
107 | {
108 | foreground: '75715e',
109 | token: 'meta.diff.header',
110 | },
111 | {
112 | foreground: 'f92672',
113 | token: 'markup.deleted',
114 | },
115 | {
116 | foreground: 'a6e22e',
117 | token: 'markup.inserted',
118 | },
119 | {
120 | foreground: 'e6db74',
121 | token: 'markup.changed',
122 | },
123 | {
124 | foreground: 'ae81ffa0',
125 | token: 'constant.numeric.line-number.find-in-files - match',
126 | },
127 | {
128 | foreground: 'e6db74',
129 | token: 'entity.name.filename.find-in-files',
130 | },
131 | ],
132 | colors: {
133 | 'editor.foreground': '#F8F8F2',
134 | 'editor.background': '#1f1f1f',
135 | 'editor.selectionBackground': '#49483E',
136 | 'editor.lineHighlightBackground': '#333333',
137 | 'editorCursor.foreground': '#F8F8F0',
138 | 'editorWhitespace.foreground': '#3B3A32',
139 | 'editorIndentGuide.activeBackground': '#9D550FB0',
140 | 'editor.selectionHighlightBorder': '#222218',
141 | },
142 | }
143 | export default theme
144 |
--------------------------------------------------------------------------------
/src/ui/Form/Context.ts:
--------------------------------------------------------------------------------
1 | import { Sotore } from 'lib/sotore'
2 | import { createContext } from 'react'
3 |
4 | export const Context = createContext>>(null!)
5 |
--------------------------------------------------------------------------------
/src/ui/Form/Form.tsx:
--------------------------------------------------------------------------------
1 | import { sotore } from 'lib/sotore'
2 | import { FC, FormEventHandler, ReactNode, useMemo } from 'react'
3 | import { Context } from './Context'
4 |
5 | type Props = {
6 | className?: string
7 | children: ReactNode
8 | values?: Record
9 | onSubmit(values: Record): void
10 | }
11 |
12 | export const Form: FC = (props) => {
13 | const { className, children, values, onSubmit } = props
14 |
15 | const store = useMemo(() => sotore(values ?? {}), [values])
16 |
17 | const handleSubmit: FormEventHandler = (e) => {
18 | e.preventDefault()
19 | onSubmit(store.get())
20 | }
21 |
22 | return (
23 |
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/src/ui/Form/FormTextField.module.styl:
--------------------------------------------------------------------------------
1 |
2 | .text-field {
3 | display: flex
4 | flex-direction: column
5 | gap: 8px
6 | }
7 |
--------------------------------------------------------------------------------
/src/ui/Form/FormTextField.tsx:
--------------------------------------------------------------------------------
1 | import { useFilter } from 'lib/sotore'
2 | import { FC, useCallback, useContext } from 'react'
3 | import { Text } from 'ui/Text'
4 | import { TextField } from 'ui/TextField'
5 | import { Context } from './Context'
6 |
7 | import { $text_field } from './FormTextField.module.styl'
8 |
9 | type Props = {
10 | name: string
11 | placeholder: string
12 | }
13 |
14 | export const FormTextField: FC = (props) => {
15 | const { name, placeholder } = props
16 |
17 | const store = useContext(Context)
18 | const [value] = useFilter(store, name)
19 |
20 | const handleChange = useCallback(
21 | (value: string) => {
22 | store.lay({ [name]: value })
23 | },
24 | [store],
25 | )
26 |
27 | return (
28 |
29 | {placeholder}
30 |
31 | {value}
32 |
33 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/src/ui/Form/index.ts:
--------------------------------------------------------------------------------
1 | export { Form } from './Form'
2 | export { FormTextField as TextField } from './FormTextField'
3 |
--------------------------------------------------------------------------------
/src/ui/Group/Group.module.styl:
--------------------------------------------------------------------------------
1 |
2 | .group {
3 | display: flex
4 | flex-wrap: wrap
5 | gap: 12px
6 |
7 | @media (max-width: 480px) {
8 | flex-direction: column
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/ui/Group/Group.tsx:
--------------------------------------------------------------------------------
1 | import cnj from 'cnj'
2 | import { ComponentProps, FC } from 'react'
3 |
4 | import { $group } from './Group.module.styl'
5 |
6 | type Props = ComponentProps<'div'>
7 |
8 | export const Group: FC = ({ children, className }) => {
9 | return {children}
10 | }
11 |
--------------------------------------------------------------------------------
/src/ui/Group/index.ts:
--------------------------------------------------------------------------------
1 | export { Group } from './Group'
2 |
--------------------------------------------------------------------------------
/src/ui/Icon/Icon.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react'
2 |
3 | type Props = {
4 | symbol: string
5 | className?: string
6 | }
7 |
8 | export const Icon: FC = (props) => {
9 | const { symbol, className } = props
10 |
11 | return (
12 |
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/src/ui/Icon/index.ts:
--------------------------------------------------------------------------------
1 | export { Icon } from './Icon'
2 |
--------------------------------------------------------------------------------
/src/ui/Link/Link.module.styl:
--------------------------------------------------------------------------------
1 | .link {
2 | color: currentColor
3 | cursor: pointer
4 | transition: 0.2s ease all
5 |
6 | &:hover {
7 | opacity: 0.75
8 | }
9 | }
10 |
11 | .current {
12 | color: var(--theme-primary-fill)
13 | text-decoration: none
14 | }
15 |
--------------------------------------------------------------------------------
/src/ui/Link/Link.tsx:
--------------------------------------------------------------------------------
1 | import cnj from 'cnj'
2 | import { FC, HTMLAttributeAnchorTarget, ReactNode, useMemo } from 'react'
3 | import { NavLink } from 'react-router-dom'
4 |
5 | import { $link, $current } from './Link.module.styl'
6 |
7 | type Props = {
8 | href: string
9 | target?: HTMLAttributeAnchorTarget
10 | children: ReactNode
11 | className?: string
12 | }
13 |
14 | export const Link: FC = ({ href, target, children, className }) => {
15 | const localHref = useMemo(() => {
16 | try {
17 | if (typeof location === 'undefined') {
18 | return false
19 | }
20 |
21 | const url = new URL(href)
22 | return url.hostname === location.hostname ? url.pathname : false
23 | } catch (e) {
24 | return href
25 | }
26 | }, [href])
27 |
28 | return localHref ? (
29 | {
33 | return cnj(className, $link, isActive && $current)
34 | }}
35 | >
36 | {children}
37 |
38 | ) : (
39 |
40 | {children}
41 |
42 | )
43 | }
44 |
--------------------------------------------------------------------------------
/src/ui/Link/index.ts:
--------------------------------------------------------------------------------
1 | export { Link } from './Link'
2 |
--------------------------------------------------------------------------------
/src/ui/Modal/Modal.module.styl:
--------------------------------------------------------------------------------
1 |
2 | .modal {
3 | background: #00000033
4 | position: fixed
5 | display: flex
6 | left: 0
7 | right: 0
8 | top: 0
9 | bottom: 0
10 | backdrop-filter: blur(8px)
11 | -webkit-backdrop-filter: blur(8px)
12 | z-index: 10000
13 | }
14 |
15 | .block {
16 | display: flex
17 | margin: auto
18 | flex-direction: column
19 | max-width: 1100px
20 | max-height: 80%
21 | overflow: hidden
22 | transition: inherit
23 | transition-timing-function: cubic-bezier(0.25, 3, 0.5, 2)
24 | }
25 |
26 | .fade {
27 | opacity: 0
28 |
29 | & > .block {
30 | transform: scale(0.95)
31 | transition-timing-function: ease
32 | }
33 | }
--------------------------------------------------------------------------------
/src/ui/Modal/Modal.tsx:
--------------------------------------------------------------------------------
1 | import { Mote, withMote } from 'lib/mote'
2 | import { MouseEvent, ReactNode } from 'react'
3 |
4 | import { $modal, $fade, $block } from './Modal.module.styl'
5 |
6 | type Props = {
7 | children: ReactNode
8 | visible?: boolean
9 | onDismiss?(visible: false): void
10 | }
11 |
12 | export const Modal = withMote((props) => {
13 | const { children, visible, onDismiss } = props
14 |
15 | const handleClick = (e: MouseEvent) => {
16 | console.log(e)
17 | if (onDismiss && e.currentTarget === e.target) {
18 | onDismiss(false)
19 | }
20 | }
21 |
22 | return (
23 |
34 | {children}
35 |
36 | )
37 | }, 'modal')
38 |
--------------------------------------------------------------------------------
/src/ui/Modal/index.ts:
--------------------------------------------------------------------------------
1 | export { Modal } from './Modal'
2 |
--------------------------------------------------------------------------------
/src/ui/Monaco/Monaco.tsx:
--------------------------------------------------------------------------------
1 | import { editor } from 'monaco-editor'
2 | import { forwardRef, useEffect, useRef } from 'react'
3 |
4 | type Props = {
5 | config: editor.IStandaloneEditorConstructionOptions
6 | className?: string
7 | }
8 |
9 | function extendEditor(editor: editor.IStandaloneCodeEditor) {
10 | return Object.assign(editor, {
11 | disableKeybinding(action: string) {
12 | ;(editor as any)._standaloneKeybindingService.addDynamicKeybinding(
13 | `-${action}`,
14 | undefined,
15 | () => {},
16 | )
17 | },
18 | })
19 | }
20 |
21 | export type ExtendedMonaco = ReturnType
22 |
23 | export const Monaco = forwardRef(
24 | (props, ref) => {
25 | const { config, className } = props
26 |
27 | const editorRef = useRef()
28 | const nodeRef = useRef(null)
29 |
30 | useEffect(() => {
31 | editorRef.current?.updateOptions(config)
32 |
33 | setTimeout(() => {
34 | if (!editorRef.current && nodeRef.current) {
35 | const monaco = extendEditor(editor.create(nodeRef.current, config))
36 | editorRef.current = monaco
37 | if (typeof ref === 'function') {
38 | ref(monaco)
39 | } else if (ref) {
40 | ref.current = monaco
41 | }
42 | }
43 | })
44 |
45 | return () => {
46 | editorRef.current?.dispose()
47 | delete editorRef.current
48 | }
49 | }, [config])
50 |
51 | return
52 | },
53 | )
54 |
--------------------------------------------------------------------------------
/src/ui/Monaco/index.ts:
--------------------------------------------------------------------------------
1 | export { Monaco } from './Monaco'
2 | export type { ExtendedMonaco } from './Monaco'
3 |
--------------------------------------------------------------------------------
/src/ui/TestCase/TestCase.module.styl:
--------------------------------------------------------------------------------
1 |
2 | .item {
3 | position: relative
4 | display: grid
5 |
6 | background: var(--theme-surface-fill)
7 |
8 | grid-template-areas: 'indicator stats controls' 'indicator code controls'
9 | grid-template-rows: 22px 1fr
10 | grid-template-columns: 28px 1fr auto
11 | }
12 |
13 |
14 | .drag_indicator {
15 | grid-area: indicator
16 | display: flex
17 | justify-content: center
18 | align-items: center
19 | flex-direction: column
20 | gap: 12px
21 | padding: 12px
22 | position: relative
23 | z-index: 2
24 | cursor: grab
25 |
26 | .drag_handle {
27 | color: var(--theme-bg-text_secondary)
28 | height: 12px
29 | }
30 |
31 | &::after {
32 | content: ''
33 | display: block
34 | width: 4px
35 | flex: 1
36 |
37 | background: var(--theme-bg-fill)
38 | border-radius: 2px
39 | }
40 | }
41 |
42 | .stats {
43 | grid-area: stats
44 | display: flex
45 | justify-content: space-between
46 | position: relative
47 | padding-top: 12px
48 |
49 | z-index: 1
50 |
51 | > * {
52 | line-height: 10px
53 | }
54 | }
55 |
56 | .running {
57 | color: var(--theme-orange)
58 | }
59 |
60 | .fastest {
61 | color: var(--theme-green)
62 | }
63 |
64 | .slower {
65 | color: var(--theme-red)
66 | }
67 |
68 | .notTested {
69 | color: var(--theme-bg-text_secondary)
70 | }
71 |
72 | .ops {
73 | position: relative
74 | padding: 12px
75 |
76 | z-index: 1
77 | line-height: 10px
78 | }
79 |
80 | .code {
81 | grid-area: code
82 | margin: 0 -12px
83 | }
84 |
85 | .controls {
86 | grid-area: controls
87 |
88 | align-self: flex-end
89 |
90 | display: flex
91 | flex-direction: column
92 | gap: 12px
93 |
94 | padding: 12px
95 | }
96 |
--------------------------------------------------------------------------------
/src/ui/TestCase/TestCase.strings.ts:
--------------------------------------------------------------------------------
1 | import { State } from './TestCase'
2 |
3 | export const state = (state: State) => {
4 | switch (state) {
5 | case 'running':
6 | return 'Running'
7 | case 'fastest':
8 | return 'Fastest'
9 | case 'slower':
10 | return 'Slower'
11 | }
12 |
13 | return 'Not tested'
14 | }
15 |
16 | export const number = (value: number) => {
17 | return value.toLocaleString(undefined, {
18 | minimumFractionDigits: 2,
19 | maximumFractionDigits: 2,
20 | })
21 | }
22 |
--------------------------------------------------------------------------------
/src/ui/TestCase/TestCase.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react'
2 | import { Button } from 'ui/Button'
3 | import { CodeBlock } from 'ui/CodeBlock'
4 | import { Icon } from 'ui/Icon'
5 | import { Text } from 'ui/Text'
6 | import Drag from 'icon/Drag.svg'
7 | import Stop from 'icon/Stop.svg'
8 | import Start from 'icon/Start.svg'
9 | import Remove from 'icon/Remove.svg'
10 |
11 | import * as strings from './TestCase.strings'
12 |
13 | import {
14 | $item,
15 | $drag_indicator,
16 | $drag_handle,
17 | $stats,
18 | $fastest,
19 | $slower,
20 | $running,
21 | $notTested,
22 | $controls,
23 | $code,
24 | } from './TestCase.module.styl'
25 |
26 | const stateMap = {
27 | running: $running,
28 | fastest: $fastest,
29 | slower: $slower,
30 | notTested: $notTested,
31 | }
32 |
33 | export type State = keyof typeof stateMap
34 |
35 | type Props = {
36 | source: string
37 | state: State
38 | running?: boolean
39 | hz?: number
40 | rme?: number
41 | onChange?(value: string): void
42 | onRun?(): void
43 | onStop?(): void
44 | onRemove?(): void
45 | }
46 |
47 | export const TestCase: FC = (props) => {
48 | const {
49 | source,
50 | state,
51 | hz,
52 | rme,
53 | running,
54 | onChange,
55 | onRun,
56 | onStop,
57 | onRemove,
58 | } = props
59 |
60 | return (
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | {strings.state(state)}
69 |
70 | {typeof hz === 'number' && typeof rme === 'number' && (
71 |
72 | {strings.number(hz)} ops/s ± {strings.number(rme)}%
73 |
74 | )}
75 |
76 |
77 |
78 |
79 |
80 |
86 |
87 |
88 |
89 | )
90 | }
91 |
--------------------------------------------------------------------------------
/src/ui/TestCase/index.ts:
--------------------------------------------------------------------------------
1 | export { TestCase } from './TestCase'
2 | export type { State } from './TestCase'
3 |
--------------------------------------------------------------------------------
/src/ui/TestCases/TestCases.tsx:
--------------------------------------------------------------------------------
1 | import { withId } from 'lib/withId'
2 | import { Test } from 'model/suite'
3 | import { lazy, useMemo } from 'react'
4 | import { ClotGroup } from 'ui/ClotGroup'
5 | import { State } from 'ui/TestCase'
6 |
7 | type Props = {
8 | onChange?(id: string, value: string): void
9 | onRun?(id: string): void
10 | onRemove?(id: string): void
11 | onStop?(id: string): void
12 | tests: Test[]
13 | className?: string
14 | }
15 |
16 | const WrappedTestCase = withId(
17 | lazy(async () => ({ default: (await import('ui/TestCase')).TestCase })),
18 | )
19 |
20 | export const TestCases = (props: Props) => {
21 | const { tests, className, onChange, onRemove, onRun, onStop } = props
22 |
23 | const fastest = useMemo(() => {
24 | let fastest: Test | undefined
25 |
26 | for (let test of tests) {
27 | if (typeof test.hz === 'number') {
28 | if (test.hz > (fastest?.hz ?? 0)) {
29 | fastest = test
30 | }
31 | }
32 | }
33 |
34 | return fastest
35 | }, [tests])
36 |
37 | return (
38 |
39 | {tests.map((test) => {
40 | let state: State = 'notTested'
41 |
42 | if (test.abort) {
43 | state = 'running'
44 | } else if (typeof test.hz === 'number') {
45 | state = fastest === test ? 'fastest' : 'slower'
46 | }
47 |
48 | return (
49 |
59 | )
60 | })}
61 |
62 | )
63 | }
64 |
--------------------------------------------------------------------------------
/src/ui/TestCases/index.ts:
--------------------------------------------------------------------------------
1 | export { TestCases } from './TestCases'
2 |
--------------------------------------------------------------------------------
/src/ui/Text/Text.module.styl:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;1,300;1,400;1,500;1,600;1,700;1,800&display=swap')
2 |
3 | .text {
4 | font-family: 'Open Sans', sans-serif
5 | font-size: 16px
6 | line-height: 1
7 | }
8 |
9 | .bold {
10 | font-weight: 600
11 | }
12 |
13 | .uppercase {
14 | text-transform: uppercase
15 | }
16 |
17 | .error {
18 | composes uppercase bold
19 | color: var(--theme-red)
20 | }
21 |
22 | .secondary {
23 | color: var(--theme-bg-text_secondary)
24 | }
--------------------------------------------------------------------------------
/src/ui/Text/Text.tsx:
--------------------------------------------------------------------------------
1 | import { cnj } from 'cnj'
2 | import { FC, ReactNode } from 'react'
3 | import {
4 | $text,
5 | $bold,
6 | $uppercase,
7 | $error,
8 | $secondary,
9 | } from './Text.module.styl'
10 |
11 | const types = {
12 | error: $error,
13 | secondary: $secondary,
14 | }
15 |
16 | type Props = {
17 | bold?: boolean
18 | uppercase?: boolean
19 | type?: keyof typeof types
20 | children: ReactNode
21 | className?: string
22 | }
23 |
24 | export const Text: FC = (props) => {
25 | const { children, className, bold, uppercase, type } = props
26 |
27 | const cn = cnj(
28 | $text,
29 | bold && $bold,
30 | uppercase && $uppercase,
31 | type && types[type],
32 | className,
33 | )
34 |
35 | return {children}
36 | }
37 |
--------------------------------------------------------------------------------
/src/ui/Text/Title.module.styl:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Fira+Code:wght@300;400;500;600;700&display=swap');
2 | @import url('https://fonts.googleapis.com/css2?family=Raleway:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap')
3 |
4 | .title {
5 | font-family: 'Raleway', sans-serif
6 | margin: 0
7 | }
8 |
9 | h1.title {
10 | font-size: 72px
11 | line-height: 64px
12 | font-weight: 800
13 | }
14 |
15 | h2.title {
16 | font-size: 52px
17 | line-height: 48px
18 | font-weight: 700
19 | }
20 |
21 | h3.title {
22 | font-size: 36px
23 | line-height: 32px
24 | font-weight: 600
25 | }
26 |
--------------------------------------------------------------------------------
/src/ui/Text/Title.tsx:
--------------------------------------------------------------------------------
1 | import { FC, ReactNode } from 'react'
2 |
3 | import { $title } from './Title.module.styl'
4 |
5 | const sizes = {
6 | large: 'h1',
7 | medium: 'h2',
8 | small: 'h3',
9 | } as const
10 |
11 | type Props = {
12 | size: keyof typeof sizes
13 | children: ReactNode
14 | }
15 |
16 | export const Title: FC = (props) => {
17 | const { size, children } = props
18 | const Tag = sizes[size]
19 |
20 | return {children}
21 | }
22 |
--------------------------------------------------------------------------------
/src/ui/Text/index.ts:
--------------------------------------------------------------------------------
1 | export { Text } from './Text'
2 | export { Title } from './Title'
3 |
--------------------------------------------------------------------------------
/src/ui/TextField/TextField.module.styl:
--------------------------------------------------------------------------------
1 |
2 | .text-field {
3 | display: inline-grid
4 | align-items: center
5 | }
6 |
7 | .text-field::after {
8 | content: attr(data-value) " "
9 | min-width: 0
10 | visibility: hidden
11 | }
12 |
13 | .input, .text-field::after {
14 | height: 32px
15 | padding: 8px
16 | font: inherit
17 | white-space: pre
18 | grid-area: 1/1
19 | }
20 |
21 | .input {
22 | width: 100%
23 | border: none
24 | border-radius: 8px
25 | }
26 |
--------------------------------------------------------------------------------
/src/ui/TextField/TextField.tsx:
--------------------------------------------------------------------------------
1 | import cnj from 'cnj'
2 | import { ChangeEvent, ComponentProps, forwardRef } from 'react'
3 |
4 | import { $text } from 'ui/Text/Text.module.styl'
5 | import { $text_field, $input } from './TextField.module.styl'
6 |
7 | export const TextField = forwardRef((props, ref) => {
8 | const { children, onChange, className, ...rest } = props
9 |
10 | const handleChange = (e: ChangeEvent) => {
11 | onChange(e.target.value)
12 | }
13 |
14 | return (
15 |
16 |
24 |
25 | )
26 | })
27 |
28 | interface Props extends Omit, 'value' | 'onChange'> {
29 | onChange(value: string): void
30 | children: string
31 | className?: string
32 | }
33 |
--------------------------------------------------------------------------------
/src/ui/TextField/index.ts:
--------------------------------------------------------------------------------
1 | export { TextField } from './TextField'
2 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["ES2022", "DOM"],
4 | "baseUrl": "./",
5 | "jsx": "react-jsx",
6 | "module": "ESNext",
7 | "target": "ES2020",
8 | "moduleResolution": "node",
9 | "allowJs": true,
10 | "strict": true,
11 | "sourceMap": true,
12 | "paths": {
13 | "ui/*": ["./src/ui/*"],
14 | "icon/*": ["./src/icons/*"],
15 | "page/*": ["./src/pages/*"],
16 | "model/*": ["./src/models/*"],
17 | "lay/*": ["./src/layout/*"],
18 | "lib/*": ["./src/lib/*"],
19 | "sotore": ["./src/global/sotore"]
20 | },
21 | "outDir": "./dist",
22 | "allowSyntheticDefaultImports": true,
23 | "resolveJsonModule": true
24 | },
25 | "ts-node": {
26 | "esm": true
27 | },
28 | "files": ["declare.d.ts"],
29 | "include": ["src"]
30 | }
31 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import tsAlias from 'vite-plugin-ts-alias'
3 | import react from '@vitejs/plugin-react'
4 | import createSvgSpritePlugin from 'vite-plugin-svg-sprite'
5 | import { dirname, join } from 'node:path'
6 |
7 | const root = dirname(new URL(import.meta.url).pathname)
8 |
9 | export default defineConfig({
10 | root: './src',
11 | build: {
12 | outDir: '../dist',
13 | assetsDir: './',
14 | emptyOutDir: true,
15 | rollupOptions: {
16 | input: {
17 | main: join(root, 'src/index.html'),
18 | notfound: join(root, 'src/404.html'),
19 | },
20 | },
21 | },
22 | server: {
23 | host: '0.0.0.0',
24 | port: 80,
25 | },
26 | css: {
27 | modules: {
28 | localsConvention: ((name: string) => {
29 | return '$' + name.replace(/-/g, '_')
30 | }) as any,
31 | },
32 | },
33 | plugins: [
34 | tsAlias(),
35 | react(),
36 | createSvgSpritePlugin({
37 | symbolId: 'icon-[name]-[hash]',
38 | }),
39 | ],
40 | })
41 |
--------------------------------------------------------------------------------