├── .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 | #
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 |
34 | ```
35 |
--------------------------------------------------------------------------------
/docs/concepts/loaders/document.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Loaders
3 | packages:
4 | - retil-mount
5 | - retil-nav
6 | ---
7 |
8 | #
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 | #
--------------------------------------------------------------------------------
/docs/packages/retil-mount/document.mdx:
--------------------------------------------------------------------------------
1 | #
2 |
3 | Load and mount content.
4 |
5 | ## Concepts
6 |
7 |
8 |
9 | ## Examples
10 |
11 |
--------------------------------------------------------------------------------
/docs/packages/retil-nav/document.mdx:
--------------------------------------------------------------------------------
1 | #
2 |
3 | Navigation how it's meant to be done.
4 |
5 | ## Examples
6 |
7 |
--------------------------------------------------------------------------------
/docs/site/index.mdx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/react'
2 |
3 | retil
10 |
11 |
15 |
16 | **retil gives you superpowers**
17 |
18 |
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 | #
14 |
15 |
16 |
17 | ## app.tsx
18 |
19 |
20 |
21 | ## main.tsx
22 |
23 |
--------------------------------------------------------------------------------
/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( )
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( ))
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) {
9 | switch (nav.pathname) {
10 | case nav.matchname:
11 | return Welcome!
12 |
13 | case nav.matchname + '/about':
14 | return About
15 |
16 | default:
17 | return Not Found
18 | }
19 | }
20 |
21 | export interface AppProps {
22 | env: AppEnv | Source
23 | }
24 |
25 | export function App({ env }: AppProps) {
26 | return (
27 |
28 |
29 | Home
30 | ·
31 | About
32 | ·
33 | Not Found
34 |
35 |
36 |
37 | )
38 | }
39 |
40 | const Content = () => useMountContent()
41 |
42 | const Link = ({ to, children }: { to: string; children: React.ReactNode }) => {
43 | const linkProps = useNavLinkProps(to)
44 | const match = useNavMatcher()
45 | return (
46 |
47 | {children}
48 |
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 | #
9 |
10 |
11 |
12 | ## app.tsx
13 |
14 |
15 |
16 | ## main.tsx
17 |
18 |
--------------------------------------------------------------------------------
/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( )
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( ))
30 | } finally {
31 | mount.seal()
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/examples/buttonSurface/example.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title:
3 | packages:
4 | - retil-interaction
5 | ---
6 |
7 | #
8 |
9 |
10 |
11 |
12 | ## app.tsx
13 |
14 |
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 & {
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( )
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 | Basic usage
50 |
51 | Button
52 |
53 | Hover on
54 |
55 | Button
56 |
57 | Hover off
58 |
59 | Button
60 |
61 | >
62 | )
63 | }
64 |
65 | export default App
66 |
--------------------------------------------------------------------------------
/examples/connectSurfaceSelectors-styled/example.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: (styled components)
3 | packages:
4 | - retil-css
5 | - retil-interaction
6 | ---
7 |
8 | #
9 |
10 |
11 |
12 |
13 | ## app.tsx
14 |
15 |
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 | #
8 |
9 |
10 |
11 |
12 | ## app.tsx
13 |
14 |
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 | #
8 |
9 |
10 |
11 |
12 | ## app.tsx
13 |
14 |
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 {
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, '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) => {
13 | if (onChange) {
14 | onChange(event.target.value)
15 | }
16 | },
17 | [onChange],
18 | )
19 |
20 | return
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 | #
10 |
11 |
12 |
13 |
14 | ## app.tsx
15 |
16 |
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 | #
9 |
10 |
11 |
12 |
13 | ## app.tsx
14 |
15 |
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 | '/': Welcome! ,
13 | '/about': About ,
14 | }),
15 | (env) => ,
16 | )
17 |
18 | const NavLinkBody = (props: any) => {
19 | return (
20 |
26 | {props.children}
27 |
28 | )
29 | }
30 |
31 | function App() {
32 | const [navSource] = getDefaultBrowserNavEnvService()
33 |
34 | return (
35 |
36 |
37 |
38 | Home
39 |
40 | ·
41 |
42 | About
43 |
44 | ·
45 |
46 | Not Found
47 |
48 |
49 |
50 |
51 | )
52 | }
53 |
54 | function NotFound({ pathname }: { pathname: string }) {
55 | return 404 Not Found - {pathname}
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 | #
8 |
9 |
10 |
11 |
12 | ## app.tsx
13 |
14 |
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 | #
8 |
9 |
10 |
11 |
12 | ## app.tsx
13 |
14 |
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 | #
9 |
10 |
11 |
12 |
13 | ## app.tsx
14 |
15 |
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 | #
8 |
9 |
10 |
11 |
12 | ## app.tsx
13 |
14 |
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 |
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 | #
8 |
9 |
10 |
11 |
12 | ## app.tsx
13 |
14 |
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 | #
9 |
10 |
11 |
12 |
13 | ## app.tsx
14 |
15 |
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 | #
9 |
10 |
11 |
12 |
13 | ## app.tsx
14 |
15 |
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 | #
9 |
10 |
11 |
12 |
13 | ## app.tsx
14 |
15 |
16 |
17 | ## main.tsx
18 |
19 |
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( )
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( ))
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 | Large
12 | ))}
13 | {renderWhenMedium((hideCSS) => (
14 | Medium
15 | ))}
16 | {renderWhenSmall((hideCSS) => (
17 | Small
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 | #
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 |
17 |
18 |
19 | ## app.tsx
20 |
21 |
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 | Large
17 | ))}
18 | {renderWhenMedium((x) => (
19 | Medium
20 | ))}
21 | {renderWhenSmall((x) => (
22 | Small
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 | #
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 |
17 |
18 |
19 | ## app.tsx
20 |
21 |
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: '/../../',
9 | }),
10 | modulePaths: ['/src/'],
11 | modulePathIgnorePatterns: ['/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 ",
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( )
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: '/../../',
9 | }),
10 | modulePaths: ['/src/'],
11 | modulePathIgnorePatterns: ['/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 ",
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 =
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
26 | (...args: Array): any
27 | }
28 |
29 | // Equivalent to the CSSObject type expected by styled-components and emotion.
30 | export type CSSProperties = CSS.Properties
31 | export type CSSPropertiesWithMultiValues = {
32 | [K in keyof CSSProperties]:
33 | | CSSProperties[K]
34 | | Array>
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: '/../../',
9 | }),
10 | modulePaths: ['/src/'],
11 | modulePathIgnorePatterns: ['/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 ",
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
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( )
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: '/../../',
9 | }),
10 | modulePaths: ['/src/'],
11 | modulePathIgnorePatterns: ['/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 ",
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 | >(
8 | mergeProps?: TMergeProps & TMergeableProps & Record,
9 | ): Omit & 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 = 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,
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 | 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 |
16 |
17 |
18 | trigger
19 |
20 |
21 | popup
22 |
23 |
24 | ,
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((props, ref) => (
17 |
27 | ))
28 |
29 | test('add pseudoselector styles when appropriate', () => {
30 | function Test() {
31 | const [, mergeProps, provide] = useSurfaceSelectorsConnector()
32 |
33 | return provide(
34 |
35 |
36 | ,
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 |
44 |
45 | ,
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: '/../../',
9 | }),
10 | modulePaths: ['/src/'],
11 | modulePathIgnorePatterns: ['/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 ",
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['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['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>>>,
40 | fallbackMessages: Partial> = {},
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 {
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,
17 | >(
18 | issues: AddIssuesFunction,
19 | validator: AsyncValidator,
20 | options: UseAsyncValidatorOptions = {},
21 | ): readonly [trigger: () => Promise, 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: '/../../',
9 | }),
10 | modulePaths: ['/src/'],
11 | modulePathIgnorePatterns: ['/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 ",
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: '/../../',
9 | }),
10 | modulePaths: ['/src/'],
11 | modulePathIgnorePatterns: ['/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 ",
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[] = []
7 |
8 | add(promise: PromiseLike): 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 {
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(
5 | load: () => PromiseLike<{ default: Loader }>,
6 | ): Loader {
7 | let loader: Loader | 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
9 | extends UseMountSourceOptions {
10 | children: ReactNode
11 | loader: Loader
12 | env: CastableToEnvSource
13 | }
14 |
15 | export function Mount(
16 | props: MountProps,
17 | ) {
18 | const { children, loader, env } = props
19 | const mount = useMount(loader, env)
20 | return {children}
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(
7 | loader: Loader,
8 | env: CastableToEnvSource,
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 | Source | Fusor
7 |
8 | export type LoaderProps = Env & {
9 | mount: MountSnapshot
10 | }
11 |
12 | export type Loader = (
13 | props: LoaderProps,
14 | ) => Content
15 |
16 | export interface MountContentRef {
17 | readonly current?: Content
18 | }
19 |
20 | export interface MountSnapshot {
21 | dependencies: DependencyList
22 | env: Env
23 | contentRef: MountContentRef
24 | signal: AbortSignal
25 | }
26 |
27 | export interface MountSnapshotWithContent<
28 | Env extends object,
29 | Content = ReactNode,
30 | > extends MountSnapshot {
31 | contentRef: { readonly current: Content }
32 | }
33 |
34 | export type MountSource = Source<
35 | MountSnapshotWithContent
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
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 {
14 | loader: Loader
15 | env: CastableToEnvSource
16 | source: MountSource
17 |
18 | constructor(loader: Loader, env: CastableToEnvSource) {
19 | this.loader = loader
20 | this.env = env
21 | }
22 |
23 | preload(): Promise> {
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 |
45 | {element}
46 |
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
7 | env: CastableToEnvSource
8 | source: MountSource
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 = (
12 | loader: Loader,
13 | env: CastableToEnvSource,
14 | options?: UseMountSourceOptions,
15 | ): UseMountState => {
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 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: '/../../',
9 | }),
10 | modulePaths: ['/src/'],
11 | modulePathIgnorePatterns: ['/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 ",
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: '/../../',
9 | }),
10 | modulePaths: ['/src/'],
11 | modulePathIgnorePatterns: ['/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 ",
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 | TContent
13 | }
14 |
15 | export function loadMatch(
16 | handlers: LoadMatchOptions,
17 | ): Loader {
18 | const tests: [Matcher, Loader][] = []
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)
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(
8 | to: NavAction | ((env: NavEnv) => NavAction),
9 | status = 302,
10 | ): Loader {
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( ))
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 |
21 |
22 |
23 |
24 | ,
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: '/../../',
9 | }),
10 | modulePaths: ['/src/'],
11 | modulePathIgnorePatterns: ['/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 ",
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: '/../../',
9 | }),
10 | modulePaths: ['/src/'],
11 | modulePathIgnorePatterns: ['/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 ",
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(
12 | source: Source,
13 | predicate: (value: T) => boolean,
14 | ): Source {
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(promise: PromiseLike): Source {
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 | (callback: (...args: V) => U, args: V): U
10 | (callback: () => U): U
11 | }
12 | export type FusorUse = (
13 | source: Source,
14 | ...defaultValues: Maybe
15 | ) => U | V
16 | export type Fusor = (
17 | use: FusorUse,
18 | act: FusorAct,
19 | memo: FusorMemo,
20 | ) => T | FuseAct
21 |
22 | export type VectorFusorUse = (
23 | source: Source,
24 | ...defaultValues: Maybe
25 | ) => U[] | V[]
26 | export type VectorFusor = (
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(
10 | source: Source,
11 | callback: (value: T) => U,
12 | ): Source {
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,
14 | callback: (value: SourceSelection[]) => MappedValue[],
15 | ): Source {
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 = (
5 | previousResult: U[],
6 | nextValue: T[],
7 | ) => U[]
8 |
9 | export function reduceVector(
10 | source: Source,
11 | callback: ReduceVectorCallback,
12 | initial: U[] = [],
13 | ): Source {
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(
11 | [core, parentSelect, act]: Source,
12 | selector: (value: T) => U,
13 | ): Source {
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 {
5 | (state: T): void
6 | (updater: (state: T) => T): void
7 | }
8 |
9 | const defaultIsEqual = (x: T, y: T) => x === y
10 |
11 | export function createState(
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, StateController, 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 = UseSourceOptions
7 |
8 | export interface UseServiceFunction {
9 | (
10 | service: Service,
11 | options?: UseSourceOptions,
12 | ): readonly [T | U, Controller]
13 | }
14 |
15 | export type Service = readonly [
16 | source: Source,
17 | controller: Controller,
18 | ]
19 |
20 | export const useService: UseServiceFunction = (
21 | [source, controller]: Service,
22 | options: UseServiceOptions = {},
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 | ///
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 {
15 | defaultValue?: U
16 | }
17 |
18 | export interface UseMaybeSourceOptions extends UseSourceOptions {
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 | (source: Source, options?: UseSourceOptions): T | U
26 | (maybeSource: null, options: UseMaybeSourceOptions): U
27 | (
28 | maybeSource: Source | null,
29 | options: UseMaybeSourceOptions,
30 | ): T | U | null
31 | }
32 |
33 | export const useSource: UseSourceFunction = (
34 | maybeSource: Source | null,
35 | options: UseSourceOptions = {},
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 = (
4 | source: Source,
5 | maybePredicate?: (value: T) => boolean,
6 | ): void | Promise => {
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()
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(source: Source): T[]
10 | export function sendToArray(source: Source, sealWith: U): (T | U)[]
11 | export function sendToArray(source: Source, 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(source: Source): T[][]
33 | export function sendVectorToArray(
34 | source: Source,
35 | sealWith: U,
36 | ): (T[] | U)[]
37 | export function sendVectorToArray(
38 | source: Source,
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 ",
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 {
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) => 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((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 {
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(
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 =
4 | | undefined
5 | | false
6 | | null
7 | | T
8 | | (undefined | false | null | CastableToTruthyArrayOf)[]
9 |
10 | export function ensureTruthyArray(
11 | maybeArray: CastableToTruthyArrayOf,
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(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 = (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 {
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(
8 | x: EventHandler,
9 | y?: EventHandler,
10 | ...zs: (EventHandler | undefined)[]
11 | ): EventHandler
12 | export function joinEventHandlers(
13 | x: EventHandler | undefined,
14 | y: EventHandler,
15 | ...zs: (EventHandler | undefined)[]
16 | ): EventHandler
17 | export function joinEventHandlers(
18 | x?: EventHandler,
19 | y?: EventHandler,
20 | ...zs: (EventHandler | undefined)[]
21 | ): EventHandler | undefined
22 | export function joinEventHandlers(
23 | x?: EventHandler,
24 | y?: EventHandler,
25 | ...zs: (EventHandler | undefined)[]
26 | ): EventHandler | 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] | []
2 |
3 | export const hasValue = (maybe: Maybe): 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 = {
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 = (
6 | initialConfig: Config,
7 | ) => readonly [reconfigure: (nextConfig: Config) => void, value: Value]
8 |
9 | export function useConfigurator(
10 | configurator: Configurator,
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(
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(): (x: T) => T {
9 | return useMemo(
10 | () => memoizeOne(identity, ([x], [y]) => areShallowEqual(x, y)),
11 | [],
12 | )
13 | }
14 |
15 | export function useMemoizeOneValue(): (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 ",
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
5 | from?: Record
6 | enter?: ControllerUpdate>
7 | exit?: ControllerUpdate>
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
5 | hide: () => Promise
6 | }
7 |
8 | export type TransitionHandleRef = Ref
9 |
10 | export function useTransitionHandleRef(): RefObject