@viniaraujo68/plinth 0.3.0 → 0.5.0
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 +189 -19
- package/dist/components/ErrorDisplay.svelte +30 -16
- package/dist/components/ErrorDisplay.svelte.d.ts +4 -0
- package/dist/components/JsonView.svelte +43 -0
- package/dist/components/JsonView.svelte.d.ts +4 -0
- package/dist/components/JsonViewFrame.svelte +654 -0
- package/dist/components/JsonViewFrame.svelte.d.ts +31 -0
- package/dist/components/Paginator.svelte +34 -8
- package/dist/components/Paginator.svelte.d.ts +10 -0
- package/dist/components/RequestErrorDisplay.svelte +43 -0
- package/dist/components/RequestErrorDisplay.svelte.d.ts +11 -0
- package/dist/components/Tabs.svelte +204 -0
- package/dist/components/Tabs.svelte.d.ts +76 -0
- package/dist/components/VirtualJsonView.svelte +67 -0
- package/dist/components/VirtualJsonView.svelte.d.ts +4 -0
- package/dist/components/index.d.ts +8 -1
- package/dist/components/index.js +11 -5
- package/dist/components/json-view.d.ts +70 -0
- package/dist/components/json-view.js +302 -0
- package/dist/components/request-error.d.ts +8 -0
- package/dist/components/request-error.js +55 -0
- package/dist/components/tabs.d.ts +5 -0
- package/dist/components/tabs.js +1 -0
- package/dist/http.d.ts +10 -5
- package/dist/http.js +34 -6
- package/dist/i18n/LocaleToggle.svelte +45 -0
- package/dist/i18n/LocaleToggle.svelte.d.ts +48 -0
- package/dist/i18n/i18n.svelte.d.ts +56 -0
- package/dist/i18n/i18n.svelte.js +94 -0
- package/dist/i18n/index.d.ts +3 -0
- package/dist/i18n/index.js +7 -0
- package/dist/i18n/translate.d.ts +30 -0
- package/dist/i18n/translate.js +38 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/redirect.d.ts +40 -0
- package/dist/redirect.js +59 -0
- package/dist/routing.svelte.d.ts +1 -0
- package/dist/routing.svelte.js +3 -0
- package/dist/shell/AppShell.svelte +269 -95
- package/dist/shell/AppShell.svelte.d.ts +3 -2
- package/dist/shell/NavigationProgress.svelte +85 -0
- package/dist/shell/NavigationProgress.svelte.d.ts +31 -0
- package/dist/shell/TopBar.svelte +191 -0
- package/dist/shell/TopBar.svelte.d.ts +41 -0
- package/dist/shell/UserAvatar.svelte +50 -0
- package/dist/shell/UserAvatar.svelte.d.ts +14 -0
- package/dist/shell/index.d.ts +2 -0
- package/dist/shell/index.js +7 -3
- package/dist/shell/initials.d.ts +6 -0
- package/dist/shell/initials.js +13 -0
- package/dist/table/DataTable.svelte +199 -9
- package/dist/table/DataTable.svelte.d.ts +28 -5
- package/dist/table/FilterBar.svelte +417 -0
- package/dist/table/FilterBar.svelte.d.ts +34 -0
- package/dist/table/FilterRuleEditor.svelte +290 -0
- package/dist/table/FilterRuleEditor.svelte.d.ts +33 -0
- package/dist/table/filter-labels.d.ts +23 -0
- package/dist/table/filter-labels.js +30 -0
- package/dist/table/filter.d.ts +27 -0
- package/dist/table/filter.js +279 -0
- package/dist/table/index.d.ts +4 -1
- package/dist/table/index.js +5 -2
- package/dist/table/types.d.ts +49 -0
- package/dist/toast/Toaster.svelte +7 -3
- package/dist/toast/index.d.ts +1 -1
- package/dist/toast/toast-manager.svelte.d.ts +29 -1
- package/dist/toast/toast-manager.svelte.js +61 -1
- package/dist/url-state/codec.d.ts +15 -0
- package/dist/url-state/codec.js +1 -0
- package/dist/url-state/index.d.ts +5 -0
- package/dist/url-state/index.js +4 -0
- package/dist/url-state/params-codec.d.ts +73 -0
- package/dist/url-state/params-codec.js +116 -0
- package/dist/url-state/search-params.d.ts +4 -0
- package/dist/url-state/search-params.js +28 -0
- package/dist/url-state/synced-state.svelte.d.ts +20 -0
- package/dist/url-state/synced-state.svelte.js +88 -0
- package/dist/url-state/url-store.svelte.d.ts +12 -0
- package/dist/url-state/url-store.svelte.js +56 -0
- package/dist/virtual/DataList.svelte +215 -0
- package/dist/virtual/DataList.svelte.d.ts +52 -0
- package/dist/virtual/VirtualList.svelte +150 -0
- package/dist/virtual/VirtualList.svelte.d.ts +42 -0
- package/dist/virtual/index.d.ts +7 -0
- package/dist/virtual/index.js +6 -0
- package/dist/virtual/keyed-cache.d.ts +7 -0
- package/dist/virtual/keyed-cache.js +20 -0
- package/dist/virtual/labels.d.ts +12 -0
- package/dist/virtual/labels.js +6 -0
- package/dist/virtual/paging.d.ts +5 -0
- package/dist/virtual/paging.js +11 -0
- package/dist/virtual/range.d.ts +9 -0
- package/dist/virtual/range.js +31 -0
- package/dist/virtual/size-tree.d.ts +10 -0
- package/dist/virtual/size-tree.js +56 -0
- package/dist/virtual/virtualizer.svelte.d.ts +38 -0
- package/dist/virtual/virtualizer.svelte.js +295 -0
- package/dist/virtual/windowed-data.svelte.d.ts +38 -0
- package/dist/virtual/windowed-data.svelte.js +139 -0
- package/dist/virtual/windowed-virtualizer.svelte.d.ts +9 -0
- package/dist/virtual/windowed-virtualizer.svelte.js +22 -0
- package/package.json +13 -1
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { interpolate, matchLocale, pickPhrase, } from "./translate.js";
|
|
2
|
+
/**
|
|
3
|
+
* The locale an app is shown in, and the `t()` that reads from it.
|
|
4
|
+
*
|
|
5
|
+
* Build one per app, at module level, and import it wherever text is rendered: `locale` is
|
|
6
|
+
* `$state`, so every `t()` read in a template or a `$derived` re-renders when it changes.
|
|
7
|
+
*
|
|
8
|
+
* On the server the instance is shared by every request the process handles and there is no
|
|
9
|
+
* per-request place to keep a reader's language, so it renders the fallback locale and refuses to
|
|
10
|
+
* switch — a switch there would leak one visitor's language into the next one's page. The browser
|
|
11
|
+
* corrects the text during hydration, from the remembered pick or the browser's languages.
|
|
12
|
+
*/
|
|
13
|
+
export class I18n {
|
|
14
|
+
/** The current locale. Switch it with {@link I18n.setLocale}, which also remembers the pick. */
|
|
15
|
+
locale = $state();
|
|
16
|
+
/** Every locale on offer, in the order the dictionaries were declared. */
|
|
17
|
+
locales;
|
|
18
|
+
#dictionaries;
|
|
19
|
+
#tags;
|
|
20
|
+
#fallback;
|
|
21
|
+
#storageKey;
|
|
22
|
+
constructor({ dictionaries, tags, fallback, unmatched, storageKey = null }) {
|
|
23
|
+
this.#dictionaries = dictionaries;
|
|
24
|
+
this.#tags = tags;
|
|
25
|
+
this.#fallback = fallback;
|
|
26
|
+
this.#storageKey = storageKey;
|
|
27
|
+
this.locales = Object.keys(dictionaries);
|
|
28
|
+
this.locale = this.#initial(unmatched ?? fallback);
|
|
29
|
+
this.#applyLang();
|
|
30
|
+
}
|
|
31
|
+
/** The BCP 47 tag of the current locale, for `Intl` formatters and `toLocaleDateString`. */
|
|
32
|
+
get tag() {
|
|
33
|
+
return this.#tags[this.locale] ?? this.#tags[this.#fallback];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The text for `key` in the current locale, with `{name}` placeholders filled from `params` and
|
|
37
|
+
* a plural form picked by `params.count`. A key the current dictionary lacks is read from the
|
|
38
|
+
* fallback one; a key neither has renders as itself — loud on screen, harmless otherwise.
|
|
39
|
+
*
|
|
40
|
+
* An arrow property rather than a method, so `const { t } = i18n` keeps working.
|
|
41
|
+
*/
|
|
42
|
+
t = (key, params) => {
|
|
43
|
+
const phrase = this.#dictionaries[this.locale]?.[key] ?? this.#dictionaries[this.#fallback]?.[key];
|
|
44
|
+
return phrase === undefined ? key : interpolate(pickPhrase(phrase, params), params);
|
|
45
|
+
};
|
|
46
|
+
/** Whether any dictionary has `key` — for text that is optional rather than missing. */
|
|
47
|
+
has = (key) => key in (this.#dictionaries[this.locale] ?? {}) ||
|
|
48
|
+
key in (this.#dictionaries[this.#fallback] ?? {});
|
|
49
|
+
/** Switches to `locale` and remembers it. Ignored on the server and for a locale not on offer. */
|
|
50
|
+
setLocale = (locale) => {
|
|
51
|
+
if (typeof window === "undefined" || !this.locales.includes(locale))
|
|
52
|
+
return;
|
|
53
|
+
this.locale = locale;
|
|
54
|
+
this.#remember(locale);
|
|
55
|
+
this.#applyLang();
|
|
56
|
+
};
|
|
57
|
+
#initial(unmatched) {
|
|
58
|
+
if (typeof window === "undefined")
|
|
59
|
+
return this.#fallback;
|
|
60
|
+
const saved = this.#remembered();
|
|
61
|
+
if (saved)
|
|
62
|
+
return saved;
|
|
63
|
+
const languages = navigator.languages?.length ? navigator.languages : [navigator.language];
|
|
64
|
+
return matchLocale(languages.filter(Boolean), this.#tags) ?? unmatched;
|
|
65
|
+
}
|
|
66
|
+
#remembered() {
|
|
67
|
+
if (!this.#storageKey)
|
|
68
|
+
return null;
|
|
69
|
+
try {
|
|
70
|
+
const saved = localStorage.getItem(this.#storageKey);
|
|
71
|
+
return saved && this.locales.includes(saved) ? saved : null;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Storage blocked: fall through to the browser's languages.
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
#remember(locale) {
|
|
79
|
+
if (!this.#storageKey)
|
|
80
|
+
return;
|
|
81
|
+
try {
|
|
82
|
+
localStorage.setItem(this.#storageKey, locale);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// Storage blocked, as in some private modes: the pick still holds for this visit.
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
#applyLang() {
|
|
89
|
+
if (typeof document !== "undefined")
|
|
90
|
+
document.documentElement.lang = this.tag;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/** An {@link I18n} for `options` — the one place an app declares its locales. */
|
|
94
|
+
export const createI18n = (options) => new I18n(options);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// The app's own strings in more than one language: the current locale as state, a `t()` that reads
|
|
2
|
+
// it, and the toggle that switches it. The library still ships no translations of its own — its
|
|
3
|
+
// components take every word through props — so this is for the app's text, and those props are
|
|
4
|
+
// where its `t()` calls go. `@viniaraujo68/plinth/i18n`.
|
|
5
|
+
export { createI18n, I18n } from "./i18n.svelte.js";
|
|
6
|
+
export { default as LocaleToggle } from "./LocaleToggle.svelte";
|
|
7
|
+
export { interpolate, matchLocale, pickPhrase, } from "./translate.js";
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A dictionary value: plain text, or a pair picked by the `count` parameter.
|
|
3
|
+
*
|
|
4
|
+
* Two forms only, `one` for a count of exactly 1 and `other` for everything else. That is
|
|
5
|
+
* deliberately not CLDR: `Intl.PluralRules` files 0 under "one" in Portuguese, which would print
|
|
6
|
+
* "0 dia" where a Brazilian reader expects "0 dias". A language that needs more forms than two is
|
|
7
|
+
* the point at which a real i18n library earns its weight.
|
|
8
|
+
*/
|
|
9
|
+
export type Phrase = string | {
|
|
10
|
+
one: string;
|
|
11
|
+
other: string;
|
|
12
|
+
};
|
|
13
|
+
/** One locale's strings, flat and dot-namespaced: `"nav.login": "Entrar"`. */
|
|
14
|
+
export type Dictionary = Readonly<Record<string, Phrase>>;
|
|
15
|
+
/** What a `{name}` placeholder is filled with. `count` also picks a plural form. */
|
|
16
|
+
export type TranslationParams = Readonly<Record<string, string | number | null | undefined>>;
|
|
17
|
+
/** The form of `phrase` for `params`: the text itself, or the `one`/`other` the count picks. */
|
|
18
|
+
export declare const pickPhrase: (phrase: Phrase, params?: TranslationParams) => string;
|
|
19
|
+
/**
|
|
20
|
+
* `text` with each `{name}` replaced by `params.name`. A placeholder with no value — absent,
|
|
21
|
+
* `null` or `undefined` — is left as written, so a missing parameter shows up on screen instead of
|
|
22
|
+
* printing "undefined" into a sentence.
|
|
23
|
+
*/
|
|
24
|
+
export declare const interpolate: (text: string, params?: TranslationParams) => string;
|
|
25
|
+
/**
|
|
26
|
+
* Which of `locales` a reader's browser asks for. `languages` is `navigator.languages`, most
|
|
27
|
+
* preferred first: each is matched against the locale tags exactly, then by language alone, so a
|
|
28
|
+
* browser in `pt-PT` still lands on a `pt-BR` locale. `null` when none of them matches.
|
|
29
|
+
*/
|
|
30
|
+
export declare const matchLocale: <L extends string>(languages: readonly string[], tags: Readonly<Record<L, string>>) => L | null;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** The form of `phrase` for `params`: the text itself, or the `one`/`other` the count picks. */
|
|
2
|
+
export const pickPhrase = (phrase, params) => {
|
|
3
|
+
if (typeof phrase === "string")
|
|
4
|
+
return phrase;
|
|
5
|
+
return params?.count === 1 ? phrase.one : phrase.other;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* `text` with each `{name}` replaced by `params.name`. A placeholder with no value — absent,
|
|
9
|
+
* `null` or `undefined` — is left as written, so a missing parameter shows up on screen instead of
|
|
10
|
+
* printing "undefined" into a sentence.
|
|
11
|
+
*/
|
|
12
|
+
export const interpolate = (text, params) => {
|
|
13
|
+
if (!params)
|
|
14
|
+
return text;
|
|
15
|
+
return text.replace(/\{(\w+)\}/g, (placeholder, name) => {
|
|
16
|
+
const value = params[name];
|
|
17
|
+
return value === null || value === undefined ? placeholder : String(value);
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Which of `locales` a reader's browser asks for. `languages` is `navigator.languages`, most
|
|
22
|
+
* preferred first: each is matched against the locale tags exactly, then by language alone, so a
|
|
23
|
+
* browser in `pt-PT` still lands on a `pt-BR` locale. `null` when none of them matches.
|
|
24
|
+
*/
|
|
25
|
+
export const matchLocale = (languages, tags) => {
|
|
26
|
+
const entries = Object.entries(tags);
|
|
27
|
+
const primary = (tag) => tag.toLowerCase().split("-")[0];
|
|
28
|
+
for (const language of languages) {
|
|
29
|
+
const wanted = language.toLowerCase();
|
|
30
|
+
const exact = entries.find(([, tag]) => tag.toLowerCase() === wanted);
|
|
31
|
+
if (exact)
|
|
32
|
+
return exact[0];
|
|
33
|
+
const loose = entries.find(([, tag]) => primary(tag) === primary(wanted));
|
|
34
|
+
if (loose)
|
|
35
|
+
return loose[0];
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,6 @@
|
|
|
5
5
|
* no portable way to import its own manifest, and a consumer that only wants to log a version
|
|
6
6
|
* should not have to bundle one. `version.spec.ts` fails when this drifts from the manifest.
|
|
7
7
|
*/
|
|
8
|
-
export declare const LIBRARY_VERSION = "0.
|
|
8
|
+
export declare const LIBRARY_VERSION = "0.5.0";
|
|
9
9
|
export { AsyncAction, type AsyncActionStatus, type AsyncFn } from "./async-action.svelte.js";
|
|
10
10
|
export { reportError, setErrorReporter, type ErrorReporter } from "./error.js";
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* no portable way to import its own manifest, and a consumer that only wants to log a version
|
|
6
6
|
* should not have to bundle one. `version.spec.ts` fails when this drifts from the manifest.
|
|
7
7
|
*/
|
|
8
|
-
export const LIBRARY_VERSION = "0.
|
|
8
|
+
export const LIBRARY_VERSION = "0.5.0";
|
|
9
9
|
// The root entry carries only what is not tied to one area: the error channel an app installs
|
|
10
10
|
// once, and the async state machine the components are built on. Everything visual is reached
|
|
11
11
|
// through a subpath — `/theme`, `/attachments`, `/components`, `/toast` — so importing a button
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `?next=` round trip of a sign-in page: where a guard sends a visitor who is not signed in,
|
|
3
|
+
* and where the sign-in page may send them back to afterwards.
|
|
4
|
+
*
|
|
5
|
+
* The value arrives in a URL anyone can write, so it is attacker-controlled. Handed straight to
|
|
6
|
+
* `goto` or `location`, `?next=//evil.example` would send someone who just typed their password to
|
|
7
|
+
* another site — an open redirect, the first step of most credential phishing.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* `value` when it is a path on this site, normalised; `null` for anything else.
|
|
11
|
+
*
|
|
12
|
+
* A leading `/` is not enough on its own: `//evil.example` and `/\evil.example` are
|
|
13
|
+
* protocol-relative to a browser, and the URL parser drops tabs and line breaks before it reads a
|
|
14
|
+
* URL at all, so `/<tab>/evil.example` turns into one of them on the way in. Rather than chase every
|
|
15
|
+
* spelling, the candidate is resolved the way the browser would resolve it and kept only if it
|
|
16
|
+
* stays on the same origin.
|
|
17
|
+
*/
|
|
18
|
+
export declare const safeNext: (value: string | null | undefined) => string | null;
|
|
19
|
+
export interface LoginUrlOptions {
|
|
20
|
+
/** The sign-in page. Defaults to `/login`. */
|
|
21
|
+
path?: string;
|
|
22
|
+
/** The search parameter the return path travels in. Defaults to `next`. */
|
|
23
|
+
param?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Where a guard sends a visitor who has to sign in first: the sign-in page, carrying the current
|
|
27
|
+
* path so it can send them back.
|
|
28
|
+
*
|
|
29
|
+
* Only the path travels. The query string is dropped on purpose: it is where share tokens and
|
|
30
|
+
* one-off codes live, and a sign-in URL gets logged, bookmarked and pasted far more than the page
|
|
31
|
+
* it came from. Nothing is carried from the site root or from the sign-in page itself, where
|
|
32
|
+
* "come back here" means nothing.
|
|
33
|
+
*/
|
|
34
|
+
export declare const loginUrl: (current: URL | string, { path, param }?: LoginUrlOptions) => string;
|
|
35
|
+
/**
|
|
36
|
+
* `base` carrying `next` along — the link between the sign-in and the sign-up pages, so the
|
|
37
|
+
* return path survives a visitor who picked the wrong one first. `next` is checked again here, so
|
|
38
|
+
* an unchecked value cannot slip through a link built from it.
|
|
39
|
+
*/
|
|
40
|
+
export declare const withNext: (base: string, next: string | null | undefined, param?: string) => string;
|
package/dist/redirect.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `?next=` round trip of a sign-in page: where a guard sends a visitor who is not signed in,
|
|
3
|
+
* and where the sign-in page may send them back to afterwards.
|
|
4
|
+
*
|
|
5
|
+
* The value arrives in a URL anyone can write, so it is attacker-controlled. Handed straight to
|
|
6
|
+
* `goto` or `location`, `?next=//evil.example` would send someone who just typed their password to
|
|
7
|
+
* another site — an open redirect, the first step of most credential phishing.
|
|
8
|
+
*/
|
|
9
|
+
/** A base no real link can point at, used only to let the URL parser resolve a candidate path. */
|
|
10
|
+
const PROBE_ORIGIN = "https://plinth.invalid";
|
|
11
|
+
/**
|
|
12
|
+
* `value` when it is a path on this site, normalised; `null` for anything else.
|
|
13
|
+
*
|
|
14
|
+
* A leading `/` is not enough on its own: `//evil.example` and `/\evil.example` are
|
|
15
|
+
* protocol-relative to a browser, and the URL parser drops tabs and line breaks before it reads a
|
|
16
|
+
* URL at all, so `/<tab>/evil.example` turns into one of them on the way in. Rather than chase every
|
|
17
|
+
* spelling, the candidate is resolved the way the browser would resolve it and kept only if it
|
|
18
|
+
* stays on the same origin.
|
|
19
|
+
*/
|
|
20
|
+
export const safeNext = (value) => {
|
|
21
|
+
if (typeof value !== "string" || !value.startsWith("/"))
|
|
22
|
+
return null;
|
|
23
|
+
let url;
|
|
24
|
+
try {
|
|
25
|
+
url = new URL(value, PROBE_ORIGIN);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
if (url.origin !== PROBE_ORIGIN)
|
|
31
|
+
return null;
|
|
32
|
+
return `${url.pathname}${url.search}${url.hash}`;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Where a guard sends a visitor who has to sign in first: the sign-in page, carrying the current
|
|
36
|
+
* path so it can send them back.
|
|
37
|
+
*
|
|
38
|
+
* Only the path travels. The query string is dropped on purpose: it is where share tokens and
|
|
39
|
+
* one-off codes live, and a sign-in URL gets logged, bookmarked and pasted far more than the page
|
|
40
|
+
* it came from. Nothing is carried from the site root or from the sign-in page itself, where
|
|
41
|
+
* "come back here" means nothing.
|
|
42
|
+
*/
|
|
43
|
+
export const loginUrl = (current, { path = "/login", param = "next" } = {}) => {
|
|
44
|
+
const pathname = typeof current === "string" ? new URL(current, PROBE_ORIGIN).pathname : current.pathname;
|
|
45
|
+
if (pathname === "/" || pathname === path)
|
|
46
|
+
return path;
|
|
47
|
+
return `${path}?${param}=${encodeURIComponent(pathname)}`;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* `base` carrying `next` along — the link between the sign-in and the sign-up pages, so the
|
|
51
|
+
* return path survives a visitor who picked the wrong one first. `next` is checked again here, so
|
|
52
|
+
* an unchecked value cannot slip through a link built from it.
|
|
53
|
+
*/
|
|
54
|
+
export const withNext = (base, next, param = "next") => {
|
|
55
|
+
const safe = safeNext(next);
|
|
56
|
+
if (!safe)
|
|
57
|
+
return base;
|
|
58
|
+
return `${base}${base.includes("?") ? "&" : "?"}${param}=${encodeURIComponent(safe)}`;
|
|
59
|
+
};
|
package/dist/routing.svelte.d.ts
CHANGED
|
@@ -161,6 +161,7 @@ export declare class RoutingContext<Id extends string = string> {
|
|
|
161
161
|
*/
|
|
162
162
|
export declare const resolvePathname: (pathname: string) => string;
|
|
163
163
|
export declare const getRoutingContext: () => RoutingContext<string>, setRoutingContext: (context: RoutingContext<string>) => RoutingContext<string>;
|
|
164
|
+
export { loginUrl, safeNext, withNext, type LoginUrlOptions } from "./redirect.js";
|
|
164
165
|
/**
|
|
165
166
|
* A copy of `url` with some search parameters set and others removed — `null` or `undefined`
|
|
166
167
|
* removes. It never edits the URL it was given, which is usually the page store's own and is read
|
package/dist/routing.svelte.js
CHANGED
|
@@ -189,6 +189,9 @@ export class RoutingContext {
|
|
|
189
189
|
*/
|
|
190
190
|
export const resolvePathname = (pathname) => resolve(pathname);
|
|
191
191
|
export const [getRoutingContext, setRoutingContext] = createContext();
|
|
192
|
+
// The `?next=` round trip of a sign-in page. Pure functions over strings, kept in their own module
|
|
193
|
+
// so they can be read and tested without a routing context.
|
|
194
|
+
export { loginUrl, safeNext, withNext } from "./redirect.js";
|
|
192
195
|
/**
|
|
193
196
|
* A copy of `url` with some search parameters set and others removed — `null` or `undefined`
|
|
194
197
|
* removes. It never edits the URL it was given, which is usually the page store's own and is read
|