@-reddacted-/react-ui 0.1.0 → 0.2.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/dist/components/Button.d.ts +9 -0
- package/dist/components/Button.d.ts.map +1 -0
- package/dist/components/Button.js +44 -0
- package/dist/components/ColorField.d.ts +9 -0
- package/dist/components/ColorField.d.ts.map +1 -0
- package/dist/components/ColorField.js +34 -0
- package/dist/components/ColorPicker.d.ts +11 -0
- package/dist/components/ColorPicker.d.ts.map +1 -0
- package/dist/components/ColorPicker.js +103 -0
- package/dist/components/ConfirmDeleteButton.d.ts +13 -0
- package/dist/components/ConfirmDeleteButton.d.ts.map +1 -0
- package/dist/components/ConfirmDeleteButton.js +25 -0
- package/dist/components/Modal.d.ts +25 -0
- package/dist/components/Modal.d.ts.map +1 -0
- package/dist/components/Modal.js +195 -0
- package/dist/components/NavBar.d.ts +20 -0
- package/dist/components/NavBar.d.ts.map +1 -0
- package/dist/components/NavBar.js +31 -0
- package/dist/components/PageStatus.d.ts +16 -0
- package/dist/components/PageStatus.d.ts.map +1 -0
- package/dist/components/PageStatus.js +31 -0
- package/dist/index.d.ts +19 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -3
- package/dist/net/fetchJson.d.ts +2 -0
- package/dist/net/fetchJson.d.ts.map +1 -0
- package/dist/net/fetchJson.js +11 -0
- package/dist/net/useDocumentTitle.d.ts +2 -0
- package/dist/net/useDocumentTitle.d.ts.map +1 -0
- package/dist/net/useDocumentTitle.js +8 -0
- package/dist/popup/PopupHost.d.ts +8 -0
- package/dist/popup/PopupHost.d.ts.map +1 -0
- package/dist/popup/PopupHost.js +46 -0
- package/dist/popup/PopupProvider.d.ts +9 -0
- package/dist/popup/PopupProvider.d.ts.map +1 -0
- package/dist/popup/PopupProvider.js +28 -0
- package/dist/popup/errorRules.d.ts +23 -0
- package/dist/popup/errorRules.d.ts.map +1 -0
- package/dist/popup/errorRules.js +59 -0
- package/dist/popup/fetchInterceptor.d.ts +7 -0
- package/dist/popup/fetchInterceptor.d.ts.map +1 -0
- package/dist/popup/fetchInterceptor.js +16 -0
- package/dist/theme/ThemeProvider.d.ts +24 -0
- package/dist/theme/ThemeProvider.d.ts.map +1 -0
- package/dist/theme/ThemeProvider.js +63 -0
- package/dist/theme/ThemeSelect.d.ts +9 -0
- package/dist/theme/ThemeSelect.d.ts.map +1 -0
- package/dist/theme/ThemeSelect.js +17 -0
- package/dist/theme/clipboard.d.ts +6 -0
- package/dist/theme/clipboard.d.ts.map +1 -0
- package/dist/theme/clipboard.js +26 -0
- package/dist/theme/palette.d.ts +12 -0
- package/dist/theme/palette.d.ts.map +1 -0
- package/dist/theme/palette.js +72 -0
- package/dist/theme/store.d.ts +11 -0
- package/dist/theme/store.d.ts.map +1 -0
- package/dist/theme/store.js +92 -0
- package/dist/theme/types.d.ts +17 -0
- package/dist/theme/types.d.ts.map +1 -0
- package/dist/theme/types.js +1 -0
- package/package.json +9 -6
package/dist/index.js
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
-
export { default as Modal, openModal, closeModal } from "./Modal";
|
|
2
|
-
export { default as ColorPicker } from "./ColorPicker";
|
|
3
|
-
export { default as ConfirmDeleteButton } from "./ConfirmDeleteButton";
|
|
1
|
+
export { default as Modal, openModal, closeModal } from "./components/Modal";
|
|
2
|
+
export { default as ColorPicker } from "./components/ColorPicker";
|
|
3
|
+
export { default as ConfirmDeleteButton } from "./components/ConfirmDeleteButton";
|
|
4
|
+
export { ColorField } from "./components/ColorField";
|
|
5
|
+
export { Button } from "./components/Button";
|
|
6
|
+
export { NavBar } from "./components/NavBar";
|
|
7
|
+
export { PageMessage, LoadingMessage, ErrorMessage } from "./components/PageStatus";
|
|
8
|
+
export { ThemeProvider, useTheme } from "./theme/ThemeProvider";
|
|
9
|
+
export { ThemeSelect } from "./theme/ThemeSelect";
|
|
10
|
+
export { defaultPalette, themeVariableGroups, themeVariableKeys } from "./theme/palette";
|
|
11
|
+
export { createLocalThemeStore, createHttpThemeStore } from "./theme/store";
|
|
12
|
+
export { serializeColors, parseColors, isHexColor, randomHex } from "./theme/clipboard";
|
|
13
|
+
export { PopupProvider } from "./popup/PopupProvider";
|
|
14
|
+
export { PopupHost } from "./popup/PopupHost";
|
|
15
|
+
export { createInterceptedFetch } from "./popup/fetchInterceptor";
|
|
16
|
+
export { createErrorRules, defaultMessages, } from "./popup/errorRules";
|
|
17
|
+
export { fetchJson } from "./net/fetchJson";
|
|
18
|
+
export { useDocumentTitle } from "./net/useDocumentTitle";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetchJson.d.ts","sourceRoot":"","sources":["../../src/net/fetchJson.ts"],"names":[],"mappings":"AAEA,wBAAsB,SAAS,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAOvF"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Reads a JSON response. A non-2xx status throws so the caller can tell a failure
|
|
2
|
+
// apart from "no content yet": an empty body yields null.
|
|
3
|
+
export async function fetchJson(url, signal) {
|
|
4
|
+
const res = await fetch(url, { signal });
|
|
5
|
+
if (!res.ok)
|
|
6
|
+
throw new Error(`${url} -> ${res.status}`);
|
|
7
|
+
const body = await res.text();
|
|
8
|
+
if (body.trim() === "")
|
|
9
|
+
return null;
|
|
10
|
+
return JSON.parse(body);
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useDocumentTitle.d.ts","sourceRoot":"","sources":["../../src/net/useDocumentTitle.ts"],"names":[],"mappings":"AAIA,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,QAI9F"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
// Without this every page shares the title of index.html in the tab, the history
|
|
3
|
+
// and the search results.
|
|
4
|
+
export function useDocumentTitle(title, siteName, fallback) {
|
|
5
|
+
useEffect(() => {
|
|
6
|
+
document.title = title ? `${title} — ${siteName}` : (fallback ?? siteName);
|
|
7
|
+
}, [title, siteName, fallback]);
|
|
8
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
type PopupHostProps = {
|
|
2
|
+
messages: string[];
|
|
3
|
+
onDismiss: () => void;
|
|
4
|
+
closeLabel?: string;
|
|
5
|
+
};
|
|
6
|
+
export declare function PopupHost({ messages, onDismiss, closeLabel }: PopupHostProps): import("react").JSX.Element | null;
|
|
7
|
+
export {};
|
|
8
|
+
//# sourceMappingURL=PopupHost.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PopupHost.d.ts","sourceRoot":"","sources":["../../src/popup/PopupHost.tsx"],"names":[],"mappings":"AAEA,KAAK,cAAc,GAAG;IACpB,QAAQ,EAAE,MAAM,EAAE,CAAA;IAClB,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB,CAAA;AAED,wBAAgB,SAAS,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,UAAoB,EAAE,EAAE,cAAc,sCAiBtF"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
export function PopupHost({ messages, onDismiss, closeLabel = "Close" }) {
|
|
3
|
+
if (messages.length === 0)
|
|
4
|
+
return null;
|
|
5
|
+
return (_jsxs("div", { role: "alert", "aria-live": "assertive", style: hostStyle, children: [_jsx("div", { style: bodyStyle, children: messages.map(message => (_jsx("p", { style: messageStyle, children: message }, message))) }), _jsx("button", { type: "button", "aria-label": closeLabel, onClick: onDismiss, style: closeStyle, children: "\u00D7" })] }));
|
|
6
|
+
}
|
|
7
|
+
const hostStyle = {
|
|
8
|
+
position: "fixed",
|
|
9
|
+
top: "1rem",
|
|
10
|
+
left: "50%",
|
|
11
|
+
transform: "translateX(-50%)",
|
|
12
|
+
zIndex: 2000,
|
|
13
|
+
display: "flex",
|
|
14
|
+
alignItems: "flex-start",
|
|
15
|
+
gap: "0.75rem",
|
|
16
|
+
maxWidth: "min(32rem, calc(100vw - 2rem))",
|
|
17
|
+
padding: "0.75rem 0.75rem 0.75rem 1rem",
|
|
18
|
+
borderRadius: "8px",
|
|
19
|
+
border: "1px solid var(--color-danger)",
|
|
20
|
+
backgroundColor: "var(--color-danger-soft)",
|
|
21
|
+
boxShadow: "0 4px 16px color-mix(in srgb, var(--color-text) 15%, transparent)",
|
|
22
|
+
};
|
|
23
|
+
const bodyStyle = {
|
|
24
|
+
display: "flex",
|
|
25
|
+
flexDirection: "column",
|
|
26
|
+
gap: "0.25rem",
|
|
27
|
+
};
|
|
28
|
+
const messageStyle = {
|
|
29
|
+
margin: 0,
|
|
30
|
+
fontSize: "0.875rem",
|
|
31
|
+
lineHeight: 1.4,
|
|
32
|
+
color: "var(--color-text)",
|
|
33
|
+
};
|
|
34
|
+
const closeStyle = {
|
|
35
|
+
flexShrink: 0,
|
|
36
|
+
padding: 0,
|
|
37
|
+
width: "1.5rem",
|
|
38
|
+
height: "1.5rem",
|
|
39
|
+
lineHeight: 1,
|
|
40
|
+
fontSize: "1.125rem",
|
|
41
|
+
cursor: "pointer",
|
|
42
|
+
border: "none",
|
|
43
|
+
borderRadius: "4px",
|
|
44
|
+
backgroundColor: "transparent",
|
|
45
|
+
color: "var(--color-danger)",
|
|
46
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { ErrorRulesOptions } from "./errorRules";
|
|
3
|
+
type PopupProviderProps = ErrorRulesOptions & {
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
closeLabel?: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function PopupProvider({ children, closeLabel, ...ruleOptions }: PopupProviderProps): import("react").JSX.Element;
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=PopupProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PopupProvider.d.ts","sourceRoot":"","sources":["../../src/popup/PopupProvider.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEtC,OAAO,KAAK,EAAE,iBAAiB,EAAc,MAAM,cAAc,CAAA;AAMjE,KAAK,kBAAkB,GAAG,iBAAiB,GAAG;IAC5C,QAAQ,EAAE,SAAS,CAAA;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB,CAAA;AAED,wBAAgB,aAAa,CAAC,EAAE,QAAQ,EAAE,UAAU,EAAE,GAAG,WAAW,EAAE,EAAE,kBAAkB,+BAkCzF"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import { createErrorRules } from "./errorRules";
|
|
4
|
+
import { createInterceptedFetch } from "./fetchInterceptor";
|
|
5
|
+
import { PopupHost } from "./PopupHost";
|
|
6
|
+
export function PopupProvider({ children, closeLabel, ...ruleOptions }) {
|
|
7
|
+
const [messages, setMessages] = useState([]);
|
|
8
|
+
// Les options arrivent en littéraux : les garder dans un ref évite de réinstaller
|
|
9
|
+
// l'interception à chaque rendu du parent.
|
|
10
|
+
const rulesRef = useRef(null);
|
|
11
|
+
if (rulesRef.current === null)
|
|
12
|
+
rulesRef.current = createErrorRules(ruleOptions);
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
const original = window.fetch;
|
|
15
|
+
if (original.__popupIntercepted)
|
|
16
|
+
return;
|
|
17
|
+
const intercepted = createInterceptedFetch(original, {
|
|
18
|
+
onRequestStart: () => setMessages(current => (current.length === 0 ? current : [])),
|
|
19
|
+
onError: message => setMessages(current => (current.includes(message) ? current : [...current, message])),
|
|
20
|
+
}, rulesRef.current);
|
|
21
|
+
intercepted.__popupIntercepted = true;
|
|
22
|
+
window.fetch = intercepted;
|
|
23
|
+
return () => {
|
|
24
|
+
window.fetch = original;
|
|
25
|
+
};
|
|
26
|
+
}, []);
|
|
27
|
+
return (_jsxs(_Fragment, { children: [children, _jsx(PopupHost, { messages: messages, onDismiss: () => setMessages([]), closeLabel: closeLabel })] }));
|
|
28
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type PopupMessages = {
|
|
2
|
+
networkFailure: string;
|
|
3
|
+
forbidden: string;
|
|
4
|
+
notFound: string;
|
|
5
|
+
serverError: string;
|
|
6
|
+
requestFailed: (status: number) => string;
|
|
7
|
+
};
|
|
8
|
+
export declare const defaultMessages: PopupMessages;
|
|
9
|
+
export type ErrorRulesOptions = {
|
|
10
|
+
/** Statuses that never raise a popup. Form input errors by default. */
|
|
11
|
+
ignoredStatuses?: number[];
|
|
12
|
+
/** Paths whose 401 is a session probe, not a failure the user should see. */
|
|
13
|
+
authProbePaths?: string[];
|
|
14
|
+
messages?: Partial<PopupMessages>;
|
|
15
|
+
};
|
|
16
|
+
export type ErrorRules = {
|
|
17
|
+
networkFailureMessage: string;
|
|
18
|
+
shouldReportResponse: (res: Response) => boolean;
|
|
19
|
+
shouldReportFailure: (cause: unknown) => boolean;
|
|
20
|
+
describeResponse: (res: Response) => Promise<string>;
|
|
21
|
+
};
|
|
22
|
+
export declare function createErrorRules(options?: ErrorRulesOptions): ErrorRules;
|
|
23
|
+
//# sourceMappingURL=errorRules.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errorRules.d.ts","sourceRoot":"","sources":["../../src/popup/errorRules.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,aAAa,GAAG;IAC1B,cAAc,EAAE,MAAM,CAAA;IACtB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,aAAa,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAA;CAC1C,CAAA;AAED,eAAO,MAAM,eAAe,EAAE,aAM7B,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,uEAAuE;IACvE,eAAe,CAAC,EAAE,MAAM,EAAE,CAAA;IAC1B,6EAA6E;IAC7E,cAAc,CAAC,EAAE,MAAM,EAAE,CAAA;IACzB,QAAQ,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,CAAA;CAClC,CAAA;AAED,MAAM,MAAM,UAAU,GAAG;IACvB,qBAAqB,EAAE,MAAM,CAAA;IAC7B,oBAAoB,EAAE,CAAC,GAAG,EAAE,QAAQ,KAAK,OAAO,CAAA;IAChD,mBAAmB,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAA;IAChD,gBAAgB,EAAE,CAAC,GAAG,EAAE,QAAQ,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;CACrD,CAAA;AAED,wBAAgB,gBAAgB,CAAC,OAAO,GAAE,iBAAsB,GAAG,UAAU,CAkC5E"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export const defaultMessages = {
|
|
2
|
+
networkFailure: "The server is unreachable.",
|
|
3
|
+
forbidden: "You are not allowed to do this.",
|
|
4
|
+
notFound: "The requested resource was not found.",
|
|
5
|
+
serverError: "The server ran into an error.",
|
|
6
|
+
requestFailed: status => `The request failed (${status}).`,
|
|
7
|
+
};
|
|
8
|
+
export function createErrorRules(options = {}) {
|
|
9
|
+
const ignoredStatuses = options.ignoredStatuses ?? [400, 422];
|
|
10
|
+
const authProbePaths = options.authProbePaths ?? ["/auth/me"];
|
|
11
|
+
const messages = { ...defaultMessages, ...options.messages };
|
|
12
|
+
const isAuthProbe = (url) => {
|
|
13
|
+
const matches = (path) => {
|
|
14
|
+
try {
|
|
15
|
+
return new URL(url).pathname.endsWith(path);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return url.includes(path);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
return authProbePaths.some(matches);
|
|
22
|
+
};
|
|
23
|
+
return {
|
|
24
|
+
networkFailureMessage: messages.networkFailure,
|
|
25
|
+
shouldReportResponse(res) {
|
|
26
|
+
if (res.ok)
|
|
27
|
+
return false;
|
|
28
|
+
if (ignoredStatuses.includes(res.status))
|
|
29
|
+
return false;
|
|
30
|
+
if (res.status === 401 && isAuthProbe(res.url))
|
|
31
|
+
return false;
|
|
32
|
+
return true;
|
|
33
|
+
},
|
|
34
|
+
shouldReportFailure(cause) {
|
|
35
|
+
return !(cause instanceof Error && cause.name === "AbortError");
|
|
36
|
+
},
|
|
37
|
+
async describeResponse(res) {
|
|
38
|
+
return (await readBodyMessage(res)) ?? genericMessage(res.status, messages);
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
async function readBodyMessage(res) {
|
|
43
|
+
try {
|
|
44
|
+
const body = (await res.clone().json());
|
|
45
|
+
return typeof body.message === "string" && body.message.trim() !== "" ? body.message.trim() : null;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function genericMessage(status, messages) {
|
|
52
|
+
if (status === 401 || status === 403)
|
|
53
|
+
return messages.forbidden;
|
|
54
|
+
if (status === 404)
|
|
55
|
+
return messages.notFound;
|
|
56
|
+
if (status >= 500)
|
|
57
|
+
return messages.serverError;
|
|
58
|
+
return messages.requestFailed(status);
|
|
59
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ErrorRules } from "./errorRules";
|
|
2
|
+
export type InterceptorHandlers = {
|
|
3
|
+
onRequestStart: () => void;
|
|
4
|
+
onError: (message: string) => void;
|
|
5
|
+
};
|
|
6
|
+
export declare function createInterceptedFetch(original: typeof fetch, handlers: InterceptorHandlers, rules: ErrorRules): typeof fetch;
|
|
7
|
+
//# sourceMappingURL=fetchInterceptor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetchInterceptor.d.ts","sourceRoot":"","sources":["../../src/popup/fetchInterceptor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAE9C,MAAM,MAAM,mBAAmB,GAAG;IAChC,cAAc,EAAE,MAAM,IAAI,CAAA;IAC1B,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CACnC,CAAA;AAED,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,OAAO,KAAK,EACtB,QAAQ,EAAE,mBAAmB,EAC7B,KAAK,EAAE,UAAU,GAChB,OAAO,KAAK,CAYd"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function createInterceptedFetch(original, handlers, rules) {
|
|
2
|
+
return async (input, init) => {
|
|
3
|
+
handlers.onRequestStart();
|
|
4
|
+
try {
|
|
5
|
+
const res = await original(input, init);
|
|
6
|
+
if (rules.shouldReportResponse(res))
|
|
7
|
+
handlers.onError(await rules.describeResponse(res));
|
|
8
|
+
return res;
|
|
9
|
+
}
|
|
10
|
+
catch (cause) {
|
|
11
|
+
if (rules.shouldReportFailure(cause))
|
|
12
|
+
handlers.onError(rules.networkFailureMessage);
|
|
13
|
+
throw cause;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { ThemeStore } from "./store";
|
|
3
|
+
import type { ThemeColors, ThemeDefinition } from "./types";
|
|
4
|
+
type ThemeContextValue = {
|
|
5
|
+
themes: ThemeDefinition[];
|
|
6
|
+
defaultId: string | null;
|
|
7
|
+
currentId: string | null;
|
|
8
|
+
currentColors: ThemeColors;
|
|
9
|
+
selectTheme: (id: string) => void;
|
|
10
|
+
basePalette: ThemeColors;
|
|
11
|
+
store: ThemeStore;
|
|
12
|
+
reload: () => Promise<void>;
|
|
13
|
+
};
|
|
14
|
+
type ThemeProviderProps = {
|
|
15
|
+
children: ReactNode;
|
|
16
|
+
apiUrl?: string;
|
|
17
|
+
store?: ThemeStore;
|
|
18
|
+
basePalette?: ThemeColors;
|
|
19
|
+
selectionKey?: string;
|
|
20
|
+
};
|
|
21
|
+
export declare function ThemeProvider({ children, apiUrl, store, basePalette, selectionKey, }: ThemeProviderProps): import("react").JSX.Element;
|
|
22
|
+
export declare function useTheme(): ThemeContextValue;
|
|
23
|
+
export {};
|
|
24
|
+
//# sourceMappingURL=ThemeProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ThemeProvider.d.ts","sourceRoot":"","sources":["../../src/theme/ThemeProvider.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAGtC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAI3D,KAAK,iBAAiB,GAAG;IACvB,MAAM,EAAE,eAAe,EAAE,CAAA;IACzB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,aAAa,EAAE,WAAW,CAAA;IAC1B,WAAW,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;IACjC,WAAW,EAAE,WAAW,CAAA;IACxB,KAAK,EAAE,UAAU,CAAA;IACjB,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC5B,CAAA;AAID,KAAK,kBAAkB,GAAG;IACxB,QAAQ,EAAE,SAAS,CAAA;IACnB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,UAAU,CAAA;IAClB,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB,CAAA;AAOD,wBAAgB,aAAa,CAAC,EAC5B,QAAQ,EACR,MAAM,EACN,KAAK,EACL,WAA4B,EAC5B,YAA4B,GAC7B,EAAE,kBAAkB,+BA6DpB;AAED,wBAAgB,QAAQ,IAAI,iBAAiB,CAI5C"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
|
3
|
+
import { defaultPalette } from "./palette";
|
|
4
|
+
import { createHttpThemeStore, createLocalThemeStore } from "./store";
|
|
5
|
+
const SELECTION_KEY = "theme-id";
|
|
6
|
+
const ThemeContext = createContext(null);
|
|
7
|
+
function readSelection(key) {
|
|
8
|
+
if (typeof localStorage === "undefined")
|
|
9
|
+
return null;
|
|
10
|
+
return localStorage.getItem(key);
|
|
11
|
+
}
|
|
12
|
+
export function ThemeProvider({ children, apiUrl, store, basePalette = defaultPalette, selectionKey = SELECTION_KEY, }) {
|
|
13
|
+
const resolvedStore = useMemo(() => store ?? (apiUrl ? createHttpThemeStore(apiUrl) : createLocalThemeStore()), [store, apiUrl]);
|
|
14
|
+
const [themes, setThemes] = useState([]);
|
|
15
|
+
const [defaultId, setDefaultId] = useState(null);
|
|
16
|
+
const [selectedId, setSelectedId] = useState(() => readSelection(selectionKey));
|
|
17
|
+
const reload = useCallback(async () => {
|
|
18
|
+
const catalog = await resolvedStore.loadVisible();
|
|
19
|
+
setThemes(catalog.themes);
|
|
20
|
+
setDefaultId(catalog.defaultId);
|
|
21
|
+
}, [resolvedStore]);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
void reload();
|
|
24
|
+
}, [reload]);
|
|
25
|
+
// Un thème devenu invisible ou supprimé ne doit pas laisser le visiteur sur une palette
|
|
26
|
+
// qui n'existe plus : on retombe sur le défaut.
|
|
27
|
+
const currentId = useMemo(() => {
|
|
28
|
+
if (selectedId && themes.some(theme => theme.id === selectedId))
|
|
29
|
+
return selectedId;
|
|
30
|
+
return defaultId;
|
|
31
|
+
}, [selectedId, themes, defaultId]);
|
|
32
|
+
const currentColors = useMemo(() => {
|
|
33
|
+
const current = themes.find(theme => theme.id === currentId);
|
|
34
|
+
return { ...basePalette, ...(current?.colors ?? {}) };
|
|
35
|
+
}, [themes, currentId, basePalette]);
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
for (const [key, value] of Object.entries(currentColors)) {
|
|
38
|
+
document.documentElement.style.setProperty(key, value);
|
|
39
|
+
}
|
|
40
|
+
}, [currentColors]);
|
|
41
|
+
const selectTheme = useCallback((id) => {
|
|
42
|
+
setSelectedId(id);
|
|
43
|
+
if (typeof localStorage !== "undefined")
|
|
44
|
+
localStorage.setItem(selectionKey, id);
|
|
45
|
+
}, [selectionKey]);
|
|
46
|
+
const value = useMemo(() => ({
|
|
47
|
+
themes,
|
|
48
|
+
defaultId,
|
|
49
|
+
currentId,
|
|
50
|
+
currentColors,
|
|
51
|
+
selectTheme,
|
|
52
|
+
basePalette,
|
|
53
|
+
store: resolvedStore,
|
|
54
|
+
reload,
|
|
55
|
+
}), [themes, defaultId, currentId, currentColors, selectTheme, basePalette, resolvedStore, reload]);
|
|
56
|
+
return _jsx(ThemeContext.Provider, { value: value, children: children });
|
|
57
|
+
}
|
|
58
|
+
export function useTheme() {
|
|
59
|
+
const ctx = useContext(ThemeContext);
|
|
60
|
+
if (!ctx)
|
|
61
|
+
throw new Error("useTheme must be used within ThemeProvider");
|
|
62
|
+
return ctx;
|
|
63
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
type ThemeSelectProps = {
|
|
3
|
+
ariaLabel?: string;
|
|
4
|
+
className?: string;
|
|
5
|
+
style?: CSSProperties;
|
|
6
|
+
};
|
|
7
|
+
export declare function ThemeSelect({ ariaLabel, className, style }: ThemeSelectProps): import("react").JSX.Element | null;
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=ThemeSelect.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ThemeSelect.d.ts","sourceRoot":"","sources":["../../src/theme/ThemeSelect.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAA;AAG1C,KAAK,gBAAgB,GAAG;IACtB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,aAAa,CAAA;CACtB,CAAA;AAED,wBAAgB,WAAW,CAAC,EAAE,SAAmB,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE,gBAAgB,sCAoBtF"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useTheme } from "./ThemeProvider";
|
|
3
|
+
export function ThemeSelect({ ariaLabel = "Theme", className, style }) {
|
|
4
|
+
const { themes, currentId, selectTheme } = useTheme();
|
|
5
|
+
if (themes.length < 2)
|
|
6
|
+
return null;
|
|
7
|
+
return (_jsx("select", { "aria-label": ariaLabel, className: className, value: currentId ?? "", onChange: event => selectTheme(event.target.value), style: { ...selectStyle, ...style }, children: themes.map(theme => (_jsx("option", { value: theme.id, children: theme.name }, theme.id))) }));
|
|
8
|
+
}
|
|
9
|
+
const selectStyle = {
|
|
10
|
+
padding: "0.3rem 0.5rem",
|
|
11
|
+
borderRadius: "6px",
|
|
12
|
+
border: "1px solid var(--color-border)",
|
|
13
|
+
backgroundColor: "var(--color-bg)",
|
|
14
|
+
color: "var(--color-text)",
|
|
15
|
+
fontSize: "0.8125rem",
|
|
16
|
+
cursor: "pointer",
|
|
17
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ThemeColors } from "./types";
|
|
2
|
+
export declare function serializeColors(colors: ThemeColors): string;
|
|
3
|
+
export declare function parseColors(text: string): ThemeColors;
|
|
4
|
+
export declare function isHexColor(value: string): boolean;
|
|
5
|
+
export declare function randomHex(): string;
|
|
6
|
+
//# sourceMappingURL=clipboard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clipboard.d.ts","sourceRoot":"","sources":["../../src/theme/clipboard.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAI1C,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAK3D;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAOrD;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEjD;AAED,wBAAgB,SAAS,IAAI,MAAM,CAOlC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { themeVariableKeys } from "./palette";
|
|
2
|
+
const HEX = /^#[0-9a-fA-F]{6}$/;
|
|
3
|
+
export function serializeColors(colors) {
|
|
4
|
+
const lines = themeVariableKeys
|
|
5
|
+
.filter(key => colors[key])
|
|
6
|
+
.map(key => ` ${key}: ${colors[key]};`);
|
|
7
|
+
return `:root {\n${lines.join("\n")}\n}`;
|
|
8
|
+
}
|
|
9
|
+
export function parseColors(text) {
|
|
10
|
+
const parsed = {};
|
|
11
|
+
for (const key of themeVariableKeys) {
|
|
12
|
+
const match = new RegExp(`${key}\s*:\s*(#[0-9a-fA-F]{6})`).exec(text);
|
|
13
|
+
if (match)
|
|
14
|
+
parsed[key] = match[1];
|
|
15
|
+
}
|
|
16
|
+
return parsed;
|
|
17
|
+
}
|
|
18
|
+
export function isHexColor(value) {
|
|
19
|
+
return HEX.test(value);
|
|
20
|
+
}
|
|
21
|
+
export function randomHex() {
|
|
22
|
+
return ("#" +
|
|
23
|
+
Math.floor(Math.random() * 0xffffff)
|
|
24
|
+
.toString(16)
|
|
25
|
+
.padStart(6, "0"));
|
|
26
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ThemeColors } from "./types";
|
|
2
|
+
export declare const defaultPalette: ThemeColors;
|
|
3
|
+
export type ThemeVariableGroup = {
|
|
4
|
+
label: string;
|
|
5
|
+
variables: Array<{
|
|
6
|
+
key: string;
|
|
7
|
+
label: string;
|
|
8
|
+
}>;
|
|
9
|
+
};
|
|
10
|
+
export declare const themeVariableGroups: ThemeVariableGroup[];
|
|
11
|
+
export declare const themeVariableKeys: string[];
|
|
12
|
+
//# sourceMappingURL=palette.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"palette.d.ts","sourceRoot":"","sources":["../../src/theme/palette.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAE1C,eAAO,MAAM,cAAc,EAAE,WAsB5B,CAAA;AAED,MAAM,MAAM,kBAAkB,GAAG;IAC/B,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CACjD,CAAA;AAED,eAAO,MAAM,mBAAmB,EAAE,kBAAkB,EA+CnD,CAAA;AAED,eAAO,MAAM,iBAAiB,EAAE,MAAM,EAErC,CAAA"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export const defaultPalette = {
|
|
2
|
+
"--color-bg": "#ffffff",
|
|
3
|
+
"--color-surface": "#f8fafc",
|
|
4
|
+
"--color-text": "#1a1a1a",
|
|
5
|
+
"--color-text-muted": "#64748b",
|
|
6
|
+
"--color-primary": "#6366f1",
|
|
7
|
+
"--color-primary-hover": "#4f46e5",
|
|
8
|
+
"--color-on-primary": "#ffffff",
|
|
9
|
+
"--color-primary-soft": "#e0e7ff",
|
|
10
|
+
"--color-border": "#e2e8f0",
|
|
11
|
+
"--color-danger": "#dc2626",
|
|
12
|
+
"--color-danger-soft": "#fee2e2",
|
|
13
|
+
"--color-mindmap-red": "#dc2626",
|
|
14
|
+
"--color-mindmap-red-soft": "#fee2e2",
|
|
15
|
+
"--color-mindmap-orange": "#ea580c",
|
|
16
|
+
"--color-mindmap-orange-soft": "#ffedd5",
|
|
17
|
+
"--color-mindmap-green": "#16a34a",
|
|
18
|
+
"--color-mindmap-green-soft": "#dcfce7",
|
|
19
|
+
"--color-mindmap-blue": "#2563eb",
|
|
20
|
+
"--color-mindmap-blue-soft": "#dbeafe",
|
|
21
|
+
"--color-mindmap-purple": "#9333ea",
|
|
22
|
+
"--color-mindmap-purple-soft": "#f3e8ff",
|
|
23
|
+
};
|
|
24
|
+
export const themeVariableGroups = [
|
|
25
|
+
{
|
|
26
|
+
label: "Surfaces",
|
|
27
|
+
variables: [
|
|
28
|
+
{ key: "--color-bg", label: "Background" },
|
|
29
|
+
{ key: "--color-surface", label: "Surface" },
|
|
30
|
+
{ key: "--color-border", label: "Border" },
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
label: "Text",
|
|
35
|
+
variables: [
|
|
36
|
+
{ key: "--color-text", label: "Text" },
|
|
37
|
+
{ key: "--color-text-muted", label: "Muted text" },
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
label: "Primary",
|
|
42
|
+
variables: [
|
|
43
|
+
{ key: "--color-primary", label: "Primary" },
|
|
44
|
+
{ key: "--color-primary-hover", label: "Primary (hover)" },
|
|
45
|
+
{ key: "--color-on-primary", label: "On primary" },
|
|
46
|
+
{ key: "--color-primary-soft", label: "Primary (soft)" },
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
label: "Danger",
|
|
51
|
+
variables: [
|
|
52
|
+
{ key: "--color-danger", label: "Danger" },
|
|
53
|
+
{ key: "--color-danger-soft", label: "Danger (soft)" },
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
label: "Mindmap",
|
|
58
|
+
variables: [
|
|
59
|
+
{ key: "--color-mindmap-red", label: "Red" },
|
|
60
|
+
{ key: "--color-mindmap-red-soft", label: "Red (soft)" },
|
|
61
|
+
{ key: "--color-mindmap-orange", label: "Orange" },
|
|
62
|
+
{ key: "--color-mindmap-orange-soft", label: "Orange (soft)" },
|
|
63
|
+
{ key: "--color-mindmap-green", label: "Green" },
|
|
64
|
+
{ key: "--color-mindmap-green-soft", label: "Green (soft)" },
|
|
65
|
+
{ key: "--color-mindmap-blue", label: "Blue" },
|
|
66
|
+
{ key: "--color-mindmap-blue-soft", label: "Blue (soft)" },
|
|
67
|
+
{ key: "--color-mindmap-purple", label: "Purple" },
|
|
68
|
+
{ key: "--color-mindmap-purple-soft", label: "Purple (soft)" },
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
];
|
|
72
|
+
export const themeVariableKeys = themeVariableGroups.flatMap(group => group.variables.map(variable => variable.key));
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ThemeCatalog, ThemeDefinition, ThemeDraft } from "./types";
|
|
2
|
+
export type ThemeStore = {
|
|
3
|
+
loadVisible(): Promise<ThemeCatalog>;
|
|
4
|
+
loadAll(): Promise<ThemeCatalog>;
|
|
5
|
+
create(draft: ThemeDraft): Promise<ThemeDefinition>;
|
|
6
|
+
update(id: string, draft: ThemeDraft): Promise<void>;
|
|
7
|
+
setDefault(id: string): Promise<void>;
|
|
8
|
+
};
|
|
9
|
+
export declare function createLocalThemeStore(storageKey?: string): ThemeStore;
|
|
10
|
+
export declare function createHttpThemeStore(apiUrl: string): ThemeStore;
|
|
11
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/theme/store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAExE,MAAM,MAAM,UAAU,GAAG;IACvB,WAAW,IAAI,OAAO,CAAC,YAAY,CAAC,CAAA;IACpC,OAAO,IAAI,OAAO,CAAC,YAAY,CAAC,CAAA;IAChC,MAAM,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,eAAe,CAAC,CAAA;IACnD,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACpD,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACtC,CAAA;AAYD,wBAAgB,qBAAqB,CAAC,UAAU,GAAE,MAAoB,GAAG,UAAU,CA8ClF;AAmBD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAiB/D"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
const CATALOG_KEY = "theme-catalog";
|
|
2
|
+
function emptyCatalog() {
|
|
3
|
+
return { themes: [], defaultId: null };
|
|
4
|
+
}
|
|
5
|
+
function newId() {
|
|
6
|
+
return Math.random().toString(36).slice(2, 10);
|
|
7
|
+
}
|
|
8
|
+
export function createLocalThemeStore(storageKey = CATALOG_KEY) {
|
|
9
|
+
const read = () => {
|
|
10
|
+
if (typeof localStorage === "undefined")
|
|
11
|
+
return emptyCatalog();
|
|
12
|
+
try {
|
|
13
|
+
const raw = localStorage.getItem(storageKey);
|
|
14
|
+
return raw ? JSON.parse(raw) : emptyCatalog();
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return emptyCatalog();
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
const write = (catalog) => {
|
|
21
|
+
if (typeof localStorage === "undefined")
|
|
22
|
+
return;
|
|
23
|
+
localStorage.setItem(storageKey, JSON.stringify(catalog));
|
|
24
|
+
};
|
|
25
|
+
return {
|
|
26
|
+
async loadVisible() {
|
|
27
|
+
const catalog = read();
|
|
28
|
+
return { themes: catalog.themes.filter(theme => theme.visible), defaultId: catalog.defaultId };
|
|
29
|
+
},
|
|
30
|
+
async loadAll() {
|
|
31
|
+
return read();
|
|
32
|
+
},
|
|
33
|
+
async create(draft) {
|
|
34
|
+
const catalog = read();
|
|
35
|
+
const theme = { id: newId(), ...draft };
|
|
36
|
+
catalog.themes.push(theme);
|
|
37
|
+
if (!catalog.defaultId)
|
|
38
|
+
catalog.defaultId = theme.id;
|
|
39
|
+
write(catalog);
|
|
40
|
+
return theme;
|
|
41
|
+
},
|
|
42
|
+
async update(id, draft) {
|
|
43
|
+
const catalog = read();
|
|
44
|
+
const index = catalog.themes.findIndex(theme => theme.id === id);
|
|
45
|
+
if (index === -1)
|
|
46
|
+
return;
|
|
47
|
+
catalog.themes[index] = { id, ...draft };
|
|
48
|
+
write(catalog);
|
|
49
|
+
},
|
|
50
|
+
async setDefault(id) {
|
|
51
|
+
const catalog = read();
|
|
52
|
+
if (!catalog.themes.some(theme => theme.id === id))
|
|
53
|
+
return;
|
|
54
|
+
catalog.defaultId = id;
|
|
55
|
+
write(catalog);
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
async function readJson(url, init) {
|
|
60
|
+
const res = await fetch(url, { credentials: "include", ...init });
|
|
61
|
+
if (!res.ok)
|
|
62
|
+
throw new Error(`${url} -> ${res.status}`);
|
|
63
|
+
return (await res.json());
|
|
64
|
+
}
|
|
65
|
+
async function send(url, method, body) {
|
|
66
|
+
const res = await fetch(url, {
|
|
67
|
+
method,
|
|
68
|
+
credentials: "include",
|
|
69
|
+
headers: { "Content-Type": "application/json" },
|
|
70
|
+
body: JSON.stringify(body),
|
|
71
|
+
});
|
|
72
|
+
if (!res.ok)
|
|
73
|
+
throw new Error(`${url} -> ${res.status}`);
|
|
74
|
+
return res;
|
|
75
|
+
}
|
|
76
|
+
export function createHttpThemeStore(apiUrl) {
|
|
77
|
+
const base = apiUrl.replace(/\/+$/, "");
|
|
78
|
+
return {
|
|
79
|
+
loadVisible: () => readJson(`${base}/themes`),
|
|
80
|
+
loadAll: () => readJson(`${base}/themes/all`),
|
|
81
|
+
async create(draft) {
|
|
82
|
+
const res = await send(`${base}/themes`, "POST", draft);
|
|
83
|
+
return (await res.json());
|
|
84
|
+
},
|
|
85
|
+
async update(id, draft) {
|
|
86
|
+
await send(`${base}/themes/${encodeURIComponent(id)}`, "PUT", draft);
|
|
87
|
+
},
|
|
88
|
+
async setDefault(id) {
|
|
89
|
+
await send(`${base}/themes/default`, "PUT", { id });
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|