diff --git a/package-lock.json b/package-lock.json index e9648912..47556103 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "state-in-url", - "version": "4.1.2", + "version": "4.1.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "state-in-url", - "version": "4.1.2", + "version": "4.1.5", "license": "MIT", "workspaces": [ "packages/urlstate", @@ -2702,6 +2702,59 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz", + "integrity": "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.8" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.12", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.12.tgz", + "integrity": "sha512-NP83c0HjokcGVEMeoStg317VD9W7eDlGK7457dMBANbKA6GJZdc7rjujdgqzTaz93jkGgc5P/jeWbaCHnMNc+w==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.8" + } + }, + "node_modules/@floating-ui/react": { + "version": "0.26.28", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz", + "integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.2", + "@floating-ui/utils": "^0.2.8", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", + "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz", + "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==", + "license": "MIT" + }, "node_modules/@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", @@ -28309,6 +28362,12 @@ "node": ">=12.20" } }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "license": "MIT" + }, "node_modules/tailwindcss": { "version": "3.4.14", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.14.tgz", @@ -30959,6 +31018,7 @@ "packages/example-nextjs14": { "version": "0.1.0", "dependencies": { + "@floating-ui/react": "^0.26.28", "@next/third-parties": "^15.0.2", "@shikijs/transformers": "^1.12.1", "@vercel/analytics": "^1.3.1", diff --git a/packages/example-nextjs14/package.json b/packages/example-nextjs14/package.json index b9276dc2..c4765f0f 100644 --- a/packages/example-nextjs14/package.json +++ b/packages/example-nextjs14/package.json @@ -10,6 +10,7 @@ "lint": "next lint" }, "dependencies": { + "@floating-ui/react": "^0.26.28", "@next/third-parties": "^15.0.2", "@shikijs/transformers": "^1.12.1", "@vercel/analytics": "^1.3.1", diff --git a/packages/example-nextjs14/src/app/components/CodeBlockState.tsx b/packages/example-nextjs14/src/app/(demo)/CodeBlockState.tsx similarity index 90% rename from packages/example-nextjs14/src/app/components/CodeBlockState.tsx rename to packages/example-nextjs14/src/app/(demo)/CodeBlockState.tsx index ebab6030..b39c5c2e 100644 --- a/packages/example-nextjs14/src/app/components/CodeBlockState.tsx +++ b/packages/example-nextjs14/src/app/(demo)/CodeBlockState.tsx @@ -1,4 +1,4 @@ -import { File } from './File'; +import { File } from '../components/File'; export const CodeBlockState = () => { return ( diff --git a/packages/example-nextjs14/src/app/components/CodeBlocksNext.tsx b/packages/example-nextjs14/src/app/(demo)/CodeBlocksNext.tsx similarity index 94% rename from packages/example-nextjs14/src/app/components/CodeBlocksNext.tsx rename to packages/example-nextjs14/src/app/(demo)/CodeBlocksNext.tsx index dd12b439..b7e27467 100644 --- a/packages/example-nextjs14/src/app/components/CodeBlocksNext.tsx +++ b/packages/example-nextjs14/src/app/(demo)/CodeBlocksNext.tsx @@ -1,5 +1,8 @@ -import { File } from './File'; +'use client'; + +import { File } from '../components/File'; import { CodeBlockState } from './CodeBlockState'; +import { tooltips } from './tooltips'; export const CodeBlocks = () => { return ( @@ -15,6 +18,7 @@ export const CodeBlocks = () => { { // [!code word:urlState] return
name: {urlState.name}
};`} + matchers={tooltips} />
3. Can create self-sufficient hook to manage slice of some state.
{// }, [setUrlBase]); return { urlState, setUrl, resetUrl: reset }; -};`}/> +};`} /> ); }; diff --git a/packages/example-nextjs14/src/app/(demo)/page.tsx b/packages/example-nextjs14/src/app/(demo)/page.tsx index 970c8b0c..697f01df 100644 --- a/packages/example-nextjs14/src/app/(demo)/page.tsx +++ b/packages/example-nextjs14/src/app/(demo)/page.tsx @@ -3,9 +3,10 @@ import React from 'react'; import { DemoPart } from '../DemoPart'; import { Description } from '../components/Description'; +import { Tabs } from '../components/Tabs'; const CodeBlocks = dynamicImport( - () => import('../components/CodeBlocksNext') + () => import('./CodeBlocksNext') .then((mod) => mod.CodeBlocks), { loading: () =>
, @@ -16,9 +17,10 @@ export default async function Home({ searchParams }: { searchParams: object }) { return ( <> + + +
- {/* TODO:Shiki is a little shitty https://github.com/vercel/next.js/issues/64434 - try https://codehike.org/docs/code/twoslash */}
diff --git a/packages/example-nextjs14/src/app/components/CodeBlocksRR.tsx b/packages/example-nextjs14/src/app/(demo)/react-router/CodeBlocksRR.tsx similarity index 92% rename from packages/example-nextjs14/src/app/components/CodeBlocksRR.tsx rename to packages/example-nextjs14/src/app/(demo)/react-router/CodeBlocksRR.tsx index d597cb30..552459d6 100644 --- a/packages/example-nextjs14/src/app/components/CodeBlocksRR.tsx +++ b/packages/example-nextjs14/src/app/(demo)/react-router/CodeBlocksRR.tsx @@ -1,5 +1,8 @@ -import { File } from './File'; -import { CodeBlockState } from './CodeBlockState'; +'use client'; + +import { File } from '../../components/File'; +import { CodeBlockState } from '../CodeBlockState'; +import { tooltips } from '../tooltips'; export const CodeBlocksRR = () => { return ( @@ -15,6 +18,7 @@ export const CodeBlocksRR = () => { { /> { import('../../components/CodeBlocksRR').then((mod) => mod.CodeBlocksRR), + () => import('./CodeBlocksRR').then((mod) => mod.CodeBlocksRR), { loading: () =>
, }, @@ -16,6 +17,8 @@ export default async function Home({ searchParams }: { searchParams: object }) { <> + +
diff --git a/packages/example-nextjs14/src/app/(demo)/template.tsx b/packages/example-nextjs14/src/app/(demo)/template.tsx index 5f55e5be..9f29c781 100644 --- a/packages/example-nextjs14/src/app/(demo)/template.tsx +++ b/packages/example-nextjs14/src/app/(demo)/template.tsx @@ -3,7 +3,6 @@ import React from 'react'; import 'shared/styles.css'; -import { Tabs } from '../components/Tabs'; import { Logo } from '../components/Logo'; const Footer = dynamic( @@ -18,7 +17,7 @@ export default async function Template({ }) { return (
-
+
@@ -26,7 +25,7 @@ export default async function Template({

State in url

-

State management and deep links

+

State management and URL sync

@@ -36,8 +35,6 @@ export default async function Template({
- - {children}
@@ -45,8 +42,3 @@ export default async function Template({
); } - -const TABS = [ - { text: "Next.js", url: '/' }, - { text: "react-router", url: '/react-router' }, -] diff --git a/packages/example-nextjs14/src/app/(demo)/tooltips.ts b/packages/example-nextjs14/src/app/(demo)/tooltips.ts new file mode 100644 index 00000000..2a56aae9 --- /dev/null +++ b/packages/example-nextjs14/src/app/(demo)/tooltips.ts @@ -0,0 +1,138 @@ +import { type Tooltip, type Matcher } from "../types" + +const formTooltip: Tooltip[] = [{ + text: `(alias) const form: Form\n +import form`, lang: 'tsx' +}] + +const urlStateTooltip: Tooltip[] = [ + { + text: `const urlState: Form\n\n`, + lang: 'tsx' + }, + { + text: `State object. Don't mutate directly, use setState or setUrl`, + lang: 'markdown' + } +] + +const descTooltip: Tooltip = { + text: ` +* JSDoc description, examples, and link to Docs + + ... + ... +* Docs @link +`, lang: 'markdown' +} + +const useUrlStateTooltipImp: Tooltip[] = [ + { + text: `(alias) function useUrlState({ defaultState: T, searchParams, replace, scroll, useHistory, }: OldParams): { + urlState: T; + setState: (value: Partial | ((currState: T) => T)) => void; + setUrl: (value?: Partial | ((currState: T) => T)) => void; +} (+1 overload) +import useUrlState`, + lang: 'tsx' + }, + descTooltip +] + +const useUrlStateTooltip: Tooltip[] = [ + { + text: ` +(alias) useUrlState
(defaultState: Form, params?: Params): { + urlState: Form; + setState: (value: Partial | ((currState: Form) => Form)) => void; + setUrl: (value?: Partial<...> | ... 1 more ... | undefined, options?: Options) => void; + reset: (options?: Options & { + [key: string]: unknown; + }) => void; +} (+1 overload) +import useUrlState`, lang: 'tsx' + }, + + descTooltip +] + +const setStateTooltip: Tooltip[] = [{ text: `const setState: (value: Partial | ((currState: Form) => Form)) => void`, lang: 'tsx' }, descTooltip] + +const setUrlTooltip: Tooltip[] = [{ text: `const setUrl: (value?: Partial | ((currState: Form) => Form) | undefined, options?: Options) => void`, lang: 'tsx' }, descTooltip] + +const resetTooltip: Tooltip[] = [{ + text: `const reset: (options?: Options & { + [key: string]: unknown; +}) => void`, lang: 'tsx' +}, descTooltip] + +const formParamTooltip: Tooltip[] = [{ text: `(parameter) curr: Form`, lang: 'tsx' }] + +export const tooltips: Matcher[] = [ + ['useUrlState}', useUrlStateTooltipImp], + ['form}', formTooltip], + ['form);', formTooltip], + ['form, {', formTooltip], + ['useUrlState(', useUrlStateTooltip], + [ + 'urlState,', urlStateTooltip + ], + [ + 'urlState}', urlStateTooltip + ], + [ + '{urlState.name', urlStateTooltip + ], + [ + 'urlState.', urlStateTooltip + ], + [ + 'name}', [ + { text: '(property) name: string', lang: 'tsx' } + ] + ], + [ + 'name}', formParamTooltip], + ['curr, name:', formParamTooltip], + [ + 'setUrlBase,', setUrlTooltip + ], + [ + 'setUrlBase(', setUrlTooltip + ], + [ + 'reset}', resetTooltip + ] + +] diff --git a/packages/example-nextjs14/src/app/components/Code.tsx b/packages/example-nextjs14/src/app/components/Code.tsx new file mode 100644 index 00000000..9713ccb1 --- /dev/null +++ b/packages/example-nextjs14/src/app/components/Code.tsx @@ -0,0 +1,18 @@ +'use client' +import React from 'react'; + +import { highlight } from '../highlighter'; +import { Langs } from '../types'; + +export const Code = ({ content, lang, ...rest }: { content: string, lang?: Langs } & React.ComponentPropsWithoutRef<'div'>) => { + + const [text, setText] = React.useState(''); + + React.useEffect(() => { + highlight(content, { lang }).then(setText) + }, [content, lang]) + + return text ? +
+ :
{content.split('\n').map((el, ind) => (
{el || '_'}
))}
+} diff --git a/packages/example-nextjs14/src/app/components/File.tsx b/packages/example-nextjs14/src/app/components/File.tsx index 8f6fb783..546ce661 100644 --- a/packages/example-nextjs14/src/app/components/File.tsx +++ b/packages/example-nextjs14/src/app/components/File.tsx @@ -1,17 +1,57 @@ -import { highlight } from '../highlighter'; +'use client' +import React from 'react'; -export const File = async ({ +import { useFloating, autoUpdate, useClientPoint, offset } from '@floating-ui/react'; + +import { Code } from './Code'; +import { type Matcher, type Tooltip } from '../types'; + +export const File = ({ name, content, + matchers }: { name: string; content: string; + matchers?: Matcher[] }) => { - const text = await highlight(content); + const [tooltip, setTooltip] = React.useState<{ x: number, y: number, nodes: Tooltip[] }>({ nodes: [], x: 0, y: 0 }); + + const { refs, floatingStyles, context } = useFloating({ + whileElementsMounted: autoUpdate, + placement: 'top', + middleware: [ + offset({ mainAxis: 20, crossAxis: 50 }) + ] + }); + useClientPoint(context, + { x: tooltip.x, y: tooltip.y } + ) + + const matchTooltips = (ev: React.MouseEvent) => { + // @ts-expect-error fots + const text = (ev?.target?.textContent || '').trim(); + // @ts-expect-error fots + const next = (ev?.target?.nextSibling?.textContent || '').trim() + + // if (text?.length < 12) { + // console.log(`${text}${next}`, { ev, context }) + // } + + const match = matchers?.find(el => el[0] === `${text}${next}`) + + if (match) { + if (match[1] !== tooltip.nodes) { + setTooltip({ nodes: match[1], x: ev.clientX, y: ev.clientY }) + } + } else if (tooltip.nodes.length) { + setTooltip(curr => ({ ...curr, nodes: [] })) + } + } return (
@@ -27,12 +67,22 @@ export const File = async ({
- -

-      
+ {tooltip.nodes.length ?
+ {tooltip.nodes.map((node, ind) => ( + + ))} +
: null} + +
+ + + +
+
); }; diff --git a/packages/example-nextjs14/src/app/components/Tabs.tsx b/packages/example-nextjs14/src/app/components/Tabs.tsx index 888f477e..8be8d016 100644 --- a/packages/example-nextjs14/src/app/components/Tabs.tsx +++ b/packages/example-nextjs14/src/app/components/Tabs.tsx @@ -7,24 +7,24 @@ import { usePathname, useRouter } from 'next/navigation' const scroll = false; -export const Tabs = ({ entries, className = '' }: { entries: { text: string, url: string }[], className: string }) => { +export const Tabs = ({ className = '' }: { className: string }) => { const pathname = usePathname(); const router = useRouter(); return ( -