@fluid-app/portal-sdk 0.1.158 → 0.1.160

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