@usequeek/theme-kit 0.1.9 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -1
- package/client.ts +1 -0
- package/navigation.tsx +58 -0
- package/package.json +1 -1
- package/tests/navigation.test.tsx +30 -0
package/README.md
CHANGED
|
@@ -128,10 +128,25 @@ export default function Footer() {
|
|
|
128
128
|
the seam for a possible future plan perk. There is no merchant-facing
|
|
129
129
|
toggle for it in v1 — always render it visible.
|
|
130
130
|
|
|
131
|
+
## Links and navigation
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
import { Link, useRouter, usePathname } from '@usequeek/theme-kit/navigation';
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
A theme links and navigates through these, never through `next/link` or
|
|
138
|
+
`next/navigation`. Which framework runs the storefront is Queek's decision: the
|
|
139
|
+
kit is the one place that knows it, so a change there is a kit release, not an
|
|
140
|
+
update to every theme. `Link` is an `<a>` that moves inside the store without a
|
|
141
|
+
full page load (`href`, `prefetch`, `replace`, plus any anchor attribute);
|
|
142
|
+
`useRouter()` gives `push`, `replace`, `back` and `refresh`; `usePathname()`
|
|
143
|
+
returns the current path. The theme check rejects a theme that imports `next`.
|
|
144
|
+
|
|
131
145
|
## Boundary
|
|
132
146
|
|
|
133
147
|
The kit must never import the storefront app, its themes, or `lib/storefront`.
|
|
134
|
-
That one-way rule is what makes this package extractable at all.
|
|
148
|
+
That one-way rule is what makes this package extractable at all. The reverse
|
|
149
|
+
also holds for the framework: the kit may import `next`, a theme may not.
|
|
135
150
|
|
|
136
151
|
## Develop
|
|
137
152
|
|
package/client.ts
CHANGED
package/navigation.tsx
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* How a theme links and moves between pages. Themes import these, never
|
|
5
|
+
* `next/link` or `next/navigation`: which framework runs the storefront is
|
|
6
|
+
* Queek's decision, and a theme written against this module keeps working
|
|
7
|
+
* when that changes — only this file does.
|
|
8
|
+
*
|
|
9
|
+
* import { Link, useRouter, usePathname } from '@usequeek/theme-kit/navigation';
|
|
10
|
+
*
|
|
11
|
+
* The surface is deliberately small (what every theme so far has needed).
|
|
12
|
+
* Ask for an addition rather than reaching past it to the framework.
|
|
13
|
+
*/
|
|
14
|
+
import NextLink from 'next/link';
|
|
15
|
+
import { usePathname as useFrameworkPathname, useRouter as useFrameworkRouter } from 'next/navigation';
|
|
16
|
+
import { useMemo, type AnchorHTMLAttributes, type ReactNode, type Ref } from 'react';
|
|
17
|
+
|
|
18
|
+
export interface LinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> {
|
|
19
|
+
/** A path inside the store (`menuItemToHref()`, `useHref()`) or a full URL. */
|
|
20
|
+
href: string;
|
|
21
|
+
/** Load the page in the background before it is clicked. Leave unset and the storefront decides. */
|
|
22
|
+
prefetch?: boolean;
|
|
23
|
+
/** Replace the current history entry instead of adding one. */
|
|
24
|
+
replace?: boolean;
|
|
25
|
+
ref?: Ref<HTMLAnchorElement>;
|
|
26
|
+
children?: ReactNode;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** An `<a>` that moves inside the store without a full page load. */
|
|
30
|
+
export function Link({ href, prefetch, replace, ...anchor }: LinkProps) {
|
|
31
|
+
return <NextLink href={href} prefetch={prefetch} replace={replace} {...anchor} />;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface Router {
|
|
35
|
+
/** Go to `href`, adding a history entry. */
|
|
36
|
+
push(href: string): void;
|
|
37
|
+
/** Go to `href`, replacing the current history entry. */
|
|
38
|
+
replace(href: string): void;
|
|
39
|
+
back(): void;
|
|
40
|
+
/** Re-render the current page with fresh data from the server. */
|
|
41
|
+
refresh(): void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Move between pages from code — after a form submits, a filter changes, a search runs. */
|
|
45
|
+
export function useRouter(): Router {
|
|
46
|
+
const router = useFrameworkRouter();
|
|
47
|
+
return useMemo(() => ({
|
|
48
|
+
push: (href: string) => router.push(href),
|
|
49
|
+
replace: (href: string) => router.replace(href),
|
|
50
|
+
back: () => router.back(),
|
|
51
|
+
refresh: () => router.refresh(),
|
|
52
|
+
}), [router]);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The current path, e.g. `/medley/shop` — to mark the active menu item. */
|
|
56
|
+
export function usePathname(): string {
|
|
57
|
+
return useFrameworkPathname() ?? '';
|
|
58
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { renderToStaticMarkup } from 'react-dom/server';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import * as navigation from '../navigation';
|
|
5
|
+
import { Link } from '../navigation';
|
|
6
|
+
|
|
7
|
+
describe('navigation — what a theme links and navigates with', () => {
|
|
8
|
+
it('is exactly Link, useRouter and usePathname', () => {
|
|
9
|
+
expect(Object.keys(navigation).sort()).toEqual(['Link', 'usePathname', 'useRouter']);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('renders Link as a plain anchor carrying its href, class and children', () => {
|
|
13
|
+
const html = renderToStaticMarkup(
|
|
14
|
+
React.createElement(Link, { href: '/medley/shop', className: 'md-nav__link', 'aria-current': 'page' }, 'Shop'),
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
expect(html).toContain('<a');
|
|
18
|
+
expect(html).toContain('href="/medley/shop"');
|
|
19
|
+
expect(html).toContain('class="md-nav__link"');
|
|
20
|
+
expect(html).toContain('aria-current="page"');
|
|
21
|
+
expect(html).toContain('>Shop</a>');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('keeps a full URL as is', () => {
|
|
25
|
+
const html = renderToStaticMarkup(React.createElement(Link, { href: 'https://usequeek.com/business', target: '_blank' }, 'Queek'));
|
|
26
|
+
|
|
27
|
+
expect(html).toContain('href="https://usequeek.com/business"');
|
|
28
|
+
expect(html).toContain('target="_blank"');
|
|
29
|
+
});
|
|
30
|
+
});
|