@webround/core 0.3.2 → 0.3.4

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.
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // packages/core/src/context/WrAppContext.tsx
3
3
  import { createContext, useContext, useState, useCallback, useEffect, useMemo, useSyncExternalStore, lazy, Suspense, } from 'react';
4
4
  import { createBrowserRouter, RouterProvider, useLocation, useParams } from 'react-router-dom';
5
+ import { WrScroll } from '../scroll';
5
6
  import { wrResolveRoutes, wrBuildRegistry, wrBuildLazyRegistry, wrBuildPageConfigMap, wrExpandRoutesForLangs, wrExtractLang, wrBuildLangPath, } from '../routing';
6
7
  import { WrPageProvider } from './WrPageContext';
7
8
  import { WrLayout } from './WrLayout';
@@ -47,7 +48,7 @@ function WrRouteElement({ route, pageConfigMap, lazyFallback: LazyFallback, Page
47
48
  const Component = route.Component;
48
49
  return _jsx(Component, {});
49
50
  })();
50
- return (_jsx(PageProvider, { config: config, pageName: pageName, children: _jsxs(WrSharedContext.Provider, { value: shared, children: [_jsx(WrSeoHead, { site: seo, config: config, pathname: pathname, lang: lang, langs: langs }), _jsx(WrLayout, { Header: Header, Footer: Footer, showHeader: route.header, showFooter: route.footer, pageConfigMap: pageConfigMap, ScopeProvider: PageProvider, children: body })] }) }, pageName));
51
+ return (_jsxs(PageProvider, { config: config, pageName: pageName, children: [_jsx(WrScroll, {}), _jsxs(WrSharedContext.Provider, { value: shared, children: [_jsx(WrSeoHead, { site: seo, config: config, pathname: pathname, lang: lang, langs: langs }), _jsx(WrLayout, { Header: Header, Footer: Footer, showHeader: route.header, showFooter: route.footer, pageConfigMap: pageConfigMap, ScopeProvider: PageProvider, children: body })] })] }, pageName));
51
52
  }
52
53
  export function WrAppProvider({ routesFile, eagerModules, lazyModules, configModules, langs, initialLang = 'default', lazyFallback = WrDefaultPageFallback, onRouterReady, PageProvider = WrPageProvider, Header, Footer, seo, children, }) {
53
54
  const router = useMemo(() => {
package/dist/routing.d.ts CHANGED
@@ -115,6 +115,16 @@ export declare function wrExtractLang(pathname: string, langs: WrLangConfig): La
115
115
  export declare function wrBuildLangPath(targetLang: LangCode, pathname: string, langs: WrLangConfig): string;
116
116
  /** Where an address that leads to no page goes: the home page of the language it is in (`/en/nope` → `/en`, `/nope` → `/`). */
117
117
  export declare function wrHomeOf(pathname: string, langs?: WrLangConfig): string;
118
+ export type WrScrollAction = 'anchor' | 'top' | 'none';
119
+ /**
120
+ * What the scroll does when the address changes (the one rule, used by `WrScroll`). A link to a place of a page (`#list`) goes to that place; a NEW page (another path) starts at the top;
121
+ * "back" and "forward" leave it to the browser, which puts the page where it was left; and the same page in another state (a change of the query: the filters or the page of a shop) keeps its place.
122
+ * `previous` is the path of the page before, `null` for the first one the visitor sees.
123
+ */
124
+ export declare function wrScrollAction(previous: string | null, location: {
125
+ pathname: string;
126
+ hash: string;
127
+ }, navigationType: 'PUSH' | 'REPLACE' | 'POP'): WrScrollAction;
118
128
  /** An address that leaves the site: it has a scheme (`https:`, `mailto:`, `tel:`) or starts with `//`. Anything else is the site's own and is a router `Link`, whatever `target` it was given. */
119
129
  export declare const wrIsExternalHref: (href: string) => boolean;
120
130
  export declare function wrLocalizeHref(href: string, lang: string): string;
package/dist/routing.js CHANGED
@@ -178,6 +178,18 @@ export function wrBuildLangPath(targetLang, pathname, langs) {
178
178
  export function wrHomeOf(pathname, langs) {
179
179
  return langs ? wrBuildLangPath(wrExtractLang(pathname, langs), '/', langs) : '/';
180
180
  }
181
+ /**
182
+ * What the scroll does when the address changes (the one rule, used by `WrScroll`). A link to a place of a page (`#list`) goes to that place; a NEW page (another path) starts at the top;
183
+ * "back" and "forward" leave it to the browser, which puts the page where it was left; and the same page in another state (a change of the query: the filters or the page of a shop) keeps its place.
184
+ * `previous` is the path of the page before, `null` for the first one the visitor sees.
185
+ */
186
+ export function wrScrollAction(previous, location, navigationType) {
187
+ if (location.hash)
188
+ return 'anchor';
189
+ if (previous === null || previous === location.pathname)
190
+ return 'none';
191
+ return navigationType === 'POP' ? 'none' : 'top';
192
+ }
181
193
  /** An address that leaves the site: it has a scheme (`https:`, `mailto:`, `tel:`) or starts with `//`. Anything else is the site's own and is a router `Link`, whatever `target` it was given. */
182
194
  export const wrIsExternalHref = (href) => /^([a-z][a-z0-9+.-]*:|\/\/)/i.test(href);
183
195
  export function wrLocalizeHref(href, lang) {
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Puts the scroll where a visitor expects it when the address changes: at the top on a new page, at the place a link points to, and leaves it alone on the same page in another state
3
+ * (the rule is `wrScrollAction`). Renders nothing. One per page, inside the router; core and editor mount the same one.
4
+ */
5
+ export declare function WrScroll(): null;
6
+ //# sourceMappingURL=scroll.d.ts.map
package/dist/scroll.js ADDED
@@ -0,0 +1,31 @@
1
+ import { useLayoutEffect } from 'react';
2
+ import { useLocation, useNavigationType } from 'react-router-dom';
3
+ import { wrScrollAction } from './routing';
4
+ // The path of the page before: kept here and not in the component, because the component is inside the page, which is made again for every page (its `key` is the page's name)
5
+ let previousPath = null;
6
+ /**
7
+ * Puts the scroll where a visitor expects it when the address changes: at the top on a new page, at the place a link points to, and leaves it alone on the same page in another state
8
+ * (the rule is `wrScrollAction`). Renders nothing. One per page, inside the router; core and editor mount the same one.
9
+ */
10
+ export function WrScroll() {
11
+ const { pathname, hash } = useLocation();
12
+ const type = useNavigationType();
13
+ useLayoutEffect(() => {
14
+ const before = previousPath;
15
+ previousPath = pathname;
16
+ let action = wrScrollAction(before, { pathname, hash }, type);
17
+ if (action === 'anchor') {
18
+ const place = document.getElementById(decodeURIComponent(hash.slice(1)));
19
+ if (place) {
20
+ place.scrollIntoView();
21
+ return;
22
+ }
23
+ // a place that is not there: the page is treated as if the link had no `#`
24
+ action = wrScrollAction(before, { pathname, hash: '' }, type);
25
+ }
26
+ if (action === 'top')
27
+ window.scrollTo(0, 0);
28
+ }, [pathname, hash, type]);
29
+ return null;
30
+ }
31
+ //# sourceMappingURL=scroll.js.map
package/dist/seo.d.ts CHANGED
@@ -74,6 +74,9 @@ export type WrSeoJsonLd = WrJsonLd[] | Record<string, WrJsonLd[]>;
74
74
  export declare const WR_ROBOTS_FILE = "robots.txt";
75
75
  export declare const WR_SITEMAP_INDEX_FILE = "sitemap-index.xml";
76
76
  export declare const WR_SITEMAP_PAGES_FILE = "sitemap-pages.xml";
77
+ /** The sitemaps the shop's server writes every day (not the build): a site with the page of a product (or of a collection) always has its index point to the sitemap of them. */
78
+ export declare const WR_SITEMAP_PRODUCTS_FILE = "sitemap-products.xml";
79
+ export declare const WR_SITEMAP_COLLECTIONS_FILE = "sitemap-collections.xml";
77
80
  /** A page's SEO data out of its JSON (an empty object when it says nothing). */
78
81
  export declare function wrSeoPageOf(config: WrPageConfig | undefined): WrSeoPage;
79
82
  /** The site's address without a trailing slash, or undefined when it is not an http(s) address. */
package/dist/seo.js CHANGED
@@ -20,6 +20,10 @@ export function wrSearchConsoleCode(input) {
20
20
  export const WR_ROBOTS_FILE = 'robots.txt';
21
21
  export const WR_SITEMAP_INDEX_FILE = 'sitemap-index.xml';
22
22
  export const WR_SITEMAP_PAGES_FILE = 'sitemap-pages.xml';
23
+ /** The sitemaps the shop's server writes every day (not the build): a site with the page of a product (or of a collection) always has its index point to the sitemap of them. */
24
+ export const WR_SITEMAP_PRODUCTS_FILE = 'sitemap-products.xml';
25
+ export const WR_SITEMAP_COLLECTIONS_FILE = 'sitemap-collections.xml';
26
+ const WR_SHOP_SITEMAPS = { 'product/[slug]': WR_SITEMAP_PRODUCTS_FILE, 'shop/[slug]': WR_SITEMAP_COLLECTIONS_FILE };
23
27
  /** A page's SEO data out of its JSON (an empty object when it says nothing). */
24
28
  export function wrSeoPageOf(config) {
25
29
  const raw = config?.[WR_SEO_KEY];
@@ -211,10 +215,12 @@ export function wrSeoFiles(input) {
211
215
  if (!siteUrl)
212
216
  return null;
213
217
  const entries = wrSeoEntries(input);
214
- const extra = (input.site?.extraSitemaps ?? []).filter(name => typeof name === 'string' && name.trim() && name !== WR_SITEMAP_PAGES_FILE);
218
+ // The shop's sitemaps are cited whether the files exist yet or not: the server writes them daily and the index must already say where they are
219
+ const own = [WR_SITEMAP_PAGES_FILE, ...Object.entries(WR_SHOP_SITEMAPS).filter(([route]) => route in input.routes).map(([, file]) => file)];
220
+ const extra = (input.site?.extraSitemaps ?? []).filter(name => typeof name === 'string' && name.trim() && !own.includes(name));
215
221
  return {
216
222
  [WR_ROBOTS_FILE]: wrRobotsTxt(siteUrl),
217
- [WR_SITEMAP_INDEX_FILE]: wrSitemapIndexXml(siteUrl, [WR_SITEMAP_PAGES_FILE, ...extra]),
223
+ [WR_SITEMAP_INDEX_FILE]: wrSitemapIndexXml(siteUrl, [...own, ...extra]),
218
224
  [WR_SITEMAP_PAGES_FILE]: wrSitemapPagesXml(entries, input.langs),
219
225
  };
220
226
  }
package/dist/theme.d.ts CHANGED
@@ -19,4 +19,10 @@ export declare function wrThemeHasDark(css: string): boolean;
19
19
  export declare function wrThemeStartsFromSystem(css: string): boolean;
20
20
  /** Whether the page's stylesheets have a `.dark` rule: the same definition as `wrThemeHasDark`, read from the page the visitor has (the toggle is only there when it can do something). */
21
21
  export declare function wrThemeSiteHasDark(doc: Document): boolean;
22
+ /** The weights as they are written into the theme: only what is a weight, once each, in order; nothing left is `undefined` (Google's own default). */
23
+ export declare function wrGoogleWeights(weights: string | undefined): string | undefined;
24
+ /** The `family=…` part of the address of Google's stylesheet for one family: `family=Inter:wght@400;700`, and with an italic one `family=Inter:ital,wght@0,400;1,400`. */
25
+ export declare function wrGoogleFamilyParam(family: string, weights?: string): string;
26
+ /** The weights of a family in the address of a stylesheet (what follows the `:` of `family=Inter:ital,wght@0,400;1,400`), as the theme says them. */
27
+ export declare function wrGoogleWeightsOfParam(axes: string | undefined): string | undefined;
22
28
  //# sourceMappingURL=theme.d.ts.map
package/dist/theme.js CHANGED
@@ -42,4 +42,50 @@ export function wrThemeSiteHasDark(doc) {
42
42
  return false;
43
43
  } });
44
44
  }
45
+ const GOOGLE_WEIGHT = /^(\d{1,4})(?:\.\.(\d{1,4}))?(i?)$/;
46
+ function wrGoogleWeightsParsed(weights) {
47
+ const found = new Map();
48
+ for (const part of (weights ?? '').split(';')) {
49
+ const match = GOOGLE_WEIGHT.exec(part.trim());
50
+ if (!match)
51
+ continue;
52
+ const from = Number(match[1]);
53
+ const to = match[2] === undefined ? null : Number(match[2]);
54
+ if (from < 1 || from > 1000 || (to !== null && (to > 1000 || to < from)))
55
+ continue;
56
+ const weight = { from, to: to === from ? null : to, italic: match[3] === 'i' };
57
+ found.set(wrGoogleWeightText(weight), weight);
58
+ }
59
+ // Upright first, then italic, each by weight: the order Google asks for
60
+ return [...found.values()].sort((a, b) => Number(a.italic) - Number(b.italic) || a.from - b.from);
61
+ }
62
+ const wrGoogleWeightValue = ({ from, to }) => (to === null ? `${from}` : `${from}..${to}`);
63
+ const wrGoogleWeightText = (weight) => `${wrGoogleWeightValue(weight)}${weight.italic ? 'i' : ''}`;
64
+ /** The weights as they are written into the theme: only what is a weight, once each, in order; nothing left is `undefined` (Google's own default). */
65
+ export function wrGoogleWeights(weights) {
66
+ const parsed = wrGoogleWeightsParsed(weights);
67
+ return parsed.length > 0 ? parsed.map(wrGoogleWeightText).join(';') : undefined;
68
+ }
69
+ /** The `family=…` part of the address of Google's stylesheet for one family: `family=Inter:wght@400;700`, and with an italic one `family=Inter:ital,wght@0,400;1,400`. */
70
+ export function wrGoogleFamilyParam(family, weights) {
71
+ const name = encodeURIComponent(family.trim()).replace(/%20/g, '+');
72
+ const parsed = wrGoogleWeightsParsed(weights);
73
+ if (parsed.length === 0)
74
+ return `family=${name}`;
75
+ if (parsed.some(weight => weight.italic))
76
+ return `family=${name}:ital,wght@${parsed.map(weight => `${weight.italic ? 1 : 0},${wrGoogleWeightValue(weight)}`).join(';')}`;
77
+ return `family=${name}:wght@${parsed.map(wrGoogleWeightValue).join(';')}`;
78
+ }
79
+ /** The weights of a family in the address of a stylesheet (what follows the `:` of `family=Inter:ital,wght@0,400;1,400`), as the theme says them. */
80
+ export function wrGoogleWeightsOfParam(axes) {
81
+ if (!axes)
82
+ return undefined;
83
+ if (axes.startsWith('wght@'))
84
+ return wrGoogleWeights(axes.slice('wght@'.length));
85
+ if (axes.startsWith('ital,wght@')) {
86
+ const parts = axes.slice('ital,wght@'.length).split(';').map(tuple => tuple.split(','));
87
+ return wrGoogleWeights(parts.map(([ital, value]) => `${value}${ital === '1' ? 'i' : ''}`).join(';'));
88
+ }
89
+ return undefined;
90
+ }
45
91
  //# sourceMappingURL=theme.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webround/core",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -61,6 +61,11 @@
61
61
  "types": "./dist/seo-react.d.ts",
62
62
  "development": "./src/seo-react.tsx"
63
63
  },
64
+ "./scroll": {
65
+ "import": "./dist/scroll.js",
66
+ "types": "./dist/scroll.d.ts",
67
+ "development": "./src/scroll.tsx"
68
+ },
64
69
  "./not-found": {
65
70
  "import": "./dist/not-found.js",
66
71
  "types": "./dist/not-found.d.ts",