├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── .vscode ├── launch.json └── settings.json ├── README.md ├── docs ├── concepts │ ├── fusors │ │ └── document.mdx │ ├── high-style │ │ └── document.mdx │ ├── loaders │ │ └── document.mdx │ ├── media-selectors │ │ └── document.mdx │ ├── surface-selectors │ │ └── document.mdx │ └── surfaces │ │ └── document.mdx ├── glossary.md ├── packages │ ├── retil-mount │ │ └── document.mdx │ └── retil-nav │ │ └── document.mdx ├── router-api.md └── site │ └── index.mdx ├── examples ├── app-with-transitions │ ├── app.tsx │ ├── example.mdx │ ├── index.tsx │ └── main.tsx ├── bare-metal │ ├── app.tsx │ ├── example.mdx │ ├── index.tsx │ └── main.tsx ├── buttonSurface │ ├── app.tsx │ ├── example.mdx │ └── index.tsx ├── connectSurfaceSelectors-styled │ ├── app.styled.tsx │ ├── example.mdx │ └── index.tsx ├── editor-and-menu │ ├── app.tsx │ ├── example.mdx │ └── index.tsx ├── issues-and-operations │ ├── app.tsx │ ├── example.mdx │ ├── fakeAuth.tsx │ ├── index.tsx │ ├── input.tsx │ └── model.tsx ├── mediaSurfaceCombination-emotion │ ├── app.tsx │ ├── example.mdx │ └── index.tsx ├── mount-error-boundary │ ├── app.tsx │ ├── error-boundary.tsx │ ├── example.mdx │ └── index.tsx ├── not-found-boundary │ ├── app.tsx │ ├── example.mdx │ └── index.tsx ├── number-input │ ├── app.tsx │ ├── example.mdx │ └── index.tsx ├── override-action-surface-selectors │ ├── app.tsx │ ├── example.mdx │ └── index.tsx ├── popup-dialog-animated-react-spring │ ├── app.tsx │ ├── example.mdx │ ├── index.tsx │ ├── popupArrowStyles.tsx │ ├── popupDialog.tsx │ └── popupStyles.tsx ├── popup-dialog │ ├── app.tsx │ ├── example.mdx │ └── index.tsx ├── provideMediaSelectors-emotion │ ├── app.tsx │ ├── example.mdx │ └── index.tsx ├── provideMediaSelectors-styled │ ├── app.styled.tsx │ ├── example.mdx │ └── index.tsx ├── suspense-loading-indicators │ ├── app.tsx │ ├── example.mdx │ ├── index.tsx │ └── main.tsx ├── tsconfig.json ├── useMediaRenderer-emotion │ ├── app.tsx │ ├── example.mdx │ └── index.tsx └── useMediaRenderer-styled │ ├── app.styled.tsx │ ├── example.mdx │ └── index.tsx ├── lerna.json ├── package.json ├── packages ├── retil-boundary │ ├── LICENSE │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── boundary.tsx │ │ └── index.ts │ ├── test │ │ └── boundaryEffect.test.tsx │ └── tsconfig.build.json ├── retil-css │ ├── LICENSE │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── constants.ts │ │ ├── context.tsx │ │ ├── highStyle.ts │ │ ├── index.ts │ │ ├── selector.tsx │ │ ├── stringifyTransition.tsx │ │ └── types.ts │ ├── test │ │ ├── highStyle.test.tsx │ │ └── stringifyTransition.test.ts │ └── tsconfig.build.json ├── retil-hydration │ ├── LICENSE │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── hydrationContext.tsx │ │ ├── hydrationEnvService.ts │ │ ├── hydrationTypes.ts │ │ └── index.ts │ ├── test │ │ └── hydrationContext.test.tsx │ └── tsconfig.build.json ├── retil-interaction │ ├── LICENSE │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── connector.ts │ │ ├── defaultSurfaceSelectors.ts │ │ ├── disableable.tsx │ │ ├── escape.tsx │ │ ├── focusable.tsx │ │ ├── focusableSelectable.tsx │ │ ├── focusableSelection.tsx │ │ ├── focusableTrap.tsx │ │ ├── index.ts │ │ ├── keyboard.tsx │ │ ├── listCursor.tsx │ │ ├── menu.tsx │ │ ├── outsideClick.tsx │ │ ├── popup.tsx │ │ ├── popupPositioner.ts │ │ ├── popupTrigger.ts │ │ ├── surfaceSelector.tsx │ │ ├── surfaces │ │ │ ├── actionSurface.tsx │ │ │ ├── anchorSurface.tsx │ │ │ ├── buttonSurface.tsx │ │ │ ├── linkSurface.tsx │ │ │ ├── matchedLinkSurface.tsx │ │ │ ├── modalSurface.tsx │ │ │ ├── popupDialogSurface.tsx │ │ │ ├── popupMenuSurface.tsx │ │ │ ├── popupTriggerSurface.tsx │ │ │ └── submitButtonSurface.tsx │ │ └── unscrollableBody.tsx │ ├── test │ │ ├── popupDialog.test.tsx │ │ ├── popupTrigger.test.tsx │ │ └── surfaceSelectors.test.tsx │ └── tsconfig.build.json ├── retil-issues │ ├── LICENSE │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── getIssueMessage.ts │ │ ├── index.ts │ │ ├── issueTypes.ts │ │ ├── useAsyncValidator.ts │ │ ├── useIssues.ts │ │ └── useValidator.ts │ ├── test │ │ ├── useIssues.test.tsx │ │ └── useValidator.test.tsx │ └── tsconfig.build.json ├── retil-media │ ├── LICENSE │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── defaultMedia.ts │ │ ├── index.ts │ │ ├── mediaSelector.tsx │ │ ├── useFirstMatchingMediaSelector.ts │ │ ├── useMediaRenderer.tsx │ │ └── useMediaSelector.ts │ ├── test │ │ └── useMediaSelector.test.tsx │ └── tsconfig.build.json ├── retil-mount │ ├── LICENSE │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── dependencyList.ts │ │ ├── index.ts │ │ ├── loadAsync.tsx │ │ ├── loadLazy.ts │ │ ├── mount.ts │ │ ├── mountComponents.tsx │ │ ├── mountContext.tsx │ │ ├── mountOnce.ts │ │ ├── mountTypes.ts │ │ ├── serverMount.tsx │ │ ├── serverMountContext.ts │ │ ├── useMount.ts │ │ └── useMountSource.ts │ ├── test │ │ ├── mount.test.ts │ │ ├── serverMount.test.tsx │ │ └── useMountSource.test.tsx │ └── tsconfig.build.json ├── retil-nav-scheme │ ├── LICENSE │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ └── index.ts │ ├── test │ │ └── navScheme.test.ts │ └── tsconfig.build.json ├── retil-nav │ ├── LICENSE │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── browserNavEnvService.ts │ │ ├── hooks │ │ │ ├── useBoundaryNavScroller.ts │ │ │ ├── useNavLinkProps.ts │ │ │ ├── useNavMatcher.ts │ │ │ └── useNavResolver.ts │ │ ├── index.ts │ │ ├── loaders │ │ │ ├── loadMatch.tsx │ │ │ ├── loadNotFoundBoundary.tsx │ │ │ └── loadRedirect.tsx │ │ ├── matcher.ts │ │ ├── navContext.tsx │ │ ├── navTypes.ts │ │ ├── navUtils.ts │ │ ├── noopNavController.ts │ │ ├── notFoundError.ts │ │ ├── serverNavEnv.ts │ │ └── staticNavEnv.ts │ ├── test │ │ ├── browserNavService.test.ts │ │ ├── loadMatch.test.ts │ │ ├── loadNotFoundBoundary.test.tsx │ │ ├── loadRedirect.test.ts │ │ ├── navUtils.test.ts │ │ └── useNavMatch.test.tsx │ └── tsconfig.build.json ├── retil-operation │ ├── LICENSE │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── useOperation.ts │ ├── test │ │ └── useOperation.test.tsx │ └── tsconfig.build.json ├── retil-source │ ├── LICENSE │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── filter.ts │ │ ├── fromPromise.ts │ │ ├── fuse.ts │ │ ├── fuseVector.ts │ │ ├── fusor.ts │ │ ├── index.ts │ │ ├── map.ts │ │ ├── mapVector.ts │ │ ├── observe.ts │ │ ├── reduceVector.ts │ │ ├── select.ts │ │ ├── source.ts │ │ ├── state.ts │ │ ├── stateVector.ts │ │ ├── useService.ts │ │ ├── useSource.ts │ │ └── wait.ts │ ├── test │ │ ├── fromPromise.test.ts │ │ ├── fuse.test.ts │ │ ├── fuseVector.test.ts │ │ ├── observe.test.ts │ │ ├── reduceVector.test.ts │ │ ├── useSource.test.tsx │ │ └── utils │ │ │ └── sendToArray.ts │ └── tsconfig.build.json ├── retil-support │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src │ │ ├── areShallowEqual.ts │ │ ├── arrayKeyedMap.ts │ │ ├── compose.ts │ │ ├── deferred.ts │ │ ├── delay.ts │ │ ├── delayOne.ts │ │ ├── emptyArray.ts │ │ ├── emptyObject.ts │ │ ├── ensureTruthyArray.ts │ │ ├── fastCartesian.ts │ │ ├── fromEntries.ts │ │ ├── getForm.ts │ │ ├── identity.ts │ │ ├── index.ts │ │ ├── isPlainObject.ts │ │ ├── isPromiseLike.ts │ │ ├── joinClassNames.ts │ │ ├── joinEventHandlers.ts │ │ ├── joinRefs.ts │ │ ├── keyPartitioner.ts │ │ ├── maybe.ts │ │ ├── memo.ts │ │ ├── noop.ts │ │ ├── pendingPromiseLike.ts │ │ ├── preventDefaultEventHandler.ts │ │ ├── root.ts │ │ ├── suspendIndefinitely.ts │ │ ├── useConfigurator.ts │ │ ├── useEffectOnce.ts │ │ ├── useFirstInstanceOfLatestValue.ts │ │ ├── useMemoizeOneValue.ts │ │ └── useSilencedLayoutEffect.ts │ └── tsconfig.build.json ├── retil-transition │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src │ │ ├── columnTransition.tsx │ │ ├── index.ts │ │ ├── transitionConfigs.ts │ │ ├── transitionHandle.ts │ │ └── transitionHandleRefContext.ts │ └── tsconfig.build.json ├── retil │ ├── LICENSE │ ├── README.md │ ├── index.ts │ ├── package.json │ ├── rollup.config.js │ └── tsconfig.build.json ├── tool-next │ ├── README.md │ ├── jest.config.js │ ├── package.json-bak │ ├── src │ │ ├── index.ts │ │ ├── mapPathnameToRoute.ts │ │ ├── nextilApp.tsx │ │ ├── nextilConstants.ts │ │ ├── nextilHistory.ts │ │ ├── nextilNotFound.tsx │ │ ├── nextilRoutedPage.tsx │ │ ├── nextilRouter.tsx │ │ └── nextilTypes.ts │ └── tsconfig.build.json ├── tool-vite-plugin-code-as-content │ ├── README.md │ ├── client.d.ts │ ├── compat.cjs │ ├── package.json │ ├── src │ │ ├── importFrontMatterPlugin.ts │ │ ├── importGlobExtensionPlugin.ts │ │ ├── importHighlightedSourcePlugin.ts │ │ ├── index.ts │ │ ├── mdxPlugin.ts │ │ └── typography.ts │ ├── tsconfig.build.json │ └── type │ │ ├── hast-util-to-string.d.ts │ │ ├── importMeta.d.ts │ │ ├── mdx.d.ts │ │ ├── remark-slug.d.ts │ │ ├── typographic.d.ts │ │ └── unist-util-visit.d.ts ├── tool-vite-plugin-emotion │ ├── package.json │ ├── src │ │ └── index.ts │ ├── tsconfig.build.json │ └── type │ │ └── emotion__babel-plugin.d.ts ├── tool-vite-plugin-styled-components │ ├── package.json │ ├── src │ │ └── index.ts │ ├── tsconfig.build.json │ └── type │ │ └── babel-plugin-styled-components.d.ts └── tool-vite-plugin-svg │ ├── package.json │ ├── src │ └── index.ts │ └── tsconfig.build.json ├── site ├── .gitignore ├── index.html ├── package.json ├── prerender.ts ├── react-shim.js ├── server.js ├── src │ ├── app │ │ ├── app.tsx │ │ ├── appLoader.tsx │ │ ├── appScheme.tsx │ │ ├── concepts │ │ │ ├── conceptIndexPage.tsx │ │ │ ├── conceptLoader.tsx │ │ │ ├── conceptPage.tsx │ │ │ └── conceptScheme.tsx │ │ ├── examples │ │ │ ├── exampleIndexPage.tsx │ │ │ ├── exampleLoader.tsx │ │ │ ├── examplePage.tsx │ │ │ └── exampleScheme.tsx │ │ ├── notFoundLoader.tsx │ │ └── packages │ │ │ ├── packageIndexPage.tsx │ │ │ ├── packageLoader.tsx │ │ │ ├── packagePage.tsx │ │ │ └── packageScheme.tsx │ ├── asset │ │ └── favicon.ico │ ├── component │ │ ├── codeBlock │ │ │ ├── codeBlock.tsx │ │ │ └── index.ts │ │ ├── document │ │ │ ├── documentContent.tsx │ │ │ ├── documentFooter.tsx │ │ │ ├── documentStyles.tsx │ │ │ └── index.ts │ │ ├── layout │ │ │ ├── index.ts │ │ │ ├── layout.tsx │ │ │ └── layoutLoadingFallback.tsx │ │ └── link │ │ │ ├── index.ts │ │ │ └── link.tsx │ ├── context │ │ └── mdx.ts │ ├── data │ │ ├── README.md │ │ ├── conceptContent.ts │ │ ├── conceptIndex.ts │ │ ├── conceptMeta.ts │ │ ├── exampleContent.tsx │ │ ├── exampleDefaultDoc.mdx │ │ ├── exampleIndex.ts │ │ ├── exampleMeta.ts │ │ ├── packageContent.ts │ │ ├── packageIndex.ts │ │ └── packageMeta.ts │ ├── entry-client.tsx │ ├── entry-server.tsx │ ├── env │ │ ├── env.tsx │ │ └── index.ts │ ├── head │ │ ├── head.tsx │ │ └── index.ts │ ├── style │ │ ├── colors.ts │ │ ├── dimensions.ts │ │ └── globalStyle.tsx │ └── util │ │ └── extractGlobData.ts ├── tsconfig.json ├── tsconfig.tsbuildinfo └── vite.config.ts ├── tsconfig.build.json ├── tsconfig.json └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'react-app', 3 | parser: '@typescript-eslint/parser', 4 | plugins: ['@typescript-eslint'], 5 | settings: { 6 | react: { 7 | version: '16.8', 8 | }, 9 | 'import/parsers': { 10 | '@typescript-eslint/parser': ['.ts', '.tsx'], 11 | }, 12 | }, 13 | rules: { 14 | '@typescript-eslint/no-angle-bracket-type-assertion': 'off', 15 | 'import/no-anonymous-default-export': 'off', 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .vercel 3 | dist 4 | node_modules 5 | notes -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": false, 3 | "printWidth": 80, 4 | "tabWidth": 2, 5 | "singleQuote": true, 6 | "trailingComma": "all", 7 | "jsxBracketSameLine": true, 8 | "parser": "typescript", 9 | "semi": false, 10 | "rcVerbose": false 11 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug Tests - retil-router", 6 | "type": "node", 7 | "cwd": "${workspaceFolder}/packages/retil-router", 8 | "request": "launch", 9 | "runtimeArgs": [ 10 | "--inspect-brk", 11 | "${workspaceRoot}/node_modules/.bin/jest", 12 | "--runInBand", 13 | "--watch" 14 | ], 15 | "console": "integratedTerminal", 16 | "internalConsoleOptions": "neverOpen", 17 | "port": 9229 18 | }, 19 | { 20 | "name": "Debug Tests - retil-source", 21 | "type": "node", 22 | "cwd": "${workspaceFolder}/packages/retil-source", 23 | "request": "launch", 24 | "runtimeArgs": [ 25 | "--inspect-brk", 26 | "${workspaceRoot}/node_modules/.bin/jest", 27 | "--runInBand", 28 | "--watch", 29 | "${file}" 30 | ], 31 | "console": "integratedTerminal", 32 | "internalConsoleOptions": "neverOpen", 33 | "port": 9229 34 | } 35 | ] 36 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.tabSize": 2, 4 | "eslint.workingDirectories": [ 5 | "./" 6 | ], 7 | "eslint.validate": [ 8 | "javascript", 9 | "javascriptreact", 10 | "typescript", 11 | "typescriptreact" 12 | ], 13 | "javascript.updateImportsOnFileMove.enabled": "always", 14 | "javascript.preferences.importModuleSpecifier": "non-relative", 15 | "typescript.tsdk": "node_modules/typescript/lib", 16 | "[html]": { 17 | "editor.formatOnSave": false 18 | }, 19 | "[markdown]": { 20 | "editor.formatOnSave": false 21 | }, 22 | "[json]": { 23 | "editor.formatOnSave": false 24 | } 25 | } -------------------------------------------------------------------------------- /docs/concepts/fusors/document.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Fusors 3 | packages: 4 | - retil-source 5 | --- 6 | 7 | # 8 | 9 | Composable, react-independent effects and state management. 10 | 11 | ## The rules of fusors 12 | 13 | - any given set of `use` results should produce the same return value. 14 | - this return value can be an actor call, in which case, the actor must cause the `use` results to change. 15 | - hooks must be called synchronously with the fusor, and may not be called within try/catch blocks 16 | 17 | corollaries: 18 | 19 | - returning an effect when all your used sources are sealed is illegal 20 | - any function passed to `memo` must be pure 21 | -------------------------------------------------------------------------------- /docs/concepts/high-style/document.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: High Style 3 | packages: 4 | - retil-css 5 | - retil-media 6 | - retil-interaction 7 | --- 8 | 9 | # <Title /> 10 | 11 | Re-usable style components free you up to create high-quality component libraries and design systems, and retil-css facilitates this too with **high style** – style objects that allow for different values in different surfaces. 12 | 13 | ```jsx 14 | import { highStyle } from 'retil-css' 15 | import { createSurfaceSelector, inHoveredSurface } from 'retil-interaction' 16 | import { media } from 'retil-media' 17 | 18 | // High Style works with custom surface selectors too! 19 | const inCheckedSurface = createSurfaceSelector('[aria-checked="true"]') 20 | 21 | <div css={highStyle({ 22 | color: { 23 | default: 'black', 24 | [inHoveredSurface]: 'red', 25 | [media.small]: { 26 | [inCheckedSurface]: 'black', 27 | } 28 | }, 29 | fontSize: { 30 | default: '16px', 31 | [media.small]: '14px', 32 | } 33 | })} /> 34 | ``` 35 | -------------------------------------------------------------------------------- /docs/concepts/loaders/document.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Loaders 3 | packages: 4 | - retil-mount 5 | - retil-nav 6 | --- 7 | 8 | # <Title /> 9 | 10 | Loaders are a type of function that map props to content. 11 | 12 | ```ts 13 | type Loader = (props) => Content 14 | ``` 15 | 16 | 17 | -------------------------------------------------------------------------------- /docs/concepts/media-selectors/document.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Media Selectors 3 | packages: 4 | - retil-media 5 | - retil-css 6 | --- 7 | 8 | # <Title /> -------------------------------------------------------------------------------- /docs/packages/retil-mount/document.mdx: -------------------------------------------------------------------------------- 1 | # <Title /> 2 | 3 | Load and mount content. 4 | 5 | ## Concepts 6 | 7 | <ConceptList /> 8 | 9 | ## Examples 10 | 11 | <ExampleList /> -------------------------------------------------------------------------------- /docs/packages/retil-nav/document.mdx: -------------------------------------------------------------------------------- 1 | # <Title /> 2 | 3 | Navigation how it's meant to be done. 4 | 5 | ## Examples 6 | 7 | <ExampleList /> -------------------------------------------------------------------------------- /docs/site/index.mdx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react' 2 | 3 | <h1 css={css` 4 | font-size: 3rem; 5 | line-height: 4rem; 6 | font-weight: 900; 7 | margin-top: 3rem; 8 | margin-bottom: 2rem; 9 | `}>retil</h1> 10 | 11 | <div css={css` 12 | font-style: italic; 13 | margin-bottom: 2rem; 14 | `}> 15 | 16 | **retil gives you superpowers** 17 | 18 | </div> 19 | 20 | 21 | Retil takes care of your app's foundation -- including authentication, navigation, validation, async operations, state management, hydration, and media queries -- so that you can focus on the bits that matter. 22 | 23 | It works great with CRA, Next.js, and Vite. 24 | 25 | - [see all examples](/examples) 26 | - [see all packages](/packages) 27 | -------------------------------------------------------------------------------- /examples/app-with-transitions/example.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: App with Transitions 3 | packages: 4 | - retil-hydration 5 | - retil-interaction 6 | - retil-mount 7 | - retil-nav 8 | - retil-source 9 | - retil-transition 10 | --- 11 | import { css } from '@emotion/react' 12 | 13 | # <Title /> 14 | 15 | <Example /> 16 | 17 | ## app.tsx 18 | 19 | <Source filename='app.tsx' /> 20 | 21 | ## main.tsx 22 | 23 | <Source filename='main.tsx' /> -------------------------------------------------------------------------------- /examples/app-with-transitions/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './main' 2 | export { default as Doc, meta } from './example.mdx' 3 | export const sources = import.meta.highlightedSourceGlobEager('./*.tsx') 4 | export const matchNestedRoutes = true 5 | -------------------------------------------------------------------------------- /examples/app-with-transitions/main.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from 'react' 2 | import { ServerMount } from 'retil-mount' 3 | import { 4 | createServerNavEnv, 5 | NavEnvService, 6 | NavRequest, 7 | NavResponse, 8 | } from 'retil-nav' 9 | 10 | import { App, appLoader } from './app' 11 | 12 | export async function clientMain( 13 | render: (element: ReactElement) => void, 14 | getDefaultBrowserNavEnvService: () => NavEnvService, 15 | ) { 16 | const [navEnvSource] = getDefaultBrowserNavEnvService() 17 | 18 | render(<App env={navEnvSource} />) 19 | } 20 | 21 | export async function serverMain( 22 | render: (element: ReactElement) => void, 23 | request: NavRequest, 24 | response: NavResponse, 25 | ) { 26 | const env = createServerNavEnv(request, response) 27 | const mount = new ServerMount(appLoader, env) 28 | try { 29 | await mount.preload() 30 | render(mount.provide(<App env={env} />)) 31 | } finally { 32 | mount.seal() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/bare-metal/app.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from 'react' 2 | import { Mount, LoaderProps, useMountContent } from 'retil-mount' 3 | import { NavEnv, useNavLinkProps, useNavMatcher } from 'retil-nav' 4 | import { Source } from 'retil-source' 5 | 6 | export interface AppEnv extends NavEnv {} 7 | 8 | export function appLoader({ nav }: LoaderProps<AppEnv>) { 9 | switch (nav.pathname) { 10 | case nav.matchname: 11 | return <h1>Welcome!</h1> 12 | 13 | case nav.matchname + '/about': 14 | return <h1>About</h1> 15 | 16 | default: 17 | return <h1>Not Found</h1> 18 | } 19 | } 20 | 21 | export interface AppProps { 22 | env: AppEnv | Source<AppEnv> 23 | } 24 | 25 | export function App({ env }: AppProps) { 26 | return ( 27 | <Mount env={env} loader={appLoader}> 28 | <nav> 29 | <Link to="/">Home</Link> 30 |  ·  31 | <Link to="/about">About</Link> 32 |  ·  33 | <Link to="/not-found">Not Found</Link> 34 | </nav> 35 | <Content /> 36 | </Mount> 37 | ) 38 | } 39 | 40 | const Content = () => useMountContent<ReactElement>() 41 | 42 | const Link = ({ to, children }: { to: string; children: React.ReactNode }) => { 43 | const linkProps = useNavLinkProps(to) 44 | const match = useNavMatcher() 45 | return ( 46 | <a {...linkProps} style={{ color: match(to) ? 'red' : 'black' }}> 47 | {children} 48 | </a> 49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /examples/bare-metal/example.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Bare Metal Routing 3 | packages: 4 | - retil-mount 5 | - retil-nav 6 | --- 7 | 8 | # <Title /> 9 | 10 | <Example /> 11 | 12 | ## app.tsx 13 | 14 | <Source filename='app.tsx' /> 15 | 16 | ## main.tsx 17 | 18 | <Source filename='main.tsx' /> -------------------------------------------------------------------------------- /examples/bare-metal/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './main' 2 | export { default as Doc, meta } from './example.mdx' 3 | export const sources = import.meta.highlightedSourceGlobEager('./*.tsx') 4 | export const matchNestedRoutes = true 5 | -------------------------------------------------------------------------------- /examples/bare-metal/main.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from 'react' 2 | import { ServerMount } from 'retil-mount' 3 | import { 4 | createServerNavEnv, 5 | NavEnvService, 6 | NavRequest, 7 | NavResponse, 8 | } from 'retil-nav' 9 | 10 | import { App, appLoader } from './app' 11 | 12 | export async function clientMain( 13 | render: (element: ReactElement) => void, 14 | getDefaultBrowserNavEnvService: () => NavEnvService, 15 | ) { 16 | const [envSource] = getDefaultBrowserNavEnvService() 17 | render(<App env={envSource} />) 18 | } 19 | 20 | export async function serverMain( 21 | render: (element: ReactElement) => void, 22 | request: NavRequest, 23 | response: NavResponse, 24 | ) { 25 | const env = createServerNavEnv(request, response) 26 | const mount = new ServerMount(appLoader, env) 27 | try { 28 | await mount.preload() 29 | render(mount.provide(<App env={env} />)) 30 | } finally { 31 | mount.seal() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/buttonSurface/example.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: <ButtonSurface> 3 | packages: 4 | - retil-interaction 5 | --- 6 | 7 | # <Title /> 8 | 9 | <Example /> 10 | 11 | 12 | ## app.tsx 13 | 14 | <Source filename='app.tsx' /> 15 | -------------------------------------------------------------------------------- /examples/buttonSurface/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as App } from './app' 2 | export { default as Doc, meta } from './example.mdx' 3 | export const sources = import.meta.highlightedSourceGlobEager('./*.tsx') 4 | -------------------------------------------------------------------------------- /examples/connectSurfaceSelectors-styled/app.styled.tsx: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components' 2 | import { 3 | useSurfaceSelectorsConnector, 4 | createSurfaceSelector, 5 | } from 'retil-interaction' 6 | 7 | const selectActive = createSurfaceSelector( 8 | (selector, surface) => selector`${surface}:active`, 9 | ) 10 | const selectDisabled = createSurfaceSelector(false) 11 | const selectHover = createSurfaceSelector(':hover') 12 | 13 | type ButtonSurfaceProps = Omit<JSX.IntrinsicElements['button'], 'ref'> & { 14 | active?: boolean 15 | disabled?: boolean 16 | hover?: boolean 17 | } 18 | 19 | const ButtonSurface = ({ 20 | active, 21 | disabled, 22 | hover, 23 | ...restProps 24 | }: ButtonSurfaceProps) => { 25 | const [, mergeProps, provide] = useSurfaceSelectorsConnector([ 26 | [selectActive, active ?? null], 27 | [selectDisabled, disabled ?? null], 28 | [selectHover, hover ?? null], 29 | ]) 30 | 31 | return provide(<button {...mergeProps(restProps)} />) 32 | } 33 | 34 | const StyledButtonBody = styled.div` 35 | border-radius: 8px; 36 | border: 2px solid black; 37 | background: white; 38 | cursor: pointer; 39 | padding: 8px; 40 | 41 | ${selectHover(css` 42 | border-color: red; 43 | `)} 44 | ` 45 | 46 | const App = () => { 47 | return ( 48 | <> 49 | <h2>Basic usage</h2> 50 | <ButtonSurface> 51 | <StyledButtonBody>Button</StyledButtonBody> 52 | </ButtonSurface> 53 | <h2>Hover on</h2> 54 | <ButtonSurface hover> 55 | <StyledButtonBody>Button</StyledButtonBody> 56 | </ButtonSurface> 57 | <h2>Hover off</h2> 58 | <ButtonSurface hover={false}> 59 | <StyledButtonBody>Button</StyledButtonBody> 60 | </ButtonSurface> 61 | </> 62 | ) 63 | } 64 | 65 | export default App 66 | -------------------------------------------------------------------------------- /examples/connectSurfaceSelectors-styled/example.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: <ConnectSurfaceSelectors> (styled components) 3 | packages: 4 | - retil-css 5 | - retil-interaction 6 | --- 7 | 8 | # <Title /> 9 | 10 | <Example /> 11 | 12 | 13 | ## app.tsx 14 | 15 | <Source filename='app.styled.tsx' /> 16 | -------------------------------------------------------------------------------- /examples/connectSurfaceSelectors-styled/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as App } from './app.styled' 2 | export { default as Doc, meta } from './example.mdx' 3 | export const sources = import.meta.highlightedSourceGlobEager('./*.tsx') 4 | export const styledComponents = true 5 | -------------------------------------------------------------------------------- /examples/editor-and-menu/example.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Editor and menu 3 | packages: 4 | - retil-interaction 5 | --- 6 | 7 | # <Title /> 8 | 9 | <Example /> 10 | 11 | 12 | ## app.tsx 13 | 14 | <Source filename='app.tsx' /> 15 | -------------------------------------------------------------------------------- /examples/editor-and-menu/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as App } from './app' 2 | export { default as Doc, meta } from './example.mdx' 3 | export const sources = import.meta.highlightedSourceGlobEager('./*.tsx') 4 | -------------------------------------------------------------------------------- /examples/issues-and-operations/example.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Issues and operations 3 | packages: 4 | - retil-issues 5 | --- 6 | 7 | # <Title /> 8 | 9 | <Example /> 10 | 11 | 12 | ## app.tsx 13 | 14 | <Source filename='app.tsx' /> 15 | -------------------------------------------------------------------------------- /examples/issues-and-operations/fakeAuth.tsx: -------------------------------------------------------------------------------- 1 | import { ValidatorIssues } from 'retil-issues' 2 | import { delay, root } from 'retil-support' 3 | 4 | export interface FakeAuthSignInRequest { 5 | email: string 6 | password: string 7 | } 8 | 9 | export type FakeAuthSignInCodes = { 10 | [root]: 'error' 11 | email: 'missing' | 'invalid' 12 | password: 'missing' | 'invalid' | 'mismatch' 13 | } 14 | 15 | export type FakeAuthSignInIssues = ValidatorIssues< 16 | FakeAuthSignInRequest, 17 | FakeAuthSignInCodes 18 | > 19 | 20 | export async function fakeAuthSignInWithPassword( 21 | request: FakeAuthSignInRequest, 22 | ): Promise<null | FakeAuthSignInIssues> { 23 | await delay(500) 24 | 25 | if (!request.email) { 26 | return { 27 | email: ['missing'], 28 | } 29 | } else if (!request.email.includes('@')) { 30 | return { 31 | email: ['invalid'], 32 | } 33 | } else if (!request.password) { 34 | return { 35 | password: ['missing'], 36 | } 37 | } else if (request.password !== 'password') { 38 | return { 39 | password: ['mismatch'], 40 | } 41 | } else if (request.email.startsWith('fail')) { 42 | return [ 43 | { 44 | code: 'error', 45 | }, 46 | ] 47 | } 48 | 49 | return null 50 | } 51 | -------------------------------------------------------------------------------- /examples/issues-and-operations/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as App } from './app' 2 | export { default as Doc, meta } from './example.mdx' 3 | export const sources = import.meta.highlightedSourceGlobEager('./*.tsx') 4 | -------------------------------------------------------------------------------- /examples/issues-and-operations/input.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react' 2 | 3 | export interface InputProps 4 | extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange'> { 5 | onChange?: (text: string) => void 6 | } 7 | 8 | export function Input(props: InputProps) { 9 | const { onChange, ...rest } = props 10 | 11 | const handleChange = useCallback( 12 | (event: React.ChangeEvent<HTMLInputElement>) => { 13 | if (onChange) { 14 | onChange(event.target.value) 15 | } 16 | }, 17 | [onChange], 18 | ) 19 | 20 | return <input {...(rest as any)} onChange={handleChange} /> 21 | } 22 | -------------------------------------------------------------------------------- /examples/mediaSurfaceCombination-emotion/example.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Media and surface selectors (emotion) 3 | packages: 4 | - retil-css 5 | - retil-media 6 | - retil-interaction 7 | --- 8 | 9 | # <Title /> 10 | 11 | <Example /> 12 | 13 | 14 | ## app.tsx 15 | 16 | <Source filename='app.tsx' /> 17 | -------------------------------------------------------------------------------- /examples/mediaSurfaceCombination-emotion/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as App } from './app' 2 | export { default as Doc, meta } from './example.mdx' 3 | export const sources = import.meta.highlightedSourceGlobEager('./*.tsx') 4 | -------------------------------------------------------------------------------- /examples/mount-error-boundary/example.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Mount Error Boundary 3 | packages: 4 | - retil-mount 5 | - retil-nav 6 | --- 7 | 8 | # <Title /> 9 | 10 | <Example /> 11 | 12 | 13 | ## app.tsx 14 | 15 | <Source filename='app.tsx' /> 16 | -------------------------------------------------------------------------------- /examples/mount-error-boundary/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as App } from './app' 2 | export { default as Doc, meta } from './example.mdx' 3 | export const sources = import.meta.highlightedSourceGlobEager('./*.tsx') 4 | export const matchNestedRoutes = true 5 | -------------------------------------------------------------------------------- /examples/not-found-boundary/app.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react' 2 | import { Mount, MountedContent } from 'retil-mount' 3 | import { MatchedLinkSurface, inToggledSurface } from 'retil-interaction' 4 | import { 5 | getDefaultBrowserNavEnvService, 6 | loadMatch, 7 | loadNotFoundBoundary, 8 | } from 'retil-nav' 9 | 10 | const rootLoader = loadNotFoundBoundary( 11 | loadMatch({ 12 | '/': <h1>Welcome!</h1>, 13 | '/about': <h1>About</h1>, 14 | }), 15 | (env) => <NotFound pathname={env.nav.pathname} />, 16 | ) 17 | 18 | const NavLinkBody = (props: any) => { 19 | return ( 20 | <span 21 | css={[ 22 | inToggledSurface(css` 23 | color: red; 24 | `), 25 | ]}> 26 | {props.children} 27 | </span> 28 | ) 29 | } 30 | 31 | function App() { 32 | const [navSource] = getDefaultBrowserNavEnvService() 33 | 34 | return ( 35 | <Mount env={navSource} loader={rootLoader}> 36 | <nav> 37 | <MatchedLinkSurface href="/" match="/"> 38 | <NavLinkBody>Home</NavLinkBody> 39 | </MatchedLinkSurface> 40 |  ·  41 | <MatchedLinkSurface href="/about"> 42 | <NavLinkBody>About</NavLinkBody> 43 | </MatchedLinkSurface> 44 |  ·  45 | <MatchedLinkSurface href="/not-found"> 46 | <NavLinkBody>Not Found</NavLinkBody> 47 | </MatchedLinkSurface> 48 | </nav> 49 | <MountedContent /> 50 | </Mount> 51 | ) 52 | } 53 | 54 | function NotFound({ pathname }: { pathname: string }) { 55 | return <h1>404 Not Found - {pathname}</h1> 56 | } 57 | 58 | export default App 59 | -------------------------------------------------------------------------------- /examples/not-found-boundary/example.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Not Found Boundary 3 | packages: 4 | - retil-nav 5 | --- 6 | 7 | # <Title /> 8 | 9 | <Example /> 10 | 11 | 12 | ## app.tsx 13 | 14 | <Source filename='app.tsx' /> 15 | -------------------------------------------------------------------------------- /examples/not-found-boundary/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as App } from './app' 2 | export { default as Doc, meta } from './example.mdx' 3 | export const sources = import.meta.highlightedSourceGlobEager('./*.tsx') 4 | export const matchNestedRoutes = true 5 | -------------------------------------------------------------------------------- /examples/number-input/example.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Creating a number input with surfaces and focus delegation 3 | packages: 4 | - retil-interaction 5 | --- 6 | 7 | # <Title /> 8 | 9 | <Example /> 10 | 11 | 12 | ## app.tsx 13 | 14 | <Source filename='app.tsx' /> 15 | -------------------------------------------------------------------------------- /examples/number-input/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as App } from './app' 2 | export { default as Doc, meta } from './example.mdx' 3 | export const sources = import.meta.highlightedSourceGlobEager('./*.tsx') 4 | -------------------------------------------------------------------------------- /examples/override-action-surface-selectors/example.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Overriding Action Surface Selectors 3 | packages: 4 | - retil-css 5 | - retil-interaction 6 | --- 7 | 8 | # <Title /> 9 | 10 | <Example /> 11 | 12 | 13 | ## app.tsx 14 | 15 | <Source filename='app.tsx' /> 16 | -------------------------------------------------------------------------------- /examples/override-action-surface-selectors/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as App } from './app' 2 | export { default as Doc, meta } from './example.mdx' 3 | export const sources = import.meta.highlightedSourceGlobEager('./*.tsx') 4 | -------------------------------------------------------------------------------- /examples/popup-dialog-animated-react-spring/example.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Popup Dialog (animated with react-spring) 3 | packages: 4 | - retil-interaction 5 | --- 6 | 7 | # <Title /> 8 | 9 | <Example /> 10 | 11 | 12 | ## app.tsx 13 | 14 | <Source filename='app.tsx' /> 15 | -------------------------------------------------------------------------------- /examples/popup-dialog-animated-react-spring/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as App } from './app' 2 | export { default as Doc, meta } from './example.mdx' 3 | export const sources = import.meta.highlightedSourceGlobEager('./*.tsx') 4 | -------------------------------------------------------------------------------- /examples/popup-dialog-animated-react-spring/popupStyles.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react' 2 | import React from 'react' 3 | import { animated, to as interpolate } from 'react-spring' 4 | 5 | export interface PopupCardProps extends React.ComponentProps<'div'> { 6 | radius?: string 7 | raised?: boolean 8 | rounded?: boolean 9 | } 10 | 11 | const AnimatedDiv = animated.div as unknown as 'div' 12 | 13 | export const AnimatedPopupCard = React.forwardRef< 14 | HTMLDivElement, 15 | PopupCardProps 16 | >(({ radius = '3px', raised = true, rounded, ...rest }, ref) => ( 17 | <AnimatedDiv 18 | ref={ref} 19 | css={css` 20 | position: relative; 21 | display: flex; 22 | flex-direction: column; 23 | justify-content: center; 24 | align-items: center; 25 | top: 0; 26 | left: 0; 27 | z-index: 1000; 28 | transform-origin: top center; 29 | -webkit-overflow-scrolling: touch; 30 | `} 31 | {...rest} 32 | /> 33 | )) 34 | 35 | export const createMergeStyle = 36 | ({ opacity, top: topOffset }: any) => 37 | ( 38 | { left, top, ...popupStyle }: React.CSSProperties = {}, 39 | styleProp?: React.CSSProperties, 40 | ) => ({ 41 | ...styleProp, 42 | ...popupStyle, 43 | opacity, 44 | transform: interpolate( 45 | [topOffset], 46 | (topOffset) => 47 | `translate3d(${parseInt(left as string, 10)}px, ${ 48 | parseInt(top as string, 10) + (topOffset as number) 49 | }px, 0)`, 50 | ) as any, 51 | }) 52 | -------------------------------------------------------------------------------- /examples/popup-dialog/example.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Popup Dialog 3 | packages: 4 | - retil-interaction 5 | --- 6 | 7 | # <Title /> 8 | 9 | <Example /> 10 | 11 | 12 | ## app.tsx 13 | 14 | <Source filename='app.tsx' /> 15 | -------------------------------------------------------------------------------- /examples/popup-dialog/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as App } from './app' 2 | export { default as Doc, meta } from './example.mdx' 3 | export const sources = import.meta.highlightedSourceGlobEager('./*.tsx') 4 | -------------------------------------------------------------------------------- /examples/provideMediaSelectors-emotion/example.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Overriding Media Selectors (emotion) 3 | packages: 4 | - retil-css 5 | - retil-media 6 | --- 7 | 8 | # <Title /> 9 | 10 | <Example /> 11 | 12 | 13 | ## app.tsx 14 | 15 | <Source filename='app.tsx' /> 16 | -------------------------------------------------------------------------------- /examples/provideMediaSelectors-emotion/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as App } from './app' 2 | export { default as Doc, meta } from './example.mdx' 3 | export const sources = import.meta.highlightedSourceGlobEager('./*.tsx') 4 | -------------------------------------------------------------------------------- /examples/provideMediaSelectors-styled/example.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Overriding Media Selectors (styled components) 3 | packages: 4 | - retil-css 5 | - retil-media 6 | --- 7 | 8 | # <Title /> 9 | 10 | <Example /> 11 | 12 | 13 | ## app.tsx 14 | 15 | <Source filename='app.styled.tsx' /> 16 | -------------------------------------------------------------------------------- /examples/provideMediaSelectors-styled/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as App } from './app.styled' 2 | export { default as Doc, meta } from './example.mdx' 3 | export const sources = import.meta.highlightedSourceGlobEager('./*.tsx') 4 | export const styledComponents = true 5 | -------------------------------------------------------------------------------- /examples/suspense-loading-indicators/example.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Suspense Loading Indicators 3 | packages: 4 | - retil-mount 5 | - retil-nav 6 | --- 7 | 8 | # <Title /> 9 | 10 | <Example /> 11 | 12 | 13 | ## app.tsx 14 | 15 | <Source filename='app.tsx' /> 16 | 17 | ## main.tsx 18 | 19 | <Source filename='main.tsx' /> 20 | -------------------------------------------------------------------------------- /examples/suspense-loading-indicators/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './main' 2 | export { default as Doc, meta } from './example.mdx' 3 | export const sources = import.meta.highlightedSourceGlobEager('./*.tsx') 4 | export const matchNestedRoutes = true 5 | -------------------------------------------------------------------------------- /examples/suspense-loading-indicators/main.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from 'react' 2 | import { ServerMount } from 'retil-mount' 3 | import { 4 | createServerNavEnv, 5 | NavEnvService, 6 | NavRequest, 7 | NavResponse, 8 | } from 'retil-nav' 9 | 10 | import { App, appLoader } from './app' 11 | 12 | export async function clientMain( 13 | render: (element: ReactElement) => void, 14 | getDefaultBrowserNavEnvService: () => NavEnvService, 15 | ) { 16 | const [envSource] = getDefaultBrowserNavEnvService() 17 | render(<App env={envSource} />) 18 | } 19 | 20 | export async function serverMain( 21 | render: (element: ReactElement) => void, 22 | request: NavRequest, 23 | response: NavResponse, 24 | ) { 25 | const env = createServerNavEnv(request, response) 26 | const mount = new ServerMount(appLoader, env) 27 | try { 28 | await mount.preload() 29 | render(mount.provide(<App env={env} />)) 30 | } finally { 31 | mount.seal() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "skipLibCheck": false, 5 | "esModuleInterop": false, 6 | "allowSyntheticDefaultImports": true, 7 | "isolatedModules": true, 8 | "jsx": "react-jsx", 9 | "jsxImportSource": "@emotion/react", 10 | "types": ["@emotion/react"], 11 | "noEmit": true, 12 | }, 13 | "include": ["../site/src/@types/**.ts", "**/*.tsx"] 14 | } 15 | -------------------------------------------------------------------------------- /examples/useMediaRenderer-emotion/app.tsx: -------------------------------------------------------------------------------- 1 | import { media, useMediaRenderer } from 'retil-media' 2 | 3 | const App = () => { 4 | const renderWhenLarge = useMediaRenderer(media.large) 5 | const renderWhenMedium = useMediaRenderer(media.medium) 6 | const renderWhenSmall = useMediaRenderer(media.small) 7 | 8 | return ( 9 | <> 10 | {renderWhenLarge((hideCSS) => ( 11 | <div css={hideCSS}>Large</div> 12 | ))} 13 | {renderWhenMedium((hideCSS) => ( 14 | <div css={hideCSS}>Medium</div> 15 | ))} 16 | {renderWhenSmall((hideCSS) => ( 17 | <div css={hideCSS}>Small</div> 18 | ))} 19 | </> 20 | ) 21 | } 22 | 23 | export default App 24 | -------------------------------------------------------------------------------- /examples/useMediaRenderer-emotion/example.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: useMediaRenderer (emotion) 3 | packages: 4 | - retil-css 5 | - retil-media 6 | --- 7 | 8 | # <Title /> 9 | 10 | The `useMediaRenderer` hook returns a function which can be used to render content only when the media query matches. 11 | 12 | The render function should itself receive a function that will receive an argument that can be passed to your rendered element's `css` prop to hide the element using plain CSS, which is useful for server rendering. 13 | 14 | You can see the example in action by resizing this page – a different element will be rendered below based on the size of the page. 15 | 16 | <Example /> 17 | 18 | 19 | ## app.tsx 20 | 21 | <Source filename='app.tsx' /> 22 | -------------------------------------------------------------------------------- /examples/useMediaRenderer-emotion/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as App } from './app' 2 | export { default as Doc, meta } from './example.mdx' 3 | export const sources = import.meta.highlightedSourceGlobEager('./*.tsx') 4 | -------------------------------------------------------------------------------- /examples/useMediaRenderer-styled/app.styled.tsx: -------------------------------------------------------------------------------- 1 | import { media, useMediaRenderer } from 'retil-media' 2 | import styled from 'styled-components' 3 | 4 | const StyledDiv = styled.div<{ x: any }>` 5 | ${(props) => props.x} 6 | ` 7 | 8 | const App = () => { 9 | const renderWhenLarge = useMediaRenderer(media.large) 10 | const renderWhenMedium = useMediaRenderer(media.medium) 11 | const renderWhenSmall = useMediaRenderer(media.small) 12 | 13 | return ( 14 | <> 15 | {renderWhenLarge((x) => ( 16 | <StyledDiv x={x}>Large</StyledDiv> 17 | ))} 18 | {renderWhenMedium((x) => ( 19 | <StyledDiv x={x}>Medium</StyledDiv> 20 | ))} 21 | {renderWhenSmall((x) => ( 22 | <StyledDiv x={x}>Small</StyledDiv> 23 | ))} 24 | </> 25 | ) 26 | } 27 | 28 | export default App 29 | -------------------------------------------------------------------------------- /examples/useMediaRenderer-styled/example.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: useMediaRenderer (styled components) 3 | packages: 4 | - retil-css 5 | - retil-media 6 | --- 7 | 8 | # <Title /> 9 | 10 | The `useMediaRenderer` hook returns a function which can be used to render content only when the media query matches. 11 | 12 | The render function should itself receive a function that will receive an argument that can be passed to your rendered element's `css` prop to hide the element using plain CSS, which is useful for server rendering. 13 | 14 | You can see the example in action by resizing this page – a different element will be rendered below based on the size of the page. 15 | 16 | <Example /> 17 | 18 | 19 | ## app.tsx 20 | 21 | <Source filename='app.styled.tsx' /> 22 | -------------------------------------------------------------------------------- /examples/useMediaRenderer-styled/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as App } from './app.styled' 2 | export { default as Doc, meta } from './example.mdx' 3 | export const sources = import.meta.highlightedSourceGlobEager('./*.tsx') 4 | export const styledComponents = true 5 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "npmClient": "yarn", 3 | "useWorkspaces": true, 4 | "lerna": "3.22.1", 5 | "packages": [ 6 | "packages/*" 7 | ], 8 | "version": "0.28.4" 9 | } 10 | -------------------------------------------------------------------------------- /packages/retil-boundary/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 James K Nelson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | 'Software'), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /packages/retil-boundary/jest.config.js: -------------------------------------------------------------------------------- 1 | const { pathsToModuleNameMapper } = require('ts-jest/utils') 2 | // In the following statement, replace `./tsconfig` with the path to your `tsconfig` file 3 | // which contains the path mapping (ie the `compilerOptions.paths` option): 4 | const { compilerOptions } = require('../../tsconfig') 5 | 6 | module.exports = { 7 | moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { 8 | prefix: '<rootDir>/../../', 9 | }), 10 | modulePaths: ['<rootDir>/src/'], 11 | modulePathIgnorePatterns: ['<rootDir>/demo/'], 12 | preset: 'ts-jest', 13 | testEnvironment: 'jsdom', 14 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$', 15 | } 16 | -------------------------------------------------------------------------------- /packages/retil-boundary/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "retil-boundary", 3 | "version": "0.28.4", 4 | "description": "Superpowers for concurrent React apps.", 5 | "author": "James K Nelson <james@jamesknelson.com>", 6 | "license": "MIT", 7 | "main": "dist/commonjs/index.js", 8 | "module": "dist/es/index.js", 9 | "types": "dist/es/index.d.ts", 10 | "scripts": { 11 | "clean": "rimraf dist", 12 | "build:commonjs": "tsc -p tsconfig.build.json --module commonjs --outDir dist/commonjs", 13 | "build:es-and-types": "tsc -p tsconfig.build.json --module es2015 --outDir dist/es --declaration", 14 | "build": "yarn run clean && yarn build:es-and-types && yarn build:commonjs", 15 | "build:watch": "yarn run clean && yarn build:es-and-types -- --watch", 16 | "lint": "eslint --ext ts,tsx src", 17 | "prepare": "yarn test && yarn build", 18 | "test": "jest", 19 | "test:watch": "jest --watch" 20 | }, 21 | "dependencies": { 22 | "tslib": "^2.2.0" 23 | }, 24 | "devDependencies": { 25 | "typescript": "4.6.2" 26 | }, 27 | "files": [ 28 | "dist" 29 | ], 30 | "keywords": [ 31 | "react", 32 | "loader" 33 | ], 34 | "gitHead": "6c99063d3d8d4539ae81cce4f1b172ac2948c951" 35 | } 36 | -------------------------------------------------------------------------------- /packages/retil-boundary/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './boundary' 2 | -------------------------------------------------------------------------------- /packages/retil-boundary/test/boundaryEffect.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, cleanup } from '@testing-library/react' 2 | import React from 'react' 3 | 4 | import { useBoundaryEffect } from '../src' 5 | 6 | afterEach(cleanup) 7 | 8 | describe('useBoundaryEffect', () => { 9 | test('works without boundary', () => { 10 | let passes = false 11 | function Test() { 12 | useBoundaryEffect(() => { 13 | passes = true 14 | }) 15 | return null 16 | } 17 | 18 | render(<Test />) 19 | 20 | expect(passes).toBe(true) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /packages/retil-boundary/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.build.json", 3 | "include": [ 4 | "src/**/*" 5 | ] 6 | } -------------------------------------------------------------------------------- /packages/retil-css/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 James K Nelson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | 'Software'), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /packages/retil-css/jest.config.js: -------------------------------------------------------------------------------- 1 | const { pathsToModuleNameMapper } = require('ts-jest/utils') 2 | // In the following statement, replace `./tsconfig` with the path to your `tsconfig` file 3 | // which contains the path mapping (ie the `compilerOptions.paths` option): 4 | const { compilerOptions } = require('../../tsconfig') 5 | 6 | module.exports = { 7 | moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { 8 | prefix: '<rootDir>/../../', 9 | }), 10 | modulePaths: ['<rootDir>/src/'], 11 | modulePathIgnorePatterns: ['<rootDir>/demo/'], 12 | preset: 'ts-jest', 13 | testEnvironment: 'jsdom', 14 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$', 15 | } 16 | -------------------------------------------------------------------------------- /packages/retil-css/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "retil-css", 3 | "version": "0.28.4", 4 | "description": "Superpowers for decoupling style from behavior", 5 | "author": "James K Nelson <james@jamesknelson.com>", 6 | "license": "MIT", 7 | "main": "dist/commonjs/index.js", 8 | "module": "dist/es/index.js", 9 | "types": "dist/es/index.d.ts", 10 | "scripts": { 11 | "clean": "rimraf dist", 12 | "build:commonjs": "tsc -p tsconfig.build.json --module commonjs --outDir dist/commonjs", 13 | "build:es-and-types": "tsc -p tsconfig.build.json --module es2015 --outDir dist/es --declaration", 14 | "build": "yarn run clean && yarn build:es-and-types && yarn build:commonjs", 15 | "build:watch": "yarn run clean && yarn build:es-and-types -- --watch", 16 | "lint": "eslint --ext ts,tsx src", 17 | "prepare": "yarn test && yarn build", 18 | "test": "jest", 19 | "test:watch": "jest --watch" 20 | }, 21 | "dependencies": { 22 | "fast-cartesian": "^5.1.0", 23 | "retil-hydration": "^0.28.4", 24 | "retil-support": "^0.28.4", 25 | "tslib": "^2.2.0" 26 | }, 27 | "devDependencies": { 28 | "typescript": "4.6.2" 29 | }, 30 | "peerDependencies": { 31 | "react": "^17.0.1" 32 | }, 33 | "files": [ 34 | "dist" 35 | ], 36 | "gitHead": "9d352aa11ce02bdf184dfe8ff15a90a3ad4543bc" 37 | } 38 | -------------------------------------------------------------------------------- /packages/retil-css/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const selectionsSymbol = Symbol.for('retil:css:selections') 2 | export const themeRiderSymbol = Symbol.for('retil:css:themeRider') 3 | -------------------------------------------------------------------------------- /packages/retil-css/src/index.ts: -------------------------------------------------------------------------------- 1 | export type { CSSProviderProps } from './context' 2 | export { CSSProvider, useCSSRuntime } from './context' 3 | export * from './highStyle' 4 | export * from './selector' 5 | export * from './stringifyTransition' 6 | export * from './types' 7 | -------------------------------------------------------------------------------- /packages/retil-css/src/types.ts: -------------------------------------------------------------------------------- 1 | import * as CSS from 'csstype' 2 | 3 | import type { themeRiderSymbol } from './constants' 4 | 5 | export type BaseExtensibleObject = { 6 | [key: string]: any 7 | } 8 | 9 | export interface CSSThemeRider { 10 | runtime: CSSRuntime 11 | selectorTypeContexts: unknown[] 12 | } 13 | 14 | export type CSSInterpolationContext<Theme extends CSSTheme = CSSTheme> = 15 | | Theme 16 | | (BaseExtensibleObject & { 17 | theme?: Theme 18 | }) 19 | 20 | export type CSSTheme = BaseExtensibleObject & { 21 | [themeRiderSymbol]?: CSSThemeRider 22 | } 23 | 24 | export interface CSSRuntime { 25 | (template: TemplateStringsArray, ...args: Array<any>): any 26 | (...args: Array<any>): any 27 | } 28 | 29 | // Equivalent to the CSSObject type expected by styled-components and emotion. 30 | export type CSSProperties = CSS.Properties<string | number> 31 | export type CSSPropertiesWithMultiValues = { 32 | [K in keyof CSSProperties]: 33 | | CSSProperties[K] 34 | | Array<Extract<CSSProperties[K], string>> 35 | } 36 | 37 | export type CSSPseudos = { [K in CSS.Pseudos]?: CSSObject } 38 | 39 | export interface CSSObject 40 | extends CSSPropertiesWithMultiValues, 41 | CSSPseudos, 42 | CSSOthersObject {} 43 | 44 | export interface CSSOthersObject { 45 | [propertiesName: string]: any 46 | } 47 | 48 | // When an array of CSS selector strings is provided, any of those selectors 49 | // will be used to match the state. When `true`, the applicable styles will 50 | // always be used. When `false`, they never will be. 51 | export type CSSSelector = string[] | string | boolean 52 | -------------------------------------------------------------------------------- /packages/retil-css/test/stringifyTransition.test.ts: -------------------------------------------------------------------------------- 1 | import { stringifyTransition } from '../src' 2 | 3 | describe('stringifyTransition()', () => { 4 | test('sets defaults on passed-in transitions', () => { 5 | const str = stringifyTransition( 6 | { property: 'transform' }, 7 | { 8 | defaults: { 9 | duration: '500ms', 10 | timing: 'linear', 11 | }, 12 | }, 13 | ) 14 | 15 | expect(str).toEqual('transform 500ms linear') 16 | }) 17 | 18 | test('uses ms as the default unit for numeric duration and delay', () => { 19 | const str = stringifyTransition({ 20 | property: 'transform', 21 | duration: 500, 22 | delay: 500, 23 | timing: 'linear', 24 | }) 25 | 26 | expect(str).toEqual('transform 500ms linear 500ms') 27 | }) 28 | 29 | test('maps property groups', () => { 30 | const str = stringifyTransition( 31 | { property: 'size', duration: '500ms', timing: 'linear' }, 32 | { 33 | properties: { 34 | size: ['margin-bottom', 'border-width'], 35 | }, 36 | }, 37 | ) 38 | 39 | expect(str).toEqual('margin-bottom 500ms linear, border-width 500ms linear') 40 | }) 41 | 42 | test('uses all specified properties as the default', () => { 43 | const str = stringifyTransition( 44 | { duration: '500ms', timing: 'linear' }, 45 | { 46 | properties: { 47 | color: true, 48 | size: 'margin-bottom', 49 | }, 50 | }, 51 | ) 52 | 53 | expect(str).toEqual('color 500ms linear, margin-bottom 500ms linear') 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /packages/retil-css/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.build.json", 3 | "include": [ 4 | "src/**/*" 5 | ] 6 | } -------------------------------------------------------------------------------- /packages/retil-hydration/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 James K Nelson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | 'Software'), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /packages/retil-hydration/jest.config.js: -------------------------------------------------------------------------------- 1 | const { pathsToModuleNameMapper } = require('ts-jest/utils') 2 | // In the following statement, replace `./tsconfig` with the path to your `tsconfig` file 3 | // which contains the path mapping (ie the `compilerOptions.paths` option): 4 | const { compilerOptions } = require('../../tsconfig') 5 | 6 | module.exports = { 7 | moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { 8 | prefix: '<rootDir>/../../', 9 | }), 10 | modulePaths: ['<rootDir>/src/'], 11 | modulePathIgnorePatterns: ['<rootDir>/demo/'], 12 | preset: 'ts-jest', 13 | testEnvironment: 'jsdom', 14 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$', 15 | } 16 | -------------------------------------------------------------------------------- /packages/retil-hydration/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "retil-hydration", 3 | "version": "0.28.4", 4 | "description": "Superpowers for hydration of server-rendered React.", 5 | "author": "James K Nelson <james@jamesknelson.com>", 6 | "license": "MIT", 7 | "main": "dist/commonjs/index.js", 8 | "module": "dist/es/index.js", 9 | "types": "dist/es/index.d.ts", 10 | "scripts": { 11 | "clean": "rimraf dist", 12 | "build:commonjs": "tsc -p tsconfig.build.json --module commonjs --outDir dist/commonjs", 13 | "build:es-and-types": "tsc -p tsconfig.build.json --module es2015 --outDir dist/es --declaration", 14 | "build": "yarn run clean && yarn build:es-and-types && yarn build:commonjs", 15 | "build:watch": "yarn run clean && yarn build:es-and-types -- --watch", 16 | "lint": "eslint --ext ts,tsx src", 17 | "prepare": "yarn test && yarn build", 18 | "test": "jest", 19 | "test:watch": "jest --watch" 20 | }, 21 | "dependencies": { 22 | "retil-boundary": "^0.28.4", 23 | "retil-mount": "^0.28.4", 24 | "retil-source": "^0.28.4", 25 | "retil-support": "^0.28.4", 26 | "tslib": "^2.2.0" 27 | }, 28 | "devDependencies": { 29 | "typescript": "4.6.2" 30 | }, 31 | "files": [ 32 | "dist" 33 | ], 34 | "keywords": [ 35 | "react", 36 | "loader" 37 | ], 38 | "gitHead": "9d352aa11ce02bdf184dfe8ff15a90a3ad4543bc" 39 | } 40 | -------------------------------------------------------------------------------- /packages/retil-hydration/src/hydrationTypes.ts: -------------------------------------------------------------------------------- 1 | import { Source } from 'retil-source' 2 | 3 | export interface HydrationEnv { 4 | hydrating?: boolean 5 | } 6 | 7 | export type HydrationEnvSource = Source<HydrationEnv> 8 | 9 | export type HydrationEnvService = readonly [ 10 | source: HydrationEnvSource, 11 | completeHydration: () => void, 12 | ] 13 | -------------------------------------------------------------------------------- /packages/retil-hydration/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './hydrationContext' 2 | export { 3 | createBrowserHydrationEnvService, 4 | getDefaultHydrationEnvService, 5 | } from './hydrationEnvService' 6 | export * from './hydrationTypes' 7 | -------------------------------------------------------------------------------- /packages/retil-hydration/test/hydrationContext.test.tsx: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/extend-expect' 2 | import { act, render, cleanup } from '@testing-library/react' 3 | import React from 'react' 4 | 5 | import { useHasHydrated, useMarkAsHydrated } from '../src' 6 | 7 | afterEach(cleanup) 8 | 9 | describe('useHasHydrated', () => { 10 | test('returns false until hydrated', () => { 11 | let markAsHydrated: () => void 12 | 13 | function HasHydrated() { 14 | markAsHydrated = useMarkAsHydrated() 15 | return <>{String(useHasHydrated())}</> 16 | } 17 | 18 | const { container } = render(<HasHydrated />) 19 | 20 | expect(container).toHaveTextContent('false') 21 | 22 | act(() => { 23 | markAsHydrated() 24 | }) 25 | 26 | expect(container).toHaveTextContent('true') 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /packages/retil-hydration/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.build.json", 3 | "include": [ 4 | "src/**/*" 5 | ] 6 | } -------------------------------------------------------------------------------- /packages/retil-interaction/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 James K Nelson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | 'Software'), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /packages/retil-interaction/README.md: -------------------------------------------------------------------------------- 1 | retil-interactions 2 | ================== 3 | 4 | **Superpowers for building interactive components.** 5 | 6 | React gave you the power of re-usable components. Retil Interactions gives you the power of re-usable *behaviors*. 7 | 8 | - Create re-usable focus and hover indicators. 9 | - Re-use the same styled components across your links, forms, menus, navbars, popup triggers, toggles, and other behaviors. 10 | - Create controls which keep focus in one place, while containing many different interactive surfaces. 11 | - Easily add smartly-positioned popups which respond to focus, hover or press events. 12 | -------------------------------------------------------------------------------- /packages/retil-interaction/jest.config.js: -------------------------------------------------------------------------------- 1 | const { pathsToModuleNameMapper } = require('ts-jest/utils') 2 | // In the following statement, replace `./tsconfig` with the path to your `tsconfig` file 3 | // which contains the path mapping (ie the `compilerOptions.paths` option): 4 | const { compilerOptions } = require('../../tsconfig') 5 | 6 | module.exports = { 7 | moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { 8 | prefix: '<rootDir>/../../', 9 | }), 10 | modulePaths: ['<rootDir>/src/'], 11 | modulePathIgnorePatterns: ['<rootDir>/demo/'], 12 | preset: 'ts-jest', 13 | testEnvironment: 'jsdom', 14 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$', 15 | } 16 | -------------------------------------------------------------------------------- /packages/retil-interaction/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "retil-interaction", 3 | "version": "0.28.4", 4 | "description": "Utilities to create unstyled interactive components", 5 | "author": "James K Nelson <james@jamesknelson.com>", 6 | "license": "MIT", 7 | "main": "dist/commonjs/index.js", 8 | "module": "dist/es/index.js", 9 | "types": "dist/es/index.d.ts", 10 | "scripts": { 11 | "clean": "rimraf dist", 12 | "build:commonjs": "tsc -p tsconfig.build.json --module commonjs --outDir dist/commonjs", 13 | "build:es-and-types": "tsc -p tsconfig.build.json --module es2015 --outDir dist/es --declaration", 14 | "build": "yarn run clean && yarn build:es-and-types && yarn build:commonjs", 15 | "build:watch": "yarn run clean && yarn build:es-and-types -- --watch", 16 | "lint": "eslint --ext ts,tsx src", 17 | "prepare": "yarn test && yarn build", 18 | "test": "jest", 19 | "test:watch": "jest --watch" 20 | }, 21 | "dependencies": { 22 | "@popperjs/core": "^2.11.2", 23 | "focus-trap": "^6.7.2", 24 | "retil-css": "^0.28.4", 25 | "retil-source": "^0.28.4", 26 | "retil-support": "^0.28.4", 27 | "tslib": "^2.2.0" 28 | }, 29 | "devDependencies": { 30 | "@emotion/jest": "^11.8.0", 31 | "@emotion/react": "^11.8.1", 32 | "typescript": "4.6.2" 33 | }, 34 | "peerDependencies": { 35 | "react": "^17.0.1" 36 | }, 37 | "files": [ 38 | "dist" 39 | ], 40 | "gitHead": "9d352aa11ce02bdf184dfe8ff15a90a3ad4543bc" 41 | } 42 | -------------------------------------------------------------------------------- /packages/retil-interaction/src/connector.ts: -------------------------------------------------------------------------------- 1 | import type { identity } from 'retil-support' 2 | 3 | export interface ConnectorMergeProps< 4 | TMergeableProps extends object, 5 | TMergedProps extends object, 6 | > { 7 | <TMergeProps extends TMergeableProps & Record<string, any>>( 8 | mergeProps?: TMergeProps & TMergeableProps & Record<string, any>, 9 | ): Omit<TMergeProps, keyof TMergeableProps> & TMergedProps 10 | } 11 | 12 | export type ConnectorProvide = (children: React.ReactNode) => React.ReactElement 13 | 14 | export type Connector< 15 | TSnapshot extends object = {}, 16 | TMergeProps extends ConnectorMergeProps<any, any> = typeof identity, 17 | > = readonly [ 18 | snapshot: TSnapshot, 19 | mergeProps: TMergeProps, 20 | provide: ConnectorProvide, 21 | ] 22 | 23 | // function combineConnectors(connectors: []) 24 | 25 | // function connect(connector, componentType, props, ...children) 26 | -------------------------------------------------------------------------------- /packages/retil-interaction/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './surfaces/actionSurface' 2 | export * from './surfaces/anchorSurface' 3 | export * from './surfaces/buttonSurface' 4 | export * from './surfaces/linkSurface' 5 | export * from './surfaces/matchedLinkSurface' 6 | export * from './surfaces/modalSurface' 7 | export * from './surfaces/popupDialogSurface' 8 | export * from './surfaces/popupMenuSurface' 9 | export * from './surfaces/popupTriggerSurface' 10 | export * from './surfaces/submitButtonSurface' 11 | 12 | export * from './connector' 13 | export * from './defaultSurfaceSelectors' 14 | export * from './disableable' 15 | export * from './escape' 16 | export * from './focusable' 17 | export * from './focusableSelectable' 18 | export * from './focusableSelection' 19 | export * from './focusableTrap' 20 | export * from './keyboard' 21 | export * from './listCursor' 22 | export * from './menu' 23 | export * from './popup' 24 | export * from './popupPositioner' 25 | export * from './popupTrigger' 26 | export * from './surfaceSelector' 27 | export * from './unscrollableBody' 28 | -------------------------------------------------------------------------------- /packages/retil-interaction/src/surfaces/anchorSurface.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Anchors cannot be disabled or have delegated focus, as their behavior is 3 | * handled natively by the browser, even before the page is hydrated – and 4 | * there is no native way to disable them or delegate their focus. 5 | */ 6 | 7 | import React, { forwardRef } from 'react' 8 | 9 | import { 10 | ActionSurfaceOptions, 11 | splitActionSurfaceOptions, 12 | useActionSurfaceConnector, 13 | } from './actionSurface' 14 | 15 | export interface AnchorSurfaceProps 16 | extends React.AnchorHTMLAttributes<HTMLAnchorElement>, 17 | ActionSurfaceOptions {} 18 | 19 | export const AnchorSurface = /*#__PURE__*/ forwardRef< 20 | HTMLAnchorElement, 21 | AnchorSurfaceProps 22 | >((props, ref) => { 23 | const [actionSurfaceOptions, rest] = splitActionSurfaceOptions(props) 24 | const [actionSurfaceState, mergeActionSurfaceProps, provideActionSurface] = 25 | useActionSurfaceConnector(actionSurfaceOptions) 26 | 27 | return provideActionSurface( 28 | // eslint-disable-next-line jsx-a11y/anchor-has-content 29 | <a 30 | {...mergeActionSurfaceProps({ 31 | ...rest, 32 | ref, 33 | // <a> tags can't be disabled during SSR, but are still rendered 34 | // before the page becomes active, so if necessary we'll disable 35 | // them by removing the `href`. 36 | href: actionSurfaceState.disabled ? undefined : props.href, 37 | })} 38 | />, 39 | ) 40 | }) 41 | -------------------------------------------------------------------------------- /packages/retil-interaction/src/unscrollableBody.tsx: -------------------------------------------------------------------------------- 1 | import { useSilencedLayoutEffect } from 'retil-support' 2 | 3 | let activeDisableScrollingCount = 0 4 | 5 | export function useUnscrollableBody(active = true, returnTo = 'auto') { 6 | useSilencedLayoutEffect(() => { 7 | if (active) { 8 | activeDisableScrollingCount += 1 9 | if (activeDisableScrollingCount === 1) { 10 | document.body.style.overflow = 'hidden' 11 | } 12 | return () => { 13 | activeDisableScrollingCount -= 1 14 | if (activeDisableScrollingCount === 0) { 15 | document.body.style.overflow = returnTo 16 | } 17 | } 18 | } 19 | }, [active]) 20 | } 21 | -------------------------------------------------------------------------------- /packages/retil-interaction/test/popupDialog.test.tsx: -------------------------------------------------------------------------------- 1 | import { css, ThemeContext } from '@emotion/react' 2 | import React from 'react' 3 | import { CSSProvider } from 'retil-css' 4 | import { act, render, fireEvent, cleanup } from '@testing-library/react' 5 | import '@testing-library/jest-dom/extend-expect' 6 | 7 | import { PopupDialogSurface, PopupProvider, PopupTriggerSurface } from '../src' 8 | 9 | afterEach(cleanup) 10 | 11 | describe('PopupDialog', () => { 12 | describe('with default settings', () => { 13 | test('renders after clicking the trigger', async () => { 14 | const { getByTestId } = render( 15 | <CSSProvider runtime={css} themeContext={ThemeContext}> 16 | <PopupProvider> 17 | <PopupTriggerSurface data-testid="trigger"> 18 | trigger 19 | </PopupTriggerSurface> 20 | <PopupDialogSurface data-testid="popup" placement="bottom"> 21 | popup 22 | </PopupDialogSurface> 23 | </PopupProvider> 24 | </CSSProvider>, 25 | ) 26 | 27 | expect(getByTestId('popup')).not.toBeVisible() 28 | 29 | await act(async () => { 30 | const trigger = getByTestId('trigger') 31 | fireEvent.mouseDown(trigger) 32 | fireEvent.mouseUp(trigger) 33 | }) 34 | 35 | expect(getByTestId('popup')).toBeVisible() 36 | }) 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /packages/retil-interaction/test/surfaceSelectors.test.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { matchers } from '@emotion/jest' 3 | import { css, jsx, ThemeContext } from '@emotion/react' 4 | import { render, cleanup } from '@testing-library/react' 5 | import { forwardRef } from 'react' 6 | import { CSSProvider, highStyle } from 'retil-css' 7 | 8 | import { inHoveredSurface, useSurfaceSelectorsConnector } from '../src' 9 | 10 | // Add the custom matchers provided by '@emotion/jest' 11 | expect.extend(matchers) 12 | 13 | afterEach(cleanup) 14 | 15 | describe('Surface Selectors', () => { 16 | const ButtonBody = forwardRef<HTMLDivElement, any>((props, ref) => ( 17 | <div 18 | {...props} 19 | ref={ref} 20 | css={highStyle({ 21 | color: { 22 | default: 'black', 23 | [inHoveredSurface]: 'red', 24 | }, 25 | })} 26 | /> 27 | )) 28 | 29 | test('add pseudoselector styles when appropriate', () => { 30 | function Test() { 31 | const [, mergeProps, provide] = useSurfaceSelectorsConnector() 32 | 33 | return provide( 34 | <button {...mergeProps()}> 35 | <ButtonBody data-testid="body" /> 36 | </button>, 37 | ) 38 | } 39 | 40 | const { getByTestId } = render( 41 | // Use a raw SurfaceController so that we can set the surfaceClassName 42 | // prop, as we need to know what it is when looking for the selector. 43 | <CSSProvider runtime={css} themeContext={ThemeContext}> 44 | <Test /> 45 | </CSSProvider>, 46 | ) 47 | 48 | expect(getByTestId('body')).toHaveStyleRule('color', 'red', { 49 | target: '.rx-1:hover', 50 | }) 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /packages/retil-interaction/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.build.json", 3 | "include": [ 4 | "src/**/*" 5 | ] 6 | } -------------------------------------------------------------------------------- /packages/retil-issues/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 James K Nelson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | 'Software'), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /packages/retil-issues/jest.config.js: -------------------------------------------------------------------------------- 1 | const { pathsToModuleNameMapper } = require('ts-jest/utils') 2 | // In the following statement, replace `./tsconfig` with the path to your `tsconfig` file 3 | // which contains the path mapping (ie the `compilerOptions.paths` option): 4 | const { compilerOptions } = require('../../tsconfig') 5 | 6 | module.exports = { 7 | moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { 8 | prefix: '<rootDir>/../../', 9 | }), 10 | modulePaths: ['<rootDir>/src/'], 11 | modulePathIgnorePatterns: ['<rootDir>/demo/'], 12 | preset: 'ts-jest', 13 | testEnvironment: 'jsdom', 14 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$', 15 | } 16 | -------------------------------------------------------------------------------- /packages/retil-issues/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "retil-issues", 3 | "version": "0.28.4", 4 | "description": "Super-powers for validation with React.", 5 | "author": "James K Nelson <james@jamesknelson.com>", 6 | "license": "MIT", 7 | "main": "dist/commonjs/index.js", 8 | "module": "dist/es/index.js", 9 | "types": "dist/es/index.d.ts", 10 | "scripts": { 11 | "clean": "rimraf dist", 12 | "build:commonjs": "tsc -p tsconfig.build.json --module commonjs --outDir dist/commonjs", 13 | "build:es-and-types": "tsc -p tsconfig.build.json --module es2015 --outDir dist/es --declaration", 14 | "build": "yarn run clean && yarn build:es-and-types && yarn build:commonjs", 15 | "build:watch": "yarn run clean && yarn build:es-and-types -- --watch", 16 | "lint": "eslint --ext ts,tsx src", 17 | "prepare": "yarn test && yarn build", 18 | "test": "jest", 19 | "test:watch": "jest --watch" 20 | }, 21 | "dependencies": { 22 | "retil-support": "^0.28.4", 23 | "tslib": "^2.2.0" 24 | }, 25 | "devDependencies": { 26 | "typescript": "4.6.2" 27 | }, 28 | "files": [ 29 | "dist" 30 | ], 31 | "gitHead": "9d352aa11ce02bdf184dfe8ff15a90a3ad4543bc" 32 | } 33 | -------------------------------------------------------------------------------- /packages/retil-issues/src/getIssueMessage.ts: -------------------------------------------------------------------------------- 1 | import { Root } from 'retil-support' 2 | 3 | export function getIssueMessage< 4 | TIssues extends { 5 | path: string | Root 6 | code: string 7 | }, 8 | >( 9 | issue: TIssues, 10 | messages: { 11 | [Path in TIssues['path']]: { 12 | [C in Extract<TIssues, { path: Path }>['code']]: string 13 | } 14 | }, 15 | ): string 16 | export function getIssueMessage< 17 | TIssues extends { 18 | path: string | Root 19 | code: string 20 | }, 21 | >( 22 | issue: TIssues, 23 | messages: { 24 | [Path in TIssues['path']]?: { 25 | [C in Extract<TIssues, { path: Path }>['code']]?: string 26 | } 27 | }, 28 | fallbackMessages: { 29 | [C in TIssues['code']]?: string 30 | }, 31 | ): string | undefined 32 | export function getIssueMessage< 33 | TIssues extends { 34 | path: string | Root 35 | code: string 36 | }, 37 | >( 38 | issue: TIssues, 39 | messages: Partial<Record<string, Partial<Record<string, string>>>>, 40 | fallbackMessages: Partial<Record<string, string>> = {}, 41 | ): string | undefined { 42 | return (messages[issue.path as never] ?? fallbackMessages)[issue.code] 43 | } 44 | -------------------------------------------------------------------------------- /packages/retil-issues/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './getIssueMessage' 2 | export * from './issueTypes' 3 | // export * from './useAsyncValidator' 4 | export * from './useIssues' 5 | export * from './useValidator' 6 | -------------------------------------------------------------------------------- /packages/retil-issues/src/useAsyncValidator.ts: -------------------------------------------------------------------------------- 1 | import { AddIssuesFunction, AsyncValidator, CodesByPath } from './issueTypes' 2 | 3 | export interface UseAsyncValidatorOptions<Data> { 4 | debounceMs?: number 5 | resolveWhen?: (latestData: Data, invalidData: Data) => boolean 6 | } 7 | 8 | // calling `trigger` will first run the async validation function, and only *if* 9 | // it returns an issue, will the issue be added. in this case, the issue will be 10 | // set to be removed if any of the used data changes, but this can be configured 11 | // by passing `resolver` function. 12 | // `valid` will be `null` before the first validation, and `undefined` while 13 | // pending. 14 | export function useAynscValidator< 15 | TValue extends object, 16 | TCodes extends CodesByPath = CodesByPath<TValue>, 17 | >( 18 | issues: AddIssuesFunction<TValue, TCodes>, 19 | validator: AsyncValidator<TValue, TCodes>, 20 | options: UseAsyncValidatorOptions<TValue> = {}, 21 | ): readonly [trigger: () => Promise<boolean>, valid?: boolean | null] { 22 | throw new Error('unimplemented') 23 | 24 | // TODO: 25 | // - wrap the validator data in a proxy so we can record what data is used, 26 | // and automatically create a revalidate function which removes the issue 27 | // when any of that data changes 28 | 29 | // const { 30 | // debounceMs = 200, 31 | // resolveWhen = areNotShallowEqual, 32 | // } = options 33 | } 34 | -------------------------------------------------------------------------------- /packages/retil-issues/test/useValidator.test.tsx: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/extend-expect' 2 | 3 | describe('useValidator', () => { 4 | test.todo(`returns a function that runs validation`) 5 | }) 6 | -------------------------------------------------------------------------------- /packages/retil-issues/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.build.json", 3 | "include": [ 4 | "src/**/*" 5 | ] 6 | } -------------------------------------------------------------------------------- /packages/retil-media/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 James K Nelson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | 'Software'), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /packages/retil-media/jest.config.js: -------------------------------------------------------------------------------- 1 | const { pathsToModuleNameMapper } = require('ts-jest/utils') 2 | // In the following statement, replace `./tsconfig` with the path to your `tsconfig` file 3 | // which contains the path mapping (ie the `compilerOptions.paths` option): 4 | const { compilerOptions } = require('../../tsconfig') 5 | 6 | module.exports = { 7 | moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { 8 | prefix: '<rootDir>/../../', 9 | }), 10 | modulePaths: ['<rootDir>/src/'], 11 | modulePathIgnorePatterns: ['<rootDir>/demo/'], 12 | preset: 'ts-jest', 13 | testEnvironment: 'jsdom', 14 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$', 15 | } 16 | -------------------------------------------------------------------------------- /packages/retil-media/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "retil-media", 3 | "version": "0.28.4", 4 | "description": "Superpowers for developers building responsive React apps", 5 | "author": "James K Nelson <james@jamesknelson.com>", 6 | "license": "MIT", 7 | "main": "dist/commonjs/index.js", 8 | "module": "dist/es/index.js", 9 | "types": "dist/es/index.d.ts", 10 | "scripts": { 11 | "clean": "rimraf dist", 12 | "build:commonjs": "tsc -p tsconfig.build.json --module commonjs --outDir dist/commonjs", 13 | "build:es-and-types": "tsc -p tsconfig.build.json --module es2015 --outDir dist/es --declaration", 14 | "build": "yarn run clean && yarn build:es-and-types && yarn build:commonjs", 15 | "build:watch": "yarn run clean && yarn build:es-and-types -- --watch", 16 | "lint": "eslint --ext ts,tsx src", 17 | "prepare": "yarn test && yarn build", 18 | "test": "jest", 19 | "test:watch": "jest --watch" 20 | }, 21 | "dependencies": { 22 | "retil-css": "^0.28.4", 23 | "retil-hydration": "^0.28.4", 24 | "retil-support": "^0.28.4", 25 | "tslib": "^2.2.0" 26 | }, 27 | "devDependencies": { 28 | "@emotion/react": "^11.8.1", 29 | "typescript": "4.6.2" 30 | }, 31 | "peerDependencies": { 32 | "react": "^17.0.1" 33 | }, 34 | "files": [ 35 | "dist" 36 | ], 37 | "gitHead": "9d352aa11ce02bdf184dfe8ff15a90a3ad4543bc" 38 | } 39 | -------------------------------------------------------------------------------- /packages/retil-media/src/defaultMedia.ts: -------------------------------------------------------------------------------- 1 | import { createMediaSelector } from './mediaSelector' 2 | 3 | export const mediaQueries = { 4 | xSmall: `all and (max-width: 359px)`, 5 | small: `all and (max-width: 767px)`, 6 | atLeastMedium: `all and (min-width: 768px)`, 7 | medium: `all and (min-width: 768px) and (max-width: 999px)`, 8 | atMostMedium: `all and (max-width: 999px)`, 9 | large: `all and (min-width: 1000px)`, 10 | xLarge: `all and (min-width: 1200px)`, 11 | } 12 | 13 | export const media = { 14 | xSmall: /*#__PURE__*/ createMediaSelector(mediaQueries.xSmall), 15 | small: /*#__PURE__*/ createMediaSelector(mediaQueries.small), 16 | atLeastMedium: /*#__PURE__*/ createMediaSelector(mediaQueries.atLeastMedium), 17 | medium: /*#__PURE__*/ createMediaSelector(mediaQueries.medium), 18 | atMostMedium: /*#__PURE__*/ createMediaSelector(mediaQueries.atMostMedium), 19 | large: /*#__PURE__*/ createMediaSelector(mediaQueries.large), 20 | xLarge: /*#__PURE__*/ createMediaSelector(mediaQueries.xLarge), 21 | } 22 | -------------------------------------------------------------------------------- /packages/retil-media/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './defaultMedia' 2 | export * from './mediaSelector' 3 | export * from './useFirstMatchingMediaSelector' 4 | export * from './useMediaRenderer' 5 | export * from './useMediaSelector' 6 | -------------------------------------------------------------------------------- /packages/retil-media/src/useMediaRenderer.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement, useCallback, useMemo } from 'react' 2 | import { useCSSRuntime, useCSSSelectors } from 'retil-css' 3 | 4 | import { MediaSelector } from './mediaSelector' 5 | import { useMediaSelector } from './useMediaSelector' 6 | 7 | export function useMediaRenderer( 8 | mediaSelector: MediaSelector, 9 | ): (render: (mediaCSS: any) => ReactElement) => ReactElement | null { 10 | const css = useCSSRuntime() 11 | const [selector] = useCSSSelectors([mediaSelector]) 12 | const nonArraySelector = ( 13 | Array.isArray(selector) ? selector.join(',') : selector 14 | ) as string | boolean 15 | const result = useMediaSelector(mediaSelector) 16 | const mediaCSS = useMemo( 17 | () => 18 | nonArraySelector === false 19 | ? css` 20 | display: none; 21 | ` 22 | : nonArraySelector === true 23 | ? undefined 24 | : css` 25 | @media not ${nonArraySelector.replace('@media ', '')} { 26 | display: none !important; 27 | } 28 | `, 29 | [css, nonArraySelector], 30 | ) 31 | const cssRenderer = useCallback( 32 | (render: (mediaCSS: any) => ReactElement): ReactElement => render(mediaCSS), 33 | [mediaCSS], 34 | ) 35 | const jsRenderer = useCallback( 36 | (render: (mediaCSS: any) => ReactElement): ReactElement | null => 37 | result ? render(null) : null, 38 | [result], 39 | ) 40 | 41 | return result === undefined ? cssRenderer : jsRenderer 42 | } 43 | -------------------------------------------------------------------------------- /packages/retil-media/src/useMediaSelector.ts: -------------------------------------------------------------------------------- 1 | import { MediaSelector } from './mediaSelector' 2 | import { useFirstMatchingMediaSelector } from './useFirstMatchingMediaSelector' 3 | 4 | export function useMediaSelector(query: MediaSelector): boolean | undefined { 5 | const index = useFirstMatchingMediaSelector([query]) 6 | return index === undefined ? undefined : index === 0 7 | } 8 | -------------------------------------------------------------------------------- /packages/retil-media/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.build.json", 3 | "include": [ 4 | "src/**/*" 5 | ] 6 | } -------------------------------------------------------------------------------- /packages/retil-mount/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 James K Nelson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | 'Software'), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /packages/retil-mount/jest.config.js: -------------------------------------------------------------------------------- 1 | const { pathsToModuleNameMapper } = require('ts-jest/utils') 2 | // In the following statement, replace `./tsconfig` with the path to your `tsconfig` file 3 | // which contains the path mapping (ie the `compilerOptions.paths` option): 4 | const { compilerOptions } = require('../../tsconfig') 5 | 6 | module.exports = { 7 | moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { 8 | prefix: '<rootDir>/../../', 9 | }), 10 | modulePaths: ['<rootDir>/src/'], 11 | modulePathIgnorePatterns: ['<rootDir>/demo/'], 12 | preset: 'ts-jest', 13 | testEnvironment: 'jsdom', 14 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$', 15 | } 16 | -------------------------------------------------------------------------------- /packages/retil-mount/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "retil-mount", 3 | "version": "0.28.4", 4 | "description": "Superpowers for loading screens in React apps.", 5 | "author": "James K Nelson <james@jamesknelson.com>", 6 | "license": "MIT", 7 | "main": "dist/commonjs/index.js", 8 | "module": "dist/es/index.js", 9 | "types": "dist/es/index.d.ts", 10 | "scripts": { 11 | "clean": "rimraf dist", 12 | "build:commonjs": "tsc -p tsconfig.build.json --module commonjs --outDir dist/commonjs", 13 | "build:es-and-types": "tsc -p tsconfig.build.json --module es2015 --outDir dist/es --declaration", 14 | "build": "yarn run clean && yarn build:es-and-types && yarn build:commonjs", 15 | "build:watch": "yarn run clean && yarn build:es-and-types -- --watch", 16 | "lint": "eslint --ext ts,tsx src", 17 | "prepare": "yarn test && yarn build", 18 | "test": "jest", 19 | "test:watch": "jest --watch" 20 | }, 21 | "dependencies": { 22 | "abort-controller": "^3.0.0", 23 | "retil-source": "^0.28.4", 24 | "retil-support": "^0.28.4", 25 | "tslib": "^2.2.0" 26 | }, 27 | "devDependencies": { 28 | "typescript": "4.6.2" 29 | }, 30 | "files": [ 31 | "dist" 32 | ], 33 | "keywords": [ 34 | "react", 35 | "loader" 36 | ], 37 | "gitHead": "9d352aa11ce02bdf184dfe8ff15a90a3ad4543bc" 38 | } 39 | -------------------------------------------------------------------------------- /packages/retil-mount/src/dependencyList.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Keep track of a list of promises that'll cause the synchronous content 3 | * gathered by a collector to suspend if rendered immediately. 4 | */ 5 | export class DependencyList { 6 | private unresolvedDependencies: PromiseLike<any>[] = [] 7 | 8 | add(promise: PromiseLike<any>): void { 9 | this.unresolvedDependencies.push(promise) 10 | } 11 | 12 | /** 13 | * Wait until all suspenses added to the response have resolved. This is 14 | * useful when using renderToString on the server, or for waiting until 15 | * routes have finished loading before showing new content. 16 | */ 17 | resolve(): Promise<void> { 18 | const waitingPromises = this.unresolvedDependencies.slice(0) 19 | // Use `Promise.all` to eagerly start any lazy promises 20 | return Promise.all(waitingPromises).then(() => { 21 | for (let i = 0; i < waitingPromises.length; i++) { 22 | const promise = waitingPromises[i] 23 | const pendingIndex = this.unresolvedDependencies.indexOf(promise) 24 | if (pendingIndex !== -1) { 25 | this.unresolvedDependencies.splice(pendingIndex, 1) 26 | } 27 | } 28 | if (this.unresolvedDependencies.length) { 29 | return this.resolve() 30 | } 31 | }) 32 | } 33 | 34 | get unresolved() { 35 | return !!this.unresolvedDependencies.length 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/retil-mount/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './dependencyList' 2 | export * from './loadAsync' 3 | export * from './loadLazy' 4 | export * from './mount' 5 | export * from './mountComponents' 6 | export * from './mountContext' 7 | export * from './mountOnce' 8 | export * from './mountTypes' 9 | export * from './serverMount' 10 | export * from './serverMountContext' 11 | export * from './useMount' 12 | export * from './useMountSource' 13 | -------------------------------------------------------------------------------- /packages/retil-mount/src/loadLazy.ts: -------------------------------------------------------------------------------- 1 | import { loadAsync } from './loadAsync' 2 | import { Loader } from './mountTypes' 3 | 4 | export function loadLazy<Env extends object>( 5 | load: () => PromiseLike<{ default: Loader<Env> }>, 6 | ): Loader<Env> { 7 | let loader: Loader<Env> | undefined 8 | 9 | return loadAsync(async (env) => { 10 | if (!loader) { 11 | loader = (await load()).default 12 | } 13 | return loader(env) 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /packages/retil-mount/src/mountComponents.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement, ReactNode } from 'react' 2 | 3 | import { MountProvider, useMountContent } from './mountContext' 4 | import { CastableToEnvSource, Loader } from './mountTypes' 5 | import { useMount } from './useMount' 6 | import { UseMountSourceOptions } from './useMountSource' 7 | 8 | export interface MountProps<Env extends object, Content> 9 | extends UseMountSourceOptions { 10 | children: ReactNode 11 | loader: Loader<Env, Content> 12 | env: CastableToEnvSource<Env> 13 | } 14 | 15 | export function Mount<Env extends object, Content>( 16 | props: MountProps<Env, Content>, 17 | ) { 18 | const { children, loader, env } = props 19 | const mount = useMount(loader, env) 20 | return <MountProvider value={mount}>{children}</MountProvider> 21 | } 22 | 23 | export function MountedContent() { 24 | // Cast this to ReactElement to appease TypeScript. 25 | return useMountContent() as ReactElement 26 | } 27 | -------------------------------------------------------------------------------- /packages/retil-mount/src/mountOnce.ts: -------------------------------------------------------------------------------- 1 | import { getSnapshot } from 'retil-source' 2 | 3 | import { mount } from './mount' 4 | import { CastableToEnvSource, Loader } from './mountTypes' 5 | 6 | export function mountOnce<Env extends object, Content>( 7 | loader: Loader<Env, Content>, 8 | env: CastableToEnvSource<Env>, 9 | ) { 10 | const mountSource = mount(loader, env) 11 | const snapshot = getSnapshot(mountSource) 12 | return snapshot.dependencies.resolve().then(() => ({ 13 | content: snapshot.contentRef.current, 14 | env: snapshot.env, 15 | })) 16 | } 17 | -------------------------------------------------------------------------------- /packages/retil-mount/src/mountTypes.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react' 2 | import { Fusor, Source } from 'retil-source' 3 | 4 | import { DependencyList } from './dependencyList' 5 | 6 | export type CastableToEnvSource<T extends object> = T | Source<T> | Fusor<T> 7 | 8 | export type LoaderProps<Env extends object> = Env & { 9 | mount: MountSnapshot<Env, unknown> 10 | } 11 | 12 | export type Loader<Env extends object, Content = ReactNode> = ( 13 | props: LoaderProps<Env>, 14 | ) => Content 15 | 16 | export interface MountContentRef<Content = ReactNode> { 17 | readonly current?: Content 18 | } 19 | 20 | export interface MountSnapshot<Env extends object, Content = ReactNode> { 21 | dependencies: DependencyList 22 | env: Env 23 | contentRef: MountContentRef<Content> 24 | signal: AbortSignal 25 | } 26 | 27 | export interface MountSnapshotWithContent< 28 | Env extends object, 29 | Content = ReactNode, 30 | > extends MountSnapshot<Env, Content> { 31 | contentRef: { readonly current: Content } 32 | } 33 | 34 | export type MountSource<Env extends object, Content = ReactNode> = Source< 35 | MountSnapshotWithContent<Env, Content> 36 | > 37 | 38 | export interface UseMountState< 39 | Env extends object = object, 40 | Content = ReactNode, 41 | > { 42 | env: Env 43 | content: Content 44 | pending: boolean 45 | pendingEnv: Env | null 46 | 47 | /** 48 | * Returns a promise that will be resolved in a React effect that will be run 49 | * once the current env has no remaining suspensions. 50 | */ 51 | waitUntilStable: () => Promise<void> 52 | } 53 | -------------------------------------------------------------------------------- /packages/retil-mount/src/serverMount.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from 'react' 2 | import { getSnapshot } from 'retil-source' 3 | 4 | import { mount } from './mount' 5 | import { 6 | CastableToEnvSource, 7 | Loader, 8 | MountSnapshotWithContent, 9 | MountSource, 10 | } from './mountTypes' 11 | import { ServerMountContext } from './serverMountContext' 12 | 13 | export class ServerMount<Env extends object, Content> { 14 | loader: Loader<Env, Content> 15 | env: CastableToEnvSource<Env> 16 | source: MountSource<Env, Content> 17 | 18 | constructor(loader: Loader<Env, Content>, env: CastableToEnvSource<Env>) { 19 | this.loader = loader 20 | this.env = env 21 | } 22 | 23 | preload(): Promise<MountSnapshotWithContent<Env, Content>> { 24 | if (this.source) { 25 | throw new Error( 26 | `The "preload" method of ServerMount may only be called once.`, 27 | ) 28 | } 29 | 30 | this.source = mount(this.loader, this.env) 31 | 32 | const snapshot = getSnapshot(this.source) 33 | return snapshot.dependencies.resolve().then(() => snapshot) 34 | } 35 | 36 | provide(element: ReactElement): ReactElement { 37 | if (!this.source) { 38 | throw new Error( 39 | `The "provide" method of ServerMount must be called *after* "preload" has been called.`, 40 | ) 41 | } 42 | 43 | return ( 44 | <ServerMountContext.Provider value={this}> 45 | {element} 46 | </ServerMountContext.Provider> 47 | ) 48 | } 49 | 50 | seal(): void { 51 | // This is part of the API in case it turns out to be needed. But for now, 52 | // so long as the env is a constant source, I don't think we *do* need to 53 | // clean up. 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/retil-mount/src/serverMountContext.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react' 2 | 3 | import { CastableToEnvSource, Loader, MountSource } from './mountTypes' 4 | 5 | export const ServerMountContext = /*#__PURE__*/ createContext<null | { 6 | loader: Loader<any> 7 | env: CastableToEnvSource<object> 8 | source: MountSource<any, any> 9 | }>(null) 10 | -------------------------------------------------------------------------------- /packages/retil-mount/src/useMount.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react' 2 | import { createWeakMemo } from 'retil-support' 3 | 4 | import { mount } from './mount' 5 | import { CastableToEnvSource, Loader, UseMountState } from './mountTypes' 6 | import { ServerMountContext } from './serverMountContext' 7 | import { UseMountSourceOptions, useMountSource } from './useMountSource' 8 | 9 | const mountSourceMemo = createWeakMemo() 10 | 11 | export const useMount = <Env extends object, Content>( 12 | loader: Loader<Env, Content>, 13 | env: CastableToEnvSource<Env>, 14 | options?: UseMountSourceOptions, 15 | ): UseMountState<Env, Content> => { 16 | const serverMount = useContext(ServerMountContext) 17 | 18 | if ( 19 | serverMount && 20 | (serverMount.loader !== loader || 21 | serverMount.env !== env || 22 | !serverMount.source) 23 | ) { 24 | throw new Error( 25 | 'The ServerMount loader/env must match the <Mount> or useMount() loader/env.', 26 | ) 27 | } 28 | 29 | // Even in strict mode, we don't want to create two mount sources, so instead 30 | // of useMemo, we'll use a WeakMemo to memoize the mount sources externally to 31 | // the component. 32 | const mountSource = 33 | serverMount?.source || 34 | mountSourceMemo(() => mount(loader, env), [env, loader]) 35 | 36 | return useMountSource(mountSource, options) 37 | } 38 | -------------------------------------------------------------------------------- /packages/retil-mount/test/mount.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createState, 3 | fuse, 4 | getSnapshot, 5 | getSnapshotPromise, 6 | } from 'retil-source' 7 | import { Deferred } from 'retil-support' 8 | 9 | import { mount } from '../src' 10 | 11 | describe('mount', () => { 12 | test('errors when an error is emitted on the env source', async () => { 13 | const [stateSource, setState] = createState({ pathname: '/start' }) 14 | const deferred = new Deferred() 15 | const envSource = fuse((use, act) => { 16 | const state = use(stateSource) 17 | if (state.pathname === '/start') { 18 | return state 19 | } else { 20 | return act(() => deferred.promise) 21 | } 22 | }) 23 | 24 | const mountSource = mount((env) => env.pathname, envSource) 25 | 26 | expect(getSnapshot(mountSource).env.pathname).toBe('/start') 27 | 28 | setState({ pathname: '/end' }) 29 | 30 | await Promise.resolve() 31 | 32 | deferred.reject('test') 33 | 34 | await expect(getSnapshotPromise(mountSource)).rejects.toBe('test') 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /packages/retil-mount/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.build.json", 3 | "include": [ 4 | "src/**/*" 5 | ] 6 | } -------------------------------------------------------------------------------- /packages/retil-nav-scheme/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 James K Nelson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | 'Software'), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /packages/retil-nav-scheme/jest.config.js: -------------------------------------------------------------------------------- 1 | const { pathsToModuleNameMapper } = require('ts-jest/utils') 2 | // In the following statement, replace `./tsconfig` with the path to your `tsconfig` file 3 | // which contains the path mapping (ie the `compilerOptions.paths` option): 4 | const { compilerOptions } = require('../../tsconfig') 5 | 6 | module.exports = { 7 | moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { 8 | prefix: '<rootDir>/../../', 9 | }), 10 | modulePaths: ['<rootDir>/src/'], 11 | modulePathIgnorePatterns: ['<rootDir>/demo/'], 12 | preset: 'ts-jest', 13 | testEnvironment: 'jsdom', 14 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$', 15 | } 16 | -------------------------------------------------------------------------------- /packages/retil-nav-scheme/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "retil-nav-scheme", 3 | "version": "0.28.4", 4 | "description": "Consistent URLs made easy", 5 | "author": "James K Nelson <james@jamesknelson.com>", 6 | "license": "MIT", 7 | "main": "dist/commonjs/index.js", 8 | "module": "dist/es/index.js", 9 | "types": "dist/es/index.d.ts", 10 | "scripts": { 11 | "clean": "rimraf dist", 12 | "build:commonjs": "tsc -p tsconfig.build.json --module commonjs --outDir dist/commonjs", 13 | "build:es-and-types": "tsc -p tsconfig.build.json --module es2015 --outDir dist/es --declaration", 14 | "build": "yarn run clean && yarn build:es-and-types && yarn build:commonjs", 15 | "build:watch": "yarn run clean && yarn build:es-and-types -- --watch", 16 | "lint": "eslint --ext ts,tsx src", 17 | "prepare": "yarn test && yarn build", 18 | "test": "jest", 19 | "test:watch": "jest --watch" 20 | }, 21 | "dependencies": { 22 | "retil-nav": "^0.28.4", 23 | "retil-support": "^0.28.4" 24 | }, 25 | "devDependencies": { 26 | "typescript": "4.6.2" 27 | }, 28 | "files": [ 29 | "dist" 30 | ], 31 | "gitHead": "9d352aa11ce02bdf184dfe8ff15a90a3ad4543bc" 32 | } 33 | -------------------------------------------------------------------------------- /packages/retil-nav-scheme/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.build.json", 3 | "include": [ 4 | "src/**/*" 5 | ] 6 | } -------------------------------------------------------------------------------- /packages/retil-nav/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 James K Nelson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | 'Software'), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /packages/retil-nav/jest.config.js: -------------------------------------------------------------------------------- 1 | const { pathsToModuleNameMapper } = require('ts-jest/utils') 2 | // In the following statement, replace `./tsconfig` with the path to your `tsconfig` file 3 | // which contains the path mapping (ie the `compilerOptions.paths` option): 4 | const { compilerOptions } = require('../../tsconfig') 5 | 6 | module.exports = { 7 | moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { 8 | prefix: '<rootDir>/../../', 9 | }), 10 | modulePaths: ['<rootDir>/src/'], 11 | modulePathIgnorePatterns: ['<rootDir>/demo/'], 12 | preset: 'ts-jest', 13 | testEnvironment: 'jsdom', 14 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$', 15 | } 16 | -------------------------------------------------------------------------------- /packages/retil-nav/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "retil-nav", 3 | "version": "0.28.4", 4 | "description": "Superpowers for routing and navigation in React apps.", 5 | "author": "James K Nelson <james@jamesknelson.com>", 6 | "license": "MIT", 7 | "main": "dist/commonjs/index.js", 8 | "module": "dist/es/index.js", 9 | "types": "dist/es/index.d.ts", 10 | "scripts": { 11 | "clean": "rimraf dist", 12 | "build:commonjs": "tsc -p tsconfig.build.json --module commonjs --outDir dist/commonjs", 13 | "build:es-and-types": "tsc -p tsconfig.build.json --module es2015 --outDir dist/es --declaration", 14 | "build": "yarn run clean && yarn build:es-and-types && yarn build:commonjs", 15 | "build:watch": "yarn run clean && yarn build:es-and-types -- --watch", 16 | "lint": "eslint --ext ts,tsx src", 17 | "prepare": "yarn test && yarn build", 18 | "test": "jest", 19 | "test:watch": "jest --watch" 20 | }, 21 | "dependencies": { 22 | "path-to-regexp": "^6.2.0", 23 | "querystring": "^0.2.1", 24 | "retil-boundary": "^0.28.4", 25 | "retil-mount": "^0.28.4", 26 | "retil-source": "^0.28.4", 27 | "retil-support": "^0.28.4", 28 | "tslib": "^2.2.0" 29 | }, 30 | "devDependencies": { 31 | "typescript": "4.6.2" 32 | }, 33 | "files": [ 34 | "dist" 35 | ], 36 | "keywords": [ 37 | "react", 38 | "loader", 39 | "routing", 40 | "router", 41 | "navigation" 42 | ], 43 | "gitHead": "9d352aa11ce02bdf184dfe8ff15a90a3ad4543bc" 44 | } 45 | -------------------------------------------------------------------------------- /packages/retil-nav/src/hooks/useNavMatcher.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react' 2 | 3 | import { createMatcher } from '../matcher' 4 | import { useNavSnapshot } from '../navContext' 5 | 6 | import { useNavResolver } from './useNavResolver' 7 | 8 | export const useNavMatcher = (): ((actionPattern: string) => boolean) => { 9 | const resolve = useNavResolver() 10 | const { pathname } = useNavSnapshot() 11 | return useCallback( 12 | (actionPattern: string) => 13 | !!createMatcher(resolve(actionPattern).pathname)(pathname), 14 | [pathname, resolve], 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /packages/retil-nav/src/hooks/useNavResolver.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react' 2 | import { memoizeOne } from 'retil-support' 3 | 4 | import { useNavSnapshot } from '../navContext' 5 | import { NavAction, NavLocation } from '../navTypes' 6 | import { areActionsEqual, parseAction, resolveAction } from '../navUtils' 7 | 8 | export const useNavResolver = (): (( 9 | action: NavAction, 10 | state?: object, 11 | ) => NavLocation) => { 12 | const { basename, pathname } = useNavSnapshot() 13 | return useMemo( 14 | () => 15 | memoizeOne( 16 | (action: NavAction, state?: object) => 17 | resolveAction(parseAction(action, state), pathname, basename), 18 | ([xa, xs], [ya, ys]) => xs === ys && areActionsEqual(xa, ya), 19 | ), 20 | [basename, pathname], 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /packages/retil-nav/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './hooks/useBoundaryNavScroller' 2 | export * from './hooks/useNavLinkProps' 3 | export * from './hooks/useNavMatcher' 4 | export * from './hooks/useNavResolver' 5 | 6 | export * from './loaders/loadMatch' 7 | export * from './loaders/loadNotFoundBoundary' 8 | export * from './loaders/loadRedirect' 9 | 10 | export * from './browserNavEnvService' 11 | export * from './navContext' 12 | export * from './navTypes' 13 | export * from './navUtils' 14 | export * from './noopNavController' 15 | export * from './notFoundError' 16 | export * from './serverNavEnv' 17 | export * from './staticNavEnv' 18 | -------------------------------------------------------------------------------- /packages/retil-nav/src/loaders/loadMatch.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react' 2 | import { Loader } from 'retil-mount' 3 | 4 | import { NavEnv } from '../navTypes' 5 | import { Matcher, createMatcher } from '../matcher' 6 | import { joinPathnames } from '../navUtils' 7 | 8 | export interface LoadMatchOptions< 9 | TEnv extends NavEnv = NavEnv, 10 | TContent = ReactNode, 11 | > { 12 | [pattern: string]: Loader<TEnv, TContent> | TContent 13 | } 14 | 15 | export function loadMatch<TEnv extends NavEnv = NavEnv, TContent = ReactNode>( 16 | handlers: LoadMatchOptions<TEnv, TContent>, 17 | ): Loader<TEnv, TContent | ReactNode> { 18 | const tests: [Matcher, Loader<TEnv, TContent>][] = [] 19 | 20 | const patterns = Object.keys(handlers) 21 | for (const rawPattern of patterns) { 22 | const handler = handlers[rawPattern] 23 | const loader = 24 | typeof handler === 'function' 25 | ? (handler as Loader<TEnv, TContent>) 26 | : () => handler as TContent 27 | 28 | const matcher = createMatcher(rawPattern) 29 | tests.push([matcher, loader]) 30 | } 31 | 32 | return (env) => { 33 | const { matchname, pathname, params } = env.nav 34 | const unmatchedPathname = 35 | (pathname.slice(0, matchname.length) === matchname 36 | ? pathname.slice(matchname.length) 37 | : pathname) || '/' 38 | 39 | for (const [matcher, router] of tests) { 40 | const match = matcher(unmatchedPathname) 41 | if (match) { 42 | return router({ 43 | ...env, 44 | nav: { 45 | ...env.nav, 46 | matchname: joinPathnames(matchname, match.pathname), 47 | params: { ...params, ...match.params }, 48 | }, 49 | }) 50 | } 51 | } 52 | 53 | return env.nav.notFound() 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/retil-nav/src/loaders/loadRedirect.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react' 2 | import { Loader } from 'retil-mount' 3 | 4 | import { NavAction, NavEnv } from '../navTypes' 5 | import { parseAction } from '../navUtils' 6 | 7 | export function loadRedirect<TEnv extends NavEnv>( 8 | to: NavAction | ((env: NavEnv) => NavAction), 9 | status = 302, 10 | ): Loader<TEnv, ReactNode> { 11 | return (env) => { 12 | const toAction = parseAction(typeof to === 'function' ? to(env) : to) 13 | return env.nav.redirect(status, toAction) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/retil-nav/src/noopNavController.ts: -------------------------------------------------------------------------------- 1 | import { noop } from 'retil-support' 2 | 3 | import { NavController } from './navTypes' 4 | 5 | const resolveToFalse = () => Promise.resolve(false) 6 | 7 | // TODO: outside of production, emit warnings when this is used 8 | export const noopNavController: NavController = { 9 | back: resolveToFalse, 10 | block: () => noop, 11 | go: resolveToFalse, 12 | navigate: resolveToFalse, 13 | precache: () => noop, 14 | } 15 | -------------------------------------------------------------------------------- /packages/retil-nav/src/notFoundError.ts: -------------------------------------------------------------------------------- 1 | import { NavSnapshot } from './navTypes' 2 | 3 | /** 4 | * This can be thrown form React components rendered inside a not found 5 | * boundary. However, for loaders, you should prefer calling env.nav.notFound 6 | * (as throwing NotFoundError inside a loader will not trigger the not found 7 | * boundary during server rendering). 8 | */ 9 | export class NotFoundError { 10 | constructor(readonly nav: NavSnapshot) {} 11 | } 12 | -------------------------------------------------------------------------------- /packages/retil-nav/test/loadNotFoundBoundary.test.tsx: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/extend-expect' 2 | import React from 'react' 3 | import { renderToString } from 'react-dom/server' 4 | import { ServerMount, loadAsync, useMount } from 'retil-mount' 5 | import { Deferred } from 'retil-support' 6 | 7 | import { createStaticNavEnv, loadMatch, loadNotFoundBoundary } from '../src' 8 | 9 | describe('loadNotFoundBoundary()', () => { 10 | test(`works during SSR with async routes`, async () => { 11 | const deferred = new Deferred() 12 | const innerLoader = loadMatch({ 13 | '/found': ({ nav }) => 'found' + nav.pathname, 14 | }) 15 | const loader = loadNotFoundBoundary( 16 | loadAsync(async (env) => { 17 | await deferred.promise 18 | return innerLoader(env) 19 | }), 20 | (env) => 'not-found' + env.nav.pathname, 21 | ) 22 | const env = createStaticNavEnv({ url: '/test-1' }) 23 | const mount = new ServerMount(loader, env) 24 | 25 | const mountPromise = mount.preload() 26 | 27 | deferred.resolve('test') 28 | 29 | await mountPromise 30 | 31 | const Test = () => { 32 | const route = useMount(loader, env) 33 | return <>{route.content}</> 34 | } 35 | 36 | const html = renderToString(mount.provide(<Test />)) 37 | 38 | expect(html).toEqual('not-found/test-1') 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /packages/retil-nav/test/loadRedirect.test.ts: -------------------------------------------------------------------------------- 1 | import { mountOnce } from 'retil-mount' 2 | 3 | import { 4 | createStaticNavEnv, 5 | createStaticNavResponse, 6 | loadRedirect, 7 | } from '../src' 8 | 9 | describe('loadRedirect()', () => { 10 | test(`supports relative redirects`, async () => { 11 | const loader = loadRedirect('./acquisition') 12 | const response = createStaticNavResponse() 13 | const env = createStaticNavEnv({ 14 | url: '/browse/deck', 15 | response, 16 | basename: '/browse/deck', 17 | }) 18 | 19 | await mountOnce(loader, env) 20 | 21 | expect(response.getHeaders().Location).toBe('/browse/deck/acquisition') 22 | }) 23 | 24 | test(`supports absolute redirects`, async () => { 25 | const loader = loadRedirect('/test') 26 | const response = createStaticNavResponse() 27 | const env = createStaticNavEnv({ 28 | url: '/browse/deck', 29 | response, 30 | basename: '/browse/deck', 31 | }) 32 | 33 | await mountOnce(loader, env) 34 | 35 | expect(response.getHeaders().Location).toBe('/test') 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /packages/retil-nav/test/navUtils.test.ts: -------------------------------------------------------------------------------- 1 | import { resolveAction, createHref, parseAction } from '../src' 2 | 3 | describe(`nav utils`, () => { 4 | test(`createHref(parseAction()) is a noop`, () => { 5 | const location = '/test?param=1&another=two#id' 6 | expect(createHref(parseAction(location))).toBe(location) 7 | }) 8 | 9 | test(`parseAction() generates correct searches from queries`, () => { 10 | expect(parseAction({ query: { param: '1', another: 'two' } }).search).toBe( 11 | '?param=1&another=two', 12 | ) 13 | 14 | expect(parseAction({ query: {} }).search).toBe('') 15 | }) 16 | 17 | test(`resolveAction() works in pathnames with no leading . or /`, () => { 18 | const pathname = '/browse/deck/word' 19 | 20 | expect(resolveAction('test', pathname).pathname).toBe('/browse/deck/test') 21 | expect(resolveAction({ pathname: 'test' }, pathname).pathname).toBe( 22 | '/browse/deck/test', 23 | ) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /packages/retil-nav/test/useNavMatch.test.tsx: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/extend-expect' 2 | import React, { StrictMode } from 'react' 3 | import { render } from '@testing-library/react' 4 | 5 | import { NavProvider, createStaticNavEnv, useNavMatcher } from '../src' 6 | 7 | function testUseNavMatcher( 8 | description: string, 9 | pattern: string, 10 | pathname: string, 11 | expectedResult: boolean, 12 | ) { 13 | test(description, async () => { 14 | const env = createStaticNavEnv({ url: pathname }) 15 | const Test = () => { 16 | const matcher = useNavMatcher() 17 | return <>{matcher(pattern) ? 'match' : 'miss'}</> 18 | } 19 | const { container } = render( 20 | <StrictMode> 21 | <NavProvider env={env}> 22 | <Test /> 23 | </NavProvider> 24 | </StrictMode>, 25 | ) 26 | expect(container).toHaveTextContent(expectedResult ? 'match' : 'miss') 27 | }) 28 | } 29 | 30 | describe('useNavMatcher()', () => { 31 | testUseNavMatcher( 32 | 'matches nested paths on wildcard patterns', 33 | '/test*', 34 | '/test/nested', 35 | true, 36 | ) 37 | 38 | testUseNavMatcher( 39 | 'matches exact paths on wildcard patterns', 40 | '/test*', 41 | '/test', 42 | true, 43 | ) 44 | 45 | testUseNavMatcher( 46 | 'matches exact paths on exact patterns', 47 | '/test', 48 | '/test', 49 | true, 50 | ) 51 | 52 | testUseNavMatcher('does not match parent paths', '/test*', '/', false) 53 | }) 54 | -------------------------------------------------------------------------------- /packages/retil-nav/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.build.json", 3 | "include": [ 4 | "src/**/*" 5 | ] 6 | } -------------------------------------------------------------------------------- /packages/retil-operation/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 James K Nelson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | 'Software'), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /packages/retil-operation/jest.config.js: -------------------------------------------------------------------------------- 1 | const { pathsToModuleNameMapper } = require('ts-jest/utils') 2 | // In the following statement, replace `./tsconfig` with the path to your `tsconfig` file 3 | // which contains the path mapping (ie the `compilerOptions.paths` option): 4 | const { compilerOptions } = require('../../tsconfig') 5 | 6 | module.exports = { 7 | moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { 8 | prefix: '<rootDir>/../../', 9 | }), 10 | modulePaths: ['<rootDir>/src/'], 11 | modulePathIgnorePatterns: ['<rootDir>/demo/'], 12 | preset: 'ts-jest', 13 | testEnvironment: 'jsdom', 14 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$', 15 | } 16 | -------------------------------------------------------------------------------- /packages/retil-operation/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "retil-operation", 3 | "version": "0.28.4", 4 | "description": "Super-powers for asynchronous operations with React.", 5 | "author": "James K Nelson <james@jamesknelson.com>", 6 | "license": "MIT", 7 | "main": "dist/commonjs/index.js", 8 | "module": "dist/es/index.js", 9 | "types": "dist/es/index.d.ts", 10 | "scripts": { 11 | "clean": "rimraf dist", 12 | "build:commonjs": "tsc -p tsconfig.build.json --module commonjs --outDir dist/commonjs", 13 | "build:es-and-types": "tsc -p tsconfig.build.json --module es2015 --outDir dist/es --declaration", 14 | "build": "yarn run clean && yarn build:es-and-types && yarn build:commonjs", 15 | "build:watch": "yarn run clean && yarn build:es-and-types -- --watch", 16 | "lint": "eslint --ext ts,tsx src", 17 | "prepare": "yarn test && yarn build", 18 | "test": "jest", 19 | "test:watch": "jest --watch" 20 | }, 21 | "dependencies": { 22 | "retil-support": "^0.28.4", 23 | "tslib": "^2.2.0" 24 | }, 25 | "devDependencies": { 26 | "typescript": "4.6.2" 27 | }, 28 | "files": [ 29 | "dist" 30 | ], 31 | "gitHead": "9d352aa11ce02bdf184dfe8ff15a90a3ad4543bc" 32 | } 33 | -------------------------------------------------------------------------------- /packages/retil-operation/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useOperation' 2 | -------------------------------------------------------------------------------- /packages/retil-operation/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.build.json", 3 | "include": [ 4 | "src/**/*" 5 | ] 6 | } -------------------------------------------------------------------------------- /packages/retil-source/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 James K Nelson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | 'Software'), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /packages/retil-source/jest.config.js: -------------------------------------------------------------------------------- 1 | const { pathsToModuleNameMapper } = require('ts-jest/utils') 2 | // In the following statement, replace `./tsconfig` with the path to your `tsconfig` file 3 | // which contains the path mapping (ie the `compilerOptions.paths` option): 4 | const { compilerOptions } = require('../../tsconfig') 5 | 6 | module.exports = { 7 | moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { 8 | prefix: '<rootDir>/../../', 9 | }), 10 | modulePaths: ['<rootDir>/src/'], 11 | modulePathIgnorePatterns: ['<rootDir>/demo/'], 12 | preset: 'ts-jest', 13 | testEnvironment: 'jsdom', 14 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$', 15 | } 16 | -------------------------------------------------------------------------------- /packages/retil-source/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "retil-source", 3 | "version": "0.28.4", 4 | "description": "Utilities to create and combine suspendable, reactive sources", 5 | "author": "James K Nelson <james@jamesknelson.com>", 6 | "license": "MIT", 7 | "main": "dist/commonjs/index.js", 8 | "module": "dist/es/index.js", 9 | "types": "dist/es/index.d.ts", 10 | "scripts": { 11 | "clean": "rimraf dist", 12 | "build:commonjs": "tsc -p tsconfig.build.json --module commonjs --outDir dist/commonjs", 13 | "build:es-and-types": "tsc -p tsconfig.build.json --module es2015 --outDir dist/es --declaration", 14 | "build": "yarn run clean && yarn build:es-and-types && yarn build:commonjs", 15 | "build:watch": "yarn run clean && yarn build:es-and-types -- --watch", 16 | "lint": "eslint --ext ts,tsx src", 17 | "prepare": "yarn test && yarn build", 18 | "test": "jest", 19 | "test:watch": "jest --watch" 20 | }, 21 | "dependencies": { 22 | "retil-support": "^0.28.4", 23 | "tslib": "^2.2.0" 24 | }, 25 | "devDependencies": { 26 | "typescript": "4.6.2" 27 | }, 28 | "peerDependencies": { 29 | "react": "^17.0.1" 30 | }, 31 | "files": [ 32 | "dist" 33 | ], 34 | "gitHead": "9d352aa11ce02bdf184dfe8ff15a90a3ad4543bc" 35 | } 36 | -------------------------------------------------------------------------------- /packages/retil-source/src/filter.ts: -------------------------------------------------------------------------------- 1 | import { mapVector } from './mapVector' 2 | import { Source } from './source' 3 | 4 | /** 5 | * Make the source valueless when the current value doesn't match the given 6 | * filter function. 7 | * 8 | * Use with `mergeLatest` to create a source that keeps the latest matching 9 | * value. 10 | */ 11 | export function filter<T>( 12 | source: Source<T>, 13 | predicate: (value: T) => boolean, 14 | ): Source<T> { 15 | return mapVector(source, (vector: T[]) => vector.filter(predicate)) 16 | } 17 | -------------------------------------------------------------------------------- /packages/retil-source/src/fromPromise.ts: -------------------------------------------------------------------------------- 1 | import { noop } from 'retil-support' 2 | 3 | import { observe } from './observe' 4 | import { Source } from './source' 5 | 6 | export function fromPromise<T>(promise: PromiseLike<T>): Source<T> { 7 | return observe((next, error, complete) => { 8 | promise.then((value) => { 9 | next([value]) 10 | complete() 11 | }, error) 12 | return noop 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /packages/retil-source/src/fusor.ts: -------------------------------------------------------------------------------- 1 | import { Maybe } from 'retil-support' 2 | 3 | import { Source } from './source' 4 | 5 | export const FuseActSymbol = Symbol('act') 6 | export type FuseAct = typeof FuseActSymbol 7 | export type FusorAct = (callback: () => any) => FuseAct 8 | export interface FusorMemo { 9 | <U, V extends any[]>(callback: (...args: V) => U, args: V): U 10 | <U>(callback: () => U): U 11 | } 12 | export type FusorUse = <U, V = U>( 13 | source: Source<U>, 14 | ...defaultValues: Maybe<V> 15 | ) => U | V 16 | export type Fusor<T> = ( 17 | use: FusorUse, 18 | act: FusorAct, 19 | memo: FusorMemo, 20 | ) => T | FuseAct 21 | 22 | export type VectorFusorUse = <U, V = U>( 23 | source: Source<U>, 24 | ...defaultValues: Maybe<V> 25 | ) => U[] | V[] 26 | export type VectorFusor<T> = ( 27 | use: VectorFusorUse, 28 | act: FusorAct, 29 | memo: FusorMemo, 30 | ) => T[] | FuseAct 31 | -------------------------------------------------------------------------------- /packages/retil-source/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './filter' 2 | export * from './fromPromise' 3 | export * from './fuse' 4 | export * from './fuseVector' 5 | export * from './fusor' 6 | export * from './map' 7 | export * from './mapVector' 8 | export * from './observe' 9 | export * from './reduceVector' 10 | export * from './select' 11 | export * from './source' 12 | export * from './state' 13 | export * from './stateVector' 14 | export * from './useService' 15 | export * from './useSource' 16 | export * from './wait' 17 | -------------------------------------------------------------------------------- /packages/retil-source/src/map.ts: -------------------------------------------------------------------------------- 1 | import { mapVector } from './mapVector' 2 | import { Source } from './source' 3 | 4 | /** 5 | * Map differs from select in that the map function will only be called 6 | * upon changes to the original source, and thus it is safe to create new 7 | * objects in the map function. 8 | */ 9 | export function map<T, U>( 10 | source: Source<T>, 11 | callback: (value: T) => U, 12 | ): Source<U> { 13 | return mapVector(source, (vector: T[]) => vector.map(callback)) 14 | } 15 | -------------------------------------------------------------------------------- /packages/retil-source/src/mapVector.ts: -------------------------------------------------------------------------------- 1 | import { observe } from './observe' 2 | import { Source } from './source' 3 | 4 | /** 5 | * Map the underlying vector, as opposed to `map`, which maps the values inside 6 | * the vector. 7 | */ 8 | export function mapVector< 9 | SourceSelection, 10 | SourceValue = SourceSelection, 11 | MappedValue = SourceSelection, 12 | >( 13 | source: Source<SourceSelection, SourceValue>, 14 | callback: (value: SourceSelection[]) => MappedValue[], 15 | ): Source<MappedValue, MappedValue> { 16 | const [[getVector, subscribe], select, act] = source 17 | 18 | return observe((next, error, seal) => { 19 | const handleChange = () => { 20 | try { 21 | next(callback(getVector().map(select))) 22 | } catch (err) { 23 | error(err) 24 | } 25 | } 26 | // Ensure we catch any events that are side effects of the initial 27 | // `handleChange`. 28 | const initialUnsubscribe = subscribe(handleChange) 29 | handleChange() 30 | // Now subscribe to to `seal()` events too -- we can't do this until we've 31 | // made the initial call to `handleChange()`. 32 | const unsubscribe = subscribe(handleChange, seal) 33 | initialUnsubscribe() 34 | return unsubscribe 35 | }, act) 36 | } 37 | -------------------------------------------------------------------------------- /packages/retil-source/src/reduceVector.ts: -------------------------------------------------------------------------------- 1 | import { observe } from './observe' 2 | import { Source } from './source' 3 | 4 | export type ReduceVectorCallback<T, U> = ( 5 | previousResult: U[], 6 | nextValue: T[], 7 | ) => U[] 8 | 9 | export function reduceVector<T, U>( 10 | source: Source<T>, 11 | callback: ReduceVectorCallback<T, U>, 12 | initial: U[] = [], 13 | ): Source<U> { 14 | const [[getVector, subscribe], select, act] = source 15 | 16 | return observe((next, error, seal) => { 17 | let vector = initial 18 | const handleChange = () => { 19 | try { 20 | vector = callback(vector, getVector().map(select)) 21 | next(vector) 22 | } catch (err) { 23 | error(err) 24 | } 25 | } 26 | // Ensure we catch any events that are side effects of the initial 27 | // `handleChange`. 28 | const initialUnsubscribe = subscribe(handleChange) 29 | handleChange() 30 | // Now subscribe to to `seal()` events too -- we can't do this until we've 31 | // made the initial call to `handleChange()`. 32 | const unsubscribe = subscribe(handleChange, seal) 33 | initialUnsubscribe() 34 | return unsubscribe 35 | }, act) 36 | } 37 | -------------------------------------------------------------------------------- /packages/retil-source/src/select.ts: -------------------------------------------------------------------------------- 1 | import { Source } from './source' 2 | 3 | /** 4 | * Select differs from map, in that it is not de-duped, so you should only 5 | * use it to drill down into existing objects -- not to create new ones. 6 | * 7 | * Select can also be used without memoization, while map creates a new 8 | * underlying source core each time and thus requires memoization. 9 | */ 10 | export function select<T, U, V>( 11 | [core, parentSelect, act]: Source<T, V>, 12 | selector: (value: T) => U, 13 | ): Source<U, V> { 14 | const select = (value: V): U => selector(parentSelect(value)) 15 | return [core, select, act] 16 | } 17 | -------------------------------------------------------------------------------- /packages/retil-source/src/state.ts: -------------------------------------------------------------------------------- 1 | import { Source } from './source' 2 | import { StateSealer, createStateVector } from './stateVector' 3 | 4 | export interface StateController<T> { 5 | (state: T): void 6 | (updater: (state: T) => T): void 7 | } 8 | 9 | const defaultIsEqual = <T>(x: T, y: T) => x === y 10 | 11 | export function createState<T>( 12 | // Doesn't accept a setter function, as it's not a hook that stores state 13 | // between renders and thus it wouldn't make sense to do so. 14 | initialState?: T, 15 | // If provided, the state will only be updated if this function indicates 16 | // that it is not equal to the current state. 17 | isEqual: (x: T, y: T) => boolean = defaultIsEqual, 18 | ): readonly [Source<T>, StateController<T>, StateSealer] { 19 | const areVectorsEqual = (x: T[], y: T[]) => 20 | x.length === y.length && isEqual(x[0], y[0]) 21 | const [source, setVectorState, sealState] = createStateVector( 22 | arguments.length === 0 ? [] : [initialState as T], 23 | areVectorsEqual, 24 | ) 25 | 26 | const setState = (stateOrUpdater: T | ((state: T) => T)) => { 27 | if (typeof stateOrUpdater === 'function') { 28 | const updater = stateOrUpdater as (state: T) => T 29 | setVectorState((vector) => [updater(vector[0])]) 30 | } else { 31 | setVectorState([stateOrUpdater as T]) 32 | } 33 | } 34 | 35 | return [source, setState, sealState] 36 | } 37 | -------------------------------------------------------------------------------- /packages/retil-source/src/useService.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react' 2 | 3 | import { Source } from './source' 4 | import { UseSourceOptions, useSource } from './useSource' 5 | 6 | export type UseServiceOptions<U> = UseSourceOptions<U> 7 | 8 | export interface UseServiceFunction { 9 | <Controller, T, U = T>( 10 | service: Service<T, Controller>, 11 | options?: UseSourceOptions<U>, 12 | ): readonly [T | U, Controller] 13 | } 14 | 15 | export type Service<T, Controller> = readonly [ 16 | source: Source<T>, 17 | controller: Controller, 18 | ] 19 | 20 | export const useService: UseServiceFunction = <Controller, T, U = T>( 21 | [source, controller]: Service<T, Controller>, 22 | options: UseServiceOptions<U> = {}, 23 | ): readonly [T | U, Controller] => { 24 | const value = useSource(source, options) 25 | const service = useMemo( 26 | () => [value, controller] as const, 27 | [value, controller], 28 | ) 29 | return service 30 | } 31 | -------------------------------------------------------------------------------- /packages/retil-source/src/useSource.ts: -------------------------------------------------------------------------------- 1 | /// <reference types="react/next" /> 2 | 3 | import { useSyncExternalStore } from 'react' 4 | 5 | import { 6 | Source, 7 | getSnapshotPromise, 8 | identitySelector, 9 | nullSource, 10 | } from './source' 11 | 12 | const MissingToken = Symbol() 13 | 14 | export interface UseSourceOptions<U> { 15 | defaultValue?: U 16 | } 17 | 18 | export interface UseMaybeSourceOptions<U> extends UseSourceOptions<U> { 19 | // The defaultValue is required for a null source, as a null source can't 20 | // produce a promise letting us know when to try again. 21 | defaultValue: U 22 | } 23 | 24 | export interface UseSourceFunction { 25 | <T, U = T>(source: Source<T>, options?: UseSourceOptions<U>): T | U 26 | <U>(maybeSource: null, options: UseMaybeSourceOptions<U>): U 27 | <T = null, U = T>( 28 | maybeSource: Source<T> | null, 29 | options: UseMaybeSourceOptions<U>, 30 | ): T | U | null 31 | } 32 | 33 | export const useSource: UseSourceFunction = <T = null, U = T>( 34 | maybeSource: Source<T> | null, 35 | options: UseSourceOptions<U> = {}, 36 | ): T | U | null => { 37 | const hasDefaultValue = 'defaultValue' in options 38 | const { defaultValue } = options 39 | const [core, select] = maybeSource || nullSource 40 | const subscribe = core[1] 41 | const getSnapshot = () => { 42 | const vector = core[0]() 43 | return vector.length ? select(vector[0]) : MissingToken 44 | } 45 | const value = useSyncExternalStore(subscribe, getSnapshot, getSnapshot) 46 | 47 | if (value === MissingToken && !hasDefaultValue) { 48 | throw getSnapshotPromise([core, identitySelector]) 49 | } 50 | 51 | return value === MissingToken || maybeSource === null ? defaultValue! : value 52 | } 53 | -------------------------------------------------------------------------------- /packages/retil-source/src/wait.ts: -------------------------------------------------------------------------------- 1 | import { Source, getSnapshot, hasSnapshot, subscribe } from './source' 2 | 3 | export const wait = <T>( 4 | source: Source<T>, 5 | maybePredicate?: (value: T) => boolean, 6 | ): void | Promise<void> => { 7 | // Don't wait for a predicate that already matches. 8 | if ( 9 | maybePredicate && 10 | hasSnapshot(source) && 11 | maybePredicate(getSnapshot(source)) 12 | ) { 13 | return 14 | } 15 | 16 | // By default, any snapshot other than the current value will do. 17 | const predicate = maybePredicate || (() => true) 18 | 19 | // Get a promise that resolves once the source's snapshot matches the 20 | // condition. 21 | return new Promise((resolve, reject) => { 22 | const unsubscribe = subscribe(source, () => { 23 | if (hasSnapshot(source)) { 24 | try { 25 | if (predicate(getSnapshot(source))) { 26 | unsubscribe() 27 | resolve() 28 | } 29 | } catch (error) { 30 | reject(error) 31 | } 32 | } 33 | }) 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /packages/retil-source/test/fromPromise.test.ts: -------------------------------------------------------------------------------- 1 | import { Deferred } from 'retil-support' 2 | import { fromPromise } from '../src' 3 | import { sendToArray } from './utils/sendToArray' 4 | 5 | describe(`fromPromise`, () => { 6 | test(`creates a source outputs the promised value`, async () => { 7 | const deferred = new Deferred() 8 | const source = fromPromise(deferred.promise) 9 | const output = sendToArray(source, 'seal') 10 | 11 | expect(output).toEqual([]) 12 | 13 | // Await the next tick 14 | await deferred.resolve('test') 15 | 16 | expect(output).toEqual(['test', 'seal']) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /packages/retil-source/test/fuseVector.test.ts: -------------------------------------------------------------------------------- 1 | import { Deferred, delay } from 'retil-support' 2 | import { TEARDOWN_DELAY, createState, fuseVector, wait } from '../src' 3 | import { sendVectorToArray } from './utils/sendToArray' 4 | 5 | describe(`fuseVector`, () => { 6 | test('used sources should stay subscribed while waiting for an async act() to complete', async () => { 7 | const [source1, setState] = createState(1) 8 | 9 | let unsubscribes = 0 10 | const source2 = fuseVector( 11 | (use) => use(source1), 12 | () => { 13 | unsubscribes++ 14 | }, 15 | ) 16 | 17 | const deferred = new Deferred() 18 | const source = fuseVector((use, act) => { 19 | const [state] = use(source2) 20 | if (state % 2 === 1) { 21 | return act(async () => { 22 | await deferred.promise 23 | setState(state + 1) 24 | }) 25 | } 26 | 27 | return [state] 28 | }) 29 | 30 | const output = sendVectorToArray(source) 31 | 32 | expect(unsubscribes).toEqual(0) 33 | expect(output).toEqual([]) 34 | 35 | await delay(TEARDOWN_DELAY + 50) 36 | deferred.resolve(null) 37 | await wait(source, (value) => { 38 | return value === 2 39 | }) 40 | 41 | expect(output).toEqual([[2]]) 42 | expect(unsubscribes).toEqual(0) 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /packages/retil-source/test/reduceVector.test.ts: -------------------------------------------------------------------------------- 1 | import { createState, fuse, reduceVector } from '../src' 2 | import { sendToArray } from './utils/sendToArray' 3 | 4 | describe(`reduceVector`, () => { 5 | test(`can create a source combining previous values and missing state`, () => { 6 | const [stateSource, setState] = createState(1) 7 | const [missingSource] = createState<number>() 8 | const evenSource = fuse((use) => { 9 | const state = use(stateSource) 10 | return state % 2 === 0 ? use(missingSource, state) : use(missingSource) 11 | }) 12 | const source = reduceVector( 13 | evenSource, 14 | (previousOutput, currentVector) => 15 | previousOutput.length || currentVector.length 16 | ? [ 17 | { 18 | latest: currentVector.length 19 | ? currentVector[0] 20 | : previousOutput[0].latest, 21 | missing: !currentVector.length, 22 | }, 23 | ] 24 | : [], 25 | [] as { latest: number; missing: boolean }[], 26 | ) 27 | const output = sendToArray(source) 28 | 29 | expect(output.reverse()).toEqual([]) 30 | 31 | setState(2) 32 | setState(3) 33 | setState(4) 34 | 35 | expect(output.reverse()).toEqual([ 36 | { latest: 2, missing: false }, 37 | { latest: 2, missing: true }, 38 | { latest: 4, missing: false }, 39 | ]) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /packages/retil-source/test/utils/sendToArray.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Source, 3 | getSnapshot, 4 | getVector, 5 | hasSnapshot, 6 | subscribe, 7 | } from '../../src' 8 | 9 | export function sendToArray<T>(source: Source<T>): T[] 10 | export function sendToArray<T, U>(source: Source<T>, sealWith: U): (T | U)[] 11 | export function sendToArray<T, U>(source: Source<T>, sealWith?: U): (T | U)[] { 12 | const array = [] as (T | U)[] 13 | if (hasSnapshot(source)) { 14 | array.unshift(getSnapshot(source)) 15 | } 16 | subscribe( 17 | source, 18 | () => { 19 | if (hasSnapshot(source)) { 20 | array.unshift(getSnapshot(source)) 21 | } 22 | }, 23 | () => { 24 | if (sealWith) { 25 | array.push(sealWith) 26 | } 27 | }, 28 | ) 29 | return array 30 | } 31 | 32 | export function sendVectorToArray<T>(source: Source<T>): T[][] 33 | export function sendVectorToArray<T, U>( 34 | source: Source<T>, 35 | sealWith: U, 36 | ): (T[] | U)[] 37 | export function sendVectorToArray<T, U>( 38 | source: Source<T>, 39 | sealWith?: U, 40 | ): (T[] | U)[] { 41 | const array = [] as (T[] | U)[] 42 | if (hasSnapshot(source)) { 43 | array.unshift(getVector(source)) 44 | } 45 | subscribe( 46 | source, 47 | () => array.unshift(getVector(source)), 48 | () => { 49 | if (sealWith) { 50 | array.push(sealWith) 51 | } 52 | }, 53 | ) 54 | return array 55 | } 56 | -------------------------------------------------------------------------------- /packages/retil-source/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.build.json", 3 | "include": [ 4 | "src/**/*" 5 | ] 6 | } -------------------------------------------------------------------------------- /packages/retil-support/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 James K Nelson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | 'Software'), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /packages/retil-support/README.md: -------------------------------------------------------------------------------- 1 | Contains utilities that aren't specific to retil itself. -------------------------------------------------------------------------------- /packages/retil-support/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "retil-support", 3 | "version": "0.28.4", 4 | "description": "Support utilities for retil", 5 | "author": "James K Nelson <james@jamesknelson.com>", 6 | "license": "MIT", 7 | "main": "dist/commonjs/index.js", 8 | "module": "dist/es/index.js", 9 | "types": "dist/es/index.d.ts", 10 | "scripts": { 11 | "clean": "rimraf dist", 12 | "build:commonjs": "tsc -p tsconfig.build.json --module commonjs --outDir dist/commonjs", 13 | "build:es-and-types": "tsc -p tsconfig.build.json --module es2015 --outDir dist/es --declaration", 14 | "build": "yarn run clean && yarn build:es-and-types && yarn build:commonjs", 15 | "build:watch": "yarn run clean && yarn build:es-and-types -- --watch", 16 | "lint": "eslint --ext ts,tsx src", 17 | "prepare": "yarn build" 18 | }, 19 | "dependencies": { 20 | "fast-deep-equal": "^3.1.3", 21 | "memoize-one": "^5.1.1", 22 | "ramda": "^0.27.1", 23 | "tslib": "^2.2.0" 24 | }, 25 | "devDependencies": { 26 | "@types/ramda": "^0.27.44", 27 | "typescript": "4.6.2" 28 | }, 29 | "files": [ 30 | "dist" 31 | ], 32 | "gitHead": "6c99063d3d8d4539ae81cce4f1b172ac2948c951" 33 | } 34 | -------------------------------------------------------------------------------- /packages/retil-support/src/areShallowEqual.ts: -------------------------------------------------------------------------------- 1 | export function areShallowEqual(a: any, b: any): boolean { 2 | const aIsNull = a === null 3 | const bIsNull = b === null 4 | 5 | if (aIsNull !== bIsNull) return false 6 | 7 | const aIsArray = Array.isArray(a) 8 | const bIsArray = Array.isArray(b) 9 | 10 | if (aIsArray !== bIsArray) return false 11 | 12 | const aTypeof = typeof a 13 | const bTypeof = typeof b 14 | 15 | if (aTypeof !== bTypeof) return false 16 | if (isPrimitive(aTypeof)) return a === b 17 | 18 | return aIsArray ? areArraysShallowEqual(a, b) : areObjectsShallowEqual(a, b) 19 | } 20 | 21 | export function areArraysShallowEqual(a: any[], b: any[]): boolean { 22 | const l = a.length 23 | if (l !== b.length) return false 24 | 25 | for (let i = 0; i < l; i++) { 26 | if (a[i] !== b[i]) return false 27 | } 28 | 29 | return true 30 | } 31 | 32 | export function areObjectsShallowEqual( 33 | a: { [key: string]: any }, 34 | b: { [key: string]: any }, 35 | ): boolean { 36 | let ka = 0 37 | let kb = 0 38 | 39 | for (const key in a) { 40 | if (a.hasOwnProperty(key) && a[key] !== b[key]) return false 41 | 42 | ka++ 43 | } 44 | 45 | for (const key in b) { 46 | if (b.hasOwnProperty(key)) kb++ 47 | } 48 | 49 | return ka === kb 50 | } 51 | 52 | function isPrimitive(type: string) { 53 | return type !== 'function' && type !== 'object' 54 | } 55 | -------------------------------------------------------------------------------- /packages/retil-support/src/deferred.ts: -------------------------------------------------------------------------------- 1 | // From https://developer.mozilla.org/en-US/docs/Mozilla/JavaScript_code_modules/Promise.jsm/Deferred 2 | export class Deferred<T = any> { 3 | /* A method to resolve the associated Promise with the value passed. 4 | * If the promise is already settled it does nothing. 5 | * 6 | * @param {anything} value : This value is used to resolve the promise 7 | * If the value is a Promise then the associated promise assumes the state 8 | * of Promise passed as value. 9 | */ 10 | resolve: (value: Promise<T> | T) => void = undefined as any 11 | 12 | /* A method to reject the assocaited Promise with the value passed. 13 | * If the promise is already settled it does nothing. 14 | * 15 | * @param {anything} reason: The reason for the rejection of the Promise. 16 | * Generally its an Error object. If however a Promise is passed, then the Promise 17 | * itself will be the reason for rejection no matter the state of the Promise. 18 | */ 19 | reject: (reason: any) => void = undefined as any 20 | 21 | /* A newly created Promise object. 22 | * Initially in pending state. 23 | */ 24 | promise = new Promise<T>((resolve: any, reject: any) => { 25 | this.resolve = resolve 26 | this.reject = reject 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /packages/retil-support/src/delay.ts: -------------------------------------------------------------------------------- 1 | export function delay(milliseconds: number): Promise<void> { 2 | if (milliseconds === Infinity) { 3 | milliseconds = Number.MAX_SAFE_INTEGER 4 | if (process.env.NODE_ENV !== 'production') { 5 | console.warn( 6 | 'You cannot create delays of infinite length. A very large number was used instead, but you should try to refactor your code to do without the long delay, as it can cause memory leaks.', 7 | ) 8 | } 9 | } 10 | return new Promise((resolve) => setTimeout(resolve, milliseconds)) 11 | } 12 | -------------------------------------------------------------------------------- /packages/retil-support/src/delayOne.ts: -------------------------------------------------------------------------------- 1 | export function delayOne<Result, Args extends any[], FirstResult>( 2 | fn: (...args: Args) => Result, 3 | firstResult: FirstResult, 4 | ): readonly [ 5 | delay: (...args: Args) => Result | FirstResult, 6 | peek: () => Result | FirstResult, 7 | ] { 8 | let nextResult: Result | FirstResult = firstResult 9 | return [ 10 | (...args: Args): Result | FirstResult => { 11 | const thisResult = nextResult 12 | nextResult = fn(...args) 13 | return thisResult 14 | }, 15 | () => nextResult, 16 | ] as const 17 | } 18 | -------------------------------------------------------------------------------- /packages/retil-support/src/emptyArray.ts: -------------------------------------------------------------------------------- 1 | export const emptyArray = [] 2 | -------------------------------------------------------------------------------- /packages/retil-support/src/emptyObject.ts: -------------------------------------------------------------------------------- 1 | export const emptyObject = {} 2 | -------------------------------------------------------------------------------- /packages/retil-support/src/ensureTruthyArray.ts: -------------------------------------------------------------------------------- 1 | import { chain } from 'ramda' 2 | 3 | export type CastableToTruthyArrayOf<T> = 4 | | undefined 5 | | false 6 | | null 7 | | T 8 | | (undefined | false | null | CastableToTruthyArrayOf<T>)[] 9 | 10 | export function ensureTruthyArray<T>( 11 | maybeArray: CastableToTruthyArrayOf<T>, 12 | ): T[] { 13 | if (!maybeArray) { 14 | return [] 15 | } else if (Array.isArray(maybeArray)) { 16 | return chain(ensureTruthyArray, maybeArray.filter(Boolean)) 17 | } else { 18 | return [maybeArray] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/retil-support/src/fromEntries.ts: -------------------------------------------------------------------------------- 1 | export function fromEntries<V>(entries: [string, V][]): { [key: string]: V } { 2 | const obj = {} as { [key: string]: any } 3 | for (const [key, value] of entries) { 4 | obj[key] = value 5 | } 6 | return obj 7 | } 8 | -------------------------------------------------------------------------------- /packages/retil-support/src/getForm.ts: -------------------------------------------------------------------------------- 1 | export function getForm(node: HTMLElement | null): HTMLFormElement | null { 2 | while (node) { 3 | if (node.tagName && node.tagName.toLowerCase() === 'form') { 4 | return node as HTMLFormElement 5 | } 6 | node = node.parentNode as HTMLElement 7 | } 8 | return null 9 | } 10 | -------------------------------------------------------------------------------- /packages/retil-support/src/identity.ts: -------------------------------------------------------------------------------- 1 | export const identity = <T>(x: T) => x 2 | -------------------------------------------------------------------------------- /packages/retil-support/src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as areDeepEqual } from 'fast-deep-equal' 2 | export { default as memoizeOne } from 'memoize-one' 3 | 4 | export * from 'ramda' 5 | 6 | export * from './areShallowEqual' 7 | export * from './arrayKeyedMap' 8 | export * from './deferred' 9 | export * from './delay' 10 | export * from './delayOne' 11 | export * from './emptyArray' 12 | export * from './emptyObject' 13 | export * from './ensureTruthyArray' 14 | export * from './fastCartesian' 15 | export * from './fromEntries' 16 | export * from './getForm' 17 | export { identity } from './identity' 18 | export * from './isPlainObject' 19 | export * from './isPromiseLike' 20 | export * from './joinClassNames' 21 | export * from './joinEventHandlers' 22 | export * from './joinRefs' 23 | export * from './keyPartitioner' 24 | export * from './maybe' 25 | export * from './memo' 26 | export * from './noop' 27 | export * from './pendingPromiseLike' 28 | export * from './preventDefaultEventHandler' 29 | export * from './root' 30 | export * from './suspendIndefinitely' 31 | export * from './useConfigurator' 32 | export * from './useEffectOnce' 33 | export * from './useFirstInstanceOfLatestValue' 34 | export * from './useMemoizeOneValue' 35 | export * from './useSilencedLayoutEffect' 36 | -------------------------------------------------------------------------------- /packages/retil-support/src/isPlainObject.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @param obj The object to inspect. 3 | * @returns True if the argument appears to be a plain object. 4 | */ 5 | export function isPlainObject(obj: any): boolean { 6 | if (typeof obj !== 'object' || obj === null) return false 7 | 8 | let proto = obj 9 | while (Object.getPrototypeOf(proto) !== null) { 10 | proto = Object.getPrototypeOf(proto) 11 | } 12 | 13 | return Object.getPrototypeOf(obj) === proto 14 | } 15 | -------------------------------------------------------------------------------- /packages/retil-support/src/isPromiseLike.ts: -------------------------------------------------------------------------------- 1 | export function isPromiseLike(x: any): x is PromiseLike<any> { 2 | return x && typeof x.then === 'function' 3 | } 4 | -------------------------------------------------------------------------------- /packages/retil-support/src/joinClassNames.ts: -------------------------------------------------------------------------------- 1 | export function joinClassNames( 2 | x: string, 3 | y?: string | false, 4 | ...zs: (string | false | undefined)[] 5 | ): string 6 | export function joinClassNames( 7 | x: string | undefined | false, 8 | y: string, 9 | ...zs: (string | false | undefined)[] 10 | ): string 11 | export function joinClassNames( 12 | x?: string | false, 13 | y?: string | false, 14 | ...zs: (string | false | undefined)[] 15 | ): string | undefined 16 | export function joinClassNames( 17 | x?: string | false, 18 | y?: string | false, 19 | ...zs: (string | false | undefined)[] 20 | ): string | undefined { 21 | const classNames = [x, y, ...zs].filter(Boolean) 22 | return classNames.length ? classNames.join(' ') : undefined 23 | } 24 | -------------------------------------------------------------------------------- /packages/retil-support/src/joinEventHandlers.ts: -------------------------------------------------------------------------------- 1 | import memoizeOne from 'memoize-one' 2 | import { EventHandler, SyntheticEvent, useMemo } from 'react' 3 | 4 | // Execute two event handlers in sequence, unless the first event handler 5 | // prevents the default action, in which case the second handler will 6 | // be abandoned. 7 | export function joinEventHandlers<E extends SyntheticEvent>( 8 | x: EventHandler<E>, 9 | y?: EventHandler<E>, 10 | ...zs: (EventHandler<E> | undefined)[] 11 | ): EventHandler<E> 12 | export function joinEventHandlers<E extends SyntheticEvent>( 13 | x: EventHandler<E> | undefined, 14 | y: EventHandler<E>, 15 | ...zs: (EventHandler<E> | undefined)[] 16 | ): EventHandler<E> 17 | export function joinEventHandlers<E extends SyntheticEvent>( 18 | x?: EventHandler<E>, 19 | y?: EventHandler<E>, 20 | ...zs: (EventHandler<E> | undefined)[] 21 | ): EventHandler<E> | undefined 22 | export function joinEventHandlers<E extends SyntheticEvent>( 23 | x?: EventHandler<E>, 24 | y?: EventHandler<E>, 25 | ...zs: (EventHandler<E> | undefined)[] 26 | ): EventHandler<E> | undefined { 27 | const joined = 28 | !x || !y 29 | ? x || y 30 | : (event: E): void => { 31 | x(event) 32 | if (!event.defaultPrevented) { 33 | y(event) 34 | } 35 | } 36 | return zs.length ? joinEventHandlers(joined, ...zs) : joined 37 | } 38 | 39 | export function useJoinEventHandlers() { 40 | return useMemo(() => memoizeOne(joinEventHandlers), []) 41 | } 42 | 43 | export const useJoinedEventHandler: typeof joinEventHandlers = ( 44 | x: any, 45 | y: any, 46 | ) => useMemo(() => joinEventHandlers(x, y), [x, y]) 47 | -------------------------------------------------------------------------------- /packages/retil-support/src/maybe.ts: -------------------------------------------------------------------------------- 1 | export type Maybe<T> = [T] | [] 2 | 3 | export const hasValue = <T>(maybe: Maybe<T>): maybe is [T] => maybe.length > 0 4 | -------------------------------------------------------------------------------- /packages/retil-support/src/noop.ts: -------------------------------------------------------------------------------- 1 | export const noop = () => {} 2 | -------------------------------------------------------------------------------- /packages/retil-support/src/pendingPromiseLike.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * An always-pending promise that doesn't leak memory like a sieve. 3 | */ 4 | export const pendingPromiseLike: PromiseLike<never> = { 5 | then: () => pendingPromiseLike, 6 | } 7 | -------------------------------------------------------------------------------- /packages/retil-support/src/preventDefaultEventHandler.ts: -------------------------------------------------------------------------------- 1 | export const preventDefaultEventHandler = (event: { 2 | preventDefault: () => void 3 | }) => { 4 | event.preventDefault() 5 | } 6 | -------------------------------------------------------------------------------- /packages/retil-support/src/root.ts: -------------------------------------------------------------------------------- 1 | // A utility type for specifying a "root" property in an object, e.g. for 2 | // attaching information to the object itself, as opposed to a nested path. 3 | 4 | export const root = Symbol.for('root') 5 | export type Root = typeof root 6 | -------------------------------------------------------------------------------- /packages/retil-support/src/suspendIndefinitely.ts: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react' 2 | 3 | import { pendingPromiseLike } from './pendingPromiseLike' 4 | 5 | export const SuspendIndefinitely: FunctionComponent<{}> = () => { 6 | throw pendingPromiseLike 7 | } 8 | -------------------------------------------------------------------------------- /packages/retil-support/src/useConfigurator.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from 'react' 2 | 3 | import { areObjectsShallowEqual } from './areShallowEqual' 4 | 5 | export type Configurator<Config extends object, Value> = ( 6 | initialConfig: Config, 7 | ) => readonly [reconfigure: (nextConfig: Config) => void, value: Value] 8 | 9 | export function useConfigurator<Config extends object, Value>( 10 | configurator: Configurator<Config, Value>, 11 | config: Config, 12 | ): Value { 13 | const [[reconfigure, value], setState] = useState(() => configurator(config)) 14 | 15 | const latestConfiguratorRef = useRef(configurator) 16 | const latestConfigRef = useRef(config) 17 | 18 | useEffect(() => { 19 | if (configurator !== latestConfiguratorRef.current) { 20 | setState(configurator(config)) 21 | } else if (!areObjectsShallowEqual(config, latestConfigRef.current)) { 22 | reconfigure(config) 23 | } 24 | latestConfiguratorRef.current = configurator 25 | latestConfigRef.current = config 26 | }, [configurator, config, reconfigure]) 27 | 28 | return value 29 | } 30 | -------------------------------------------------------------------------------- /packages/retil-support/src/useEffectOnce.ts: -------------------------------------------------------------------------------- 1 | import { EffectCallback, useEffect } from 'react' 2 | 3 | export function useEffectOnce(cb: EffectCallback) { 4 | // eslint-disable-next-line react-hooks/exhaustive-deps 5 | useEffect(cb, []) 6 | } 7 | -------------------------------------------------------------------------------- /packages/retil-support/src/useFirstInstanceOfLatestValue.ts: -------------------------------------------------------------------------------- 1 | import deepEqual from 'fast-deep-equal' 2 | import { useEffect, useRef } from 'react' 3 | 4 | export function useFirstInstanceOfLatestValue<T>( 5 | instance: T, 6 | isEqual: (x: T, y: T) => boolean = deepEqual, 7 | ): T { 8 | const ref = useRef(instance) 9 | const firstInstance = isEqual(ref.current, instance) ? ref.current : instance 10 | useEffect(() => { 11 | ref.current = firstInstance 12 | }, [firstInstance]) 13 | return firstInstance 14 | } 15 | -------------------------------------------------------------------------------- /packages/retil-support/src/useMemoizeOneValue.ts: -------------------------------------------------------------------------------- 1 | import deepEqual from 'fast-deep-equal' 2 | import memoizeOne from 'memoize-one' 3 | import { useMemo } from 'react' 4 | 5 | import { areShallowEqual } from './areShallowEqual' 6 | import { identity } from './identity' 7 | 8 | export function useMemoizeOneShallowValue(): <T>(x: T) => T { 9 | return useMemo( 10 | () => memoizeOne(identity, ([x], [y]) => areShallowEqual(x, y)), 11 | [], 12 | ) 13 | } 14 | 15 | export function useMemoizeOneValue(): <T>(x: T) => T { 16 | return useMemo(() => memoizeOne(identity, deepEqual), []) 17 | } 18 | -------------------------------------------------------------------------------- /packages/retil-support/src/useSilencedLayoutEffect.ts: -------------------------------------------------------------------------------- 1 | import { useLayoutEffect } from 'react' 2 | 3 | import { noop } from './noop' 4 | 5 | // React currently throws a warning when using useLayoutEffect on the server. 6 | // To get around it, we can conditionally useEffect on the server (no-op) and 7 | // useLayoutEffect in the browser. We need useLayoutEffect because we want 8 | // `connect` to perform sync updates to a ref to save the latest props after 9 | // a render is actually committed to the DOM. 10 | export const useSilencedLayoutEffect = 11 | typeof window !== 'undefined' ? useLayoutEffect : noop 12 | -------------------------------------------------------------------------------- /packages/retil-support/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.build.json", 3 | "include": [ 4 | "src/**/*" 5 | ] 6 | } -------------------------------------------------------------------------------- /packages/retil-transition/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 James K Nelson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | 'Software'), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /packages/retil-transition/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "retil-transition", 3 | "version": "0.28.4", 4 | "description": "Smooth transitions made easy", 5 | "author": "James K Nelson <james@jamesknelson.com>", 6 | "license": "MIT", 7 | "main": "dist/commonjs/index.js", 8 | "module": "dist/es/index.js", 9 | "types": "dist/es/index.d.ts", 10 | "scripts": { 11 | "clean": "rimraf dist", 12 | "build:commonjs": "tsc -p tsconfig.build.json --module commonjs --outDir dist/commonjs", 13 | "build:es-and-types": "tsc -p tsconfig.build.json --module es2015 --outDir dist/es --declaration", 14 | "build": "yarn run clean && yarn build:es-and-types && yarn build:commonjs", 15 | "build:watch": "yarn run clean && yarn build:es-and-types -- --watch", 16 | "lint": "eslint --ext ts,tsx src", 17 | "prepare": "yarn build" 18 | }, 19 | "dependencies": { 20 | "retil-hydration": "^0.28.4", 21 | "retil-support": "^0.28.4" 22 | }, 23 | "devDependencies": { 24 | "react-spring": "^9.4.2", 25 | "typescript": "4.6.2" 26 | }, 27 | "peerDependencies": { 28 | "react-spring": "^9.4.2" 29 | }, 30 | "files": [ 31 | "dist" 32 | ], 33 | "gitHead": "9d352aa11ce02bdf184dfe8ff15a90a3ad4543bc" 34 | } 35 | -------------------------------------------------------------------------------- /packages/retil-transition/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './columnTransition' 2 | export * from './transitionConfigs' 3 | export * from './transitionHandle' 4 | export * from './transitionHandleRefContext' 5 | -------------------------------------------------------------------------------- /packages/retil-transition/src/transitionConfigs.ts: -------------------------------------------------------------------------------- 1 | import { ControllerUpdate } from 'react-spring' 2 | 3 | export interface TransitionConfig { 4 | initial?: Record<string, any> 5 | from?: Record<string, any> 6 | enter?: ControllerUpdate<Record<string, any>> 7 | exit?: ControllerUpdate<Record<string, any>> 8 | } 9 | 10 | export const dropfadeTransitionConfig: TransitionConfig = { 11 | initial: { 12 | opacity: 1, 13 | transform: 'none', 14 | }, 15 | from: { 16 | opacity: 0, 17 | transform: 'translateY(-10vh)', 18 | }, 19 | enter: { 20 | opacity: 1, 21 | transform: 'translateY(0vh)', 22 | config: { 23 | friction: 50, 24 | mass: 1, 25 | tension: 500, 26 | }, 27 | }, 28 | exit: { 29 | opacity: 0, 30 | transform: 'translateY(10vh)', 31 | config: { 32 | clamp: true, 33 | friction: 15, 34 | mass: 1, 35 | tension: 300, 36 | }, 37 | }, 38 | } 39 | 40 | const crossfadeConfig = { 41 | mass: 5, 42 | tension: 50, 43 | friction: 10, 44 | clamp: true, 45 | } 46 | 47 | export const crossfadeTransitionConfig: TransitionConfig = { 48 | initial: { 49 | opacity: 0, 50 | }, 51 | from: { 52 | opacity: 0, 53 | }, 54 | enter: { 55 | opacity: 1, 56 | config: crossfadeConfig, 57 | }, 58 | exit: { 59 | opacity: 0, 60 | config: crossfadeConfig, 61 | }, 62 | } 63 | -------------------------------------------------------------------------------- /packages/retil-transition/src/transitionHandle.ts: -------------------------------------------------------------------------------- 1 | import { Ref, RefObject, useCallback, useImperativeHandle, useRef } from 'react' 2 | 3 | export interface TransitionHandle { 4 | show: () => Promise<void> 5 | hide: () => Promise<void> 6 | } 7 | 8 | export type TransitionHandleRef = Ref<TransitionHandle> 9 | 10 | export function useTransitionHandleRef(): RefObject<TransitionHandle> { 11 | return useRef<TransitionHandle>(null) 12 | } 13 | 14 | export function useTransitionHandle( 15 | transitionHandleRef: TransitionHandleRef | undefined, 16 | { 17 | show, 18 | hide, 19 | }: { 20 | show: () => Promise<unknown> 21 | hide: () => Promise<unknown> 22 | }, 23 | deps: any[], 24 | ): void { 25 | const wrappedHide = useCallback(async () => { 26 | const result = await hide() 27 | await (Array.isArray(result) ? Promise.all(result) : result) 28 | }, [hide]) 29 | 30 | const wrappedShow = useCallback(async () => { 31 | const result = await show() 32 | await (Array.isArray(result) ? Promise.all(result) : result) 33 | }, [show]) 34 | 35 | useImperativeHandle( 36 | transitionHandleRef, 37 | () => ({ 38 | show: wrappedShow, 39 | hide: wrappedHide, 40 | }), 41 | // eslint-disable-next-line react-hooks/exhaustive-deps 42 | deps, 43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /packages/retil-transition/src/transitionHandleRefContext.ts: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from 'react' 2 | 3 | import type { TransitionHandleRef } from './transitionHandle' 4 | 5 | /** 6 | * Allows an element to provide methods to transition itself in and out, 7 | * typically overriding any default transition provided by its ancestors. 8 | */ 9 | export const transitionHandleRefContext = 10 | createContext<null | TransitionHandleRef>(null) 11 | 12 | export function useTransitionHandleRefContext() { 13 | return useContext(transitionHandleRefContext) 14 | } 15 | -------------------------------------------------------------------------------- /packages/retil-transition/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.build.json", 3 | "include": [ 4 | "src/**/*" 5 | ] 6 | } -------------------------------------------------------------------------------- /packages/retil/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 James K Nelson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | 'Software'), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /packages/retil/README.md: -------------------------------------------------------------------------------- 1 | # retil 2 | 3 | **The React Utility Belt** 4 | 5 | This package is an empty placeholder – code is located in sub-packages. See https://github.com/jamesknelson/retil for more details. -------------------------------------------------------------------------------- /packages/retil/index.ts: -------------------------------------------------------------------------------- 1 | // import 'retil-boundary' 2 | // import 'retil-css' 3 | // import 'retil-hydration' 4 | // import 'retil-interaction' 5 | // import 'retil-issues' 6 | // import 'retil-media' 7 | // import 'retil-mount' 8 | // import 'retil-nav' 9 | // import 'retil-nav-scheme' 10 | // import 'retil-operation' 11 | // import 'retil-source' 12 | // import 'retil-support' 13 | 14 | // This is a placeholder package. Please import directly from the package you 15 | // require. 16 | export default null 17 | -------------------------------------------------------------------------------- /packages/retil/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "retil", 3 | "version": "0.28.4", 4 | "description": "Superpowers for React Developers.", 5 | "author": "James K Nelson <james@jamesknelson.com>", 6 | "license": "MIT", 7 | "main": "dist/commonjs/index.js", 8 | "module": "dist/es/index.js", 9 | "types": "dist/es/index.d.ts", 10 | "scripts": { 11 | "clean": "rimraf dist", 12 | "build:commonjs": "tsc -p tsconfig.build.json --module commonjs --outDir dist/commonjs", 13 | "build:es-and-types": "tsc -p tsconfig.build.json --module es2015 --outDir dist/es --declaration", 14 | "build:umd": "tsc -p tsconfig.build.json --pretty --module es2015 --outDir dist/umd-intermediate && cross-env NODE_ENV=development rollup -c -o dist/umd/retil.js && rimraf dist/umd-intermediate", 15 | "build:umd:min": "tsc -p tsconfig.build.json --module es2015 --outDir dist/umd-intermediate && cross-env NODE_ENV=production rollup -c -o dist/umd/retil.min.js && rimraf dist/umd-intermediate", 16 | "build": "yarn run clean && yarn build:es-and-types && yarn build:commonjs && yarn build:umd && yarn build:umd:min", 17 | "build:watch": "yarn run clean && yarn build:es-and-types -- --watch", 18 | "prepare": "yarn build" 19 | }, 20 | "dependencies": { 21 | "tslib": "^2.2.0" 22 | }, 23 | "devDependencies": { 24 | "typescript": "4.6.2" 25 | }, 26 | "files": [ 27 | "dist" 28 | ], 29 | "gitHead": "6c99063d3d8d4539ae81cce4f1b172ac2948c951" 30 | } 31 | -------------------------------------------------------------------------------- /packages/retil/rollup.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is based on the rollup config from Redux 3 | */ 4 | 5 | import * as React from 'react' 6 | 7 | import commonjs from '@rollup/plugin-commonjs' 8 | import builtins from 'rollup-plugin-node-builtins' 9 | import nodeResolve from '@rollup/plugin-node-resolve' 10 | import replace from '@rollup/plugin-replace' 11 | import { terser } from 'rollup-plugin-terser' 12 | 13 | const env = process.env.NODE_ENV 14 | const config = { 15 | external: ['react'], 16 | input: 'dist/umd-intermediate/index.js', 17 | output: { 18 | format: 'umd', 19 | globals: { 20 | react: 'React', 21 | }, 22 | name: 'Retil', 23 | }, 24 | onwarn: function (warning) { 25 | // Suppress warning caused by TypeScript classes using "this" 26 | // https://github.com/rollup/rollup/wiki/Troubleshooting#this-is-undefined 27 | if (warning.code === 'THIS_IS_UNDEFINED') { 28 | return 29 | } 30 | console.error(warning.message) 31 | }, 32 | plugins: [ 33 | builtins(), 34 | 35 | nodeResolve(), 36 | 37 | commonjs({ 38 | namedExports: { 39 | react: Object.keys(React), 40 | }, 41 | }), 42 | 43 | replace({ 44 | 'process.env.NODE_ENV': JSON.stringify(env), 45 | }), 46 | ], 47 | } 48 | 49 | if (env === 'production') { 50 | config.plugins.push(terser()) 51 | } 52 | 53 | export default config 54 | -------------------------------------------------------------------------------- /packages/retil/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.build.json", 3 | "include": [ 4 | "index.ts" 5 | ] 6 | } -------------------------------------------------------------------------------- /packages/tool-next/jest.config.js: -------------------------------------------------------------------------------- 1 | const { pathsToModuleNameMapper } = require('ts-jest/utils') 2 | // In the following statement, replace `./tsconfig` with the path to your `tsconfig` file 3 | // which contains the path mapping (ie the `compilerOptions.paths` option): 4 | const { compilerOptions } = require('../../tsconfig') 5 | 6 | module.exports = { 7 | moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { 8 | prefix: '<rootDir>/../../', 9 | }), 10 | modulePaths: ['<rootDir>/src/'], 11 | modulePathIgnorePatterns: ['<rootDir>/demo/'], 12 | preset: 'ts-jest', 13 | testEnvironment: 'jsdom', 14 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$', 15 | } 16 | -------------------------------------------------------------------------------- /packages/tool-next/package.json-bak: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@retil/tool-next", 3 | "version": "0.20.1", 4 | "description": "The missing piece of Next.js.", 5 | "author": "James K Nelson <james@jamesknelson.com>", 6 | "license": "MIT", 7 | "main": "dist/commonjs/index.js", 8 | "module": "dist/es/index.js", 9 | "types": "dist/types/index.d.ts", 10 | "scripts": { 11 | "clean": "rimraf dist", 12 | "build:commonjs": "tsc -p tsconfig.build.json --module commonjs --outDir dist/commonjs", 13 | "build:es": "tsc -p tsconfig.build.json --module es2015 --outDir dist/es", 14 | "build:types": "tsc -p tsconfig.build.json --declaration --emitDeclarationOnly --outDir dist/types --isolatedModules false", 15 | "build": "yarn run clean && yarn build:es && yarn build:commonjs && yarn build:types", 16 | "build:watch": "yarn run clean && yarn build:es -- --types --watch", 17 | "lint": "eslint --ext ts,tsx src", 18 | "prepare": "yarn build", 19 | "test": "jest", 20 | "test:watch": "jest --watch" 21 | }, 22 | "dependencies": { 23 | "retil-history": "^0.20.1", 24 | "retil-router": "^0.20.1", 25 | "tslib": "2.0.1" 26 | }, 27 | "devDependencies": { 28 | "@types/webpack": "^4.41.26", 29 | "next": "10.0.5", 30 | "typescript": "4.6.2" 31 | }, 32 | "files": [ 33 | "dist" 34 | ], 35 | "gitHead": "c3d07b313e425c572bf9b7bce2ca8ff09fb0f446" 36 | } 37 | -------------------------------------------------------------------------------- /packages/tool-next/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './nextilApp' 2 | export * from './nextilRoutedPage' 3 | export * from './nextilRouter' 4 | export * from './nextilTypes' 5 | -------------------------------------------------------------------------------- /packages/tool-next/src/nextilConstants.ts: -------------------------------------------------------------------------------- 1 | export const BypassSerializationHack = Symbol() 2 | -------------------------------------------------------------------------------- /packages/tool-next/src/nextilNotFound.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { RouterFunction } from 'retil-router' 3 | 4 | import { NextilRequest, NextilResponse } from './nextilTypes' 5 | 6 | const defaultNotFoundRouter = () => ( 7 | <div> 8 | <h1>404 Not Found</h1> 9 | </div> 10 | ) 11 | 12 | export const notFoundRouterRef: { 13 | current: RouterFunction<NextilRequest, NextilResponse> 14 | } = { 15 | current: defaultNotFoundRouter, 16 | } 17 | -------------------------------------------------------------------------------- /packages/tool-next/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.build.json", 3 | "include": [ 4 | "src/**/*" 5 | ] 6 | } -------------------------------------------------------------------------------- /packages/tool-vite-plugin-code-as-content/README.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | This Vite config helps you create React websites which that follow a Code As Content paradigm. To facilitate this, it gives you: 4 | 5 | - Basic MDX support 6 | - Improved typography 7 | - Syntax highlighting 8 | - Front matter support 9 | - Support for glob-importing front matter or syntax-highlighted versions of your source files 10 | 11 | 12 | ## For contributors 13 | 14 | At the time of writing, this package structures its distributables and build system differently to most other packages in this repository. While other packages build both CommonJS and ES Modules distributables, this package builds only ESM, and provides a pre-made CommonJS wrapper to load the ESM build output when CommonJS is required. 15 | 16 | This structure is due to the fact that many content processing modules do not provide CommonJS outputs, which means they cannot be imported directly in CommonJS-based vite servers. 17 | 18 | -------------------------------------------------------------------------------- /packages/tool-vite-plugin-code-as-content/client.d.ts: -------------------------------------------------------------------------------- 1 | /// <reference path="./type/importMeta.d.ts" /> 2 | /// <reference path="./type/mdx.d.ts" /> 3 | -------------------------------------------------------------------------------- /packages/tool-vite-plugin-code-as-content/compat.cjs: -------------------------------------------------------------------------------- 1 | async function getCodeAsContentPlugins(options) { 2 | const { getCodeAsContentPlugins } = await import('./dist/index.js') 3 | return getCodeAsContentPlugins(options) 4 | } 5 | 6 | async function registerRefractorLanguages(...languageNames) { 7 | const { registerRefractorLanguages } = await import('./dist/index.js') 8 | return registerRefractorLanguages(languageNames) 9 | } 10 | 11 | module.exports = { 12 | getCodeAsContentPlugins, 13 | registerRefractorLanguages, 14 | } 15 | -------------------------------------------------------------------------------- /packages/tool-vite-plugin-code-as-content/src/importFrontMatterPlugin.ts: -------------------------------------------------------------------------------- 1 | import matter from 'gray-matter' 2 | import { promises as fs } from 'fs' 3 | import { Plugin } from 'vite' 4 | 5 | const trigger = '?frontMatter' 6 | 7 | const importFrontMatterPlugin = (): Plugin => { 8 | return { 9 | name: 'frontMatter', 10 | 11 | resolveId(id) { 12 | if (id.endsWith(trigger)) { 13 | return id 14 | } 15 | }, 16 | 17 | async load(maybeIdWithTrigger) { 18 | if (maybeIdWithTrigger.endsWith(trigger)) { 19 | const id = maybeIdWithTrigger.slice(0, -trigger.length) 20 | const resolution = await this.resolve(id) 21 | if (resolution?.id) { 22 | return await fs.readFile(resolution?.id, 'utf8') 23 | } 24 | } 25 | return null 26 | }, 27 | 28 | transform(source, maybeIdWithTrigger) { 29 | if (maybeIdWithTrigger.endsWith(trigger)) { 30 | const data = matter(source).data || {} 31 | return `export default ${JSON.stringify(data)};` 32 | } 33 | }, 34 | } 35 | } 36 | 37 | export default importFrontMatterPlugin 38 | -------------------------------------------------------------------------------- /packages/tool-vite-plugin-code-as-content/src/typography.ts: -------------------------------------------------------------------------------- 1 | import textr from 'textr' 2 | import apostrophes from 'typographic-apostrophes' 3 | import quotes from 'typographic-quotes' 4 | import apostrophesForPlurals from 'typographic-apostrophes-for-possessive-plurals' 5 | import ellipses from 'typographic-ellipses' 6 | 7 | export default textr().use( 8 | apostrophes, 9 | quotes, 10 | apostrophesForPlurals, 11 | ellipses, 12 | // em dashes 13 | (input: string) => input.replace(/--/gim, '—'), 14 | // en dashes, 15 | (input: string) => input.replace(/(\d)-(\d)/gim, '$1–$2'), 16 | ) 17 | -------------------------------------------------------------------------------- /packages/tool-vite-plugin-code-as-content/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.build.json", 3 | "include": [ 4 | "type/*.d.ts", 5 | "src/**/*" 6 | ] 7 | } -------------------------------------------------------------------------------- /packages/tool-vite-plugin-code-as-content/type/hast-util-to-string.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'hast-util-to-string' { 2 | const fn: (input: any) => string 3 | export default fn 4 | } 5 | -------------------------------------------------------------------------------- /packages/tool-vite-plugin-code-as-content/type/importMeta.d.ts: -------------------------------------------------------------------------------- 1 | interface ImportMeta { 2 | frontMatterGlobEager(pattern: string): Record<string, Record<string, any>> 3 | highlightedSourceGlobEager(pattern: string): Record<string, string> 4 | } 5 | -------------------------------------------------------------------------------- /packages/tool-vite-plugin-code-as-content/type/mdx.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.mdx' { 2 | export const meta: Record<string, any> 3 | 4 | const MDXComponent: React.ComponentType 5 | export default MDXComponent 6 | } 7 | -------------------------------------------------------------------------------- /packages/tool-vite-plugin-code-as-content/type/remark-slug.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'remark-*' { 2 | import { Plugin } from 'unified' 3 | const plugin: Plugin 4 | export default plugin 5 | } 6 | -------------------------------------------------------------------------------- /packages/tool-vite-plugin-code-as-content/type/typographic.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'typographic-*' { 2 | const fn: (input: string) => string 3 | export default fn 4 | } 5 | -------------------------------------------------------------------------------- /packages/tool-vite-plugin-code-as-content/type/unist-util-visit.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'unist-util-visit' { 2 | const fn: (input: any) => string 3 | export default fn 4 | } 5 | -------------------------------------------------------------------------------- /packages/tool-vite-plugin-emotion/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@retil/tool-vite-plugin-emotion", 3 | "version": "0.28.4", 4 | "private": false, 5 | "description": "Add emotion support to vite", 6 | "author": "James K Nelson <james@jamesknelson.com>", 7 | "license": "MIT", 8 | "main": "dist/commonjs/index.js", 9 | "module": "dist/es/index.js", 10 | "types": "dist/es/index.d.ts", 11 | "scripts": { 12 | "clean": "rimraf dist", 13 | "build:commonjs": "tsc -p tsconfig.build.json --module commonjs --outDir dist/commonjs", 14 | "build:es-and-types": "tsc -p tsconfig.build.json --module es2015 --outDir dist/es --declaration", 15 | "build": "yarn run clean && yarn build:es-and-types && yarn build:commonjs", 16 | "build:watch": "yarn run clean && yarn build:es-and-types -- --watch", 17 | "lint": "eslint --ext ts,tsx src", 18 | "prepare": "yarn build" 19 | }, 20 | "dependencies": { 21 | "@emotion/babel-plugin": "^11.7.2" 22 | }, 23 | "peerDependencies": { 24 | "@babel/core": "^7.0.0", 25 | "vite": "^2.8.0" 26 | }, 27 | "devDependencies": { 28 | "@babel/core": "^7.13.10", 29 | "vite": "^2.8.6" 30 | }, 31 | "files": [ 32 | "dist" 33 | ], 34 | "gitHead": "6c99063d3d8d4539ae81cce4f1b172ac2948c951" 35 | } 36 | -------------------------------------------------------------------------------- /packages/tool-vite-plugin-emotion/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.build.json", 3 | "include": [ 4 | "type/*.d.ts", 5 | "src/**/*" 6 | ] 7 | } -------------------------------------------------------------------------------- /packages/tool-vite-plugin-emotion/type/emotion__babel-plugin.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@emotion/babel-plugin' { 2 | export default function plugin(bytes: any): any 3 | } 4 | -------------------------------------------------------------------------------- /packages/tool-vite-plugin-styled-components/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@retil/tool-vite-plugin-styled-components", 3 | "version": "0.28.4", 4 | "private": false, 5 | "description": "Add styled-components support to vite", 6 | "author": "James K Nelson <james@jamesknelson.com>", 7 | "license": "MIT", 8 | "main": "dist/commonjs/index.js", 9 | "module": "dist/es/index.js", 10 | "types": "dist/es/index.d.ts", 11 | "scripts": { 12 | "clean": "rimraf dist", 13 | "build:commonjs": "tsc -p tsconfig.build.json --module commonjs --outDir dist/commonjs", 14 | "build:es-and-types": "tsc -p tsconfig.build.json --module es2015 --outDir dist/es --declaration", 15 | "build": "yarn run clean && yarn build:es-and-types && yarn build:commonjs", 16 | "build:watch": "yarn run clean && yarn build:es-and-types -- --watch", 17 | "lint": "eslint --ext ts,tsx src", 18 | "prepare": "yarn build" 19 | }, 20 | "dependencies": { 21 | "babel-plugin-styled-components": "^2.0.6" 22 | }, 23 | "peerDependencies": { 24 | "@babel/core": "^7.0.0", 25 | "vite": "^2.8.0" 26 | }, 27 | "devDependencies": { 28 | "@babel/core": "^7.13.10", 29 | "vite": "^2.8.6" 30 | }, 31 | "files": [ 32 | "dist" 33 | ], 34 | "gitHead": "6c99063d3d8d4539ae81cce4f1b172ac2948c951" 35 | } 36 | -------------------------------------------------------------------------------- /packages/tool-vite-plugin-styled-components/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.build.json", 3 | "include": [ 4 | "type/*.d.ts", 5 | "src/**/*" 6 | ] 7 | } -------------------------------------------------------------------------------- /packages/tool-vite-plugin-styled-components/type/babel-plugin-styled-components.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'babel-plugin-styled-components' { 2 | export default function plugin(bytes: any): any 3 | } 4 | -------------------------------------------------------------------------------- /packages/tool-vite-plugin-svg/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@retil/tool-vite-plugin-svg", 3 | "version": "0.28.4", 4 | "private": false, 5 | "description": "Add SVG support to vite", 6 | "author": "James K Nelson <james@jamesknelson.com>", 7 | "license": "MIT", 8 | "main": "dist/commonjs/index.js", 9 | "module": "dist/es/index.js", 10 | "types": "dist/es/index.d.ts", 11 | "scripts": { 12 | "clean": "rimraf dist", 13 | "build:commonjs": "tsc -p tsconfig.build.json --module commonjs --outDir dist/commonjs", 14 | "build:es-and-types": "tsc -p tsconfig.build.json --module es2015 --outDir dist/es --declaration", 15 | "build": "yarn run clean && yarn build:es-and-types && yarn build:commonjs", 16 | "build:watch": "yarn run clean && yarn build:es-and-types -- --watch", 17 | "lint": "eslint --ext ts,tsx src", 18 | "prepare": "yarn build" 19 | }, 20 | "dependencies": { 21 | "@babel/plugin-transform-react-jsx": "^7.17.0", 22 | "@svgr/core": "^6.1.2", 23 | "@svgr/plugin-jsx": "^6.1.2", 24 | "@svgr/plugin-svgo": "^6.1.2" 25 | }, 26 | "peerDependencies": { 27 | "@babel/core": "^7.0.0", 28 | "vite": "^2.8.0" 29 | }, 30 | "devDependencies": { 31 | "@babel/core": "^7.13.10", 32 | "@types/svgo": "^2.6.2", 33 | "vite": "^2.8.6" 34 | }, 35 | "files": [ 36 | "dist" 37 | ], 38 | "gitHead": "6c99063d3d8d4539ae81cce4f1b172ac2948c951" 39 | } 40 | -------------------------------------------------------------------------------- /packages/tool-vite-plugin-svg/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.build.json", 3 | "include": [ 4 | "src/**/*" 5 | ] 6 | } -------------------------------------------------------------------------------- /site/.gitignore: -------------------------------------------------------------------------------- 1 | .vercel 2 | -------------------------------------------------------------------------------- /site/index.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html lang="en"> 3 | 4 | <head> 5 | <meta charset="UTF-8" /> 6 | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 7 | <link rel="icon" href="/src/asset/favicon.ico"> 8 | <!--head-html--> 9 | </head> 10 | 11 | <body> 12 | <div id="root"><!--app-html--></div> 13 | <script type="module" src="/src/entry-client.tsx"></script> 14 | </body> 15 | 16 | </html> -------------------------------------------------------------------------------- /site/prerender.ts: -------------------------------------------------------------------------------- 1 | // Pre-render the app into static HTML. 2 | // run `yarn generate` and then `dist/static` can be served as a static site. 3 | 4 | const fs = require('fs') 5 | const path = require('path') 6 | 7 | const toAbsolute = (p) => path.resolve(__dirname, p) 8 | 9 | const template = fs.readFileSync(toAbsolute('dist/static/index.html'), 'utf-8') 10 | const { render } = require('./dist/server/entry-server.js') 11 | 12 | // determine routes to pre-render from src/pages 13 | const routesToPrerender = fs 14 | .readdirSync(toAbsolute('src/pages')) 15 | .map((file) => { 16 | const name = file.replace(/\.jsx$/, '').toLowerCase() 17 | return name === 'home' ? `/` : `/${name}` 18 | }) 19 | 20 | ;(async () => { 21 | // pre-render each route... 22 | for (const url of routesToPrerender) { 23 | const context = {} 24 | const appHtml = await render(url, context) 25 | 26 | const html = template.replace(`<!--app-html-->`, appHtml) 27 | 28 | const filePath = `dist/static${url === '/' ? '/index' : url}.html` 29 | fs.writeFileSync(toAbsolute(filePath), html) 30 | console.log('pre-rendered:', filePath) 31 | } 32 | })() 33 | -------------------------------------------------------------------------------- /site/react-shim.js: -------------------------------------------------------------------------------- 1 | import { jsx } from '@emotion/react' 2 | import { Fragment } from 'react' 3 | export { jsx } 4 | export { Fragment } 5 | -------------------------------------------------------------------------------- /site/src/app/appLoader.tsx: -------------------------------------------------------------------------------- 1 | import { loadAsync } from 'retil-mount' 2 | import { loadMatch, loadNotFoundBoundary } from 'retil-nav' 3 | import { patternFor } from 'retil-nav-scheme' 4 | 5 | import { DocumentContent } from 'site/src/component/document' 6 | 7 | import conceptLoader from './concepts/conceptLoader' 8 | import exampleLoader from './examples/exampleLoader' 9 | import packageLoader from './packages/packageLoader' 10 | 11 | import scheme from './appScheme' 12 | import notFoundLoader from './notFoundLoader' 13 | 14 | const appLoader = loadNotFoundBoundary( 15 | loadMatch({ 16 | [patternFor(scheme.top)]: loadAsync(async () => { 17 | const { default: Component } = await import( 18 | '../../../docs/site/index.mdx' 19 | ) 20 | return <DocumentContent Doc={Component} /> 21 | }), 22 | 23 | [patternFor(scheme.concepts)]: conceptLoader, 24 | [patternFor(scheme.examples)]: exampleLoader, 25 | [patternFor(scheme.packages)]: packageLoader, 26 | }), 27 | notFoundLoader, 28 | ) 29 | 30 | export default appLoader 31 | -------------------------------------------------------------------------------- /site/src/app/appScheme.tsx: -------------------------------------------------------------------------------- 1 | import { createScheme, nestScheme } from 'retil-nav-scheme' 2 | 3 | import conceptScheme from './concepts/conceptScheme' 4 | import exampleScheme from './examples/exampleScheme' 5 | import packageScheme from './packages/packageScheme' 6 | 7 | export default createScheme({ 8 | top: () => '/', 9 | 10 | concepts: nestScheme('/concepts', conceptScheme), 11 | examples: nestScheme('/examples', exampleScheme), 12 | packages: nestScheme('/packages', packageScheme), 13 | }) 14 | -------------------------------------------------------------------------------- /site/src/app/concepts/conceptIndexPage.tsx: -------------------------------------------------------------------------------- 1 | import appScheme from 'site/src/app/appScheme' 2 | import { 3 | DocLink, 4 | DocTitle, 5 | DocUnorderedList, 6 | DocWrapper, 7 | } from 'site/src/component/document' 8 | import { ConceptMeta } from 'site/src/data/conceptMeta' 9 | 10 | interface Props { 11 | data: ConceptMeta[] 12 | } 13 | 14 | function Page(props: Props) { 15 | const { data } = props 16 | 17 | return ( 18 | <DocWrapper> 19 | <DocTitle>Concepts</DocTitle> 20 | <DocUnorderedList> 21 | {data.map((conceptModule) => ( 22 | <li key={conceptModule.slug}> 23 | <DocLink href={appScheme.concepts.one(conceptModule)}> 24 | {conceptModule.title} 25 | </DocLink> 26 | </li> 27 | ))} 28 | </DocUnorderedList> 29 | </DocWrapper> 30 | ) 31 | } 32 | 33 | export default Page 34 | -------------------------------------------------------------------------------- /site/src/app/concepts/conceptLoader.tsx: -------------------------------------------------------------------------------- 1 | import { loadAsync } from 'retil-mount' 2 | import { loadMatch } from 'retil-nav' 3 | import { patternFor } from 'retil-nav-scheme' 4 | 5 | import { Env } from 'site/src/env' 6 | import { getConceptContent } from 'site/src/data/conceptContent' 7 | 8 | import scheme from './conceptScheme' 9 | 10 | export default loadMatch({ 11 | [patternFor(scheme.index)]: loadAsync<Env>(async (props) => { 12 | props.head.push(<title>retil - concepts) 13 | const [{ default: data }, { default: Page }] = await Promise.all([ 14 | import('site/src/data/conceptIndex'), 15 | import('./conceptIndexPage'), 16 | ]) 17 | return 18 | }), 19 | 20 | [patternFor(scheme.one)]: loadAsync(async (props) => { 21 | const { mount, head, ...env } = props 22 | const params = env.nav.params 23 | const pageModulePromise = import('./conceptPage') 24 | const content = await getConceptContent(params.slug as string) 25 | 26 | if (!content) { 27 | return env.nav.notFound() 28 | } 29 | 30 | const meta = content.meta 31 | const { default: Page } = await pageModulePromise 32 | 33 | head.push({meta.title} example) 34 | 35 | return 36 | }), 37 | }) 38 | -------------------------------------------------------------------------------- /site/src/app/concepts/conceptPage.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from 'react' 2 | 3 | import { DocumentContent, DocumentFooter } from 'site/src/component/document' 4 | import { ConceptContent } from 'site/src/data/conceptContent' 5 | 6 | const ExampleContext = createContext(undefined as any) 7 | 8 | export interface ConceptPageProps { 9 | content: ConceptContent 10 | } 11 | 12 | function Page({ content }: ConceptPageProps) { 13 | return ( 14 | 15 | 16 | 19 | 20 | ) 21 | } 22 | 23 | const Title = () => <>{useContext(ExampleContext).meta.title} 24 | 25 | const components = { Title } 26 | 27 | export default Page 28 | -------------------------------------------------------------------------------- /site/src/app/concepts/conceptScheme.tsx: -------------------------------------------------------------------------------- 1 | import { createScheme } from 'retil-nav-scheme' 2 | 3 | export type ConceptParams = { 4 | slug: string 5 | } 6 | 7 | export default createScheme({ 8 | index: () => `/`, 9 | one: (params: ConceptParams) => `/${params.slug}`, 10 | }) 11 | -------------------------------------------------------------------------------- /site/src/app/examples/exampleIndexPage.tsx: -------------------------------------------------------------------------------- 1 | import appScheme from 'site/src/app/appScheme' 2 | import { 3 | DocLink, 4 | DocTitle, 5 | DocUnorderedList, 6 | DocWrapper, 7 | } from 'site/src/component/document' 8 | import { ExampleMeta } from 'site/src/data/exampleMeta' 9 | 10 | interface Props { 11 | data: ExampleMeta[] 12 | } 13 | 14 | function Page(props: Props) { 15 | const { data } = props 16 | 17 | return ( 18 | 19 | Examples 20 | 21 | {data.map((exampleModule) => ( 22 |
  • 23 | 24 | {exampleModule.title} 25 | 26 |
  • 27 | ))} 28 |
    29 |
    30 | ) 31 | } 32 | 33 | export default Page 34 | -------------------------------------------------------------------------------- /site/src/app/examples/exampleScheme.tsx: -------------------------------------------------------------------------------- 1 | import { createScheme } from 'retil-nav-scheme' 2 | 3 | export type ExampleParams = { 4 | slug: string 5 | } 6 | 7 | export default createScheme({ 8 | index: () => `/`, 9 | one: (params: ExampleParams) => `/${params.slug}`, 10 | }) 11 | -------------------------------------------------------------------------------- /site/src/app/notFoundLoader.tsx: -------------------------------------------------------------------------------- 1 | function NotFound() { 2 | return

    404 Not Found

    3 | } 4 | 5 | const notFoundLoader = () => 6 | 7 | export default notFoundLoader 8 | -------------------------------------------------------------------------------- /site/src/app/packages/packageIndexPage.tsx: -------------------------------------------------------------------------------- 1 | import appScheme from 'site/src/app/appScheme' 2 | import { 3 | DocLink, 4 | DocTitle, 5 | DocUnorderedList, 6 | DocWrapper, 7 | } from 'site/src/component/document' 8 | import { PackageMeta } from 'site/src/data/packageMeta' 9 | 10 | interface Props { 11 | data: PackageMeta[] 12 | } 13 | 14 | function Page(props: Props) { 15 | const { data } = props 16 | 17 | return ( 18 | 19 | Packages 20 | 21 | {data.map((packageMeta) => ( 22 |
  • 23 | 24 | {packageMeta.packageName} 25 | 26 |
  • 27 | ))} 28 |
    29 |
    30 | ) 31 | } 32 | 33 | export default Page 34 | -------------------------------------------------------------------------------- /site/src/app/packages/packageLoader.tsx: -------------------------------------------------------------------------------- 1 | import { loadAsync } from 'retil-mount' 2 | import { loadMatch } from 'retil-nav' 3 | import { patternFor } from 'retil-nav-scheme' 4 | 5 | import { Env } from 'site/src/env' 6 | import { getPackageContent } from 'site/src/data/packageContent' 7 | 8 | import scheme from './packageScheme' 9 | 10 | export default loadMatch({ 11 | [patternFor(scheme.index)]: loadAsync(async (props) => { 12 | props.head.push(retil - concepts) 13 | const [{ default: data }, { default: Page }] = await Promise.all([ 14 | import('site/src/data/packageIndex'), 15 | import('./packageIndexPage'), 16 | ]) 17 | return 18 | }), 19 | 20 | [patternFor(scheme.one)]: loadAsync(async (props) => { 21 | const { mount, head, ...env } = props 22 | const params = env.nav.params 23 | const pageModulePromise = import('./packagePage') 24 | const content = await getPackageContent(params.packageName as string) 25 | 26 | if (!content) { 27 | return props.nav.notFound() 28 | } 29 | 30 | const meta = content.meta 31 | const { default: PackagePage } = await pageModulePromise 32 | 33 | head.push( 34 | 35 | {meta.title} example – {meta.packageName} 36 | , 37 | ) 38 | 39 | return 40 | }), 41 | }) 42 | -------------------------------------------------------------------------------- /site/src/app/packages/packagePage.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from 'react' 2 | import { LinkSurface } from 'retil-interaction' 3 | 4 | import appScheme from 'site/src/app/appScheme' 5 | import { DocumentContent } from 'site/src/component/document' 6 | import { PackageContent } from 'site/src/data/packageContent' 7 | 8 | const PackageContext = createContext(undefined as any) 9 | 10 | export interface PackagePageProps { 11 | content: PackageContent 12 | } 13 | 14 | function PackagePage({ content }: PackagePageProps) { 15 | return ( 16 | 17 | 18 | 19 | ) 20 | } 21 | 22 | const Title = () => <>{useContext(PackageContext).meta.title} 23 | 24 | const ConceptList = () => ( 25 |
      26 | {useContext(PackageContext).concepts.map((concept) => ( 27 |
    • 28 | 29 | {concept.title} 30 | 31 |
    • 32 | ))} 33 |
    34 | ) 35 | 36 | const ExampleList = () => ( 37 |
      38 | {useContext(PackageContext).examples.map((example) => ( 39 |
    • 40 | 41 | {example.title} 42 | 43 |
    • 44 | ))} 45 |
    46 | ) 47 | 48 | const components = { ConceptList, ExampleList, Title } 49 | 50 | export default PackagePage 51 | -------------------------------------------------------------------------------- /site/src/app/packages/packageScheme.tsx: -------------------------------------------------------------------------------- 1 | import { createScheme } from 'retil-nav-scheme' 2 | 3 | export type PackageParams = { 4 | packageName: string 5 | } 6 | 7 | export default createScheme({ 8 | index: () => `/`, 9 | one: (params: PackageParams) => `/${params.packageName}`, 10 | }) 11 | -------------------------------------------------------------------------------- /site/src/asset/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesknelson/retil/608c59dd61104f1414057f34e2c8a879ffa77a5d/site/src/asset/favicon.ico -------------------------------------------------------------------------------- /site/src/component/codeBlock/index.ts: -------------------------------------------------------------------------------- 1 | export * from './codeBlock' 2 | -------------------------------------------------------------------------------- /site/src/component/document/documentContent.tsx: -------------------------------------------------------------------------------- 1 | import type { MDXComponents } from 'mdx/types' 2 | import type { ComponentType } from 'react' 3 | 4 | import { MDXProvider } from 'mdx-context' 5 | import { useMemo } from 'react' 6 | 7 | import * as styles from './documentStyles' 8 | 9 | export interface DocumentContentProps { 10 | Doc: ComponentType 11 | components?: MDXComponents 12 | } 13 | 14 | const components = { 15 | a: styles.DocLink, 16 | blockquote: styles.DocBlockquote, 17 | code: styles.DocCode, 18 | em: styles.DocEmphasis, 19 | h1: styles.DocTitle, 20 | h2: styles.DocHeading, 21 | h3: styles.DocSubHeading, 22 | h4: styles.DocSubHeading, 23 | h5: styles.DocSubHeading, 24 | h6: styles.DocSubHeading, 25 | hr: styles.DocHorizontalRule, 26 | img: styles.DocImage, 27 | ol: styles.DocOrderedList, 28 | p: styles.DocParagraph, 29 | pre: styles.DocCodeBlock, 30 | strong: styles.DocStrong, 31 | ul: styles.DocUnorderedList, 32 | } as MDXComponents 33 | 34 | export function DocumentContent({ 35 | Doc, 36 | components: componentsProp, 37 | }: DocumentContentProps) { 38 | const mergedComponents = useMemo( 39 | () => ({ 40 | ...components, 41 | ...componentsProp, 42 | }), 43 | [componentsProp], 44 | ) 45 | 46 | return ( 47 | 48 | {/* Note: as of MDX 2.0-rc.2, the wrapper cannot be set as a component, 49 | because it'll cause the document to be unmounted on each render. */} 50 | 51 | 52 | 53 | 54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /site/src/component/document/documentFooter.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react' 2 | 3 | import { colors } from 'site/src/style/colors' 4 | 5 | export interface DocumentFooterProps { 6 | githubEditURL?: string 7 | } 8 | 9 | export function DocumentFooter(props: DocumentFooterProps) { 10 | return ( 11 | 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /site/src/component/document/index.ts: -------------------------------------------------------------------------------- 1 | export * from './documentContent' 2 | export * from './documentFooter' 3 | export * from './documentStyles' 4 | -------------------------------------------------------------------------------- /site/src/component/layout/index.ts: -------------------------------------------------------------------------------- 1 | export * from './layout' 2 | export * from './layoutLoadingFallback' 3 | -------------------------------------------------------------------------------- /site/src/component/layout/layoutLoadingFallback.tsx: -------------------------------------------------------------------------------- 1 | export function LayoutLoadingFallback() { 2 | return
    loading...
    3 | } 4 | -------------------------------------------------------------------------------- /site/src/component/link/index.ts: -------------------------------------------------------------------------------- 1 | export * from './link' 2 | -------------------------------------------------------------------------------- /site/src/component/link/link.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, forwardRef } from 'react' 2 | import { AnchorSurface, LinkSurface } from 'retil-interaction' 3 | import { NavAction, isExternalAction } from 'retil-nav' 4 | 5 | export type LinkProps = Omit & { 6 | children: ReactNode 7 | href: NavAction 8 | } 9 | 10 | export const Link = forwardRef(function Link( 11 | { children, href, ...rest }, 12 | ref, 13 | ) { 14 | return href === 'string' && isExternalAction(href) ? ( 15 | // eslint-disable-next-line jsx-a11y/anchor-has-content 16 | 17 | ) : ( 18 | 19 | ) 20 | }) 21 | -------------------------------------------------------------------------------- /site/src/context/mdx.ts: -------------------------------------------------------------------------------- 1 | import type { MDXComponents } from 'mdx/types' 2 | import type { ReactNode } from 'react' 3 | 4 | import { createContext, createElement, useContext, useMemo } from 'react' 5 | 6 | const MDXContext = createContext({}) 7 | 8 | export function useMDXComponents(components: MDXComponents) { 9 | const contextComponents = useContext(MDXContext) 10 | return useMemo( 11 | () => ({ ...contextComponents, ...components }), 12 | [contextComponents, components], 13 | ) 14 | } 15 | 16 | export interface MDXProviderProps { 17 | components: MDXComponents 18 | children: ReactNode 19 | } 20 | 21 | export function MDXProvider({ 22 | components: componentsProp, 23 | children, 24 | }: MDXProviderProps) { 25 | let components = useMDXComponents(componentsProp) 26 | 27 | return createElement(MDXContext.Provider, { value: components }, children) 28 | } 29 | -------------------------------------------------------------------------------- /site/src/data/README.md: -------------------------------------------------------------------------------- 1 | To get the best loading speeds, data should only be loaded once it's actually 2 | needed. And to facilitate this, we've split the data into following format: 3 | 4 | - `index` files which include a list of available files and their metadata 5 | - `content` files which export a function that'll take a slug, and return 6 | a promise to the full content for the page if it exists – or null if it 7 | doesn't. -------------------------------------------------------------------------------- /site/src/data/conceptContent.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { ConceptMeta, getConceptMeta } from './conceptMeta' 3 | 4 | export interface ConceptContent { 5 | meta: ConceptMeta 6 | Doc: React.ComponentType 7 | } 8 | 9 | export async function getConceptContent( 10 | slug: string, 11 | ): Promise { 12 | const loaders = import.meta.glob('../../../docs/concepts/*/document.mdx') 13 | const key = `../../../docs/concepts/${slug}/document.mdx` 14 | const loader = loaders[key] 15 | 16 | if (!loader) { 17 | return null 18 | } 19 | 20 | const { default: Doc, meta: frontMatter } = await loader() 21 | const meta = getConceptMeta(slug, frontMatter) 22 | 23 | return { Doc, meta } 24 | } 25 | -------------------------------------------------------------------------------- /site/src/data/conceptIndex.ts: -------------------------------------------------------------------------------- 1 | import { extractGlobData } from 'site/src/util/extractGlobData' 2 | 3 | import { getConceptMeta } from './conceptMeta' 4 | 5 | // These two strings should match! The second one must be provided directly as 6 | // a string literal to placate vite, while the first one should match the 7 | // second one so that we're able to create a pattern that correctly extracts 8 | // the package and example names. 9 | // 10 | // prettier-ignore 11 | const glob = 12 | '../../../docs/concepts/*/document.mdx' 13 | const frontMatters = import.meta.frontMatterGlobEager( 14 | '../../../docs/concepts/*/document.mdx', 15 | ) 16 | 17 | const metas = extractGlobData(glob, frontMatters).map( 18 | ({ value, matches: [slug] }) => getConceptMeta(slug, value), 19 | ) 20 | 21 | export default metas 22 | -------------------------------------------------------------------------------- /site/src/data/conceptMeta.ts: -------------------------------------------------------------------------------- 1 | import startCase from 'lodash/startCase' 2 | 3 | export interface ConceptMeta { 4 | blurb?: string 5 | packages?: string[] 6 | slug: string 7 | title: string 8 | } 9 | 10 | export function getConceptMeta( 11 | slug: string, 12 | frontMatter: Record, 13 | ): ConceptMeta { 14 | const metaDefaults = { 15 | slug, 16 | title: startCase(slug), 17 | } 18 | return { 19 | ...metaDefaults, 20 | ...frontMatter, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /site/src/data/exampleDefaultDoc.mdx: -------------------------------------------------------------------------------- 1 | ## 2 | 3 | <Example /> 4 | 5 | <Sources /> -------------------------------------------------------------------------------- /site/src/data/exampleIndex.ts: -------------------------------------------------------------------------------- 1 | import { extractGlobData } from 'site/src/util/extractGlobData' 2 | 3 | import { getExampleMeta } from './exampleMeta' 4 | 5 | // These two strings should match! The second one must be provided directly as 6 | // a string literal to placate vite, while the first one should match the 7 | // second one so that we're able to create a pattern that correctly extracts 8 | // the package and example names. 9 | // 10 | // prettier-ignore 11 | const glob = 12 | '../../../examples/*/example.mdx' 13 | const frontMatters = import.meta.frontMatterGlobEager( 14 | '../../../examples/*/example.mdx', 15 | ) 16 | 17 | const metas = extractGlobData(glob, frontMatters).map( 18 | ({ value, matches: [slug] }) => getExampleMeta(slug, value), 19 | ) 20 | 21 | export default metas 22 | -------------------------------------------------------------------------------- /site/src/data/exampleMeta.ts: -------------------------------------------------------------------------------- 1 | import startCase from 'lodash/startCase' 2 | 3 | export interface ExampleMeta { 4 | description?: string 5 | packages?: string[] 6 | slug: string 7 | title: string 8 | } 9 | 10 | export function getExampleMeta( 11 | slug: string, 12 | frontMatter: Record<string, any>, 13 | ): ExampleMeta { 14 | const metaDefaults = { 15 | slug, 16 | title: startCase(slug), 17 | } 18 | return { 19 | ...metaDefaults, 20 | ...frontMatter, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /site/src/data/packageContent.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { ConceptMeta } from './conceptMeta' 3 | import { ExampleMeta } from './exampleMeta' 4 | import { getPackageMeta, PackageMeta } from './packageMeta' 5 | 6 | export interface PackageContent { 7 | concepts: ConceptMeta[] 8 | examples: ExampleMeta[] 9 | meta: PackageMeta 10 | Doc: React.ComponentType 11 | } 12 | 13 | export async function getPackageContent( 14 | packageName: string, 15 | ): Promise<null | PackageContent> { 16 | const loaders = import.meta.glob('../../../docs/packages/*/document.mdx') 17 | const key = `../../../docs/packages/${packageName}/document.mdx` 18 | const loader = loaders[key] 19 | 20 | if (!loader) { 21 | return null 22 | } 23 | 24 | const [ 25 | { default: Doc, meta: moduleMeta }, 26 | { default: conceptIndex }, 27 | { default: exampleIndex }, 28 | ] = await Promise.all([ 29 | loader(), 30 | import('./conceptIndex'), 31 | import('./exampleIndex'), 32 | ]) 33 | 34 | const meta = getPackageMeta(packageName, moduleMeta) 35 | const concepts = conceptIndex.filter((meta) => 36 | meta.packages?.includes(packageName), 37 | ) 38 | const examples = exampleIndex.filter((meta) => 39 | meta.packages?.includes(packageName), 40 | ) 41 | 42 | return { Doc, concepts, examples, meta } 43 | } 44 | -------------------------------------------------------------------------------- /site/src/data/packageIndex.ts: -------------------------------------------------------------------------------- 1 | import { extractGlobData } from 'site/src/util/extractGlobData' 2 | 3 | import { getPackageMeta } from './packageMeta' 4 | 5 | // These two strings should match! The second one must be provided directly as 6 | // a string literal to placate vite, while the first one should match the 7 | // second one so that we're able to create a pattern that correctly extracts 8 | // the package and example names. 9 | // 10 | // prettier-ignore 11 | const glob = 12 | '../../../docs/packages/*/document.mdx' 13 | const frontMatters = import.meta.frontMatterGlobEager( 14 | '../../../docs/packages/*/document.mdx', 15 | ) 16 | 17 | const metas = extractGlobData(glob, frontMatters) 18 | .map(({ value, matches: [packageName] }) => 19 | getPackageMeta(packageName, value), 20 | ) 21 | .filter((meta) => meta.packageName !== 'site') 22 | 23 | export default metas 24 | -------------------------------------------------------------------------------- /site/src/data/packageMeta.ts: -------------------------------------------------------------------------------- 1 | import startCase from 'lodash/startCase' 2 | 3 | export interface PackageMeta { 4 | description?: string 5 | packageName: string 6 | slug: string 7 | title: string 8 | } 9 | 10 | export function getPackageMeta( 11 | packageName: string, 12 | frontMatter: Record<string, any>, 13 | ): PackageMeta { 14 | const metaDefaults = { 15 | packageName, 16 | slug: packageName, 17 | title: startCase(packageName), 18 | } 19 | return { 20 | ...metaDefaults, 21 | ...frontMatter, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /site/src/entry-client.tsx: -------------------------------------------------------------------------------- 1 | /// <reference types="@retil/tool-vite-plugin-code-as-content/client" /> 2 | /// <reference types="react/next" /> 3 | /// <reference types="react-dom/next" /> 4 | /// <reference types="vite/client" /> 5 | 6 | import createStyleCache from '@emotion/cache' 7 | import { hydrateRoot } from 'react-dom' 8 | import { getDefaultHydrationEnvService } from 'retil-hydration' 9 | import { getDefaultBrowserNavEnvService } from 'retil-nav' 10 | import { fuse } from 'retil-source' 11 | 12 | import { App } from './app/app' 13 | 14 | const styleCache = createStyleCache({ key: 'sskk' }) 15 | const rootNode = document.getElementById('root')! 16 | const [hydrationEnvSource] = getDefaultHydrationEnvService() 17 | const [navEnvSource] = getDefaultBrowserNavEnvService() 18 | 19 | const envSource = fuse((use) => { 20 | const hydrationEnv = use(hydrationEnvSource) 21 | const navEnv = use(navEnvSource) 22 | 23 | return { 24 | ...hydrationEnv, 25 | ...navEnv, 26 | head: [], 27 | } 28 | }) 29 | 30 | hydrateRoot(rootNode, <App env={envSource} styleCache={styleCache} />) 31 | -------------------------------------------------------------------------------- /site/src/env/env.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactElement } from 'react' 2 | import type { HydrationEnv } from 'retil-hydration' 3 | import { useMountEnv } from 'retil-mount' 4 | import type { NavEnv } from 'retil-nav' 5 | 6 | export interface Env extends HydrationEnv, NavEnv { 7 | head: ReactElement[] 8 | } 9 | 10 | export function useEnv(): Env { 11 | return useMountEnv<Env>() 12 | } 13 | -------------------------------------------------------------------------------- /site/src/env/index.ts: -------------------------------------------------------------------------------- 1 | export * from './env' 2 | -------------------------------------------------------------------------------- /site/src/head/head.tsx: -------------------------------------------------------------------------------- 1 | import { cloneElement } from 'react' 2 | import { Helmet, HelmetData, HelmetProvider } from 'react-helmet-async' 3 | 4 | import { useEnv } from 'site/src/env' 5 | 6 | export type HeadSink = { helmet?: HelmetData } 7 | 8 | export interface HeadProps { 9 | sink?: HeadSink 10 | } 11 | 12 | /** 13 | * Renders any elements in the "head" property of the environment context 14 | * to the provided "context" prop, or if not provided, renders to the browser 15 | * document head. 16 | * 17 | * @param props 18 | * @returns 19 | */ 20 | export function Head(props: HeadProps) { 21 | const env = useEnv() 22 | const elements = env.hydrating ? null : env.head 23 | return elements ? ( 24 | <HelmetProvider context={props.sink}> 25 | <Helmet> 26 | <title>retil.tech 27 | {elements.map((item, i) => cloneElement(item, { key: i }))} 28 | 29 | 30 | ) : null 31 | } 32 | 33 | export function renderHeadSinkToString(sink: HeadSink) { 34 | return ` 35 | ${sink.helmet?.title.toString()} 36 | ${sink.helmet?.link.toString()} 37 | ${sink.helmet?.meta.toString()} 38 | ${sink.helmet?.script.toString()} 39 | ${sink.helmet?.style.toString()} 40 | ` 41 | } 42 | 43 | export function createHeadSink(): HeadSink { 44 | return {} 45 | } 46 | -------------------------------------------------------------------------------- /site/src/head/index.ts: -------------------------------------------------------------------------------- 1 | export * from './head' 2 | -------------------------------------------------------------------------------- /site/src/style/dimensions.ts: -------------------------------------------------------------------------------- 1 | export const columnWidth = '700px' 2 | -------------------------------------------------------------------------------- /site/src/style/globalStyle.tsx: -------------------------------------------------------------------------------- 1 | import { Global, css } from '@emotion/react' 2 | 3 | import { colors } from './colors' 4 | 5 | export const GlobalStyle = () => ( 6 | 54 | ) 55 | -------------------------------------------------------------------------------- /site/src/util/extractGlobData.ts: -------------------------------------------------------------------------------- 1 | import escapeRegExp from 'lodash/escapeRegExp' 2 | 3 | export function extractGlobData( 4 | glob: string, 5 | values: Record, 6 | ): { value: Value; matches: string[] }[] { 7 | const pattern = new RegExp( 8 | '^' + glob.split(/\*/g).map(escapeRegExp).join('([\\w-]+)') + '$', 9 | ) 10 | return Object.keys(values).map((key) => { 11 | const matchResult = key.match(pattern)! 12 | return { value: values[key], matches: matchResult!.slice(1) } 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /site/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "skipLibCheck": false, 5 | "esModuleInterop": true, 6 | "allowSyntheticDefaultImports": true, 7 | "isolatedModules": true, 8 | "jsx": "react-jsx", 9 | "target": "ES2020", 10 | "module": "ES2020", 11 | "jsxImportSource": "@emotion/react", 12 | "types": ["@emotion/react"], 13 | "noEmit": true, 14 | }, 15 | "include": ["src/@types/**.ts", "./src"] 16 | } 17 | -------------------------------------------------------------------------------- /site/vite.config.ts: -------------------------------------------------------------------------------- 1 | import alias from '@rollup/plugin-alias' 2 | import react from '@vitejs/plugin-react' 3 | import { resolve } from 'path' 4 | import { defineConfig } from 'vite' 5 | import tsconfigPaths from 'vite-tsconfig-paths' 6 | 7 | import { getCodeAsContentPlugins } from '@retil/tool-vite-plugin-code-as-content' 8 | import emotionPlugin from '@retil/tool-vite-plugin-emotion' 9 | import styledComponentsPlugin from '@retil/tool-vite-plugin-styled-components' 10 | 11 | const projectRootDir = resolve(__dirname) 12 | 13 | // https://vitejs.dev/config/ 14 | export default defineConfig(async ({ mode }) => ({ 15 | define: { 16 | 'process.env.NODE_ENV': JSON.stringify(mode), 17 | }, 18 | server: { 19 | host: '*', 20 | port: 9001, 21 | }, 22 | plugins: [ 23 | mode !== 'production' && 24 | tsconfigPaths({ 25 | root: resolve(__dirname, '..'), 26 | projects: ['.'], 27 | }), 28 | alias({ 29 | entries: [ 30 | { find: 'site/src', replacement: resolve(projectRootDir, 'src') }, 31 | ], 32 | }) as any, 33 | react({ 34 | jsxImportSource: '@emotion/react', 35 | }), 36 | emotionPlugin(), 37 | styledComponentsPlugin(), 38 | await getCodeAsContentPlugins({ 39 | jsxImportSource: '@emotion/react', 40 | providerImportSource: 'mdx-context', 41 | }), 42 | ], 43 | optimizeDeps: { 44 | include: ['hoist-non-react-statics', 'react-is'], 45 | }, 46 | resolve: { 47 | dedupe: ['react', 'react-dom', 'react-is'], 48 | }, 49 | ssr: { 50 | external: [require.resolve('styled-components')], 51 | }, 52 | })) 53 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": false, 4 | "esModuleInterop": true, 5 | "forceConsistentCasingInFileNames": true, 6 | "incremental": true, 7 | "module": "esnext", 8 | "moduleResolution": "node", 9 | "jsx": "react", 10 | "baseUrl": "src", 11 | "importHelpers": true, 12 | "removeComments": false, 13 | "resolveJsonModule": true, 14 | "sourceMap": false, 15 | "strict": true, 16 | "strictPropertyInitialization": false, 17 | "target": "es6", 18 | "lib": [ 19 | "dom", 20 | "dom.iterable", 21 | "esnext" 22 | ] 23 | }, 24 | } 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.build.json", 3 | 4 | "compilerOptions": { 5 | "sourceMap": true, 6 | "baseUrl": ".", 7 | "paths": { 8 | "retil": ["packages/retil/src"], 9 | "retil-boundary": ["packages/retil-boundary/src"], 10 | "retil-css": ["packages/retil-css/src"], 11 | "retil-hydration": ["packages/retil-hydration/src"], 12 | "retil-interaction": ["packages/retil-interaction/src"], 13 | "retil-issues": ["packages/retil-issues/src"], 14 | "retil-link": ["packages/retil-link/src"], 15 | "retil-media": ["packages/retil-media/src"], 16 | "retil-mount": ["packages/retil-mount/src"], 17 | "retil-nav": ["packages/retil-nav/src"], 18 | "retil-nav-scheme": ["packages/retil-nav-scheme/src"], 19 | "retil-operation": ["packages/retil-operation/src"], 20 | "retil-source": ["packages/retil-source/src"], 21 | "retil-support": ["packages/retil-support/src"], 22 | "retil-transition": ["packages/retil-transition/src"], 23 | "site": ["site/src"] 24 | } 25 | } 26 | } --------------------------------------------------------------------------------