@coveord/plasma-mantine 55.7.0 → 55.7.1

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.
Files changed (28) hide show
  1. package/.turbo/turbo-build.log +3 -3
  2. package/.turbo/turbo-test.log +54 -52
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/cjs/components/code-editor/CodeEditor.d.ts +7 -0
  5. package/dist/cjs/components/code-editor/CodeEditor.d.ts.map +1 -1
  6. package/dist/cjs/components/code-editor/CodeEditor.js +6 -3
  7. package/dist/cjs/components/code-editor/CodeEditor.js.map +1 -1
  8. package/dist/cjs/components/modal/Modal.d.ts +2 -2
  9. package/dist/cjs/components/modal/Modal.d.ts.map +1 -1
  10. package/dist/cjs/components/modal/Modal.js.map +1 -1
  11. package/dist/cjs/components/table/use-url-synced-state.d.ts.map +1 -1
  12. package/dist/cjs/components/table/use-url-synced-state.js +29 -19
  13. package/dist/cjs/components/table/use-url-synced-state.js.map +1 -1
  14. package/dist/esm/components/code-editor/CodeEditor.d.ts +7 -0
  15. package/dist/esm/components/code-editor/CodeEditor.d.ts.map +1 -1
  16. package/dist/esm/components/code-editor/CodeEditor.js +4 -2
  17. package/dist/esm/components/code-editor/CodeEditor.js.map +1 -1
  18. package/dist/esm/components/modal/Modal.d.ts +2 -2
  19. package/dist/esm/components/modal/Modal.d.ts.map +1 -1
  20. package/dist/esm/components/modal/Modal.js.map +1 -1
  21. package/dist/esm/components/table/use-url-synced-state.d.ts.map +1 -1
  22. package/dist/esm/components/table/use-url-synced-state.js +26 -18
  23. package/dist/esm/components/table/use-url-synced-state.js.map +1 -1
  24. package/package.json +13 -13
  25. package/src/components/code-editor/CodeEditor.tsx +8 -1
  26. package/src/components/modal/Modal.tsx +2 -2
  27. package/src/components/table/__tests__/use-url-synced-state.unit.spec.ts +189 -138
  28. package/src/components/table/use-url-synced-state.ts +36 -18
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/components/table/use-url-synced-state.ts"],"sourcesContent":["import {Dispatch, SetStateAction, useMemo, useState} from 'react';\n\n/**\n * A search param entry defines the encoded value of a search parameter as `[key, value, alwaysEmit?]`.\n * The third entry is an optional boolean that defaults to `false`.\n * Setting `alwaysEmit` to `true` means any non-nullish value is always written to the search params,\n * even if it matches the initial value. It is also written on initialization.\n */\nexport type SearchParamEntry = [string, string | null | undefined, boolean?];\n\n/**\n * Get the index of the ? in a URL that denotes the start of the \"search\".\n * Performs a nested search for '#/', to detect hash router urls and take the params of the hash in that case.\n *\n * @param url The URL to search.\n * @returns The location of the question mark, or `-1` if not found.\n */\nconst indexOfSearch = (url: string): number => url.indexOf('?', url.indexOf('#/') + 1);\n\n/**\n * Read the **current** search params from `window.location`, with support for detecting React's HashRouter.\n * Also returns a method that will yield the href (string) value, after any changes made on the params object.\n *\n * @returns The `URLSearchParams` instance, and a function that can be used to get an updated href.\n */\nconst getSearchParams = (): URLSearchParams => {\n const href = window.location.href;\n const searchStart = indexOfSearch(href);\n return new URLSearchParams(searchStart < 0 ? undefined : href.substring(searchStart));\n};\n\n/**\n * Apply the search params to the current location, using `replaceState` (no navigation history).\n * Note that only parameters in the `params` argument will be set, any other current params will be removed.\n *\n * @param params The parameters to apply.\n */\nconst applySearchParams = (params: URLSearchParams): void => {\n const currentHref = window.location.href;\n const index = indexOfSearch(currentHref);\n let nextHref = index < 0 ? currentHref : currentHref.substring(0, index);\n if (params.size > 0) {\n nextHref = nextHref.concat('?', params.toString());\n }\n if (nextHref !== currentHref) {\n window.history.replaceState(null, '', nextHref);\n }\n};\n\nexport interface UseUrlSyncedStateOptions<T> {\n /**\n * The initial state to use, if there would be no search params to deserialize from.\n * These values are also treated as defaults, and if the current state matches the initialState,\n * no value will be written to the search params.\n */\n initialState: T | (() => T);\n /**\n * The serializer function is used to determine how the state is translated to url search parameters.\n * Called each time the state changes.\n * Note that the serializer should always return entries for keys it controls, also if the current value is \"unset\" (`null` or empty).\n * This ensures params get removed from the search when they are being unset.\n *\n * @param stateValue The new state value to serialize.\n * @returns An iterable of `[key, value]` to set as url search parameters.\n * @example (filterValue) => [['filter', filterValue]] // ?filter=filterValue\n */\n serializer: (stateValue: T) => Iterable<SearchParamEntry>;\n /**\n * The deserializer function is used to determine how the url parameters influence the initial state.\n * May return a partial state, values that are not deserialed are taken from the `initialState`.\n * Called only once when initializing the state.\n * @param params All the search parameters of the current url.\n * @param initialState The initialState, can be used to take defaults from.\n * @returns The initial state based on the current url.\n * @example (params) => params.get('filter') ?? '',\n */\n deserializer: (params: URLSearchParams, initialState: T) => T;\n /**\n * Whether the state should be synced with the url, defaults to `true`.\n * When set to `false`, the hook behaves just like a regular `useState` hook from react.\n */\n sync?: boolean;\n}\n\nconst getInitialState = <T>(options: UseUrlSyncedStateOptions<T>): T =>\n options.initialState instanceof Function ? options.initialState() : options.initialState;\n\nexport const useUrlSyncedState = <T>(options: UseUrlSyncedStateOptions<T>) => {\n const sync = options.sync !== false;\n const [state, setState] = useState<T>(() => {\n const initialState = getInitialState(options);\n return sync ? options.deserializer(getSearchParams(), initialState) : initialState;\n });\n // Capture the initial state as a map (first render only!), to compare values and see if they should be set to the params.\n const initialStateSerialized = useMemo(() => {\n const stateMap = new Map<string, string>();\n let initialize: URLSearchParams | null = null;\n for (const [key, value, alwaysEmit] of options.serializer(getInitialState(options))) {\n stateMap.set(key, value);\n if (alwaysEmit && value) {\n initialize ??= getSearchParams();\n initialize.set(key, value);\n }\n }\n if (initialize) {\n applySearchParams(initialize);\n }\n return stateMap;\n }, []);\n\n const enhancedSetState = useMemo<Dispatch<SetStateAction<T>>>(() => {\n if (!sync) {\n return setState;\n }\n return (updater: SetStateAction<T>) => {\n setState((old) => {\n const newValue = updater instanceof Function ? updater(old) : updater;\n\n const search = getSearchParams();\n for (const [key, value, alwaysEmit] of options.serializer(newValue)) {\n if (value && (alwaysEmit || !Object.is(initialStateSerialized.get(key), value))) {\n search.set(key, value);\n } else {\n search.delete(key);\n }\n }\n applySearchParams(search);\n\n return newValue;\n });\n };\n }, [sync]);\n\n return [state, enhancedSetState] as const;\n};\n"],"names":["useMemo","useState","indexOfSearch","url","indexOf","getSearchParams","href","window","location","searchStart","URLSearchParams","undefined","substring","applySearchParams","params","currentHref","index","nextHref","size","concat","toString","history","replaceState","getInitialState","options","initialState","Function","useUrlSyncedState","sync","state","setState","deserializer","initialStateSerialized","stateMap","Map","initialize","key","value","alwaysEmit","serializer","set","enhancedSetState","updater","old","newValue","search","Object","is","get","delete"],"mappings":"AAAA,SAAkCA,OAAO,EAAEC,QAAQ,QAAO,QAAQ;AAUlE;;;;;;CAMC,GACD,MAAMC,gBAAgB,CAACC,MAAwBA,IAAIC,OAAO,CAAC,KAAKD,IAAIC,OAAO,CAAC,QAAQ;AAEpF;;;;;CAKC,GACD,MAAMC,kBAAkB;IACpB,MAAMC,OAAOC,OAAOC,QAAQ,CAACF,IAAI;IACjC,MAAMG,cAAcP,cAAcI;IAClC,OAAO,IAAII,gBAAgBD,cAAc,IAAIE,YAAYL,KAAKM,SAAS,CAACH;AAC5E;AAEA;;;;;CAKC,GACD,MAAMI,oBAAoB,CAACC;IACvB,MAAMC,cAAcR,OAAOC,QAAQ,CAACF,IAAI;IACxC,MAAMU,QAAQd,cAAca;IAC5B,IAAIE,WAAWD,QAAQ,IAAID,cAAcA,YAAYH,SAAS,CAAC,GAAGI;IAClE,IAAIF,OAAOI,IAAI,GAAG,GAAG;QACjBD,WAAWA,SAASE,MAAM,CAAC,KAAKL,OAAOM,QAAQ;IACnD;IACA,IAAIH,aAAaF,aAAa;QAC1BR,OAAOc,OAAO,CAACC,YAAY,CAAC,MAAM,IAAIL;IAC1C;AACJ;AAqCA,MAAMM,kBAAkB,CAAIC,UACxBA,QAAQC,YAAY,YAAYC,WAAWF,QAAQC,YAAY,KAAKD,QAAQC,YAAY;AAE5F,OAAO,MAAME,oBAAoB,CAAIH;IACjC,MAAMI,OAAOJ,QAAQI,IAAI,KAAK;IAC9B,MAAM,CAACC,OAAOC,SAAS,GAAG7B,SAAY;QAClC,MAAMwB,eAAeF,gBAAgBC;QACrC,OAAOI,OAAOJ,QAAQO,YAAY,CAAC1B,mBAAmBoB,gBAAgBA;IAC1E;IACA,0HAA0H;IAC1H,MAAMO,yBAAyBhC,QAAQ;QACnC,MAAMiC,WAAW,IAAIC;QACrB,IAAIC,aAAqC;QACzC,KAAK,MAAM,CAACC,KAAKC,OAAOC,WAAW,IAAId,QAAQe,UAAU,CAAChB,gBAAgBC,UAAW;YACjFS,SAASO,GAAG,CAACJ,KAAKC;YAClB,IAAIC,cAAcD,OAAO;gBACrBF,eAAAA,aAAe9B;gBACf8B,WAAWK,GAAG,CAACJ,KAAKC;YACxB;QACJ;QACA,IAAIF,YAAY;YACZtB,kBAAkBsB;QACtB;QACA,OAAOF;IACX,GAAG,EAAE;IAEL,MAAMQ,mBAAmBzC,QAAqC;QAC1D,IAAI,CAAC4B,MAAM;YACP,OAAOE;QACX;QACA,OAAO,CAACY;YACJZ,SAAS,CAACa;gBACN,MAAMC,WAAWF,mBAAmBhB,WAAWgB,QAAQC,OAAOD;gBAE9D,MAAMG,SAASxC;gBACf,KAAK,MAAM,CAAC+B,KAAKC,OAAOC,WAAW,IAAId,QAAQe,UAAU,CAACK,UAAW;oBACjE,IAAIP,SAAUC,CAAAA,cAAc,CAACQ,OAAOC,EAAE,CAACf,uBAAuBgB,GAAG,CAACZ,MAAMC,MAAK,GAAI;wBAC7EQ,OAAOL,GAAG,CAACJ,KAAKC;oBACpB,OAAO;wBACHQ,OAAOI,MAAM,CAACb;oBAClB;gBACJ;gBACAvB,kBAAkBgC;gBAElB,OAAOD;YACX;QACJ;IACJ,GAAG;QAAChB;KAAK;IAET,OAAO;QAACC;QAAOY;KAAiB;AACpC,EAAE"}
1
+ {"version":3,"sources":["../../../../src/components/table/use-url-synced-state.ts"],"sourcesContent":["import {Dispatch, SetStateAction, useMemo, useState} from 'react';\n\n/**\n * A search param entry defines the encoded value of a search parameter as `[key, value, alwaysEmit?]`.\n * The third entry is an optional boolean that defaults to `false`.\n * Setting `alwaysEmit` to `true` means any non-nullish value is always written to the search params,\n * even if it matches the initial value. It is also written on initialization.\n */\nexport type SearchParamEntry = [string, string | null | undefined, boolean?];\n\n/** A URL split into an array of length 4, as [pathname, search, hash, hashSearch] */\ntype UrlParts = [string, string, string, string];\n\nconst slice = Function.prototype.call.bind(Array.prototype.slice) as <T>(\n from: ArrayLike<T>,\n start?: number,\n end?: number,\n) => T[];\n\n/**\n * Split a url into its parts.\n *\n * @param href The url to extract the parts from.\n * @returns The separate parts, all are an empty string if not present.\n */\nconst extractParts = (href: string) => slice(/^([^?#]*)(\\?[^#]*|)(#[^?]*|)(\\?.*|)$/.exec(href ?? ''), 1, 5) as UrlParts;\n\n/**\n * The index of the search parameter to use, e.g. hashSearch for hash routes (hash starts with '#/').\n *\n * @param parts: The url parts, as returned by `extractParts`.\n * @returns The index of the search parameter to use (1 or 3).\n */\nconst searchIndex = (parts: UrlParts): 1 | 3 => (/^#\\//.test(parts[2]) ? 3 : 1);\n\n/**\n * Read the **current** search params from `window.location`, with support for detecting React's HashRouter.\n * Also returns a method that will yield the href (string) value, after any changes made on the params object.\n *\n * @returns The `URLSearchParams` instance, and a function that can be used to get an updated href.\n */\nconst getSearchParams = (): URLSearchParams => {\n const parts = extractParts(window.location.href);\n return new URLSearchParams(parts[searchIndex(parts)]);\n};\n\n/**\n * Apply the search params to the current location, using `replaceState` (no navigation history).\n * Note that only parameters in the `params` argument will be set, any other current params will be removed.\n *\n * @param params The parameters to apply.\n */\nconst applySearchParams = (params: URLSearchParams): void => {\n const currentHref = window.location.href;\n const parts = extractParts(currentHref);\n const search = params.size > 0 ? `?${params.toString()}` : '';\n const index = searchIndex(parts);\n if (parts[index] !== search) {\n parts[index] = search;\n window.history.replaceState(null, '', parts.join(''));\n }\n};\n\nexport interface UseUrlSyncedStateOptions<T> {\n /**\n * The initial state to use, if there would be no search params to deserialize from.\n * These values are also treated as defaults, and if the current state matches the initialState,\n * no value will be written to the search params.\n */\n initialState: T | (() => T);\n /**\n * The serializer function is used to determine how the state is translated to url search parameters.\n * Called each time the state changes.\n * Note that the serializer should always return entries for keys it controls, also if the current value is \"unset\" (`null` or empty).\n * This ensures params get removed from the search when they are being unset.\n *\n * @param stateValue The new state value to serialize.\n * @returns An iterable of `[key, value]` to set as url search parameters.\n * @example (filterValue) => [['filter', filterValue]] // ?filter=filterValue\n */\n serializer: (stateValue: T) => Iterable<SearchParamEntry>;\n /**\n * The deserializer function is used to determine how the url parameters influence the initial state.\n * May return a partial state, values that are not deserialed are taken from the `initialState`.\n * Called only once when initializing the state.\n * @param params All the search parameters of the current url.\n * @param initialState The initialState, can be used to take defaults from.\n * @returns The initial state based on the current url.\n * @example (params) => params.get('filter') ?? '',\n */\n deserializer: (params: URLSearchParams, initialState: T) => T;\n /**\n * Whether the state should be synced with the url, defaults to `true`.\n * When set to `false`, the hook behaves just like a regular `useState` hook from react.\n */\n sync?: boolean;\n}\n\nconst getInitialState = <T>(options: UseUrlSyncedStateOptions<T>): T =>\n options.initialState instanceof Function ? options.initialState() : options.initialState;\n\nexport const useUrlSyncedState = <T>(options: UseUrlSyncedStateOptions<T>) => {\n const sync = options.sync !== false;\n const [state, setState] = useState<T>(() => {\n const initialState = getInitialState(options);\n return sync ? options.deserializer(getSearchParams(), initialState) : initialState;\n });\n // Capture the initial state as a map (first render only!), to compare values and see if they should be set to the params.\n const initialStateSerialized = useMemo(() => {\n const stateMap = new Map<string, string>();\n let initialize: URLSearchParams | null = null;\n let needsApply = false;\n for (const [key, value, alwaysEmit] of options.serializer(getInitialState(options))) {\n stateMap.set(key, value);\n if (sync && alwaysEmit && value) {\n initialize ??= getSearchParams();\n if (!initialize.has(key)) {\n needsApply = true;\n initialize.set(key, value);\n }\n }\n }\n if (needsApply) {\n applySearchParams(initialize);\n }\n return stateMap;\n }, []);\n\n const enhancedSetState = useMemo<Dispatch<SetStateAction<T>>>(() => {\n if (!sync) {\n return setState;\n }\n return (updater: SetStateAction<T>) => {\n setState((old) => {\n const newValue = updater instanceof Function ? updater(old) : updater;\n\n const search = getSearchParams();\n for (const [key, value, alwaysEmit] of options.serializer(newValue)) {\n if (value && (alwaysEmit || !Object.is(initialStateSerialized.get(key), value))) {\n search.set(key, value);\n } else {\n search.delete(key);\n }\n }\n applySearchParams(search);\n\n return newValue;\n });\n };\n }, [sync]);\n\n return [state, enhancedSetState] as const;\n};\n"],"names":["useMemo","useState","slice","Function","prototype","call","bind","Array","extractParts","href","exec","searchIndex","parts","test","getSearchParams","window","location","URLSearchParams","applySearchParams","params","currentHref","search","size","toString","index","history","replaceState","join","getInitialState","options","initialState","useUrlSyncedState","sync","state","setState","deserializer","initialStateSerialized","stateMap","Map","initialize","needsApply","key","value","alwaysEmit","serializer","set","has","enhancedSetState","updater","old","newValue","Object","is","get","delete"],"mappings":"AAAA,SAAkCA,OAAO,EAAEC,QAAQ,QAAO,QAAQ;AAalE,MAAMC,QAAQC,SAASC,SAAS,CAACC,IAAI,CAACC,IAAI,CAACC,MAAMH,SAAS,CAACF,KAAK;AAMhE;;;;;CAKC,GACD,MAAMM,eAAe,CAACC,OAAiBP,MAAM,uCAAuCQ,IAAI,CAACD,QAAQ,KAAK,GAAG;AAEzG;;;;;CAKC,GACD,MAAME,cAAc,CAACC,QAA4B,OAAOC,IAAI,CAACD,KAAK,CAAC,EAAE,IAAI,IAAI;AAE7E;;;;;CAKC,GACD,MAAME,kBAAkB;IACpB,MAAMF,QAAQJ,aAAaO,OAAOC,QAAQ,CAACP,IAAI;IAC/C,OAAO,IAAIQ,gBAAgBL,KAAK,CAACD,YAAYC,OAAO;AACxD;AAEA;;;;;CAKC,GACD,MAAMM,oBAAoB,CAACC;IACvB,MAAMC,cAAcL,OAAOC,QAAQ,CAACP,IAAI;IACxC,MAAMG,QAAQJ,aAAaY;IAC3B,MAAMC,SAASF,OAAOG,IAAI,GAAG,IAAI,CAAC,CAAC,EAAEH,OAAOI,QAAQ,IAAI,GAAG;IAC3D,MAAMC,QAAQb,YAAYC;IAC1B,IAAIA,KAAK,CAACY,MAAM,KAAKH,QAAQ;QACzBT,KAAK,CAACY,MAAM,GAAGH;QACfN,OAAOU,OAAO,CAACC,YAAY,CAAC,MAAM,IAAId,MAAMe,IAAI,CAAC;IACrD;AACJ;AAqCA,MAAMC,kBAAkB,CAAIC,UACxBA,QAAQC,YAAY,YAAY3B,WAAW0B,QAAQC,YAAY,KAAKD,QAAQC,YAAY;AAE5F,OAAO,MAAMC,oBAAoB,CAAIF;IACjC,MAAMG,OAAOH,QAAQG,IAAI,KAAK;IAC9B,MAAM,CAACC,OAAOC,SAAS,GAAGjC,SAAY;QAClC,MAAM6B,eAAeF,gBAAgBC;QACrC,OAAOG,OAAOH,QAAQM,YAAY,CAACrB,mBAAmBgB,gBAAgBA;IAC1E;IACA,0HAA0H;IAC1H,MAAMM,yBAAyBpC,QAAQ;QACnC,MAAMqC,WAAW,IAAIC;QACrB,IAAIC,aAAqC;QACzC,IAAIC,aAAa;QACjB,KAAK,MAAM,CAACC,KAAKC,OAAOC,WAAW,IAAId,QAAQe,UAAU,CAAChB,gBAAgBC,UAAW;YACjFQ,SAASQ,GAAG,CAACJ,KAAKC;YAClB,IAAIV,QAAQW,cAAcD,OAAO;gBAC7BH,eAAAA,aAAezB;gBACf,IAAI,CAACyB,WAAWO,GAAG,CAACL,MAAM;oBACtBD,aAAa;oBACbD,WAAWM,GAAG,CAACJ,KAAKC;gBACxB;YACJ;QACJ;QACA,IAAIF,YAAY;YACZtB,kBAAkBqB;QACtB;QACA,OAAOF;IACX,GAAG,EAAE;IAEL,MAAMU,mBAAmB/C,QAAqC;QAC1D,IAAI,CAACgC,MAAM;YACP,OAAOE;QACX;QACA,OAAO,CAACc;YACJd,SAAS,CAACe;gBACN,MAAMC,WAAWF,mBAAmB7C,WAAW6C,QAAQC,OAAOD;gBAE9D,MAAM3B,SAASP;gBACf,KAAK,MAAM,CAAC2B,KAAKC,OAAOC,WAAW,IAAId,QAAQe,UAAU,CAACM,UAAW;oBACjE,IAAIR,SAAUC,CAAAA,cAAc,CAACQ,OAAOC,EAAE,CAAChB,uBAAuBiB,GAAG,CAACZ,MAAMC,MAAK,GAAI;wBAC7ErB,OAAOwB,GAAG,CAACJ,KAAKC;oBACpB,OAAO;wBACHrB,OAAOiC,MAAM,CAACb;oBAClB;gBACJ;gBACAvB,kBAAkBG;gBAElB,OAAO6B;YACX;QACJ;IACJ,GAAG;QAAClB;KAAK;IAET,OAAO;QAACC;QAAOc;KAAiB;AACpC,EAAE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coveord/plasma-mantine",
3
- "version": "55.7.0",
3
+ "version": "55.7.1",
4
4
  "description": "A Plasma flavoured Mantine theme",
5
5
  "keywords": [
6
6
  "plasma",
@@ -42,20 +42,20 @@
42
42
  "lodash.debounce": "4.0.8",
43
43
  "lodash.defaultsdeep": "4.6.1",
44
44
  "monaco-editor": "0.52.2",
45
- "@coveord/plasma-react-icons": "55.7.0",
46
- "@coveord/plasma-tokens": "55.7.0"
45
+ "@coveord/plasma-react-icons": "55.7.1",
46
+ "@coveord/plasma-tokens": "55.7.1"
47
47
  },
48
48
  "devDependencies": {
49
- "@mantine/carousel": "7.16.0",
50
- "@mantine/code-highlight": "7.16.0",
51
- "@mantine/core": "7.16.0",
52
- "@mantine/dates": "7.16.0",
53
- "@mantine/form": "7.16.0",
54
- "@mantine/hooks": "7.16.0",
55
- "@mantine/modals": "7.16.0",
56
- "@mantine/notifications": "7.16.0",
49
+ "@mantine/carousel": "7.16.1",
50
+ "@mantine/code-highlight": "7.16.1",
51
+ "@mantine/core": "7.16.1",
52
+ "@mantine/dates": "7.16.1",
53
+ "@mantine/form": "7.16.1",
54
+ "@mantine/hooks": "7.16.1",
55
+ "@mantine/modals": "7.16.1",
56
+ "@mantine/notifications": "7.16.1",
57
57
  "@swc/cli": "0.6.0",
58
- "@swc/core": "1.10.7",
58
+ "@swc/core": "1.10.8",
59
59
  "@testing-library/dom": "10.4.0",
60
60
  "@testing-library/jest-dom": "6.6.3",
61
61
  "@testing-library/react": "16.2.0",
@@ -77,7 +77,7 @@
77
77
  "sass": "1.83.4",
78
78
  "tslib": "2.8.1",
79
79
  "typescript": "5.7.3",
80
- "vitest": "2.1.8"
80
+ "vitest": "3.0.5"
81
81
  },
82
82
  "peerDependencies": {
83
83
  "@mantine/carousel": "^7.6.1",
@@ -72,6 +72,12 @@ interface CodeEditorProps
72
72
  * @default 'local'
73
73
  */
74
74
  monacoLoader?: 'cdn' | 'local';
75
+ /**
76
+ * Options to pass to the monaco editor.
77
+ * Currently only supporting [`tabSize`](https://microsoft.github.io/monaco-editor/typedoc/interfaces/editor.IStandaloneEditorConstructionOptions.html#tabSize).
78
+ *
79
+ */
80
+ options?: Pick<monacoEditor.IStandaloneEditorConstructionOptions, 'tabSize'>;
75
81
  }
76
82
 
77
83
  const defaultProps: Partial<CodeEditorProps> = {
@@ -101,6 +107,7 @@ export const CodeEditor: FunctionComponent<CodeEditorProps> = (props) => {
101
107
  maxHeight,
102
108
  disabled,
103
109
  monacoLoader,
110
+ options: {tabSize} = {tabSize: 2},
104
111
  ...others
105
112
  } = useProps('CodeEditor', defaultProps, props);
106
113
  const [loaded, setLoaded] = useState(false);
@@ -230,7 +237,7 @@ export const CodeEditor: FunctionComponent<CodeEditorProps> = (props) => {
230
237
  formatOnPaste: true,
231
238
  fontSize: px(theme.fontSizes.xs) as number,
232
239
  readOnly: disabled,
233
- tabSize: 2,
240
+ tabSize,
234
241
  }}
235
242
  value={_value}
236
243
  onChange={handleChange}
@@ -4,12 +4,12 @@ import {
4
4
  ModalFactory as MantineModalFactory,
5
5
  ModalProps as MantineModalProps,
6
6
  } from '@mantine/core';
7
- import {ModalFooter, ModalFooter as PlasmaModalFooter} from './ModalFooter';
7
+ import {ModalFooter as PlasmaModalFooter} from './ModalFooter';
8
8
 
9
9
  // Need to redeclare the factory to override and add footer to the props type
10
10
  type PlasmaModalFactory = Omit<MantineModalFactory, 'staticComponents'> & {
11
11
  staticComponents: MantineModalFactory['staticComponents'] & {
12
- Footer: typeof ModalFooter;
12
+ Footer: typeof PlasmaModalFooter;
13
13
  };
14
14
  };
15
15
 
@@ -1,118 +1,119 @@
1
1
  import {act, renderHook} from '@test-utils';
2
2
  import {useUrlSyncedState} from '../use-url-synced-state';
3
3
 
4
- describe('useUrlSyncedState', () => {
5
- afterEach(() => {
6
- window.history.replaceState(null, '', '/');
7
- });
8
-
9
- it('serializes the state value as url parameter when the state changes', () => {
10
- const {result} = renderHook(() =>
11
- useUrlSyncedState({
12
- initialState: '',
13
- serializer: (state) => [['key', state]],
14
- deserializer: (params) => params.get('key') ?? '',
15
- }),
16
- );
17
- act(() => result.current[1]('value'));
18
- expect(result.current[0]).toBe('value');
19
- expect(window.location.search).toBe('?key=value');
20
- });
21
-
22
- it('allows to serialize the state value into multiple parameters', () => {
23
- const {result} = renderHook(() =>
24
- useUrlSyncedState({
25
- initialState: new Date(),
26
- serializer: (state) => {
27
- const iso = state.toISOString();
28
- return [
29
- ['date', iso.substring(0, 10)],
30
- ['time', iso.substring(11, 24)],
31
- ];
32
- },
33
- deserializer: (params) =>
34
- new Date(`${params.get('date') ?? '2025-01-01'}T${params.get('time') ?? '00:00:00.000Z'}`),
35
- }),
36
- );
37
- act(() => result.current[1](new Date(Date.UTC(2025, 0, 31, 12, 34, 56, 789))));
38
- expect(window.location.search).toBe('?date=2025-01-31&time=12%3A34%3A56.789Z');
39
- });
40
-
41
- it('removes the parameter from the url if the state serializes to the same value as the initial state', () => {
42
- const {result} = renderHook(() =>
43
- useUrlSyncedState({
44
- initialState: true,
45
- serializer: (state) => [['key', state ? 'true' : 'false']],
46
- deserializer: (params, initialState) =>
47
- params.has('key') ? params.get('key') === 'true' : initialState,
48
- sync: true,
49
- }),
50
- );
51
- act(() => result.current[1](false));
52
- expect(window.location.search).toBe('?key=false');
53
- act(() => result.current[1](true));
54
- expect(window.location.search).toBe('');
55
- });
56
-
57
- it('removes the parameter from the url if the state serializes to the empty string', () => {
58
- const {result} = renderHook(() =>
59
- useUrlSyncedState({
60
- initialState: 'initial',
61
- serializer: (state) => [['key', state]],
62
- deserializer: (params) => params.get('key') ?? '',
63
- }),
64
- );
65
- act(() => result.current[1]('value'));
66
- expect(window.location.search).toBe('?key=value');
67
- act(() => result.current[1](''));
68
- expect(window.location.search).toBe('');
69
- });
70
-
71
- it('does not sync with the url if the sync parameter is set to false', () => {
72
- const {result} = renderHook(() =>
73
- useUrlSyncedState({
74
- initialState: '',
75
- serializer: (state) => [['key', state]],
76
- deserializer: (params) => params.get('key') ?? '',
77
- sync: false,
78
- }),
79
- );
80
- act(() => result.current[1]('value'));
81
- expect(result.current[0]).toBe('value');
82
- expect(window.location.search).toBe('');
83
- });
84
-
85
- it('derives the initial state from the url on first render', () => {
86
- window.history.replaceState(null, '', '?key=value');
87
-
88
- const {result} = renderHook(() =>
89
- useUrlSyncedState({
90
- initialState: 'initial',
91
- serializer: (state) => [['key', state]],
92
- deserializer: (params) => params.get('key') ?? '',
93
- }),
94
- );
95
- expect(result.current[0]).toBe('value');
96
- });
97
-
98
- it('does not derive the initial state from the url on first render if sync option is false', () => {
99
- window.history.replaceState(null, '', '?key=value');
100
-
101
- const {result} = renderHook(() =>
102
- useUrlSyncedState({
103
- initialState: 'initial',
104
- serializer: (state) => [['key', state]],
105
- deserializer: (params) => params.get('key') ?? '',
106
- sync: false,
107
- }),
108
- );
109
- expect(result.current[0]).toBe('initial');
110
- });
111
-
112
- describe('with hash router urls', () => {
113
- it('reads values from the hash parameters', () => {
114
- window.history.replaceState(null, '', '?key=unexpected#/hash/route?key=value');
4
+ /**
5
+ * Split a url into its parts.
6
+ * Note that this method is purposefully different from the implementation,
7
+ * and it splits to an object instead of an Array.
8
+ *
9
+ * @param href The url to extract the parts from.
10
+ * @returns The separate parts, all are an empty string if not present.
11
+ */
12
+ const extractParts = (href: string) =>
13
+ /^(?<pathname>[^?#]*)(?<search>\?[^#]*|)(?<hash>#[^?]*|)(?<hashSearch>\?.*|)$/.exec(href).groups as {
14
+ pathname: string;
15
+ search: string;
16
+ hash: string;
17
+ hashSearch: string;
18
+ };
19
+
20
+ describe.each(['/', '/#', '/#?', '/#hash?with-question=mark', '/sub/path', '/?leave=untouched#/', '/?dev#/sub/path/'])(
21
+ 'useUrlSyncedState with location %s',
22
+ (location) => {
23
+ const locationParts = extractParts(location);
24
+ const isHashRoute = locationParts.hash.startsWith('#/');
25
+
26
+ beforeEach(() => {
27
+ window.history.replaceState(null, '', location);
28
+ });
29
+
30
+ afterEach(() => {
31
+ window.history.replaceState(null, '', '/');
32
+ });
115
33
 
34
+ /**
35
+ * Utility that will set the search to be a specific value, taking into account hash routes.
36
+ *
37
+ * @param search The search value to set.
38
+ */
39
+ const setSearch = (search: string) => {
40
+ const parts = extractParts(location);
41
+ if (isHashRoute) {
42
+ parts.hashSearch = search;
43
+ } else {
44
+ parts.search = search;
45
+ }
46
+ window.history.replaceState(null, '', `${parts.pathname}${parts.search}${parts.hash}${parts.hashSearch}`);
47
+ };
48
+
49
+ /**
50
+ * Utility expect that will automatically check either the hash route search or normal search,
51
+ * and also validates the other part is left as-is, if necessary.
52
+ *
53
+ * @param expectedSearch The expected search
54
+ */
55
+ const expectSearch = (expectedSearch: string) => {
56
+ expect(window.location.pathname).toBe(locationParts.pathname);
57
+ if (isHashRoute) {
58
+ expect(window.location.search).toBe(locationParts.search);
59
+ expect(window.location.hash).toBe(`${locationParts.hash}${expectedSearch}`);
60
+ } else {
61
+ expect(window.location.search).toBe(expectedSearch);
62
+ const hash = `${locationParts.hash}${locationParts.hashSearch}`;
63
+ // "fun" quirk: if the hash is only #, location.hash is empty.
64
+ expect(window.location.hash).toBe(hash === '#' ? '' : hash);
65
+ }
66
+ };
67
+
68
+ it('serializes the state value as url parameter when the state changes', () => {
69
+ const {result} = renderHook(() =>
70
+ useUrlSyncedState({
71
+ initialState: '',
72
+ serializer: (state) => [['key', state]],
73
+ deserializer: (params) => params.get('key') ?? '',
74
+ }),
75
+ );
76
+ act(() => result.current[1]('value'));
77
+ expect(result.current[0]).toBe('value');
78
+ expectSearch('?key=value');
79
+ });
80
+
81
+ it('allows to serialize the state value into multiple parameters', () => {
82
+ const {result} = renderHook(() =>
83
+ useUrlSyncedState({
84
+ initialState: new Date(),
85
+ serializer: (state) => {
86
+ const iso = state.toISOString();
87
+ return [
88
+ ['date', iso.substring(0, 10)],
89
+ ['time', iso.substring(11, 24)],
90
+ ];
91
+ },
92
+ deserializer: (params) =>
93
+ new Date(`${params.get('date') ?? '2025-01-01'}T${params.get('time') ?? '00:00:00.000Z'}`),
94
+ }),
95
+ );
96
+ act(() => result.current[1](new Date(Date.UTC(2025, 0, 31, 12, 34, 56, 789))));
97
+ expectSearch('?date=2025-01-31&time=12%3A34%3A56.789Z');
98
+ });
99
+
100
+ it('removes the parameter from the url if the state serializes to the same value as the initial state', () => {
101
+ const {result} = renderHook(() =>
102
+ useUrlSyncedState({
103
+ initialState: true,
104
+ serializer: (state) => [['key', state ? 'true' : 'false']],
105
+ deserializer: (params, initialState) =>
106
+ params.has('key') ? params.get('key') === 'true' : initialState,
107
+ sync: true,
108
+ }),
109
+ );
110
+ act(() => result.current[1](false));
111
+ expectSearch('?key=false');
112
+ act(() => result.current[1](true));
113
+ expectSearch('');
114
+ });
115
+
116
+ it('removes the parameter from the url if the state serializes to the empty string', () => {
116
117
  const {result} = renderHook(() =>
117
118
  useUrlSyncedState({
118
119
  initialState: 'initial',
@@ -120,48 +121,98 @@ describe('useUrlSyncedState', () => {
120
121
  deserializer: (params) => params.get('key') ?? '',
121
122
  }),
122
123
  );
124
+ act(() => result.current[1]('value'));
125
+ expectSearch('?key=value');
126
+ act(() => result.current[1](''));
127
+ expectSearch('');
128
+ });
129
+
130
+ it('does not sync with the url if the sync parameter is set to false', () => {
131
+ const {result} = renderHook(() =>
132
+ useUrlSyncedState({
133
+ initialState: '',
134
+ serializer: (state) => [['key', state]],
135
+ deserializer: (params) => params.get('key') ?? '',
136
+ sync: false,
137
+ }),
138
+ );
139
+ act(() => result.current[1]('value'));
123
140
  expect(result.current[0]).toBe('value');
141
+ expectSearch('');
124
142
  });
125
143
 
126
- it('serializes the state values to the hash route parameters', () => {
127
- window.history.replaceState(null, '', '?a=untouched#/hash/route');
144
+ it('derives the initial state from the url on first render', () => {
145
+ setSearch('?key=value');
128
146
 
129
147
  const {result} = renderHook(() =>
130
148
  useUrlSyncedState({
131
- initialState: {a: null, b: null},
132
- serializer: (state) => [
133
- ['a', state.a],
134
- ['b', state.b],
135
- ],
136
- deserializer: (params) => ({a: params.get('a') ?? '', b: params.get('b')}),
149
+ initialState: 'initial',
150
+ serializer: (state) => [['key', state]],
151
+ deserializer: (params) => params.get('key') ?? '',
152
+ }),
153
+ );
154
+ expect(result.current[0]).toBe('value');
155
+ });
156
+
157
+ it('does not derive the initial state from the url on first render if sync option is false', () => {
158
+ setSearch('?key=value');
159
+
160
+ const {result} = renderHook(() =>
161
+ useUrlSyncedState({
162
+ initialState: 'initial',
163
+ serializer: (state) => [['key', state]],
164
+ deserializer: (params) => params.get('key') ?? '',
165
+ sync: false,
166
+ }),
167
+ );
168
+ expect(result.current[0]).toBe('initial');
169
+ });
170
+
171
+ it('does not serializes initial state for "always emit" value, if sync option is false', () => {
172
+ const {result} = renderHook(() =>
173
+ useUrlSyncedState({
174
+ initialState: 'initial',
175
+ serializer: (state) => [['key', state, true]],
176
+ deserializer: (params) => params.get('key') ?? '',
177
+ sync: false,
178
+ }),
179
+ );
180
+ expect(result.current[0]).toBe('initial');
181
+ expectSearch('');
182
+ });
183
+
184
+ it('serializes initial state for "always emit" value', () => {
185
+ setSearch('?keep=as-is');
186
+
187
+ const {result} = renderHook(() =>
188
+ useUrlSyncedState({
189
+ initialState: 'initial',
190
+ serializer: (state) => [['key', state, true]],
191
+ deserializer: (params, initial) => params.get('key') ?? initial,
137
192
  }),
138
193
  );
139
- act(() => result.current[1]({a: 'test', b: 'state'}));
140
- expect(result.current[0]).toStrictEqual({a: 'test', b: 'state'});
141
- expect(window.location.search).toBe('?a=untouched');
142
- expect(window.location.hash).toBe('#/hash/route?a=test&b=state');
194
+ expect(result.current[0]).toBe('initial');
195
+ expectSearch('?keep=as-is&key=initial');
143
196
  });
144
197
 
145
- it('removes the state values from the hash route parameters', () => {
146
- window.history.replaceState(null, '', '?a=untouched&b=part-of-search#/hash/route?a=1&b=2');
198
+ it('deserializes initial state for "always emit" value, without changing it', () => {
199
+ setSearch('?text=value');
147
200
 
148
201
  const {result} = renderHook(() =>
149
- useUrlSyncedState<{a: number | null; b: number}>({
150
- initialState: {a: 13, b: 37},
202
+ useUrlSyncedState({
203
+ initialState: {text: 'initial', nr: 42},
151
204
  serializer: (state) => [
152
- ['a', state.a?.toString()],
153
- ['b', state.b?.toString()],
205
+ ['text', state.text, true],
206
+ ['number', String(state.nr), true],
154
207
  ],
155
- deserializer: (params) => ({
156
- a: Number.parseInt(params.get('a') ?? '0', 10),
157
- b: Number.parseInt(params.get('b') ?? '0', 10),
208
+ deserializer: (params, initial) => ({
209
+ text: params.get('text') ?? initial.text,
210
+ nr: params.has('number') ? Number.parseInt(params.get('number'), 10) : initial.nr,
158
211
  }),
159
212
  }),
160
213
  );
161
- act(() => result.current[1]({a: null, b: 37}));
162
- expect(result.current[0]).toStrictEqual({a: null, b: 37});
163
- expect(window.location.search).toBe('?a=untouched&b=part-of-search');
164
- expect(window.location.hash).toBe('#/hash/route');
214
+ expect(result.current[0]).toStrictEqual({text: 'value', nr: 42});
215
+ expectSearch('?text=value&number=42');
165
216
  });
166
- });
167
- });
217
+ },
218
+ );
@@ -8,14 +8,30 @@ import {Dispatch, SetStateAction, useMemo, useState} from 'react';
8
8
  */
9
9
  export type SearchParamEntry = [string, string | null | undefined, boolean?];
10
10
 
11
+ /** A URL split into an array of length 4, as [pathname, search, hash, hashSearch] */
12
+ type UrlParts = [string, string, string, string];
13
+
14
+ const slice = Function.prototype.call.bind(Array.prototype.slice) as <T>(
15
+ from: ArrayLike<T>,
16
+ start?: number,
17
+ end?: number,
18
+ ) => T[];
19
+
20
+ /**
21
+ * Split a url into its parts.
22
+ *
23
+ * @param href The url to extract the parts from.
24
+ * @returns The separate parts, all are an empty string if not present.
25
+ */
26
+ const extractParts = (href: string) => slice(/^([^?#]*)(\?[^#]*|)(#[^?]*|)(\?.*|)$/.exec(href ?? ''), 1, 5) as UrlParts;
27
+
11
28
  /**
12
- * Get the index of the ? in a URL that denotes the start of the "search".
13
- * Performs a nested search for '#/', to detect hash router urls and take the params of the hash in that case.
29
+ * The index of the search parameter to use, e.g. hashSearch for hash routes (hash starts with '#/').
14
30
  *
15
- * @param url The URL to search.
16
- * @returns The location of the question mark, or `-1` if not found.
31
+ * @param parts: The url parts, as returned by `extractParts`.
32
+ * @returns The index of the search parameter to use (1 or 3).
17
33
  */
18
- const indexOfSearch = (url: string): number => url.indexOf('?', url.indexOf('#/') + 1);
34
+ const searchIndex = (parts: UrlParts): 1 | 3 => (/^#\//.test(parts[2]) ? 3 : 1);
19
35
 
20
36
  /**
21
37
  * Read the **current** search params from `window.location`, with support for detecting React's HashRouter.
@@ -24,9 +40,8 @@ const indexOfSearch = (url: string): number => url.indexOf('?', url.indexOf('#/'
24
40
  * @returns The `URLSearchParams` instance, and a function that can be used to get an updated href.
25
41
  */
26
42
  const getSearchParams = (): URLSearchParams => {
27
- const href = window.location.href;
28
- const searchStart = indexOfSearch(href);
29
- return new URLSearchParams(searchStart < 0 ? undefined : href.substring(searchStart));
43
+ const parts = extractParts(window.location.href);
44
+ return new URLSearchParams(parts[searchIndex(parts)]);
30
45
  };
31
46
 
32
47
  /**
@@ -37,13 +52,12 @@ const getSearchParams = (): URLSearchParams => {
37
52
  */
38
53
  const applySearchParams = (params: URLSearchParams): void => {
39
54
  const currentHref = window.location.href;
40
- const index = indexOfSearch(currentHref);
41
- let nextHref = index < 0 ? currentHref : currentHref.substring(0, index);
42
- if (params.size > 0) {
43
- nextHref = nextHref.concat('?', params.toString());
44
- }
45
- if (nextHref !== currentHref) {
46
- window.history.replaceState(null, '', nextHref);
55
+ const parts = extractParts(currentHref);
56
+ const search = params.size > 0 ? `?${params.toString()}` : '';
57
+ const index = searchIndex(parts);
58
+ if (parts[index] !== search) {
59
+ parts[index] = search;
60
+ window.history.replaceState(null, '', parts.join(''));
47
61
  }
48
62
  };
49
63
 
@@ -95,14 +109,18 @@ export const useUrlSyncedState = <T>(options: UseUrlSyncedStateOptions<T>) => {
95
109
  const initialStateSerialized = useMemo(() => {
96
110
  const stateMap = new Map<string, string>();
97
111
  let initialize: URLSearchParams | null = null;
112
+ let needsApply = false;
98
113
  for (const [key, value, alwaysEmit] of options.serializer(getInitialState(options))) {
99
114
  stateMap.set(key, value);
100
- if (alwaysEmit && value) {
115
+ if (sync && alwaysEmit && value) {
101
116
  initialize ??= getSearchParams();
102
- initialize.set(key, value);
117
+ if (!initialize.has(key)) {
118
+ needsApply = true;
119
+ initialize.set(key, value);
120
+ }
103
121
  }
104
122
  }
105
- if (initialize) {
123
+ if (needsApply) {
106
124
  applySearchParams(initialize);
107
125
  }
108
126
  return stateMap;