@fluid-app/portal-sdk 0.1.159 → 0.1.161

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 (78) hide show
  1. package/README.md +2 -196
  2. package/dist/{FluidProvider-Cqf2kmUc.mjs → FluidProvider-B00jTGTH.mjs} +1510 -1624
  3. package/dist/FluidProvider-B00jTGTH.mjs.map +1 -0
  4. package/dist/{FluidProvider-Bc-3uN7M.cjs → FluidProvider-BtGi2jJt.cjs} +1458 -1614
  5. package/dist/FluidProvider-BtGi2jJt.cjs.map +1 -0
  6. package/dist/{MessagingScreen-BGqIn-c2.cjs → MessagingScreen-BRCUJDDZ.cjs} +2 -2
  7. package/dist/{MessagingScreen-CbmuvlH6.mjs → MessagingScreen-BWpSXB8Q.mjs} +2 -2
  8. package/dist/{MessagingScreen-CbmuvlH6.mjs.map → MessagingScreen-BWpSXB8Q.mjs.map} +1 -1
  9. package/dist/{MessagingScreen-CBPjP4du.cjs → MessagingScreen-C_OLIWCC.cjs} +2 -2
  10. package/dist/{MessagingScreen-CBPjP4du.cjs.map → MessagingScreen-C_OLIWCC.cjs.map} +1 -1
  11. package/dist/{MySiteScreen-DSDLDnCN.cjs → MySiteScreen-B90qzZPe.cjs} +2 -2
  12. package/dist/{MySiteScreen-Cl6nuU99.cjs → MySiteScreen-DLuHDXB1.cjs} +2 -2
  13. package/dist/{MySiteScreen-Cl6nuU99.cjs.map → MySiteScreen-DLuHDXB1.cjs.map} +1 -1
  14. package/dist/{MySiteScreen-BT1PBPsH.mjs → MySiteScreen-VaOB-vWk.mjs} +2 -2
  15. package/dist/{MySiteScreen-BT1PBPsH.mjs.map → MySiteScreen-VaOB-vWk.mjs.map} +1 -1
  16. package/dist/ProductsScreen-B8OynxlP.cjs +13 -0
  17. package/dist/{ProductsScreen-4WRrJcvw.mjs → ProductsScreen-CbVSNv1l.mjs} +3 -5
  18. package/dist/ProductsScreen-CbVSNv1l.mjs.map +1 -0
  19. package/dist/{ProductsScreen-BCs3YKVk.cjs → ProductsScreen-D1bw4ZIH.cjs} +3 -5
  20. package/dist/ProductsScreen-D1bw4ZIH.cjs.map +1 -0
  21. package/dist/ProductsScreen-Pq3j09nI.mjs +11 -0
  22. package/dist/{ProfileScreen-BgyrIQdL.mjs → ProfileScreen-BCHljkWD.mjs} +2 -2
  23. package/dist/{ProfileScreen-BgyrIQdL.mjs.map → ProfileScreen-BCHljkWD.mjs.map} +1 -1
  24. package/dist/{ProfileScreen-B0KNWXpV.cjs → ProfileScreen-Bf-lYNzz.cjs} +2 -2
  25. package/dist/{ProfileScreen-DLLLRNB2.cjs → ProfileScreen-ksdtbydb.cjs} +2 -2
  26. package/dist/{ProfileScreen-DLLLRNB2.cjs.map → ProfileScreen-ksdtbydb.cjs.map} +1 -1
  27. package/dist/{ShareablesScreen-ySSwCVSI.cjs → ShareablesScreen-6wgiwi_9.cjs} +3 -5
  28. package/dist/ShareablesScreen-6wgiwi_9.cjs.map +1 -0
  29. package/dist/{ShareablesScreen-B8cmh8JC.mjs → ShareablesScreen-BLCukNTk.mjs} +3 -5
  30. package/dist/{ShareablesScreen-B8cmh8JC.mjs.map → ShareablesScreen-BLCukNTk.mjs.map} +1 -1
  31. package/dist/ShareablesScreen-CM9OH-Nx.mjs +11 -0
  32. package/dist/ShareablesScreen-rmLcUhbL.cjs +13 -0
  33. package/dist/{ShopScreen-DhMo8bvP.cjs → ShopScreen-8KKwwjka.cjs} +3 -3
  34. package/dist/{ShopScreen-DhMo8bvP.cjs.map → ShopScreen-8KKwwjka.cjs.map} +1 -1
  35. package/dist/{ShopScreen-BkvyW0pE.cjs → ShopScreen-CjEbB3Q7.cjs} +2 -2
  36. package/dist/{ShopScreen-BFFGYwdV.mjs → ShopScreen-Dn5LwwYD.mjs} +3 -3
  37. package/dist/{ShopScreen-BFFGYwdV.mjs.map → ShopScreen-Dn5LwwYD.mjs.map} +1 -1
  38. package/dist/{SubscriptionsScreen-RScBCcY0.cjs → SubscriptionsScreen-B16wPAoA.cjs} +2 -2
  39. package/dist/SubscriptionsScreen-B16wPAoA.cjs.map +1 -0
  40. package/dist/{SubscriptionsScreen-CiNR7JUC.mjs → SubscriptionsScreen-B4cTlgDU.mjs} +2 -2
  41. package/dist/SubscriptionsScreen-B4cTlgDU.mjs.map +1 -0
  42. package/dist/{SubscriptionsScreen-DMh-GE6n.cjs → SubscriptionsScreen-nRUMdnx7.cjs} +1 -1
  43. package/dist/{dist-Bg8UyHyM.cjs → dist-lO2OG0T5.cjs} +1 -1
  44. package/dist/{dist-Bg8UyHyM.cjs.map → dist-lO2OG0T5.cjs.map} +1 -1
  45. package/dist/index.cjs +134 -143
  46. package/dist/index.cjs.map +1 -1
  47. package/dist/index.d.cts +444 -655
  48. package/dist/index.d.cts.map +1 -1
  49. package/dist/index.d.mts +444 -655
  50. package/dist/index.d.mts.map +1 -1
  51. package/dist/index.mjs +127 -130
  52. package/dist/index.mjs.map +1 -1
  53. package/dist/{sortable.esm-Cz-CP2N8.mjs → sortable.esm-DSrWP4x9.mjs} +1 -1
  54. package/dist/{sortable.esm-Cz-CP2N8.mjs.map → sortable.esm-DSrWP4x9.mjs.map} +1 -1
  55. package/dist/{use-portal-products-client-BmlUixy4.cjs → use-portal-products-client-BHSBT64s.cjs} +2 -2
  56. package/dist/use-portal-products-client-BHSBT64s.cjs.map +1 -0
  57. package/dist/{use-portal-products-client-DQK9nFxT.mjs → use-portal-products-client-tbqk6XUq.mjs} +2 -2
  58. package/dist/use-portal-products-client-tbqk6XUq.mjs.map +1 -0
  59. package/dist/{use-portal-shareables-api-KVPj0Jfr.mjs → use-portal-shareables-api-B9B4XTjw.mjs} +107 -201
  60. package/dist/use-portal-shareables-api-B9B4XTjw.mjs.map +1 -0
  61. package/dist/{use-portal-shareables-api-D5D6uIJy.cjs → use-portal-shareables-api-MBl0d0eQ.cjs} +106 -206
  62. package/dist/use-portal-shareables-api-MBl0d0eQ.cjs.map +1 -0
  63. package/package.json +13 -13
  64. package/dist/FluidProvider-Bc-3uN7M.cjs.map +0 -1
  65. package/dist/FluidProvider-Cqf2kmUc.mjs.map +0 -1
  66. package/dist/ProductsScreen-4WRrJcvw.mjs.map +0 -1
  67. package/dist/ProductsScreen-BCs3YKVk.cjs.map +0 -1
  68. package/dist/ProductsScreen-BR9TN4So.cjs +0 -48
  69. package/dist/ProductsScreen-Da6eWJ_c.mjs +0 -46
  70. package/dist/ShareablesScreen-DW0wCYOj.mjs +0 -46
  71. package/dist/ShareablesScreen-DbPJOtCF.cjs +0 -48
  72. package/dist/ShareablesScreen-ySSwCVSI.cjs.map +0 -1
  73. package/dist/SubscriptionsScreen-CiNR7JUC.mjs.map +0 -1
  74. package/dist/SubscriptionsScreen-RScBCcY0.cjs.map +0 -1
  75. package/dist/use-portal-products-client-BmlUixy4.cjs.map +0 -1
  76. package/dist/use-portal-products-client-DQK9nFxT.mjs.map +0 -1
  77. package/dist/use-portal-shareables-api-D5D6uIJy.cjs.map +0 -1
  78. package/dist/use-portal-shareables-api-KVPj0Jfr.mjs.map +0 -1
@@ -489,7 +489,7 @@ function createPersister() {
489
489
  });
490
490
  }
491
491
  //#endregion
492
- //#region ../core/src/types/fluidos-api-context.ts
492
+ //#region ../core/src/fluidos-api-context.ts
493
493
  const FluidOsReadApiContext = (0, react.createContext)(null);
494
494
  const FluidOsBuilderApiContext = (0, react.createContext)(null);
495
495
  /** Provider for read-only manifest fetching (used by SDK). */
@@ -710,18 +710,6 @@ function createFetchClient(config) {
710
710
  };
711
711
  }
712
712
  //#endregion
713
- //#region ../../api-clients/fluidos/src/namespaces/fluid_os.ts
714
- /**
715
- * Get active Fluid OS definition
716
- * Retrieve the active Fluid OS definition manifest for a specific platform
717
- *
718
- * @param client - Fetch client instance
719
- * @param params - params
720
- */
721
- async function getFluidOSManifest(client, params) {
722
- return client.get(`/api/fluid_os/definitions/active`, params);
723
- }
724
- //#endregion
725
713
  //#region src/client/types.ts
726
714
  /**
727
715
  * HTTP methods supported by the API client.
@@ -735,1807 +723,1705 @@ const HTTP_METHODS = {
735
723
  DELETE: "DELETE"
736
724
  };
737
725
  //#endregion
738
- //#region ../core/src/theme/types.ts
739
- const SEMANTIC_COLOR_NAMES = [
740
- "background",
741
- "foreground",
742
- "primary",
743
- "secondary",
744
- "accent",
745
- "muted",
746
- "destructive"
747
- ];
748
- const SHADE_STEPS = [
749
- 100,
750
- 200,
751
- 300,
752
- 400,
753
- 500,
754
- 600,
755
- 700,
756
- 800,
757
- 900
758
- ];
759
- const FONT_SIZE_KEYS = [
760
- "extraSmall",
761
- "small",
762
- "regular",
763
- "large",
764
- "extraLarge",
765
- "giant"
766
- ];
767
- const FONT_FAMILY_KEYS = ["header", "body"];
768
- const RADIUS_KEYS = [
769
- "small",
770
- "medium",
771
- "large",
772
- "extraLarge"
773
- ];
774
- //#endregion
775
- //#region ../core/src/theme/color-engine.ts
726
+ //#region src/client/fluid-client.ts
776
727
  /**
777
- * Attempt to convert any string into a Color using colorjs.io.
778
- * If the string is exactly 6 characters it is assumed to be a bare hex value
779
- * (e.g. "3b82f6") and a "#" prefix is added before parsing.
780
- *
781
- * @returns the parsed Color, or a neutral gray (`oklch(0.5 0 0)`) on failure
728
+ * Fluid API Client
729
+ * Adapted from: packages/fluidos-api-client/src/lib/fetch-client.ts
730
+ * Provides authenticated API access with domain-specific methods
782
731
  */
783
- function parseColor(value) {
784
- if (value.length === 6) value = `#${value}`;
785
- try {
786
- return new colorjs_io.default(value);
787
- } catch (error) {
788
- console.warn("[theme] Failed to parse color:", value, error);
789
- return new colorjs_io.default("oklch", [
790
- .5,
791
- 0,
792
- 0
793
- ]);
732
+ /**
733
+ * API Error class for structured error handling
734
+ */
735
+ var ApiError = class ApiError extends Error {
736
+ status;
737
+ data;
738
+ constructor(message, status, data) {
739
+ super(message);
740
+ this.name = "ApiError";
741
+ this.status = status;
742
+ this.data = data;
743
+ const errorWithCapture = Error;
744
+ if (errorWithCapture.captureStackTrace) errorWithCapture.captureStackTrace(this, ApiError);
794
745
  }
795
- }
746
+ toJSON() {
747
+ return {
748
+ name: this.name,
749
+ message: this.message,
750
+ status: this.status,
751
+ data: this.data
752
+ };
753
+ }
754
+ };
796
755
  /**
797
- * Returns either the original foreground or a corrected lightness variant,
798
- * whichever provides better contrast against `color`.
799
- * Inversion triggers when the APCA contrast is below 50.
756
+ * Type guard for ApiError
800
757
  */
801
- function getForegroundColor(foreground, color) {
802
- if (foreground.oklch.l == null || color.oklch.l == null) return foreground;
803
- if (color.contrastAPCA(foreground) < 50) return new colorjs_io.default("oklch", [
804
- color.oklch.l < .7 ? .95 : .15,
805
- foreground.oklch.c || 0,
806
- foreground.oklch.h || 0
807
- ]);
808
- return foreground;
758
+ function isApiError(error) {
759
+ return error instanceof ApiError;
809
760
  }
810
761
  /**
811
- * Generate a 100–900 shade ramp from a base color.
812
- * Base anchors at 500. Light shades (100–400) step toward white,
813
- * dark shades (600–900) step toward black. Dark steps use an asymmetric
814
- * multiplier (1.6×, 1.875×, 3×, 4× of `darkStep`) for a more gradual
815
- * initial descent. Chroma is nudged per step for perceptually natural ramps.
762
+ * Type guard to check if a value is a non-null string
816
763
  */
817
- function generateShades(base) {
818
- const l = base.oklch.l ?? 0;
819
- const c = base.oklch.c ?? 0;
820
- const h = base.oklch.h ?? 0;
821
- const safeMax = l >= .885 ? .995 : .97;
822
- const safeMin = l <= .33 ? 0 : .21;
823
- const lightStep = (safeMax - l) / 5;
824
- const darkStep = -(l - safeMin) / 8;
825
- const shade = (lDelta, cDelta) => {
826
- return new colorjs_io.default("oklch", [
827
- Math.max(0, Math.min(1, l + lDelta)),
828
- c <= .001 ? c : Math.max(0, c + cDelta),
829
- h
830
- ]);
831
- };
832
- return {
833
- 100: shade(5 * lightStep, -.00375),
834
- 200: shade(4 * lightStep, -.00375),
835
- 300: shade(3 * lightStep, -.00375),
836
- 400: shade(2 * lightStep, -.00375),
837
- 500: new colorjs_io.default("oklch", [
838
- l,
839
- c,
840
- h
841
- ]),
842
- 600: shade(1.6 * darkStep, .025),
843
- 700: shade(1.875 * 2 * darkStep, .05),
844
- 800: shade(6 * darkStep, .075),
845
- 900: shade(8 * darkStep, .1)
846
- };
847
- }
848
- const DARK_DERIVATION_CONFIG = {
849
- background: {
850
- baseLightness: .15,
851
- fgLightness: .93
852
- },
853
- foreground: {
854
- baseLightness: .93,
855
- fgLightness: .15
856
- },
857
- muted: {
858
- baseLightness: .22,
859
- fgLightness: .75
860
- },
861
- primary: {
862
- baseLightness: "invert",
863
- fgLightness: .95,
864
- chromaScale: .9
865
- },
866
- secondary: {
867
- baseLightness: "invert",
868
- fgLightness: .93,
869
- chromaScale: .85
870
- },
871
- accent: {
872
- baseLightness: "invert",
873
- fgLightness: .95,
874
- chromaScale: .9
875
- },
876
- destructive: {
877
- baseLightness: "invert",
878
- fgLightness: .95,
879
- chromaScale: .95
880
- }
881
- };
882
- /** Invert OKLCH lightness (1 - l), clamped to [0.35, 0.75] to avoid extremes. */
883
- function invertLightness(l) {
884
- const inverted = 1 - l;
885
- return Math.max(.35, Math.min(.75, inverted));
764
+ function isString(value) {
765
+ return typeof value === "string";
886
766
  }
887
767
  /**
888
- * Derive a dark-mode ThemeColorInput from its light-mode counterpart.
768
+ * Extract error message from API response data using `in` operator narrowing.
769
+ * Checks common error message field names in order of precedence.
889
770
  */
890
- function deriveDarkVariant(name, light) {
891
- const config = DARK_DERIVATION_CONFIG[name];
892
- const chromaScale = config.chromaScale ?? 1;
893
- const baseLightness = config.baseLightness === "invert" ? invertLightness(light.base.oklch.l ?? 0) : config.baseLightness;
894
- const fgLightness = config.fgLightness === "invert" ? invertLightness(light.foreground.oklch.l ?? 0) : config.fgLightness;
895
- return {
896
- base: new colorjs_io.default("oklch", [
897
- baseLightness,
898
- (light.base.oklch.c || 0) * chromaScale,
899
- light.base.oklch.h || 0
900
- ]),
901
- foreground: new colorjs_io.default("oklch", [
902
- fgLightness,
903
- (light.foreground.oklch.c || 0) * chromaScale,
904
- light.foreground.oklch.h || 0
905
- ])
906
- };
771
+ function extractErrorMessage(data, fallback) {
772
+ if ("message" in data && isString(data.message)) return data.message;
773
+ if ("error_message" in data && isString(data.error_message)) return data.error_message;
774
+ if ("error" in data && isString(data.error)) return data.error;
775
+ return fallback;
907
776
  }
908
777
  /**
909
- * Merge auto-derived dark colors with any user-specified overrides.
910
- * For each semantic color, if the user has fully overridden both base and
911
- * foreground those are used; otherwise the missing channels are derived.
778
+ * Type guard to detect whether a parsed JSON value is an API envelope.
779
+ * Envelopes always have numeric `status` and a `data` key.
912
780
  */
913
- function mergeDarkOverrides(def) {
914
- const darkColors = {};
915
- for (const name of SEMANTIC_COLOR_NAMES) {
916
- const lightInput = def.light[name];
917
- const darkOverride = def.dark[name];
918
- if (darkOverride?.base && darkOverride?.foreground) darkColors[name] = darkOverride;
919
- else if (darkOverride) {
920
- const base = darkOverride.base ?? deriveDarkVariant(name, lightInput).base;
921
- darkColors[name] = {
922
- base,
923
- foreground: darkOverride.foreground ?? getForegroundColor(def.light.foreground.base, base)
924
- };
925
- } else darkColors[name] = deriveDarkVariant(name, lightInput);
926
- }
927
- return darkColors;
928
- }
929
- function resolveColorSet(colors) {
930
- const resolved = {};
931
- for (const name of SEMANTIC_COLOR_NAMES) {
932
- const input = colors[name];
933
- const shades = generateShades(input.base);
934
- const resolvedShades = {};
935
- for (const step of SHADE_STEPS) resolvedShades[step] = shades[step];
936
- resolved[name] = {
937
- base: input.base.clone(),
938
- foreground: input.foreground.clone(),
939
- shades: resolvedShades
940
- };
941
- }
942
- return resolved;
781
+ function isApiEnvelope(value) {
782
+ return typeof value === "object" && value !== null && "status" in value && typeof value.status === "number" && "data" in value;
943
783
  }
944
784
  /**
945
- * Resolve a ThemeDefinition into a complete ResolvedTheme.
946
- * Dark mode colors are derived from light where not overridden.
785
+ * Creates a configured Fluid API client instance
947
786
  */
948
- function resolveTheme(def) {
949
- return {
950
- id: def.id,
951
- name: def.name,
952
- light: resolveColorSet(def.light),
953
- dark: resolveColorSet(mergeDarkOverrides(def)),
954
- fontSizes: { ...def.fontSizes },
955
- fontFamilies: { ...def.fontFamilies },
956
- spacing: def.spacing,
957
- radii: { ...def.radii }
958
- };
959
- }
960
- //#endregion
961
- //#region ../core/src/theme/tailwind-overrides.ts
962
- /**
963
- * Specific overrides, otherwise all the overrides are generated using emitTailwindOverrides
964
- */
965
- const OVERRIDES = {
966
- "--color-gray-50": "var(--color-muted)",
967
- "--color-gray-100": "var(--color-muted-600)",
968
- "--color-gray-200": "var(--color-border)"
969
- };
970
- /**
971
- * Returns the inverted shade for dark mode foreground colors.
972
- * In dark mode, light shades (50, 100) should map to dark values (950, 900) and vice versa.
973
- */
974
- function getInvertedStep(shade) {
975
- const shadeIndex = SHADE_STEPS.indexOf(shade);
976
- return SHADE_STEPS[SHADE_STEPS.length - 1 - shadeIndex] || 500;
977
- }
978
- /**
979
- * Map semantic colors to Tailwind built-in color names.
980
- */
981
- function emitTailwindOverrides(darkMode = false) {
982
- const TAILWIND_COLOR_MAP = {
983
- gray: "foreground",
984
- red: "destructive",
985
- blue: "primary",
986
- green: "accent"
987
- };
988
- const TAILWIND_SHADES = [
989
- 50,
990
- 100,
991
- 200,
992
- 300,
993
- 400,
994
- 500,
995
- 600,
996
- 700,
997
- 800,
998
- 900,
999
- 950
1000
- ];
1001
- const SHADE_REMAP = {
1002
- 50: 100,
1003
- 950: 900
1004
- };
1005
- const lines = [];
1006
- for (const [twName, semantic] of Object.entries(TAILWIND_COLOR_MAP)) for (const shade of TAILWIND_SHADES) {
1007
- const step = SHADE_REMAP[shade] ?? shade;
1008
- const override = OVERRIDES[`--color-${twName}-${shade}`];
1009
- lines.push(`--color-${twName}-${shade}: ${override ? override : `var(--color-${semantic}-${semantic === "foreground" && darkMode === true ? getInvertedStep(step) : step})`};`);
1010
- }
1011
- lines.push("--color-white: var(--color-background);");
1012
- lines.push("--color-black: var(--color-foreground);");
1013
- return lines;
1014
- }
1015
- //#endregion
1016
- //#region ../core/src/theme/css-generator.ts
1017
- function colorToCSS(color) {
1018
- const result = color.toString({ format: "oklch" });
1019
- if (result.includes("NaN")) {
1020
- console.warn("[theme] colorToCSS produced NaN, using neutral fallback:", result);
1021
- return "oklch(0.5 0 0)";
1022
- }
1023
- return result;
1024
- }
1025
- function camelToKebab(str) {
1026
- return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
1027
- }
1028
- /**
1029
- * Emit --color-{name}, --color-{name}-foreground, --color-{name}-{shade} vars.
1030
- * Uses --color- prefix to match portal-widgets/tailwind.config.ts.
1031
- */
1032
- function emitColorVars(colors) {
1033
- const lines = [];
1034
- for (const name of SEMANTIC_COLOR_NAMES) {
1035
- const color = colors[name];
1036
- lines.push(`--color-${name}: ${colorToCSS(color.base)};`);
1037
- lines.push(`--color-${name}-foreground: ${colorToCSS(color.foreground)};`);
1038
- for (const step of SHADE_STEPS) lines.push(`--color-${name}-${step}: ${colorToCSS(color.shades[step])};`);
1039
- }
1040
- return lines;
1041
- }
1042
- /**
1043
- * Format a font family value for CSS output.
1044
- * - If the value starts with "var(" (legacy), pass through as-is
1045
- * - If the value already contains a comma (has fallback), pass through as-is
1046
- * - Otherwise, wrap in quotes and append a generic sans-serif fallback
1047
- */
1048
- function formatFontFamily(value) {
1049
- if (value.startsWith("var(")) return value;
1050
- if (value.includes(",")) return value;
1051
- return `'${value}', sans-serif`;
1052
- }
1053
- /**
1054
- * Emit non-color CSS variables (font sizes, families, spacing, radii).
1055
- */
1056
- function emitNonColorVars(theme) {
1057
- const lines = [];
1058
- for (const key of FONT_SIZE_KEYS) lines.push(`--font-size-${camelToKebab(key)}: ${theme.fontSizes[key]};`);
1059
- for (const key of FONT_FAMILY_KEYS) lines.push(`--font-${key}: ${formatFontFamily(theme.fontFamilies[key])};`);
1060
- lines.push(`--spacing: ${theme.spacing};`);
1061
- for (const key of RADIUS_KEYS) lines.push(`--radius-${camelToKebab(key)}: ${theme.radii[key]};`);
1062
- return lines;
1063
- }
1064
- /**
1065
- * Static CSS alias variables that bridge theme var names to Tailwind/component conventions.
1066
- * These are always emitted and not mode-dependent.
1067
- */
1068
- const globalCSSOverride = [
1069
- "--color-background-foreground: var(--color-foreground);",
1070
- "--color-foreground-foreground: var(--color-background);",
1071
- "--color-contrast: var(--color-foreground);",
1072
- ...SEMANTIC_COLOR_NAMES.map((value) => `--${value}: var(--color-${value});`),
1073
- ...SEMANTIC_COLOR_NAMES.map((value) => `--${value}-foreground: var(--color-${value}-foreground);`),
1074
- "--sidebar-ring: var(--color-primary);",
1075
- "--sidebar-border: var(--color-border);",
1076
- "--sidebar-accent-foreground: var(--color-accent-foreground);",
1077
- "--sidebar-accent: var(--color-accent);",
1078
- "--sidebar-primary-foreground: var(--color-primary-foreground);",
1079
- "--sidebar-primary: var(--color-primary);",
1080
- "--sidebar-foreground: var(--color-muted-foreground);",
1081
- "--sidebar: var(--color-muted);",
1082
- "--border: var(--color-background-600);",
1083
- "--ring: var(--color-primary);",
1084
- "--popover: var(--color-background);",
1085
- "--popover-foreground: var(--color-foreground);",
1086
- "--card: var(--color-muted);",
1087
- "--card-foreground: var(--color-muted-foreground);",
1088
- "--radius-sm: var(--radius-small);",
1089
- "--radius-md: var(--radius-medium);",
1090
- "--radius-lg: var(--radius-large);",
1091
- "--radius-xl: var(--radius-extra-large);",
1092
- "--text-xs: var(--font-size-extra-small);",
1093
- "--text-sm: var(--font-size-small);",
1094
- "--text-base: var(--font-size-regular);",
1095
- "--text-lg: var(--font-size-large);",
1096
- "--text-xl: var(--font-size-extra-large);",
1097
- "--text-2xl: var(--font-size-giant);"
1098
- ];
1099
- /**
1100
- * Overrides for global tailwindcss for specifically dark mode.
1101
- */
1102
- const globalDarkCSSOverride = ["--border: var(--color-background-400);"];
1103
- /**
1104
- * Generate a complete CSS string for a resolved theme.
1105
- * Outputs 2–3 blocks: light default, dark explicit via `[data-theme-mode="dark"]`,
1106
- * and (unless `disableAutoTheme`) a `prefers-color-scheme: dark` media query block.
1107
- */
1108
- function generateThemeCSS(theme, options = {}) {
1109
- const sel = `[data-theme="${theme.id}"]`;
1110
- const tw = options.mapTailwindColors ?? true;
1111
- const blocks = [];
1112
- blocks.push(`${sel} {`);
1113
- blocks.push(...globalCSSOverride);
1114
- blocks.push(...emitNonColorVars(theme));
1115
- blocks.push(...emitColorVars(theme.light));
1116
- if (tw) blocks.push(...emitTailwindOverrides());
1117
- blocks.push(`}`);
1118
- blocks.push(`${sel}[data-theme-mode="dark"] {`);
1119
- blocks.push(...globalDarkCSSOverride);
1120
- blocks.push(...emitColorVars(theme.dark));
1121
- if (tw) blocks.push(...emitTailwindOverrides(true));
1122
- blocks.push(`}`);
1123
- if (!options.disableAutoTheme) {
1124
- blocks.push(`@media (prefers-color-scheme: dark) {`);
1125
- blocks.push(`${sel}:not([data-theme-mode]) {`);
1126
- blocks.push(...globalDarkCSSOverride);
1127
- blocks.push(...emitColorVars(theme.dark).map((l) => `${l}`));
1128
- if (tw) blocks.push(...emitTailwindOverrides(true).map((l) => `${l}`));
1129
- blocks.push(`}`);
1130
- blocks.push(`}`);
1131
- }
1132
- return blocks.join("\n");
1133
- }
1134
- //#endregion
1135
- //#region ../core/src/theme/defaults.ts
1136
- const DEFAULT_FONT_SIZES = {
1137
- extraSmall: "0.75rem",
1138
- small: "0.875rem",
1139
- regular: "1rem",
1140
- large: "1.125rem",
1141
- extraLarge: "1.25rem",
1142
- giant: "1.5rem"
1143
- };
1144
- const DEFAULT_FONT_FAMILIES = {
1145
- header: "Inter",
1146
- body: "Inter"
1147
- };
1148
- const DEFAULT_SPACING = "0.25rem";
1149
- const DEFAULT_RADII = {
1150
- small: "0.25rem",
1151
- medium: "0.5rem",
1152
- large: "0.75rem",
1153
- extraLarge: "1rem"
1154
- };
1155
- const DEFAULT_COLORS = {
1156
- background: "#ffffff",
1157
- foreground: "#1a1a1a",
1158
- primary: "#3b82f6",
1159
- secondary: "#6b7280",
1160
- accent: "#10b981",
1161
- muted: "#f3f4f6",
1162
- destructive: "#ef4444",
1163
- mutedForeground: "#6b7280"
1164
- };
1165
- const DEFAULT_THEME_ID = "default";
1166
- const DEFAULT_THEME_NAME = "Default Theme";
1167
- /**
1168
- * Build a fresh ThemeDefinition populated with all defaults.
1169
- * Returns a new object each call because Color instances are mutable — do not cache the result.
1170
- */
1171
- function getDefaultThemeDefinition() {
1172
- const bg = new colorjs_io.default(DEFAULT_COLORS.background);
1173
- const fg = new colorjs_io.default(DEFAULT_COLORS.foreground);
1174
- const primary = new colorjs_io.default(DEFAULT_COLORS.primary);
1175
- const secondary = new colorjs_io.default(DEFAULT_COLORS.secondary);
1176
- const accent = new colorjs_io.default(DEFAULT_COLORS.accent);
1177
- const muted = new colorjs_io.default(DEFAULT_COLORS.muted);
1178
- const destructive = new colorjs_io.default(DEFAULT_COLORS.destructive);
1179
- const mutedFg = new colorjs_io.default(DEFAULT_COLORS.mutedForeground);
1180
- const darkBg = new colorjs_io.default("#0a0a0a");
1181
- const darkFg = new colorjs_io.default("#fafafa");
1182
- const darkMuted = new colorjs_io.default("#171717");
1183
- const darkMutedForeground = new colorjs_io.default("#dddddd");
1184
- return {
1185
- id: DEFAULT_THEME_ID,
1186
- name: DEFAULT_THEME_NAME,
1187
- light: {
1188
- background: {
1189
- base: bg,
1190
- foreground: fg
1191
- },
1192
- foreground: {
1193
- base: fg,
1194
- foreground: bg
1195
- },
1196
- primary: {
1197
- base: primary,
1198
- foreground: getForegroundColor(fg, primary)
1199
- },
1200
- secondary: {
1201
- base: secondary,
1202
- foreground: getForegroundColor(fg, secondary)
1203
- },
1204
- accent: {
1205
- base: accent,
1206
- foreground: getForegroundColor(fg, accent)
1207
- },
1208
- muted: {
1209
- base: muted,
1210
- foreground: mutedFg
1211
- },
1212
- destructive: {
1213
- base: destructive,
1214
- foreground: getForegroundColor(fg, destructive)
1215
- }
1216
- },
1217
- dark: {
1218
- background: {
1219
- base: darkBg,
1220
- foreground: darkFg
1221
- },
1222
- foreground: {
1223
- base: darkFg,
1224
- foreground: darkBg
1225
- },
1226
- muted: {
1227
- base: darkMuted,
1228
- foreground: darkMutedForeground
1229
- }
1230
- },
1231
- fontSizes: { ...DEFAULT_FONT_SIZES },
1232
- fontFamilies: { ...DEFAULT_FONT_FAMILIES },
1233
- spacing: DEFAULT_SPACING,
1234
- radii: { ...DEFAULT_RADII }
1235
- };
1236
- }
1237
- //#endregion
1238
- //#region ../core/src/theme/serialisation.ts
1239
- function colorToPlain(color) {
1240
- return {
1241
- l: color.oklch.l ?? 0,
1242
- c: color.oklch.c ?? 0,
1243
- h: color.oklch.h ?? 0
1244
- };
1245
- }
1246
- function plainToColor(plain) {
1247
- return new colorjs_io.default("oklch", [
1248
- plain.l,
1249
- plain.c,
1250
- plain.h
1251
- ]);
1252
- }
1253
- /**
1254
- * Serialise a ThemeDefinition (with Color objects) to a plain JSON payload
1255
- * suitable for backend storage.
1256
- */
1257
- function serialiseTheme(def) {
1258
- const light = {};
1259
- for (const name of SEMANTIC_COLOR_NAMES) light[name] = {
1260
- base: colorToPlain(def.light[name].base),
1261
- foreground: colorToPlain(def.light[name].foreground)
1262
- };
1263
- const dark = {};
1264
- for (const [name, value] of Object.entries(def.dark)) {
1265
- if (!value) continue;
1266
- dark[name] = {
1267
- ...value.base ? { base: colorToPlain(value.base) } : {},
1268
- ...value.foreground ? { foreground: colorToPlain(value.foreground) } : {}
1269
- };
1270
- }
1271
- return {
1272
- id: def.id,
1273
- name: def.name,
1274
- light,
1275
- dark,
1276
- fontSizes: { ...def.fontSizes },
1277
- fontFamilies: { ...def.fontFamilies },
1278
- spacing: def.spacing,
1279
- radii: { ...def.radii },
1280
- ...def.syncWithBrandColors ? { syncWithBrandColors: true } : {}
1281
- };
1282
- }
1283
- /**
1284
- * Deserialise a backend payload into a ThemeDefinition with Color objects.
1285
- * Accepts `Record<string, unknown>` because API data is untyped at the boundary.
1286
- * Falls back to default colors for any missing light-mode entries.
1287
- */
1288
- function deserialiseTheme(payload) {
1289
- const lightRaw = payload.light ?? {};
1290
- const darkRaw = payload.dark ?? {};
1291
- const defaults = getDefaultThemeDefinition();
1292
- const light = {};
1293
- for (const name of SEMANTIC_COLOR_NAMES) {
1294
- const entry = lightRaw[name];
1295
- if (entry) light[name] = {
1296
- base: plainToColor(entry.base),
1297
- foreground: plainToColor(entry.foreground)
1298
- };
1299
- else {
1300
- console.warn(`[theme] deserialiseTheme: missing light color "${name}", using default`);
1301
- light[name] = defaults.light[name];
1302
- }
1303
- }
1304
- const dark = {};
1305
- for (const [name, value] of Object.entries(darkRaw)) {
1306
- if (!value) continue;
1307
- dark[name] = {
1308
- ...value.base ? { base: plainToColor(value.base) } : {},
1309
- ...value.foreground ? { foreground: plainToColor(value.foreground) } : {}
1310
- };
1311
- }
1312
- return {
1313
- id: payload.id,
1314
- name: payload.name,
1315
- light,
1316
- dark,
1317
- fontSizes: payload.fontSizes ?? DEFAULT_FONT_SIZES,
1318
- fontFamilies: payload.fontFamilies ?? DEFAULT_FONT_FAMILIES,
1319
- spacing: payload.spacing ?? "0.25rem",
1320
- radii: payload.radii ?? DEFAULT_RADII,
1321
- ...payload.syncWithBrandColors === true ? { syncWithBrandColors: true } : {}
1322
- };
1323
- }
1324
- //#endregion
1325
- //#region ../core/src/theme/transforms.ts
1326
- /**
1327
- * Check if a theme config uses the new structured format (has a `light` key
1328
- * that is an object) vs the legacy flat format.
1329
- */
1330
- function isNewThemeFormat(config) {
1331
- return config.light != null && typeof config.light === "object";
1332
- }
1333
- /**
1334
- * Convert a legacy flat config to a ThemeDefinition.
1335
- * Legacy format: { base: "#fff", text: "#000", primary: "oklch(0.6 0.2 250)", ... }
1336
- */
1337
- function legacyConfigToDefinition(id, name, config) {
1338
- const bg = parseColor(config.base ?? config.background ?? DEFAULT_COLORS.background);
1339
- const fg = parseColor(config.text ?? config.foreground ?? DEFAULT_COLORS.foreground);
1340
- const primary = parseColor(config.primary ?? DEFAULT_COLORS.primary);
1341
- const secondary = parseColor(config.secondary ?? DEFAULT_COLORS.secondary);
1342
- const accent = parseColor(config.accent ?? DEFAULT_COLORS.accent);
1343
- const muted = parseColor(config.muted ?? DEFAULT_COLORS.muted);
1344
- const destructive = parseColor(config.destructive ?? DEFAULT_COLORS.destructive);
1345
- const mutedFg = parseColor(config.mutedForeground ?? DEFAULT_COLORS.mutedForeground);
1346
- return {
1347
- id: String(id),
1348
- name,
1349
- light: {
1350
- background: {
1351
- base: bg,
1352
- foreground: fg
1353
- },
1354
- foreground: {
1355
- base: fg,
1356
- foreground: bg
1357
- },
1358
- primary: {
1359
- base: primary,
1360
- foreground: getForegroundColor(fg, primary)
1361
- },
1362
- secondary: {
1363
- base: secondary,
1364
- foreground: getForegroundColor(fg, secondary)
1365
- },
1366
- accent: {
1367
- base: accent,
1368
- foreground: getForegroundColor(fg, accent)
1369
- },
1370
- muted: {
1371
- base: muted,
1372
- foreground: mutedFg
1373
- },
1374
- destructive: {
1375
- base: destructive,
1376
- foreground: getForegroundColor(fg, destructive)
787
+ function createFluidClient(config) {
788
+ const { baseUrl, getAuthToken, onAuthError, defaultHeaders = {} } = config;
789
+ const fetchClient = createFetchClient({
790
+ baseUrl,
791
+ ...getAuthToken ? { getAuthToken } : {},
792
+ onAuthError,
793
+ defaultHeaders,
794
+ credentials: "include"
795
+ });
796
+ /**
797
+ * Build headers for a request.
798
+ * Auth is handled by session cookies via `credentials: 'include'` on fetch calls.
799
+ */
800
+ function buildHeaders(customHeaders) {
801
+ return {
802
+ "Content-Type": "application/json",
803
+ ...defaultHeaders,
804
+ ...customHeaders
805
+ };
806
+ }
807
+ /**
808
+ * Build URL with query parameters (Rails-compatible)
809
+ */
810
+ function buildUrl(endpoint, params) {
811
+ const normalizedBase = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
812
+ const normalizedEndpoint = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
813
+ const url = normalizedBase ? new URL(normalizedBase + normalizedEndpoint) : new URL(normalizedEndpoint, typeof window !== "undefined" ? window.location.origin : "http://localhost");
814
+ if (params) for (const [key, value] of Object.entries(params)) {
815
+ if (value === void 0 || value === null) continue;
816
+ if (Array.isArray(value)) for (const item of value) url.searchParams.append(`${key}[]`, String(item));
817
+ else if (typeof value === "object") for (const [subKey, subValue] of Object.entries(value)) {
818
+ if (subValue === void 0 || subValue === null) continue;
819
+ if (Array.isArray(subValue)) for (const item of subValue) url.searchParams.append(`${key}[${subKey}][]`, String(item));
820
+ else url.searchParams.append(`${key}[${subKey}]`, String(subValue));
1377
821
  }
1378
- },
1379
- dark: {},
1380
- fontSizes: {
1381
- extraSmall: config.extraSmall ?? DEFAULT_FONT_SIZES.extraSmall,
1382
- small: config.small ?? DEFAULT_FONT_SIZES.small,
1383
- regular: config.regular ?? DEFAULT_FONT_SIZES.regular,
1384
- large: config.large ?? DEFAULT_FONT_SIZES.large,
1385
- extraLarge: config.extraLarge ?? DEFAULT_FONT_SIZES.extraLarge,
1386
- giant: config.giant ?? DEFAULT_FONT_SIZES.giant
1387
- },
1388
- fontFamilies: {
1389
- header: config.headerFont ?? DEFAULT_FONT_FAMILIES.header,
1390
- body: config.bodyFont ?? DEFAULT_FONT_FAMILIES.body
1391
- },
1392
- spacing: config.globalSpacing ?? "0.25rem",
1393
- radii: {
1394
- small: config.radiusSmall ?? DEFAULT_RADII.small,
1395
- medium: config.radiusMedium ?? DEFAULT_RADII.medium,
1396
- large: config.radiusLarge ?? DEFAULT_RADII.large,
1397
- extraLarge: config.radiusExtraLarge ?? DEFAULT_RADII.extraLarge
822
+ else url.searchParams.append(key, String(value));
1398
823
  }
1399
- };
1400
- }
1401
- /**
1402
- * Build a ThemeDefinition from a single API theme object.
1403
- * Handles both new structured format and legacy flat format.
1404
- */
1405
- function buildThemeDefinition(theme) {
1406
- const config = theme.config ?? {};
1407
- if (isNewThemeFormat(config)) return deserialiseTheme({
1408
- ...config,
1409
- id: String(theme.id),
1410
- name: theme.name ?? "Untitled Theme"
1411
- });
1412
- return legacyConfigToDefinition(theme.id, theme.name ?? "Untitled Theme", config);
1413
- }
1414
- /**
1415
- * Transform raw API themes to ThemeDefinition[].
1416
- * Catches and logs errors per theme (graceful degradation).
1417
- */
1418
- function transformThemes(themes) {
1419
- return themes.flatMap((theme) => {
824
+ return url.toString();
825
+ }
826
+ /**
827
+ * Default request options for type-safe defaults.
828
+ * Uses `satisfies` to validate against RequestOptions while preserving literal types.
829
+ */
830
+ const defaultRequestOptions = { method: HTTP_METHODS.GET };
831
+ /**
832
+ * Main request function
833
+ */
834
+ async function request(endpoint, options = {}) {
835
+ const { method = defaultRequestOptions.method, headers: customHeaders, params, body, signal } = options;
836
+ const url = buildUrl(endpoint, method === HTTP_METHODS.GET ? params : void 0);
837
+ const headers = buildHeaders(customHeaders);
838
+ let response;
1420
839
  try {
1421
- return [buildThemeDefinition(theme)];
1422
- } catch (error) {
1423
- console.error(`[theme] Failed to build theme id=${theme.id}:`, error);
1424
- return [];
840
+ const fetchOptions = {
841
+ method,
842
+ headers,
843
+ credentials: "include"
844
+ };
845
+ if (signal !== void 0) fetchOptions.signal = signal;
846
+ if (body && method !== HTTP_METHODS.GET) fetchOptions.body = JSON.stringify(body);
847
+ response = await fetch(url, fetchOptions);
848
+ } catch (networkError) {
849
+ throw new ApiError(`Network error: ${networkError instanceof Error ? networkError.message : "Unknown network error"}`, 0, null);
1425
850
  }
1426
- });
1427
- }
1428
- /**
1429
- * Get the active theme ID from a list of raw API themes.
1430
- * Falls back to the first theme if none is marked active.
1431
- */
1432
- function getActiveThemeId(themes) {
1433
- const active = themes.find((t) => t.active) ?? themes[0];
1434
- return active ? String(active.id) : void 0;
1435
- }
1436
- //#endregion
1437
- //#region ../core/src/theme/theme-applicator.ts
1438
- const STYLE_PREFIX = "theme-style-";
1439
- const FONT_LINK_PREFIX = "theme-font-";
1440
- const SYSTEM_FONTS = new Set([
1441
- "sans-serif",
1442
- "serif",
1443
- "monospace",
1444
- "cursive",
1445
- "fantasy",
1446
- "system-ui",
1447
- "ui-sans-serif",
1448
- "ui-serif",
1449
- "ui-monospace"
1450
- ]);
1451
- /** Build a Google Fonts CSS2 URL for a given font family with all weights. */
1452
- function buildGoogleFontUrl(family) {
1453
- return `https://fonts.googleapis.com/css2?family=${encodeURIComponent(family).replace(/%20/g, "+")}:wght@100;200;300;400;500;600;700;800;900&display=swap`;
1454
- }
1455
- /** Check if a font family value needs to be loaded (i.e. is not a CSS var or system font). */
1456
- function isLoadableFont(value) {
1457
- if (!value) return false;
1458
- if (value.startsWith("var(")) return false;
1459
- return !SYSTEM_FONTS.has(value.toLowerCase());
1460
- }
1461
- /** Deterministic link element ID for a font family. */
1462
- function fontLinkId(family) {
1463
- return `${FONT_LINK_PREFIX}${family.replace(/\s+/g, "-").toLowerCase()}`;
1464
- }
1465
- /**
1466
- * Inject or update `<link>` elements for Google Fonts used by the theme.
1467
- * Removes links for fonts that are no longer referenced.
1468
- */
1469
- function loadThemeFonts(theme) {
1470
- if (typeof document === "undefined") return;
1471
- const fontsToLoad = /* @__PURE__ */ new Set();
1472
- for (const key of FONT_FAMILY_KEYS) {
1473
- const value = theme.fontFamilies[key];
1474
- if (isLoadableFont(value)) fontsToLoad.add(value);
1475
- }
1476
- document.querySelectorAll(`link[id^="${FONT_LINK_PREFIX}"]`).forEach((link) => {
1477
- const owners = link.getAttribute("data-font-theme-ids")?.split(",") ?? [];
1478
- if (!owners.includes(theme.id)) return;
1479
- const fontName = link.getAttribute("data-font-family");
1480
- if (fontName && !fontsToLoad.has(fontName)) {
1481
- const remaining = owners.filter((id) => id !== theme.id);
1482
- if (remaining.length === 0) link.remove();
1483
- else link.setAttribute("data-font-theme-ids", remaining.join(","));
851
+ if (response.status === 401) {
852
+ onAuthError?.();
853
+ throw new ApiError("Authentication required", 401, null);
1484
854
  }
1485
- });
1486
- for (const family of fontsToLoad) {
1487
- const id = fontLinkId(family);
1488
- const existing = document.getElementById(id);
1489
- if (existing) {
1490
- const owners = existing.getAttribute("data-font-theme-ids")?.split(",") ?? [];
1491
- if (!owners.includes(theme.id)) existing.setAttribute("data-font-theme-ids", [...owners, theme.id].join(","));
1492
- } else {
1493
- const link = document.createElement("link");
1494
- link.id = id;
1495
- link.rel = "stylesheet";
1496
- link.href = buildGoogleFontUrl(family);
1497
- link.setAttribute("data-font-family", family);
1498
- link.setAttribute("data-font-theme-ids", theme.id);
1499
- document.head.appendChild(link);
855
+ if (!response.ok) try {
856
+ if (response.headers.get("content-type")?.includes("application/json")) {
857
+ const data = await response.json();
858
+ throw new ApiError(extractErrorMessage(data, `${method} request failed`), response.status, "errors" in data ? data.errors : data);
859
+ } else throw new ApiError(`${method} request failed with status ${response.status}`, response.status, null);
860
+ } catch (error) {
861
+ if (isApiError(error)) throw error;
862
+ throw new ApiError(`${method} request failed with status ${response.status}`, response.status, null);
863
+ }
864
+ if (response.status === 204 || response.headers.get("content-length") === "0") return null;
865
+ try {
866
+ const raw = await response.json();
867
+ if (raw === null || raw === void 0) throw new ApiError("Unexpected null/undefined in JSON response", response.status, null);
868
+ return isApiEnvelope(raw) ? raw.data : raw;
869
+ } catch (parseError) {
870
+ if (isApiError(parseError)) throw parseError;
871
+ throw new ApiError("Failed to parse response as JSON", response.status, null);
1500
872
  }
1501
873
  }
1502
- }
1503
- /** Remove all font `<link>` elements injected by the theme system. */
1504
- function removeAllFontLinks() {
1505
- if (typeof document === "undefined") return;
1506
- document.querySelectorAll(`link[id^="${FONT_LINK_PREFIX}"]`).forEach((el) => el.remove());
1507
- }
1508
- /**
1509
- * Inject or update a `<style>` element in `<head>` for the given theme.
1510
- * The element ID is deterministic (`theme-style-{themeId}`) so repeated calls
1511
- * for the same theme are idempotent the existing element is updated in place.
1512
- * Also loads Google Fonts referenced by the theme's font families.
1513
- * No-op when `document` is unavailable (SSR).
1514
- */
1515
- function applyTheme(theme, options) {
1516
- if (typeof document === "undefined") return;
1517
- try {
1518
- loadThemeFonts(theme);
1519
- const styleId = `${STYLE_PREFIX}${theme.id}`;
1520
- let el = document.getElementById(styleId);
1521
- if (!el) {
1522
- el = document.createElement("style");
1523
- el.id = styleId;
1524
- document.head.appendChild(el);
874
+ /**
875
+ * Request function for endpoints that may return null (204 No Content).
876
+ * Properly types the return as T | null.
877
+ */
878
+ async function requestNullable(endpoint, options = {}) {
879
+ return request(endpoint, options);
880
+ }
881
+ /**
882
+ * Safe request wrapper that returns a discriminated union instead of throwing.
883
+ * Use `isApiSuccess` or `isApiFailure` to narrow the result.
884
+ */
885
+ async function safeRequest(endpoint, options = {}) {
886
+ try {
887
+ return {
888
+ success: true,
889
+ data: await request(endpoint, options)
890
+ };
891
+ } catch (error) {
892
+ if (isApiError(error)) return {
893
+ success: false,
894
+ error
895
+ };
896
+ return {
897
+ success: false,
898
+ error: new ApiError(error instanceof Error ? error.message : "Unknown error", 0, null)
899
+ };
1525
900
  }
1526
- el.textContent = generateThemeCSS(theme, options);
1527
- } catch (error) {
1528
- console.error(`[theme] applyTheme failed for "${theme.id}":`, error);
1529
901
  }
1530
- }
1531
- /** Remove an injected theme stylesheet and clean up font link ownership. No-op during SSR. */
1532
- function removeTheme(themeId) {
1533
- if (typeof document === "undefined") return;
1534
- document.getElementById(`${STYLE_PREFIX}${themeId}`)?.remove();
1535
- document.querySelectorAll(`link[id^="${FONT_LINK_PREFIX}"]`).forEach((link) => {
1536
- const remaining = (link.getAttribute("data-font-theme-ids")?.split(",") ?? []).filter((id) => id !== themeId);
1537
- if (remaining.length === 0) link.remove();
1538
- else link.setAttribute("data-font-theme-ids", remaining.join(","));
902
+ /**
903
+ * Helper to safely convert typed params to Record<string, unknown>.
904
+ * Type assertion required: TypeScript's structural typing allows any object
905
+ * to be treated as Record<string, unknown> when we only need to iterate
906
+ * over its entries. This is safe because buildUrl only reads properties.
907
+ */
908
+ function toParams(params) {
909
+ return params;
910
+ }
911
+ const get = (endpoint, params, options) => {
912
+ const baseOptions = {
913
+ ...options,
914
+ method: HTTP_METHODS.GET
915
+ };
916
+ const convertedParams = toParams(params);
917
+ return request(endpoint, convertedParams !== void 0 ? {
918
+ ...baseOptions,
919
+ params: convertedParams
920
+ } : baseOptions);
921
+ };
922
+ const post = (endpoint, body, options) => request(endpoint, {
923
+ ...options,
924
+ method: HTTP_METHODS.POST,
925
+ body
1539
926
  });
1540
- }
1541
- /** Remove all injected theme stylesheets and font links. No-op during SSR. */
1542
- function removeAllThemes() {
1543
- if (typeof document === "undefined") return;
1544
- document.querySelectorAll(`style[id^="${STYLE_PREFIX}"]`).forEach((el) => el.remove());
1545
- removeAllFontLinks();
927
+ const put = (endpoint, body, options) => request(endpoint, {
928
+ ...options,
929
+ method: HTTP_METHODS.PUT,
930
+ body
931
+ });
932
+ const patch = (endpoint, body, options) => request(endpoint, {
933
+ ...options,
934
+ method: HTTP_METHODS.PATCH,
935
+ body
936
+ });
937
+ const del = (endpoint, options) => request(endpoint, {
938
+ ...options,
939
+ method: HTTP_METHODS.DELETE
940
+ });
941
+ return {
942
+ fetchClient,
943
+ request,
944
+ requestNullable,
945
+ safeRequest,
946
+ get,
947
+ post,
948
+ put,
949
+ patch,
950
+ delete: del
951
+ };
1546
952
  }
1547
953
  //#endregion
1548
- //#region src/transforms/screen-transforms.ts
954
+ //#region ../../api-clients/fluidos/src/namespaces/fluid_os.ts
1549
955
  /**
1550
- * Normalize component_tree to always be an array.
1551
- * The API stores component_tree as a hash (object), but the frontend expects an array.
956
+ * Get active Fluid OS definition
957
+ * Retrieve the active Fluid OS definition manifest for a specific platform
958
+ *
959
+ * @param client - Fetch client instance
960
+ * @param params - params
1552
961
  */
1553
- function normalizeComponentTree(componentTree) {
1554
- if (!componentTree) return [];
1555
- if (Array.isArray(componentTree)) return componentTree;
1556
- if (typeof componentTree === "object") return [componentTree];
1557
- return [];
962
+ async function getFluidOSManifest(client, params) {
963
+ return client.get(`/api/fluid_os/definitions/active`, params);
1558
964
  }
1559
- /**
1560
- * Convert a raw FluidOS screen to ScreenDefinition.
1561
- * Normalizes component_tree and converts string IDs to numbers.
1562
- */
1563
- function toScreenDefinition(screen) {
965
+ //#endregion
966
+ //#region src/adapters/fluidos-api-adapter.ts
967
+ /** Create a FluidOsReadApi adapter backed by a FetchClient. */
968
+ function createFluidOsReadAdapter(client) {
969
+ return { getManifest: (params) => getFluidOSManifest(client, params) };
970
+ }
971
+ //#endregion
972
+ //#region src/adapters/app-definition-api-adapter.ts
973
+ function mapDefinition(raw) {
1564
974
  return {
1565
- id: Number(screen.id),
1566
- slug: screen.slug ?? "",
1567
- name: screen.name ?? "",
1568
- component_tree: normalizeComponentTree(screen.component_tree)
975
+ id: raw.id ?? 0,
976
+ name: raw.name ?? "",
977
+ version: raw.version ?? null,
978
+ components: raw.components ?? [],
979
+ active: raw.active ?? false
1569
980
  };
1570
981
  }
1571
- //#endregion
1572
- //#region src/transforms/navigation-transforms.ts
1573
982
  /**
1574
- * Convert a raw FluidOS navigation item to NavigationItem.
1575
- * Recursively transforms children and sorts by position.
983
+ * Creates an AppDefinitionApi adapter backed by the portal-tenant BFF client.
984
+ *
985
+ * Maps the generated portal-tenant `app_definition_show` response to the
986
+ * `AppDefinitionApi` port, applying runtime defaults for optional BFF fields
987
+ * so TypeScript catches schema drift at compile time via `satisfies`.
1576
988
  */
1577
- function toNavigationItem(item) {
1578
- const children = (item.children ?? []).map(toNavigationItem).sort((a, b) => (a.position ?? 0) - (b.position ?? 0));
989
+ function createAppDefinitionApiAdapter(client) {
990
+ return { fetchDefinition: async () => {
991
+ const response = await require_portal_tenant.app_definition_show(client);
992
+ return {
993
+ definition: mapDefinition(response.definition ?? {}),
994
+ meta: {
995
+ request_id: response.meta?.request_id ?? "",
996
+ timestamp: response.meta?.timestamp ?? ""
997
+ }
998
+ };
999
+ } };
1000
+ }
1001
+ //#endregion
1002
+ //#region src/adapters/account-api-adapter.ts
1003
+ function mapAccount(raw) {
1579
1004
  return {
1580
- id: Number(item.id),
1581
- label: item.label ?? "Untitled",
1582
- ...item.slug != null ? { slug: String(item.slug) } : {},
1583
- ...item.icon != null ? { icon: String(item.icon) } : {},
1584
- ...item.screen_id != null ? { screen_id: Number(item.screen_id) } : {},
1585
- ...item.parent_id != null ? { parent_id: Number(item.parent_id) } : {},
1586
- ...item.source != null ? { source: item.source } : {},
1587
- position: item.position ?? 0,
1588
- children
1005
+ id: raw.id ?? 0,
1006
+ member_type: raw.member_type ?? "rep",
1007
+ first_name: raw.first_name ?? "",
1008
+ last_name: raw.last_name ?? "",
1009
+ email: raw.email ?? "",
1010
+ phone: raw.phone ?? null,
1011
+ bio: raw.bio ?? null,
1012
+ avatar_url: raw.avatar_url ?? null,
1013
+ slug: raw.slug ?? "",
1014
+ social_links: raw.social_links ?? null
1015
+ };
1016
+ }
1017
+ function createAccountApiAdapter(client) {
1018
+ return {
1019
+ fetchAccount: async () => {
1020
+ const response = await require_portal_tenant.account_show(client);
1021
+ return {
1022
+ account: mapAccount(response.account ?? {}),
1023
+ meta: {
1024
+ request_id: response.meta?.request_id ?? "",
1025
+ timestamp: response.meta?.timestamp ?? ""
1026
+ }
1027
+ };
1028
+ },
1029
+ updateAccount: async (body) => {
1030
+ const response = await require_portal_tenant.account_update(client, body);
1031
+ return {
1032
+ account: mapAccount(response.account ?? {}),
1033
+ meta: {
1034
+ request_id: response.meta?.request_id ?? "",
1035
+ timestamp: response.meta?.timestamp ?? ""
1036
+ }
1037
+ };
1038
+ }
1589
1039
  };
1590
1040
  }
1591
1041
  //#endregion
1592
- //#region src/transforms/index.ts
1042
+ //#region ../../api-clients/portal-tenant-pay/src/namespaces/portal_tenant_pay.ts
1593
1043
  /**
1594
- * Convert a raw FluidOS API response to RawManifestResponse.
1044
+ * List addresses
1045
+ * Returns addresses associated with the member's customer record in this tenant.
1595
1046
  *
1596
- * The wire format includes `navigation_items` and full theme objects,
1597
- * but the generated OpenAPI types differ (e.g. `navigation_tree`, `themes: number[]`).
1598
- * This function bridges that gap so callers avoid `as unknown as` casts.
1047
+ * @param client - Fetch client instance
1048
+ * @param [params] - params
1599
1049
  */
1600
- function toRawManifest(raw) {
1601
- if (!raw.manifest) throw new Error("FluidOS API returned empty manifest");
1602
- return raw;
1050
+ async function addresses_list(client, params) {
1051
+ return client.get(`/api/pay/addresses`, params);
1603
1052
  }
1604
1053
  /**
1605
- * Transform a raw FluidOS manifest API response into RepAppData.
1054
+ * Create an address
1055
+ * Adds a new address to the member's customer record. If an identical address already exists it is returned instead of creating a duplicate.
1606
1056
  *
1607
- * This is the top-level transform used by `FluidClient.app.get()`.
1608
- * It handles:
1609
- * - Theme transformation (legacy and new formats)
1610
- * - Screen normalization (component_tree array wrapping)
1611
- * - Navigation item transformation (recursive with position sorting)
1612
- */
1613
- function transformManifestToRepAppData(response) {
1614
- const manifest = response.manifest;
1615
- const rawProfile = manifest.profile;
1616
- const rawThemes = Array.isArray(rawProfile?.themes) ? rawProfile.themes : [];
1617
- const screens = (manifest.screens ?? []).map((screen) => toScreenDefinition(screen));
1618
- const navigationItems = (rawProfile?.navigation?.navigation_items ?? []).map(toNavigationItem);
1619
- const nav = rawProfile?.navigation;
1620
- const mobileNav = rawProfile?.mobile_navigation;
1621
- const mobileNavigationItems = (mobileNav?.navigation_items ?? []).map(toNavigationItem);
1622
- const activeThemeId = getActiveThemeId(rawThemes);
1623
- return {
1624
- definition_id: manifest.definition_id,
1625
- published_version: manifest.published_version ?? 0,
1626
- screens,
1627
- profile: {
1628
- name: rawProfile?.name ?? "Default",
1629
- definition_id: rawProfile?.definition_id ?? manifest.definition_id,
1630
- themes: transformThemes(rawThemes),
1631
- ...activeThemeId !== void 0 ? { activeThemeId } : {},
1632
- navigation: {
1633
- definition_id: nav?.definition_id ?? manifest.definition_id,
1634
- id: nav?.id ?? 0,
1635
- name: nav?.name ?? "Main Navigation",
1636
- navigation_items: navigationItems,
1637
- screens
1638
- },
1639
- ...mobileNav ? { mobile_navigation: {
1640
- definition_id: mobileNav.definition_id ?? manifest.definition_id,
1641
- id: mobileNav.id ?? 0,
1642
- name: mobileNav.name ?? "Mobile Navigation",
1643
- navigation_items: mobileNavigationItems,
1644
- screens
1645
- } } : {}
1646
- }
1647
- };
1057
+ * @param client - Fetch client instance
1058
+ * @param body - body
1059
+ */
1060
+ async function addresses_create(client, body) {
1061
+ return client.post(`/api/pay/addresses`, body);
1648
1062
  }
1649
- //#endregion
1650
- //#region src/client/fluid-client.ts
1651
1063
  /**
1652
- * API Error class for structured error handling
1064
+ * Update an address
1065
+ * Creates a new address with the merged attributes and discards the old one, preserving references from existing orders.
1066
+ *
1067
+ * @param client - Fetch client instance
1068
+ * @param id - id
1069
+ * @param body - body
1653
1070
  */
1654
- var ApiError = class ApiError extends Error {
1655
- status;
1656
- data;
1657
- constructor(message, status, data) {
1658
- super(message);
1659
- this.name = "ApiError";
1660
- this.status = status;
1661
- this.data = data;
1662
- const errorWithCapture = Error;
1663
- if (errorWithCapture.captureStackTrace) errorWithCapture.captureStackTrace(this, ApiError);
1664
- }
1665
- toJSON() {
1666
- return {
1667
- name: this.name,
1668
- message: this.message,
1669
- status: this.status,
1670
- data: this.data
1671
- };
1672
- }
1673
- };
1071
+ async function addresses_update(client, id, body) {
1072
+ return client.patch(`/api/pay/addresses/${id}`, body);
1073
+ }
1674
1074
  /**
1675
- * Type guard for ApiError
1075
+ * Delete an address
1076
+ * Removes an address from the member's customer record. The default address cannot be removed.
1077
+ *
1078
+ * @param client - Fetch client instance
1079
+ * @param id - id
1676
1080
  */
1677
- function isApiError(error) {
1678
- return error instanceof ApiError;
1081
+ async function addresses_destroy(client, id) {
1082
+ return client.delete(`/api/pay/addresses/${id}`);
1679
1083
  }
1680
1084
  /**
1681
- * Type guard to check if a value is a non-null string
1085
+ * List payment methods
1086
+ * Returns displayable payment methods on the member's customer record, excluding Apple Pay sources.
1087
+ *
1088
+ * @param client - Fetch client instance
1089
+ * @param [params] - params
1682
1090
  */
1683
- function isString(value) {
1684
- return typeof value === "string";
1091
+ async function payment_methods_list(client, params) {
1092
+ return client.get(`/api/pay/payment_methods`, params);
1685
1093
  }
1686
1094
  /**
1687
- * Extract error message from API response data using `in` operator narrowing.
1688
- * Checks common error message field names in order of precedence.
1095
+ * Create a payment method
1096
+ * Tokenizes and stores a new payment method via the vault provider. Requires a vault token obtained from the vault credentials endpoint.
1097
+ *
1098
+ * @param client - Fetch client instance
1099
+ * @param body - body
1689
1100
  */
1690
- function extractErrorMessage(data, fallback) {
1691
- if ("message" in data && isString(data.message)) return data.message;
1692
- if ("error_message" in data && isString(data.error_message)) return data.error_message;
1693
- if ("error" in data && isString(data.error)) return data.error;
1694
- return fallback;
1101
+ async function payment_methods_create(client, body) {
1102
+ return client.post(`/api/pay/payment_methods`, body);
1103
+ }
1104
+ /**
1105
+ * Update a payment method
1106
+ * Updates a payment method's attributes. Currently supports setting a payment method as the default.
1107
+ *
1108
+ * @param client - Fetch client instance
1109
+ * @param id - id
1110
+ * @param body - body
1111
+ */
1112
+ async function payment_methods_update(client, id, body) {
1113
+ return client.patch(`/api/pay/payment_methods/${id}`, body);
1114
+ }
1115
+ /**
1116
+ * Delete a payment method
1117
+ * Removes a payment method from the member's customer record. If the removed method was the default, the default is cleared.
1118
+ *
1119
+ * @param client - Fetch client instance
1120
+ * @param id - id
1121
+ */
1122
+ async function payment_methods_destroy(client, id) {
1123
+ return client.delete(`/api/pay/payment_methods/${id}`);
1695
1124
  }
1696
1125
  /**
1697
- * Type guard to detect whether a parsed JSON value is an API envelope.
1698
- * Envelopes always have numeric `status` and a `data` key.
1126
+ * Get vault credentials
1127
+ * Returns a short-lived vault token and environment identifier for initializing the client-side payment vault SDK.
1128
+ *
1129
+ * @param client - Fetch client instance
1130
+
1699
1131
  */
1700
- function isApiEnvelope(value) {
1701
- return typeof value === "object" && value !== null && "status" in value && typeof value.status === "number" && "data" in value;
1132
+ async function payment_methods_vault_show(client) {
1133
+ return client.get(`/api/pay/payment_methods/vault`);
1702
1134
  }
1703
1135
  /**
1704
- * Creates a configured Fluid API client instance
1136
+ * List points ledger entries
1137
+ * Returns loyalty points ledger entries for the member's customer record in this tenant, ordered by most recent first.
1138
+ *
1139
+ * @param client - Fetch client instance
1140
+ * @param [params] - params
1705
1141
  */
1706
- function createFluidClient(config) {
1707
- const { baseUrl, getAuthToken, onAuthError, defaultHeaders = {} } = config;
1708
- const fetchClient = createFetchClient({
1709
- baseUrl,
1710
- ...getAuthToken ? { getAuthToken } : {},
1711
- onAuthError,
1712
- defaultHeaders,
1713
- credentials: "include"
1714
- });
1715
- /**
1716
- * Build headers for a request.
1717
- * Auth is handled by session cookies via `credentials: 'include'` on fetch calls.
1718
- */
1719
- function buildHeaders(customHeaders) {
1720
- return {
1721
- "Content-Type": "application/json",
1722
- ...defaultHeaders,
1723
- ...customHeaders
1724
- };
1725
- }
1726
- /**
1727
- * Build URL with query parameters (Rails-compatible)
1728
- */
1729
- function buildUrl(endpoint, params) {
1730
- const normalizedBase = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
1731
- const normalizedEndpoint = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
1732
- const url = normalizedBase ? new URL(normalizedBase + normalizedEndpoint) : new URL(normalizedEndpoint, typeof window !== "undefined" ? window.location.origin : "http://localhost");
1733
- if (params) for (const [key, value] of Object.entries(params)) {
1734
- if (value === void 0 || value === null) continue;
1735
- if (Array.isArray(value)) for (const item of value) url.searchParams.append(`${key}[]`, String(item));
1736
- else if (typeof value === "object") for (const [subKey, subValue] of Object.entries(value)) {
1737
- if (subValue === void 0 || subValue === null) continue;
1738
- if (Array.isArray(subValue)) for (const item of subValue) url.searchParams.append(`${key}[${subKey}][]`, String(item));
1739
- else url.searchParams.append(`${key}[${subKey}]`, String(subValue));
1740
- }
1741
- else url.searchParams.append(key, String(value));
1742
- }
1743
- return url.toString();
1744
- }
1745
- /**
1746
- * Default request options for type-safe defaults.
1747
- * Uses `satisfies` to validate against RequestOptions while preserving literal types.
1748
- */
1749
- const defaultRequestOptions = { method: HTTP_METHODS.GET };
1750
- /**
1751
- * Main request function
1752
- */
1753
- async function request(endpoint, options = {}) {
1754
- const { method = defaultRequestOptions.method, headers: customHeaders, params, body, signal } = options;
1755
- const url = buildUrl(endpoint, method === HTTP_METHODS.GET ? params : void 0);
1756
- const headers = buildHeaders(customHeaders);
1757
- let response;
1758
- try {
1759
- const fetchOptions = {
1760
- method,
1761
- headers,
1762
- credentials: "include"
1763
- };
1764
- if (signal !== void 0) fetchOptions.signal = signal;
1765
- if (body && method !== HTTP_METHODS.GET) fetchOptions.body = JSON.stringify(body);
1766
- response = await fetch(url, fetchOptions);
1767
- } catch (networkError) {
1768
- throw new ApiError(`Network error: ${networkError instanceof Error ? networkError.message : "Unknown network error"}`, 0, null);
1769
- }
1770
- if (response.status === 401) {
1771
- onAuthError?.();
1772
- throw new ApiError("Authentication required", 401, null);
1773
- }
1774
- if (!response.ok) try {
1775
- if (response.headers.get("content-type")?.includes("application/json")) {
1776
- const data = await response.json();
1777
- throw new ApiError(extractErrorMessage(data, `${method} request failed`), response.status, "errors" in data ? data.errors : data);
1778
- } else throw new ApiError(`${method} request failed with status ${response.status}`, response.status, null);
1779
- } catch (error) {
1780
- if (isApiError(error)) throw error;
1781
- throw new ApiError(`${method} request failed with status ${response.status}`, response.status, null);
1782
- }
1783
- if (response.status === 204 || response.headers.get("content-length") === "0") return null;
1784
- try {
1785
- const raw = await response.json();
1786
- if (raw === null || raw === void 0) throw new ApiError("Unexpected null/undefined in JSON response", response.status, null);
1787
- return isApiEnvelope(raw) ? raw.data : raw;
1788
- } catch (parseError) {
1789
- if (isApiError(parseError)) throw parseError;
1790
- throw new ApiError("Failed to parse response as JSON", response.status, null);
1791
- }
1792
- }
1793
- /**
1794
- * Request function for endpoints that may return null (204 No Content).
1795
- * Properly types the return as T | null.
1796
- */
1797
- async function requestNullable(endpoint, options = {}) {
1798
- return request(endpoint, options);
1799
- }
1800
- /**
1801
- * Safe request wrapper that returns a discriminated union instead of throwing.
1802
- * Use `isApiSuccess` or `isApiFailure` to narrow the result.
1803
- */
1804
- async function safeRequest(endpoint, options = {}) {
1805
- try {
1142
+ async function points_ledgers_list(client, params) {
1143
+ return client.get(`/api/pay/points_ledgers`, params);
1144
+ }
1145
+ //#endregion
1146
+ //#region src/adapters/pay-api-adapter.ts
1147
+ function mapAddress(raw) {
1148
+ return {
1149
+ id: raw.id ?? 0,
1150
+ street1: raw.street1 ?? "",
1151
+ street2: raw.street2 ?? null,
1152
+ city: raw.city ?? "",
1153
+ state: raw.state ?? "",
1154
+ zip: raw.zip ?? "",
1155
+ country: raw.country ?? "",
1156
+ default: raw.default ?? false,
1157
+ created_at: raw.created_at ?? null,
1158
+ updated_at: raw.updated_at ?? null
1159
+ };
1160
+ }
1161
+ function mapPaymentMethod(raw) {
1162
+ return {
1163
+ id: raw.id ?? 0,
1164
+ type: raw.type ?? "card",
1165
+ brand: raw.brand ?? null,
1166
+ last_four: raw.last_four ?? "",
1167
+ exp_month: raw.exp_month ?? null,
1168
+ exp_year: raw.exp_year ?? null,
1169
+ default: raw.default ?? false,
1170
+ created_at: raw.created_at ?? null,
1171
+ updated_at: raw.updated_at ?? null
1172
+ };
1173
+ }
1174
+ function createPortalTenantPayAdapter(client) {
1175
+ return {
1176
+ fetchAddresses: async () => {
1177
+ const response = await addresses_list(client);
1806
1178
  return {
1807
- success: true,
1808
- data: await request(endpoint, options)
1809
- };
1810
- } catch (error) {
1811
- if (isApiError(error)) return {
1812
- success: false,
1813
- error
1179
+ addresses: (response.addresses ?? []).map(mapAddress),
1180
+ meta: {
1181
+ request_id: response.meta?.request_id ?? "",
1182
+ timestamp: response.meta?.timestamp ?? ""
1183
+ }
1814
1184
  };
1185
+ },
1186
+ createAddress: async (body) => {
1187
+ await addresses_create(client, body);
1188
+ },
1189
+ updateAddress: async (addressId, body) => {
1190
+ await addresses_update(client, addressId, body);
1191
+ },
1192
+ deleteAddress: async (addressId) => {
1193
+ await addresses_destroy(client, addressId);
1194
+ },
1195
+ fetchPaymentMethods: async () => {
1196
+ const response = await payment_methods_list(client);
1815
1197
  return {
1816
- success: false,
1817
- error: new ApiError(error instanceof Error ? error.message : "Unknown error", 0, null)
1198
+ payment_methods: (response.payment_methods ?? []).map(mapPaymentMethod),
1199
+ meta: {
1200
+ request_id: response.meta?.request_id ?? "",
1201
+ timestamp: response.meta?.timestamp ?? ""
1202
+ }
1818
1203
  };
1819
- }
1820
- }
1821
- /**
1822
- * Helper to safely convert typed params to Record<string, unknown>.
1823
- * Type assertion required: TypeScript's structural typing allows any object
1824
- * to be treated as Record<string, unknown> when we only need to iterate
1825
- * over its entries. This is safe because buildUrl only reads properties.
1826
- */
1827
- function toParams(params) {
1828
- return params;
1829
- }
1830
- const get = (endpoint, params, options) => {
1831
- const baseOptions = {
1832
- ...options,
1833
- method: HTTP_METHODS.GET
1834
- };
1835
- const convertedParams = toParams(params);
1836
- return request(endpoint, convertedParams !== void 0 ? {
1837
- ...baseOptions,
1838
- params: convertedParams
1839
- } : baseOptions);
1840
- };
1841
- const post = (endpoint, body, options) => request(endpoint, {
1842
- ...options,
1843
- method: HTTP_METHODS.POST,
1844
- body
1845
- });
1846
- const put = (endpoint, body, options) => request(endpoint, {
1847
- ...options,
1848
- method: HTTP_METHODS.PUT,
1849
- body
1850
- });
1851
- const patch = (endpoint, body, options) => request(endpoint, {
1852
- ...options,
1853
- method: HTTP_METHODS.PATCH,
1854
- body
1855
- });
1856
- const del = (endpoint, options) => request(endpoint, {
1857
- ...options,
1858
- method: HTTP_METHODS.DELETE
1859
- });
1860
- return {
1861
- fetchClient,
1862
- request,
1863
- requestNullable,
1864
- safeRequest,
1865
- get,
1866
- post,
1867
- put,
1868
- patch,
1869
- delete: del,
1870
- reps: {
1871
- current: () => get("/api/reps/me"),
1872
- updateProfile: (data) => patch("/api/reps/me", data)
1873
1204
  },
1874
- app: {
1875
- getRaw: async () => {
1876
- return toRawManifest(await getFluidOSManifest(fetchClient, { platform: "browser" }));
1877
- },
1878
- get: async () => {
1879
- return transformManifestToRepAppData(toRawManifest(await getFluidOSManifest(fetchClient, { platform: "browser" })));
1880
- }
1205
+ createPaymentMethod: async (body) => {
1206
+ await payment_methods_create(client, body);
1207
+ },
1208
+ updatePaymentMethod: async (paymentMethodId, body) => {
1209
+ await payment_methods_update(client, paymentMethodId, body);
1210
+ },
1211
+ deletePaymentMethod: async (paymentMethodId) => {
1212
+ await payment_methods_destroy(client, paymentMethodId);
1213
+ },
1214
+ fetchVaultCredentials: async () => {
1215
+ const response = await payment_methods_vault_show(client);
1216
+ return {
1217
+ vault: {
1218
+ token: response.vault?.token ?? null,
1219
+ environment: response.vault?.environment ?? "production"
1220
+ },
1221
+ meta: {
1222
+ request_id: response.meta?.request_id ?? "",
1223
+ timestamp: response.meta?.timestamp ?? ""
1224
+ }
1225
+ };
1881
1226
  },
1882
- permissions: { get: () => get("/api/company/roles/my_permissions") },
1883
- analytics: {
1884
- dashboard: () => get("/api/analytics/dashboard"),
1885
- sales: (params) => get("/api/analytics/sales", params)
1227
+ fetchPointsLedgers: async () => {
1228
+ const response = await points_ledgers_list(client);
1229
+ return {
1230
+ points_ledgers: (response.points_ledgers ?? []).map((entry) => ({
1231
+ id: entry.id ?? 0,
1232
+ amount: entry.amount ?? 0,
1233
+ total_balance: entry.total_balance ?? 0,
1234
+ metadata: entry.metadata ?? null,
1235
+ created_at: entry.created_at ?? ""
1236
+ })),
1237
+ meta: {
1238
+ request_id: response.meta?.request_id ?? "",
1239
+ timestamp: response.meta?.timestamp ?? ""
1240
+ }
1241
+ };
1886
1242
  }
1887
1243
  };
1888
1244
  }
1889
1245
  //#endregion
1890
- //#region src/adapters/fluidos-api-adapter.ts
1891
- /** Create a FluidOsReadApi adapter backed by a FetchClient. */
1892
- function createFluidOsReadAdapter(client) {
1893
- return { getManifest: (params) => getFluidOSManifest(client, params) };
1246
+ //#region ../../api-clients/portal-tenant-store/src/namespaces/portal_tenant_store.ts
1247
+ /**
1248
+ * List available countries
1249
+ * Returns countries enabled for the tenant store, each with its ISO 3166 subdivisions (states/provinces).
1250
+ *
1251
+ * @param client - Fetch client instance
1252
+ * @param [params] - params
1253
+ */
1254
+ async function countries_list(client, params) {
1255
+ return client.get(`/api/store/countries`, params);
1256
+ }
1257
+ /**
1258
+ * List available languages
1259
+ * Returns languages enabled for the tenant store, sorted by name.
1260
+ *
1261
+ * @param client - Fetch client instance
1262
+ * @param [params] - params
1263
+ */
1264
+ async function languages_list(client, params) {
1265
+ return client.get(`/api/store/languages`, params);
1894
1266
  }
1895
1267
  //#endregion
1896
- //#region src/adapters/app-definition-api-adapter.ts
1897
- function mapDefinition(raw) {
1268
+ //#region ../../store/api-client/src/portal-tenant-countries-adapter.ts
1269
+ /**
1270
+ * Maps a BFF state to the port's State shape.
1271
+ */
1272
+ function mapState(raw) {
1898
1273
  return {
1899
- id: raw.id ?? 0,
1274
+ code: raw.code ?? "",
1275
+ name: raw.name ?? ""
1276
+ };
1277
+ }
1278
+ /**
1279
+ * Maps a BFF country to the port's Country shape.
1280
+ */
1281
+ function mapCountry(raw) {
1282
+ return {
1283
+ code: raw.code ?? "",
1900
1284
  name: raw.name ?? "",
1901
- version: raw.version ?? null,
1902
- components: raw.components ?? [],
1903
- active: raw.active ?? false
1285
+ currency_code: raw.currency_code ?? "",
1286
+ states: (raw.states ?? []).map(mapState)
1904
1287
  };
1905
1288
  }
1906
1289
  /**
1907
- * Creates an AppDefinitionApi adapter backed by the portal-tenant BFF client.
1290
+ * Maps the BFF meta envelope to the port's ApiMeta shape.
1291
+ */
1292
+ function mapMeta$1(raw) {
1293
+ return {
1294
+ request_id: raw?.request_id ?? null,
1295
+ timestamp: raw?.timestamp ?? "",
1296
+ pagination: raw?.pagination ? {
1297
+ cursor: raw.pagination.cursor ?? null,
1298
+ limit: raw.pagination.limit,
1299
+ next_cursor: raw.pagination.next_cursor ?? null,
1300
+ prev_cursor: raw.pagination.prev_cursor ?? null
1301
+ } : void 0
1302
+ };
1303
+ }
1304
+ /**
1305
+ * Creates a CountriesApi adapter backed by the portal-tenant store BFF.
1908
1306
  *
1909
- * Maps the generated portal-tenant `app_definition_show` response to the
1910
- * `AppDefinitionApi` port, applying runtime defaults for optional BFF fields
1911
- * so TypeScript catches schema drift at compile time via `satisfies`.
1307
+ * Maps the generated portal-tenant-store namespace functions to the abstract
1308
+ * CountriesApi port, closing over the FetchClient so consumers don't need
1309
+ * to pass it per-call.
1912
1310
  */
1913
- function createAppDefinitionApiAdapter(client) {
1914
- return { fetchDefinition: async () => {
1915
- const response = await require_portal_tenant.app_definition_show(client);
1311
+ function createPortalTenantCountriesAdapter(client) {
1312
+ return { listCountries: async (params) => {
1313
+ const response = await countries_list(client, {
1314
+ "page[cursor]": params?.cursor,
1315
+ "page[limit]": params?.limit
1316
+ });
1916
1317
  return {
1917
- definition: mapDefinition(response.definition ?? {}),
1918
- meta: {
1919
- request_id: response.meta?.request_id ?? "",
1920
- timestamp: response.meta?.timestamp ?? ""
1921
- }
1318
+ countries: (response.countries ?? []).map(mapCountry),
1319
+ meta: mapMeta$1(response.meta)
1922
1320
  };
1923
1321
  } };
1924
1322
  }
1925
1323
  //#endregion
1926
- //#region src/adapters/account-api-adapter.ts
1927
- function mapAccount(raw) {
1324
+ //#region ../../store/api-client/src/portal-tenant-languages-adapter.ts
1325
+ /**
1326
+ * Maps a BFF language to the port's Language shape.
1327
+ */
1328
+ function mapLanguage(raw) {
1928
1329
  return {
1929
- id: raw.id ?? 0,
1930
- member_type: raw.member_type ?? "rep",
1931
- first_name: raw.first_name ?? "",
1932
- last_name: raw.last_name ?? "",
1933
- email: raw.email ?? "",
1934
- phone: raw.phone ?? null,
1935
- bio: raw.bio ?? null,
1936
- avatar_url: raw.avatar_url ?? null,
1937
- slug: raw.slug ?? "",
1938
- social_links: raw.social_links ?? null
1330
+ code: raw.code ?? "",
1331
+ name: raw.name ?? ""
1939
1332
  };
1940
1333
  }
1941
- function createAccountApiAdapter(client) {
1334
+ /**
1335
+ * Maps the BFF meta envelope to the port's ApiMeta shape.
1336
+ */
1337
+ function mapMeta(raw) {
1942
1338
  return {
1943
- fetchAccount: async () => {
1944
- const response = await require_portal_tenant.account_show(client);
1945
- return {
1946
- account: mapAccount(response.account ?? {}),
1947
- meta: {
1948
- request_id: response.meta?.request_id ?? "",
1949
- timestamp: response.meta?.timestamp ?? ""
1950
- }
1951
- };
1952
- },
1953
- updateAccount: async (body) => {
1954
- const response = await require_portal_tenant.account_update(client, body);
1955
- return {
1956
- account: mapAccount(response.account ?? {}),
1957
- meta: {
1958
- request_id: response.meta?.request_id ?? "",
1959
- timestamp: response.meta?.timestamp ?? ""
1960
- }
1961
- };
1962
- }
1339
+ request_id: raw?.request_id ?? null,
1340
+ timestamp: raw?.timestamp ?? "",
1341
+ pagination: raw?.pagination ? {
1342
+ cursor: raw.pagination.cursor ?? null,
1343
+ limit: raw.pagination.limit,
1344
+ next_cursor: raw.pagination.next_cursor ?? null,
1345
+ prev_cursor: raw.pagination.prev_cursor ?? null
1346
+ } : void 0
1963
1347
  };
1964
1348
  }
1349
+ /**
1350
+ * Creates a LanguagesApi adapter backed by the portal-tenant store BFF.
1351
+ *
1352
+ * Maps the generated portal-tenant-store namespace functions to the abstract
1353
+ * LanguagesApi port, closing over the FetchClient so consumers don't need
1354
+ * to pass it per-call.
1355
+ */
1356
+ function createPortalTenantLanguagesAdapter(client) {
1357
+ return { listLanguages: async (params) => {
1358
+ const response = await languages_list(client, {
1359
+ "page[cursor]": params?.cursor,
1360
+ "page[limit]": params?.limit
1361
+ });
1362
+ return {
1363
+ languages: (response.languages ?? []).map(mapLanguage),
1364
+ meta: mapMeta(response.meta)
1365
+ };
1366
+ } };
1367
+ }
1965
1368
  //#endregion
1966
- //#region ../../api-clients/portal-tenant-pay/src/namespaces/portal_tenant_pay.ts
1369
+ //#region src/adapters/countries-api-adapter.ts
1967
1370
  /**
1968
- * List addresses
1969
- * Returns addresses associated with the member's customer record in this tenant.
1371
+ * Creates a CountriesApi adapter backed by the portal-tenant store BFF.
1372
+ *
1373
+ * Delegates to the adapter factory in `@fluid-app/store-api-client` which
1374
+ * maps the generated portal-tenant-store namespace functions to the abstract
1375
+ * CountriesApi port.
1376
+ */
1377
+ function createCountriesApiAdapter(client) {
1378
+ return createPortalTenantCountriesAdapter(client);
1379
+ }
1380
+ //#endregion
1381
+ //#region src/adapters/languages-api-adapter.ts
1382
+ /**
1383
+ * Creates a LanguagesApi adapter backed by the portal-tenant store BFF.
1384
+ *
1385
+ * Delegates to the adapter factory in `@fluid-app/store-api-client` which
1386
+ * maps the generated portal-tenant-store namespace functions to the abstract
1387
+ * LanguagesApi port.
1388
+ */
1389
+ function createLanguagesApiAdapter(client) {
1390
+ return createPortalTenantLanguagesAdapter(client);
1391
+ }
1392
+ //#endregion
1393
+ //#region ../../api-clients/portal-tenant-mysite/src/namespaces/portal_tenant_mysite.ts
1394
+ /**
1395
+ * Get MySite profile for the current user
1396
+ * Returns the member's MySite profile.
1397
+ *
1398
+ * @param client - Fetch client instance
1399
+
1400
+ */
1401
+ async function mysite_profile_show(client) {
1402
+ return client.get(`/api/mysite/profile`);
1403
+ }
1404
+ /**
1405
+ * Update MySite profile
1406
+ * Updates the member's MySite profile fields.
1407
+ *
1408
+ * @param client - Fetch client instance
1409
+ * @param body - body
1410
+ */
1411
+ async function mysite_profile_update(client, body) {
1412
+ return client.put(`/api/mysite/profile`, body);
1413
+ }
1414
+ /**
1415
+ * List available MySite themes
1416
+ * Returns available MySite themes.
1970
1417
  *
1971
1418
  * @param client - Fetch client instance
1972
1419
  * @param [params] - params
1973
1420
  */
1974
- async function addresses_list(client, params) {
1975
- return client.get(`/api/pay/addresses`, params);
1421
+ async function mysite_themes_list(client, params) {
1422
+ return client.get(`/api/mysite/themes`, params);
1976
1423
  }
1977
1424
  /**
1978
- * Create an address
1979
- * Adds a new address to the member's customer record. If an identical address already exists it is returned instead of creating a duplicate.
1425
+ * Update MySite settings
1426
+ * Updates the member's MySite settings.
1980
1427
  *
1981
1428
  * @param client - Fetch client instance
1982
1429
  * @param body - body
1983
1430
  */
1984
- async function addresses_create(client, body) {
1985
- return client.post(`/api/pay/addresses`, body);
1431
+ async function mysite_settings_update(client, body) {
1432
+ return client.put(`/api/mysite/settings`, body);
1986
1433
  }
1987
1434
  /**
1988
- * Update an address
1989
- * Creates a new address with the merged attributes and discards the old one, preserving references from existing orders.
1435
+ * List MySite links for the current user
1436
+ * Returns the member's MySite links, ordered by position.
1437
+ *
1438
+ * @param client - Fetch client instance
1439
+ * @param [params] - params
1440
+ */
1441
+ async function mysite_links_list(client, params) {
1442
+ return client.get(`/api/mysite/links`, params);
1443
+ }
1444
+ /**
1445
+ * Create a MySite link
1446
+ * Adds a new link to the member's MySite.
1990
1447
  *
1991
1448
  * @param client - Fetch client instance
1992
- * @param id - id
1993
1449
  * @param body - body
1994
1450
  */
1995
- async function addresses_update(client, id, body) {
1996
- return client.patch(`/api/pay/addresses/${id}`, body);
1451
+ async function mysite_links_create(client, body) {
1452
+ return client.post(`/api/mysite/links`, body);
1997
1453
  }
1998
1454
  /**
1999
- * Delete an address
2000
- * Removes an address from the member's customer record. The default address cannot be removed.
1455
+ * Update a MySite link
1456
+ * Updates an existing MySite link.
2001
1457
  *
2002
1458
  * @param client - Fetch client instance
2003
1459
  * @param id - id
1460
+ * @param body - body
2004
1461
  */
2005
- async function addresses_destroy(client, id) {
2006
- return client.delete(`/api/pay/addresses/${id}`);
1462
+ async function mysite_links_update(client, id, body) {
1463
+ return client.put(`/api/mysite/links/${id}`, body);
2007
1464
  }
2008
1465
  /**
2009
- * List payment methods
2010
- * Returns displayable payment methods on the member's customer record, excluding Apple Pay sources.
1466
+ * Delete a MySite link
1467
+ * Removes a link from the member's MySite.
2011
1468
  *
2012
1469
  * @param client - Fetch client instance
2013
- * @param [params] - params
1470
+ * @param id - id
2014
1471
  */
2015
- async function payment_methods_list(client, params) {
2016
- return client.get(`/api/pay/payment_methods`, params);
1472
+ async function mysite_links_destroy(client, id) {
1473
+ return client.delete(`/api/mysite/links/${id}`);
2017
1474
  }
2018
1475
  /**
2019
- * Create a payment method
2020
- * Tokenizes and stores a new payment method via the vault provider. Requires a vault token obtained from the vault credentials endpoint.
1476
+ * Reorder MySite links
1477
+ * Reorders MySite links by providing an ordered list of IDs.
2021
1478
  *
2022
1479
  * @param client - Fetch client instance
2023
1480
  * @param body - body
2024
1481
  */
2025
- async function payment_methods_create(client, body) {
2026
- return client.post(`/api/pay/payment_methods`, body);
1482
+ async function mysite_links_bulk_reorder(client, body) {
1483
+ return client.patch(`/api/mysite/links/bulk`, body);
2027
1484
  }
2028
1485
  /**
2029
- * Update a payment method
2030
- * Updates a payment method's attributes. Currently supports setting a payment method as the default.
1486
+ * List MySite favorite products
1487
+ * Returns the member's MySite favorite products.
2031
1488
  *
2032
1489
  * @param client - Fetch client instance
2033
- * @param id - id
2034
- * @param body - body
1490
+ * @param [params] - params
2035
1491
  */
2036
- async function payment_methods_update(client, id, body) {
2037
- return client.patch(`/api/pay/payment_methods/${id}`, body);
1492
+ async function mysite_favorites_list(client, params) {
1493
+ return client.get(`/api/mysite/favorites`, params);
2038
1494
  }
2039
1495
  /**
2040
- * Delete a payment method
2041
- * Removes a payment method from the member's customer record. If the removed method was the default, the default is cleared.
1496
+ * Add a product to MySite favorites
1497
+ * Adds a product to the member's MySite favorites.
2042
1498
  *
2043
1499
  * @param client - Fetch client instance
2044
- * @param id - id
1500
+ * @param body - body
2045
1501
  */
2046
- async function payment_methods_destroy(client, id) {
2047
- return client.delete(`/api/pay/payment_methods/${id}`);
1502
+ async function mysite_favorites_create(client, body) {
1503
+ return client.post(`/api/mysite/favorites`, body);
2048
1504
  }
2049
1505
  /**
2050
- * Get vault credentials
2051
- * Returns a short-lived vault token and environment identifier for initializing the client-side payment vault SDK.
1506
+ * Remove a product from MySite favorites
1507
+ * Removes a product from the member's MySite favorites.
2052
1508
  *
2053
1509
  * @param client - Fetch client instance
2054
-
1510
+ * @param id - id
2055
1511
  */
2056
- async function payment_methods_vault_show(client) {
2057
- return client.get(`/api/pay/payment_methods/vault`);
1512
+ async function mysite_favorites_destroy(client, id) {
1513
+ return client.delete(`/api/mysite/favorites/${id}`);
2058
1514
  }
2059
1515
  /**
2060
- * List points ledger entries
2061
- * Returns loyalty points ledger entries for the member's customer record in this tenant, ordered by most recent first.
1516
+ * Reorder MySite favorites
1517
+ * Reorders MySite favorites by providing an ordered list of IDs.
2062
1518
  *
2063
1519
  * @param client - Fetch client instance
2064
- * @param [params] - params
1520
+ * @param body - body
2065
1521
  */
2066
- async function points_ledgers_list(client, params) {
2067
- return client.get(`/api/pay/points_ledgers`, params);
1522
+ async function mysite_favorites_bulk_reorder(client, body) {
1523
+ return client.patch(`/api/mysite/favorites/bulk`, body);
2068
1524
  }
2069
1525
  //#endregion
2070
- //#region src/adapters/pay-api-adapter.ts
2071
- function mapAddress(raw) {
1526
+ //#region src/adapters/mysite-api-adapter.ts
1527
+ function mapProfile(raw) {
2072
1528
  return {
2073
1529
  id: raw.id ?? 0,
2074
- street1: raw.street1 ?? "",
2075
- street2: raw.street2 ?? null,
2076
- city: raw.city ?? "",
2077
- state: raw.state ?? "",
2078
- zip: raw.zip ?? "",
2079
- country: raw.country ?? "",
2080
- default: raw.default ?? false,
2081
- created_at: raw.created_at ?? null,
2082
- updated_at: raw.updated_at ?? null
1530
+ mysite_url: raw.mysite_url ?? null,
1531
+ theme_id: raw.theme_id ?? null,
1532
+ bio: raw.bio ?? null,
1533
+ avatar_url: raw.avatar_url ?? null,
1534
+ display_name: raw.display_name ?? null,
1535
+ slug: raw.slug ?? null
2083
1536
  };
2084
1537
  }
2085
- function mapPaymentMethod(raw) {
1538
+ function mapLink(raw) {
2086
1539
  return {
2087
1540
  id: raw.id ?? 0,
2088
- type: raw.type ?? "card",
2089
- brand: raw.brand ?? null,
2090
- last_four: raw.last_four ?? "",
2091
- exp_month: raw.exp_month ?? null,
2092
- exp_year: raw.exp_year ?? null,
2093
- default: raw.default ?? false,
2094
- created_at: raw.created_at ?? null,
2095
- updated_at: raw.updated_at ?? null
1541
+ url: raw.url ?? "",
1542
+ title: raw.title ?? "",
1543
+ position: raw.position ?? 0
2096
1544
  };
2097
1545
  }
2098
- function createPortalTenantPayAdapter(client) {
1546
+ function mapFavorite(raw) {
2099
1547
  return {
2100
- fetchAddresses: async () => {
2101
- const response = await addresses_list(client);
2102
- return {
2103
- addresses: (response.addresses ?? []).map(mapAddress),
2104
- meta: {
2105
- request_id: response.meta?.request_id ?? "",
2106
- timestamp: response.meta?.timestamp ?? ""
2107
- }
2108
- };
1548
+ id: raw.id ?? 0,
1549
+ product_id: raw.product_id ?? 0,
1550
+ product_name: raw.product_name ?? null,
1551
+ product_image_url: raw.product_image_url ?? null,
1552
+ position: raw.position ?? 0,
1553
+ created_at: raw.created_at ?? null
1554
+ };
1555
+ }
1556
+ function mapTheme(raw) {
1557
+ return {
1558
+ id: raw.id ?? 0,
1559
+ name: raw.name ?? "",
1560
+ preview_url: raw.preview_url ?? null
1561
+ };
1562
+ }
1563
+ function createMySiteApiAdapter(client) {
1564
+ return {
1565
+ fetchProfile: async () => {
1566
+ return mapProfile((await mysite_profile_show(client)).profile ?? {});
2109
1567
  },
2110
- createAddress: async (body) => {
2111
- await addresses_create(client, body);
1568
+ updateProfile: async (body) => {
1569
+ return mapProfile((await mysite_profile_update(client, { profile: {
1570
+ display_name: body.display_name,
1571
+ bio: body.bio,
1572
+ avatar_url: body.avatar_url
1573
+ } })).profile ?? {});
2112
1574
  },
2113
- updateAddress: async (addressId, body) => {
2114
- await addresses_update(client, addressId, body);
1575
+ updateSettings: async (body) => {
1576
+ await Promise.all([body.theme_id !== void 0 ? mysite_settings_update(client, { settings: { theme_id: body.theme_id } }) : Promise.resolve(), body.slug !== void 0 ? mysite_profile_update(client, { profile: { slug: body.slug } }) : Promise.resolve()]);
2115
1577
  },
2116
- deleteAddress: async (addressId) => {
2117
- await addresses_destroy(client, addressId);
1578
+ listLinks: async () => {
1579
+ return ((await mysite_links_list(client)).links ?? []).map(mapLink);
2118
1580
  },
2119
- fetchPaymentMethods: async () => {
2120
- const response = await payment_methods_list(client);
2121
- return {
2122
- payment_methods: (response.payment_methods ?? []).map(mapPaymentMethod),
2123
- meta: {
2124
- request_id: response.meta?.request_id ?? "",
2125
- timestamp: response.meta?.timestamp ?? ""
2126
- }
2127
- };
1581
+ createLink: async (body) => {
1582
+ return mapLink((await mysite_links_create(client, { link: {
1583
+ title: body.title,
1584
+ url: body.url
1585
+ } })).link ?? {});
2128
1586
  },
2129
- createPaymentMethod: async (body) => {
2130
- await payment_methods_create(client, body);
1587
+ updateLink: async (linkId, body) => {
1588
+ return mapLink((await mysite_links_update(client, linkId, { link: {
1589
+ title: body.title,
1590
+ url: body.url
1591
+ } })).link ?? {});
2131
1592
  },
2132
- updatePaymentMethod: async (paymentMethodId, body) => {
2133
- await payment_methods_update(client, paymentMethodId, body);
1593
+ deleteLink: async (linkId) => {
1594
+ await mysite_links_destroy(client, linkId);
2134
1595
  },
2135
- deletePaymentMethod: async (paymentMethodId) => {
2136
- await payment_methods_destroy(client, paymentMethodId);
1596
+ reorderLinks: async (orderedIds) => {
1597
+ return ((await mysite_links_bulk_reorder(client, { ordered_ids: orderedIds })).links ?? []).map(mapLink);
2137
1598
  },
2138
- fetchVaultCredentials: async () => {
2139
- const response = await payment_methods_vault_show(client);
2140
- return {
2141
- vault: {
2142
- token: response.vault?.token ?? null,
2143
- environment: response.vault?.environment ?? "production"
2144
- },
2145
- meta: {
2146
- request_id: response.meta?.request_id ?? "",
2147
- timestamp: response.meta?.timestamp ?? ""
2148
- }
2149
- };
1599
+ listFavorites: async () => {
1600
+ return ((await mysite_favorites_list(client)).favorites ?? []).map(mapFavorite);
2150
1601
  },
2151
- fetchPointsLedgers: async () => {
2152
- const response = await points_ledgers_list(client);
2153
- return {
2154
- points_ledgers: (response.points_ledgers ?? []).map((entry) => ({
2155
- id: entry.id ?? 0,
2156
- amount: entry.amount ?? 0,
2157
- total_balance: entry.total_balance ?? 0,
2158
- metadata: entry.metadata ?? null,
2159
- created_at: entry.created_at ?? ""
2160
- })),
2161
- meta: {
2162
- request_id: response.meta?.request_id ?? "",
2163
- timestamp: response.meta?.timestamp ?? ""
2164
- }
2165
- };
1602
+ addFavorite: async (body) => {
1603
+ return mapFavorite((await mysite_favorites_create(client, { favorite: { product_id: body.product_id } })).favorite ?? {});
1604
+ },
1605
+ deleteFavorite: async (favoriteId) => {
1606
+ await mysite_favorites_destroy(client, favoriteId);
1607
+ },
1608
+ reorderFavorites: async (orderedIds) => {
1609
+ return ((await mysite_favorites_bulk_reorder(client, { ordered_ids: orderedIds })).favorites ?? []).map(mapFavorite);
1610
+ },
1611
+ listThemes: async () => {
1612
+ return ((await mysite_themes_list(client)).themes ?? []).map(mapTheme);
2166
1613
  }
2167
1614
  };
2168
1615
  }
2169
1616
  //#endregion
2170
- //#region ../../api-clients/portal-tenant-store/src/namespaces/portal_tenant_store.ts
1617
+ //#region ../core/src/theme/types.ts
1618
+ const SEMANTIC_COLOR_NAMES = [
1619
+ "background",
1620
+ "foreground",
1621
+ "primary",
1622
+ "secondary",
1623
+ "accent",
1624
+ "muted",
1625
+ "destructive"
1626
+ ];
1627
+ const SHADE_STEPS = [
1628
+ 100,
1629
+ 200,
1630
+ 300,
1631
+ 400,
1632
+ 500,
1633
+ 600,
1634
+ 700,
1635
+ 800,
1636
+ 900
1637
+ ];
1638
+ const FONT_SIZE_KEYS = [
1639
+ "extraSmall",
1640
+ "small",
1641
+ "regular",
1642
+ "large",
1643
+ "extraLarge",
1644
+ "giant"
1645
+ ];
1646
+ const FONT_FAMILY_KEYS = ["header", "body"];
1647
+ const RADIUS_KEYS = [
1648
+ "small",
1649
+ "medium",
1650
+ "large",
1651
+ "extraLarge"
1652
+ ];
1653
+ //#endregion
1654
+ //#region ../core/src/theme/color-engine.ts
2171
1655
  /**
2172
- * List available countries
2173
- * Returns countries enabled for the tenant store, each with its ISO 3166 subdivisions (states/provinces).
1656
+ * Attempt to convert any string into a Color using colorjs.io.
1657
+ * If the string is exactly 6 characters it is assumed to be a bare hex value
1658
+ * (e.g. "3b82f6") and a "#" prefix is added before parsing.
2174
1659
  *
2175
- * @param client - Fetch client instance
2176
- * @param [params] - params
1660
+ * @returns the parsed Color, or a neutral gray (`oklch(0.5 0 0)`) on failure
2177
1661
  */
2178
- async function countries_list(client, params) {
2179
- return client.get(`/api/store/countries`, params);
1662
+ function parseColor(value) {
1663
+ if (value.length === 6) value = `#${value}`;
1664
+ try {
1665
+ return new colorjs_io.default(value);
1666
+ } catch (error) {
1667
+ console.warn("[theme] Failed to parse color:", value, error);
1668
+ return new colorjs_io.default("oklch", [
1669
+ .5,
1670
+ 0,
1671
+ 0
1672
+ ]);
1673
+ }
2180
1674
  }
2181
1675
  /**
2182
- * List available languages
2183
- * Returns languages enabled for the tenant store, sorted by name.
2184
- *
2185
- * @param client - Fetch client instance
2186
- * @param [params] - params
1676
+ * Returns either the original foreground or a corrected lightness variant,
1677
+ * whichever provides better contrast against `color`.
1678
+ * Inversion triggers when the APCA contrast is below 50.
2187
1679
  */
2188
- async function languages_list(client, params) {
2189
- return client.get(`/api/store/languages`, params);
1680
+ function getForegroundColor(foreground, color) {
1681
+ if (foreground.oklch.l == null || color.oklch.l == null) return foreground;
1682
+ if (color.contrastAPCA(foreground) < 50) return new colorjs_io.default("oklch", [
1683
+ color.oklch.l < .7 ? .95 : .15,
1684
+ foreground.oklch.c || 0,
1685
+ foreground.oklch.h || 0
1686
+ ]);
1687
+ return foreground;
2190
1688
  }
2191
- //#endregion
2192
- //#region ../../store/api-client/src/portal-tenant-countries-adapter.ts
2193
1689
  /**
2194
- * Maps a BFF state to the port's State shape.
1690
+ * Generate a 100–900 shade ramp from a base color.
1691
+ * Base anchors at 500. Light shades (100–400) step toward white,
1692
+ * dark shades (600–900) step toward black. Dark steps use an asymmetric
1693
+ * multiplier (1.6×, 1.875×, 3×, 4× of `darkStep`) for a more gradual
1694
+ * initial descent. Chroma is nudged per step for perceptually natural ramps.
2195
1695
  */
2196
- function mapState(raw) {
1696
+ function generateShades(base) {
1697
+ const l = base.oklch.l ?? 0;
1698
+ const c = base.oklch.c ?? 0;
1699
+ const h = base.oklch.h ?? 0;
1700
+ const safeMax = l >= .885 ? .995 : .97;
1701
+ const safeMin = l <= .33 ? 0 : .21;
1702
+ const lightStep = (safeMax - l) / 5;
1703
+ const darkStep = -(l - safeMin) / 8;
1704
+ const shade = (lDelta, cDelta) => {
1705
+ return new colorjs_io.default("oklch", [
1706
+ Math.max(0, Math.min(1, l + lDelta)),
1707
+ c <= .001 ? c : Math.max(0, c + cDelta),
1708
+ h
1709
+ ]);
1710
+ };
2197
1711
  return {
2198
- code: raw.code ?? "",
2199
- name: raw.name ?? ""
1712
+ 100: shade(5 * lightStep, -.00375),
1713
+ 200: shade(4 * lightStep, -.00375),
1714
+ 300: shade(3 * lightStep, -.00375),
1715
+ 400: shade(2 * lightStep, -.00375),
1716
+ 500: new colorjs_io.default("oklch", [
1717
+ l,
1718
+ c,
1719
+ h
1720
+ ]),
1721
+ 600: shade(1.6 * darkStep, .025),
1722
+ 700: shade(1.875 * 2 * darkStep, .05),
1723
+ 800: shade(6 * darkStep, .075),
1724
+ 900: shade(8 * darkStep, .1)
2200
1725
  };
2201
1726
  }
1727
+ const DARK_DERIVATION_CONFIG = {
1728
+ background: {
1729
+ baseLightness: .15,
1730
+ fgLightness: .93
1731
+ },
1732
+ foreground: {
1733
+ baseLightness: .93,
1734
+ fgLightness: .15
1735
+ },
1736
+ muted: {
1737
+ baseLightness: .22,
1738
+ fgLightness: .75
1739
+ },
1740
+ primary: {
1741
+ baseLightness: "invert",
1742
+ fgLightness: .95,
1743
+ chromaScale: .9
1744
+ },
1745
+ secondary: {
1746
+ baseLightness: "invert",
1747
+ fgLightness: .93,
1748
+ chromaScale: .85
1749
+ },
1750
+ accent: {
1751
+ baseLightness: "invert",
1752
+ fgLightness: .95,
1753
+ chromaScale: .9
1754
+ },
1755
+ destructive: {
1756
+ baseLightness: "invert",
1757
+ fgLightness: .95,
1758
+ chromaScale: .95
1759
+ }
1760
+ };
1761
+ /** Invert OKLCH lightness (1 - l), clamped to [0.35, 0.75] to avoid extremes. */
1762
+ function invertLightness(l) {
1763
+ const inverted = 1 - l;
1764
+ return Math.max(.35, Math.min(.75, inverted));
1765
+ }
2202
1766
  /**
2203
- * Maps a BFF country to the port's Country shape.
1767
+ * Derive a dark-mode ThemeColorInput from its light-mode counterpart.
2204
1768
  */
2205
- function mapCountry(raw) {
1769
+ function deriveDarkVariant(name, light) {
1770
+ const config = DARK_DERIVATION_CONFIG[name];
1771
+ const chromaScale = config.chromaScale ?? 1;
1772
+ const baseLightness = config.baseLightness === "invert" ? invertLightness(light.base.oklch.l ?? 0) : config.baseLightness;
1773
+ const fgLightness = config.fgLightness === "invert" ? invertLightness(light.foreground.oklch.l ?? 0) : config.fgLightness;
2206
1774
  return {
2207
- code: raw.code ?? "",
2208
- name: raw.name ?? "",
2209
- currency_code: raw.currency_code ?? "",
2210
- states: (raw.states ?? []).map(mapState)
1775
+ base: new colorjs_io.default("oklch", [
1776
+ baseLightness,
1777
+ (light.base.oklch.c || 0) * chromaScale,
1778
+ light.base.oklch.h || 0
1779
+ ]),
1780
+ foreground: new colorjs_io.default("oklch", [
1781
+ fgLightness,
1782
+ (light.foreground.oklch.c || 0) * chromaScale,
1783
+ light.foreground.oklch.h || 0
1784
+ ])
2211
1785
  };
2212
1786
  }
2213
1787
  /**
2214
- * Maps the BFF meta envelope to the port's ApiMeta shape.
1788
+ * Merge auto-derived dark colors with any user-specified overrides.
1789
+ * For each semantic color, if the user has fully overridden both base and
1790
+ * foreground those are used; otherwise the missing channels are derived.
2215
1791
  */
2216
- function mapMeta$1(raw) {
1792
+ function mergeDarkOverrides(def) {
1793
+ const darkColors = {};
1794
+ for (const name of SEMANTIC_COLOR_NAMES) {
1795
+ const lightInput = def.light[name];
1796
+ const darkOverride = def.dark[name];
1797
+ if (darkOverride?.base && darkOverride?.foreground) darkColors[name] = darkOverride;
1798
+ else if (darkOverride) {
1799
+ const base = darkOverride.base ?? deriveDarkVariant(name, lightInput).base;
1800
+ darkColors[name] = {
1801
+ base,
1802
+ foreground: darkOverride.foreground ?? getForegroundColor(def.light.foreground.base, base)
1803
+ };
1804
+ } else darkColors[name] = deriveDarkVariant(name, lightInput);
1805
+ }
1806
+ return darkColors;
1807
+ }
1808
+ function resolveColorSet(colors) {
1809
+ const resolved = {};
1810
+ for (const name of SEMANTIC_COLOR_NAMES) {
1811
+ const input = colors[name];
1812
+ const shades = generateShades(input.base);
1813
+ const resolvedShades = {};
1814
+ for (const step of SHADE_STEPS) resolvedShades[step] = shades[step];
1815
+ resolved[name] = {
1816
+ base: input.base.clone(),
1817
+ foreground: input.foreground.clone(),
1818
+ shades: resolvedShades
1819
+ };
1820
+ }
1821
+ return resolved;
1822
+ }
1823
+ /**
1824
+ * Resolve a ThemeDefinition into a complete ResolvedTheme.
1825
+ * Dark mode colors are derived from light where not overridden.
1826
+ */
1827
+ function resolveTheme(def) {
2217
1828
  return {
2218
- request_id: raw?.request_id ?? null,
2219
- timestamp: raw?.timestamp ?? "",
2220
- pagination: raw?.pagination ? {
2221
- cursor: raw.pagination.cursor ?? null,
2222
- limit: raw.pagination.limit,
2223
- next_cursor: raw.pagination.next_cursor ?? null,
2224
- prev_cursor: raw.pagination.prev_cursor ?? null
2225
- } : void 0
1829
+ id: def.id,
1830
+ name: def.name,
1831
+ light: resolveColorSet(def.light),
1832
+ dark: resolveColorSet(mergeDarkOverrides(def)),
1833
+ fontSizes: { ...def.fontSizes },
1834
+ fontFamilies: { ...def.fontFamilies },
1835
+ spacing: def.spacing,
1836
+ radii: { ...def.radii }
1837
+ };
1838
+ }
1839
+ //#endregion
1840
+ //#region ../core/src/theme/tailwind-overrides.ts
1841
+ /**
1842
+ * Specific overrides, otherwise all the overrides are generated using emitTailwindOverrides
1843
+ */
1844
+ const OVERRIDES = {
1845
+ "--color-gray-50": "var(--color-muted)",
1846
+ "--color-gray-100": "var(--color-muted-600)",
1847
+ "--color-gray-200": "var(--color-border)"
1848
+ };
1849
+ /**
1850
+ * Returns the inverted shade for dark mode foreground colors.
1851
+ * In dark mode, light shades (50, 100) should map to dark values (950, 900) and vice versa.
1852
+ */
1853
+ function getInvertedStep(shade) {
1854
+ const shadeIndex = SHADE_STEPS.indexOf(shade);
1855
+ return SHADE_STEPS[SHADE_STEPS.length - 1 - shadeIndex] || 500;
1856
+ }
1857
+ /**
1858
+ * Map semantic colors to Tailwind built-in color names.
1859
+ */
1860
+ function emitTailwindOverrides(darkMode = false) {
1861
+ const TAILWIND_COLOR_MAP = {
1862
+ gray: "foreground",
1863
+ red: "destructive",
1864
+ blue: "primary",
1865
+ green: "accent"
2226
1866
  };
1867
+ const TAILWIND_SHADES = [
1868
+ 50,
1869
+ 100,
1870
+ 200,
1871
+ 300,
1872
+ 400,
1873
+ 500,
1874
+ 600,
1875
+ 700,
1876
+ 800,
1877
+ 900,
1878
+ 950
1879
+ ];
1880
+ const SHADE_REMAP = {
1881
+ 50: 100,
1882
+ 950: 900
1883
+ };
1884
+ const lines = [];
1885
+ for (const [twName, semantic] of Object.entries(TAILWIND_COLOR_MAP)) for (const shade of TAILWIND_SHADES) {
1886
+ const step = SHADE_REMAP[shade] ?? shade;
1887
+ const override = OVERRIDES[`--color-${twName}-${shade}`];
1888
+ lines.push(`--color-${twName}-${shade}: ${override ? override : `var(--color-${semantic}-${semantic === "foreground" && darkMode === true ? getInvertedStep(step) : step})`};`);
1889
+ }
1890
+ lines.push("--color-white: var(--color-background);");
1891
+ lines.push("--color-black: var(--color-foreground);");
1892
+ return lines;
1893
+ }
1894
+ //#endregion
1895
+ //#region ../core/src/theme/css-generator.ts
1896
+ function colorToCSS(color) {
1897
+ const result = color.toString({ format: "oklch" });
1898
+ if (result.includes("NaN")) {
1899
+ console.warn("[theme] colorToCSS produced NaN, using neutral fallback:", result);
1900
+ return "oklch(0.5 0 0)";
1901
+ }
1902
+ return result;
1903
+ }
1904
+ function camelToKebab(str) {
1905
+ return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
2227
1906
  }
2228
1907
  /**
2229
- * Creates a CountriesApi adapter backed by the portal-tenant store BFF.
2230
- *
2231
- * Maps the generated portal-tenant-store namespace functions to the abstract
2232
- * CountriesApi port, closing over the FetchClient so consumers don't need
2233
- * to pass it per-call.
1908
+ * Emit --color-{name}, --color-{name}-foreground, --color-{name}-{shade} vars.
1909
+ * Uses --color- prefix to match portal-widgets/tailwind.config.ts.
2234
1910
  */
2235
- function createPortalTenantCountriesAdapter(client) {
2236
- return { listCountries: async (params) => {
2237
- const response = await countries_list(client, {
2238
- "page[cursor]": params?.cursor,
2239
- "page[limit]": params?.limit
2240
- });
2241
- return {
2242
- countries: (response.countries ?? []).map(mapCountry),
2243
- meta: mapMeta$1(response.meta)
2244
- };
2245
- } };
1911
+ function emitColorVars(colors) {
1912
+ const lines = [];
1913
+ for (const name of SEMANTIC_COLOR_NAMES) {
1914
+ const color = colors[name];
1915
+ lines.push(`--color-${name}: ${colorToCSS(color.base)};`);
1916
+ lines.push(`--color-${name}-foreground: ${colorToCSS(color.foreground)};`);
1917
+ for (const step of SHADE_STEPS) lines.push(`--color-${name}-${step}: ${colorToCSS(color.shades[step])};`);
1918
+ }
1919
+ return lines;
2246
1920
  }
2247
- //#endregion
2248
- //#region ../../store/api-client/src/portal-tenant-languages-adapter.ts
2249
1921
  /**
2250
- * Maps a BFF language to the port's Language shape.
1922
+ * Format a font family value for CSS output.
1923
+ * - If the value starts with "var(" (legacy), pass through as-is
1924
+ * - If the value already contains a comma (has fallback), pass through as-is
1925
+ * - Otherwise, wrap in quotes and append a generic sans-serif fallback
2251
1926
  */
2252
- function mapLanguage(raw) {
2253
- return {
2254
- code: raw.code ?? "",
2255
- name: raw.name ?? ""
2256
- };
1927
+ function formatFontFamily(value) {
1928
+ if (value.startsWith("var(")) return value;
1929
+ if (value.includes(",")) return value;
1930
+ return `'${value}', sans-serif`;
2257
1931
  }
2258
1932
  /**
2259
- * Maps the BFF meta envelope to the port's ApiMeta shape.
1933
+ * Emit non-color CSS variables (font sizes, families, spacing, radii).
2260
1934
  */
2261
- function mapMeta(raw) {
2262
- return {
2263
- request_id: raw?.request_id ?? null,
2264
- timestamp: raw?.timestamp ?? "",
2265
- pagination: raw?.pagination ? {
2266
- cursor: raw.pagination.cursor ?? null,
2267
- limit: raw.pagination.limit,
2268
- next_cursor: raw.pagination.next_cursor ?? null,
2269
- prev_cursor: raw.pagination.prev_cursor ?? null
2270
- } : void 0
2271
- };
1935
+ function emitNonColorVars(theme) {
1936
+ const lines = [];
1937
+ for (const key of FONT_SIZE_KEYS) lines.push(`--font-size-${camelToKebab(key)}: ${theme.fontSizes[key]};`);
1938
+ for (const key of FONT_FAMILY_KEYS) lines.push(`--font-${key}: ${formatFontFamily(theme.fontFamilies[key])};`);
1939
+ lines.push(`--spacing: ${theme.spacing};`);
1940
+ for (const key of RADIUS_KEYS) lines.push(`--radius-${camelToKebab(key)}: ${theme.radii[key]};`);
1941
+ return lines;
2272
1942
  }
2273
1943
  /**
2274
- * Creates a LanguagesApi adapter backed by the portal-tenant store BFF.
2275
- *
2276
- * Maps the generated portal-tenant-store namespace functions to the abstract
2277
- * LanguagesApi port, closing over the FetchClient so consumers don't need
2278
- * to pass it per-call.
1944
+ * Static CSS alias variables that bridge theme var names to Tailwind/component conventions.
1945
+ * These are always emitted and not mode-dependent.
2279
1946
  */
2280
- function createPortalTenantLanguagesAdapter(client) {
2281
- return { listLanguages: async (params) => {
2282
- const response = await languages_list(client, {
2283
- "page[cursor]": params?.cursor,
2284
- "page[limit]": params?.limit
2285
- });
2286
- return {
2287
- languages: (response.languages ?? []).map(mapLanguage),
2288
- meta: mapMeta(response.meta)
2289
- };
2290
- } };
2291
- }
2292
- //#endregion
2293
- //#region src/adapters/countries-api-adapter.ts
1947
+ const globalCSSOverride = [
1948
+ "--color-background-foreground: var(--color-foreground);",
1949
+ "--color-foreground-foreground: var(--color-background);",
1950
+ "--color-contrast: var(--color-foreground);",
1951
+ ...SEMANTIC_COLOR_NAMES.map((value) => `--${value}: var(--color-${value});`),
1952
+ ...SEMANTIC_COLOR_NAMES.map((value) => `--${value}-foreground: var(--color-${value}-foreground);`),
1953
+ "--sidebar-ring: var(--color-primary);",
1954
+ "--sidebar-border: var(--color-border);",
1955
+ "--sidebar-accent-foreground: var(--color-accent-foreground);",
1956
+ "--sidebar-accent: var(--color-accent);",
1957
+ "--sidebar-primary-foreground: var(--color-primary-foreground);",
1958
+ "--sidebar-primary: var(--color-primary);",
1959
+ "--sidebar-foreground: var(--color-muted-foreground);",
1960
+ "--sidebar: var(--color-muted);",
1961
+ "--border: var(--color-background-600);",
1962
+ "--ring: var(--color-primary);",
1963
+ "--popover: var(--color-background);",
1964
+ "--popover-foreground: var(--color-foreground);",
1965
+ "--card: var(--color-muted);",
1966
+ "--card-foreground: var(--color-muted-foreground);",
1967
+ "--radius-sm: var(--radius-small);",
1968
+ "--radius-md: var(--radius-medium);",
1969
+ "--radius-lg: var(--radius-large);",
1970
+ "--radius-xl: var(--radius-extra-large);",
1971
+ "--text-xs: var(--font-size-extra-small);",
1972
+ "--text-sm: var(--font-size-small);",
1973
+ "--text-base: var(--font-size-regular);",
1974
+ "--text-lg: var(--font-size-large);",
1975
+ "--text-xl: var(--font-size-extra-large);",
1976
+ "--text-2xl: var(--font-size-giant);"
1977
+ ];
2294
1978
  /**
2295
- * Creates a CountriesApi adapter backed by the portal-tenant store BFF.
2296
- *
2297
- * Delegates to the adapter factory in `@fluid-app/store-api-client` which
2298
- * maps the generated portal-tenant-store namespace functions to the abstract
2299
- * CountriesApi port.
1979
+ * Overrides for global tailwindcss for specifically dark mode.
2300
1980
  */
2301
- function createCountriesApiAdapter(client) {
2302
- return createPortalTenantCountriesAdapter(client);
1981
+ const globalDarkCSSOverride = ["--border: var(--color-background-400);"];
1982
+ /**
1983
+ * Generate a complete CSS string for a resolved theme.
1984
+ * Outputs 2–3 blocks: light default, dark explicit via `[data-theme-mode="dark"]`,
1985
+ * and (unless `disableAutoTheme`) a `prefers-color-scheme: dark` media query block.
1986
+ */
1987
+ function generateThemeCSS(theme, options = {}) {
1988
+ const sel = `[data-theme="${theme.id}"]`;
1989
+ const tw = options.mapTailwindColors ?? true;
1990
+ const blocks = [];
1991
+ blocks.push(`${sel} {`);
1992
+ blocks.push(...globalCSSOverride);
1993
+ blocks.push(...emitNonColorVars(theme));
1994
+ blocks.push(...emitColorVars(theme.light));
1995
+ if (tw) blocks.push(...emitTailwindOverrides());
1996
+ blocks.push(`}`);
1997
+ blocks.push(`${sel}[data-theme-mode="dark"] {`);
1998
+ blocks.push(...globalDarkCSSOverride);
1999
+ blocks.push(...emitColorVars(theme.dark));
2000
+ if (tw) blocks.push(...emitTailwindOverrides(true));
2001
+ blocks.push(`}`);
2002
+ if (!options.disableAutoTheme) {
2003
+ blocks.push(`@media (prefers-color-scheme: dark) {`);
2004
+ blocks.push(`${sel}:not([data-theme-mode]) {`);
2005
+ blocks.push(...globalDarkCSSOverride);
2006
+ blocks.push(...emitColorVars(theme.dark).map((l) => `${l}`));
2007
+ if (tw) blocks.push(...emitTailwindOverrides(true).map((l) => `${l}`));
2008
+ blocks.push(`}`);
2009
+ blocks.push(`}`);
2010
+ }
2011
+ return blocks.join("\n");
2303
2012
  }
2304
2013
  //#endregion
2305
- //#region src/adapters/languages-api-adapter.ts
2014
+ //#region ../core/src/theme/defaults.ts
2015
+ const DEFAULT_FONT_SIZES = {
2016
+ extraSmall: "0.75rem",
2017
+ small: "0.875rem",
2018
+ regular: "1rem",
2019
+ large: "1.125rem",
2020
+ extraLarge: "1.25rem",
2021
+ giant: "1.5rem"
2022
+ };
2023
+ const DEFAULT_FONT_FAMILIES = {
2024
+ header: "Inter",
2025
+ body: "Inter"
2026
+ };
2027
+ const DEFAULT_SPACING = "0.25rem";
2028
+ const DEFAULT_RADII = {
2029
+ small: "0.25rem",
2030
+ medium: "0.5rem",
2031
+ large: "0.75rem",
2032
+ extraLarge: "1rem"
2033
+ };
2034
+ const DEFAULT_COLORS = {
2035
+ background: "#ffffff",
2036
+ foreground: "#1a1a1a",
2037
+ primary: "#3b82f6",
2038
+ secondary: "#6b7280",
2039
+ accent: "#10b981",
2040
+ muted: "#f3f4f6",
2041
+ destructive: "#ef4444",
2042
+ mutedForeground: "#6b7280"
2043
+ };
2044
+ const DEFAULT_THEME_ID = "default";
2045
+ const DEFAULT_THEME_NAME = "Default Theme";
2306
2046
  /**
2307
- * Creates a LanguagesApi adapter backed by the portal-tenant store BFF.
2308
- *
2309
- * Delegates to the adapter factory in `@fluid-app/store-api-client` which
2310
- * maps the generated portal-tenant-store namespace functions to the abstract
2311
- * LanguagesApi port.
2047
+ * Build a fresh ThemeDefinition populated with all defaults.
2048
+ * Returns a new object each call because Color instances are mutable — do not cache the result.
2312
2049
  */
2313
- function createLanguagesApiAdapter(client) {
2314
- return createPortalTenantLanguagesAdapter(client);
2050
+ function getDefaultThemeDefinition() {
2051
+ const bg = new colorjs_io.default(DEFAULT_COLORS.background);
2052
+ const fg = new colorjs_io.default(DEFAULT_COLORS.foreground);
2053
+ const primary = new colorjs_io.default(DEFAULT_COLORS.primary);
2054
+ const secondary = new colorjs_io.default(DEFAULT_COLORS.secondary);
2055
+ const accent = new colorjs_io.default(DEFAULT_COLORS.accent);
2056
+ const muted = new colorjs_io.default(DEFAULT_COLORS.muted);
2057
+ const destructive = new colorjs_io.default(DEFAULT_COLORS.destructive);
2058
+ const mutedFg = new colorjs_io.default(DEFAULT_COLORS.mutedForeground);
2059
+ const darkBg = new colorjs_io.default("#0a0a0a");
2060
+ const darkFg = new colorjs_io.default("#fafafa");
2061
+ const darkMuted = new colorjs_io.default("#171717");
2062
+ const darkMutedForeground = new colorjs_io.default("#dddddd");
2063
+ return {
2064
+ id: DEFAULT_THEME_ID,
2065
+ name: DEFAULT_THEME_NAME,
2066
+ light: {
2067
+ background: {
2068
+ base: bg,
2069
+ foreground: fg
2070
+ },
2071
+ foreground: {
2072
+ base: fg,
2073
+ foreground: bg
2074
+ },
2075
+ primary: {
2076
+ base: primary,
2077
+ foreground: getForegroundColor(fg, primary)
2078
+ },
2079
+ secondary: {
2080
+ base: secondary,
2081
+ foreground: getForegroundColor(fg, secondary)
2082
+ },
2083
+ accent: {
2084
+ base: accent,
2085
+ foreground: getForegroundColor(fg, accent)
2086
+ },
2087
+ muted: {
2088
+ base: muted,
2089
+ foreground: mutedFg
2090
+ },
2091
+ destructive: {
2092
+ base: destructive,
2093
+ foreground: getForegroundColor(fg, destructive)
2094
+ }
2095
+ },
2096
+ dark: {
2097
+ background: {
2098
+ base: darkBg,
2099
+ foreground: darkFg
2100
+ },
2101
+ foreground: {
2102
+ base: darkFg,
2103
+ foreground: darkBg
2104
+ },
2105
+ muted: {
2106
+ base: darkMuted,
2107
+ foreground: darkMutedForeground
2108
+ }
2109
+ },
2110
+ fontSizes: { ...DEFAULT_FONT_SIZES },
2111
+ fontFamilies: { ...DEFAULT_FONT_FAMILIES },
2112
+ spacing: DEFAULT_SPACING,
2113
+ radii: { ...DEFAULT_RADII }
2114
+ };
2315
2115
  }
2316
2116
  //#endregion
2317
- //#region ../../api-clients/portal-tenant-mysite/src/namespaces/portal_tenant_mysite.ts
2318
- /**
2319
- * Get MySite profile for the current user
2320
- * Returns the member's MySite profile.
2321
- *
2322
- * @param client - Fetch client instance
2323
-
2324
- */
2325
- async function mysite_profile_show(client) {
2326
- return client.get(`/api/mysite/profile`);
2327
- }
2328
- /**
2329
- * Update MySite profile
2330
- * Updates the member's MySite profile fields.
2331
- *
2332
- * @param client - Fetch client instance
2333
- * @param body - body
2334
- */
2335
- async function mysite_profile_update(client, body) {
2336
- return client.put(`/api/mysite/profile`, body);
2337
- }
2338
- /**
2339
- * List available MySite themes
2340
- * Returns available MySite themes.
2341
- *
2342
- * @param client - Fetch client instance
2343
- * @param [params] - params
2344
- */
2345
- async function mysite_themes_list(client, params) {
2346
- return client.get(`/api/mysite/themes`, params);
2347
- }
2348
- /**
2349
- * Update MySite settings
2350
- * Updates the member's MySite settings.
2351
- *
2352
- * @param client - Fetch client instance
2353
- * @param body - body
2354
- */
2355
- async function mysite_settings_update(client, body) {
2356
- return client.put(`/api/mysite/settings`, body);
2357
- }
2358
- /**
2359
- * List MySite links for the current user
2360
- * Returns the member's MySite links, ordered by position.
2361
- *
2362
- * @param client - Fetch client instance
2363
- * @param [params] - params
2364
- */
2365
- async function mysite_links_list(client, params) {
2366
- return client.get(`/api/mysite/links`, params);
2117
+ //#region ../core/src/theme/serialisation.ts
2118
+ function colorToPlain(color) {
2119
+ return {
2120
+ l: color.oklch.l ?? 0,
2121
+ c: color.oklch.c ?? 0,
2122
+ h: color.oklch.h ?? 0
2123
+ };
2367
2124
  }
2368
- /**
2369
- * Create a MySite link
2370
- * Adds a new link to the member's MySite.
2371
- *
2372
- * @param client - Fetch client instance
2373
- * @param body - body
2374
- */
2375
- async function mysite_links_create(client, body) {
2376
- return client.post(`/api/mysite/links`, body);
2125
+ function plainToColor(plain) {
2126
+ return new colorjs_io.default("oklch", [
2127
+ plain.l,
2128
+ plain.c,
2129
+ plain.h
2130
+ ]);
2377
2131
  }
2378
2132
  /**
2379
- * Update a MySite link
2380
- * Updates an existing MySite link.
2381
- *
2382
- * @param client - Fetch client instance
2383
- * @param id - id
2384
- * @param body - body
2133
+ * Serialise a ThemeDefinition (with Color objects) to a plain JSON payload
2134
+ * suitable for backend storage.
2385
2135
  */
2386
- async function mysite_links_update(client, id, body) {
2387
- return client.put(`/api/mysite/links/${id}`, body);
2136
+ function serialiseTheme(def) {
2137
+ const light = {};
2138
+ for (const name of SEMANTIC_COLOR_NAMES) light[name] = {
2139
+ base: colorToPlain(def.light[name].base),
2140
+ foreground: colorToPlain(def.light[name].foreground)
2141
+ };
2142
+ const dark = {};
2143
+ for (const [name, value] of Object.entries(def.dark)) {
2144
+ if (!value) continue;
2145
+ dark[name] = {
2146
+ ...value.base ? { base: colorToPlain(value.base) } : {},
2147
+ ...value.foreground ? { foreground: colorToPlain(value.foreground) } : {}
2148
+ };
2149
+ }
2150
+ return {
2151
+ id: def.id,
2152
+ name: def.name,
2153
+ light,
2154
+ dark,
2155
+ fontSizes: { ...def.fontSizes },
2156
+ fontFamilies: { ...def.fontFamilies },
2157
+ spacing: def.spacing,
2158
+ radii: { ...def.radii },
2159
+ ...def.syncWithBrandColors ? { syncWithBrandColors: true } : {}
2160
+ };
2388
2161
  }
2389
2162
  /**
2390
- * Delete a MySite link
2391
- * Removes a link from the member's MySite.
2392
- *
2393
- * @param client - Fetch client instance
2394
- * @param id - id
2163
+ * Deserialise a backend payload into a ThemeDefinition with Color objects.
2164
+ * Accepts `Record<string, unknown>` because API data is untyped at the boundary.
2165
+ * Falls back to default colors for any missing light-mode entries.
2395
2166
  */
2396
- async function mysite_links_destroy(client, id) {
2397
- return client.delete(`/api/mysite/links/${id}`);
2167
+ function deserialiseTheme(payload) {
2168
+ const lightRaw = payload.light ?? {};
2169
+ const darkRaw = payload.dark ?? {};
2170
+ const defaults = getDefaultThemeDefinition();
2171
+ const light = {};
2172
+ for (const name of SEMANTIC_COLOR_NAMES) {
2173
+ const entry = lightRaw[name];
2174
+ if (entry) light[name] = {
2175
+ base: plainToColor(entry.base),
2176
+ foreground: plainToColor(entry.foreground)
2177
+ };
2178
+ else {
2179
+ console.warn(`[theme] deserialiseTheme: missing light color "${name}", using default`);
2180
+ light[name] = defaults.light[name];
2181
+ }
2182
+ }
2183
+ const dark = {};
2184
+ for (const [name, value] of Object.entries(darkRaw)) {
2185
+ if (!value) continue;
2186
+ dark[name] = {
2187
+ ...value.base ? { base: plainToColor(value.base) } : {},
2188
+ ...value.foreground ? { foreground: plainToColor(value.foreground) } : {}
2189
+ };
2190
+ }
2191
+ return {
2192
+ id: payload.id,
2193
+ name: payload.name,
2194
+ light,
2195
+ dark,
2196
+ fontSizes: payload.fontSizes ?? DEFAULT_FONT_SIZES,
2197
+ fontFamilies: payload.fontFamilies ?? DEFAULT_FONT_FAMILIES,
2198
+ spacing: payload.spacing ?? "0.25rem",
2199
+ radii: payload.radii ?? DEFAULT_RADII,
2200
+ ...payload.syncWithBrandColors === true ? { syncWithBrandColors: true } : {}
2201
+ };
2398
2202
  }
2203
+ //#endregion
2204
+ //#region ../core/src/theme/transforms.ts
2399
2205
  /**
2400
- * Reorder MySite links
2401
- * Reorders MySite links by providing an ordered list of IDs.
2402
- *
2403
- * @param client - Fetch client instance
2404
- * @param body - body
2206
+ * Check if a theme config uses the new structured format (has a `light` key
2207
+ * that is an object) vs the legacy flat format.
2405
2208
  */
2406
- async function mysite_links_bulk_reorder(client, body) {
2407
- return client.patch(`/api/mysite/links/bulk`, body);
2209
+ function isNewThemeFormat(config) {
2210
+ return config.light != null && typeof config.light === "object";
2408
2211
  }
2409
2212
  /**
2410
- * List MySite favorite products
2411
- * Returns the member's MySite favorite products.
2412
- *
2413
- * @param client - Fetch client instance
2414
- * @param [params] - params
2213
+ * Convert a legacy flat config to a ThemeDefinition.
2214
+ * Legacy format: { base: "#fff", text: "#000", primary: "oklch(0.6 0.2 250)", ... }
2415
2215
  */
2416
- async function mysite_favorites_list(client, params) {
2417
- return client.get(`/api/mysite/favorites`, params);
2216
+ function legacyConfigToDefinition(id, name, config) {
2217
+ const bg = parseColor(config.base ?? config.background ?? DEFAULT_COLORS.background);
2218
+ const fg = parseColor(config.text ?? config.foreground ?? DEFAULT_COLORS.foreground);
2219
+ const primary = parseColor(config.primary ?? DEFAULT_COLORS.primary);
2220
+ const secondary = parseColor(config.secondary ?? DEFAULT_COLORS.secondary);
2221
+ const accent = parseColor(config.accent ?? DEFAULT_COLORS.accent);
2222
+ const muted = parseColor(config.muted ?? DEFAULT_COLORS.muted);
2223
+ const destructive = parseColor(config.destructive ?? DEFAULT_COLORS.destructive);
2224
+ const mutedFg = parseColor(config.mutedForeground ?? DEFAULT_COLORS.mutedForeground);
2225
+ return {
2226
+ id: String(id),
2227
+ name,
2228
+ light: {
2229
+ background: {
2230
+ base: bg,
2231
+ foreground: fg
2232
+ },
2233
+ foreground: {
2234
+ base: fg,
2235
+ foreground: bg
2236
+ },
2237
+ primary: {
2238
+ base: primary,
2239
+ foreground: getForegroundColor(fg, primary)
2240
+ },
2241
+ secondary: {
2242
+ base: secondary,
2243
+ foreground: getForegroundColor(fg, secondary)
2244
+ },
2245
+ accent: {
2246
+ base: accent,
2247
+ foreground: getForegroundColor(fg, accent)
2248
+ },
2249
+ muted: {
2250
+ base: muted,
2251
+ foreground: mutedFg
2252
+ },
2253
+ destructive: {
2254
+ base: destructive,
2255
+ foreground: getForegroundColor(fg, destructive)
2256
+ }
2257
+ },
2258
+ dark: {},
2259
+ fontSizes: {
2260
+ extraSmall: config.extraSmall ?? DEFAULT_FONT_SIZES.extraSmall,
2261
+ small: config.small ?? DEFAULT_FONT_SIZES.small,
2262
+ regular: config.regular ?? DEFAULT_FONT_SIZES.regular,
2263
+ large: config.large ?? DEFAULT_FONT_SIZES.large,
2264
+ extraLarge: config.extraLarge ?? DEFAULT_FONT_SIZES.extraLarge,
2265
+ giant: config.giant ?? DEFAULT_FONT_SIZES.giant
2266
+ },
2267
+ fontFamilies: {
2268
+ header: config.headerFont ?? DEFAULT_FONT_FAMILIES.header,
2269
+ body: config.bodyFont ?? DEFAULT_FONT_FAMILIES.body
2270
+ },
2271
+ spacing: config.globalSpacing ?? "0.25rem",
2272
+ radii: {
2273
+ small: config.radiusSmall ?? DEFAULT_RADII.small,
2274
+ medium: config.radiusMedium ?? DEFAULT_RADII.medium,
2275
+ large: config.radiusLarge ?? DEFAULT_RADII.large,
2276
+ extraLarge: config.radiusExtraLarge ?? DEFAULT_RADII.extraLarge
2277
+ }
2278
+ };
2418
2279
  }
2419
2280
  /**
2420
- * Add a product to MySite favorites
2421
- * Adds a product to the member's MySite favorites.
2422
- *
2423
- * @param client - Fetch client instance
2424
- * @param body - body
2281
+ * Build a ThemeDefinition from a single API theme object.
2282
+ * Handles both new structured format and legacy flat format.
2425
2283
  */
2426
- async function mysite_favorites_create(client, body) {
2427
- return client.post(`/api/mysite/favorites`, body);
2284
+ function buildThemeDefinition(theme) {
2285
+ const config = theme.config ?? {};
2286
+ if (isNewThemeFormat(config)) return deserialiseTheme({
2287
+ ...config,
2288
+ id: String(theme.id),
2289
+ name: theme.name ?? "Untitled Theme"
2290
+ });
2291
+ return legacyConfigToDefinition(theme.id, theme.name ?? "Untitled Theme", config);
2428
2292
  }
2429
2293
  /**
2430
- * Remove a product from MySite favorites
2431
- * Removes a product from the member's MySite favorites.
2432
- *
2433
- * @param client - Fetch client instance
2434
- * @param id - id
2294
+ * Transform raw API themes to ThemeDefinition[].
2295
+ * Catches and logs errors per theme (graceful degradation).
2435
2296
  */
2436
- async function mysite_favorites_destroy(client, id) {
2437
- return client.delete(`/api/mysite/favorites/${id}`);
2297
+ function transformThemes(themes) {
2298
+ return themes.flatMap((theme) => {
2299
+ try {
2300
+ return [buildThemeDefinition(theme)];
2301
+ } catch (error) {
2302
+ console.error(`[theme] Failed to build theme id=${theme.id}:`, error);
2303
+ return [];
2304
+ }
2305
+ });
2438
2306
  }
2439
2307
  /**
2440
- * Reorder MySite favorites
2441
- * Reorders MySite favorites by providing an ordered list of IDs.
2442
- *
2443
- * @param client - Fetch client instance
2444
- * @param body - body
2308
+ * Get the active theme ID from a list of raw API themes.
2309
+ * Falls back to the first theme if none is marked active.
2445
2310
  */
2446
- async function mysite_favorites_bulk_reorder(client, body) {
2447
- return client.patch(`/api/mysite/favorites/bulk`, body);
2311
+ function getActiveThemeId(themes) {
2312
+ const active = themes.find((t) => t.active) ?? themes[0];
2313
+ return active ? String(active.id) : void 0;
2448
2314
  }
2449
2315
  //#endregion
2450
- //#region src/adapters/mysite-api-adapter.ts
2451
- function mapProfile(raw) {
2452
- return {
2453
- id: raw.id ?? 0,
2454
- mysite_url: raw.mysite_url ?? null,
2455
- theme_id: raw.theme_id ?? null,
2456
- bio: raw.bio ?? null,
2457
- avatar_url: raw.avatar_url ?? null,
2458
- display_name: raw.display_name ?? null,
2459
- slug: raw.slug ?? null
2460
- };
2316
+ //#region ../core/src/theme/theme-applicator.ts
2317
+ const STYLE_PREFIX = "theme-style-";
2318
+ const FONT_LINK_PREFIX = "theme-font-";
2319
+ const SYSTEM_FONTS = new Set([
2320
+ "sans-serif",
2321
+ "serif",
2322
+ "monospace",
2323
+ "cursive",
2324
+ "fantasy",
2325
+ "system-ui",
2326
+ "ui-sans-serif",
2327
+ "ui-serif",
2328
+ "ui-monospace"
2329
+ ]);
2330
+ /** Build a Google Fonts CSS2 URL for a given font family with all weights. */
2331
+ function buildGoogleFontUrl(family) {
2332
+ return `https://fonts.googleapis.com/css2?family=${encodeURIComponent(family).replace(/%20/g, "+")}:wght@100;200;300;400;500;600;700;800;900&display=swap`;
2461
2333
  }
2462
- function mapLink(raw) {
2463
- return {
2464
- id: raw.id ?? 0,
2465
- url: raw.url ?? "",
2466
- title: raw.title ?? "",
2467
- position: raw.position ?? 0
2468
- };
2334
+ /** Check if a font family value needs to be loaded (i.e. is not a CSS var or system font). */
2335
+ function isLoadableFont(value) {
2336
+ if (!value) return false;
2337
+ if (value.startsWith("var(")) return false;
2338
+ return !SYSTEM_FONTS.has(value.toLowerCase());
2469
2339
  }
2470
- function mapFavorite(raw) {
2471
- return {
2472
- id: raw.id ?? 0,
2473
- product_id: raw.product_id ?? 0,
2474
- product_name: raw.product_name ?? null,
2475
- product_image_url: raw.product_image_url ?? null,
2476
- position: raw.position ?? 0,
2477
- created_at: raw.created_at ?? null
2478
- };
2340
+ /** Deterministic link element ID for a font family. */
2341
+ function fontLinkId(family) {
2342
+ return `${FONT_LINK_PREFIX}${family.replace(/\s+/g, "-").toLowerCase()}`;
2479
2343
  }
2480
- function mapTheme(raw) {
2481
- return {
2482
- id: raw.id ?? 0,
2483
- name: raw.name ?? "",
2484
- preview_url: raw.preview_url ?? null
2485
- };
2344
+ /**
2345
+ * Inject or update `<link>` elements for Google Fonts used by the theme.
2346
+ * Removes links for fonts that are no longer referenced.
2347
+ */
2348
+ function loadThemeFonts(theme) {
2349
+ if (typeof document === "undefined") return;
2350
+ const fontsToLoad = /* @__PURE__ */ new Set();
2351
+ for (const key of FONT_FAMILY_KEYS) {
2352
+ const value = theme.fontFamilies[key];
2353
+ if (isLoadableFont(value)) fontsToLoad.add(value);
2354
+ }
2355
+ document.querySelectorAll(`link[id^="${FONT_LINK_PREFIX}"]`).forEach((link) => {
2356
+ const owners = link.getAttribute("data-font-theme-ids")?.split(",") ?? [];
2357
+ if (!owners.includes(theme.id)) return;
2358
+ const fontName = link.getAttribute("data-font-family");
2359
+ if (fontName && !fontsToLoad.has(fontName)) {
2360
+ const remaining = owners.filter((id) => id !== theme.id);
2361
+ if (remaining.length === 0) link.remove();
2362
+ else link.setAttribute("data-font-theme-ids", remaining.join(","));
2363
+ }
2364
+ });
2365
+ for (const family of fontsToLoad) {
2366
+ const id = fontLinkId(family);
2367
+ const existing = document.getElementById(id);
2368
+ if (existing) {
2369
+ const owners = existing.getAttribute("data-font-theme-ids")?.split(",") ?? [];
2370
+ if (!owners.includes(theme.id)) existing.setAttribute("data-font-theme-ids", [...owners, theme.id].join(","));
2371
+ } else {
2372
+ const link = document.createElement("link");
2373
+ link.id = id;
2374
+ link.rel = "stylesheet";
2375
+ link.href = buildGoogleFontUrl(family);
2376
+ link.setAttribute("data-font-family", family);
2377
+ link.setAttribute("data-font-theme-ids", theme.id);
2378
+ document.head.appendChild(link);
2379
+ }
2380
+ }
2486
2381
  }
2487
- function createMySiteApiAdapter(client) {
2488
- return {
2489
- fetchProfile: async () => {
2490
- return mapProfile((await mysite_profile_show(client)).profile ?? {});
2491
- },
2492
- updateProfile: async (body) => {
2493
- return mapProfile((await mysite_profile_update(client, { profile: {
2494
- display_name: body.display_name,
2495
- bio: body.bio,
2496
- avatar_url: body.avatar_url
2497
- } })).profile ?? {});
2498
- },
2499
- updateSettings: async (body) => {
2500
- await Promise.all([body.theme_id !== void 0 ? mysite_settings_update(client, { settings: { theme_id: body.theme_id } }) : Promise.resolve(), body.slug !== void 0 ? mysite_profile_update(client, { profile: { slug: body.slug } }) : Promise.resolve()]);
2501
- },
2502
- listLinks: async () => {
2503
- return ((await mysite_links_list(client)).links ?? []).map(mapLink);
2504
- },
2505
- createLink: async (body) => {
2506
- return mapLink((await mysite_links_create(client, { link: {
2507
- title: body.title,
2508
- url: body.url
2509
- } })).link ?? {});
2510
- },
2511
- updateLink: async (linkId, body) => {
2512
- return mapLink((await mysite_links_update(client, linkId, { link: {
2513
- title: body.title,
2514
- url: body.url
2515
- } })).link ?? {});
2516
- },
2517
- deleteLink: async (linkId) => {
2518
- await mysite_links_destroy(client, linkId);
2519
- },
2520
- reorderLinks: async (orderedIds) => {
2521
- return ((await mysite_links_bulk_reorder(client, { ordered_ids: orderedIds })).links ?? []).map(mapLink);
2522
- },
2523
- listFavorites: async () => {
2524
- return ((await mysite_favorites_list(client)).favorites ?? []).map(mapFavorite);
2525
- },
2526
- addFavorite: async (body) => {
2527
- return mapFavorite((await mysite_favorites_create(client, { favorite: { product_id: body.product_id } })).favorite ?? {});
2528
- },
2529
- deleteFavorite: async (favoriteId) => {
2530
- await mysite_favorites_destroy(client, favoriteId);
2531
- },
2532
- reorderFavorites: async (orderedIds) => {
2533
- return ((await mysite_favorites_bulk_reorder(client, { ordered_ids: orderedIds })).favorites ?? []).map(mapFavorite);
2534
- },
2535
- listThemes: async () => {
2536
- return ((await mysite_themes_list(client)).themes ?? []).map(mapTheme);
2382
+ /** Remove all font `<link>` elements injected by the theme system. */
2383
+ function removeAllFontLinks() {
2384
+ if (typeof document === "undefined") return;
2385
+ document.querySelectorAll(`link[id^="${FONT_LINK_PREFIX}"]`).forEach((el) => el.remove());
2386
+ }
2387
+ /**
2388
+ * Inject or update a `<style>` element in `<head>` for the given theme.
2389
+ * The element ID is deterministic (`theme-style-{themeId}`) so repeated calls
2390
+ * for the same theme are idempotent — the existing element is updated in place.
2391
+ * Also loads Google Fonts referenced by the theme's font families.
2392
+ * No-op when `document` is unavailable (SSR).
2393
+ */
2394
+ function applyTheme(theme, options) {
2395
+ if (typeof document === "undefined") return;
2396
+ try {
2397
+ loadThemeFonts(theme);
2398
+ const styleId = `${STYLE_PREFIX}${theme.id}`;
2399
+ let el = document.getElementById(styleId);
2400
+ if (!el) {
2401
+ el = document.createElement("style");
2402
+ el.id = styleId;
2403
+ document.head.appendChild(el);
2537
2404
  }
2538
- };
2405
+ el.textContent = generateThemeCSS(theme, options);
2406
+ } catch (error) {
2407
+ console.error(`[theme] applyTheme failed for "${theme.id}":`, error);
2408
+ }
2409
+ }
2410
+ /** Remove an injected theme stylesheet and clean up font link ownership. No-op during SSR. */
2411
+ function removeTheme(themeId) {
2412
+ if (typeof document === "undefined") return;
2413
+ document.getElementById(`${STYLE_PREFIX}${themeId}`)?.remove();
2414
+ document.querySelectorAll(`link[id^="${FONT_LINK_PREFIX}"]`).forEach((link) => {
2415
+ const remaining = (link.getAttribute("data-font-theme-ids")?.split(",") ?? []).filter((id) => id !== themeId);
2416
+ if (remaining.length === 0) link.remove();
2417
+ else link.setAttribute("data-font-theme-ids", remaining.join(","));
2418
+ });
2419
+ }
2420
+ /** Remove all injected theme stylesheets and font links. No-op during SSR. */
2421
+ function removeAllThemes() {
2422
+ if (typeof document === "undefined") return;
2423
+ document.querySelectorAll(`style[id^="${STYLE_PREFIX}"]`).forEach((el) => el.remove());
2424
+ removeAllFontLinks();
2539
2425
  }
2540
2426
  //#endregion
2541
2427
  //#region src/providers/FluidThemeProvider.tsx
@@ -2942,18 +2828,6 @@ Object.defineProperty(exports, "buildThemeDefinition", {
2942
2828
  return buildThemeDefinition;
2943
2829
  }
2944
2830
  });
2945
- Object.defineProperty(exports, "createFetchClient", {
2946
- enumerable: true,
2947
- get: function() {
2948
- return createFetchClient;
2949
- }
2950
- });
2951
- Object.defineProperty(exports, "createFluidClient", {
2952
- enumerable: true,
2953
- get: function() {
2954
- return createFluidClient;
2955
- }
2956
- });
2957
2831
  Object.defineProperty(exports, "createPersister", {
2958
2832
  enumerable: true,
2959
2833
  get: function() {
@@ -3038,12 +2912,6 @@ Object.defineProperty(exports, "mergeDarkOverrides", {
3038
2912
  return mergeDarkOverrides;
3039
2913
  }
3040
2914
  });
3041
- Object.defineProperty(exports, "normalizeComponentTree", {
3042
- enumerable: true,
3043
- get: function() {
3044
- return normalizeComponentTree;
3045
- }
3046
- });
3047
2915
  Object.defineProperty(exports, "parseColor", {
3048
2916
  enumerable: true,
3049
2917
  get: function() {
@@ -3074,30 +2942,6 @@ Object.defineProperty(exports, "serialiseTheme", {
3074
2942
  return serialiseTheme;
3075
2943
  }
3076
2944
  });
3077
- Object.defineProperty(exports, "toNavigationItem", {
3078
- enumerable: true,
3079
- get: function() {
3080
- return toNavigationItem;
3081
- }
3082
- });
3083
- Object.defineProperty(exports, "toRawManifest", {
3084
- enumerable: true,
3085
- get: function() {
3086
- return toRawManifest;
3087
- }
3088
- });
3089
- Object.defineProperty(exports, "toScreenDefinition", {
3090
- enumerable: true,
3091
- get: function() {
3092
- return toScreenDefinition;
3093
- }
3094
- });
3095
- Object.defineProperty(exports, "transformManifestToRepAppData", {
3096
- enumerable: true,
3097
- get: function() {
3098
- return transformManifestToRepAppData;
3099
- }
3100
- });
3101
2945
  Object.defineProperty(exports, "transformThemes", {
3102
2946
  enumerable: true,
3103
2947
  get: function() {
@@ -3147,4 +2991,4 @@ Object.defineProperty(exports, "widgetPropertySchemas", {
3147
2991
  }
3148
2992
  });
3149
2993
 
3150
- //# sourceMappingURL=FluidProvider-Bc-3uN7M.cjs.map
2994
+ //# sourceMappingURL=FluidProvider-BtGi2jJt.cjs.map