mirror of
https://github.com/enso-org/enso.git
synced 2024-12-22 22:21:40 +03:00
parent
6665c22eb9
commit
7c3e316239
@ -30,14 +30,13 @@ const NAME = 'enso'
|
|||||||
* `yargs` is a modules we explicitly want the default imports of.
|
* `yargs` is a modules we explicitly want the default imports of.
|
||||||
* `node:process` is here because `process.on` does not exist on the namespace import. */
|
* `node:process` is here because `process.on` does not exist on the namespace import. */
|
||||||
const DEFAULT_IMPORT_ONLY_MODULES =
|
const DEFAULT_IMPORT_ONLY_MODULES =
|
||||||
'@vitejs\\u002Fplugin-react|node:process|chalk|string-length|yargs|yargs\\u002Fyargs|sharp|to-ico|connect|morgan|serve-static|create-servers|electron-is-dev|fast-glob|esbuild-plugin-.+|opener|tailwindcss.*|enso-assets.*|@modyfi\\u002Fvite-plugin-yaml|is-network-error|validator.+'
|
'@vitejs\\u002Fplugin-react|node:process|chalk|string-length|yargs|yargs\\u002Fyargs|sharp|to-ico|connect|morgan|serve-static|tiny-invariant|clsx|create-servers|electron-is-dev|fast-glob|esbuild-plugin-.+|opener|tailwindcss.*|enso-assets.*|@modyfi\\u002Fvite-plugin-yaml|is-network-error|validator.+'
|
||||||
const OUR_MODULES = 'enso-.*'
|
const OUR_MODULES = 'enso-.*'
|
||||||
const RELATIVE_MODULES =
|
const RELATIVE_MODULES =
|
||||||
'bin\\u002Fproject-manager|bin\\u002Fserver|config\\u002Fparser|authentication|config|debug|detect|file-associations|index|ipc|log|naming|paths|preload|project-management|security|url-associations|#\\u002F.*'
|
'bin\\u002Fproject-manager|bin\\u002Fserver|config\\u002Fparser|authentication|config|debug|detect|file-associations|index|ipc|log|naming|paths|preload|project-management|security|url-associations|#\\u002F.*'
|
||||||
const ALLOWED_DEFAULT_IMPORT_MODULES = `${DEFAULT_IMPORT_ONLY_MODULES}|postcss|ajv\\u002Fdist\\u002F2020|${RELATIVE_MODULES}`
|
const ALLOWED_DEFAULT_IMPORT_MODULES = `${DEFAULT_IMPORT_ONLY_MODULES}|postcss|ajv\\u002Fdist\\u002F2020|${RELATIVE_MODULES}`
|
||||||
const STRING_LITERAL = ':matches(Literal[raw=/^["\']/], TemplateLiteral)'
|
const STRING_LITERAL = ':matches(Literal[raw=/^["\']/], TemplateLiteral)'
|
||||||
const JSX = ':matches(JSXElement, JSXFragment)'
|
const JSX = ':matches(JSXElement, JSXFragment)'
|
||||||
const NOT_PASCAL_CASE = '/^(?!do[A-Z])(?!_?([A-Z][a-z0-9]*)+$)/'
|
|
||||||
const NOT_CAMEL_CASE = '/^(?!_?[a-z][a-z0-9*]*([A-Z0-9][a-z0-9]*)*$)(?!React$)/'
|
const NOT_CAMEL_CASE = '/^(?!_?[a-z][a-z0-9*]*([A-Z0-9][a-z0-9]*)*$)(?!React$)/'
|
||||||
const WHITELISTED_CONSTANTS = 'logger|.+Context|interpolationFunction.+'
|
const WHITELISTED_CONSTANTS = 'logger|.+Context|interpolationFunction.+'
|
||||||
const NOT_CONSTANT_CASE = `/^(?!${WHITELISTED_CONSTANTS}$|_?[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$)/`
|
const NOT_CONSTANT_CASE = `/^(?!${WHITELISTED_CONSTANTS}$|_?[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$)/`
|
||||||
@ -117,11 +116,6 @@ const RESTRICTED_SYNTAXES = [
|
|||||||
message:
|
message:
|
||||||
'No aliases to primitives - consider using brands instead: `string & { _brand: "BrandName"; }`',
|
'No aliases to primitives - consider using brands instead: `string & { _brand: "BrandName"; }`',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
// Matches functions and arrow functions, but not methods.
|
|
||||||
selector: `:matches(FunctionDeclaration[id.name=${NOT_PASCAL_CASE}]:has(${JSX}), VariableDeclarator[id.name=${NOT_PASCAL_CASE}]:has(:matches(ArrowFunctionExpression.init ${JSX})))`,
|
|
||||||
message: 'Use `PascalCase` for React components',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
// Matches other functions, non-consts, and consts not at the top level.
|
// Matches other functions, non-consts, and consts not at the top level.
|
||||||
selector: `:matches(FunctionDeclaration[id.name=${NOT_CAMEL_CASE}]:not(:has(${JSX})), VariableDeclarator[id.name=${NOT_CAMEL_CASE}]:has(ArrowFunctionExpression.init:not(:has(${JSX}))), :matches(VariableDeclaration[kind^=const], Program :not(ExportNamedDeclaration, TSModuleBlock) > VariableDeclaration[kind=const], ExportNamedDeclaration > * VariableDeclaration[kind=const]) > VariableDeclarator[id.name=${NOT_CAMEL_CASE}]:not(:has(ArrowFunctionExpression)))`,
|
selector: `:matches(FunctionDeclaration[id.name=${NOT_CAMEL_CASE}]:not(:has(${JSX})), VariableDeclarator[id.name=${NOT_CAMEL_CASE}]:has(ArrowFunctionExpression.init:not(:has(${JSX}))), :matches(VariableDeclaration[kind^=const], Program :not(ExportNamedDeclaration, TSModuleBlock) > VariableDeclaration[kind=const], ExportNamedDeclaration > * VariableDeclaration[kind=const]) > VariableDeclarator[id.name=${NOT_CAMEL_CASE}]:not(:has(ArrowFunctionExpression)))`,
|
||||||
@ -228,11 +222,6 @@ const RESTRICTED_SYNTAXES = [
|
|||||||
selector: 'CallExpression[callee.name=toastAndLog][arguments.0.value=/\\.$/]',
|
selector: 'CallExpression[callee.name=toastAndLog][arguments.0.value=/\\.$/]',
|
||||||
message: '`toastAndLog` already includes a trailing `.`',
|
message: '`toastAndLog` already includes a trailing `.`',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
selector:
|
|
||||||
'JSXElement[closingElement!=null]:not(:has(.children:matches(JSXText[raw=/\\S/], :not(JSXText))))',
|
|
||||||
message: 'Use self-closing tags (`<tag />`) for tags without children',
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
// ============================
|
// ============================
|
||||||
@ -275,7 +264,7 @@ export default [
|
|||||||
...tsEslint.configs.recommended?.rules,
|
...tsEslint.configs.recommended?.rules,
|
||||||
...tsEslint.configs['recommended-requiring-type-checking']?.rules,
|
...tsEslint.configs['recommended-requiring-type-checking']?.rules,
|
||||||
...tsEslint.configs.strict?.rules,
|
...tsEslint.configs.strict?.rules,
|
||||||
...react.configs.recommended.rules,
|
...react.configs['jsx-runtime'].rules,
|
||||||
eqeqeq: ['error', 'always', { null: 'never' }],
|
eqeqeq: ['error', 'always', { null: 'never' }],
|
||||||
'jsdoc/require-jsdoc': [
|
'jsdoc/require-jsdoc': [
|
||||||
'error',
|
'error',
|
||||||
@ -303,8 +292,10 @@ export default [
|
|||||||
'prefer-const': 'error',
|
'prefer-const': 'error',
|
||||||
// Not relevant because TypeScript checks types.
|
// Not relevant because TypeScript checks types.
|
||||||
'react/prop-types': 'off',
|
'react/prop-types': 'off',
|
||||||
|
'react/self-closing-comp': 'error',
|
||||||
'react-hooks/rules-of-hooks': 'error',
|
'react-hooks/rules-of-hooks': 'error',
|
||||||
'react-hooks/exhaustive-deps': 'error',
|
'react-hooks/exhaustive-deps': 'error',
|
||||||
|
'react/jsx-pascal-case': ['error', { allowNamespace: true }],
|
||||||
// Prefer `interface` over `type`.
|
// Prefer `interface` over `type`.
|
||||||
'@typescript-eslint/consistent-type-definitions': 'error',
|
'@typescript-eslint/consistent-type-definitions': 'error',
|
||||||
'@typescript-eslint/consistent-type-imports': 'error',
|
'@typescript-eslint/consistent-type-imports': 'error',
|
||||||
|
6
app/ide-desktop/lib/assets/dismiss.svg
Normal file
6
app/ide-desktop/lib/assets/dismiss.svg
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g>
|
||||||
|
<rect x="16.2426" y="17.6569" width="14" height="2" transform="rotate(-135 16.2426 17.6569)" fill="white" />
|
||||||
|
<rect x="6.34302" y="16.2426" width="14" height="2" transform="rotate(-45 6.34302 16.2426)" fill="white" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 354 B |
@ -43,7 +43,14 @@
|
|||||||
"react-router-dom": "^6.8.1",
|
"react-router-dom": "^6.8.1",
|
||||||
"react-toastify": "^9.1.3",
|
"react-toastify": "^9.1.3",
|
||||||
"ts-results": "^3.3.0",
|
"ts-results": "^3.3.0",
|
||||||
"validator": "^13.11.0"
|
"validator": "^13.11.0",
|
||||||
|
"monaco-editor": "0.47.0",
|
||||||
|
"@monaco-editor/react": "4.6.0",
|
||||||
|
"@tanstack/react-query": "^5.27.5",
|
||||||
|
"clsx": "^1.1.1",
|
||||||
|
"tiny-invariant": "^1.3.3",
|
||||||
|
"tailwind-merge": "^2.2.1",
|
||||||
|
"react-aria-components": "^1.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/plugin-syntax-import-assertions": "^7.23.3",
|
"@babel/plugin-syntax-import-assertions": "^7.23.3",
|
||||||
@ -76,6 +83,7 @@
|
|||||||
"prettier-plugin-tailwindcss": "^0.5.11",
|
"prettier-plugin-tailwindcss": "^0.5.11",
|
||||||
"react-toastify": "^9.1.3",
|
"react-toastify": "^9.1.3",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
|
"tailwindcss-react-aria-components": "^1.1.1",
|
||||||
"ts-plugin-namespace-auto-import": "^1.0.0",
|
"ts-plugin-namespace-auto-import": "^1.0.0",
|
||||||
"typescript": "~5.2.2",
|
"typescript": "~5.2.2",
|
||||||
"vite": "^4.4.9",
|
"vite": "^4.4.9",
|
||||||
|
@ -35,6 +35,7 @@
|
|||||||
* {@link authProvider.FullUserSession}). */
|
* {@link authProvider.FullUserSession}). */
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
|
import * as reactQuery from '@tanstack/react-query'
|
||||||
import * as router from 'react-router-dom'
|
import * as router from 'react-router-dom'
|
||||||
import * as toastify from 'react-toastify'
|
import * as toastify from 'react-toastify'
|
||||||
|
|
||||||
@ -63,6 +64,8 @@ import SetUsername from '#/pages/authentication/SetUsername'
|
|||||||
import Dashboard from '#/pages/dashboard/Dashboard'
|
import Dashboard from '#/pages/dashboard/Dashboard'
|
||||||
import Subscribe from '#/pages/subscribe/Subscribe'
|
import Subscribe from '#/pages/subscribe/Subscribe'
|
||||||
|
|
||||||
|
import * as rootComponent from '#/components/Root'
|
||||||
|
|
||||||
import type Backend from '#/services/Backend'
|
import type Backend from '#/services/Backend'
|
||||||
import LocalBackend from '#/services/LocalBackend'
|
import LocalBackend from '#/services/LocalBackend'
|
||||||
|
|
||||||
@ -141,11 +144,12 @@ export default function App(props: AppProps) {
|
|||||||
// This is a React component even though it does not contain JSX.
|
// This is a React component even though it does not contain JSX.
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
const Router = detect.isOnElectron() ? router.HashRouter : router.BrowserRouter
|
const Router = detect.isOnElectron() ? router.HashRouter : router.BrowserRouter
|
||||||
|
const queryClient = React.useMemo(() => new reactQuery.QueryClient(), [])
|
||||||
// Both `BackendProvider` and `InputBindingsProvider` depend on `LocalStorageProvider`.
|
// Both `BackendProvider` and `InputBindingsProvider` depend on `LocalStorageProvider`.
|
||||||
// Note that the `Router` must be the parent of the `AuthProvider`, because the `AuthProvider`
|
// Note that the `Router` must be the parent of the `AuthProvider`, because the `AuthProvider`
|
||||||
// will redirect the user between the login/register pages and the dashboard.
|
// will redirect the user between the login/register pages and the dashboard.
|
||||||
return (
|
return (
|
||||||
<>
|
<reactQuery.QueryClientProvider client={queryClient}>
|
||||||
<toastify.ToastContainer
|
<toastify.ToastContainer
|
||||||
position="top-center"
|
position="top-center"
|
||||||
theme="light"
|
theme="light"
|
||||||
@ -160,7 +164,7 @@ export default function App(props: AppProps) {
|
|||||||
<AppRouter {...props} />
|
<AppRouter {...props} />
|
||||||
</LocalStorageProvider>
|
</LocalStorageProvider>
|
||||||
</Router>
|
</Router>
|
||||||
</>
|
</reactQuery.QueryClientProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -186,6 +190,9 @@ function AppRouter(props: AppProps) {
|
|||||||
window.navigate = navigate
|
window.navigate = navigate
|
||||||
}
|
}
|
||||||
const [inputBindingsRaw] = React.useState(() => inputBindingsModule.createBindings())
|
const [inputBindingsRaw] = React.useState(() => inputBindingsModule.createBindings())
|
||||||
|
const [root] = React.useState<React.RefObject<HTMLElement>>(() => ({
|
||||||
|
current: document.getElementById('enso-dashboard'),
|
||||||
|
}))
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const savedInputBindings = localStorage.get('inputBindings')
|
const savedInputBindings = localStorage.get('inputBindings')
|
||||||
@ -274,7 +281,11 @@ function AppRouter(props: AppProps) {
|
|||||||
isClick = true
|
isClick = true
|
||||||
}
|
}
|
||||||
const onMouseUp = (event: MouseEvent) => {
|
const onMouseUp = (event: MouseEvent) => {
|
||||||
if (isClick && !eventModule.isElementTextInput(event.target)) {
|
if (
|
||||||
|
isClick &&
|
||||||
|
!eventModule.isElementTextInput(event.target) &&
|
||||||
|
!eventModule.isElementPartOfMonaco(event.target)
|
||||||
|
) {
|
||||||
const selection = document.getSelection()
|
const selection = document.getSelection()
|
||||||
const app = document.getElementById('app')
|
const app = document.getElementById('app')
|
||||||
const appContainsSelection =
|
const appContainsSelection =
|
||||||
@ -359,5 +370,10 @@ function AppRouter(props: AppProps) {
|
|||||||
</SessionProvider>
|
</SessionProvider>
|
||||||
)
|
)
|
||||||
result = <LoggerProvider logger={logger}>{result}</LoggerProvider>
|
result = <LoggerProvider logger={logger}>{result}</LoggerProvider>
|
||||||
|
result = (
|
||||||
|
<rootComponent.Root rootRef={root} navigate={navigate}>
|
||||||
|
{result}
|
||||||
|
</rootComponent.Root>
|
||||||
|
)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,67 @@
|
|||||||
|
/**
|
||||||
|
* @file Button.tsx
|
||||||
|
*
|
||||||
|
* Button component
|
||||||
|
*/
|
||||||
|
import clsx from 'clsx'
|
||||||
|
import * as reactAriaComponents from 'react-aria-components'
|
||||||
|
import * as tailwindMerge from 'tailwind-merge'
|
||||||
|
|
||||||
|
import SvgMask from '#/components/SvgMask'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Props for the Button component
|
||||||
|
*/
|
||||||
|
export interface ButtonProps extends reactAriaComponents.ButtonProps {
|
||||||
|
readonly variant: 'icon'
|
||||||
|
readonly icon?: string
|
||||||
|
/**
|
||||||
|
* FIXME: This is not yet implemented
|
||||||
|
* The position of the icon in the button
|
||||||
|
* @default 'start'
|
||||||
|
*/
|
||||||
|
readonly iconPosition?: 'end' | 'start'
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_CLASSES =
|
||||||
|
'flex cursor-pointer rounded-sm border border-transparent transition-opacity duration-200 ease-in-out'
|
||||||
|
const FOCUS_CLASSES =
|
||||||
|
'focus-visible:outline-offset-2 focus:outline-none focus-visible:outline focus-visible:outline-primary'
|
||||||
|
const ICON_CLASSES = 'opacity-50 hover:opacity-100'
|
||||||
|
const EXTRA_CLICK_ZONE_CLASSES = 'flex relative before:inset-[-12px] before:absolute before:z-10'
|
||||||
|
const DISABLED_CLASSES = 'disabled:opacity-50 disabled:cursor-not-allowed'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A button allows a user to perform an action, with mouse, touch, and keyboard interactions.
|
||||||
|
*/
|
||||||
|
export function Button(props: ButtonProps) {
|
||||||
|
const { className, children, icon, ...ariaButtonProps } = props
|
||||||
|
|
||||||
|
const classes = clsx(DEFAULT_CLASSES, DISABLED_CLASSES, FOCUS_CLASSES, ICON_CLASSES)
|
||||||
|
|
||||||
|
const childrenFactory = () => {
|
||||||
|
return icon != null ? (
|
||||||
|
<>
|
||||||
|
<div className={EXTRA_CLICK_ZONE_CLASSES}>
|
||||||
|
<SvgMask src={icon} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
children
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<reactAriaComponents.Button
|
||||||
|
className={values =>
|
||||||
|
tailwindMerge.twMerge(
|
||||||
|
classes,
|
||||||
|
typeof className === 'function' ? className(values) : className
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{...ariaButtonProps}
|
||||||
|
>
|
||||||
|
{childrenFactory()}
|
||||||
|
</reactAriaComponents.Button>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,86 @@
|
|||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* A dialog is an overlay shown above other content in an application.
|
||||||
|
* Can be used to display alerts, confirmations, or other content.
|
||||||
|
*/
|
||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
import * as reactAriaComponents from 'react-aria-components'
|
||||||
|
import * as tailwindMerge from 'tailwind-merge'
|
||||||
|
|
||||||
|
import Dismiss from 'enso-assets/dismiss.svg'
|
||||||
|
|
||||||
|
import * as ariaComponents from '#/components/AriaComponents'
|
||||||
|
import * as portal from '#/components/Portal'
|
||||||
|
|
||||||
|
import type * as types from './types'
|
||||||
|
|
||||||
|
const MODAL_CLASSES =
|
||||||
|
'fixed z-1 top-0 left-0 right-0 bottom-0 bg-black/[15%] flex items-center justify-center text-center'
|
||||||
|
const DIALOG_CLASSES =
|
||||||
|
'relative flex flex-col overflow-hidden rounded-xl text-left align-middle text-slate-700 shadow-2xl bg-clip-padding border border-black/10 before:absolute before:inset before:h-full before:w-full before:rounded-default before:bg-selected-frame before:backdrop-blur-default'
|
||||||
|
|
||||||
|
const MODAL_CLASSES_BY_TYPE = {
|
||||||
|
modal: 'p-4',
|
||||||
|
popover: '',
|
||||||
|
fullscreen: 'p-4',
|
||||||
|
} satisfies Record<types.DialogType, string>
|
||||||
|
|
||||||
|
const DIALOG_CLASSES_BY_TYPE = {
|
||||||
|
modal: 'w-full max-w-md min-h-[200px] h-[90vh] max-h-[90vh]',
|
||||||
|
popover: 'rounded-lg',
|
||||||
|
fullscreen: 'w-full h-full max-w-full max-h-full bg-clip-border',
|
||||||
|
} satisfies Record<types.DialogType, string>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A dialog is an overlay shown above other content in an application.
|
||||||
|
* Can be used to display alerts, confirmations, or other content.
|
||||||
|
*/
|
||||||
|
export function Dialog(props: types.DialogProps) {
|
||||||
|
const {
|
||||||
|
children,
|
||||||
|
title,
|
||||||
|
type = 'modal',
|
||||||
|
isDismissible = true,
|
||||||
|
isKeyboardDismissDisabled = false,
|
||||||
|
className,
|
||||||
|
...ariaDialogProps
|
||||||
|
} = props
|
||||||
|
|
||||||
|
const root = portal.useStrictPortalContext()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<reactAriaComponents.Modal
|
||||||
|
className={tailwindMerge.twMerge(MODAL_CLASSES, [MODAL_CLASSES_BY_TYPE[type]])}
|
||||||
|
isDismissable={isDismissible}
|
||||||
|
isKeyboardDismissDisabled={isKeyboardDismissDisabled}
|
||||||
|
UNSTABLE_portalContainer={root.current}
|
||||||
|
>
|
||||||
|
<reactAriaComponents.Dialog
|
||||||
|
className={tailwindMerge.twMerge(DIALOG_CLASSES, [DIALOG_CLASSES_BY_TYPE[type]], className)}
|
||||||
|
{...ariaDialogProps}
|
||||||
|
>
|
||||||
|
{opts => (
|
||||||
|
<>
|
||||||
|
{typeof title === 'string' && (
|
||||||
|
<reactAriaComponents.Header className="center sticky flex flex-none border-b px-3.5 py-2.5 text-primary shadow">
|
||||||
|
<h2 className="text-l my-0 font-semibold leading-6">{title}</h2>
|
||||||
|
|
||||||
|
<ariaComponents.Button
|
||||||
|
variant="icon"
|
||||||
|
className="my-auto ml-auto"
|
||||||
|
onPress={opts.close}
|
||||||
|
icon={Dismiss}
|
||||||
|
/>
|
||||||
|
</reactAriaComponents.Header>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex-1 shrink-0">
|
||||||
|
{typeof children === 'function' ? children(opts) : children}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</reactAriaComponents.Dialog>
|
||||||
|
</reactAriaComponents.Modal>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
/**
|
||||||
|
* @file
|
||||||
|
*
|
||||||
|
* A DialogTrigger opens a dialog when a trigger element is pressed.
|
||||||
|
*/
|
||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
import * as reactAriaComponents from 'react-aria-components'
|
||||||
|
|
||||||
|
import * as modalProvider from '#/providers/ModalProvider'
|
||||||
|
|
||||||
|
import type * as types from './types'
|
||||||
|
|
||||||
|
const PLACEHOLDER = <div />
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A DialogTrigger opens a dialog when a trigger element is pressed.
|
||||||
|
*/
|
||||||
|
export function DialogTrigger(props: types.DialogTriggerProps) {
|
||||||
|
const { children, onOpenChange, ...triggerProps } = props
|
||||||
|
|
||||||
|
const { setModal, unsetModal } = modalProvider.useSetModal()
|
||||||
|
|
||||||
|
const onOpenChangeInternal = React.useCallback(
|
||||||
|
(isOpened: boolean) => {
|
||||||
|
if (isOpened) {
|
||||||
|
// we're using a placeholder here just to let the rest of the code know that the modal is open
|
||||||
|
setModal(PLACEHOLDER)
|
||||||
|
} else {
|
||||||
|
unsetModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
onOpenChange?.(isOpened)
|
||||||
|
},
|
||||||
|
[setModal, unsetModal, onOpenChange]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<reactAriaComponents.DialogTrigger
|
||||||
|
children={children}
|
||||||
|
onOpenChange={onOpenChangeInternal}
|
||||||
|
{...triggerProps}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* @file
|
||||||
|
*
|
||||||
|
* Re-exports the Dialog component.
|
||||||
|
*/
|
||||||
|
export * from './Dialog'
|
||||||
|
export * from './types'
|
||||||
|
export * from './DialogTrigger'
|
@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Contains the types for the Dialog component.
|
||||||
|
*/
|
||||||
|
import type * as reactAriaComponents from 'react-aria-components'
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type DialogType = 'fullscreen' | 'modal' | 'popover'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The props for the Dialog component.
|
||||||
|
*/
|
||||||
|
export interface DialogProps extends reactAriaComponents.DialogProps {
|
||||||
|
/**
|
||||||
|
* The type of dialog to render.
|
||||||
|
* @default 'modal'
|
||||||
|
*/
|
||||||
|
readonly type?: DialogType
|
||||||
|
readonly title?: string
|
||||||
|
readonly isDismissible?: boolean
|
||||||
|
readonly onOpenChange?: (isOpen: boolean) => void
|
||||||
|
readonly isKeyboardDismissDisabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The props for the DialogTrigger component.
|
||||||
|
*/
|
||||||
|
export interface DialogTriggerProps extends reactAriaComponents.DialogTriggerProps {}
|
@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* @file
|
||||||
|
*
|
||||||
|
* A tooltip displays a description of an element on hover or focus.
|
||||||
|
*/
|
||||||
|
import * as reactAriaComponents from 'react-aria-components'
|
||||||
|
import * as tailwindMerge from 'tailwind-merge'
|
||||||
|
|
||||||
|
import * as portal from '#/components/Portal'
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export interface TooltipProps
|
||||||
|
extends Omit<reactAriaComponents.TooltipProps, 'offset' | 'UNSTABLE_portalContainer'> {}
|
||||||
|
|
||||||
|
const DEFAULT_CLASSES = 'z-1 flex bg-neutral-800 text-white p-2 rounded-md shadow-lg text-xs'
|
||||||
|
|
||||||
|
const DEFAULT_CONTAINER_PADDING = 4
|
||||||
|
const DEFAULT_OFFSET = 4
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A tooltip displays a description of an element on hover or focus.
|
||||||
|
*/
|
||||||
|
export function Tooltip(props: TooltipProps) {
|
||||||
|
const { className, containerPadding = DEFAULT_CONTAINER_PADDING, ...ariaTooltipProps } = props
|
||||||
|
|
||||||
|
const root = portal.useStrictPortalContext()
|
||||||
|
|
||||||
|
const classes = tailwindMerge.twJoin(DEFAULT_CLASSES)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<reactAriaComponents.Tooltip
|
||||||
|
offset={DEFAULT_OFFSET}
|
||||||
|
containerPadding={containerPadding}
|
||||||
|
UNSTABLE_portalContainer={root.current}
|
||||||
|
className={values =>
|
||||||
|
tailwindMerge.twMerge(
|
||||||
|
classes,
|
||||||
|
typeof className === 'function' ? className(values) : className
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{...ariaTooltipProps}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-export the TooltipTrigger component from react-aria-components
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
export { TooltipTrigger } from 'react-aria-components'
|
@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* @file index.ts
|
||||||
|
* Index file for Aria Components
|
||||||
|
*/
|
||||||
|
export * from './Button/Button'
|
||||||
|
export * from './Tooltip/Tooltip'
|
||||||
|
export * from './Dialog'
|
@ -58,7 +58,8 @@ export default function MenuEntry(props: MenuEntryProps) {
|
|||||||
<button
|
<button
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
title={title}
|
title={title}
|
||||||
className={`flex h-row place-content-between items-center rounded-menu-entry p-menu-entry text-left selectable hover:bg-hover-bg enabled:active disabled:bg-transparent ${
|
className={`items -center flex h-row
|
||||||
|
place-content-between rounded-menu-entry p-menu-entry text-left selectable enabled:active hover:bg-hover-bg disabled:bg-transparent ${
|
||||||
isContextMenuEntry ? 'px-context-menu-entry-x' : ''
|
isContextMenuEntry ? 'px-context-menu-entry-x' : ''
|
||||||
}`}
|
}`}
|
||||||
onClick={event => {
|
onClick={event => {
|
||||||
|
@ -0,0 +1,44 @@
|
|||||||
|
/**
|
||||||
|
* @file Portal component
|
||||||
|
* Renders its children outside the current DOM hierarchy
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
import * as reactDom from 'react-dom'
|
||||||
|
|
||||||
|
import type * as types from './types'
|
||||||
|
import * as usePortal from './usePortal'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component renders its children outside the current DOM hierarchy.
|
||||||
|
*
|
||||||
|
* React [doesn't support](https://github.com/facebook/react/issues/13097) portal API in SSR, so, if you want to
|
||||||
|
* render a Portal in SSR, use prop `disabled`.
|
||||||
|
*
|
||||||
|
* By default, Portal's children render under the `<Root />` component.
|
||||||
|
*
|
||||||
|
* ***Important***: Since React doesn't support portals on SSR, `<Portal />` children render in the next tick.
|
||||||
|
* If you need to make some computations, use the `onMount` callback
|
||||||
|
* @see https://reactjs.org/docs/portals.html
|
||||||
|
* @example ```jsx
|
||||||
|
* <div>
|
||||||
|
* Portal will be rendered outside me!
|
||||||
|
*
|
||||||
|
* <Portal>
|
||||||
|
* <div>some content will be showed outside of parent container</div>
|
||||||
|
* </Portal>
|
||||||
|
* </div>
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export default function Portal(props: types.PortalProps): React.JSX.Element | null {
|
||||||
|
const { children, mountRoot, isDisabled } = usePortal.usePortal(props)
|
||||||
|
|
||||||
|
if (isDisabled) {
|
||||||
|
return <>{children}</>
|
||||||
|
} else if (mountRoot) {
|
||||||
|
return reactDom.createPortal(children, mountRoot)
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Provides the context for the Portal component
|
||||||
|
*/
|
||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
import invariant from 'tiny-invariant'
|
||||||
|
|
||||||
|
const PortalContext = React.createContext<React.RefObject<Element | null> | null>(null)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows to access the root element for the Portal component
|
||||||
|
*/
|
||||||
|
export function usePortalContext() {
|
||||||
|
const root = React.useContext(PortalContext)
|
||||||
|
|
||||||
|
return { root } as const
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows to access the root element for the Portal component
|
||||||
|
* @throws invariant the `PortalProvider` is not in the component tree
|
||||||
|
*/
|
||||||
|
export function useStrictPortalContext() {
|
||||||
|
const root = React.useContext(PortalContext)
|
||||||
|
|
||||||
|
invariant(
|
||||||
|
root != null && root.current != null,
|
||||||
|
'You should use `PortalProvider` to access the `Portal` component'
|
||||||
|
)
|
||||||
|
|
||||||
|
// This is safe because we are using `invariant` to check if the `PortalProvider` is in the component tree
|
||||||
|
// and the root element is not `null`
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
return root as { current: Element }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies the root element for the Portal component
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
export const PortalProvider = PortalContext.Provider
|
12
app/ide-desktop/lib/dashboard/src/components/Portal/index.ts
Normal file
12
app/ide-desktop/lib/dashboard/src/components/Portal/index.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* @file
|
||||||
|
*
|
||||||
|
* Re-exports the Portal component and its related types and hooks.
|
||||||
|
*/
|
||||||
|
import Portal from './Portal'
|
||||||
|
|
||||||
|
export * from './PortalProvider'
|
||||||
|
export * from './types'
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
export default Portal
|
27
app/ide-desktop/lib/dashboard/src/components/Portal/types.ts
Normal file
27
app/ide-desktop/lib/dashboard/src/components/Portal/types.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Types for the Portal component
|
||||||
|
*/
|
||||||
|
import type * as React from 'react'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The props for the Portal component
|
||||||
|
*/
|
||||||
|
export interface PortalProps {
|
||||||
|
/**
|
||||||
|
* Ref, where `<Portal />` should render its children
|
||||||
|
* By default it renders under `<Root />`
|
||||||
|
* @default null
|
||||||
|
*/
|
||||||
|
readonly root?: React.MutableRefObject<HTMLElement | null> | React.RefObject<HTMLElement | null>
|
||||||
|
/**
|
||||||
|
* Disables portal's API
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
readonly isDisabled?: boolean
|
||||||
|
/**
|
||||||
|
* Callback, will be called after portal's children mounted
|
||||||
|
*/
|
||||||
|
readonly onMount?: () => void
|
||||||
|
readonly children?: React.ReactNode
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* The hook contains the logic for mounting the children into the portal.
|
||||||
|
*/
|
||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
import invariant from 'tiny-invariant'
|
||||||
|
|
||||||
|
import * as portalProvider from './PortalProvider'
|
||||||
|
import type * as types from './types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The hook contains the logic for mounting the children into the portal.
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export function usePortal(props: types.PortalProps) {
|
||||||
|
const { children, isDisabled = false, root = null, onMount } = props
|
||||||
|
|
||||||
|
const onMountRef = React.useRef(onMount)
|
||||||
|
const portalContext = portalProvider.usePortalContext()
|
||||||
|
const [mountRoot, setMountRoot] = React.useState<Element | null>(null)
|
||||||
|
onMountRef.current = onMount
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!isDisabled) {
|
||||||
|
const contextRoot = portalContext.root
|
||||||
|
const currentRoot = root?.current ?? null
|
||||||
|
const currentContextRoot = contextRoot?.current ?? null
|
||||||
|
|
||||||
|
invariant(
|
||||||
|
!(contextRoot == null && currentRoot == null),
|
||||||
|
'Before using Portal, you need to specify a root, where the component should be mounted or put the component under the <Root /> component'
|
||||||
|
)
|
||||||
|
|
||||||
|
setMountRoot(currentRoot ?? currentContextRoot)
|
||||||
|
}
|
||||||
|
}, [root, portalContext.root, isDisabled])
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (isDisabled || mountRoot) {
|
||||||
|
onMountRef.current?.()
|
||||||
|
}
|
||||||
|
}, [isDisabled, mountRoot])
|
||||||
|
|
||||||
|
return {
|
||||||
|
isDisabled,
|
||||||
|
children,
|
||||||
|
mountRoot,
|
||||||
|
}
|
||||||
|
}
|
35
app/ide-desktop/lib/dashboard/src/components/Root.tsx
Normal file
35
app/ide-desktop/lib/dashboard/src/components/Root.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* The root component with required providers
|
||||||
|
*/
|
||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
import * as reactAriaComponents from 'react-aria-components'
|
||||||
|
|
||||||
|
import * as portal from '#/components/Portal'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Props for the root component
|
||||||
|
*/
|
||||||
|
export interface RootProps extends React.PropsWithChildren {
|
||||||
|
readonly rootRef: React.RefObject<HTMLElement>
|
||||||
|
readonly navigate: (path: string) => void
|
||||||
|
readonly locale?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The root component with required providers
|
||||||
|
*/
|
||||||
|
export function Root(props: RootProps) {
|
||||||
|
const { children, rootRef, navigate, locale = 'en-US' } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<portal.PortalProvider value={rootRef}>
|
||||||
|
<reactAriaComponents.RouterProvider navigate={navigate}>
|
||||||
|
<reactAriaComponents.I18nProvider locale={locale}>
|
||||||
|
{children}
|
||||||
|
</reactAriaComponents.I18nProvider>
|
||||||
|
</reactAriaComponents.RouterProvider>
|
||||||
|
</portal.PortalProvider>
|
||||||
|
)
|
||||||
|
}
|
@ -1,12 +1,12 @@
|
|||||||
/** @file A selection brush to indicate the area being selected by the mouse drag action. */
|
/** @file A selection brush to indicate the area being selected by the mouse drag action. */
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
import * as reactDom from 'react-dom'
|
|
||||||
|
|
||||||
import * as animationHooks from '#/hooks/animationHooks'
|
import * as animationHooks from '#/hooks/animationHooks'
|
||||||
|
|
||||||
import * as modalProvider from '#/providers/ModalProvider'
|
import * as modalProvider from '#/providers/ModalProvider'
|
||||||
|
|
||||||
|
import Portal from '#/components/Portal'
|
||||||
|
|
||||||
import * as eventModule from '#/utilities/event'
|
import * as eventModule from '#/utilities/event'
|
||||||
import type * as geometry from '#/utilities/geometry'
|
import type * as geometry from '#/utilities/geometry'
|
||||||
|
|
||||||
@ -169,15 +169,14 @@ export default function SelectionBrush(props: SelectionBrushProps) {
|
|||||||
width: `${rectangle.width}px`,
|
width: `${rectangle.width}px`,
|
||||||
height: `${rectangle.height}px`,
|
height: `${rectangle.height}px`,
|
||||||
}
|
}
|
||||||
|
return (
|
||||||
return reactDom.createPortal(
|
<Portal>
|
||||||
<div
|
<div
|
||||||
className={`pointer-events-none fixed z-1 box-content rounded-selection-brush border-transparent bg-selection-brush transition-border-margin ${
|
className={`pointer-events-none fixed z-1 box-content rounded-selection-brush border-transparent bg-selection-brush transition-border-margin ${
|
||||||
hidden ? 'm border-0' : '-m-selection-brush-border border-selection-brush'
|
hidden ? 'm border-0' : '-m-selection-brush-border border-selection-brush'
|
||||||
}`}
|
}`}
|
||||||
style={brushStyle}
|
style={brushStyle}
|
||||||
/>,
|
/>
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
</Portal>
|
||||||
document.getElementById('enso-dashboard')!
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ export default function SubmitButton(props: SubmitButtonProps) {
|
|||||||
<button
|
<button
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
type="submit"
|
type="submit"
|
||||||
className={`flex items-center justify-center gap-icon-with-text rounded-full bg-blue-600 py-auth-input-y text-white transition-all duration-auth selectable hover:bg-blue-700 focus:bg-blue-700 focus:outline-none enabled:active`}
|
className={`flex items-center justify-center gap-icon-with-text rounded-full bg-blue-600 py-auth-input-y text-white transition-all duration-auth selectable enabled:active hover:bg-blue-700 focus:bg-blue-700 focus:outline-none`}
|
||||||
>
|
>
|
||||||
{text}
|
{text}
|
||||||
<SvgMask src={icon} />
|
<SvgMask src={icon} />
|
||||||
|
@ -0,0 +1,74 @@
|
|||||||
|
/**
|
||||||
|
* @file
|
||||||
|
*
|
||||||
|
* Diff view for 2 asset versions for a specific project
|
||||||
|
*/
|
||||||
|
import * as react from '@monaco-editor/react'
|
||||||
|
|
||||||
|
import Spinner, * as spinnerModule from '#/components/Spinner'
|
||||||
|
|
||||||
|
import type * as backendService from '#/services/Backend'
|
||||||
|
import type RemoteBackend from '#/services/RemoteBackend'
|
||||||
|
|
||||||
|
import * as useFetchVersionContent from './useFetchVersionContent'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Props for the AssetDiffView component
|
||||||
|
*/
|
||||||
|
export interface AssetDiffViewProps {
|
||||||
|
readonly versionId: string
|
||||||
|
readonly latestVersionId: string
|
||||||
|
readonly projectId: backendService.ProjectId
|
||||||
|
readonly backend: RemoteBackend
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Diff view for asset versions
|
||||||
|
*/
|
||||||
|
export function AssetDiffView(props: AssetDiffViewProps) {
|
||||||
|
const { versionId, projectId, backend, latestVersionId } = props
|
||||||
|
|
||||||
|
const versionContent = useFetchVersionContent.useFetchVersionContent({
|
||||||
|
versionId,
|
||||||
|
projectId,
|
||||||
|
backend,
|
||||||
|
})
|
||||||
|
const headContent = useFetchVersionContent.useFetchVersionContent({
|
||||||
|
versionId: latestVersionId,
|
||||||
|
projectId,
|
||||||
|
backend,
|
||||||
|
})
|
||||||
|
|
||||||
|
const loader = (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<Spinner size={32} state={spinnerModule.SpinnerState.loadingMedium} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
if (versionContent.isError || headContent.isError) {
|
||||||
|
return <div className="p-indent-8 text-center">Failed to load content</div>
|
||||||
|
} else if (versionContent.isPending || headContent.isPending) {
|
||||||
|
return loader
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<react.DiffEditor
|
||||||
|
beforeMount={monaco => {
|
||||||
|
monaco.editor.defineTheme('myTheme', {
|
||||||
|
base: 'vs',
|
||||||
|
inherit: true,
|
||||||
|
rules: [],
|
||||||
|
// This comes from third-party code and we can't change it
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
colors: { 'editor.background': '#00000000' },
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
original={versionContent.data}
|
||||||
|
modified={headContent.data}
|
||||||
|
language="enso"
|
||||||
|
options={{ readOnly: true }}
|
||||||
|
loading={loader}
|
||||||
|
theme={'myTheme'}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Re-exports the AssetDiffView component.
|
||||||
|
*/
|
||||||
|
export * from './AssetDiffView'
|
@ -0,0 +1,55 @@
|
|||||||
|
/**
|
||||||
|
* @file
|
||||||
|
*
|
||||||
|
* Fetches the content of a project’s Main.enso file with specified version.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as reactQuery from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import type * as backendService from '#/services/Backend'
|
||||||
|
import type RemoteBackend from '#/services/RemoteBackend'
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export interface FetchVersionContentProps {
|
||||||
|
readonly projectId: backendService.ProjectId
|
||||||
|
readonly versionId: string
|
||||||
|
readonly backend: RemoteBackend
|
||||||
|
readonly omitMetadataFromContent?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const MS_IN_SECOND = 1000
|
||||||
|
const HUNDRED = 100
|
||||||
|
const HUNDRED_SECONDS = HUNDRED * MS_IN_SECOND
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the content of a version.
|
||||||
|
*/
|
||||||
|
export function useFetchVersionContent(params: FetchVersionContentProps) {
|
||||||
|
const { versionId, backend, projectId, omitMetadataFromContent = true } = params
|
||||||
|
|
||||||
|
return reactQuery.useQuery({
|
||||||
|
queryKey: ['versionContent', versionId],
|
||||||
|
queryFn: () => backend.getFileContent(projectId, versionId),
|
||||||
|
select: data => (omitMetadataFromContent ? omitMetadata(data) : data),
|
||||||
|
staleTime: HUNDRED_SECONDS,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the metadata from the content of a version.
|
||||||
|
*/
|
||||||
|
function omitMetadata(file: string): string {
|
||||||
|
let [withoutMetadata] = file.split('#### METADATA ####')
|
||||||
|
|
||||||
|
if (withoutMetadata == null) {
|
||||||
|
return file
|
||||||
|
} else {
|
||||||
|
while (withoutMetadata[withoutMetadata.length - 1] === '\n') {
|
||||||
|
withoutMetadata = withoutMetadata.slice(0, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return withoutMetadata
|
||||||
|
}
|
||||||
|
}
|
@ -6,7 +6,7 @@ import * as localStorageProvider from '#/providers/LocalStorageProvider'
|
|||||||
import type * as assetEvent from '#/events/assetEvent'
|
import type * as assetEvent from '#/events/assetEvent'
|
||||||
|
|
||||||
import AssetProperties from '#/layouts/AssetProperties'
|
import AssetProperties from '#/layouts/AssetProperties'
|
||||||
import AssetVersions from '#/layouts/AssetVersions'
|
import AssetVersions from '#/layouts/AssetVersions/AssetVersions'
|
||||||
import type Category from '#/layouts/CategorySwitcher/Category'
|
import type Category from '#/layouts/CategorySwitcher/Category'
|
||||||
|
|
||||||
import * as backend from '#/services/Backend'
|
import * as backend from '#/services/Backend'
|
||||||
@ -139,7 +139,7 @@ export default function AssetPanel(props: AssetPanelProps) {
|
|||||||
dispatchAssetEvent={dispatchAssetEvent}
|
dispatchAssetEvent={dispatchAssetEvent}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<AssetVersions hidden={tab !== AssetPanelTab.versions} item={item} />
|
{tab === AssetPanelTab.versions && <AssetVersions item={item} />}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,29 +1,71 @@
|
|||||||
/** @file Displays information describing a specific version of an asset. */
|
/** @file Displays information describing a specific version of an asset. */
|
||||||
import * as React from 'react'
|
import Duplicate from 'enso-assets/duplicate.svg'
|
||||||
|
|
||||||
import type * as backend from '#/services/Backend'
|
import * as ariaComponents from '#/components/AriaComponents'
|
||||||
|
|
||||||
|
import * as backendService from '#/services/Backend'
|
||||||
|
import type RemoteBackend from '#/services/RemoteBackend'
|
||||||
|
|
||||||
import * as dateTime from '#/utilities/dateTime'
|
import * as dateTime from '#/utilities/dateTime'
|
||||||
|
|
||||||
|
import * as assetDiffView from './AssetDiffView'
|
||||||
|
|
||||||
// ====================
|
// ====================
|
||||||
// === AssetVersion ===
|
// === AssetVersion ===
|
||||||
// ====================
|
// ====================
|
||||||
|
|
||||||
/** Props for a {@link AssetVersion}. */
|
/** Props for a {@link AssetVersion}. */
|
||||||
export interface AssetVersionProps {
|
export interface AssetVersionProps {
|
||||||
|
readonly item: backendService.AnyAsset
|
||||||
readonly number: number
|
readonly number: number
|
||||||
readonly version: backend.S3ObjectVersion
|
readonly version: backendService.S3ObjectVersion
|
||||||
|
readonly latestVersion: backendService.S3ObjectVersion
|
||||||
|
readonly backend: RemoteBackend
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Displays information describing a specific version of an asset. */
|
/** Displays information describing a specific version of an asset. */
|
||||||
export default function AssetVersion(props: AssetVersionProps) {
|
export default function AssetVersion(props: AssetVersionProps) {
|
||||||
const { number, version } = props
|
const { number, version, item, backend, latestVersion } = props
|
||||||
|
|
||||||
|
const isProject = item.type === backendService.AssetType.project
|
||||||
|
const versionName = `Version ${number}`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex cursor-pointer select-none flex-col overflow-y-auto rounded-default p-version transition-colors hover:bg-frame">
|
<div className="flex w-full flex-shrink-0 basis-0 select-none flex-row gap-4 rounded-2xl p-2">
|
||||||
<div>version {number}</div>
|
<div className="flex flex-1 flex-col">
|
||||||
<div className="text-xs text-not-selected">
|
<div>
|
||||||
|
{versionName} {version.isLatest && `(Latest)`}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<time className="text-xs text-not-selected">
|
||||||
on {dateTime.formatDateTime(new Date(version.lastModified))}
|
on {dateTime.formatDateTime(new Date(version.lastModified))}
|
||||||
|
</time>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{isProject && (
|
||||||
|
<ariaComponents.DialogTrigger>
|
||||||
|
<ariaComponents.TooltipTrigger>
|
||||||
|
<ariaComponents.Button
|
||||||
|
variant="icon"
|
||||||
|
aria-label="Compare with latest"
|
||||||
|
icon={Duplicate}
|
||||||
|
isDisabled={version.isLatest}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ariaComponents.Tooltip>Compare with latest</ariaComponents.Tooltip>
|
||||||
|
</ariaComponents.TooltipTrigger>
|
||||||
|
|
||||||
|
<ariaComponents.Dialog type="fullscreen" title={`Compare ${versionName} with latest`}>
|
||||||
|
<assetDiffView.AssetDiffView
|
||||||
|
latestVersionId={latestVersion.versionId}
|
||||||
|
versionId={version.versionId}
|
||||||
|
projectId={item.id}
|
||||||
|
backend={backend}
|
||||||
|
/>
|
||||||
|
</ariaComponents.Dialog>
|
||||||
|
</ariaComponents.DialogTrigger>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -1,63 +0,0 @@
|
|||||||
/** @file A list of previous versions of an asset. */
|
|
||||||
import * as React from 'react'
|
|
||||||
|
|
||||||
import * as toastAndLogHooks from '#/hooks/toastAndLogHooks'
|
|
||||||
|
|
||||||
import * as backendProvider from '#/providers/BackendProvider'
|
|
||||||
|
|
||||||
import AssetVersion from '#/layouts/AssetVersion'
|
|
||||||
|
|
||||||
import type * as backend from '#/services/Backend'
|
|
||||||
|
|
||||||
import type AssetTreeNode from '#/utilities/AssetTreeNode'
|
|
||||||
|
|
||||||
// =====================
|
|
||||||
// === AssetVersions ===
|
|
||||||
// =====================
|
|
||||||
|
|
||||||
/** Props for a {@link AssetVersions}. */
|
|
||||||
export interface AssetVersionsProps {
|
|
||||||
readonly hidden: boolean
|
|
||||||
readonly item: AssetTreeNode
|
|
||||||
}
|
|
||||||
|
|
||||||
/** A list of previous versions of an asset. */
|
|
||||||
export default function AssetVersions(props: AssetVersionsProps) {
|
|
||||||
const { hidden, item } = props
|
|
||||||
const { backend } = backendProvider.useBackend()
|
|
||||||
const toastAndLog = toastAndLogHooks.useToastAndLog()
|
|
||||||
const [initialized, setInitialized] = React.useState(false)
|
|
||||||
const [versions, setVersions] = React.useState<backend.S3ObjectVersion[]>([])
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (!hidden && !initialized) {
|
|
||||||
setInitialized(true)
|
|
||||||
void (async () => {
|
|
||||||
try {
|
|
||||||
const assetVersions = await backend.listAssetVersions(item.item.id, item.item.title)
|
|
||||||
setVersions([...assetVersions.versions].reverse())
|
|
||||||
} catch (error) {
|
|
||||||
setInitialized(false)
|
|
||||||
toastAndLog('Could not list versions', error)
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
hidden,
|
|
||||||
initialized,
|
|
||||||
backend,
|
|
||||||
item.item.id,
|
|
||||||
item.item.title,
|
|
||||||
/* should never change */ toastAndLog,
|
|
||||||
])
|
|
||||||
|
|
||||||
return hidden ? (
|
|
||||||
<></>
|
|
||||||
) : (
|
|
||||||
<div className="flex flex-col">
|
|
||||||
{versions.map((version, i) => (
|
|
||||||
<AssetVersion key={version.versionId} number={versions.length - i} version={version} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
@ -0,0 +1,80 @@
|
|||||||
|
/** @file A list of previous versions of an asset. */
|
||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
import * as toastAndLogHooks from '#/hooks/toastAndLogHooks'
|
||||||
|
|
||||||
|
import * as backendProvider from '#/providers/BackendProvider'
|
||||||
|
|
||||||
|
import AssetVersion from '#/layouts/AssetVersion'
|
||||||
|
|
||||||
|
import Spinner from '#/components/Spinner'
|
||||||
|
import * as spinnerModule from '#/components/Spinner'
|
||||||
|
|
||||||
|
import RemoteBackend from '#/services/RemoteBackend'
|
||||||
|
|
||||||
|
import type AssetTreeNode from '#/utilities/AssetTreeNode'
|
||||||
|
|
||||||
|
import * as assetVersions from './useAssetVersions'
|
||||||
|
|
||||||
|
// =====================
|
||||||
|
// === AssetVersions ===
|
||||||
|
// =====================
|
||||||
|
|
||||||
|
/** Props for a {@link AssetVersions}. */
|
||||||
|
export interface AssetVersionsProps {
|
||||||
|
readonly item: AssetTreeNode
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A list of previous versions of an asset. */
|
||||||
|
export default function AssetVersions(props: AssetVersionsProps) {
|
||||||
|
const { item } = props
|
||||||
|
|
||||||
|
const { backend } = backendProvider.useBackend()
|
||||||
|
const toastAndLog = toastAndLogHooks.useToastAndLog()
|
||||||
|
|
||||||
|
const isRemote = backend instanceof RemoteBackend
|
||||||
|
|
||||||
|
const {
|
||||||
|
status,
|
||||||
|
error,
|
||||||
|
data: versions,
|
||||||
|
isPending,
|
||||||
|
} = assetVersions.useAssetVersions({
|
||||||
|
backend,
|
||||||
|
assetId: item.item.id,
|
||||||
|
title: item.item.title,
|
||||||
|
onError: backendError => toastAndLog('Could not list versions', backendError),
|
||||||
|
enabled: isRemote,
|
||||||
|
})
|
||||||
|
|
||||||
|
const latestVersion = versions?.find(version => version.isLatest)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-1 shrink-0 flex-col items-center overflow-y-auto overflow-x-hidden">
|
||||||
|
{(() => {
|
||||||
|
if (!isRemote) {
|
||||||
|
return <div>Local assets do not have versions</div>
|
||||||
|
} else if (isPending) {
|
||||||
|
return <Spinner size={32} state={spinnerModule.SpinnerState.loadingMedium} />
|
||||||
|
} else if (status === 'error') {
|
||||||
|
return <div>Error: {error.message}</div>
|
||||||
|
} else if (versions.length === 0) {
|
||||||
|
return <div>No versions found</div>
|
||||||
|
} else if (!latestVersion) {
|
||||||
|
return <div>Could not fetch the latest version of the file</div>
|
||||||
|
} else {
|
||||||
|
return versions.map((version, i) => (
|
||||||
|
<AssetVersion
|
||||||
|
key={version.versionId}
|
||||||
|
number={versions.length - i}
|
||||||
|
version={version}
|
||||||
|
item={item.item}
|
||||||
|
backend={backend}
|
||||||
|
latestVersion={latestVersion}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Fetches the versions of the selected project asset
|
||||||
|
*/
|
||||||
|
import * as reactQuery from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import type Backend from '#/services/Backend'
|
||||||
|
import type * as backendService from '#/services/Backend'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parameters for the useAssetVersions hook
|
||||||
|
*/
|
||||||
|
export interface UseAssetVersionsParams {
|
||||||
|
readonly assetId: backendService.AssetId
|
||||||
|
readonly title: string
|
||||||
|
readonly backend: Backend
|
||||||
|
readonly queryKey?: reactQuery.QueryKey
|
||||||
|
readonly enabled?: boolean
|
||||||
|
readonly onError?: (error: unknown) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the versions of the selected project asset
|
||||||
|
*/
|
||||||
|
export function useAssetVersions(params: UseAssetVersionsParams) {
|
||||||
|
const {
|
||||||
|
enabled = true,
|
||||||
|
title,
|
||||||
|
assetId,
|
||||||
|
backend,
|
||||||
|
onError,
|
||||||
|
queryKey = ['assetVersions', assetId, title],
|
||||||
|
} = params
|
||||||
|
|
||||||
|
return reactQuery.useQuery({
|
||||||
|
queryKey,
|
||||||
|
enabled,
|
||||||
|
queryFn: () =>
|
||||||
|
backend
|
||||||
|
.listAssetVersions(assetId, title)
|
||||||
|
.then(assetVersions => assetVersions.versions)
|
||||||
|
.catch(backendError => {
|
||||||
|
onError?.(backendError)
|
||||||
|
throw backendError
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
@ -1,8 +1,6 @@
|
|||||||
/** @file The top-bar of dashboard. */
|
/** @file The top-bar of dashboard. */
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
import * as reactDom from 'react-dom'
|
|
||||||
|
|
||||||
import type * as assetSearchBar from '#/layouts/AssetSearchBar'
|
import type * as assetSearchBar from '#/layouts/AssetSearchBar'
|
||||||
import AssetSearchBar from '#/layouts/AssetSearchBar'
|
import AssetSearchBar from '#/layouts/AssetSearchBar'
|
||||||
import BackendSwitcher from '#/layouts/BackendSwitcher'
|
import BackendSwitcher from '#/layouts/BackendSwitcher'
|
||||||
@ -10,6 +8,7 @@ import PageSwitcher, * as pageSwitcher from '#/layouts/PageSwitcher'
|
|||||||
import UserBar from '#/layouts/UserBar'
|
import UserBar from '#/layouts/UserBar'
|
||||||
|
|
||||||
import AssetInfoBar from '#/components/dashboard/AssetInfoBar'
|
import AssetInfoBar from '#/components/dashboard/AssetInfoBar'
|
||||||
|
import Portal from '#/components/Portal'
|
||||||
|
|
||||||
import type * as backendModule from '#/services/Backend'
|
import type * as backendModule from '#/services/Backend'
|
||||||
|
|
||||||
@ -50,7 +49,6 @@ export default function TopBar(props: TopBarProps) {
|
|||||||
const { isEditorDisabled, setBackendType, isHelpChatOpen, setIsHelpChatOpen } = props
|
const { isEditorDisabled, setBackendType, isHelpChatOpen, setIsHelpChatOpen } = props
|
||||||
const { query, setQuery, labels, suggestions, isAssetPanelEnabled } = props
|
const { query, setQuery, labels, suggestions, isAssetPanelEnabled } = props
|
||||||
const { isAssetPanelVisible, setIsAssetPanelEnabled, doRemoveSelf, onSignOut } = props
|
const { isAssetPanelVisible, setIsAssetPanelEnabled, doRemoveSelf, onSignOut } = props
|
||||||
const [root] = React.useState(() => document.getElementById('enso-dashboard'))
|
|
||||||
const supportsCloudBackend = process.env.ENSO_CLOUD_API_URL != null
|
const supportsCloudBackend = process.env.ENSO_CLOUD_API_URL != null
|
||||||
const shouldMakeSpaceForExtendedEditorMenu = page === pageSwitcher.Page.editor
|
const shouldMakeSpaceForExtendedEditorMenu = page === pageSwitcher.Page.editor
|
||||||
|
|
||||||
@ -98,8 +96,7 @@ export default function TopBar(props: TopBarProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{root &&
|
<Portal>
|
||||||
reactDom.createPortal(
|
|
||||||
<div
|
<div
|
||||||
className={`fixed right top z-1 m-top-bar text-xs text-primary ${shouldMakeSpaceForExtendedEditorMenu ? 'mr-extended-editor-menu' : ''}`}
|
className={`fixed right top z-1 m-top-bar text-xs text-primary ${shouldMakeSpaceForExtendedEditorMenu ? 'mr-extended-editor-menu' : ''}`}
|
||||||
>
|
>
|
||||||
@ -122,9 +119,8 @@ export default function TopBar(props: TopBarProps) {
|
|||||||
onSignOut={onSignOut}
|
onSignOut={onSignOut}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>,
|
</div>
|
||||||
root
|
</Portal>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -33,6 +33,7 @@ import Settings from '#/layouts/Settings'
|
|||||||
import TopBar from '#/layouts/TopBar'
|
import TopBar from '#/layouts/TopBar'
|
||||||
|
|
||||||
import TheModal from '#/components/dashboard/TheModal'
|
import TheModal from '#/components/dashboard/TheModal'
|
||||||
|
import Portal from '#/components/Portal'
|
||||||
import type * as spinner from '#/components/Spinner'
|
import type * as spinner from '#/components/Spinner'
|
||||||
|
|
||||||
import * as backendModule from '#/services/Backend'
|
import * as backendModule from '#/services/Backend'
|
||||||
@ -545,9 +546,11 @@ export default function Dashboard(props: DashboardProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Portal>
|
||||||
<div className="select-none text-xs text-primary">
|
<div className="select-none text-xs text-primary">
|
||||||
<TheModal />
|
<TheModal />
|
||||||
</div>
|
</div>
|
||||||
|
</Portal>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -836,6 +836,10 @@ export interface S3ObjectVersion {
|
|||||||
versionId: string
|
versionId: string
|
||||||
lastModified: dateTime.Rfc3339DateTime
|
lastModified: dateTime.Rfc3339DateTime
|
||||||
isLatest: boolean
|
isLatest: boolean
|
||||||
|
/**
|
||||||
|
* The field points to an archive containing the all the project files object in the S3 bucket,
|
||||||
|
*/
|
||||||
|
key: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A list of asset versions. */
|
/** A list of asset versions. */
|
||||||
|
@ -420,6 +420,20 @@ export default class RemoteBackend extends Backend {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the content of Main.enso file for a given project.
|
||||||
|
*/
|
||||||
|
async getFileContent(projectId: backendModule.ProjectId, version: string): Promise<string> {
|
||||||
|
const path = remoteBackendPaths.getProjectContentPath(projectId, version)
|
||||||
|
const response = await this.get<string>(path)
|
||||||
|
|
||||||
|
if (!responseIsSuccessful(response)) {
|
||||||
|
return this.throw(`Could not get content of file with ProjectID '${projectId}'`, response)
|
||||||
|
} else {
|
||||||
|
return await response.text()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Change the parent directory of an asset.
|
/** Change the parent directory of an asset.
|
||||||
* @throws An error if a non-successful status code (not 200-299) was received. */
|
* @throws An error if a non-successful status code (not 200-299) was received. */
|
||||||
override async updateAsset(
|
override async updateAsset(
|
||||||
|
@ -65,6 +65,14 @@ export const GET_LOG_EVENTS_PATH = 'log_events'
|
|||||||
export function listAssetVersionsPath(assetId: backend.AssetId) {
|
export function listAssetVersionsPath(assetId: backend.AssetId) {
|
||||||
return `assets/${assetId}/versions`
|
return `assets/${assetId}/versions`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relative HTTP path to the "get Main.enso file" endpoint of the Cloud backend API.
|
||||||
|
*/
|
||||||
|
export function getProjectContentPath(projectId: backend.ProjectId, version: string) {
|
||||||
|
return `projects/${projectId}/files?versionId=${version}`
|
||||||
|
}
|
||||||
|
|
||||||
/** Relative HTTP path to the "update asset" endpoint of the Cloud backend API. */
|
/** Relative HTTP path to the "update asset" endpoint of the Cloud backend API. */
|
||||||
export function updateAssetPath(assetId: backend.AssetId) {
|
export function updateAssetPath(assetId: backend.AssetId) {
|
||||||
return `assets/${assetId}`
|
return `assets/${assetId}`
|
||||||
|
@ -60,3 +60,22 @@ export function isElementTextInput(element: EventTarget | null) {
|
|||||||
(element instanceof HTMLElement && element.isContentEditable))
|
(element instanceof HTMLElement && element.isContentEditable))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the element is part of a Monaco editor.
|
||||||
|
*/
|
||||||
|
export function isElementPartOfMonaco(element: EventTarget | null) {
|
||||||
|
const recursiveCheck = (htmlElement: HTMLElement | null): boolean => {
|
||||||
|
if (htmlElement == null || htmlElement === document.body) {
|
||||||
|
return false
|
||||||
|
} else if (
|
||||||
|
htmlElement instanceof HTMLElement &&
|
||||||
|
htmlElement.classList.contains('monaco-editor')
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return recursiveCheck(htmlElement.parentElement)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return element != null && element instanceof HTMLElement && recursiveCheck(element)
|
||||||
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
/** @file Configuration for Tailwind. */
|
/** @file Configuration for Tailwind. */
|
||||||
|
import reactAriaComponents from 'tailwindcss-react-aria-components'
|
||||||
import plugin from 'tailwindcss/plugin.js'
|
import plugin from 'tailwindcss/plugin.js'
|
||||||
|
|
||||||
// The names come from a third-party API and cannot be changed.
|
// The names come from a third-party API and cannot be changed.
|
||||||
@ -409,6 +410,7 @@ export default /** @satisfies {import('tailwindcss').Config} */ ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
|
reactAriaComponents,
|
||||||
plugin(({ addUtilities, matchUtilities, addComponents, theme }) => {
|
plugin(({ addUtilities, matchUtilities, addComponents, theme }) => {
|
||||||
addUtilities(
|
addUtilities(
|
||||||
{
|
{
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
"noEmit": false,
|
"noEmit": false,
|
||||||
"outDir": "../../../../node_modules/.cache/tsc",
|
"outDir": "../../../../node_modules/.cache/tsc",
|
||||||
"paths": { "#/*": ["./src/*"] },
|
"paths": { "#/*": ["./src/*"] },
|
||||||
|
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
"name": "ts-plugin-namespace-auto-import"
|
"name": "ts-plugin-namespace-auto-import"
|
||||||
|
2036
package-lock.json
generated
2036
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user